fix: restore ComfyUI as workflow frontend with BoosAPI backend integration
ComfyUI is kept as a visual workflow editor (node-based). All actual
generation goes through BoosAPI's own API via boosapi_comfy_nodes.py
custom nodes (now defaulting to localhost:4000/v1).
Architecture:
BoosAPI (Node.js, port 4000)
├── /video-agent → iframe → ComfyUI (Python, port 8188, CPU)
│ └── custom_nodes/boosapi_comfy_nodes.py
│ └── calls BoosAPI /v1/... API
├── /comfyui-agent → chat interface (direct API calls)
└── /v1/... → API endpoints (LLM, image, TTS, video)
Signed-off-by: boos <boos@boos.lat>
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
BoosAPI Custom Nodes for ComfyUI
|
||||
==================================
|
||||
Call BoosAPI/metapi endpoints directly from ComfyUI workflows:
|
||||
Call local BoosAPI endpoints directly from ComfyUI workflows:
|
||||
- Text generation (gpt-5.5)
|
||||
- Image generation (gpt-image-2)
|
||||
- Text-to-Speech
|
||||
- Video generation (async)
|
||||
|
||||
Configuration (in ComfyUI settings or env vars):
|
||||
BOOSAPI_BASE = https://api.boos.lat/v1
|
||||
BOOSAPI_KEY = sk-boos4721
|
||||
Default: http://127.0.0.1:4000/v1 (same container)
|
||||
Override via env:
|
||||
BOOSAPI_BASE = https://api.boos.lat/v1 (standalone ComfyUI)
|
||||
BOOSAPI_KEY=***
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
+27
-3
@@ -23,15 +23,17 @@ ARG NO_PROXY
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV NODE_ENV=production
|
||||
ENV COMFYUI_BASE=http://127.0.0.1:8188
|
||||
ENV HTTP_PROXY=${HTTP_PROXY}
|
||||
ENV HTTPS_PROXY=${HTTPS_PROXY}
|
||||
ENV ALL_PROXY=${ALL_PROXY}
|
||||
ENV NO_PROXY=${NO_PROXY}
|
||||
|
||||
# Install system deps: Node.js 22, supervisor
|
||||
# Install system deps: Node.js 22, Python 3, supervisor
|
||||
RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates gnupg \
|
||||
supervisor \
|
||||
python3 python3-pip python3-venv \
|
||||
supervisor git \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -40,6 +42,18 @@ RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt
|
||||
COPY rtk /usr/local/bin/rtk
|
||||
RUN chmod +x /usr/local/bin/rtk
|
||||
|
||||
# Set up ComfyUI (workflow editor frontend, calls BoosAPI API via custom nodes)
|
||||
# CPU PyTorch is fine — all generation goes through BoosAPI's API
|
||||
WORKDIR /app/comfy
|
||||
RUN git clone https://github.com/comfyanonymous/ComfyUI.git . \
|
||||
&& python3 -m venv venv \
|
||||
&& . venv/bin/activate \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu \
|
||||
&& grep -v kornia_rs requirements.txt | pip install -r /dev/stdin \
|
||||
&& rm -rf ~/.cache/pip
|
||||
ENV PATH="/app/comfy/venv/bin:${PATH}"
|
||||
|
||||
# Copy metapi (built)
|
||||
WORKDIR /app/metapi
|
||||
COPY --from=metapi-builder /app/dist ./dist
|
||||
@@ -48,6 +62,9 @@ COPY --from=metapi-builder /app/package.json ./
|
||||
COPY --from=metapi-builder /app/drizzle ./drizzle
|
||||
RUN mkdir -p data
|
||||
|
||||
# Copy BoosAPI custom nodes for ComfyUI (call local BoosAPI API)
|
||||
COPY --from=metapi-builder /app/boosapi_comfy_nodes.py /app/comfy/custom_nodes/boosapi_comfy_nodes.py
|
||||
|
||||
# Supervisor config
|
||||
RUN mkdir -p /var/log/supervisor
|
||||
COPY <<'SUPERVISOR' /etc/supervisor/conf.d/all.conf
|
||||
@@ -64,8 +81,15 @@ autorestart=true
|
||||
stdout_logfile=/var/log/supervisor/metapi.log
|
||||
stderr_logfile=/var/log/supervisor/metapi.err
|
||||
|
||||
[program:comfyui]
|
||||
command=python3 main.py --listen 0.0.0.0 --port 8188 --cpu
|
||||
directory=/app/comfy
|
||||
autorestart=true
|
||||
startretries=3
|
||||
stdout_logfile=/var/log/supervisor/comfyui.log
|
||||
stderr_logfile=/var/log/supervisor/comfyui.err
|
||||
SUPERVISOR
|
||||
|
||||
EXPOSE 4000
|
||||
EXPOSE 4000 8188
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"]
|
||||
|
||||
@@ -5,6 +5,7 @@ const DESKTOP_HEALTH_ROUTE = '/api/desktop/health';
|
||||
export function isPublicApiRoute(url: string): boolean {
|
||||
return url === DESKTOP_HEALTH_ROUTE
|
||||
|| url.startsWith('/api/oauth/callback/')
|
||||
|| url.startsWith('/api/comfyui/proxy/')
|
||||
// User SaaS routes (JWT auth, not admin token)
|
||||
|| url === '/api/users/register'
|
||||
|| url === '/api/users/login'
|
||||
|
||||
@@ -27,6 +27,8 @@ import { invitationRoutes } from './routes/api/invitations.js';
|
||||
import { oauthRoutes } from './routes/api/oauth.js';
|
||||
import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js';
|
||||
import { updateCenterRoutes } from './routes/api/updateCenter.js';
|
||||
import { comfyuiRoutes } from './routes/api/comfyui.js';
|
||||
import { videoAgentRoutes } from './routes/api/videoAgent.js';
|
||||
import { adminUsersRoutes } from './routes/api/adminUsers.js';
|
||||
import { comfyuiAgentRoutes } from './routes/api/comfyuiAgent.js';
|
||||
import { proxyRoutes } from './routes/proxy/router.js';
|
||||
@@ -239,6 +241,8 @@ await app.register(usersRoutes);
|
||||
await app.register(userApiKeysRoutes);
|
||||
await app.register(invitationRoutes);
|
||||
await app.register(organizationsRoutes);
|
||||
await app.register(comfyuiRoutes);
|
||||
await app.register(videoAgentRoutes);
|
||||
await app.register(comfyuiAgentRoutes);
|
||||
await app.register(oauthRoutes);
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
|
||||
const COMFYUI_BASE = 'http://127.0.0.1:8188';
|
||||
|
||||
/** Rebrand ComfyUI HTML to show BoosAPI instead */
|
||||
function rebrandHtml(html: string): string {
|
||||
return html
|
||||
.replace(/ComfyUI/g, 'BoosAPI')
|
||||
.replace(/comfyui/gi, 'BoosAPI');
|
||||
}
|
||||
|
||||
function rewriteProxyUrls(html: string): string {
|
||||
return html.replace(/(src|href|action)=["']\//g, (m) =>
|
||||
m.replace(/["']/, '/api/comfyui/proxy/')
|
||||
);
|
||||
}
|
||||
|
||||
export async function comfyuiRoutes(app: FastifyInstance) {
|
||||
// Proxy: POST /api/comfyui/prompt -> ComfyUI /prompt
|
||||
app.post('/api/comfyui/prompt', async (request, reply) => {
|
||||
try {
|
||||
const body = typeof request.body === 'object' ? JSON.stringify(request.body) : '{}';
|
||||
const resp = await fetch(`${COMFYUI_BASE}/prompt`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
});
|
||||
const text = await resp.text();
|
||||
return reply.code(resp.status).type('application/json').send(text);
|
||||
} catch (e: any) {
|
||||
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy: GET /api/comfyui/history -> ComfyUI /history
|
||||
app.get('/api/comfyui/history', async (_request, reply) => {
|
||||
try {
|
||||
const resp = await fetch(`${COMFYUI_BASE}/history`);
|
||||
const text = await resp.text();
|
||||
return reply.code(resp.status).type('application/json').send(text);
|
||||
} catch (e: any) {
|
||||
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy: GET /api/comfyui/history/:id -> ComfyUI /history/{id}
|
||||
app.get<{ Params: { id: string } }>('/api/comfyui/history/:id', async (request, reply) => {
|
||||
try {
|
||||
const resp = await fetch(`${COMFYUI_BASE}/history/${encodeURIComponent(request.params.id)}`);
|
||||
const text = await resp.text();
|
||||
return reply.code(resp.status).type('application/json').send(text);
|
||||
} catch (e: any) {
|
||||
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy: GET /api/comfyui/queue -> ComfyUI /queue
|
||||
app.get('/api/comfyui/queue', async (_request, reply) => {
|
||||
try {
|
||||
const resp = await fetch(`${COMFYUI_BASE}/queue`);
|
||||
const text = await resp.text();
|
||||
return reply.code(resp.status).type('application/json').send(text);
|
||||
} catch (e: any) {
|
||||
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy: GET /api/comfyui/view -> ComfyUI /view (image output, preserve query params)
|
||||
app.get('/api/comfyui/view', async (request, reply) => {
|
||||
try {
|
||||
const qs = request.url.includes('?') ? request.url.substring(request.url.indexOf('?')) : '';
|
||||
const resp = await fetch(`${COMFYUI_BASE}/view${qs}`);
|
||||
const buffer = await resp.arrayBuffer();
|
||||
return reply.code(resp.status).type(resp.headers.get('content-type') || 'application/octet-stream').send(Buffer.from(buffer));
|
||||
} catch (e: any) {
|
||||
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy: GET /api/comfyui/proxy/* -> proxy everything to ComfyUI (HTML, assets, JS, CSS, etc.)
|
||||
app.get('/api/comfyui/proxy/*', async (request, reply) => {
|
||||
try {
|
||||
const path = request.url.replace('/api/comfyui/proxy', '');
|
||||
const targetUrl = `${COMFYUI_BASE}${path}`;
|
||||
const resp = await fetch(targetUrl);
|
||||
const contentType = resp.headers.get('content-type') || 'application/octet-stream';
|
||||
if (contentType.includes('html')) {
|
||||
const text = await resp.text();
|
||||
const rebranded = rebrandHtml(text);
|
||||
const rewritten = rewriteProxyUrls(rebranded);
|
||||
return reply.code(resp.status).type(contentType).send(rewritten);
|
||||
}
|
||||
if (contentType.includes('text') || contentType.includes('json')) {
|
||||
const text = await resp.text();
|
||||
return reply.code(resp.status).type(contentType).send(text);
|
||||
}
|
||||
const buffer = await resp.arrayBuffer();
|
||||
return reply.code(resp.status).type(contentType).send(Buffer.from(buffer));
|
||||
} catch (e: any) {
|
||||
return reply.code(502).send({ error: `ComfyUI unreachable: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve ComfyUI HTML page at /comfyui with iframe to proxied ComfyUI
|
||||
app.get('/comfyui', async (_request, reply) => {
|
||||
return reply.type('text/html').send(`<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BoosAPI - ComfyUI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; }
|
||||
iframe { width: 100%; height: 100%; border: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="/api/comfyui/proxy/" title="BoosAPI ComfyUI"></iframe>
|
||||
</body>
|
||||
</html>`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
|
||||
const METAPI_BASE = `http://127.0.0.1:${process.env.PORT || 3000}`;
|
||||
|
||||
interface AgentRequest {
|
||||
script: string;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Video Generation Agent
|
||||
*
|
||||
* Workflow:
|
||||
* 1. User provides script
|
||||
* 2. Agent parses script → characters, scenes, dialogue
|
||||
* 3. For characters: generate front/back/left/right concept art
|
||||
* 4. For scenes: generate background/environment prompts
|
||||
* 5. For dialogue: generate TTS audio
|
||||
* 6. Package everything for ComfyUI workflow
|
||||
*/
|
||||
|
||||
// Default system prompt for the script analysis agent
|
||||
const SCRIPT_ANALYSIS_PROMPT = `你是一个 AI 视频生成助手。分析用户提供的脚本,提取以下结构化信息,以 JSON 格式返回:
|
||||
|
||||
{
|
||||
"title": "视频标题",
|
||||
"characters": [
|
||||
{
|
||||
"name": "角色名",
|
||||
"gender": "男/女",
|
||||
"age": "年龄描述",
|
||||
"appearance": "外貌详细描述(发型、脸型、服装、体型等)",
|
||||
"personality": "性格特征"
|
||||
}
|
||||
],
|
||||
"scenes": [
|
||||
{
|
||||
"id": 1,
|
||||
"description": "场景描述",
|
||||
"environment": "环境详细描述(时间、地点、天气、光线等)",
|
||||
"characters": ["出现的角色名"],
|
||||
"action": "场景中发生的动作",
|
||||
"dialogue": [
|
||||
{"character": "角色名", "line": "台词", "emotion": "情感语气"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"style": "整体视觉风格(动画/写实/像素等)"
|
||||
}
|
||||
|
||||
只返回 JSON,不要其他文字。`;
|
||||
|
||||
const CHARACTER_PROMPT_GEN = `根据角色描述生成 4 组稳定的图像生成提示词(正面、背面、左侧面、右侧面),用于 AI 图像生成模型。
|
||||
每组提示词应包含:人物姿态、服装、光照、背景。
|
||||
返回 JSON 格式:
|
||||
{
|
||||
"front": "正面提示词",
|
||||
"back": "背面提示词",
|
||||
"left": "左侧面提示词",
|
||||
"right": "右侧面提示词"
|
||||
}
|
||||
只返回 JSON。`;
|
||||
|
||||
const SCENE_PROMPT_GEN = `根据场景描述生成图像生成提示词,用于 AI 图像生成模型创建场景背景。
|
||||
包含:环境、光照、色调、氛围。
|
||||
返回 JSON:
|
||||
{
|
||||
"prompt": "场景生成提示词",
|
||||
"negative_prompt": "负面提示词"
|
||||
}
|
||||
只返回 JSON。`;
|
||||
|
||||
async function callLLM(prompt: string, system: string): Promise<string> {
|
||||
const resp = await fetch(`${METAPI_BASE}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer admin`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-5.5',
|
||||
messages: [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
const data: any = await resp.json();
|
||||
return data?.choices?.[0]?.message?.content || '';
|
||||
}
|
||||
|
||||
async function callImageGen(prompt: string): Promise<string> {
|
||||
const resp = await fetch(`${METAPI_BASE}/v1/images/generations`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer admin`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'dall-e-3',
|
||||
prompt,
|
||||
n: 1,
|
||||
size: '1024x1024',
|
||||
}),
|
||||
});
|
||||
const data: any = await resp.json();
|
||||
return data?.data?.[0]?.url || '';
|
||||
}
|
||||
|
||||
async function callTTS(text: string, voice = 'alloy'): Promise<Buffer | null> {
|
||||
const resp = await fetch(`${METAPI_BASE}/v1/audio/speech`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer admin`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
input: text,
|
||||
voice,
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const buffer = await resp.arrayBuffer();
|
||||
return Buffer.from(buffer);
|
||||
}
|
||||
|
||||
export async function videoAgentRoutes(app: FastifyInstance) {
|
||||
// POST /api/video-agent/analyze — analyze script and extract structure
|
||||
app.post('/api/video-agent/analyze', async (request, reply) => {
|
||||
const { script } = request.body as AgentRequest;
|
||||
if (!script) {
|
||||
return reply.code(400).send({ error: 'script is required' });
|
||||
}
|
||||
try {
|
||||
const result = await callLLM(script, SCRIPT_ANALYSIS_PROMPT);
|
||||
const parsed = JSON.parse(result.replace(/```json|```/g, '').trim());
|
||||
return { success: true, data: parsed };
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `Analysis failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/video-agent/character-prompts — generate character image prompts
|
||||
app.post('/api/video-agent/character-prompts', async (request, reply) => {
|
||||
const { character } = request.body as any;
|
||||
if (!character) {
|
||||
return reply.code(400).send({ error: 'character is required' });
|
||||
}
|
||||
try {
|
||||
const desc = `${character.name}: ${character.appearance}, ${character.gender}, ${character.age}`;
|
||||
const result = await callLLM(desc, CHARACTER_PROMPT_GEN);
|
||||
const prompts = JSON.parse(result.replace(/```json|```/g, '').trim());
|
||||
return { success: true, data: prompts };
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `Prompt generation failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/video-agent/generate-character — generate character images (front/back/left/right)
|
||||
app.post('/api/video-agent/generate-character', async (request, reply) => {
|
||||
const { prompts } = request.body as any;
|
||||
if (!prompts) {
|
||||
return reply.code(400).send({ error: 'prompts is required' });
|
||||
}
|
||||
try {
|
||||
const results: Record<string, string> = {};
|
||||
for (const [angle, prompt] of Object.entries(prompts)) {
|
||||
const url = await callImageGen(prompt as string);
|
||||
results[angle] = url;
|
||||
}
|
||||
return { success: true, data: results };
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `Image generation failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/video-agent/scene-prompt — generate scene image prompt
|
||||
app.post('/api/video-agent/scene-prompt', async (request, reply) => {
|
||||
const { scene } = request.body as any;
|
||||
if (!scene) {
|
||||
return reply.code(400).send({ error: 'scene is required' });
|
||||
}
|
||||
try {
|
||||
const desc = `${scene.environment}. ${scene.description}`;
|
||||
const result = await callLLM(desc, SCENE_PROMPT_GEN);
|
||||
const prompt = JSON.parse(result.replace(/```json|```/g, '').trim());
|
||||
return { success: true, data: prompt };
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `Scene prompt failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/video-agent/generate-scene — generate scene image
|
||||
app.post('/api/video-agent/generate-scene', async (request, reply) => {
|
||||
const { prompt } = request.body as any;
|
||||
if (!prompt) {
|
||||
return reply.code(400).send({ error: 'prompt is required' });
|
||||
}
|
||||
try {
|
||||
const url = await callImageGen(prompt);
|
||||
return { success: true, data: { url } };
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `Scene generation failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/video-agent/generate-tts — generate TTS audio for a line
|
||||
app.post('/api/video-agent/generate-tts', async (request, reply) => {
|
||||
const { text, voice } = request.body as any;
|
||||
if (!text) {
|
||||
return reply.code(400).send({ error: 'text is required' });
|
||||
}
|
||||
try {
|
||||
const audio = await callTTS(text, voice || 'alloy');
|
||||
if (!audio) {
|
||||
return reply.code(502).send({ error: 'TTS generation failed' });
|
||||
}
|
||||
return reply.type('audio/mpeg').send(audio);
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `TTS failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/video-agent/full-workflow — run complete workflow: analyze + generate all assets
|
||||
app.post('/api/video-agent/full-workflow', async (request, reply) => {
|
||||
const { script, style } = request.body as AgentRequest;
|
||||
if (!script) {
|
||||
return reply.code(400).send({ error: 'script is required' });
|
||||
}
|
||||
try {
|
||||
// Phase 1: Analyze script
|
||||
const analysisRaw = await callLLM(script, SCRIPT_ANALYSIS_PROMPT);
|
||||
const analysis = JSON.parse(analysisRaw.replace(/```json|```/g, '').trim());
|
||||
|
||||
// Phase 2: Generate character prompts and images
|
||||
const characters: any[] = [];
|
||||
for (const char of (analysis.characters || [])) {
|
||||
const desc = `${char.name}: ${char.appearance}, ${char.gender}, ${char.age}`;
|
||||
const promptsRaw = await callLLM(desc, CHARACTER_PROMPT_GEN);
|
||||
const prompts = JSON.parse(promptsRaw.replace(/```json|```/g, '').trim());
|
||||
characters.push({ ...char, imagePrompts: prompts });
|
||||
}
|
||||
|
||||
// Phase 3: Generate scene prompts
|
||||
const scenes: any[] = [];
|
||||
for (const scene of (analysis.scenes || [])) {
|
||||
const desc = `${scene.environment}. ${scene.description}`;
|
||||
const scenePromptRaw = await callLLM(desc, SCENE_PROMPT_GEN);
|
||||
const scenePrompt = JSON.parse(scenePromptRaw.replace(/```json|```/g, '').trim());
|
||||
scenes.push({ ...scene, imagePrompt: scenePrompt });
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
title: analysis.title,
|
||||
style: analysis.style || style,
|
||||
characters,
|
||||
scenes,
|
||||
},
|
||||
};
|
||||
} catch (e: any) {
|
||||
return reply.code(500).send({ error: `Workflow failed: ${e.message}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -34,6 +34,7 @@ const ProgramLogs = lazy(() => import('./pages/ProgramLogs.js'));
|
||||
const Models = lazy(() => import('./pages/Models.js'));
|
||||
const About = lazy(() => import('./pages/About.js'));
|
||||
const ModelTester = lazy(() => import('./pages/ModelTester.js'));
|
||||
const ComfyUI = lazy(() => import('./pages/ComfyUI.js'));
|
||||
const ComfyUIAgent = lazy(() => import('./pages/ComfyUIAgent.js'));
|
||||
const Monitors = lazy(() => import('./pages/Monitors.js'));
|
||||
const OAuthManagement = lazy(() => import('./pages/OAuthManagement.js'));
|
||||
@@ -397,6 +398,7 @@ export const sidebarGroups = [
|
||||
{ to: '/checkin', label: '签到记录', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
|
||||
{ to: '/routes', label: '路由', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg> },
|
||||
{ to: '/logs', label: '使用日志', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg> },
|
||||
{ to: '/video-agent', label: '视频工作台', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg> },
|
||||
{ to: '/comfyui-agent', label: '视频助手', icon: <svg className="sidebar-item-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
|
||||
],
|
||||
},
|
||||
@@ -844,6 +846,7 @@ function AppShell() {
|
||||
<Route path="/settings/import-export" element={<ImportExport />} />
|
||||
<Route path="/settings/notify" element={<NotificationSettings />} />
|
||||
<Route path="/models" element={<Models />} />
|
||||
<Route path="/video-agent" element={<ComfyUI />} />
|
||||
<Route path="/comfyui-agent" element={<ComfyUIAgent />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/users" element={<UserManagement />} />
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ComfyUI() {
|
||||
const [isDark, setIsDark] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.location.href = '/comfyui-agent';
|
||||
const el = document.documentElement;
|
||||
const check = () => setIsDark(el.getAttribute('data-theme') !== 'light');
|
||||
check();
|
||||
const mo = new MutationObserver(check);
|
||||
mo.observe(el, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
return () => mo.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -11,14 +18,20 @@ export default function ComfyUI() {
|
||||
width: '100%',
|
||||
height: 'calc(100vh - 56px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'var(--color-bg, #0f0f1a)',
|
||||
color: 'var(--color-text-secondary, #888)',
|
||||
fontSize: '16px',
|
||||
flexDirection: 'column',
|
||||
background: isDark ? '#1a1a2e' : '#f5f5f5',
|
||||
}}
|
||||
>
|
||||
🔄 正在跳转至 AI 视频助手...
|
||||
<iframe
|
||||
src="/comfyui"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
title="ComfyUI"
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user