forked from boos/BoosAPI
refactor: fully embed ComfyUI into BoosAPI, drop external dependency
- Remove comfyui.ts (external proxy to ComfyUI at :8188) - Remove videoAgent.ts (redundant with comfyuiAgent) - Remove ComfyUI Python process from Dockerfile (CPU-only, useless) - Update boosapi_comfy_nodes.py default to localhost BoosAPI - Clean up index.ts, App.tsx, desktop.ts registrations - Sidebar: merge '视频工作台'/'视频助手' into single '视频助手' entry Signed-off-by: boos <boos@boos.lat>
This commit is contained in:
@@ -24,8 +24,10 @@ import folder_paths
|
|||||||
|
|
||||||
# ── Config ──────────────────────────────────────────────────────────────────
|
# ── Config ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
BOOSAPI_BASE = os.getenv("BOOSAPI_BASE", "https://api.boos.lat/v1")
|
# When running inside the same container as BoosAPI, use localhost.
|
||||||
BOOSAPI_KEY = os.getenv("BOOSAPI_KEY", "sk-boos4721")
|
# For standalone ComfyUI installs, set BOOSAPI_BASE env to https://api.boos.lat/v1
|
||||||
|
BOOSAPI_BASE = os.getenv("BOOSAPI_BASE", "http://127.0.0.1:4000/v1")
|
||||||
|
BOOSAPI_KEY = os.getenv("BOOSAPI_KEY", "admin")
|
||||||
|
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"Authorization": f"Bearer {BOOSAPI_KEY}",
|
"Authorization": f"Bearer {BOOSAPI_KEY}",
|
||||||
|
|||||||
+3
-26
@@ -23,17 +23,15 @@ ARG NO_PROXY
|
|||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV COMFYUI_BASE=http://127.0.0.1:8188
|
|
||||||
ENV HTTP_PROXY=${HTTP_PROXY}
|
ENV HTTP_PROXY=${HTTP_PROXY}
|
||||||
ENV HTTPS_PROXY=${HTTPS_PROXY}
|
ENV HTTPS_PROXY=${HTTPS_PROXY}
|
||||||
ENV ALL_PROXY=${ALL_PROXY}
|
ENV ALL_PROXY=${ALL_PROXY}
|
||||||
ENV NO_PROXY=${NO_PROXY}
|
ENV NO_PROXY=${NO_PROXY}
|
||||||
|
|
||||||
# Install system deps: Node.js 22, Python 3, supervisor
|
# Install system deps: Node.js 22, supervisor
|
||||||
RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt-get update && apt-get install -y --no-install-recommends \
|
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 \
|
curl ca-certificates gnupg \
|
||||||
python3 python3-pip python3-venv \
|
supervisor \
|
||||||
supervisor git \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -42,17 +40,6 @@ RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt
|
|||||||
COPY rtk /usr/local/bin/rtk
|
COPY rtk /usr/local/bin/rtk
|
||||||
RUN chmod +x /usr/local/bin/rtk
|
RUN chmod +x /usr/local/bin/rtk
|
||||||
|
|
||||||
# Set up ComfyUI with CPU PyTorch
|
|
||||||
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)
|
# Copy metapi (built)
|
||||||
WORKDIR /app/metapi
|
WORKDIR /app/metapi
|
||||||
COPY --from=metapi-builder /app/dist ./dist
|
COPY --from=metapi-builder /app/dist ./dist
|
||||||
@@ -61,9 +48,6 @@ COPY --from=metapi-builder /app/package.json ./
|
|||||||
COPY --from=metapi-builder /app/drizzle ./drizzle
|
COPY --from=metapi-builder /app/drizzle ./drizzle
|
||||||
RUN mkdir -p data
|
RUN mkdir -p data
|
||||||
|
|
||||||
# Copy BoosAPI custom nodes for ComfyUI
|
|
||||||
COPY --from=metapi-builder /app/boosapi_comfy_nodes.py /app/comfy/custom_nodes/boosapi_comfy_nodes.py
|
|
||||||
|
|
||||||
# Supervisor config
|
# Supervisor config
|
||||||
RUN mkdir -p /var/log/supervisor
|
RUN mkdir -p /var/log/supervisor
|
||||||
COPY <<'SUPERVISOR' /etc/supervisor/conf.d/all.conf
|
COPY <<'SUPERVISOR' /etc/supervisor/conf.d/all.conf
|
||||||
@@ -80,15 +64,8 @@ autorestart=true
|
|||||||
stdout_logfile=/var/log/supervisor/metapi.log
|
stdout_logfile=/var/log/supervisor/metapi.log
|
||||||
stderr_logfile=/var/log/supervisor/metapi.err
|
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
|
SUPERVISOR
|
||||||
|
|
||||||
EXPOSE 4000 8188
|
EXPOSE 4000
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const DESKTOP_HEALTH_ROUTE = '/api/desktop/health';
|
|||||||
export function isPublicApiRoute(url: string): boolean {
|
export function isPublicApiRoute(url: string): boolean {
|
||||||
return url === DESKTOP_HEALTH_ROUTE
|
return url === DESKTOP_HEALTH_ROUTE
|
||||||
|| url.startsWith('/api/oauth/callback/')
|
|| url.startsWith('/api/oauth/callback/')
|
||||||
|| url.startsWith('/api/comfyui/proxy/')
|
|
||||||
// User SaaS routes (JWT auth, not admin token)
|
// User SaaS routes (JWT auth, not admin token)
|
||||||
|| url === '/api/users/register'
|
|| url === '/api/users/register'
|
||||||
|| url === '/api/users/login'
|
|| url === '/api/users/login'
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ import { invitationRoutes } from './routes/api/invitations.js';
|
|||||||
import { oauthRoutes } from './routes/api/oauth.js';
|
import { oauthRoutes } from './routes/api/oauth.js';
|
||||||
import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js';
|
import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js';
|
||||||
import { updateCenterRoutes } from './routes/api/updateCenter.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 { adminUsersRoutes } from './routes/api/adminUsers.js';
|
||||||
import { comfyuiAgentRoutes } from './routes/api/comfyuiAgent.js';
|
import { comfyuiAgentRoutes } from './routes/api/comfyuiAgent.js';
|
||||||
import { proxyRoutes } from './routes/proxy/router.js';
|
import { proxyRoutes } from './routes/proxy/router.js';
|
||||||
@@ -241,8 +239,6 @@ await app.register(usersRoutes);
|
|||||||
await app.register(userApiKeysRoutes);
|
await app.register(userApiKeysRoutes);
|
||||||
await app.register(invitationRoutes);
|
await app.register(invitationRoutes);
|
||||||
await app.register(organizationsRoutes);
|
await app.register(organizationsRoutes);
|
||||||
await app.register(comfyuiRoutes);
|
|
||||||
await app.register(videoAgentRoutes);
|
|
||||||
await app.register(comfyuiAgentRoutes);
|
await app.register(comfyuiAgentRoutes);
|
||||||
await app.register(oauthRoutes);
|
await app.register(oauthRoutes);
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
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>`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
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,7 +34,6 @@ const ProgramLogs = lazy(() => import('./pages/ProgramLogs.js'));
|
|||||||
const Models = lazy(() => import('./pages/Models.js'));
|
const Models = lazy(() => import('./pages/Models.js'));
|
||||||
const About = lazy(() => import('./pages/About.js'));
|
const About = lazy(() => import('./pages/About.js'));
|
||||||
const ModelTester = lazy(() => import('./pages/ModelTester.js'));
|
const ModelTester = lazy(() => import('./pages/ModelTester.js'));
|
||||||
const ComfyUI = lazy(() => import('./pages/ComfyUI.js'));
|
|
||||||
const ComfyUIAgent = lazy(() => import('./pages/ComfyUIAgent.js'));
|
const ComfyUIAgent = lazy(() => import('./pages/ComfyUIAgent.js'));
|
||||||
const Monitors = lazy(() => import('./pages/Monitors.js'));
|
const Monitors = lazy(() => import('./pages/Monitors.js'));
|
||||||
const OAuthManagement = lazy(() => import('./pages/OAuthManagement.js'));
|
const OAuthManagement = lazy(() => import('./pages/OAuthManagement.js'));
|
||||||
@@ -398,7 +397,6 @@ 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: '/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: '/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: '/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: 'ComfyUI', 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> },
|
{ 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> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -846,7 +844,6 @@ function AppShell() {
|
|||||||
<Route path="/settings/import-export" element={<ImportExport />} />
|
<Route path="/settings/import-export" element={<ImportExport />} />
|
||||||
<Route path="/settings/notify" element={<NotificationSettings />} />
|
<Route path="/settings/notify" element={<NotificationSettings />} />
|
||||||
<Route path="/models" element={<Models />} />
|
<Route path="/models" element={<Models />} />
|
||||||
<Route path="/video-agent" element={<ComfyUI />} />
|
|
||||||
<Route path="/comfyui-agent" element={<ComfyUIAgent />} />
|
<Route path="/comfyui-agent" element={<ComfyUIAgent />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/users" element={<UserManagement />} />
|
<Route path="/users" element={<UserManagement />} />
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default function ComfyUI() {
|
export default function ComfyUI() {
|
||||||
const [isDark, setIsDark] = React.useState(true);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const el = document.documentElement;
|
window.location.href = '/comfyui-agent';
|
||||||
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 (
|
return (
|
||||||
@@ -18,20 +11,14 @@ export default function ComfyUI() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: 'calc(100vh - 56px)',
|
height: 'calc(100vh - 56px)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
alignItems: 'center',
|
||||||
background: isDark ? '#1a1a2e' : '#f5f5f5',
|
justifyContent: 'center',
|
||||||
|
background: 'var(--color-bg, #0f0f1a)',
|
||||||
|
color: 'var(--color-text-secondary, #888)',
|
||||||
|
fontSize: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<iframe
|
🔄 正在跳转至 AI 视频助手...
|
||||||
src="/comfyui"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
border: 'none',
|
|
||||||
}}
|
|
||||||
title="ComfyUI"
|
|
||||||
allow="clipboard-read; clipboard-write"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user