Add native MoneyPrinter short video factory
This commit is contained in:
@@ -39,6 +39,7 @@ const ModelTester = lazy(() => import('./pages/ModelTester.js'));
|
||||
const ComfyUIAgent = lazy(() => import('./pages/ComfyUIAgent.js'));
|
||||
const PictureAgent = lazy(() => import('./pages/PictureAgent.js'));
|
||||
const DigitalHuman = lazy(() => import('./pages/DigitalHuman.js'));
|
||||
const MoneyPrinter = lazy(() => import('./pages/MoneyPrinter.js'));
|
||||
const Workspace = lazy(() => import('./pages/Workspace.js'));
|
||||
const Workflows = lazy(() => import('./pages/Workflows.js'));
|
||||
const Monitors = lazy(() => import('./pages/Monitors.js'));
|
||||
@@ -387,6 +388,7 @@ export const sidebarGroups = [
|
||||
{ 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: '/picture-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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-5-6h.01M5 4h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z" /></svg> },
|
||||
{ to: '/money-printer', 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="M7 4h10a2 2 0 012 2v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6a2 2 0 012-2z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 9l5 3-5 3V9zM8 20l2-4m6 4l-2-4" /></svg> },
|
||||
{ to: '/digital-human', 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="M12 12a4 4 0 100-8 4 4 0 000 8z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 21a8 8 0 0116 0M17 9l3 2v4l-3 2M7 9l-3 2v4l3 2" /></svg> },
|
||||
{ to: '/workflows', 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="M4 6h5v5H4zM15 4h5v5h-5zM15 15h5v5h-5zM9 8h6M17.5 9v6M9 11l6 6" /></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> },
|
||||
@@ -419,6 +421,7 @@ export const userSidebarGroups = [
|
||||
label: '创作应用',
|
||||
items: [
|
||||
{ to: '/user/picture-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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-5-6h.01M5 4h14a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z" /></svg> },
|
||||
{ to: '/user/money-printer', 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="M7 4h10a2 2 0 012 2v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6a2 2 0 012-2z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 9l5 3-5 3V9zM8 20l2-4m6 4l-2-4" /></svg> },
|
||||
{ to: '/user/digital-human', 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="M12 12a4 4 0 100-8 4 4 0 000 8z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 21a8 8 0 0116 0M17 9l3 2v4l-3 2M7 9l-3 2v4l3 2" /></svg> },
|
||||
{ to: '/user/workflows', 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="M4 6h5v5H4zM15 4h5v5h-5zM15 15h5v5h-5zM9 8h6M17.5 9v6M9 11l6 6" /></svg> },
|
||||
{ to: '/user/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> },
|
||||
@@ -430,6 +433,7 @@ const topNavItems = [
|
||||
{ label: '控制台', to: '/' },
|
||||
{ label: '模型广场', to: '/models' },
|
||||
{ label: '生图助手', to: '/picture-agent' },
|
||||
{ label: '短视频工厂', to: '/money-printer' },
|
||||
{ label: '工作流', to: '/workflows' },
|
||||
{ label: '视频助手', to: '/comfyui-agent' },
|
||||
{ label: '工作空间', to: '/workspace' },
|
||||
@@ -987,6 +991,7 @@ function AppShell() {
|
||||
<Route path="/settings/notify" element={<NotificationSettings />} />
|
||||
<Route path="/models" element={<Models />} />
|
||||
<Route path="/picture-agent" element={<PictureAgent />} />
|
||||
<Route path="/money-printer" element={<MoneyPrinter />} />
|
||||
<Route path="/digital-human" element={<DigitalHuman />} />
|
||||
<Route path="/workflows" element={<Workflows />} />
|
||||
<Route path="/video-agent" element={<Navigate to="/workflows" replace />} />
|
||||
@@ -1006,6 +1011,7 @@ function AppShell() {
|
||||
<Route path="/user/usage" element={<UsageRecords />} />
|
||||
<Route path="/user/announcements" element={<UserAnnouncements />} />
|
||||
<Route path="/user/picture-agent" element={<PictureAgent />} />
|
||||
<Route path="/user/money-printer" element={<MoneyPrinter />} />
|
||||
<Route path="/user/digital-human" element={<DigitalHuman />} />
|
||||
<Route path="/user/workflows" element={<Workflows />} />
|
||||
<Route path="/user/video-agent" element={<Navigate to="/user/workflows" replace />} />
|
||||
|
||||
@@ -1184,6 +1184,18 @@ export type DigitalHumanJobsResponse = {
|
||||
jobs: DigitalHumanJob[];
|
||||
};
|
||||
|
||||
export type MoneyPrinterJob = DigitalHumanJob;
|
||||
|
||||
export type MoneyPrinterJobResponse = {
|
||||
success: boolean;
|
||||
job: MoneyPrinterJob;
|
||||
};
|
||||
|
||||
export type MoneyPrinterJobsResponse = {
|
||||
success: boolean;
|
||||
jobs: MoneyPrinterJob[];
|
||||
};
|
||||
|
||||
export const api = {
|
||||
// Sites
|
||||
getSites: async () => {
|
||||
@@ -2081,6 +2093,20 @@ export const api = {
|
||||
request<DigitalHumanJobResponse>(`/api/digital-human/jobs/${encodeURIComponent(id)}/cancel`, { method: "POST" }),
|
||||
clearDigitalHumanJobs: () =>
|
||||
request<{ success: boolean; cleared: number }>("/api/digital-human/jobs/clear", { method: "POST" }),
|
||||
createMoneyPrinterJob: (data: any) =>
|
||||
request<MoneyPrinterJobResponse>("/api/money-printer/jobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
timeoutMs: 60_000,
|
||||
}),
|
||||
getMoneyPrinterJobs: (params?: { limit?: number }) =>
|
||||
request<MoneyPrinterJobsResponse>(`/api/money-printer/jobs${buildQueryString(params)}`),
|
||||
getMoneyPrinterJob: (id: string) =>
|
||||
request<MoneyPrinterJobResponse>(`/api/money-printer/jobs/${encodeURIComponent(id)}`),
|
||||
cancelMoneyPrinterJob: (id: string) =>
|
||||
request<MoneyPrinterJobResponse>(`/api/money-printer/jobs/${encodeURIComponent(id)}/cancel`, { method: "POST" }),
|
||||
clearMoneyPrinterJobs: () =>
|
||||
request<{ success: boolean; cleared: number }>("/api/money-printer/jobs/clear", { method: "POST" }),
|
||||
};
|
||||
export const userApi = {
|
||||
register: (data: {
|
||||
@@ -2170,4 +2196,18 @@ export const userApi = {
|
||||
userRequest<DigitalHumanJobResponse>(`/api/digital-human/jobs/${encodeURIComponent(id)}/cancel`, { method: "POST" }),
|
||||
clearDigitalHumanJobs: () =>
|
||||
userRequest<{ success: boolean; cleared: number }>("/api/digital-human/jobs/clear", { method: "POST" }),
|
||||
createMoneyPrinterJob: (data: any) =>
|
||||
userRequest<MoneyPrinterJobResponse>("/api/money-printer/jobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
timeoutMs: 60_000,
|
||||
}),
|
||||
getMoneyPrinterJobs: (params?: { limit?: number }) =>
|
||||
userRequest<MoneyPrinterJobsResponse>(`/api/money-printer/jobs${buildQueryString(params)}`),
|
||||
getMoneyPrinterJob: (id: string) =>
|
||||
userRequest<MoneyPrinterJobResponse>(`/api/money-printer/jobs/${encodeURIComponent(id)}`),
|
||||
cancelMoneyPrinterJob: (id: string) =>
|
||||
userRequest<MoneyPrinterJobResponse>(`/api/money-printer/jobs/${encodeURIComponent(id)}/cancel`, { method: "POST" }),
|
||||
clearMoneyPrinterJobs: () =>
|
||||
userRequest<{ success: boolean; cleared: number }>("/api/money-printer/jobs/clear", { method: "POST" }),
|
||||
};
|
||||
|
||||
@@ -9250,6 +9250,199 @@ textarea {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.money-printer-page {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.money-printer-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(340px, 0.95fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.money-printer-panel {
|
||||
border: 1px solid var(--color-border-light);
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 18px;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.money-printer-hero {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 18%, var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--color-primary) 7%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.money-printer-hero span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 720;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.money-printer-hero strong {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.money-printer-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.money-printer-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.money-printer-field input,
|
||||
.money-printer-field select,
|
||||
.money-printer-field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 9px 11px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.money-printer-field textarea {
|
||||
min-height: 112px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.money-printer-section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.money-printer-section-head h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.money-printer-section-head p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.money-printer-job-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.money-printer-job-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--color-bg) 66%, var(--color-bg-card));
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.money-printer-job-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.money-printer-job-head div {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.money-printer-job-head strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.money-printer-job-head span,
|
||||
.money-printer-job-head em,
|
||||
.money-printer-job-meta,
|
||||
.money-printer-hook {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.money-printer-job-head em {
|
||||
flex: none;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.money-printer-progress {
|
||||
height: 7px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-border) 58%, transparent);
|
||||
}
|
||||
|
||||
.money-printer-progress span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--color-primary);
|
||||
transition: width .25s ease;
|
||||
}
|
||||
|
||||
.money-printer-job-meta,
|
||||
.money-printer-assets,
|
||||
.money-printer-job-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.money-printer-job-meta span,
|
||||
.money-printer-job-meta a {
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-bg) 80%, var(--color-bg-card));
|
||||
border: 1px solid var(--color-border-light);
|
||||
padding: 3px 7px;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.money-printer-hook {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--color-bg) 78%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.money-printer-job-error {
|
||||
color: var(--color-danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.money-printer-assets a {
|
||||
color: var(--color-primary);
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.workspace-page .page-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { api, userApi, type MoneyPrinterJob } from '../api.js';
|
||||
import type { SessionAudience } from '../authSession.js';
|
||||
|
||||
const STYLE_PRESETS = [
|
||||
'爆款短视频,节奏紧凑,适合口播',
|
||||
'商品种草,真实体验,强转化',
|
||||
'知识科普,信息密度高,画面干净',
|
||||
'电影感广告,镜头运动丰富,高级质感',
|
||||
];
|
||||
|
||||
function terminal(status: string) {
|
||||
return ['succeeded', 'failed', 'cancelled'].includes(status);
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === 'succeeded') return '完成';
|
||||
if (status === 'failed') return '失败';
|
||||
if (status === 'cancelled') return '已取消';
|
||||
if (status === 'running') return '生成中';
|
||||
return '排队中';
|
||||
}
|
||||
|
||||
function progress(job: MoneyPrinterJob) {
|
||||
return Math.max(0, Math.min(100, Math.round((job.progress || 0) / Math.max(1, job.progressTotal || 5) * 100)));
|
||||
}
|
||||
|
||||
function formatTime(value?: string | null) {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function jobAssets(job: MoneyPrinterJob) {
|
||||
return Array.isArray(job.result?.assets) ? job.result.assets : [];
|
||||
}
|
||||
|
||||
function planValue(job: MoneyPrinterJob, key: string) {
|
||||
return job.result?.script?.plan?.[key] || '';
|
||||
}
|
||||
|
||||
export default function MoneyPrinter() {
|
||||
const location = useLocation();
|
||||
const audience: SessionAudience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
|
||||
const workspacePath = audience === 'user' ? '/user/workspace' : '/workspace';
|
||||
const client = useMemo(() => (audience === 'user' ? userApi : api), [audience]);
|
||||
const [topic, setTopic] = useState('帮我生成一个 30 秒的 AI 应用平台 BossAI 商品介绍短视频');
|
||||
const [scriptModel, setScriptModel] = useState('gpt-5.4');
|
||||
const [videoModel, setVideoModel] = useState('seedance-1-0-lite-t2v');
|
||||
const [style, setStyle] = useState(STYLE_PRESETS[0]);
|
||||
const [audienceText, setAudienceText] = useState('AI 工具用户、内容创作者、企业运营');
|
||||
const [duration, setDuration] = useState(30);
|
||||
const [ratio, setRatio] = useState('9:16');
|
||||
const [resolution, setResolution] = useState('1080p');
|
||||
const [requirements, setRequirements] = useState('画面要有科技感,突出统一网关、多模型、多模态和工作流能力。');
|
||||
const [jobs, setJobs] = useState<MoneyPrinterJob[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const loadJobs = useCallback(async () => {
|
||||
const payload = await client.getMoneyPrinterJobs({ limit: 30 });
|
||||
setJobs(Array.isArray(payload.jobs) ? payload.jobs : []);
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
loadJobs().catch((err) => setError(err?.message || '短视频任务加载失败'));
|
||||
}, [loadJobs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobs.some((job) => !terminal(job.status))) return;
|
||||
const timer = window.setInterval(() => {
|
||||
client.getMoneyPrinterJobs({ limit: 30 })
|
||||
.then((payload) => setJobs(payload.jobs || []))
|
||||
.catch(() => {});
|
||||
}, 5000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [client, jobs]);
|
||||
|
||||
const submit = async () => {
|
||||
if (!topic.trim() || submitting) return;
|
||||
setSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
const payload = await client.createMoneyPrinterJob({
|
||||
topic: topic.trim(),
|
||||
scriptModel: scriptModel.trim(),
|
||||
videoModel: videoModel.trim(),
|
||||
style,
|
||||
audience: audienceText,
|
||||
duration,
|
||||
ratio,
|
||||
resolution,
|
||||
requirements,
|
||||
language: '中文',
|
||||
});
|
||||
setJobs((prev) => [payload.job, ...prev.filter((item) => item.id !== payload.job.id)]);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '短视频任务创建失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = async (id: string) => {
|
||||
const payload = await client.cancelMoneyPrinterJob(id);
|
||||
setJobs((prev) => prev.map((item) => item.id === id ? payload.job : item));
|
||||
};
|
||||
|
||||
const clear = async () => {
|
||||
await client.clearMoneyPrinterJobs();
|
||||
await loadJobs();
|
||||
};
|
||||
|
||||
const retry = (job: MoneyPrinterJob) => {
|
||||
const request = job.result?.request || {};
|
||||
setTopic(request.topic || job.result?.topic || topic);
|
||||
setScriptModel(request.scriptModel || scriptModel);
|
||||
setVideoModel(request.videoModel || request.model || videoModel);
|
||||
setStyle(request.style || style);
|
||||
setAudienceText(request.audience || audienceText);
|
||||
setDuration(Number(request.duration || duration));
|
||||
setRatio(request.ratio || ratio);
|
||||
setResolution(request.resolution || resolution);
|
||||
setRequirements(request.requirements || requirements);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="money-printer-page animate-fade-in">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">短视频工厂</h1>
|
||||
<p className="page-subtitle">内置 MoneyPrinterTurbo 思路:主题生成脚本和分镜,再用 BoosAPI 视频模型生产成片。</p>
|
||||
</div>
|
||||
<div className="page-actions">
|
||||
<button type="button" className="btn btn-ghost" onClick={() => void loadJobs()}>刷新</button>
|
||||
<button type="button" className="btn btn-primary" onClick={submit} disabled={submitting || !topic.trim()}>
|
||||
{submitting ? '提交中...' : '生成短视频'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error" style={{ marginBottom: 12 }}>{error}</div>}
|
||||
|
||||
<div className="money-printer-layout">
|
||||
<section className="money-printer-panel">
|
||||
<div className="money-printer-hero">
|
||||
<span>MoneyPrinterTurbo inside</span>
|
||||
<strong>一句主题,自动拆成脚本、分镜、视频提示词和成片任务。</strong>
|
||||
</div>
|
||||
|
||||
<label className="money-printer-field">
|
||||
<span>短视频主题</span>
|
||||
<textarea value={topic} onChange={(event) => setTopic(event.target.value)} rows={4} />
|
||||
</label>
|
||||
|
||||
<div className="money-printer-form-grid">
|
||||
<label className="money-printer-field">
|
||||
<span>脚本模型</span>
|
||||
<input value={scriptModel} onChange={(event) => setScriptModel(event.target.value)} />
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>视频模型</span>
|
||||
<input value={videoModel} onChange={(event) => setVideoModel(event.target.value)} />
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>时长</span>
|
||||
<input type="number" min={5} max={120} value={duration} onChange={(event) => setDuration(Number(event.target.value || 30))} />
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>画幅</span>
|
||||
<select value={ratio} onChange={(event) => setRatio(event.target.value)}>
|
||||
<option value="9:16">9:16 竖屏</option>
|
||||
<option value="16:9">16:9 横屏</option>
|
||||
<option value="1:1">1:1 方屏</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>分辨率</span>
|
||||
<select value={resolution} onChange={(event) => setResolution(event.target.value)}>
|
||||
<option value="1080p">1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
<option value="480p">480p</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="money-printer-field">
|
||||
<span>风格</span>
|
||||
<select value={style} onChange={(event) => setStyle(event.target.value)}>
|
||||
{STYLE_PRESETS.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="money-printer-field">
|
||||
<span>目标受众</span>
|
||||
<input value={audienceText} onChange={(event) => setAudienceText(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<label className="money-printer-field">
|
||||
<span>额外要求</span>
|
||||
<textarea value={requirements} onChange={(event) => setRequirements(event.target.value)} rows={4} />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="money-printer-panel">
|
||||
<div className="money-printer-section-head">
|
||||
<div>
|
||||
<h2>任务队列</h2>
|
||||
<p>脚本、视频生成和工作空间归档都会在后台执行。</p>
|
||||
</div>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => void clear()}>清空已完成</button>
|
||||
</div>
|
||||
|
||||
<div className="money-printer-job-list">
|
||||
{jobs.length === 0 ? (
|
||||
<div className="empty-state" style={{ minHeight: 220 }}>
|
||||
<div className="empty-state-title">暂无短视频任务</div>
|
||||
<div className="empty-state-desc">提交主题后,系统会自动调用 BoosAPI 内置模型生成脚本和视频。</div>
|
||||
</div>
|
||||
) : jobs.map((job) => {
|
||||
const percent = progress(job);
|
||||
const assets = jobAssets(job);
|
||||
return (
|
||||
<article key={job.id} className="money-printer-job-card">
|
||||
<div className="money-printer-job-head">
|
||||
<div>
|
||||
<strong>{job.title || '短视频工厂任务'}</strong>
|
||||
<span>{job.message || statusLabel(job.status)}{formatTime(job.createdAt) ? ` · ${formatTime(job.createdAt)}` : ''}</span>
|
||||
</div>
|
||||
<em>{statusLabel(job.status)}</em>
|
||||
</div>
|
||||
<div className="money-printer-progress"><span style={{ width: `${percent}%` }} /></div>
|
||||
<div className="money-printer-job-meta">
|
||||
<span>进度 {percent}%</span>
|
||||
{job.result?.videoUrl && <a href={job.result.videoUrl} target="_blank" rel="noreferrer">远端视频</a>}
|
||||
</div>
|
||||
{planValue(job, 'hook') && <p className="money-printer-hook">{planValue(job, 'hook')}</p>}
|
||||
{job.error && <div className="money-printer-job-error">{job.error}</div>}
|
||||
{assets.length > 0 && (
|
||||
<div className="money-printer-assets">
|
||||
{assets.map((asset: any) => (
|
||||
<a key={asset.publicId || asset.remoteUrl} href={`${workspacePath}?q=${encodeURIComponent(asset.publicId || '')}`}>
|
||||
成片 · {asset.publicId || asset.status}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="money-printer-job-buttons">
|
||||
{!terminal(job.status) && <button type="button" className="btn btn-ghost" onClick={() => void cancel(job.id)}>取消</button>}
|
||||
{terminal(job.status) && <button type="button" className="btn btn-ghost" onClick={() => retry(job)}>复用参数</button>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user