更新数字人板块

This commit is contained in:
1152450298 committed 2025-12-26 18:44:35 +08:00
1 parent 40d73619b0
commit cd488022dd
7 files changed
+966 -14

No files matched your search

+237
View File
@@ -0,0 +1,237 @@
# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Style configuration components for web UI (middle column)
"""
import os
from pathlib import Path
import streamlit as st
from loguru import logger
from web.i18n import tr, get_language
from web.utils.async_helpers import run_async
from pixelle_video.config import config_manager
def render_style_config(pixelle_video):
"""Render style configuration section (middle column)"""
# TTS Section (moved from left column)
# ====================================================================
with st.container(border=True):
st.markdown(f"**{tr('section.tts')}**")
with st.expander(tr("help.feature_description"), expanded=False):
st.markdown(f"**{tr('help.what')}**")
st.markdown(tr("digital_tts.what"))
# Get TTS config
comfyui_config = config_manager.get_comfyui_config()
tts_config = comfyui_config["tts"]
# 只使用local模式,移除inference_mode选择
tts_mode = "local"
# ================================================================
# Local Mode UI (简化版本)
# ================================================================
# Import voice configuration
from pixelle_video.tts_voices import EDGE_TTS_VOICES, get_voice_display_name
# Get saved voice from config
local_config = tts_config.get("local", {})
saved_voice = local_config.get("voice", "zh-CN-YunjianNeural")
saved_speed = local_config.get("speed", 1.2)
# Build voice options with i18n
voice_options = []
voice_ids = []
default_voice_index = 0
for idx, voice_config in enumerate(EDGE_TTS_VOICES):
voice_id = voice_config["id"]
display_name = get_voice_display_name(voice_id, tr, get_language())
voice_options.append(display_name)
voice_ids.append(voice_id)
# Set default index if matches saved voice
if voice_id == saved_voice:
default_voice_index = idx
# Two-column layout: Voice | Speed
voice_col, speed_col = st.columns([1, 1])
with voice_col:
# Voice selector
selected_voice_display = st.selectbox(
tr("tts.voice_selector"),
voice_options,
index=default_voice_index,
key="digital_tts_local_voice"
)
# Get actual voice ID
selected_voice_index = voice_options.index(selected_voice_display)
selected_voice = voice_ids[selected_voice_index]
with speed_col:
# Speed slider
tts_speed = st.slider(
tr("tts.speed"),
min_value=0.5,
max_value=2.0,
value=saved_speed,
step=0.1,
format="%.1fx",
key="digital_tts_local_speed"
)
st.caption(tr("tts.speed_label", speed=f"{tts_speed:.1f}"))
# ================================================================
# TTS Preview (只支持local模式)
# ================================================================
with st.expander(tr("tts.preview_title"), expanded=False):
# Preview text input
preview_text = st.text_input(
tr("tts.preview_text"),
value="大家好,这是一段测试语音。",
placeholder=tr("tts.preview_text_placeholder"),
key="digital_tts_preview_text"
)
# Preview button
if st.button(tr("tts.preview_button"), key="digital_preview_tts", use_container_width=True):
with st.spinner(tr("tts.previewing")):
try:
# Build TTS params for local mode
tts_params = {
"text": preview_text,
"inference_mode": "local",
"voice": selected_voice,
"speed": tts_speed
}
audio_path = run_async(pixelle_video.tts(**tts_params))
# Play the audio
if audio_path:
st.success(tr("tts.preview_success"))
if audio_path.startswith('http'):
st.audio(audio_path)
else:
st.audio(audio_path, format="audio/mp3")
# Show file path
st.caption(f"📁 {audio_path}")
else:
st.error("Failed to generate preview audio")
except Exception as e:
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 {
"tts_inference_mode": "local",
"tts_voice": selected_voice,
"tts_speed": tts_speed,
"first_workflow": first_workflow,
"second_workflow": second_workflow
}
+50 -14
View File
@@ -203,20 +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",
"tts.voice.fr-FR-HenriNeural": "fr-FR-HenriNeural",
"tts.voice.pt-PT-DuarteNeural": "pt-PT-DuarteNeural",
"tts.voice.pt-PT-RaquelNeural": "pt-PT-RaquelNeural",
"tts.voice.de-DE-AmalaNeural": "de-DE-AmalaNeural",
"tts.voice.de-DE-ConradNeural": "de-DE-ConradNeural",
"tts.voice.ru-RU-DmitryNeural": "ru-RU-DmitryNeural",
"tts.voice.ru-RU-SvetlanaNeural": "ru-RU-SvetlanaNeural",
"tts.voice.tr-TR-AhmetNeural": "tr-TR-AhmetNeural",
"tts.voice.tr-TR-EmelNeural": "tr-TR-EmelNeural",
"tts.voice.es-ES-AlvaroNeural": "es-ES-AlvaroNeural",
"tts.voice.es-ES-ElviraNeural": "es-ES-ElviraNeural",
"tts.selector": "Workflow Selection",
"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)",
@@ -348,6 +348,42 @@
"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 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",
+26
View File
@@ -348,6 +348,8 @@
"pipeline.quick_create.description": "输入一个想法,AI帮你完成整条视频",
"pipeline.custom_media.name": "自定义素材",
"pipeline.custom_media.description": "用你自己的照片/视频,AI帮你配文案和配音",
"pipeline.digital_human.name": "数字人生成",
"pipeline.digital_human.description": "用一段文本、两张图片、一段音频,AI帮你生成一个数字人视频",
"asset_based.section.assets": "📦 素材上传",
"asset_based.section.video_info": "📝 视频信息",
"asset_based.section.source": "⚙️ 服务配置",
@@ -385,6 +387,30 @@
"asset_based.progress.generating_script": "📝 正在生成视频脚本...",
"asset_based.progress.script_complete": "✅ 脚本生成完成",
"asset_based.progress.concat_complete": "✅ 视频合成完成",
"digital_human.section.character_assets": "😊 人物形象上传",
"digital_human.assets.character_what": "上传数字人形象的图片",
"digital_human.assets.how": "支持 JPG/PNG/WebP 等格式图片,建议素材清晰且内容相关",
"digital_human.assets.upload": "上传素材",
"digital_human.assets.upload_help": "仅支持单张图片上传",
"digital_human.assets.character_sucess": "上传形象图片成功",
"digital_human.assets.preview": "📷 素材预览",
"digital_human.assets.character_empty_hint": "💡 请上传一个数字人形象图片素材",
"digital_human.section.goods_assets": "👝 商品图片上传",
"digital_human.assets.goods_what": "上传商品的图片",
"digital_human.assets.goods_sucess": "上传商品图片成功",
"digital_human.assets.goods_empty_hint": "💡 请上传一个商品图片素材",
"digital_human.section.goods_info": "🗃️ 商品信息",
"digital_human.goods_title": "商品名称",
"digital_human.goods_title_placeholder": "例如:口红、咖啡机、扫地机器人等",
"digital_human.goods_title_help": "商品的名称,不可为空",
"digital_human.section.workflow": "⚙️ 工作流加载",
"digital_human.workflow.what": "配置数字人视频生成的两步工作流:第一步生成拼图和文案,第二步生成最终视频",
"digital_human.workflow.first_step": "口播搞&拼图工作流成功加载",
"digital_human.workflow.second_step": "数字人合成工作流成功加载",
"digital_human.workflow.ready": "数字人工作流已就绪,可以开始生成视频",
"digital_human.workflow.missing": "缺少必要的数字人工作流,请检查配置",
"digital_tts.what": "将旁白文本转换为真人般的自然语音",
"faq.expand_to_view": "常见问题",
"faq.load_error": "无法加载常见问题内容",
"faq.more_help": "需要更多帮助?"
+1
View File
@@ -26,6 +26,7 @@ from web.pipelines.base import (
# Import all pipeline UI modules to ensure they register themselves
from web.pipelines import standard
from web.pipelines import asset_based
from web.pipelines import digital_human
__all__ = [
"PipelineUI",
+644
View File
@@ -0,0 +1,644 @@
import os
import time
import uuid
from pathlib import Path
from typing import Any
import streamlit as st
from loguru import logger
import httpx
from web.i18n import tr, get_language
from web.pipelines.base import PipelineUI, register_pipeline_ui
from web.components.content_input import render_bgm_section, render_version_info
from web.components.digital_tts_config import render_style_config
from web.utils.async_helpers import run_async
from pixelle_video.config import config_manager
from pixelle_video.utils.os_util import create_task_output_dir
class DigitalHumanPipelineUI(PipelineUI):
"""
UI for the Digital_Human Video Generation Pipeline.
Generates videos from user-provided assets (images&videos&audio).
"""
name = "digital_human"
icon = "🤖"
@property
def display_name(self):
return tr("pipeline.digital_human.name")
@property
def description(self):
return tr("pipeline.digital_human.description")
def render(self, pixelle_video: Any):
# Three-column layout
left_col, middle_col, right_col = st.columns([1, 1, 1])
# ====================================================================
# Left Column: Asset Upload
# ====================================================================
with left_col:
asset_params = self.render_digital_human_input()
# bgm_params = render_bgm_section(key_prefix="asset_")
render_version_info()
# ====================================================================
# Middle Column: Video Configuration
# ====================================================================
with middle_col:
# Style configuration (TTS, template, workflow, etc.)
style_params = render_style_config(pixelle_video)
# # ====================================================================
# # Right Column: Output Preview
# # ====================================================================
with right_col:
# Combine all parameters
video_params = {
**asset_params,
**style_params
}
self._render_output_preview(pixelle_video, video_params)
def render_digital_human_input(self) -> dict:
"""Render digital human character image upload section"""
with st.container(border=True):
st.markdown(f"**{tr('digital_human.section.character_assets')}**")
with st.expander(tr("help.feature_description"), expanded=False):
st.markdown(f"**{tr('help.what')}**")
st.markdown(tr("digital_human.assets.character_what"))
st.markdown(f"**{tr('help.how')}**")
st.markdown(tr("digital_human.assets.how"))
# File uploader for multiple files
uploaded_files = st.file_uploader(
tr("digital_human.assets.upload"),
type=["jpg", "jpeg", "png", "webp"],
accept_multiple_files=True,
help=tr("digital_human.assets.upload_help"),
key="character_files"
)
# Save uploaded files to temp directory with unique session ID
character_asset_paths = []
if uploaded_files:
import uuid
session_id = str(uuid.uuid4()).replace('-', '')[:12]
temp_dir = Path(f"temp/assets_{session_id}")
temp_dir.mkdir(parents=True, exist_ok=True)
for uploaded_file in uploaded_files:
file_path = temp_dir / uploaded_file.name
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
character_asset_paths.append(str(file_path.absolute()))
st.success(tr("digital_human.assets.character_sucess"))
# Preview uploaded assets
with st.expander(tr("digital_human.assets.preview"), expanded=True):
# Show in a grid (3 columns)
cols = st.columns(3)
for i, (file, path) in enumerate(zip(uploaded_files, character_asset_paths)):
with cols[i % 3]:
# Check if image
ext = Path(path).suffix.lower()
if ext in [".jpg", ".jpeg", ".png", ".webp"]:
st.image(file, caption=file.name, use_container_width=True)
else:
st.info(tr("digital_human.assets.character_empty_hint"))
with st.container(border=True):
st.markdown(f"**{tr('digital_human.section.goods_assets')}**")
with st.expander(tr("help.feature_description"), expanded=False):
st.markdown(f"**{tr('help.what')}**")
st.markdown(tr("digital_human.assets.goods_what"))
st.markdown(f"**{tr('help.how')}**")
st.markdown(tr("digital_human.assets.how"))
# File uploader for multiple files
uploaded_files = st.file_uploader(
tr("digital_human.assets.upload"),
type=["jpg", "jpeg", "png", "webp"],
accept_multiple_files=True,
help=tr("digital_human.assets.upload_help"),
key="goods_files"
)
# Save uploaded files to temp directory with unique session ID
goods_asset_paths = []
if uploaded_files:
import uuid
session_id = str(uuid.uuid4()).replace('-', '')[:12]
temp_dir = Path(f"temp/assets_{session_id}")
temp_dir.mkdir(parents=True, exist_ok=True)
for uploaded_file in uploaded_files:
file_path = temp_dir / uploaded_file.name
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
goods_asset_paths.append(str(file_path.absolute()))
st.success(tr("digital_human.assets.goods_sucess"))
# Preview uploaded assets
with st.expander(tr("digital_human.assets.preview"), expanded=True):
# Show in a grid (3 columns)
cols = st.columns(3)
for i, (file, path) in enumerate(zip(uploaded_files, goods_asset_paths)):
with cols[i % 3]:
# Check if image
ext = Path(path).suffix.lower()
if ext in [".jpg", ".jpeg", ".png", ".webp"]:
st.image(file, caption=file.name, use_container_width=True)
else:
st.info(tr("digital_human.assets.goods_empty_hint"))
# Goods Name or Description
with st.container(border=True):
st.markdown(f"**{tr('digital_human.section.goods_info')}**")
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,
"goods_assets": goods_asset_paths
}
def _render_output_preview(self, pixelle_video: Any, video_params: dict):
"""Render output preview section"""
with st.container(border=True):
st.markdown(f"**{tr('section.video_generation')}**")
# Check configuration
if not config_manager.validate():
st.warning(tr("settings.not_configured"))
# Get input data
character_assets = video_params.get("character_assets", [])
goods_assets = video_params.get("goods_assets", [])
goods_title = video_params.get("goods_title", "")
first_workflow = video_params.get("first_workflow")
second_workflow = video_params.get("second_workflow")
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}")
logger.info(f" - video_params中的tts_voice: {video_params.get('tts_voice', 'NOT_FOUND')}")
logger.info(f" - 所有video_params: {video_params}")
# Validation
if not character_assets or not goods_assets:
st.info("请上传人物形象图片和商品图片")
st.button(
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True,
key="digital_human_generate_disabled"
)
return
if not goods_title:
st.warning("请输入商品名称")
st.button(
tr("btn.generate"),
type="primary",
use_container_width=True,
disabled=True,
key="digital_human_generate_disabled_no_title"
)
return
# Workflow validation
if not first_workflow or not second_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
if not config_manager.validate():
st.error(tr("settings.not_configured"))
st.stop()
# Show progress
progress_bar = st.progress(0)
status_text = st.empty()
start_time = time.time()
try:
# Define async generation function
async def generate_digital_human_video():
# Create task directory
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
import json
from pathlib import Path
first_workflow_path = Path("workflows/runninghub/digital_image.json")
if not first_workflow_path.exists():
raise Exception(f"第一步工作流文件不存在: {first_workflow_path}")
with open(first_workflow_path, 'r', encoding='utf-8') as f:
first_workflow_config = json.load(f)
# Build workflow parameters for first workflow
# 注意:参数名需要根据你的 workflow 实际参数来调整
# 尝试多种可能的参数名
first_workflow_params = {
# 根据之前的日志尝试的参数名
"firstimage": character_assets[0],
"secondimage": goods_assets[0],
"goodstype": goods_title
}
logger.info(f"🔧 传递给第一步工作流的参数:")
for key, value in first_workflow_params.items():
if isinstance(value, str) and len(value) > 100:
logger.info(f" - {key}: {value[:100]}... (文件路径)")
else:
logger.info(f" - {key}: {value}")
# Execute first workflow
kit = await pixelle_video._get_or_create_comfykit()
# Use workflow_id for runninghub workflows
if first_workflow_config.get("source") == "runninghub" and "workflow_id" in first_workflow_config:
workflow_input = first_workflow_config["workflow_id"]
else:
workflow_input = str(first_workflow_path)
logger.info(f"执行第一步工作流: {workflow_input}")
first_result = await kit.execute(workflow_input, first_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 = f"为您推荐这款{goods_title},品质优良,值得拥有!"
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) # 简化timeout设置
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, # 使用voice而不是voice_id
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
# 注意:参数名需要根据你的 workflow 实际参数来调整
# 尝试多种可能的参数名
second_workflow_params = {
# 尝试常见的参数名
"videoimage": generated_image_path, # 生成的图片路径
"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) # 简化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
# Execute async generation
final_video_path = run_async(generate_digital_human_video())
total_time = time.time() - start_time
progress_bar.progress(100)
status_text.text(tr("status.success"))
# Display result
st.success(tr("status.video_generated", path=final_video_path))
st.markdown("---")
# Video info
if os.path.exists(final_video_path):
file_size_mb = os.path.getsize(final_video_path) / (1024 * 1024)
info_text = (
f"⏱️ {tr('info.generation_time')} {total_time:.1f}s "
f"📦 {file_size_mb:.2f}MB"
)
st.caption(info_text)
st.markdown("---")
# Video preview
st.video(final_video_path)
# Download button
with open(final_video_path, "rb") as video_file:
video_bytes = video_file.read()
video_filename = os.path.basename(final_video_path)
st.download_button(
label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
data=video_bytes,
file_name=video_filename,
mime="video/mp4",
use_container_width=True
)
else:
st.error(tr("status.video_not_found", path=final_video_path))
except Exception as e:
status_text.text("")
progress_bar.empty()
st.error(tr("status.error", error=str(e)))
logger.exception(e)
st.stop()
# Register self
register_pipeline_ui(DigitalHumanPipelineUI)
@@ -0,0 +1,4 @@
{
"source": "runninghub",
"workflow_id": "2003717471859294210"
}
+4
View File
@@ -0,0 +1,4 @@
{
"source": "runninghub",
"workflow_id": "2004120336125861890"
}