修改数字人口播板块代码逻辑与排版&国际化配置文件

This commit is contained in:
1152450298 committed 2026-01-14 19:06:25 +08:00
1 parent 13ba5f473a
commit 69ca38c917
5 files changed
+408 -714

No files matched your search

+4 -93
View File
@@ -40,11 +40,10 @@ def render_style_config(pixelle_video):
comfyui_config = config_manager.get_comfyui_config()
tts_config = comfyui_config["tts"]
# 只使用local模式,移除inference_mode选择
tts_mode = "local"
# ================================================================
# Local Mode UI (简化版本)
# Local Mode UI (simple)
# ================================================================
# Import voice configuration
from pixelle_video.tts_voices import EDGE_TTS_VOICES, get_voice_display_name
@@ -99,7 +98,7 @@ def render_style_config(pixelle_video):
st.caption(tr("tts.speed_label", speed=f"{tts_speed:.1f}"))
# ================================================================
# TTS Preview (只支持local模式)
# TTS Preview (only local mode)
# ================================================================
with st.expander(tr("tts.preview_title"), expanded=False):
# Preview text input
@@ -140,98 +139,10 @@ def render_style_config(pixelle_video):
st.error(tr("tts.preview_failed", error=str(e)))
logger.exception(e)
# ====================================================================
# Storyboard Template Section
# ====================================================================
def get_template_preview_path(template_path: str, language: str = "zh_CN") -> str:
"""
Get the preview image path for a template based on language.
Args:
template_path: Template path like "1080x1920/image_default.html"
language: Language code, either "zh_CN" or "en"
Returns:
Path to preview image in docs/images/
"""
# Extract size and template name from path
# e.g., "1080x1920/image_default.html" -> size="1080x1920", name="image_default"
path_parts = template_path.split('/')
if len(path_parts) >= 2:
size = path_parts[0] # e.g., "1080x1920"
template_file = path_parts[1] # e.g., "image_default.html"
template_name = template_file.replace('.html', '') # e.g., "image_default"
# Build preview image path
# Format: docs/images/{size}/{template_name}.jpg or {template_name}_en.jpg
# Chinese uses Chinese preview, all other languages use English preview for better i18n
suffix = "" if language == "zh_CN" else "_en"
# Try different image extensions
for ext in ['.jpg', '.png']:
preview_path = f"docs/images/{size}/{template_name}{suffix}{ext}"
if os.path.exists(preview_path):
return preview_path
# Fallback: try without language suffix (for templates with only one version)
for ext in ['.jpg', '.png']:
preview_path = f"docs/images/{size}/{template_name}{ext}"
if os.path.exists(preview_path):
return preview_path
# If no preview found, return empty string
return ""
# ====================================================================
# Workflow Selection Section (Auto-configured for Digital Human)
# ====================================================================
with st.container(border=True):
st.markdown(f"**{tr('digital_human.section.workflow')}**")
with st.expander(tr("help.feature_description"), expanded=False):
st.markdown(f"**{tr('help.what')}**")
st.markdown(tr("digital_human.workflow.what"))
# Get available workflows
all_workflows = pixelle_video.media.list_workflows()
# Auto-select digital human workflows (check file existence directly)
# First workflow: digital_image (generates collage and copy)
first_workflow = "runninghub/digital_image.json"
first_workflow_path = "workflows/runninghub/digital_image.json"
first_workflow_found = os.path.exists(first_workflow_path)
# Second workflow: digital_combination (generates final video)
second_workflow = "runninghub/digital_combination.json"
second_workflow_path = "workflows/runninghub/digital_combination.json"
second_workflow_found = os.path.exists(second_workflow_path)
# First workflow status
if first_workflow_found:
st.success(f"✅ {tr('digital_human.workflow.first_step')}")
else:
st.error(f"❌ {tr('digital_human.workflow.first_step')} {tr('digital_human.workflow.not_found')}")
# Second workflow status
if second_workflow_found:
st.success(f"✅ {tr('digital_human.workflow.second_step')}")
else:
st.error(f"❌ {tr('digital_human.workflow.second_step')} {tr('digital_human.workflow.not_found')}")
# Show workflow info
if first_workflow_found and second_workflow_found:
st.info(f"🎯 {tr('digital_human.workflow.ready')}")
else:
st.warning(f"⚠️ {tr('digital_human.workflow.missing')}")
# Return all style configuration parameters (简化版本,只支持local TTS)
# Return all style configuration parameters (Simplified version only local TTS)
return {
"tts_inference_mode": "local",
"tts_voice": selected_voice,
"tts_speed": tts_speed,
"first_workflow": first_workflow,
"second_workflow": second_workflow
"tts_speed": tts_speed
}
+231 -230
View File
@@ -1,12 +1,12 @@
{
"language_name": "English (US)",
"language_name": "English",
"t": {
"app.title": "⚡ Pixelle-Video - AI Fully Automatic Short Video Engine",
"app.title": "⚡ Pixelle-Video - AI Auto Short Video Engine",
"app.subtitle": "Powered by Pixelle.AI",
"section.content_input": "📝 Video Script",
"section.bgm": "🎵 Background Music",
"section.tts": "🎤 Voiceover Synthesis",
"section.image": "🎨 Illustration Generation",
"section.tts": "🎤 Voiceover",
"section.image": "🎨 Image Generation",
"section.video": "🎬 Video Generation",
"section.media": "🎨 Media Generation",
"section.template": "📐 Storyboard Template",
@@ -14,48 +14,48 @@
"input_mode.topic": "💡 Topic",
"input_mode.custom": "✍️ Custom Content",
"mode.generate": "💡 AI Creation",
"mode.fixed": "✍️ Manual Creation",
"mode.digital": "💻 E-commerce Mode",
"mode.customize": "🧐 Custom Mode",
"mode.fixed": "✍️ Custom Script",
"mode.digital": "💻 Product placement mode",
"mode.customize": "🧐 Custom mode",
"input.topic": "Topic",
"input.topic_placeholder": "AI automatically creates a specified number of narrations\nE.g.: How to build passive income, 如何增加被动收入",
"input.topic_help": "Enter a topic, AI will generate content accordingly",
"input.topic_placeholder": "AI automatically creates specified number of narrations\nExample: How to build passive income, 如何增加被动收入",
"input.topic_help": "Enter a topic, AI will generate content based on it",
"input.text": "Text Input",
"input.text_help_generate": "Enter a topic or idea (AI will create the script)",
"input.text_help_fixed": "Enter a complete narration script (used as is, no rewrite)",
"input.text_help_digital": "Enter product description; if provided, product name is not required",
"input.text_help_audio": "Enter a prompt, describing in detail the visual details you want",
"split.mode_label": "Split Method",
"split.mode_help": "Choose how to split text into video segments",
"input.text_help_generate": "Enter topic or theme (AI will create narrations)",
"input.text_help_fixed": "Enter complete narration script (used directly without modification)",
"input.text_help_digital": "Enter product description. If a product description is provided, you do not need to enter the product name.",
"input.text_help_audio": "Enter prompts/descriptions, describing the desired visual content in as much detail as possible.",
"split.mode_label": "Split Strategy",
"split.mode_help": "Choose how to split the text into video segments",
"split.mode_paragraph": "📄 By Paragraph (\\n\\n)",
"split.mode_line": "📝 By Line (\\n)",
"split.mode_sentence": "✂️ By Period (.!?)",
"split.mode_sentence": "✂️ By Sentence (。.!?)",
"input.content": "Content",
"input.content_placeholder": "Used directly, no rewrite (split by the method below)\nE.g.:\nHello everyone, here are three study tips.\n\nFirst: Focus training, 10 minutes meditation daily.\n\nSecond: Active recall, repeat immediately after learning.",
"input.content_placeholder": "Used directly without modification (split by strategy below)\nExample:\nHello everyone, today I'll share three study tips.\n\nThe first tip is focus training, meditate for 10 minutes daily.\n\nThe second tip is active recall, review immediately after learning.",
"input.content_help": "Provide your own content for video generation",
"input.title": "Title (Optional)",
"input.title_placeholder": "Video title (leave blank for auto-generation)",
"input.title_help": "Optional: Custom video title",
"input.title_placeholder": "Video title (auto-generated if empty)",
"input.title_help": "Optional: Custom title for the video",
"voice.title": "🎤 Voice Selection",
"voice.male_professional": "🎤 Male - Professional",
"voice.male_young": "🎙️ Male - Young",
"voice.female_gentle": "🎵 Female - Gentle",
"voice.female_energetic": "🎶 Female - Energetic",
"voice.preview": "▶ Voice Preview",
"voice.male_professional": "🎤 Male-Professional",
"voice.male_young": "🎙️ Male-Young",
"voice.female_gentle": "🎵 Female-Gentle",
"voice.female_energetic": "🎶 Female-Energetic",
"voice.preview": "▶ Preview Voice",
"voice.previewing": "Generating voice preview...",
"voice.preview_failed": "Preview failed: {error}",
"style.workflow": "Workflow Selection",
"style.workflow_what": "Determines how each illustration in the video is generated (e.g. FLUX, SD model)",
"style.workflow_how": "Place exported image_xxx.json workflow files (API format) in workflows/selfhost/ (local ComfyUI) or workflows/runninghub/ (cloud)",
"style.video_workflow_what": "Determines how each video segment is generated (e.g. using different video generation models)",
"style.video_workflow_how": "Place exported video_xxx.json workflow files (API format) in workflows/selfhost/ (local ComfyUI) or workflows/runninghub/ (cloud)",
"style.image_size_info": "Illustration size: {width}x{height} (set automatically by template)",
"style.video_size_info": "Video size: {width}x{height} (set automatically by template)",
"style.workflow_what": "Determines how each frame's illustration is generated and its effect (e.g., using FLUX, SD models)",
"style.workflow_how": "Place the exported image_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)",
"style.video_workflow_what": "Determines how each frame's video clip is generated and its effect (e.g., using different video generation models)",
"style.video_workflow_how": "Place the exported video_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)",
"style.image_size_info": "Image Size: {width}x{height} (auto-determined by template)",
"style.video_size_info": "Video Size: {width}x{height} (auto-determined by template)",
"style.prompt_prefix": "Prompt Prefix",
"style.prompt_prefix_what": "Automatically added to all image prompts for unified illustration style (e.g., cartoon, photo-realistic, etc.)",
"style.prompt_prefix_how": "Enter style description below. To save permanently, edit config.yaml file",
"style.prompt_prefix_placeholder": "Enter style prefix (leave blank for config default)",
"style.prompt_prefix_help": "This will be added to all image prompts. To change permanently, edit config.yaml",
"style.prompt_prefix_what": "Automatically added before all image prompts to control the illustration style uniformly (e.g., cartoon, realistic)",
"style.prompt_prefix_how": "Enter style description in the input box below. To save permanently, edit the config.yaml file",
"style.prompt_prefix_placeholder": "Enter style prefix (leave empty for config default)",
"style.prompt_prefix_help": "This text will be automatically added before all image generation prompts. To permanently change, edit config.yaml",
"style.custom": "Custom",
"style.description": "Style Description",
"style.description_placeholder": "Describe the illustration style you want (any language)...",
@@ -63,7 +63,7 @@
"style.video_preview_title": "Preview Video",
"style.test_prompt": "Test Prompt",
"style.test_video_prompt": "Test Video Prompt",
"style.test_prompt_help": "Enter a test prompt to preview style effect",
"style.test_prompt_help": "Enter test prompt to preview style effect",
"style.preview": "🖼️ Generate Preview",
"style.video_preview": "🎬 Generate Video Preview",
"style.previewing": "Generating style preview...",
@@ -74,25 +74,25 @@
"style.preview_failed": "Preview failed: {error}",
"style.preview_failed_general": "Failed to generate preview image",
"style.final_prompt_label": "Final Prompt",
"style.generated_prompt": "Generated Prompt: {prompt}",
"style.generated_prompt": "Generated prompt: {prompt}",
"template.selector": "Template Selection",
"template.select": "Select Template",
"template.select_help": "Select template and video size",
"template.video_size_info": "Final video size: {width} × {height}",
"template.separator_selected": "Please select a specific template, not group header",
"template.video_size_info": "Final Video Size: {width} × {height}",
"template.separator_selected": "Please select a specific template, not the group header",
"template.default": "Default",
"template.modern": "Modern",
"template.neon": "Neon",
"template.what": "Controls each video frame’s visual layout and design style (title, text, image arrangement, etc.)",
"template.how": "Place .html template files in templates/Size/ directory (e.g. templates/1080x1920/), and the system will auto-group by size. Custom CSS supported.\n\n**Template Naming Rules**\n\n- `static_*.html` → Static style (no AI media gen)\n- `image_*.html` → Illustration gen (AI images)\n- `video_*.html` → Video gen (AI videos)\n\n**Note**\n\nYou must install at least one of the following browsers:\n1. Google Chrome (Windows/MacOS)\n2. Chromium (Linux)\n3. Microsoft Edge",
"template.what": "Controls the visual layout and design style of each frame (title, text, image arrangement)",
"template.how": "Place .html template files in templates/SIZE/ directories (e.g., templates/1080x1920/). Templates are automatically grouped by size. Custom CSS styles are supported.\n\n**Template Naming Convention**\n\n- `static_*.html` → Static style templates (no AI-generated media)\n- `image_*.html` → Image generation templates (AI-generated images)\n- `video_*.html` → Video generation templates (AI-generated videos)\n\n**Note**\n\nAt least one of the following browsers must be installed on your computer for proper operation:\n1. Google Chrome (Windows, macOS)\n2. Chromium Browser (Linux)\n3. Microsoft Edge",
"template.size_info": "Template Size",
"template.type_selector": "Storyboard Type",
"template.type_selector": "Template Type",
"template.type.static": "📄 Static Style",
"template.type.image": "🖼️ Illustration Generation",
"template.type.video": "🎬 Video Generation",
"template.type.static_hint": "Uses built-in template style, does not require AI media generation. Background images etc. can be customized.",
"template.type.image_hint": "AI auto-generates illustrations to match the script; size is set by template.",
"template.type.video_hint": "AI auto-generates video clips to match the script; size is set by template.",
"template.type.image": "🖼️ Generate Images",
"template.type.video": "🎬 Generate Videos",
"template.type.static_hint": "Uses template's built-in styles, no AI-generated media required. You can customize background images and other parameters in the template.",
"template.type.image_hint": "AI automatically generates illustrations matching the narration content. Image size is determined by the template.",
"template.type.video_hint": "AI automatically generates video clips matching the narration content. Video size is determined by the template.",
"orientation.portrait": "Portrait",
"orientation.landscape": "Landscape",
"orientation.square": "Square",
@@ -103,7 +103,7 @@
"template.preview_param_width": "Width",
"template.preview_param_height": "Height",
"template.preview_default_title": "AI Changes Content Creation",
"template.preview_default_text": "Pixelle.AI is changing content creation with AI, letting everyone easily create professional videos.",
"template.preview_default_text": "Artificial intelligence is transforming the way Pixelle.AI creates content, making it easy for everyone to produce professional-grade videos.",
"template.preview_button": "🖼️ Generate Preview",
"template.preview_generating": "Generating template preview...",
"template.preview_success": "✅ Preview generated successfully!",
@@ -111,26 +111,26 @@
"template.preview_image_help": "Supports local path or URL",
"template.preview_caption": "Template Preview: {template}",
"template.custom_parameters": "Custom Parameters",
"template.gallery_view": "Template Library",
"template.select_button": "Select",
"template.selected": "Selected",
"template.gallery_view": "Template Gallery",
"template.select_button": "Check",
"template.selected": "Checked",
"template.selected_template": "Current Template",
"template.no_templates_with_preview": "⚠️ No templates available for this type",
"image.not_required": "The current template does not require image generation",
"image.not_required_hint": "You selected a text-only template. This will: ⚡ Speed up generation 💰 Lower generation cost",
"image.not_required": "Current template does not require image generation",
"image.not_required_hint": "The selected template is text-only and does not need images. Benefits: ⚡ Faster generation 💰 Lower cost",
"video.title": "🎬 Video Settings",
"video.frames": "Storyboards Count",
"video.frames_help": "More storyboards = longer video",
"video.frames_label": "Storyboards: {n}",
"video.frames_fixed_mode_hint": "💡 Fixed mode: storyboard count determined by script paragraphs",
"video.frames": "Scenes",
"video.frames_help": "More scenes = longer video",
"video.frames_label": "Scenes: {n}",
"video.frames_fixed_mode_hint": "💡 Fixed mode: scene count is determined by actual script segments",
"bgm.selector": "Music Selection",
"bgm.none": "🔇 No Background Music",
"bgm.none": "🔇 No BGM",
"bgm.volume": "Volume",
"bgm.volume_help": "Adjust BGM volume (0.0 = mute, 1.0 = original)",
"bgm.volume_help": "Adjust background music volume (0.0 = muted, 1.0 = original volume)",
"bgm.preview": "▶ Preview Music",
"bgm.preview_failed": "❌ Music file not found: {file}",
"bgm.what": "Add BGM to make the video more atmospheric and professional",
"bgm.how": "Place audio files (MP3/WAV/FLAC etc.) in the bgm/ folder for auto detection",
"bgm.what": "Adds background music to your video, making it more atmospheric and professional",
"bgm.how": "Place audio files (MP3/WAV/FLAC, etc.) in the bgm/ folder for automatic detection",
"btn.generate": "🎬 Generate Video",
"btn.save_config": "💾 Save Configuration",
"btn.reset_config": "🔄 Reset to Default",
@@ -142,9 +142,9 @@
"status.error": "❌ Generation failed: {error}",
"status.video_generated": "✅ Video generated: {path}",
"status.video_not_found": "Video file not found: {path}",
"status.config_saved": "✅ Config saved",
"status.config_reset": "✅ Config reset to default",
"status.llm_config_incomplete": "⚠️ LLM configuration incomplete. Please fill in API Key, Base URL, and Model",
"status.config_saved": "✅ Configuration saved",
"status.config_reset": "✅ Configuration reset to defaults",
"status.llm_config_incomplete": "⚠️ LLM configuration incomplete, please fill in API Key, Base URL and Model",
"status.save_failed": "Save failed",
"status.connection_success": "✅ Connection successful",
"status.connection_failed": "❌ Connection failed",
@@ -153,34 +153,35 @@
"progress.splitting_script": "Splitting script...",
"progress.generating_image_prompts": "Generating image prompts...",
"progress.generating_video_prompts": "Generating video prompts...",
"progress.preparing_frames": "Preparing storyboards...",
"progress.frame": "Storyboard {current}/{total}",
"progress.frame_step": "Storyboard {current}/{total} - Step {step}/4: {action}",
"progress.processing_frame": "Processing storyboard {current}/{total}...",
"progress.step_audio": "Generate audio",
"progress.step_image": "Generate illustration",
"progress.step_media": "Generate media",
"progress.step_compose": "Compose scene",
"progress.step_video": "Create video segment",
"progress.preparing_frames": "Preparing frames...",
"progress.frame": "Frame {current}/{total}",
"progress.frame_step": "Frame {current}/{total} - Step {step}/4: {action}",
"progress.processing_frame": "Processing frame {current}/{total}...",
"progress.step_audio": "Generating audio",
"progress.step_image": "Generating image",
"progress.step_media": "Generating media",
"progress.step_compose": "Composing frame",
"progress.step_video": "Creating video segment",
"progress.concatenating": "Concatenating video...",
"progress.finalizing": "Finalizing...",
"progress.completed": "✅ Generation completed",
"error.input_required": "❌ Please input a topic or content",
"progress.generation": "Video is being synthesized...",
"progress.completed": "✅ Completed",
"error.input_required": "❌ Please provide topic or content",
"error.api_key_required": "❌ Please enter API Key",
"error.missing_field": "Please fill in {field}",
"error.missing_field": "Please enter {field}",
"info.duration": "Duration",
"info.file_size": "File Size",
"info.frames": "Storyboards",
"info.scenes_unit": "Storyboard",
"info.frames": "Scenes",
"info.scenes_unit": " scenes",
"info.resolution": "Resolution",
"info.video_information": "📊 Video Info",
"info.no_video_yet": "Preview will show here after video is generated",
"info.video_information": "📊 Video Information",
"info.no_video_yet": "Video preview will appear here after generation",
"info.generation_time": "Generation Time",
"settings.title": "⚙️ System Configuration (Required)",
"settings.not_configured": "⚠️ Please complete system config before generating video",
"settings.not_configured": "⚠️ Please complete system configuration before generating videos",
"settings.llm.title": "🤖 Large Language Model",
"settings.llm.quick_select": "Quick Select",
"settings.llm.quick_select_help": "Choose preset LLM or custom config",
"settings.llm.quick_select_help": "Choose a preset LLM or custom configuration",
"settings.llm.get_api_key": "Get API Key",
"settings.llm.api_key": "API Key",
"settings.llm.api_key_help": "Enter your API Key",
@@ -188,17 +189,17 @@
"settings.llm.base_url_help": "API service address",
"settings.llm.model": "Model",
"settings.llm.model_help": "Model name",
"settings.comfyui.title": "🔧 ComfyUI Config",
"settings.comfyui.local_title": "Local/Self-built ComfyUI",
"settings.comfyui.title": "🔧 ComfyUI Configuration",
"settings.comfyui.local_title": "Local/Self-hosted ComfyUI",
"settings.comfyui.cloud_title": "RunningHub Cloud",
"settings.comfyui.comfyui_url": "ComfyUI Server Address",
"settings.comfyui.comfyui_url": "ComfyUI Server URL",
"settings.comfyui.comfyui_url_help": "Local or remote ComfyUI server address",
"settings.comfyui.comfyui_api_key": "ComfyUI API Key",
"settings.comfyui.comfyui_api_key_help": "Optional. Visit https://platform.comfy.org/profile/api-keys to get",
"settings.comfyui.comfyui_api_key_help": "Optional, get from https://platform.comfy.org/profile/api-keys",
"settings.comfyui.runninghub_api_key": "RunningHub API Key",
"settings.comfyui.runninghub_api_key_help": "Register and get at https://runninghub.ai",
"settings.comfyui.runninghub_api_key_help": "Visit https://runninghub.ai to register and get API Key",
"settings.comfyui.runninghub_hint": "No local ComfyUI? Use RunningHub Cloud:",
"settings.comfyui.runninghub_get_api_key": "Click here to get RunningHub API Key",
"settings.comfyui.runninghub_get_api_key": "Get RunningHub API Key",
"settings.comfyui.runninghub_concurrent_limit": "Concurrent Limit",
"settings.comfyui.runninghub_concurrent_limit_help": "RunningHub concurrent execution limit (1-10), default is 1 for regular members, adjust based on your membership level",
"settings.comfyui.runninghub_instance_type": "Machine Spec",
@@ -208,62 +209,62 @@
"tts.inference_mode": "Synthesis Mode",
"tts.mode.local": "Local Synthesis",
"tts.mode.comfyui": "ComfyUI Synthesis",
"tts.mode.local_hint": "💡 Uses Edge TTS, no config needed, plug-and-play (ensure internet access)",
"tts.mode.comfyui_hint": "⚙️ Uses ComfyUI workflow, flexible and powerful",
"tts.mode.local_hint": "💡 Using Edge TTS, no configuration required, ready to use",
"tts.mode.comfyui_hint": "⚙️ Using ComfyUI workflows, flexible and powerful",
"tts.voice_selector": "Voice Selection",
"tts.speed": "Speed",
"tts.speed_label": "{speed}x",
"tts.voice.zh_CN_XiaoxiaoNeural": "Female - Gentle (Xiaoxiao)",
"tts.voice.zh_CN_XiaoyiNeural": "Female - Sweet (Xiaoyi)",
"tts.voice.zh_CN_YunjianNeural": "Male - Professional (Yunjian)",
"tts.voice.zh_CN_YunxiNeural": "Male - Deep (Yunxi)",
"tts.voice.zh_CN_YunyangNeural": "Male - News (Yunyang)",
"tts.voice.zh_CN_YunyeNeural": "Male - Natural (Yunye)",
"tts.voice.zh_CN_YunfengNeural": "Male - Calm (Yunfeng)",
"tts.voice.zh_CN_liaoning_XiaobeiNeural": "Female - Northeast (Xiaobei)",
"tts.voice.en_US_AriaNeural": "Female - Natural (Aria)",
"tts.voice.en_US_JennyNeural": "Female - Warm (Jenny)",
"tts.voice.en_US_GuyNeural": "Male - Standard (Guy)",
"tts.voice.en_US_DavisNeural": "Male - Friendly (Davis)",
"tts.voice.en_GB_SoniaNeural": "Female - British (Sonia)",
"tts.voice.en_GB_RyanNeural": "Male - British (Ryan)",
"tts.voice.ko-KR-InJoonNeural": "KR Male - Friendly (InJoon)",
"tts.voice.ko-KR-SunHiNeural": "KR Female - Friendly (SunHi)",
"tts.voice.fr-FR-EloiseNeural": "FR Female - Friendly (Eloise)",
"tts.voice.fr-FR-HenriNeural": "FR Male - Friendly (Henri)",
"tts.voice.pt-PT-DuarteNeural": "PT Male - Friendly (Duarte)",
"tts.voice.pt-PT-RaquelNeural": "PT Female - Friendly (Raquel)",
"tts.voice.de-DE-AmalaNeural": "DE Female - Friendly (Amala)",
"tts.voice.de-DE-ConradNeural": "DE Male - Friendly (Conrad)",
"tts.voice.ru-RU-DmitryNeural": "RU Male - Friendly (Dmitry)",
"tts.voice.ru-RU-SvetlanaNeural": "RU Female - Friendly (Svetlana)",
"tts.voice.tr-TR-AhmetNeural": "TR Male - Friendly (Ahmet)",
"tts.voice.tr-TR-EmelNeural": "TR Female - Friendly (Emel)",
"tts.voice.es-ES-AlvaroNeural": "ES Male - Friendly (Alvaro)",
"tts.voice.es-ES-ElviraNeural": "ES Female - Friendly (Elvira)",
"tts.voice.zh_CN_XiaoxiaoNeural": "zh-CN-XiaoxiaoNeural",
"tts.voice.zh_CN_XiaoyiNeural": "zh-CN-XiaoyiNeural",
"tts.voice.zh_CN_YunjianNeural": "zh-CN-YunjianNeural",
"tts.voice.zh_CN_YunxiNeural": "zh-CN-YunxiNeural",
"tts.voice.zh_CN_YunyangNeural": "zh-CN-YunyangNeural",
"tts.voice.zh_CN_YunyeNeural": "zh-CN-YunyeNeural",
"tts.voice.zh_CN_YunfengNeural": "zh-CN-YunfengNeural",
"tts.voice.zh_CN_liaoning_XiaobeiNeural": "zh-CN-liaoning-XiaobeiNeural",
"tts.voice.en_US_AriaNeural": "en-US-AriaNeural",
"tts.voice.en_US_JennyNeural": "en-US-JennyNeural",
"tts.voice.en_US_GuyNeural": "en-US-GuyNeural",
"tts.voice.en_US_DavisNeural": "en-US-DavisNeural",
"tts.voice.en_GB_SoniaNeural": "en-GB-SoniaNeural",
"tts.voice.en_GB_RyanNeural": "en-GB-RyanNeural",
"tts.voice.ko-KR-InJoonNeural": "KR Male-Friendly(InJoon)",
"tts.voice.ko-KR-SunHiNeural": "KR Female-Friendly(SunHi)",
"tts.voice.fr-FR-EloiseNeural": "FR Female-Friendly(Eloise)",
"tts.voice.fr-FR-HenriNeural": "FR Male-Friendly(Henri)",
"tts.voice.pt-PT-DuarteNeural": "PT Male-Friendly(Duarte)",
"tts.voice.pt-PT-RaquelNeural": "PT Female-Friendly(Raquel)",
"tts.voice.de-DE-AmalaNeural": "DE Female-Friendly(Amala)",
"tts.voice.de-DE-ConradNeural": "DE Male-Friendly(Conrad)",
"tts.voice.ru-RU-DmitryNeural": "RU Male-Friendly(Dmitry)",
"tts.voice.ru-RU-SvetlanaNeural": "RU Female-Friendly(Svetlana)",
"tts.voice.tr-TR-AhmetNeural": "TR Male-Friendly(Ahmet)",
"tts.voice.tr-TR-EmelNeural": "TR Female-Friendly(Emel)",
"tts.voice.es-ES-AlvaroNeural": "ES Male-Friendly(Alvaro)",
"tts.voice.es-ES-ElviraNeural": "ES Female-Friendly(Elvira)",
"tts.selector": "Workflow Selection",
"tts.what": "Convert narration text to life-like human speech (some workflows support reference audio for voice cloning)",
"tts.how": "Place exported tts_xxx.json workflow files (API format) in workflows/selfhost/ (local ComfyUI) or workflows/runninghub/ (cloud)",
"tts.what": "Converts narration text to natural human-like speech (some workflows support reference audio for voice cloning)",
"tts.how": "Place the exported tts_xxx.json workflow file(API format) into the workflows/selfhost/ folder (for local ComfyUI) or the workflows/runninghub/ folder (for cloud)",
"tts.ref_audio": "Reference Audio",
"tts.ref_audio_help": "Upload audio for voice cloning (only supported in some workflows)",
"tts.ref_audio_help": "Upload audio file for voice cloning (only supported by some workflows)",
"tts.preview_title": "Preview TTS",
"tts.preview_text": "Preview Text",
"tts.preview_text_placeholder": "Enter text to preview audio...",
"tts.preview_text_placeholder": "Enter text to preview...",
"tts.preview_button": "🔊 Generate Preview",
"tts.previewing": "Generating TTS preview...",
"tts.preview_success": "✅ Preview generated successfully!",
"tts.preview_failed": "❌ Preview failed: {error}",
"welcome.first_time": "🎉 Welcome to Pixelle-Video! Please complete basic configuration",
"welcome.config_hint": "💡 On first use, you need to configure the API Key; later it can be changed in Advanced Settings",
"wizard.llm_required": "🤖 LLM Configuration (Required)",
"wizard.image_optional": "🎨 Image Generation Config (Optional)",
"wizard.image_hint": "💡 If image generation is not configured, default template (no AI images) will be used",
"welcome.config_hint": "💡 First-time setup requires API Key configuration, you can modify it in advanced settings later",
"wizard.llm_required": "🤖 Large Language Model Configuration (Required)",
"wizard.image_optional": "🎨 Image Generation Configuration (Optional)",
"wizard.image_hint": "💡 If not configured, default template will be used (no AI image generation)",
"wizard.configure_image": "Configure Image Generation (Recommended)",
"label.required": "(Required)",
"label.optional": "(Optional)",
"help.feature_description": "💡 Feature Description",
"help.what": "Function",
"help.how": "How to customize",
"help.what": "Purpose",
"help.how": "Customization",
"language.select": "🌐 Language",
"version.title": "📦 Version Info",
"version.current": "Current Version",
@@ -274,44 +275,44 @@
"history.failed_count": "Failed",
"history.total_duration": "Total Duration",
"history.total_size": "Total Size",
"history.filter_status": "Status Filter",
"history.filter_status": "Filter Status",
"history.status_all": "All",
"history.status_completed": "Completed",
"history.status_failed": "Failed",
"history.status_running": "Running",
"history.status_pending": "Pending",
"history.sort_by": "Sort By",
"history.sort_created_at": "Creation Time",
"history.sort_completed_at": "Completion Time",
"history.sort_created_at": "Created Time",
"history.sort_completed_at": "Completed Time",
"history.sort_title": "Title",
"history.sort_duration": "Duration",
"history.sort_order_desc": "Descending",
"history.sort_order_asc": "Ascending",
"history.page_size": "Items Per Page",
"history.page_size": "Page Size",
"history.no_tasks": "No tasks yet",
"history.task_card.title": "Title",
"history.task_card.created_at": "Created At",
"history.task_card.created_at": "Created",
"history.task_card.duration": "Duration",
"history.task_card.frames": "Storyboards",
"history.task_card.view_detail": "View Details",
"history.task_card.duplicate": "Duplicate Params",
"history.task_card.frames": "Frames",
"history.task_card.view_detail": "View Detail",
"history.task_card.duplicate": "Duplicate",
"history.task_card.delete": "Delete",
"history.task_card.download": "Download Video",
"history.task_card.download": "Download",
"history.task_card.status_completed": "✅ Completed",
"history.task_card.status_failed": "❌ Failed",
"history.task_card.status_running": "⏳ Running",
"history.task_card.status_pending": "⏸️ Pending",
"history.detail.modal_title": "Task Details",
"history.detail.modal_title": "Task Detail",
"history.detail.task_id": "Task ID",
"history.detail.input_params": "Input Params",
"history.detail.input_params": "Input Parameters",
"history.detail.text": "Text",
"history.detail.mode": "Mode",
"history.detail.n_scenes": "Storyboards",
"history.detail.n_scenes": "Scenes",
"history.detail.tts_mode": "TTS Mode",
"history.detail.voice": "Voice",
"history.detail.storyboard": "Storyboard",
"history.detail.frame_index": "Storyboard {index}",
"history.detail.frame": "Storyboard",
"history.detail.frame_index": "Frame {index}",
"history.detail.frame": "Frame",
"history.detail.download_video": "Download Video",
"history.detail.narration": "Narration",
"history.detail.image_prompt": "Image Prompt",
@@ -319,139 +320,139 @@
"history.detail.image_path": "Image",
"history.detail.video_segment_path": "Video Segment",
"history.detail.close": "Close",
"history.action.duplicate_success": "✅ Params copied, redirecting to home...",
"history.action.duplicate_failed": "❌ Copy failed: {error}",
"history.action.delete_confirm": "Are you sure to delete this task? This action cannot be undone!",
"history.action.duplicate_success": "✅ Parameters duplicated, redirecting...",
"history.action.duplicate_failed": "❌ Duplication failed: {error}",
"history.action.delete_confirm": "Confirm deletion? This action cannot be undone!",
"history.action.delete_success": "✅ Task deleted",
"history.action.delete_failed": "❌ Deletion failed: {error}",
"history.page_info": "Page {page} / {total_pages}",
"batch.mode_label": "🔢 Batch Generation Mode",
"batch.mode_help": "Generate multiple videos in batch, one topic per line",
"batch.section_title": "Batch Topic Input",
"batch.mode_help": "Generate multiple videos, one topic per line",
"batch.section_title": "Batch Topics Input",
"batch.section_generation": "📦 Batch Video Generation",
"batch.rules_title": "Batch Generation Rules",
"batch.rule_1": "Automatically uses 'AI Content Generation' mode",
"batch.rule_1": "Automatically use 'AI Generate Content' mode",
"batch.rule_2": "Enter one topic per line",
"batch.rule_3": "All videos use the same settings (TTS, template, workflow, etc.)",
"batch.rule_3": "All videos use the same configuration (TTS, template, workflow, etc.)",
"batch.topics_label": "Batch Topics (one per line)",
"batch.topics_placeholder": "Why develop a reading habit\nHow to manage time efficiently\n5 secrets for healthy living\nBenefits of getting up early\nHow to overcome procrastination\nTips to stay focused\nMethods for emotion management\nTips to boost memory\nBuilding good relationships\nBasics of wealth management",
"batch.topics_help": "One topic per line. AI will auto-generate scripts by topic",
"batch.count_success": "✅ Found {count} topics",
"batch.count_error": "❌ Batch count exceeds limit (max 100), current: {count}",
"batch.topics_placeholder": "Why develop a reading habit\nHow to manage time efficiently\n5 secrets to healthy living\nBenefits of waking up early\nHow to overcome procrastination\nTechniques to stay focused\nEmotional management methods\nTips to improve memory\nBuilding good relationships\nWealth management basics",
"batch.topics_help": "One video topic per line, AI will generate content based on the topic",
"batch.count_success": "✅ Detected {count} topics",
"batch.count_error": "❌ Batch size exceeds limit (max 100), current: {count}",
"batch.preview_title": "📋 Preview Topic List",
"batch.title_prefix_label": "Title Prefix (Optional)",
"batch.title_prefix_placeholder": "E.g.: Knowledge Sharing",
"batch.title_prefix_help": "Final title format: {prefix} - {topic}, e.g.: Knowledge Sharing - Why develop a reading habit",
"batch.n_scenes_label": "Storyboards Count (applies to all videos)",
"batch.n_scenes_help": "Number of storyboards per video, same for all",
"batch.n_scenes_caption": "Storyboards: {n}",
"batch.config_info": "Other settings: TTS, video template, image workflow etc. will use sidebar settings, same for all videos",
"batch.no_topics": "⚠️ Please input batch topics on the left, one per line",
"batch.prepare_info": "📊 Preparing to generate {count} videos (with unified config)",
"batch.estimated_time": "⏱️ Estimated total time: about {minutes} min",
"batch.generate_button": "🚀 Generate {count} videos in batch",
"batch.generate_help": "⚠️ Please keep the page open during batch generation. Do not close the browser.",
"batch.overall_progress": "Total Progress",
"batch.title_prefix_label": "Title Prefix (optional)",
"batch.title_prefix_placeholder": "e.g., Knowledge Sharing",
"batch.title_prefix_help": "Final title format: {prefix} - {topic}, e.g., Knowledge Sharing - Why develop a reading habit",
"batch.n_scenes_label": "Scenes (unified for all videos)",
"batch.n_scenes_help": "Number of scenes per video, same setting for all videos",
"batch.n_scenes_caption": "Scenes: {n}",
"batch.config_info": "Other configurations: TTS voice, video template, image workflow, etc. will use the settings from the right panel, unified for all videos",
"batch.no_topics": "⚠️ Please enter batch topics on the left (one per line)",
"batch.prepare_info": "📊 Ready to generate {count} videos (using same configuration)",
"batch.estimated_time": "⏱️ Estimated time: about {minutes} minutes",
"batch.generate_button": "🚀 Batch Generate {count} Videos",
"batch.generate_help": "⚠️ Please keep the page open during batch generation, do not close the browser",
"batch.overall_progress": "Overall Progress",
"batch.current_task": "Current Task",
"batch.completed": "Batch Generation Completed!",
"batch.completed": "Batch generation completed!",
"batch.results_title": "📊 Batch Generation Results",
"batch.total": "Total",
"batch.success": "Success",
"batch.failed": "Failed",
"batch.total_time": "Total Time",
"batch.minutes": "min",
"batch.seconds": "sec",
"batch.success_message": "✅ Batch generation completed! All videos saved to history.",
"batch.minutes": "m",
"batch.seconds": "s",
"batch.success_message": "✅ Batch generation completed! All videos have been saved to history.",
"batch.view_in_history": "💡 Tip: You can view all generated videos in the '📚 History' page.",
"batch.goto_history": "Go to History Page",
"batch.failed_list": "❌ Failed Tasks",
"batch.task": "Task",
"batch.error": "Error Message",
"batch.error_detail": "View Detailed Error Stack",
"pipeline.quick_create.name": "Quick Creation",
"pipeline.quick_create.description": "Enter an idea, AI helps you finish the whole video",
"pipeline.custom_media.name": "Custom Asset",
"pipeline.custom_media.description": "Use your own photos/videos, AI adds script and voiceover",
"pipeline.digital_human.name": "Digital Human Generation",
"pipeline.digital_human.description": "Use a paragraph of text, two images, and one audio to generate a digital human video",
"pipeline.audio-visual.name": "Audio-visual Sync",
"pipeline.audio-visual.description": "Enter image and prompt, AI instantly generates synchronized video",
"batch.error": "Error",
"batch.error_detail": "View detailed error stack",
"pipeline.quick_create.name": "Quick Create",
"pipeline.quick_create.description": "Input an idea, AI completes the entire video for you",
"pipeline.custom_media.name": "Custom Media",
"pipeline.custom_media.description": "Use your own photos/videos, AI adds narration and voiceover",
"pipeline.digital_human.name": "Digital Human Broadcast",
"pipeline.digital_human.description": "Use a piece of text, two images, and an audio clip, and AI will generate a digital human video for you",
"pipeline.audio-visual.name": "Image-to-Video",
"pipeline.audio-visual.description": "Enter an image and a prompt, and AI will instantly generate a video",
"asset_based.section.assets": "📦 Asset Upload",
"asset_based.section.video_info": "📝 Video Info",
"asset_based.section.video_info": "📝 Video Information",
"asset_based.section.source": "⚙️ Service Configuration",
"asset_based.assets.what": "Upload your images/videos, AI will analyze and auto-generate a video script",
"asset_based.assets.how": "Supports JPG/PNG/GIF/WebP images and MP4/MOV/AVI etc. videos. Each asset should be clear and relevant",
"asset_based.assets.upload": "Upload Asset",
"asset_based.assets.upload_help": "Supports uploading multiple images or videos",
"asset_based.assets.what": "Upload your images or video assets, AI will automatically analyze them and generate a video script",
"asset_based.assets.how": "Supports JPG/PNG/GIF/WebP images and MP4/MOV/AVI videos. Each asset should be clear and relevant",
"asset_based.assets.upload": "Upload Assets",
"asset_based.assets.upload_help": "Supports multiple image or video files",
"asset_based.assets.count": "✅ Uploaded {count} assets",
"asset_based.assets.preview": "📷 Asset Preview",
"asset_based.assets.empty_hint": "💡 Please upload at least one image or video asset",
"asset_based.video_title": "Video Title (Optional)",
"asset_based.video_title_placeholder": "E.g.: Year End Sale at Pet Store",
"asset_based.video_title_help": "Main video title; leave blank to not show title",
"asset_based.video_title_placeholder": "e.g., Pet Store Year-End Sale",
"asset_based.video_title_help": "Main title for the video, leave empty to hide title",
"asset_based.intent": "Video Intent",
"asset_based.intent_placeholder": "E.g.: Promote our pet store's year-end specials, attract more customers, style should be warm and friendly",
"asset_based.intent_help": "Describe the video’s purpose, message, and desired style",
"asset_based.duration": "Target Duration (Seconds)",
"asset_based.duration_help": "Expected video duration. AI will adjust based on number and length of assets",
"asset_based.duration_label": "Target Duration: {seconds} sec",
"asset_based.source.what": "Choose an image analysis service provider",
"asset_based.source.how": "RunningHub is a cloud service requiring API Key; SelfHost is local ComfyUI service",
"asset_based.intent_placeholder": "e.g., Promote our pet store's year-end special offers to attract more customers, use a warm and friendly tone",
"asset_based.intent_help": "Describe the purpose, message, and desired style of this video",
"asset_based.duration": "Target Duration (seconds)",
"asset_based.duration_help": "Expected video duration, AI will adjust based on asset count",
"asset_based.duration_label": "Target Duration: {seconds}s",
"asset_based.source.what": "Select the service provider for image analysis",
"asset_based.source.how": "RunningHub is a cloud service requiring API Key; SelfHost uses local ComfyUI",
"asset_based.source.select": "Select Service",
"asset_based.source.runninghub": "☁️ RunningHub (Cloud)",
"asset_based.source.selfhost": "🖥️ SelfHost (Local)",
"asset_based.source.runninghub_hint": "💡 Use RunningHub cloud for asset analysis",
"asset_based.source.selfhost_hint": "💡 Use local ComfyUI for asset analysis",
"asset_based.source.runninghub_hint": "💡 Using RunningHub cloud service for asset analysis",
"asset_based.source.selfhost_hint": "💡 Using local ComfyUI service for asset analysis",
"asset_based.source.runninghub_not_configured": "⚠️ RunningHub API Key not configured",
"asset_based.source.selfhost_not_configured": "⚠️ Local ComfyUI address not configured",
"asset_based.output.no_assets": "💡 Please upload assets from the left",
"asset_based.source.selfhost_not_configured": "⚠️ Local ComfyUI URL not configured",
"asset_based.output.no_assets": "💡 Please upload assets on the left first",
"asset_based.output.ready": "📦 {count} assets ready, you can start generating",
"asset_based.progress.analyzing": "🔍 Analyzing assets...",
"asset_based.progress.analyzing_start": "🔍 Starting analysis of {total} assets...",
"asset_based.progress.analyzing_start": "🔍 Starting to analyze {total} assets...",
"asset_based.progress.analyzing_asset": "🔍 Analyzing asset {current}/{total}: {name}",
"asset_based.progress.analyzing_complete": "✅ Asset analysis completed ({count})",
"asset_based.progress.analyzing_complete": "✅ Asset analysis complete ({count} total)",
"asset_based.progress.generating_script": "📝 Generating video script...",
"asset_based.progress.script_complete": "✅ Script generation completed",
"asset_based.progress.concat_complete": "✅ Video concatenation completed",
"asset_based.progress.script_complete": "✅ Script generation complete",
"asset_based.progress.concat_complete": "✅ Video concatenation complete",
"digital_human.section.character_assets": "😊 Character Image Upload",
"digital_human.assets.character_what": "Upload digital human character images",
"digital_human.assets.character_what": "Upload an image for the digital human character",
"digital_human.assets.character_warning": "Please upload a character image",
"digital_human.assets.select_mode": "Please select a mode and fill in the corresponding information",
"digital_human.assets.how": "Supports JPG/PNG/WebP etc. images; assets should be clear and relevant",
"digital_human.assets.how": "Supported formats: JPG/PNG/WebP; clear and relevant images are recommended",
"digital_human.assets.upload": "Upload Asset",
"digital_human.assets.upload_help": "Only single image upload supported",
"digital_human.assets.upload_help": "Only single image upload is supported",
"digital_human.assets.character_sucess": "Character image uploaded successfully",
"digital_human.assets.preview": "📷 Asset Preview",
"digital_human.assets.character_empty_hint": "💡 Please upload a character image asset",
"digital_human.assets.character_empty_hint": "💡 Please upload a digital human character image",
"digital_human.section.select_mode": "💫 Select Generation Mode",
"digital_human.assets.mode_what": "Choose the required digital human generation mode",
"digital_human.assets.select_how": "Supports e-commerce and custom voiceover forms",
"digital_human.input.topic_placeholder": "AI auto-creates product narration\nE.g.: Fragrance leaves behind memories.",
"digital_human.input.content_placeholder": "Used directly, no rewrite\nE.g.:\nFragrance leaves behind memories. If you want your own unique fragrance, try TALIA. Delicate and deep, it enhances your charm. Create wonderful moments with TALIA today.",
"digital_human.assets.mode_what": "Select the required digital human generation mode",
"digital_human.assets.select_how": "Supports smart product promotion mode and custom narration mode",
"digital_human.input.topic_placeholder": "Narration script\nExample: Fragrance leaves lasting memories. If you're looking for your own personalized fragrance, why not try TALIA?",
"digital_human.input.content_placeholder": "Used directly, no rewriting\nExample:\nFragrance leaves lasting memories. If you're looking for your own personalized fragrance, try TALIA. It is exquisite and profound, enhancing your charm. Create wonderful moments today with TALIA.",
"digital_human.assets.goods_sucess": "Product image uploaded successfully",
"digital_human.assets.goods_empty_hint": "💡 Please upload a product image asset",
"digital_human.section.goods_info": "🗃️ Product Info",
"digital_human.goods_title": "Product Name",
"digital_human.goods_title_placeholder": "E.g.: Lipstick, Coffee machine, Robot vacuum, etc.",
"digital_human.goods_title_help": "If product name is provided, AI can auto-generate narration. If needed, input product intro below.",
"digital_human.input_text": "Product Description",
"digital_human.customize_text": "Custom Text",
"digital_human.section.workflow": "⚙️ Workflow Load",
"digital_human.workflow.what": "Configure two-step digital human video generation workflow: step 1 generates collage and script, step 2 generates the final video",
"digital_human.workflow.first_step": "Voiceover & collage workflow loaded successfully",
"digital_human.workflow.second_step": "Digital human synthesis workflow loaded successfully",
"digital_human.workflow.ready": "Digital human workflow ready, you can start generating",
"digital_human.workflow.missing": "Missing necessary digital human workflow, please check the configuration",
"digital_tts.what": "Convert narration text into life-like speech",
"audio_visual.input.topic_placeholder": "Enter a creative prompt (e.g.: audio & rhythm, camera & movement rules, overall visual style, color & grading, etc.)",
"audio_visual.assets.upload": "Upload Video Asset",
"audio_visual.assets.upload_help": "Only support single image upload",
"digital_human.assets.goods_empty_hint": "💡 Please upload a product image",
"digital_human.section.goods_info": "🗃️ Product Information",
"digital_human.goods_title": "AI-created narration",
"digital_human.goods_title_placeholder": "Example: lipstick, coffee machine, robot vacuum, etc.",
"digital_human.goods_title_help": "The product name. If provided, AI will generate narration automatically. You don’t need to enter the product description again unless needed—add it below if required.",
"digital_human.input_text": "Narration script",
"digital_human.customize_text": "Custom text",
"digital_human.section.workflow": "⚙️ Workflow Loading",
"digital_human.workflow.what": "Configure the two-step workflow for digital human video generation: step 1 generates collage and script, step 2 generates final video",
"digital_human.workflow.first_step": "Narration & collage workflow loaded successfully",
"digital_human.workflow.second_step": "Digital human composition workflow loaded successfully",
"digital_human.workflow.ready": "Digital human workflow is ready. You can start video generation",
"digital_human.workflow.missing": "Missing required digital human workflow. Please check the configuration",
"digital_tts.what": "Convert narration text to natural human-like speech",
"audio_visual.input.topic_placeholder": "Enter creative prompts (e.g., audio and rhythm, camera movement rules, overall visual style, color, color grading, etc.)",
"audio_visual.assets.upload": "Upload video first frame image",
"audio_visual.assets.upload_help": "Only single image upload is supported",
"audio_visual.assets.character_sucess": "Video asset uploaded successfully",
"audio_visual.assets.preview": "📷 Asset Preview",
"audio_visual.assets.character_empty_hint": "💡 Please upload an image asset to generate video",
"audio_visual.input_text": "Creative Script",
"audio_visual.assets.image_warning": "Please upload video footage",
"audio_visual.assets.prompt_warning": "Please enter the video prompt message",
"audio_visual.assets.character_empty_hint": "💡 Please upload a first frame image for your video",
"audio_visual.input_text": "Video prompt",
"audio_visual.assets.image_warning": "Upload the first frame image of your video",
"audio_visual.assets.prompt_warning": "Please enter a video prompt",
"faq.expand_to_view": "FAQ",
"faq.load_error": "Failed to load FAQ content",
+11 -24
View File
@@ -163,6 +163,7 @@
"progress.step_compose": "合成画面",
"progress.step_video": "创建视频片段",
"progress.concatenating": "正在拼接视频...",
"progress.generation": "正在合成视频...",
"progress.finalizing": "完成中...",
"progress.completed": "✅ 生成完成",
"error.input_required": "❌ 请提供主题或内容",
@@ -241,20 +242,6 @@
"tts.voice.tr-TR-EmelNeural": "TR 女声-友好(埃梅尔)",
"tts.voice.es-ES-AlvaroNeural": "ES 男声-友好(阿尔瓦罗)",
"tts.voice.es-ES-ElviraNeural": "ES 女声-友好(埃尔维拉)",
"tts.voice.ko-KR-InJoonNeural": "KR 男声-友好(仁俊)",
"tts.voice.ko-KR-SunHiNeural": "KR 女声-友好(善海)",
"tts.voice.fr-FR-EloiseNeural": "FR 女声-友好(埃洛伊斯)",
"tts.voice.fr-FR-HenriNeural": "FR 男声-友好(亨利)",
"tts.voice.pt-PT-DuarteNeural": "PT 男声-友好(杜阿尔特)",
"tts.voice.pt-PT-RaquelNeural": "PT 女声-友好(雷切尔)",
"tts.voice.de-DE-AmalaNeural": "DE 女声-友好(阿玛拉)",
"tts.voice.de-DE-ConradNeural": "DE 男声-友好(康拉德)",
"tts.voice.ru-RU-DmitryNeural": "RU 男声-友好(德米特里)",
"tts.voice.ru-RU-SvetlanaNeural": "RU 女声-友好(斯韦特兰娜)",
"tts.voice.tr-TR-AhmetNeural": "TR 男声-友好(艾哈迈德)",
"tts.voice.tr-TR-EmelNeural": "TR 女声-友好(埃梅尔)",
"tts.voice.es-ES-AlvaroNeural": "ES 男声-友好(阿尔瓦罗)",
"tts.voice.es-ES-ElviraNeural": "ES 女声-友好(埃尔维拉)",
"tts.selector": "工作流选择",
"tts.what": "将旁白文本转换为真人般的自然语音(部分工作流支持参考音频克隆声音)",
"tts.how": "将导出的 tts_xxx.json 工作流文件(API格式)放入 workflows/selfhost/(本地 ComfyUI)或 workflows/runninghub/(云端)文件夹",
@@ -386,10 +373,10 @@
"pipeline.quick_create.description": "输入一个想法,AI帮你完成整条视频",
"pipeline.custom_media.name": "自定义素材",
"pipeline.custom_media.description": "用你自己的照片/视频,AI帮你配文案和配音",
"pipeline.digital_human.name": "数字人生成",
"pipeline.digital_human.name": "数字人口播",
"pipeline.digital_human.description": "用一段文本、两张图片、一段音频,AI帮你生成一个数字人视频",
"pipeline.audio-visual.name": "音画同步",
"pipeline.audio-visual.description": "输入图片和提示词,AI即刻生成音画同步视频",
"pipeline.audio-visual.name": "图生视频",
"pipeline.audio-visual.description": "输入图片和提示词,AI即刻生成视频",
"asset_based.section.assets": "📦 素材上传",
"asset_based.section.video_info": "📝 视频信息",
"asset_based.section.source": "⚙️ 服务配置",
@@ -440,15 +427,15 @@
"digital_human.section.select_mode": "💫 选择生成模式",
"digital_human.assets.mode_what": "选择需要的数字人生成模式",
"digital_human.assets.select_how": "支持智能带货模式以及自定义口播形式",
"digital_human.input.topic_placeholder" : "AI 自动创作商品旁白\n例如:香氛留下回忆、Fragrance leaves behind memories.",
"digital_human.input.topic_placeholder" : "口播文案\n例如:香氛留下回忆。如果您正在寻找属于自己的专属香氛,不妨试试 TALIA、Fragrance leaves lasting memories. If you're looking for your own personalized fragrance, why not try TALIA?",
"digital_human.input.content_placeholder" : "直接使用,不做改写\n例如:\n香氛留下回忆。如果您正在寻找属于自己的专属香氛,不妨试试 TALIA。它精致而深邃,更能提升您的魅力。今天就用 TALIA 创造专属的美好时刻。",
"digital_human.assets.goods_sucess": "上传商品图片成功",
"digital_human.assets.goods_empty_hint": "💡 请上传一个商品图片素材",
"digital_human.section.goods_info": "🗃️ 商品信息",
"digital_human.goods_title": "商品名称",
"digital_human.goods_title": "AI创作旁白",
"digital_human.goods_title_placeholder": "例如:口红、咖啡机、扫地机器人等",
"digital_human.goods_title_help": "商品的名称,若提供商品名称则AI会自动生成商品旁白,无需再次输入商品描述,若有需要请在下方输入商品介绍",
"digital_human.input_text": "商品介绍",
"digital_human.input_text": "口播文案",
"digital_human.customize_text": "自定义文本",
"digital_human.section.workflow": "⚙️ 工作流加载",
"digital_human.workflow.what": "配置数字人视频生成的两步工作流:第一步生成拼图和文案,第二步生成最终视频",
@@ -458,13 +445,13 @@
"digital_human.workflow.missing": "缺少必要的数字人工作流,请检查配置",
"digital_tts.what": "将旁白文本转换为真人般的自然语音",
"audio_visual.input.topic_placeholder" : "输入创作提示词(例如:音频与节奏、镜头与运动规则、整体视觉风格、色彩与调色等)",
"audio_visual.assets.upload": "上传视频素材",
"audio_visual.assets.upload": "上传视频首帧图像",
"audio_visual.assets.upload_help": "仅支持单张图片上传",
"audio_visual.assets.character_sucess": "上传视频素材成功",
"audio_visual.assets.preview": "📷 素材预览",
"audio_visual.assets.character_empty_hint": "💡 请上传一个想要生成视频的图片素材",
"audio_visual.input_text": "创意文案",
"audio_visual.assets.image_warning": "请上传视频素材",
"audio_visual.assets.character_empty_hint": "💡 请上传一个想要生成视频的首帧图像",
"audio_visual.input_text": "视频提示词",
"audio_visual.assets.image_warning": "上传视频首帧图片",
"audio_visual.assets.prompt_warning": "请输入视频提示词",
"faq.expand_to_view": "常见问题",
+5 -5
View File
@@ -115,7 +115,7 @@ class AudioVisualPipelineUI(PipelineUI):
audio_assets = video_params.get("audio_assets", [])
prompt_text = video_params.get("prompt_text", "")
logger.info(f" - 所有video_params: {video_params}")
logger.info(f"video_params: {video_params}")
if not audio_assets:
st.info(tr("audio_visual.assets.image_warning"))
@@ -134,7 +134,7 @@ class AudioVisualPipelineUI(PipelineUI):
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True, # 禁用
disabled=True,
key="audio_visual_generate"
)
return
@@ -161,14 +161,14 @@ class AudioVisualPipelineUI(PipelineUI):
import json
from pathlib import Path
status_text.text("加载图片成功,准备生成视频")
status_text.text(tr("progress.generation"))
progress_bar.progress(65)
image_path = audio_assets[0]
prompt = prompt_text
workflow_path = Path("workflows/runninghub/audio_visual.json")
if not workflow_path.exists():
raise Exception(f"工作流文件不存在: {workflow_path}")
raise Exception(f"The workflow file does not exist: {workflow_path}")
with open(workflow_path, 'r', encoding='utf-8') as f:
workflow_config = json.load(f)
workflow_params = {
@@ -192,7 +192,7 @@ class AudioVisualPipelineUI(PipelineUI):
generated_video_url = videos[0]
break
if not generated_video_url:
raise Exception("视频生成失败,请检查工作流配置")
raise Exception("The workflow did not return a video. Please check the workflow configuration.")
final_video_path = os.path.join(task_dir, "final.mp4")
timeout = httpx.Timeout(300.0)
+157 -362
View File
@@ -40,6 +40,7 @@ class DigitalHumanPipelineUI(PipelineUI):
# ====================================================================
with left_col:
asset_params = self.render_digital_human_input()
style_params = render_style_config(pixelle_video)
# bgm_params = render_bgm_section(key_prefix="asset_")
render_version_info()
@@ -47,8 +48,8 @@ class DigitalHumanPipelineUI(PipelineUI):
# Middle Column: Video Configuration
# ====================================================================
with middle_col:
# Style configuration (TTS, template, workflow, etc.)
style_params = render_style_config(pixelle_video)
# Style configuration ()
mode_params = self.render_digital_human_mode(asset_params["character_assets"])
# # ====================================================================
# # Right Column: Output Preview
@@ -56,6 +57,7 @@ class DigitalHumanPipelineUI(PipelineUI):
with right_col:
# Combine all parameters
video_params = {
**mode_params,
**asset_params,
**style_params
}
@@ -110,7 +112,10 @@ class DigitalHumanPipelineUI(PipelineUI):
st.image(file, caption=file.name, use_container_width=True)
else:
st.info(tr("digital_human.assets.character_empty_hint"))
return {"character_assets": character_asset_paths}
def render_digital_human_mode(self, character_asset_paths: list) -> dict:
with st.container(border=True):
st.markdown(f"**{tr('digital_human.section.select_mode')}**")
@@ -173,13 +178,6 @@ class DigitalHumanPipelineUI(PipelineUI):
else:
st.info(tr("digital_human.assets.goods_empty_hint"))
# Text input
goods_title = st.text_input(
tr("digital_human.goods_title"),
placeholder=tr("digital_human.goods_title_placeholder"),
help=tr("digital_human.goods_title_help"),
key="goods_title"
)
goods_text = st.text_area(
tr("digital_human.input_text"),
placeholder=text_placeholder,
@@ -188,6 +186,13 @@ class DigitalHumanPipelineUI(PipelineUI):
key="digital_box"
)
goods_title = st.text_input(
tr("digital_human.goods_title"),
placeholder=tr("digital_human.goods_title_placeholder"),
help=tr("digital_human.goods_title_help"),
key="goods_title"
)
return {
"character_assets": character_asset_paths,
"goods_title": goods_title,
@@ -205,13 +210,12 @@ class DigitalHumanPipelineUI(PipelineUI):
key="customize_box"
)
return {
"character_assets": character_asset_paths,
"goods_text": goods_text,
"mode": mode
}
def _render_output_preview(self, pixelle_video: Any, video_params: dict):
"""Render output preview section"""
with st.container(border=True):
@@ -227,17 +231,14 @@ class DigitalHumanPipelineUI(PipelineUI):
goods_title = video_params.get("goods_title", "")
goods_text = video_params.get("goods_text", "")
mode = video_params.get("mode")
first_workflow = video_params.get("first_workflow")
second_workflow = video_params.get("second_workflow")
third_workflow = video_params.get("third_workflow")
tts_voice = video_params.get("tts_voice", "zh-CN-YunjianNeural")
tts_speed = video_params.get("tts_speed", 1.2)
logger.info(f"🔧 获取到的TTS参数:")
logger.info(f"🔧 The obtained TTS parameters:")
logger.info(f" - tts_voice: {tts_voice}")
logger.info(f" - tts_speed: {tts_speed}")
logger.info(f" - video_params中的tts_voice: {video_params.get('tts_voice', 'NOT_FOUND')}")
logger.info(f" - 所有video_params: {video_params}")
logger.info(f" - video_params: {video_params}")
# Validation
if not character_assets:
@@ -257,23 +258,11 @@ class DigitalHumanPipelineUI(PipelineUI):
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True, # 禁用
disabled=True,
key="digital_human_generate"
)
return
# Workflow validation
# if not first_workflow or not second_workflow or not third_workflow:
# st.warning("请选择工作流配置")
# st.button(
# tr("btn.generate"),
# type="primary",
# use_container_width=True,
# disabled=True,
# key="digital_human_generate_disabled_no_workflow"
# )
# return
# Generate button
if st.button(tr("btn.generate"), type="primary", use_container_width=True, key="digital_human_generate"):
# Validate
@@ -297,12 +286,12 @@ class DigitalHumanPipelineUI(PipelineUI):
from pathlib import Path
if mode == "customize":
status_text.text("步骤 1/2: 文案转语音 ...")
status_text.text(tr("progress.step_audio"))
progress_bar.progress(25)
generated_image_path = character_assets[0] # 用户上传
generated_text = goods_text # 用户输入文本
generated_image_path = character_assets[0]
generated_text = goods_text
# TTS合成
# TTS
audio_path = os.path.join(task_dir, "narration.mp3")
await pixelle_video.tts(
text=generated_text,
@@ -312,12 +301,12 @@ class DigitalHumanPipelineUI(PipelineUI):
speed=tts_speed
)
progress_bar.progress(65)
status_text.text("步骤 2/2: 合成视频 ...")
status_text.text(tr("progress.concatenating"))
# 直接调用第二工作流
# Directly call the second workflow
second_workflow_path = Path("workflows/runninghub/digital_combination.json")
if not second_workflow_path.exists():
raise Exception(f"第二步工作流文件不存在: {second_workflow_path}")
raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
with open(second_workflow_path, 'r', encoding='utf-8') as f:
second_workflow_config = json.load(f)
second_workflow_params = {
@@ -329,7 +318,7 @@ class DigitalHumanPipelineUI(PipelineUI):
else:
workflow_input = str(second_workflow_path)
second_result = await kit.execute(workflow_input, second_workflow_params)
# 视频链接提取
# Video Link Extraction
generated_video_url = None
if hasattr(second_result, 'videos') and second_result.videos:
generated_video_url = second_result.videos[0]
@@ -341,7 +330,7 @@ class DigitalHumanPipelineUI(PipelineUI):
generated_video_url = videos[0]
break
if not generated_video_url:
raise Exception("第二步工作流未返回视频,请检查工作流配置")
raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
final_video_path = os.path.join(task_dir, "final.mp4")
timeout = httpx.Timeout(300.0)
@@ -355,339 +344,145 @@ class DigitalHumanPipelineUI(PipelineUI):
return final_video_path
else:
# Create task directory
#Initialization and parameter preparation
task_dir, task_id = create_task_output_dir()
logger.info(f"📁 Task directory created: {task_dir}")
# ============================================================
# Step 1: Call first workflow (character_image + goods_image + goods_title)
# ============================================================
status_text.text("步骤 1/3: 调用第一步工作流生成拼图和文案...")
progress_bar.progress(10)
# Directly load first workflow file
logger.info(f"[Initialization] Task Directory: {task_dir}")
first_workflow_path = Path("workflows/runninghub/digital_image.json")
third_workflow_path = Path("workflows/runninghub/digital_customize.json")
if not first_workflow_path.exists():
raise Exception(f"第一步工作流文件不存在: {first_workflow_path}")
second_workflow_path = Path("workflows/runninghub/digital_combination.json")
assert first_workflow_path.exists(), "The first_workflow file does not exist."
assert third_workflow_path.exists(), "The third_workflow file does not exist."
assert second_workflow_path.exists(), "The second_workflow file does not exist."
if not third_workflow_path.exists():
raise Exception(f"第一步工作流文件不存在: {third_workflow_path}")
if goods_text and goods_text.strip():
logger.info("✏️ 文本输入非空")
workflow_path = third_workflow_path
with open(third_workflow_path, 'r', encoding='utf-8') as f:
workflow_config = json.load(f)
workflow_params = {
"firstimage": character_assets[0],
"secondimage": goods_assets[0]
}
else:
logger.info("🖼️ 文本输入为空,按原逻辑调用第一工作流")
workflow_path = first_workflow_path
with open(first_workflow_path, 'r', encoding='utf-8') as f:
workflow_config = json.load(f)
workflow_params = {
"firstimage": character_assets[0],
"secondimage": goods_assets[0],
"goodstype": goods_title
}
workflow_params = {"firstimage": character_assets[0], "secondimage": goods_assets[0]}
status_text.text(tr("progress.step_image"))
kit = await pixelle_video._get_or_create_comfykit()
workflow_config = json.load(open(workflow_path, 'r', encoding='utf8'))
workflow_input = workflow_config.get("workflow_id", str(workflow_path))
combine_image = await kit.execute(workflow_input, workflow_params)
if combine_image.status != "completed":
raise Exception(f"workflow execution failed: {combine_image.msg}")
generated_image_url = getattr(combine_image, "images", [None])[0]
status_text.text(tr("progress.step_audio"))
audio_path = os.path.join(task_dir, "narration.mp3")
await pixelle_video.tts(
text=goods_text,
output_path=audio_path,
inference_mode="local",
voice=tts_voice,
speed=tts_speed
)
progress_bar.progress(65)
status_text.text(tr("progress.progress.concatenating"))
logger.info(f"🔧 传递给第一步工作流的参数:")
for key, value in workflow_params.items():
if isinstance(value, str) and len(value) > 100:
logger.info(f" - {key}: {value[:100]}... (文件路径)")
second_workflow_path = Path("workflows/runninghub/digital_combination.json")
if not second_workflow_path.exists():
raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
with open(second_workflow_path, 'r', encoding='utf-8') as f:
second_workflow_config = json.load(f)
second_workflow_params = {
"videoimage": generated_image_url,
"audio": audio_path
}
if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
workflow_input = second_workflow_config["workflow_id"]
else:
logger.info(f" - {key}: {value}")
kit = await pixelle_video._get_or_create_comfykit()
if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
workflow_input = workflow_config["workflow_id"]
workflow_input = str(second_workflow_path)
second_result = await kit.execute(workflow_input, second_workflow_params)
# Video Link Extraction
generated_video_url = None
if hasattr(second_result, 'videos') and second_result.videos:
generated_video_url = second_result.videos[0]
elif hasattr(second_result, 'outputs') and second_result.outputs:
for node_id, node_output in second_result.outputs.items():
if isinstance(node_output, dict) and 'videos' in node_output:
videos = node_output['videos']
if videos and len(videos) > 0:
generated_video_url = videos[0]
break
if not generated_video_url:
raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
final_video_path = os.path.join(task_dir, "final.mp4")
timeout = httpx.Timeout(300.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(generated_video_url)
response.raise_for_status()
with open(final_video_path, 'wb') as f:
f.write(response.content)
progress_bar.progress(100)
status_text.text(tr("status.success"))
return final_video_path
else:
workflow_input = str(workflow_path)
workflow_path = first_workflow_path
workflow_params = {"firstimage": character_assets[0], "secondimage": goods_assets[0], "goodstype": goods_title}
status_text.text(tr("progress.step_image"))
kit = await pixelle_video._get_or_create_comfykit()
workflow_config = json.load(open(workflow_path, 'r', encoding='utf8'))
workflow_input = workflow_config.get("workflow_id", str(workflow_path))
synthesis_result = await kit.execute(workflow_input, workflow_params)
if synthesis_result.status != "completed":
raise Exception(f"workflow execution failed: {synthesis_result.msg}")
generated_image_url = getattr(synthesis_result, "images", [None])[0]
generated_text = getattr(synthesis_result, "texts", [None])[0]
status_text.text(tr("progress.step_audio"))
audio_path = os.path.join(task_dir, "narration.mp3")
await pixelle_video.tts(
text=generated_text,
output_path=audio_path,
inference_mode="local",
voice=tts_voice,
speed=tts_speed
)
progress_bar.progress(65)
status_text.text(tr("progress.concatenating"))
logger.info(f"执行当前工作流: {workflow_input}")
first_result = await kit.execute(workflow_input, workflow_params)
if first_result.status != "completed":
raise Exception(f"第一步工作流执行失败: {first_result.msg}")
# Debug: Log the actual result structure
logger.info(f"🔍 第一步工作流返回结果调试:")
logger.info(f"- result type: {type(first_result)}")
logger.info(f"- result.__dict__: {first_result.__dict__}")
if hasattr(first_result, 'images'):
logger.info(f"- images: {first_result.images}")
if hasattr(first_result, 'texts'):
logger.info(f"- texts: {first_result.texts}")
if hasattr(first_result, 'videos'):
logger.info(f"- videos: {first_result.videos}")
if hasattr(first_result, 'outputs'):
logger.info(f"- outputs keys: {list(first_result.outputs.keys()) if first_result.outputs else 'None'}")
if first_result.outputs:
for node_id, node_output in first_result.outputs.items():
logger.info(f" - 节点 {node_id}: {node_output}")
for attr in dir(first_result):
if not attr.startswith('_'):
try:
value = getattr(first_result, attr)
if not callable(value):
logger.info(f"- {attr}: {value}")
except:
pass
# Extract results from first workflow
generated_image_url = None
generated_text = None
generated_video_url = None
if hasattr(first_result, 'videos') and first_result.videos:
generated_video_url = first_result.videos[0]
logger.info(f"✅ 第一步工作流直接生成了视频: {generated_video_url}")
progress_bar.progress(100)
status_text.text(tr("status.success"))
# Download video to local
final_video_path = os.path.join(task_dir, "final.mp4")
timeout = httpx.Timeout(300.0) # 简化timeout设置
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(generated_video_url)
response.raise_for_status()
with open(final_video_path, 'wb') as f:
f.write(response.content)
return final_video_path
# Extract image - try direct access first
if hasattr(first_result, 'images') and first_result.images:
generated_image_url = first_result.images[0]
logger.info(f"✅ 从 result.images 获取图片: {generated_image_url}")
elif hasattr(first_result, 'outputs') and first_result.outputs:
# Try to find image in outputs
for node_id, node_output in first_result.outputs.items():
logger.info(f"- 检查节点 {node_id}: {list(node_output.keys()) if isinstance(node_output, dict) else type(node_output)}")
if isinstance(node_output, dict) and 'images' in node_output:
images = node_output['images']
if images and len(images) > 0:
generated_image_url = images[0]
logger.info(f"✅ 从 outputs[{node_id}].images 获取图片: {generated_image_url}")
break
# Extract text - try direct access first
if hasattr(first_result, 'texts') and first_result.texts:
generated_text = first_result.texts[0]
logger.info(f"✅ 从 result.texts 获取文本: {generated_text[:50]}...")
elif hasattr(first_result, 'outputs') and first_result.outputs:
# Try to find text in outputs
for node_id, node_output in first_result.outputs.items():
if isinstance(node_output, dict) and 'text' in node_output:
text_list = node_output['text']
if text_list and len(text_list) > 0:
generated_text = text_list[0]
logger.info(f"✅ 从 outputs[{node_id}].text 获取文本: {generated_text[:50]}...")
break
if not generated_image_url:
logger.error("❌ 第一步工作流未返回图片,尝试查找其他输出...")
if hasattr(first_result, 'outputs') and first_result.outputs:
for node_id, node_output in first_result.outputs.items():
logger.info(f"- 节点 {node_id} 输出: {node_output}")
if hasattr(first_result, 'outputs') and first_result.outputs:
for node_id, node_output in first_result.outputs.items():
if isinstance(node_output, dict):
for key in ['images', 'image', 'output_image', 'result_image']:
if key in node_output and node_output[key]:
if isinstance(node_output[key], list) and len(node_output[key]) > 0:
generated_image_url = node_output[key][0]
logger.info(f"✅ 从 outputs[{node_id}].{key} 获取图片: {generated_image_url}")
break
elif isinstance(node_output[key], str):
generated_image_url = node_output[key]
logger.info(f"✅ 从 outputs[{node_id}].{key} 获取图片: {generated_image_url}")
break
if generated_image_url:
break
if not generated_image_url:
raise Exception("第一步工作流未返回图片,请检查工作流配置")
if not generated_text:
logger.warning("⚠️ 第一步工作流未返回文本,尝试查找其他文本输出...")
if hasattr(first_result, 'outputs') and first_result.outputs:
for node_id, node_output in first_result.outputs.items():
if isinstance(node_output, dict):
for key in ['text', 'texts', 'output_text', 'result_text', 'description', 'caption']:
if key in node_output and node_output[key]:
if isinstance(node_output[key], list) and len(node_output[key]) > 0:
generated_text = node_output[key][0]
logger.info(f"✅ 从 outputs[{node_id}].{key} 获取文本: {generated_text[:50]}...")
break
elif isinstance(node_output[key], str):
generated_text = node_output[key]
logger.info(f"✅ 从 outputs[{node_id}].{key} 获取文本: {generated_text[:50]}...")
break
if generated_text:
break
if not generated_text:
generated_text = goods_text
logger.warning(f"⚠️ 使用自定义文本: {generated_text}")
logger.info(f"✅ 第一步完成: 图片={generated_image_url}, 文本={generated_text[:50]}...")
# Download generated image to local
generated_image_path = os.path.join(task_dir, "generated_image.png")
timeout = httpx.Timeout(10.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(generated_image_url)
response.raise_for_status()
with open(generated_image_path, 'wb') as f:
f.write(response.content)
progress_bar.progress(40)
status_text.text("步骤 2/3: 将文案转换为语音...")
# ============================================================
# Step 2: TTS conversion (text -> audio)
# ============================================================
audio_path = os.path.join(task_dir, "narration.mp3")
await pixelle_video.tts(
text=generated_text,
output_path=audio_path,
inference_mode="local",
voice=tts_voice,
speed=tts_speed
)
logger.info(f"✅ 第二步完成: 语音={audio_path}")
progress_bar.progress(70)
status_text.text("步骤 3/3: 调用第二步工作流生成最终视频...")
# ============================================================
# Step 3: Call second workflow (generated_image + audio)
# ============================================================
# Directly load second workflow file
second_workflow_path = Path("workflows/runninghub/digital_combination.json")
if not second_workflow_path.exists():
raise Exception(f"第二步工作流文件不存在: {second_workflow_path}")
with open(second_workflow_path, 'r', encoding='utf-8') as f:
second_workflow_config = json.load(f)
# Build workflow parameters for second workflow
if mode == "digital":
second_workflow_params = {
"videoimage": generated_image_path,
"audio": audio_path,
}
else:
second_workflow_params = {
"videoimage": character_assets[0],
"audio": audio_path
}
logger.info(f"🔧 传递给第二步工作流的参数:")
for key, value in second_workflow_params.items():
logger.info(f" - {key}: {value}")
# Execute second workflow
if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
workflow_input = second_workflow_config["workflow_id"]
else:
workflow_input = str(second_workflow_path)
logger.info(f"执行第二步工作流: {workflow_input}")
second_result = await kit.execute(workflow_input, second_workflow_params)
if second_result.status != "completed":
raise Exception(f"第二步工作流执行失败: {second_result.msg}")
# Debug: Log the second workflow result structure
logger.info(f"🔍 第二步工作流返回结果调试:")
logger.info(f"- result type: {type(second_result)}")
logger.info(f"- result.__dict__: {second_result.__dict__}")
if hasattr(second_result, 'videos'):
logger.info(f"- videos: {second_result.videos}")
if hasattr(second_result, 'images'):
logger.info(f"- images: {second_result.images}")
if hasattr(second_result, 'texts'):
logger.info(f"- texts: {second_result.texts}")
if hasattr(second_result, 'outputs'):
logger.info(f"- outputs keys: {list(second_result.outputs.keys()) if second_result.outputs else 'None'}")
if second_result.outputs:
for node_id, node_output in second_result.outputs.items():
logger.info(f" - 节点 {node_id}: {node_output}")
for attr in dir(second_result):
if not attr.startswith('_'):
try:
value = getattr(second_result, attr)
if not callable(value):
logger.info(f"- {attr}: {value}")
except:
pass
# Extract video from second workflow result
generated_video_url = None
if hasattr(second_result, 'videos') and second_result.videos:
generated_video_url = second_result.videos[0]
logger.info(f"✅ 从 result.videos 获取视频: {generated_video_url}")
elif hasattr(second_result, 'outputs') and second_result.outputs:
for node_id, node_output in second_result.outputs.items():
logger.info(f"- 检查节点 {node_id}: {list(node_output.keys()) if isinstance(node_output, dict) else type(node_output)}")
if isinstance(node_output, dict) and 'videos' in node_output:
videos = node_output['videos']
if videos and len(videos) > 0:
generated_video_url = videos[0]
logger.info(f"✅ 从 outputs[{node_id}].videos 获取视频: {generated_video_url}")
break
if not generated_video_url:
logger.error("❌ 第二步工作流未返回视频,尝试查找其他输出...")
if hasattr(second_result, 'outputs') and second_result.outputs:
for node_id, node_output in second_result.outputs.items():
logger.info(f"- 节点 {node_id} 输出: {node_output}")
if hasattr(second_result, 'outputs') and second_result.outputs:
for node_id, node_output in second_result.outputs.items():
if isinstance(node_output, dict):
for key in ['videos', 'video', 'output_video', 'result_video', 'mp4']:
if key in node_output and node_output[key]:
if isinstance(node_output[key], list) and len(node_output[key]) > 0:
generated_video_url = node_output[key][0]
logger.info(f"✅ 从 outputs[{node_id}].{key} 获取视频: {generated_video_url}")
break
elif isinstance(node_output[key], str):
generated_video_url = node_output[key]
logger.info(f"✅ 从 outputs[{node_id}].{key} 获取视频: {generated_video_url}")
break
if generated_video_url:
break
if not generated_video_url:
raise Exception("第二步工作流未返回视频,请检查工作流配置")
logger.info(f"✅ 第三步完成: 视频={generated_video_url}")
# Download video to local
final_video_path = os.path.join(task_dir, "final.mp4")
timeout = httpx.Timeout(300.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(generated_video_url)
response.raise_for_status()
with open(final_video_path, 'wb') as f:
f.write(response.content)
return final_video_path
second_workflow_path = Path("workflows/runninghub/digital_combination.json")
if not second_workflow_path.exists():
raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
with open(second_workflow_path, 'r', encoding='utf-8') as f:
second_workflow_config = json.load(f)
second_workflow_params = {
"videoimage": generated_image_url,
"audio": audio_path
}
if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
workflow_input = second_workflow_config["workflow_id"]
else:
workflow_input = str(second_workflow_path)
second_result = await kit.execute(workflow_input, second_workflow_params)
# Video Link Extraction
generated_video_url = None
if hasattr(second_result, 'videos') and second_result.videos:
generated_video_url = second_result.videos[0]
elif hasattr(second_result, 'outputs') and second_result.outputs:
for node_id, node_output in second_result.outputs.items():
if isinstance(node_output, dict) and 'videos' in node_output:
videos = node_output['videos']
if videos and len(videos) > 0:
generated_video_url = videos[0]
break
if not generated_video_url:
raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
final_video_path = os.path.join(task_dir, "final.mp4")
timeout = httpx.Timeout(300.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(generated_video_url)
response.raise_for_status()
with open(final_video_path, 'wb') as f:
f.write(response.content)
progress_bar.progress(100)
status_text.text(tr("status.success"))
return final_video_path
# Execute async generation
final_video_path = run_async(generate_digital_human_video())