diff --git a/pixelle_video/tts_voices.py b/pixelle_video/tts_voices.py index facb1e4..02ecf20 100644 --- a/pixelle_video/tts_voices.py +++ b/pixelle_video/tts_voices.py @@ -22,6 +22,90 @@ from typing import List, Dict, Any # Edge TTS voice presets for local inference EDGE_TTS_VOICES: List[Dict[str, Any]] = [ # Chinese voices + { + "id": "zh-CN-XiaoxiaoNeural", + "label_key": "tts.voice.zh_CN_XiaoxiaoNeural", + "locale": "zh-CN", + "gender": "female" + }, + { + "id": "zh-CN-XiaoyiNeural", + "label_key": "tts.voice.zh_CN_XiaoyiNeural", + "locale": "zh-CN", + "gender": "female" + }, + { + "id": "zh-CN-YunjianNeural", + "label_key": "tts.voice.zh_CN_YunjianNeural", + "locale": "zh-CN", + "gender": "male" + }, + { + "id": "zh-CN-YunxiNeural", + "label_key": "tts.voice.zh_CN_YunxiNeural", + "locale": "zh-CN", + "gender": "male" + }, + { + "id": "zh-CN-YunyangNeural", + "label_key": "tts.voice.zh_CN_YunyangNeural", + "locale": "zh-CN", + "gender": "male" + }, + { + "id": "zh-CN-YunyeNeural", + "label_key": "tts.voice.zh_CN_YunyeNeural", + "locale": "zh-CN", + "gender": "male" + }, + { + "id": "zh-CN-YunfengNeural", + "label_key": "tts.voice.zh_CN_YunfengNeural", + "locale": "zh-CN", + "gender": "male" + }, + { + "id": "zh-CN-liaoning-XiaobeiNeural", + "label_key": "tts.voice.zh_CN_liaoning_XiaobeiNeural", + "locale": "zh-CN", + "gender": "female" + }, + { + "id": "en-US-AriaNeural", + "label_key": "tts.voice.en_US_AriaNeural", + "locale": "en-US", + "gender": "female" + }, + { + "id": "en-US-JennyNeural", + "label_key": "tts.voice.en_US_JennyNeural", + "locale": "en-US", + "gender": "female" + }, + { + "id": "en-US-GuyNeural", + "label_key": "tts.voice.en_US_GuyNeural", + "locale": "en-US", + "gender": "male" + }, + { + "id": "en-US-DavisNeural", + "label_key": "tts.voice.en_US_DavisNeural", + "locale": "en-US", + "gender": "male" + }, + { + "id": "en-GB-SoniaNeural", + "label_key": "tts.voice.en_GB_SoniaNeural", + "locale": "en-GB", + "gender": "female" + }, + { + "id": "en-GB-RyanNeural", + "label_key": "tts.voice.en_GB_RyanNeural", + "locale": "en-GB", + "gender": "male" + }, { "id": "ko-KR-InJoonNeural", "label_key": "tts.voice.ko-KR-InJoonNeural", diff --git a/web/i18n/locales/en_US.json b/web/i18n/locales/en_US.json index 89d5933..b2ec62e 100644 --- a/web/i18n/locales/en_US.json +++ b/web/i18n/locales/en_US.json @@ -203,6 +203,20 @@ "tts.voice_selector": "Voice Selection", "tts.speed": "Speed", "tts.speed_label": "{speed}x", + "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": "ko-KR-InJoonNeural", "tts.voice.ko-KR-SunHiNeural": "ko-KR-SunHiNeural", "tts.voice.fr-FR-EloiseNeural": "fr-FR-EloiseNeural", @@ -350,40 +364,6 @@ "pipeline.custom_media.description": "Use your own photos/videos, AI adds narration and voiceover", "pipeline.digital_human.name": "Digital Human Generate", "pipeline.digital_human.description": "Using a piece of text, two images, and an audio clip, AI can help you generate a digital human video.", - "digital_human.section.character_assets": "😊 Character Image Upload", - "digital_human.assets.character_what": "Upload digital human character images", - "digital_human.assets.how": "Supports JPG/PNG/WebP format images, recommend clear and relevant materials", - "digital_human.assets.upload": "Upload Assets", - "digital_human.assets.upload_help": "Only supports single image upload", - "digital_human.assets.character_sucess": "Character image uploaded successfully", - "digital_human.assets.preview": "📷 Asset Preview", - "digital_human.assets.character_empty_hint": "💡 Please upload a digital human character image", - "digital_human.section.goods_assets": "👝 Product Image Upload", - "digital_human.assets.goods_what": "Upload product images", - "digital_human.assets.goods_sucess": "Product image uploaded successfully", - "digital_human.assets.goods_empty_hint": "💡 Please upload a product image", - "digital_human.section.goods_info": "🗃️ Product Information", - "digital_human.goods_title": "Product Name", - "digital_human.goods_title_placeholder": "e.g.: lipstick, coffee machine, robot vacuum, etc.", - "digital_human.goods_title_help": "Product name, cannot be empty", - "digital_human.section.workflow": "⚙️ Workflow Configuration", - "digital_human.workflow.what": "Configure the two-step workflow for digital human video generation: first step generates collage and copy, second step generates final video", - "digital_human.workflow.how": "Select appropriate workflows to handle different generation steps, ensuring output quality", - "digital_human.workflow.first_step": "First Step Workflow", - "digital_human.workflow.first_select": "Select First Step Workflow", - "digital_human.workflow.first_help": "Used to generate collage and copy based on character image, product image and product name", - "digital_human.workflow.second_step": "Second Step Workflow", - "digital_human.workflow.second_select": "Select Second Step Workflow", - "digital_human.workflow.second_help": "Used to generate final digital human video based on generated image and audio", - "digital_human.workflow.selected_info": "Selected workflow configuration", - "digital_human.workflow.auto_selected": "Auto-selected workflow", - "digital_human.workflow.configuration": "Workflow Configuration Status", - "digital_human.workflow.not_found": "not found", - "digital_human.workflow.ready": "Digital human workflows are ready, you can start generating videos", - "digital_human.workflow.missing": "Missing required digital human workflows, please check configuration", - "digital_human.workflow.advanced_options": "Advanced Options (Manual Override)", - "digital_human.workflow.manual_override": "Manual Workflow Selection (use only when needed)", - "digital_tts.what": "Convert narration text to natural human-like speech", "asset_based.section.assets": "📦 Asset Upload", "asset_based.section.video_info": "📝 Video Information", "asset_based.section.source": "⚙️ Service Configuration", @@ -421,6 +401,30 @@ "asset_based.progress.generating_script": "📝 Generating video script...", "asset_based.progress.script_complete": "✅ Script generation complete", "asset_based.progress.concat_complete": "✅ Video concatenation complete", + "digital_human.section.character_assets": "😊 Upload a digital human character image", + "digital_human.assets.character_what": "Upload an image of your digital human character", + "digital_human.assets.how": "Supports JPG/PNG/WebP formats. Clear and relevant images are recommended.", + "digital_human.assets.upload": "Upload the image.", + "digital_human.assets.upload_help": "Single image upload only", + "digital_human.assets.character_sucess": "Image upload successful", + "digital_human.assets.preview": "📷 Image preview", + "digital_human.assets.character_empty_hint": "💡 Please upload a digital human character image", + "digital_human.section.goods_assets": "👝 Upload product images", + "digital_human.assets.goods_what": "Upload an image of your product", + "digital_human.assets.goods_sucess": "Product image uploaded successfully", + "digital_human.assets.goods_empty_hint": "💡 Please upload a product image", + "digital_human.section.goods_info": "🗃️ Product Information", + "digital_human.goods_title": "Product Name", + "digital_human.goods_title_placeholder": "Example: Lipstick, Coffee Machine, Robot Vacuum Cleaner, etc.", + "digital_human.goods_title_help": "Product Name, cannot be empty", + "digital_human.section.workflow": "⚙️ Workflow Loading", + "digital_human.workflow.what": "Configure the two-step workflow for digital human video generation: Step 1 generates the collage and text, Step 2 generates the final video", + "digital_human.workflow.first_step": "Voiceover & collage workflow successfully loaded", + "digital_human.workflow.second_step": "Digital human synthesis workflow loaded successfully", + "digital_human.workflow.ready": "Digital human workflow is ready to start video generation", + "digital_human.workflow.missing": "Necessary digital human workflow is missing, please check configuration", + "digital_tts.what": "Converts narration text into natural, lifelike speech", + "faq.expand_to_view": "FAQ", "faq.load_error": "Failed to load FAQ content", "faq.more_help": "Need more help?" diff --git a/web/i18n/locales/zh_CN.json b/web/i18n/locales/zh_CN.json index 063b092..6adab27 100644 --- a/web/i18n/locales/zh_CN.json +++ b/web/i18n/locales/zh_CN.json @@ -203,6 +203,20 @@ "tts.voice_selector": "音色选择", "tts.speed": "语速", "tts.speed_label": "{speed}x", + "tts.voice.zh_CN_XiaoxiaoNeural": "女声-温柔(晓晓)", + "tts.voice.zh_CN_XiaoyiNeural": "女声-甜美(晓伊)", + "tts.voice.zh_CN_YunjianNeural": "男声-专业(云健)", + "tts.voice.zh_CN_YunxiNeural": "男声-磁性(云希)", + "tts.voice.zh_CN_YunyangNeural": "男声-新闻(云扬)", + "tts.voice.zh_CN_YunyeNeural": "男声-自然(云野)", + "tts.voice.zh_CN_YunfengNeural": "男声-沉稳(云锋)", + "tts.voice.zh_CN_liaoning_XiaobeiNeural": "女声-东北(小北)", + "tts.voice.en_US_AriaNeural": "女声-自然(Aria)", + "tts.voice.en_US_JennyNeural": "女声-温暖(Jenny)", + "tts.voice.en_US_GuyNeural": "男声-标准(Guy)", + "tts.voice.en_US_DavisNeural": "男声-友好(Davis)", + "tts.voice.en_GB_SoniaNeural": "女声-英式(Sonia)", + "tts.voice.en_GB_RyanNeural": "男声-英式(Ryan)", "tts.voice.ko-KR-InJoonNeural": "KR 男声-友好(仁俊)", "tts.voice.ko-KR-SunHiNeural": "KR 女声-友好(善海)", "tts.voice.fr-FR-EloiseNeural": "FR 女声-友好(埃洛伊斯)", diff --git a/web/pipelines/digital_human.py b/web/pipelines/digital_human.py index d2b8765..53cdf58 100644 --- a/web/pipelines/digital_human.py +++ b/web/pipelines/digital_human.py @@ -194,7 +194,6 @@ class DigitalHumanPipelineUI(PipelineUI): tts_voice = video_params.get("tts_voice", "zh-CN-YunjianNeural") tts_speed = video_params.get("tts_speed", 1.2) - # Debug: 显示获取到的TTS参数 logger.info(f"🔧 获取到的TTS参数:") logger.info(f" - tts_voice: {tts_voice}") logger.info(f" - tts_speed: {tts_speed}") @@ -274,8 +273,6 @@ class DigitalHumanPipelineUI(PipelineUI): first_workflow_config = json.load(f) # Build workflow parameters for first workflow - # 注意:参数名需要根据你的 workflow 实际参数来调整 - # 尝试多种可能的参数名 first_workflow_params = { # 根据之前的日志尝试的参数名 "firstimage": character_assets[0], @@ -321,8 +318,7 @@ class DigitalHumanPipelineUI(PipelineUI): 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: @@ -336,13 +332,11 @@ class DigitalHumanPipelineUI(PipelineUI): 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")) @@ -357,7 +351,6 @@ class DigitalHumanPipelineUI(PipelineUI): 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] @@ -388,17 +381,14 @@ class DigitalHumanPipelineUI(PipelineUI): 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: @@ -416,12 +406,10 @@ class DigitalHumanPipelineUI(PipelineUI): 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: @@ -435,7 +423,6 @@ class DigitalHumanPipelineUI(PipelineUI): if generated_text: break - # 如果还是没有文本,使用默认文本 if not generated_text: generated_text = f"为您推荐这款{goods_title},品质优良,值得拥有!" logger.warning(f"⚠️ 使用默认文本: {generated_text}") @@ -444,7 +431,7 @@ class DigitalHumanPipelineUI(PipelineUI): # Download generated image to local generated_image_path = os.path.join(task_dir, "generated_image.png") - timeout = httpx.Timeout(10.0) # 简化timeout设置 + timeout = httpx.Timeout(10.0) async with httpx.AsyncClient(timeout=timeout) as client: response = await client.get(generated_image_url) response.raise_for_status() @@ -461,8 +448,8 @@ class DigitalHumanPipelineUI(PipelineUI): await pixelle_video.tts( text=generated_text, output_path=audio_path, - inference_mode="local", # 明确指定推理模式 - voice=tts_voice, # 使用voice而不是voice_id + inference_mode="local", + voice=tts_voice, speed=tts_speed ) @@ -482,12 +469,9 @@ class DigitalHumanPipelineUI(PipelineUI): second_workflow_config = json.load(f) # Build workflow parameters for second workflow - # 注意:参数名需要根据你的 workflow 实际参数来调整 - # 尝试多种可能的参数名 second_workflow_params = { - # 尝试常见的参数名 - "videoimage": generated_image_path, # 生成的图片路径 - "audio": audio_path, # 音频路径 + "videoimage": generated_image_path, + "audio": audio_path, } logger.info(f"🔧 传递给第二步工作流的参数:") @@ -523,7 +507,6 @@ class DigitalHumanPipelineUI(PipelineUI): 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: @@ -550,17 +533,14 @@ class DigitalHumanPipelineUI(PipelineUI): 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: @@ -581,7 +561,7 @@ class DigitalHumanPipelineUI(PipelineUI): # Download video to local final_video_path = os.path.join(task_dir, "final.mp4") - timeout = httpx.Timeout(300.0) # 简化timeout设置 + timeout = httpx.Timeout(300.0) async with httpx.AsyncClient(timeout=timeout) as client: response = await client.get(generated_video_url) response.raise_for_status()