11 files changed
+1171
-2
No files matched your search
@@ -70,8 +70,6 @@ EDGE_TTS_VOICES: List[Dict[str, Any]] = [
|
||||
"locale": "zh-CN",
|
||||
"gender": "female"
|
||||
},
|
||||
|
||||
# English voices
|
||||
{
|
||||
"id": "en-US-AriaNeural",
|
||||
"label_key": "tts.voice.en_US_AriaNeural",
|
||||
@@ -108,6 +106,92 @@ EDGE_TTS_VOICES: List[Dict[str, Any]] = [
|
||||
"locale": "en-GB",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "ko-KR-InJoonNeural",
|
||||
"label_key": "tts.voice.ko-KR-InJoonNeural",
|
||||
"locale": "ko-KR",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "ko-KR-SunHiNeural",
|
||||
"label_key": "tts.voice.ko-KR-SunHiNeural",
|
||||
"locale": "ko-KR",
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"id": "fr-FR-EloiseNeural",
|
||||
"label_key": "tts.voice.fr-FR-EloiseNeural",
|
||||
"locale": "fr-FR",
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"id": "fr-FR-HenriNeural",
|
||||
"label_key": "tts.voice.fr-FR-HenriNeural",
|
||||
"locale": "fr-FR",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "pt-PT-DuarteNeural",
|
||||
"label_key": "tts.voice.pt-PT-DuarteNeural",
|
||||
"locale": "pt-PT",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "pt-PT-RaquelNeural",
|
||||
"label_key": "tts.voice.pt-PT-RaquelNeural",
|
||||
"locale": "pt-PT",
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"id": "de-DE-AmalaNeural",
|
||||
"label_key": "tts.voice.de-DE-AmalaNeural",
|
||||
"locale": "de-DE",
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"id": "de-DE-ConradNeural",
|
||||
"label_key": "tts.voice.de-DE-ConradNeural",
|
||||
"locale": "de-DE",
|
||||
"gender": "male"
|
||||
},
|
||||
|
||||
# English voices
|
||||
{
|
||||
"id": "ru-RU-DmitryNeural",
|
||||
"label_key": "tts.voice.ru-RU-DmitryNeural",
|
||||
"locale": "ru-RU",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "ru-RU-SvetlanaNeural",
|
||||
"label_key": "tts.voice.ru-RU-SvetlanaNeural",
|
||||
"locale": "ru-RU",
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"id": "tr-TR-AhmetNeural",
|
||||
"label_key": "tts.voice.tr-TR-AhmetNeural",
|
||||
"locale": "tr-TR",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "tr-TR-EmelNeural",
|
||||
"label_key": "tts.voice.tr-TR-EmelNeural",
|
||||
"locale": "tr-TR",
|
||||
"gender": "female"
|
||||
},
|
||||
{
|
||||
"id": "es-ES-AlvaroNeural",
|
||||
"label_key": "tts.voice.es-ES-AlvaroNeural",
|
||||
"locale": "es-ES",
|
||||
"gender": "male"
|
||||
},
|
||||
{
|
||||
"id": "es-ES-ElviraNeural",
|
||||
"label_key": "tts.voice.es-ES-ElviraNeural",
|
||||
"locale": "es-ES",
|
||||
"gender": "female"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# 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"]
|
||||
|
||||
tts_mode = "local"
|
||||
|
||||
# ================================================================
|
||||
# Local Mode UI (simple)
|
||||
# ================================================================
|
||||
# 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 (only local mode)
|
||||
# ================================================================
|
||||
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)
|
||||
|
||||
# Return all style configuration parameters (Simplified version only local TTS)
|
||||
return {
|
||||
"tts_inference_mode": "local",
|
||||
"tts_voice": selected_voice,
|
||||
"tts_speed": tts_speed
|
||||
}
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
"input_mode.custom": "✍️ Custom Content",
|
||||
"mode.generate": "💡 AI Creation",
|
||||
"mode.fixed": "✍️ Custom Script",
|
||||
"mode.digital": "💻 Product placement mode",
|
||||
"mode.customize": "🧐 Custom mode",
|
||||
"input.topic": "Topic",
|
||||
"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 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)",
|
||||
@@ -160,6 +164,7 @@
|
||||
"progress.step_video": "Creating video segment",
|
||||
"progress.concatenating": "Concatenating video...",
|
||||
"progress.finalizing": "Finalizing...",
|
||||
"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",
|
||||
@@ -223,6 +228,20 @@
|
||||
"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": "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)",
|
||||
@@ -354,6 +373,10 @@
|
||||
"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 Information",
|
||||
"asset_based.section.source": "⚙️ Service Configuration",
|
||||
@@ -391,6 +414,46 @@
|
||||
"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": "😊 Character Image Upload",
|
||||
"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": "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 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 digital human character image",
|
||||
"digital_human.section.select_mode": "💫 Select Generation Mode",
|
||||
"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",
|
||||
"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 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",
|
||||
"faq.more_help": "Need more help?"
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
"input_mode.custom": "✍️ 自定义内容",
|
||||
"mode.generate": "💡 AI 创作",
|
||||
"mode.fixed": "✍️ 自行创作",
|
||||
"mode.digital" : "💻 带货模式",
|
||||
"mode.customize" : "🧐 自定义模式",
|
||||
"input.topic": "主题",
|
||||
"input.topic_placeholder": "AI 自动创作指定数量的旁白\n例如:如何增加被动收入、How to build passive income",
|
||||
"input.topic_help": "输入一个主题,AI 将根据主题生成内容",
|
||||
"input.text": "文本输入",
|
||||
"input.text_help_generate": "输入主题或话题(AI 将创作旁白)",
|
||||
"input.text_help_fixed": "输入完整的旁白脚本(直接使用,不做改写)",
|
||||
"input.text_help_digital" : "输入商品介绍,若提供了商品介绍则不用再输入商品名称",
|
||||
"input.text_help_audio" : "输入提示词描述,尽可能详细的描绘想要的画面内容细节",
|
||||
"split.mode_label": "分割方式",
|
||||
"split.mode_help": "选择如何将文本分割为视频片段",
|
||||
"split.mode_paragraph": "📄 按段落(\\n\\n)",
|
||||
@@ -159,6 +163,7 @@
|
||||
"progress.step_compose": "合成画面",
|
||||
"progress.step_video": "创建视频片段",
|
||||
"progress.concatenating": "正在拼接视频...",
|
||||
"progress.generation": "正在合成视频...",
|
||||
"progress.finalizing": "完成中...",
|
||||
"progress.completed": "✅ 生成完成",
|
||||
"error.input_required": "❌ 请提供主题或内容",
|
||||
@@ -223,6 +228,20 @@
|
||||
"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 女声-友好(埃洛伊斯)",
|
||||
"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/(云端)文件夹",
|
||||
@@ -354,6 +373,10 @@
|
||||
"pipeline.quick_create.description": "输入一个想法,AI帮你完成整条视频",
|
||||
"pipeline.custom_media.name": "自定义素材",
|
||||
"pipeline.custom_media.description": "用你自己的照片/视频,AI帮你配文案和配音",
|
||||
"pipeline.digital_human.name": "数字人口播",
|
||||
"pipeline.digital_human.description": "用一段文本、两张图片、一段音频,AI帮你生成一个数字人视频",
|
||||
"pipeline.audio-visual.name": "图生视频",
|
||||
"pipeline.audio-visual.description": "输入图片和提示词,AI即刻生成视频",
|
||||
"asset_based.section.assets": "📦 素材上传",
|
||||
"asset_based.section.video_info": "📝 视频信息",
|
||||
"asset_based.section.source": "⚙️ 服务配置",
|
||||
@@ -391,6 +414,46 @@
|
||||
"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.character_warning": "请上传人物形象图片",
|
||||
"digital_human.assets.select_mode": "请选择模式并填入对应信息",
|
||||
"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.select_mode": "💫 选择生成模式",
|
||||
"digital_human.assets.mode_what": "选择需要的数字人生成模式",
|
||||
"digital_human.assets.select_how": "支持智能带货模式以及自定义口播形式",
|
||||
"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": "AI创作旁白",
|
||||
"digital_human.goods_title_placeholder": "例如:口红、咖啡机、扫地机器人等",
|
||||
"digital_human.goods_title_help": "商品的名称,若提供商品名称则AI会自动生成商品旁白,无需再次输入商品描述,若有需要请在下方输入商品介绍",
|
||||
"digital_human.input_text": "口播文案",
|
||||
"digital_human.customize_text": "自定义文本",
|
||||
"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": "将旁白文本转换为真人般的自然语音",
|
||||
"audio_visual.input.topic_placeholder" : "输入创作提示词(例如:音频与节奏、镜头与运动规则、整体视觉风格、色彩与调色等)",
|
||||
"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.prompt_warning": "请输入视频提示词",
|
||||
|
||||
"faq.expand_to_view": "常见问题",
|
||||
"faq.load_error": "无法加载常见问题内容",
|
||||
"faq.more_help": "需要更多帮助?"
|
||||
|
||||
@@ -26,6 +26,8 @@ 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
|
||||
from web.pipelines import audio_visual
|
||||
|
||||
__all__ = [
|
||||
"PipelineUI",
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
from curses import noecho
|
||||
from multiprocessing import process
|
||||
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.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 AudioVisualPipelineUI(PipelineUI):
|
||||
"""
|
||||
UI for the Audio-visual synchronization Video Generation Pipeline.
|
||||
Generates videos from user-provided assets (images&text).
|
||||
"""
|
||||
name = "voice_synchronization"
|
||||
icon = "🎥"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return tr("pipeline.audio-visual.name")
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return tr("pipeline.audio-visual.description")
|
||||
|
||||
def render(self, pixelle_video: Any):
|
||||
# Three-column layout
|
||||
left_col,right_col = st.columns([1, 1])
|
||||
|
||||
# ====================================================================
|
||||
# Middle Column: Asset Upload
|
||||
# ====================================================================
|
||||
with left_col:
|
||||
asset_params = self.render_audio_visual_input()
|
||||
render_version_info()
|
||||
|
||||
with right_col:
|
||||
video_params = {
|
||||
**asset_params
|
||||
}
|
||||
|
||||
self._render_output_preview(pixelle_video, video_params)
|
||||
|
||||
def render_audio_visual_input(self) -> dict:
|
||||
|
||||
# File uploader for multiple files
|
||||
uploaded_files = st.file_uploader(
|
||||
tr("audio_visual.assets.upload"),
|
||||
type=["jpg", "jpeg", "png", "webp"],
|
||||
accept_multiple_files=True,
|
||||
help=tr("audio_visual.assets.upload_help"),
|
||||
key="material_files"
|
||||
)
|
||||
|
||||
# Save uploaded files to temp directory with unique session ID
|
||||
audio_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())
|
||||
audio_asset_paths.append(str(file_path.absolute()))
|
||||
|
||||
st.success(tr("audio_visual.assets.character_sucess"))
|
||||
|
||||
# Preview uploaded assets
|
||||
with st.expander(tr("audio_visual.assets.preview"), expanded=True):
|
||||
# Show in a grid (3 columns)
|
||||
cols = st.columns(3)
|
||||
for i, (file, path) in enumerate(zip(uploaded_files, audio_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("audio_visual.assets.character_empty_hint"))
|
||||
|
||||
prompt_text = st.text_area(
|
||||
tr("audio_visual.input_text"),
|
||||
placeholder=tr("audio_visual.input.topic_placeholder"),
|
||||
height=200,
|
||||
help=tr("input.text_help_audio"),
|
||||
key="audio_box"
|
||||
)
|
||||
|
||||
return {
|
||||
"audio_assets": audio_asset_paths,
|
||||
"prompt_text": prompt_text,
|
||||
}
|
||||
|
||||
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"))
|
||||
|
||||
audio_assets = video_params.get("audio_assets", [])
|
||||
prompt_text = video_params.get("prompt_text", "")
|
||||
|
||||
logger.info(f"video_params: {video_params}")
|
||||
|
||||
if not audio_assets:
|
||||
st.info(tr("audio_visual.assets.image_warning"))
|
||||
st.button(
|
||||
tr("btn.generate"),
|
||||
type="primary",
|
||||
use_container_width=True,
|
||||
disabled=True,
|
||||
key="audio_visual_generate_disabled"
|
||||
)
|
||||
return
|
||||
|
||||
if not prompt_text:
|
||||
st.info(tr("audio_visual.assets.prompt_warning"))
|
||||
st.button(
|
||||
tr("btn.generate"),
|
||||
type="primary",
|
||||
use_container_width=True,
|
||||
disabled=True,
|
||||
key="audio_visual_generate"
|
||||
)
|
||||
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_audio_visual_video():
|
||||
task_dir, task_id = create_task_output_dir()
|
||||
kit = await pixelle_video._get_or_create_comfykit()
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
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"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 = {
|
||||
"image": image_path,
|
||||
"prompt": prompt
|
||||
}
|
||||
if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
|
||||
workflow_input = workflow_config["workflow_id"]
|
||||
else:
|
||||
workflow_input = str(workflow_path)
|
||||
video_result = await kit.execute(workflow_input, workflow_params)
|
||||
|
||||
generated_video_url = None
|
||||
if hasattr(video_result, 'videos') and video_result.videos:
|
||||
generated_video_url = video_result.videos[0]
|
||||
elif hasattr(video_result, 'outputs') and video_result.outputs:
|
||||
for node_id, node_output in video_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 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_audio_visual_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_pipeline_ui(AudioVisualPipelineUI)
|
||||
@@ -0,0 +1,537 @@
|
||||
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()
|
||||
style_params = render_style_config(pixelle_video)
|
||||
# bgm_params = render_bgm_section(key_prefix="asset_")
|
||||
render_version_info()
|
||||
|
||||
# ====================================================================
|
||||
# Middle Column: Video Configuration
|
||||
# ====================================================================
|
||||
with middle_col:
|
||||
# Style configuration ()
|
||||
mode_params = self.render_digital_human_mode(asset_params["character_assets"])
|
||||
|
||||
# # ====================================================================
|
||||
# # Right Column: Output Preview
|
||||
# # ====================================================================
|
||||
with right_col:
|
||||
# Combine all parameters
|
||||
video_params = {
|
||||
**mode_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"))
|
||||
|
||||
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')}**")
|
||||
|
||||
with st.expander(tr("help.feature_description"), expanded=False):
|
||||
st.markdown(f"**{tr('help.what')}**")
|
||||
st.markdown(tr("digital_human.assets.mode_what"))
|
||||
st.markdown(f"**{tr('help.how')}**")
|
||||
st.markdown(tr("digital_human.assets.select_how"))
|
||||
|
||||
mode = st.radio(
|
||||
"Processing Mode",
|
||||
["digital", "customize"],
|
||||
horizontal=True,
|
||||
format_func=lambda x: tr(f"mode.{x}"),
|
||||
label_visibility="collapsed",
|
||||
key="mode_selection"
|
||||
)
|
||||
|
||||
# Text input (unified for both modes)
|
||||
text_placeholder = tr("digital_human.input.topic_placeholder") if mode == "digital" else tr("digital_human.input.content_placeholder")
|
||||
text_height = 120 if mode == "digital" else 200
|
||||
text_help = tr("input.text_help_digital") if mode == "digital" else tr("input.text_help_fixed")
|
||||
|
||||
if mode == "digital":
|
||||
# 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="digital_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"))
|
||||
# Text input
|
||||
goods_text = st.text_area(
|
||||
tr("digital_human.input_text"),
|
||||
placeholder=text_placeholder,
|
||||
height=text_height,
|
||||
help=text_help,
|
||||
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,
|
||||
"goods_assets": goods_asset_paths,
|
||||
"goods_text": goods_text,
|
||||
"mode": mode
|
||||
}
|
||||
|
||||
else:
|
||||
goods_text = st.text_area(
|
||||
tr("digital_human.customize_text"),
|
||||
placeholder=text_placeholder,
|
||||
height=text_height,
|
||||
help=text_help,
|
||||
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):
|
||||
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", "")
|
||||
goods_text = video_params.get("goods_text", "")
|
||||
mode = video_params.get("mode")
|
||||
tts_voice = video_params.get("tts_voice", "zh-CN-YunjianNeural")
|
||||
tts_speed = video_params.get("tts_speed", 1.2)
|
||||
|
||||
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}")
|
||||
|
||||
# Validation
|
||||
if not character_assets:
|
||||
st.info(tr("digital_human.assets.character_warning"))
|
||||
st.button(
|
||||
tr("btn.generate"),
|
||||
type="primary",
|
||||
use_container_width=True,
|
||||
disabled=True,
|
||||
key="digital_human_generate_disabled"
|
||||
)
|
||||
return
|
||||
|
||||
if not goods_text and not goods_title:
|
||||
st.info(tr("digital_human.assets.select_mode"))
|
||||
st.button(
|
||||
tr("btn.generate"),
|
||||
type="primary",
|
||||
use_container_width=True,
|
||||
disabled=True,
|
||||
key="digital_human_generate"
|
||||
)
|
||||
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():
|
||||
task_dir, task_id = create_task_output_dir()
|
||||
kit = await pixelle_video._get_or_create_comfykit()
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
if mode == "customize":
|
||||
status_text.text(tr("progress.step_audio"))
|
||||
progress_bar.progress(25)
|
||||
generated_image_path = character_assets[0]
|
||||
generated_text = goods_text
|
||||
|
||||
# TTS
|
||||
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"))
|
||||
|
||||
# Directly call the second workflow
|
||||
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_path,
|
||||
"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
|
||||
|
||||
else:
|
||||
#Initialization and parameter preparation
|
||||
task_dir, task_id = create_task_output_dir()
|
||||
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")
|
||||
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 goods_text and goods_text.strip():
|
||||
workflow_path = third_workflow_path
|
||||
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"))
|
||||
|
||||
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
|
||||
|
||||
else:
|
||||
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"))
|
||||
|
||||
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())
|
||||
|
||||
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": "2011258580393009153"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"source": "runninghub",
|
||||
"workflow_id": "2003717471859294210"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"source": "runninghub",
|
||||
"workflow_id": "2010608838151507970"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"source": "runninghub",
|
||||
"workflow_id": "2004120336125861890"
|
||||
}
|
||||
Reference in new issue
Block a user