Merge pull request #41 from AIDC-AI/muli_tts

合并muli_tts分支
This commit is contained in:
puke authored and GitHub committed 2026-01-14 22:17:14 +08:00
commit ea04bf8763
11 files changed
+1171 -2

No files matched your search

+86 -2
View File
@@ -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"
},
]
+148
View File
@@ -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
}
+63
View File
@@ -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?"
+63
View File
@@ -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": "需要更多帮助?"
+2
View File
@@ -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",
+256
View File
@@ -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)
+537
View File
@@ -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)
+4
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"source": "runninghub",
"workflow_id": "2004120336125861890"
}