Make short video factory use Comfy templates
This commit is contained in:
@@ -55,6 +55,11 @@ function mappingFromGraph(graph: any) {
|
||||
|
||||
function inferMappingFromPrompt(prompt: any) {
|
||||
const nodes = prompt && typeof prompt === 'object' && !Array.isArray(prompt) ? Object.entries(prompt as Record<string, any>) : [];
|
||||
const inputKeys = (nodeId: string) => Object.keys(prompt?.[String(nodeId)]?.inputs || {});
|
||||
const firstInputField = (nodeId: string, fields: string[], fallback: string) => {
|
||||
const keys = inputKeys(nodeId);
|
||||
return fields.find((field) => keys.includes(field)) || fallback;
|
||||
};
|
||||
const find = (classWords: string[], inputWords: string[] = []) => {
|
||||
return nodes.find(([, node]) => {
|
||||
const classType = String(node?.class_type || node?.type || '').toLowerCase();
|
||||
@@ -66,11 +71,11 @@ function inferMappingFromPrompt(prompt: any) {
|
||||
const negativeNode = find(['negative'], ['negative']);
|
||||
const videoNode = find(['wan', 'ltx', 'video'], ['duration', 'ratio', 'resolution']);
|
||||
return {
|
||||
prompt: { nodeId: String(promptNode), field: prompt?.[String(promptNode)]?.inputs?.prompt !== undefined ? 'prompt' : 'text' },
|
||||
prompt: { nodeId: String(promptNode), field: firstInputField(String(promptNode), ['prompt', 'positive', 'text', 'positive_prompt'], 'text') },
|
||||
negativePrompt: { nodeId: String(negativeNode), field: 'text' },
|
||||
ratio: { nodeId: String(videoNode), field: 'ratio' },
|
||||
resolution: { nodeId: String(videoNode), field: 'resolution' },
|
||||
duration: { nodeId: String(videoNode), field: 'duration' },
|
||||
ratio: { nodeId: String(videoNode), field: firstInputField(String(videoNode), ['ratio', 'model.ratio', 'aspect_ratio'], 'ratio') },
|
||||
resolution: { nodeId: String(videoNode), field: firstInputField(String(videoNode), ['model.resolution', 'resolution', 'size'], 'resolution') },
|
||||
duration: { nodeId: String(videoNode), field: firstInputField(String(videoNode), ['model.duration', 'duration', 'length'], 'duration') },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,7 +197,6 @@ function workflowLooksVideoCapable(workflow: WorkflowSummary) {
|
||||
function moneyPrinterWorkflows(items: WorkflowSummary[]) {
|
||||
const seen = new Set<string>();
|
||||
return items
|
||||
.filter((workflow) => workflowLooksVideoCapable(workflow))
|
||||
.filter((workflow) => {
|
||||
const key = workflow.publicId || String(workflow.id);
|
||||
if (seen.has(key)) return false;
|
||||
@@ -200,6 +204,9 @@ function moneyPrinterWorkflows(items: WorkflowSummary[]) {
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aVideo = workflowLooksVideoCapable(a);
|
||||
const bVideo = workflowLooksVideoCapable(b);
|
||||
if (aVideo !== bVideo) return aVideo ? -1 : 1;
|
||||
const aTemplate = a.isTemplate || a.visibility === 'template' || a.visibility === 'public';
|
||||
const bTemplate = b.isTemplate || b.visibility === 'template' || b.visibility === 'public';
|
||||
if (aTemplate !== bTemplate) return aTemplate ? -1 : 1;
|
||||
@@ -207,6 +214,16 @@ function moneyPrinterWorkflows(items: WorkflowSummary[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseJsonInput(value: string, label: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
throw new Error(`${label} 不是有效 JSON`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function MoneyPrinter() {
|
||||
const location = useLocation();
|
||||
const audience: SessionAudience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
|
||||
@@ -215,7 +232,7 @@ export default function MoneyPrinter() {
|
||||
const [topic, setTopic] = useState('帮我生成一个 30 秒的 AI 应用平台 BossAI 商品介绍短视频');
|
||||
const [scriptModel, setScriptModel] = useState('gpt-5.4');
|
||||
const [videoModel, setVideoModel] = useState('seedance-1-0-lite-t2v');
|
||||
const [videoBackend, setVideoBackend] = useState<'boosapi' | 'comfy'>('boosapi');
|
||||
const [videoBackend, setVideoBackend] = useState<'boosapi' | 'comfy'>('comfy');
|
||||
const [style, setStyle] = useState(STYLE_PRESETS[0]);
|
||||
const [audienceText, setAudienceText] = useState('AI 工具用户、内容创作者、企业运营');
|
||||
const [duration, setDuration] = useState(30);
|
||||
@@ -226,7 +243,7 @@ export default function MoneyPrinter() {
|
||||
const [comfyNodeMapping, setComfyNodeMapping] = useState('{\n "prompt": { "nodeId": "", "field": "prompt" },\n "negativePrompt": { "nodeId": "", "field": "text" },\n "ratio": { "nodeId": "", "field": "ratio" },\n "resolution": { "nodeId": "", "field": "resolution" },\n "duration": { "nodeId": "", "field": "duration" }\n}');
|
||||
const [workflows, setWorkflows] = useState<WorkflowSummary[]>([]);
|
||||
const [templateId, setTemplateId] = useState('');
|
||||
const [useAdvancedComfy, setUseAdvancedComfy] = useState(false);
|
||||
const [showAdvancedComfy, setShowAdvancedComfy] = useState(false);
|
||||
const [jobs, setJobs] = useState<MoneyPrinterJob[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -254,9 +271,10 @@ export default function MoneyPrinter() {
|
||||
}, [loadJobs, loadWorkflows]);
|
||||
|
||||
const selectedWorkflow = workflows.find((item) => item.publicId === templateId) || null;
|
||||
const canSubmit = Boolean(topic.trim()) && !submitting && (videoBackend !== 'comfy' || Boolean(templateId));
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedWorkflow || videoBackend !== 'comfy' || !useAdvancedComfy) return;
|
||||
if (!selectedWorkflow || videoBackend !== 'comfy') return;
|
||||
const prompt = workflowPromptFromGraph(selectedWorkflow.graph);
|
||||
if (prompt) setComfyPrompt(JSON.stringify(prompt, null, 2));
|
||||
const mapping = mappingFromGraph(selectedWorkflow.graph);
|
||||
@@ -292,8 +310,8 @@ export default function MoneyPrinter() {
|
||||
language: '中文',
|
||||
...(videoBackend === 'comfy' ? {
|
||||
templateId: templateId || undefined,
|
||||
comfyPrompt: comfyPrompt.trim() ? JSON.parse(comfyPrompt) : undefined,
|
||||
comfyNodeMapping: comfyNodeMapping.trim() ? JSON.parse(comfyNodeMapping) : undefined,
|
||||
comfyPrompt: parseJsonInput(comfyPrompt, 'Comfy prompt JSON'),
|
||||
comfyNodeMapping: parseJsonInput(comfyNodeMapping, '节点映射 JSON'),
|
||||
} : {}),
|
||||
});
|
||||
setJobs((prev) => [payload.job, ...prev.filter((item) => item.id !== payload.job.id)]);
|
||||
@@ -329,7 +347,7 @@ export default function MoneyPrinter() {
|
||||
setComfyPrompt(request.comfyPrompt ? JSON.stringify(request.comfyPrompt, null, 2) : comfyPrompt);
|
||||
setComfyNodeMapping(request.comfyNodeMapping ? JSON.stringify(request.comfyNodeMapping, null, 2) : comfyNodeMapping);
|
||||
setTemplateId(request.templateId || templateId);
|
||||
setUseAdvancedComfy(Boolean(request.templateId));
|
||||
setShowAdvancedComfy(Boolean(request.comfyPrompt || request.comfyNodeMapping));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -341,7 +359,7 @@ export default function MoneyPrinter() {
|
||||
</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()}>
|
||||
<button type="button" className="btn btn-primary" onClick={submit} disabled={!canSubmit}>
|
||||
{submitting ? '提交中...' : '生成短视频'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -353,7 +371,7 @@ export default function MoneyPrinter() {
|
||||
<section className="money-printer-panel">
|
||||
<div className="money-printer-hero">
|
||||
<span>MoneyPrinterTurbo inside</span>
|
||||
<strong>一句主题,自动拆成脚本、分镜、视频提示词和成片任务。</strong>
|
||||
<strong>一句主题,自动拆成脚本、分镜、视频提示词,再提交到 Comfy API 生成成片。</strong>
|
||||
</div>
|
||||
|
||||
<label className="money-printer-field">
|
||||
@@ -375,7 +393,12 @@ export default function MoneyPrinter() {
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>视频模型</span>
|
||||
<input value={videoModel} onChange={(event) => setVideoModel(event.target.value)} />
|
||||
<input
|
||||
value={videoModel}
|
||||
onChange={(event) => setVideoModel(event.target.value)}
|
||||
disabled={videoBackend === 'comfy'}
|
||||
title={videoBackend === 'comfy' ? 'Comfy API 模式使用所选工作流模板里的模型' : undefined}
|
||||
/>
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>时长</span>
|
||||
@@ -430,7 +453,7 @@ export default function MoneyPrinter() {
|
||||
<option value="">请选择模板</option>
|
||||
{workflows.map((workflow) => (
|
||||
<option key={workflow.publicId} value={workflow.publicId}>
|
||||
{workflowOptionLabel(workflow)}
|
||||
{workflowLooksVideoCapable(workflow) ? '视频 · ' : '模板 · '}{workflowOptionLabel(workflow)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -438,17 +461,32 @@ export default function MoneyPrinter() {
|
||||
{selectedWorkflow ? (
|
||||
<div className="money-printer-template-meta">
|
||||
<strong>{selectedWorkflow.name}</strong>
|
||||
<span>{mappingFromGraph(selectedWorkflow.graph) ? '已带节点映射' : '这个模板没有内置节点映射,系统会尝试自动推断。'}</span>
|
||||
<span>
|
||||
{workflowLooksVideoCapable(selectedWorkflow) ? '已识别为视频工作流' : '未识别视频关键词,请确认这是 Comfy 视频模板'}
|
||||
{' · '}
|
||||
{mappingFromGraph(selectedWorkflow.graph) ? '已带节点映射' : '系统会按节点结构自动推断提示词和参数映射'}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<label className="money-printer-field">
|
||||
<span>Comfy prompt JSON</span>
|
||||
<textarea value={comfyPrompt} onChange={(event) => setComfyPrompt(event.target.value)} rows={7} placeholder='{"12":{"class_type":"...","inputs":{...}}}' />
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>节点映射 JSON</span>
|
||||
<textarea value={comfyNodeMapping} onChange={(event) => setComfyNodeMapping(event.target.value)} rows={7} />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost money-printer-advanced-toggle"
|
||||
onClick={() => setShowAdvancedComfy((prev) => !prev)}
|
||||
>
|
||||
{showAdvancedComfy ? '收起模板调试' : '展开模板调试'}
|
||||
</button>
|
||||
<div className={`anim-collapse ${showAdvancedComfy ? 'is-open' : ''}`.trim()}>
|
||||
<div className="anim-collapse-inner">
|
||||
<label className="money-printer-field">
|
||||
<span>Comfy prompt JSON</span>
|
||||
<textarea value={comfyPrompt} onChange={(event) => setComfyPrompt(event.target.value)} rows={7} placeholder='{"12":{"class_type":"...","inputs":{...}}}' />
|
||||
</label>
|
||||
<label className="money-printer-field">
|
||||
<span>节点映射 JSON</span>
|
||||
<textarea value={comfyNodeMapping} onChange={(event) => setComfyNodeMapping(event.target.value)} rows={7} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user