Make short video factory use Comfy templates
This commit is contained in:
+177
-15
@@ -14,7 +14,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Map, Value};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const TASK_TYPE_MONEY_PRINTER: &str = "money-printer-video";
|
const TASK_TYPE_MONEY_PRINTER: &str = "money-printer-video";
|
||||||
@@ -585,6 +585,24 @@ fn first_node_by_class(prompt: &serde_json::Map<String, Value>, class_type: &str
|
|||||||
.map(|(id, _)| id.clone())
|
.map(|(id, _)| id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn first_existing_input_field(
|
||||||
|
prompt: &serde_json::Map<String, Value>,
|
||||||
|
node_id: &str,
|
||||||
|
fields: &[&str],
|
||||||
|
fallback: &str,
|
||||||
|
) -> String {
|
||||||
|
let inputs = prompt
|
||||||
|
.get(node_id)
|
||||||
|
.and_then(|node| node.get("inputs"))
|
||||||
|
.and_then(Value::as_object);
|
||||||
|
fields
|
||||||
|
.iter()
|
||||||
|
.find(|field| inputs.is_some_and(|inputs| inputs.contains_key(**field)))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt_contains_class_type(prompt: &Value, class_type: &str) -> bool {
|
fn prompt_contains_class_type(prompt: &Value, class_type: &str) -> bool {
|
||||||
match prompt {
|
match prompt {
|
||||||
Value::Object(object) => {
|
Value::Object(object) => {
|
||||||
@@ -616,10 +634,14 @@ fn infer_comfy_mapping(prompt: &Value) -> Value {
|
|||||||
.or_else(|| first_node_by_class(object, "KSampler"))
|
.or_else(|| first_node_by_class(object, "KSampler"))
|
||||||
.or_else(|| first_node_by_class(object, "EmptyLatentImage"))
|
.or_else(|| first_node_by_class(object, "EmptyLatentImage"))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let prompt_field = first_existing_input_field(object, &prompt_node, &["prompt", "positive", "text"], "text");
|
||||||
|
let ratio_field = first_existing_input_field(object, &video_node, &["ratio", "model.ratio", "aspect_ratio"], "ratio");
|
||||||
|
let resolution_field = first_existing_input_field(object, &video_node, &["model.resolution", "resolution", "size"], "resolution");
|
||||||
|
let duration_field = first_existing_input_field(object, &video_node, &["model.duration", "duration", "length"], "duration");
|
||||||
json!({
|
json!({
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"nodeId": prompt_node,
|
"nodeId": prompt_node,
|
||||||
"field": "text"
|
"field": prompt_field
|
||||||
},
|
},
|
||||||
"negativePrompt": {
|
"negativePrompt": {
|
||||||
"nodeId": negative_node,
|
"nodeId": negative_node,
|
||||||
@@ -627,15 +649,15 @@ fn infer_comfy_mapping(prompt: &Value) -> Value {
|
|||||||
},
|
},
|
||||||
"ratio": {
|
"ratio": {
|
||||||
"nodeId": video_node,
|
"nodeId": video_node,
|
||||||
"field": "ratio"
|
"field": ratio_field
|
||||||
},
|
},
|
||||||
"resolution": {
|
"resolution": {
|
||||||
"nodeId": video_node,
|
"nodeId": video_node,
|
||||||
"field": "resolution"
|
"field": resolution_field
|
||||||
},
|
},
|
||||||
"duration": {
|
"duration": {
|
||||||
"nodeId": video_node,
|
"nodeId": video_node,
|
||||||
"field": "duration"
|
"field": duration_field
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -744,6 +766,47 @@ fn apply_node_input(prompt: &mut Value, node_id: &str, field: &str, value: Value
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn node_has_input(prompt: &Value, node_id: &str, field: &str) -> bool {
|
||||||
|
prompt
|
||||||
|
.get(node_id)
|
||||||
|
.and_then(|node| node.get("inputs"))
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.is_some_and(|inputs| inputs.contains_key(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_optional_node_input(
|
||||||
|
prompt: &mut Value,
|
||||||
|
mapping: &Value,
|
||||||
|
mapping_keys: &[&str],
|
||||||
|
default_field: &str,
|
||||||
|
value: Value,
|
||||||
|
) {
|
||||||
|
let Some(node_id) = mapping_node_id(mapping, mapping_keys) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let field = mapping_field(mapping, mapping_keys, default_field);
|
||||||
|
if !node_has_input(prompt, &node_id, &field) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = apply_node_input(prompt, &node_id, &field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_duration_seconds(value: &Value) -> Option<i64> {
|
||||||
|
value
|
||||||
|
.as_i64()
|
||||||
|
.or_else(|| value.as_str().and_then(|text| text.trim().parse::<i64>().ok()))
|
||||||
|
.map(|duration| duration.clamp(2, 120))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_resolution_for_comfy(value: &Value) -> Value {
|
||||||
|
let text = value.as_str().unwrap_or_default().trim().to_ascii_lowercase();
|
||||||
|
match text.as_str() {
|
||||||
|
"720p" | "720" => json!("720P"),
|
||||||
|
"1080p" | "1080" => json!("1080P"),
|
||||||
|
_ => value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn inject_comfy_video_inputs(prompt: &mut Value, mapping: &Value, body: &Value, plan: &Value) -> AppResult<String> {
|
fn inject_comfy_video_inputs(prompt: &mut Value, mapping: &Value, body: &Value, plan: &Value) -> AppResult<String> {
|
||||||
let video_prompt = string_from(body, &["videoPrompt", "video_prompt"]).unwrap_or_else(|| {
|
let video_prompt = string_from(body, &["videoPrompt", "video_prompt"]).unwrap_or_else(|| {
|
||||||
plan.get("video_prompt")
|
plan.get("video_prompt")
|
||||||
@@ -773,18 +836,117 @@ fn inject_comfy_video_inputs(prompt: &mut Value, mapping: &Value, body: &Value,
|
|||||||
json!(negative),
|
json!(negative),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
for (keys, request_keys, default_field) in [
|
if let Some(value) = ["ratio", "aspectRatio", "aspect_ratio"].iter().find_map(|key| body.get(*key).cloned()) {
|
||||||
(&["ratio", "ratioNode"][..], &["ratio", "aspectRatio", "aspect_ratio"][..], "ratio"),
|
apply_optional_node_input(prompt, mapping, &["ratio", "ratioNode"], "ratio", value);
|
||||||
(&["resolution", "resolutionNode"][..], &["resolution", "size"][..], "resolution"),
|
}
|
||||||
(&["duration", "durationNode"][..], &["duration"][..], "duration"),
|
if let Some(value) = ["resolution", "size"].iter().find_map(|key| body.get(*key).cloned()) {
|
||||||
] {
|
apply_optional_node_input(
|
||||||
if let Some(node_id) = mapping_node_id(mapping, keys) {
|
prompt,
|
||||||
if let Some(value) = request_keys.iter().find_map(|key| body.get(*key).cloned()) {
|
mapping,
|
||||||
apply_node_input(prompt, &node_id, &mapping_field(mapping, keys, default_field), value)?;
|
&["resolution", "resolutionNode"],
|
||||||
}
|
"resolution",
|
||||||
|
normalize_resolution_for_comfy(&value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(value) = ["duration"].iter().find_map(|key| body.get(*key)).and_then(value_to_duration_seconds) {
|
||||||
|
apply_optional_node_input(prompt, mapping, &["duration", "durationNode"], "duration", json!(value));
|
||||||
|
}
|
||||||
|
repair_money_printer_comfy_prompt(prompt);
|
||||||
|
Ok(video_prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repair_money_printer_comfy_prompt(prompt: &mut Value) {
|
||||||
|
repair_resize_image_mask_nodes(prompt);
|
||||||
|
repair_text_generate_ltx2_prompt_nodes(prompt);
|
||||||
|
repair_wan2_api_nodes(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repair_resize_image_mask_nodes(prompt: &mut Value) {
|
||||||
|
let Some(nodes) = prompt.as_object_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (_node_id, node) in nodes.iter_mut() {
|
||||||
|
if node.get("class_type").and_then(Value::as_str) != Some("ResizeImageMaskNode") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(inputs) = node.get_mut("inputs").and_then(Value::as_object_mut) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if inputs
|
||||||
|
.get("resize_type.crop")
|
||||||
|
.is_some_and(|value| !matches!(value.as_str(), Some("disabled" | "center")))
|
||||||
|
{
|
||||||
|
inputs.insert("resize_type.crop".to_string(), Value::String("center".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(video_prompt)
|
}
|
||||||
|
|
||||||
|
fn repair_text_generate_ltx2_prompt_nodes(prompt: &mut Value) {
|
||||||
|
let Some(nodes) = prompt.as_object_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (_node_id, node) in nodes.iter_mut() {
|
||||||
|
if node.get("class_type").and_then(Value::as_str) != Some("TextGenerateLTX2Prompt") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(inputs) = node.get_mut("inputs").and_then(Value::as_object_mut) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if inputs
|
||||||
|
.get("sampling_mode.repetition_penalty")
|
||||||
|
.and_then(Value::as_f64)
|
||||||
|
.is_some_and(|value| !(0.0..=5.0).contains(&value))
|
||||||
|
{
|
||||||
|
inputs.insert("sampling_mode".to_string(), Value::String("on".to_string()));
|
||||||
|
inputs.insert("sampling_mode.temperature".to_string(), json!(0.7));
|
||||||
|
inputs.insert("sampling_mode.top_k".to_string(), json!(64));
|
||||||
|
inputs.insert("sampling_mode.top_p".to_string(), json!(0.95));
|
||||||
|
inputs.insert("sampling_mode.min_p".to_string(), json!(0.05));
|
||||||
|
inputs.insert("sampling_mode.repetition_penalty".to_string(), json!(1.05));
|
||||||
|
inputs.entry("sampling_mode.seed".to_string()).or_insert_with(|| json!(0));
|
||||||
|
inputs.insert("sampling_mode.presence_penalty".to_string(), json!(0.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repair_wan2_api_nodes(prompt: &mut Value) {
|
||||||
|
let Some(nodes) = prompt.as_object_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (_node_id, node) in nodes.iter_mut() {
|
||||||
|
let class_type = node.get("class_type").and_then(Value::as_str).unwrap_or_default();
|
||||||
|
if !class_type.starts_with("Wan2") || !class_type.ends_with("Api") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(inputs) = node.get_mut("inputs").and_then(Value::as_object_mut) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
repair_wan2_model_group(inputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repair_wan2_model_group(inputs: &mut Map<String, Value>) {
|
||||||
|
let model_value = inputs.get("model").cloned().unwrap_or(Value::Null);
|
||||||
|
if let Some(items) = model_value.as_array() {
|
||||||
|
if let Some(model) = items.first().cloned() {
|
||||||
|
inputs.insert("model".to_string(), model);
|
||||||
|
}
|
||||||
|
if let Some(resolution) = items.get(1).cloned() {
|
||||||
|
inputs.insert("model.resolution".to_string(), normalize_resolution_for_comfy(&resolution));
|
||||||
|
}
|
||||||
|
if let Some(duration) = items.get(2).and_then(value_to_duration_seconds) {
|
||||||
|
inputs.insert("model.duration".to_string(), json!(duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inputs
|
||||||
|
.get("model.resolution")
|
||||||
|
.is_some_and(|value| !matches!(value.as_str(), Some("720P" | "1080P")))
|
||||||
|
{
|
||||||
|
inputs.insert("model.resolution".to_string(), json!("1080P"));
|
||||||
|
}
|
||||||
|
if let Some(duration) = inputs.get("model.duration").and_then(value_to_duration_seconds) {
|
||||||
|
inputs.insert("model.duration".to_string(), json!(duration));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn submit_comfy_prompt(state: &AppState, prompt: Value) -> AppResult<(String, Value)> {
|
async fn submit_comfy_prompt(state: &AppState, prompt: Value) -> AppResult<(String, Value)> {
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ function mappingFromGraph(graph: any) {
|
|||||||
|
|
||||||
function inferMappingFromPrompt(prompt: any) {
|
function inferMappingFromPrompt(prompt: any) {
|
||||||
const nodes = prompt && typeof prompt === 'object' && !Array.isArray(prompt) ? Object.entries(prompt as Record<string, 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[] = []) => {
|
const find = (classWords: string[], inputWords: string[] = []) => {
|
||||||
return nodes.find(([, node]) => {
|
return nodes.find(([, node]) => {
|
||||||
const classType = String(node?.class_type || node?.type || '').toLowerCase();
|
const classType = String(node?.class_type || node?.type || '').toLowerCase();
|
||||||
@@ -66,11 +71,11 @@ function inferMappingFromPrompt(prompt: any) {
|
|||||||
const negativeNode = find(['negative'], ['negative']);
|
const negativeNode = find(['negative'], ['negative']);
|
||||||
const videoNode = find(['wan', 'ltx', 'video'], ['duration', 'ratio', 'resolution']);
|
const videoNode = find(['wan', 'ltx', 'video'], ['duration', 'ratio', 'resolution']);
|
||||||
return {
|
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' },
|
negativePrompt: { nodeId: String(negativeNode), field: 'text' },
|
||||||
ratio: { nodeId: String(videoNode), field: 'ratio' },
|
ratio: { nodeId: String(videoNode), field: firstInputField(String(videoNode), ['ratio', 'model.ratio', 'aspect_ratio'], 'ratio') },
|
||||||
resolution: { nodeId: String(videoNode), field: 'resolution' },
|
resolution: { nodeId: String(videoNode), field: firstInputField(String(videoNode), ['model.resolution', 'resolution', 'size'], 'resolution') },
|
||||||
duration: { nodeId: String(videoNode), field: 'duration' },
|
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[]) {
|
function moneyPrinterWorkflows(items: WorkflowSummary[]) {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
return items
|
return items
|
||||||
.filter((workflow) => workflowLooksVideoCapable(workflow))
|
|
||||||
.filter((workflow) => {
|
.filter((workflow) => {
|
||||||
const key = workflow.publicId || String(workflow.id);
|
const key = workflow.publicId || String(workflow.id);
|
||||||
if (seen.has(key)) return false;
|
if (seen.has(key)) return false;
|
||||||
@@ -200,6 +204,9 @@ function moneyPrinterWorkflows(items: WorkflowSummary[]) {
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.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 aTemplate = a.isTemplate || a.visibility === 'template' || a.visibility === 'public';
|
||||||
const bTemplate = b.isTemplate || b.visibility === 'template' || b.visibility === 'public';
|
const bTemplate = b.isTemplate || b.visibility === 'template' || b.visibility === 'public';
|
||||||
if (aTemplate !== bTemplate) return aTemplate ? -1 : 1;
|
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() {
|
export default function MoneyPrinter() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const audience: SessionAudience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
|
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 [topic, setTopic] = useState('帮我生成一个 30 秒的 AI 应用平台 BossAI 商品介绍短视频');
|
||||||
const [scriptModel, setScriptModel] = useState('gpt-5.4');
|
const [scriptModel, setScriptModel] = useState('gpt-5.4');
|
||||||
const [videoModel, setVideoModel] = useState('seedance-1-0-lite-t2v');
|
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 [style, setStyle] = useState(STYLE_PRESETS[0]);
|
||||||
const [audienceText, setAudienceText] = useState('AI 工具用户、内容创作者、企业运营');
|
const [audienceText, setAudienceText] = useState('AI 工具用户、内容创作者、企业运营');
|
||||||
const [duration, setDuration] = useState(30);
|
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 [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 [workflows, setWorkflows] = useState<WorkflowSummary[]>([]);
|
||||||
const [templateId, setTemplateId] = useState('');
|
const [templateId, setTemplateId] = useState('');
|
||||||
const [useAdvancedComfy, setUseAdvancedComfy] = useState(false);
|
const [showAdvancedComfy, setShowAdvancedComfy] = useState(false);
|
||||||
const [jobs, setJobs] = useState<MoneyPrinterJob[]>([]);
|
const [jobs, setJobs] = useState<MoneyPrinterJob[]>([]);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -254,9 +271,10 @@ export default function MoneyPrinter() {
|
|||||||
}, [loadJobs, loadWorkflows]);
|
}, [loadJobs, loadWorkflows]);
|
||||||
|
|
||||||
const selectedWorkflow = workflows.find((item) => item.publicId === templateId) || null;
|
const selectedWorkflow = workflows.find((item) => item.publicId === templateId) || null;
|
||||||
|
const canSubmit = Boolean(topic.trim()) && !submitting && (videoBackend !== 'comfy' || Boolean(templateId));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedWorkflow || videoBackend !== 'comfy' || !useAdvancedComfy) return;
|
if (!selectedWorkflow || videoBackend !== 'comfy') return;
|
||||||
const prompt = workflowPromptFromGraph(selectedWorkflow.graph);
|
const prompt = workflowPromptFromGraph(selectedWorkflow.graph);
|
||||||
if (prompt) setComfyPrompt(JSON.stringify(prompt, null, 2));
|
if (prompt) setComfyPrompt(JSON.stringify(prompt, null, 2));
|
||||||
const mapping = mappingFromGraph(selectedWorkflow.graph);
|
const mapping = mappingFromGraph(selectedWorkflow.graph);
|
||||||
@@ -292,8 +310,8 @@ export default function MoneyPrinter() {
|
|||||||
language: '中文',
|
language: '中文',
|
||||||
...(videoBackend === 'comfy' ? {
|
...(videoBackend === 'comfy' ? {
|
||||||
templateId: templateId || undefined,
|
templateId: templateId || undefined,
|
||||||
comfyPrompt: comfyPrompt.trim() ? JSON.parse(comfyPrompt) : undefined,
|
comfyPrompt: parseJsonInput(comfyPrompt, 'Comfy prompt JSON'),
|
||||||
comfyNodeMapping: comfyNodeMapping.trim() ? JSON.parse(comfyNodeMapping) : undefined,
|
comfyNodeMapping: parseJsonInput(comfyNodeMapping, '节点映射 JSON'),
|
||||||
} : {}),
|
} : {}),
|
||||||
});
|
});
|
||||||
setJobs((prev) => [payload.job, ...prev.filter((item) => item.id !== payload.job.id)]);
|
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);
|
setComfyPrompt(request.comfyPrompt ? JSON.stringify(request.comfyPrompt, null, 2) : comfyPrompt);
|
||||||
setComfyNodeMapping(request.comfyNodeMapping ? JSON.stringify(request.comfyNodeMapping, null, 2) : comfyNodeMapping);
|
setComfyNodeMapping(request.comfyNodeMapping ? JSON.stringify(request.comfyNodeMapping, null, 2) : comfyNodeMapping);
|
||||||
setTemplateId(request.templateId || templateId);
|
setTemplateId(request.templateId || templateId);
|
||||||
setUseAdvancedComfy(Boolean(request.templateId));
|
setShowAdvancedComfy(Boolean(request.comfyPrompt || request.comfyNodeMapping));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -341,7 +359,7 @@ export default function MoneyPrinter() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="page-actions">
|
<div className="page-actions">
|
||||||
<button type="button" className="btn btn-ghost" onClick={() => void loadJobs()}>刷新</button>
|
<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 ? '提交中...' : '生成短视频'}
|
{submitting ? '提交中...' : '生成短视频'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,7 +371,7 @@ export default function MoneyPrinter() {
|
|||||||
<section className="money-printer-panel">
|
<section className="money-printer-panel">
|
||||||
<div className="money-printer-hero">
|
<div className="money-printer-hero">
|
||||||
<span>MoneyPrinterTurbo inside</span>
|
<span>MoneyPrinterTurbo inside</span>
|
||||||
<strong>一句主题,自动拆成脚本、分镜、视频提示词和成片任务。</strong>
|
<strong>一句主题,自动拆成脚本、分镜、视频提示词,再提交到 Comfy API 生成成片。</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="money-printer-field">
|
<label className="money-printer-field">
|
||||||
@@ -375,7 +393,12 @@ export default function MoneyPrinter() {
|
|||||||
</label>
|
</label>
|
||||||
<label className="money-printer-field">
|
<label className="money-printer-field">
|
||||||
<span>视频模型</span>
|
<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>
|
||||||
<label className="money-printer-field">
|
<label className="money-printer-field">
|
||||||
<span>时长</span>
|
<span>时长</span>
|
||||||
@@ -430,7 +453,7 @@ export default function MoneyPrinter() {
|
|||||||
<option value="">请选择模板</option>
|
<option value="">请选择模板</option>
|
||||||
{workflows.map((workflow) => (
|
{workflows.map((workflow) => (
|
||||||
<option key={workflow.publicId} value={workflow.publicId}>
|
<option key={workflow.publicId} value={workflow.publicId}>
|
||||||
{workflowOptionLabel(workflow)}
|
{workflowLooksVideoCapable(workflow) ? '视频 · ' : '模板 · '}{workflowOptionLabel(workflow)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -438,17 +461,32 @@ export default function MoneyPrinter() {
|
|||||||
{selectedWorkflow ? (
|
{selectedWorkflow ? (
|
||||||
<div className="money-printer-template-meta">
|
<div className="money-printer-template-meta">
|
||||||
<strong>{selectedWorkflow.name}</strong>
|
<strong>{selectedWorkflow.name}</strong>
|
||||||
<span>{mappingFromGraph(selectedWorkflow.graph) ? '已带节点映射' : '这个模板没有内置节点映射,系统会尝试自动推断。'}</span>
|
<span>
|
||||||
|
{workflowLooksVideoCapable(selectedWorkflow) ? '已识别为视频工作流' : '未识别视频关键词,请确认这是 Comfy 视频模板'}
|
||||||
|
{' · '}
|
||||||
|
{mappingFromGraph(selectedWorkflow.graph) ? '已带节点映射' : '系统会按节点结构自动推断提示词和参数映射'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<label className="money-printer-field">
|
<button
|
||||||
<span>Comfy prompt JSON</span>
|
type="button"
|
||||||
<textarea value={comfyPrompt} onChange={(event) => setComfyPrompt(event.target.value)} rows={7} placeholder='{"12":{"class_type":"...","inputs":{...}}}' />
|
className="btn btn-ghost money-printer-advanced-toggle"
|
||||||
</label>
|
onClick={() => setShowAdvancedComfy((prev) => !prev)}
|
||||||
<label className="money-printer-field">
|
>
|
||||||
<span>节点映射 JSON</span>
|
{showAdvancedComfy ? '收起模板调试' : '展开模板调试'}
|
||||||
<textarea value={comfyNodeMapping} onChange={(event) => setComfyNodeMapping(event.target.value)} rows={7} />
|
</button>
|
||||||
</label>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user