feat: refine money printer comfy template mode
This commit is contained in:
@@ -1196,6 +1196,31 @@ export type MoneyPrinterJobsResponse = {
|
||||
jobs: MoneyPrinterJob[];
|
||||
};
|
||||
|
||||
export type MoneyPrinterStatusResponse = {
|
||||
success: boolean;
|
||||
mode: string;
|
||||
keyName?: string;
|
||||
capabilities?: string[];
|
||||
comfy?: {
|
||||
enabled?: boolean;
|
||||
backend?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
builtinCloudTemplate?: {
|
||||
enabled?: boolean;
|
||||
label?: string;
|
||||
nodeType?: string;
|
||||
model?: string;
|
||||
duration?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
default?: number;
|
||||
};
|
||||
resolutions?: string[];
|
||||
ratios?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export const api = {
|
||||
// Sites
|
||||
getSites: async () => {
|
||||
@@ -2093,6 +2118,8 @@ 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" }),
|
||||
getMoneyPrinterStatus: () =>
|
||||
request<MoneyPrinterStatusResponse>("/api/money-printer/status"),
|
||||
createMoneyPrinterJob: (data: any) =>
|
||||
request<MoneyPrinterJobResponse>("/api/money-printer/jobs", {
|
||||
method: "POST",
|
||||
@@ -2196,6 +2223,8 @@ 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" }),
|
||||
getMoneyPrinterStatus: () =>
|
||||
userRequest<MoneyPrinterStatusResponse>("/api/money-printer/status"),
|
||||
createMoneyPrinterJob: (data: any) =>
|
||||
userRequest<MoneyPrinterJobResponse>("/api/money-printer/jobs", {
|
||||
method: "POST",
|
||||
|
||||
@@ -9351,6 +9351,11 @@ textarea {
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.money-printer-template-meta.is-warning {
|
||||
border-color: color-mix(in srgb, var(--color-warning) 38%, var(--color-border-light));
|
||||
background: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-card));
|
||||
}
|
||||
|
||||
.money-printer-template-meta strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { api, userApi, type MoneyPrinterJob, type WorkflowSummary } from '../api.js';
|
||||
import { api, userApi, type MoneyPrinterJob, type MoneyPrinterStatusResponse, type WorkflowSummary } from '../api.js';
|
||||
import type { SessionAudience } from '../authSession.js';
|
||||
|
||||
const STYLE_PRESETS = [
|
||||
@@ -225,6 +225,10 @@ function parseJsonInput(value: string, label: string) {
|
||||
}
|
||||
|
||||
const BUILTIN_CLOUD_TEMPLATE_VALUE = '__builtin_cloud_seedance__';
|
||||
const DEFAULT_BUILTIN_CLOUD_DURATION = 5;
|
||||
const DEFAULT_BUILTIN_CLOUD_RESOLUTION = '480p';
|
||||
const DEFAULT_CLOUD_RATIOS = ['9:16', '16:9', '1:1'];
|
||||
const DEFAULT_CLOUD_RESOLUTIONS = ['480p', '720p', '1080p'];
|
||||
|
||||
export default function MoneyPrinter() {
|
||||
const location = useLocation();
|
||||
@@ -247,14 +251,40 @@ export default function MoneyPrinter() {
|
||||
const [templateId, setTemplateId] = useState('');
|
||||
const [showAdvancedComfy, setShowAdvancedComfy] = useState(false);
|
||||
const [jobs, setJobs] = useState<MoneyPrinterJob[]>([]);
|
||||
const [status, setStatus] = useState<MoneyPrinterStatusResponse | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const isCloudComfy = status?.comfy?.backend === 'cloud' && status?.builtinCloudTemplate?.enabled !== false;
|
||||
const cloudDurationMin = status?.builtinCloudTemplate?.duration?.min ?? 3;
|
||||
const cloudDurationMax = status?.builtinCloudTemplate?.duration?.max ?? 12;
|
||||
const cloudDefaultDuration = status?.builtinCloudTemplate?.duration?.default ?? DEFAULT_BUILTIN_CLOUD_DURATION;
|
||||
const cloudResolutions = useMemo(
|
||||
() => status?.builtinCloudTemplate?.resolutions?.length
|
||||
? status.builtinCloudTemplate.resolutions
|
||||
: DEFAULT_CLOUD_RESOLUTIONS,
|
||||
[status?.builtinCloudTemplate?.resolutions],
|
||||
);
|
||||
const cloudRatios = useMemo(
|
||||
() => status?.builtinCloudTemplate?.ratios?.length
|
||||
? status.builtinCloudTemplate.ratios
|
||||
: DEFAULT_CLOUD_RATIOS,
|
||||
[status?.builtinCloudTemplate?.ratios],
|
||||
);
|
||||
|
||||
const loadJobs = useCallback(async () => {
|
||||
const payload = await client.getMoneyPrinterJobs({ limit: 30 });
|
||||
setJobs(Array.isArray(payload.jobs) ? payload.jobs : []);
|
||||
}, [client]);
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
const payload = await client.getMoneyPrinterStatus();
|
||||
setStatus(payload);
|
||||
}, [client]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([loadJobs(), loadStatus()]);
|
||||
}, [loadJobs, loadStatus]);
|
||||
|
||||
const loadWorkflows = useCallback(async () => {
|
||||
const payload = await client.getWorkflows({ include_public: true });
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
@@ -262,19 +292,44 @@ export default function MoneyPrinter() {
|
||||
setWorkflows(videoItems);
|
||||
setTemplateId((current) => {
|
||||
if (current && videoItems.some((item) => item.publicId === current)) return current;
|
||||
if (isCloudComfy) return BUILTIN_CLOUD_TEMPLATE_VALUE;
|
||||
return videoItems.find((item) => item.isTemplate || item.visibility === 'template' || item.visibility === 'public')?.publicId
|
||||
|| videoItems[0]?.publicId
|
||||
|| '';
|
||||
});
|
||||
}, [client]);
|
||||
}, [client, isCloudComfy]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([loadJobs(), loadWorkflows()]).catch((err) => setError(err?.message || '短视频任务加载失败'));
|
||||
}, [loadJobs, loadWorkflows]);
|
||||
Promise.all([loadJobs(), loadWorkflows(), loadStatus()]).catch((err) => setError(err?.message || '短视频任务加载失败'));
|
||||
}, [loadJobs, loadWorkflows, loadStatus]);
|
||||
|
||||
const selectedWorkflow = workflows.find((item) => item.publicId === templateId) || null;
|
||||
const useBuiltinCloudTemplate = videoBackend === 'comfy' && (!templateId || templateId === BUILTIN_CLOUD_TEMPLATE_VALUE);
|
||||
const canSubmit = Boolean(topic.trim()) && !submitting;
|
||||
const useBuiltinCloudTemplate = videoBackend === 'comfy' && isCloudComfy && templateId === BUILTIN_CLOUD_TEMPLATE_VALUE;
|
||||
const comfyHasTemplate = useBuiltinCloudTemplate || Boolean(templateId && selectedWorkflow);
|
||||
const canSubmit = Boolean(topic.trim()) && !submitting && (videoBackend !== 'comfy' || comfyHasTemplate);
|
||||
const ratioOptions = useBuiltinCloudTemplate ? cloudRatios : DEFAULT_CLOUD_RATIOS;
|
||||
const resolutionOptions = useBuiltinCloudTemplate ? cloudResolutions : DEFAULT_CLOUD_RESOLUTIONS;
|
||||
const durationMin = useBuiltinCloudTemplate ? cloudDurationMin : 5;
|
||||
const durationMax = useBuiltinCloudTemplate ? cloudDurationMax : 120;
|
||||
|
||||
useEffect(() => {
|
||||
if (videoBackend !== 'comfy') return;
|
||||
if (isCloudComfy) {
|
||||
setTemplateId((current) => current || BUILTIN_CLOUD_TEMPLATE_VALUE);
|
||||
return;
|
||||
}
|
||||
setTemplateId((current) => current === BUILTIN_CLOUD_TEMPLATE_VALUE ? '' : current);
|
||||
}, [isCloudComfy, videoBackend]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useBuiltinCloudTemplate) return;
|
||||
setDuration((current) => {
|
||||
const numeric = Number.isFinite(current) ? current : cloudDefaultDuration;
|
||||
return Math.max(cloudDurationMin, Math.min(cloudDurationMax, numeric));
|
||||
});
|
||||
setResolution((current) => cloudResolutions.includes(current) ? current : DEFAULT_BUILTIN_CLOUD_RESOLUTION);
|
||||
setRatio((current) => cloudRatios.includes(current) ? current : '9:16');
|
||||
}, [cloudDefaultDuration, cloudDurationMax, cloudDurationMin, cloudRatios, cloudResolutions, useBuiltinCloudTemplate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (useBuiltinCloudTemplate) {
|
||||
@@ -304,6 +359,16 @@ export default function MoneyPrinter() {
|
||||
setSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
if (videoBackend === 'comfy' && !comfyHasTemplate) {
|
||||
throw new Error(isCloudComfy ? '请选择 Cloud 内置模板或一个真实视频工作流模板' : '当前 Comfy 后端需要选择一个真实视频工作流模板');
|
||||
}
|
||||
const nextDuration = useBuiltinCloudTemplate
|
||||
? Math.max(cloudDurationMin, Math.min(cloudDurationMax, Number(duration) || cloudDefaultDuration))
|
||||
: duration;
|
||||
const nextResolution = useBuiltinCloudTemplate && !cloudResolutions.includes(resolution)
|
||||
? DEFAULT_BUILTIN_CLOUD_RESOLUTION
|
||||
: resolution;
|
||||
const nextRatio = useBuiltinCloudTemplate && !cloudRatios.includes(ratio) ? '9:16' : ratio;
|
||||
const payload = await client.createMoneyPrinterJob({
|
||||
topic: topic.trim(),
|
||||
scriptModel: scriptModel.trim(),
|
||||
@@ -311,9 +376,9 @@ export default function MoneyPrinter() {
|
||||
videoModel: videoModel.trim(),
|
||||
style,
|
||||
audience: audienceText,
|
||||
duration,
|
||||
ratio,
|
||||
resolution,
|
||||
duration: nextDuration,
|
||||
ratio: nextRatio,
|
||||
resolution: nextResolution,
|
||||
requirements,
|
||||
language: '中文',
|
||||
...(videoBackend === 'comfy' ? {
|
||||
@@ -366,7 +431,7 @@ export default function MoneyPrinter() {
|
||||
<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-ghost" onClick={() => void refresh()}>刷新</button>
|
||||
<button type="button" className="btn btn-primary" onClick={submit} disabled={!canSubmit}>
|
||||
{submitting ? '提交中...' : '生成短视频'}
|
||||
</button>
|
||||
@@ -410,22 +475,18 @@ export default function MoneyPrinter() {
|
||||
</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))} />
|
||||
<input type="number" min={durationMin} max={durationMax} value={duration} onChange={(event) => setDuration(Number(event.target.value || durationMin))} />
|
||||
</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>
|
||||
{ratioOptions.map((item) => <option key={item} value={item}>{item}</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>
|
||||
{resolutionOptions.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -458,7 +519,8 @@ export default function MoneyPrinter() {
|
||||
<label className="money-printer-field">
|
||||
<span>工作流模板</span>
|
||||
<select value={templateId} onChange={(event) => setTemplateId(event.target.value)}>
|
||||
<option value={BUILTIN_CLOUD_TEMPLATE_VALUE}>内置 Cloud Seedance 文生视频</option>
|
||||
{isCloudComfy && <option value={BUILTIN_CLOUD_TEMPLATE_VALUE}>内置 Cloud Seedance 文生视频</option>}
|
||||
{!isCloudComfy && <option value="">选择真实 Comfy 视频模板</option>}
|
||||
{workflows.map((workflow) => (
|
||||
<option key={workflow.publicId} value={workflow.publicId}>
|
||||
{workflowLooksVideoCapable(workflow) ? '视频 · ' : '模板 · '}{workflowOptionLabel(workflow)}
|
||||
@@ -469,7 +531,10 @@ export default function MoneyPrinter() {
|
||||
{useBuiltinCloudTemplate ? (
|
||||
<div className="money-printer-template-meta">
|
||||
<strong>内置 Cloud Seedance 文生视频</strong>
|
||||
<span>无需导入模板,使用 Comfy Cloud 官方 ByteDanceTextToVideoNode。若账号无余额,会直接显示余额不足。</span>
|
||||
<span>
|
||||
当前后端为 Cloud,使用官方 {status?.builtinCloudTemplate?.nodeType || 'ByteDanceTextToVideoNode'}
|
||||
{status?.builtinCloudTemplate?.model ? ` / ${status.builtinCloudTemplate.model}` : ''}。时长 {cloudDurationMin}-{cloudDurationMax} 秒,若账号无余额会直接显示余额不足。
|
||||
</span>
|
||||
</div>
|
||||
) : selectedWorkflow ? (
|
||||
<div className="money-printer-template-meta">
|
||||
@@ -480,7 +545,12 @@ export default function MoneyPrinter() {
|
||||
{mappingFromGraph(selectedWorkflow.graph) ? '已带节点映射' : '系统会按节点结构自动推断提示词和参数映射'}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="money-printer-template-meta is-warning">
|
||||
<strong>需要视频工作流模板</strong>
|
||||
<span>{isCloudComfy ? '可以使用内置 Cloud 模板,也可以选择模板库里的真实视频工作流。' : '当前不是 Cloud 后端,内置 Seedance 模板不可用,请选择 external/runninghub 可执行的视频模板。'}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost money-printer-advanced-toggle"
|
||||
|
||||
Reference in New Issue
Block a user