返回 Pixelle-Video
digital_human.py
根目录 / web / pipelines / digital_human.py
1 import os
2 import time
3 from pathlib import Path
4 from typing import Any
5
6 import streamlit as st
7 from loguru import logger
8 import httpx
9 from web.i18n import tr, get_language
10 from web.pipelines.base import PipelineUI, register_pipeline_ui
11 from web.pipelines.api_workflows import (
12 list_api_media_workflows,
13 render_api_video_controls,
14 workflow_select_help,
15 workflow_source_help,
16 workflow_source_label,
17 )
18 from web.components.content_input import render_version_info
19 from web.components.digital_tts_config import render_style_config
20 from web.utils.async_helpers import run_async
21 from web.utils.history_persistence import save_web_generation_history
22 from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow
23 from pixelle_video.config import config_manager
24 from pixelle_video.utils.os_util import create_task_output_dir
25
26 class DigitalHumanPipelineUI(PipelineUI):
27 """
28 UI for the Digital_Human Video Generation Pipeline.
29 Generates videos from user-provided assets (images&videos&audio).
30 """
31 name = "digital_human"
32 icon = "🤖"
33
34 @property
35 def display_name(self):
36 return tr("pipeline.digital_human.name")
37
38 @property
39 def description(self):
40 return tr("pipeline.digital_human.description")
41
42 def render(self, pixelle_video: Any):
43 # Three-column layout
44 left_col, middle_col, right_col = st.columns([1, 1, 1])
45
46 # ====================================================================
47 # Left Column: Asset Upload
48 # ====================================================================
49 with left_col:
50 asset_params = self.render_digital_human_input()
51 style_params = render_style_config(pixelle_video)
52 # bgm_params = render_bgm_section(key_prefix="asset_")
53 render_version_info()
54
55 # ====================================================================
56 # Middle Column: Video Configuration
57 # ====================================================================
58 with middle_col:
59 # Style configuration ()
60 workflow_path = self.workflow_path_config(pixelle_video)
61 mode_params = self.render_digital_human_mode(asset_params["character_assets"])
62
63 # ====================================================================
64 # Right Column: Output Preview
65 # ====================================================================
66 with right_col:
67 # Combine all parameters
68 video_params = {
69 **mode_params,
70 **asset_params,
71 **style_params,
72 "workflow_path": workflow_path
73 }
74
75 self._render_output_preview(pixelle_video, video_params)
76
77 def render_digital_human_input(self) -> dict:
78 """Render digital human character image upload section"""
79 with st.container(border=True):
80 st.markdown(f"**{tr('digital_human.section.character_assets')}**")
81
82 with st.expander(tr("help.feature_description"), expanded=False):
83 st.markdown(f"**{tr('help.what')}**")
84 st.markdown(tr("digital_human.assets.character_what"))
85 st.markdown(f"**{tr('help.how')}**")
86 st.markdown(tr("digital_human.assets.how"))
87
88 # File uploader for multiple files
89 uploaded_files = st.file_uploader(
90 tr("digital_human.assets.upload"),
91 type=["jpg", "jpeg", "png", "webp"],
92 accept_multiple_files=True,
93 help=tr("digital_human.assets.upload_help"),
94 key="character_files"
95 )
96
97 # Save uploaded files to temp directory with unique session ID
98 character_asset_paths = []
99 if uploaded_files:
100 import uuid
101 session_id = str(uuid.uuid4()).replace('-', '')[:12]
102 temp_dir = Path(f"temp/assets_{session_id}")
103 temp_dir.mkdir(parents=True, exist_ok=True)
104
105 for uploaded_file in uploaded_files:
106 file_path = temp_dir / uploaded_file.name
107 with open(file_path, "wb") as f:
108 f.write(uploaded_file.getbuffer())
109 character_asset_paths.append(str(file_path.absolute()))
110
111 st.success(tr("digital_human.assets.character_sucess"))
112
113 # Preview uploaded assets
114 with st.expander(tr("digital_human.assets.preview"), expanded=True):
115 # Show in a grid (3 columns)
116 cols = st.columns(3)
117 for i, (file, path) in enumerate(zip(uploaded_files, character_asset_paths)):
118 with cols[i % 3]:
119 # Check if image
120 ext = Path(path).suffix.lower()
121 if ext in [".jpg", ".jpeg", ".png", ".webp"]:
122 st.image(file, caption=file.name, use_container_width=True)
123 else:
124 st.info(tr("digital_human.assets.character_empty_hint"))
125
126 return {"character_assets": character_asset_paths}
127
128 def workflow_path_config(self, pixelle_video: Any) -> dict:
129 # Workflow source selection
130 with st.container(border=True):
131 st.markdown(f"**{tr('asset_based.section.source')}**")
132
133 with st.expander(tr("help.feature_description"), expanded=False):
134 st.markdown(f"**{tr('help.what')}**")
135 st.markdown(tr("asset_based.source.what"))
136 st.markdown(f"**{tr('help.how')}**")
137 st.markdown(tr("asset_based.source.how"))
138
139 source_options = {
140 "runninghub": tr("asset_based.source.runninghub"),
141 "selfhost": tr("asset_based.source.selfhost"),
142 "api": workflow_source_label("api"),
143 }
144
145 # Check if RunningHub API key is configured
146 comfyui_config = config_manager.get_comfyui_config()
147 has_runninghub = bool(comfyui_config.get("runninghub_api_key"))
148 has_selfhost = bool(comfyui_config.get("comfyui_url"))
149
150 workflow_config = {
151 "first_workflow_path": "workflows/runninghub/digital_image.json",
152 "second_workflow_path": "workflows/runninghub/digital_combination.json",
153 "third_workflow_path": "workflows/runninghub/digital_customize.json",
154 }
155
156 def digital_image_workflows(source_name: str) -> list[dict]:
157 first_path = Path("workflows") / source_name / "digital_image.json"
158 third_path = Path("workflows") / source_name / "digital_customize.json"
159 if not first_path.exists() or not third_path.exists():
160 return []
161 return [
162 {
163 "key": f"{source_name}/digital_image.json",
164 "display_name": f"digital_image.json - {source_name.title()}",
165 "first_workflow_path": str(first_path),
166 "third_workflow_path": str(third_path),
167 }
168 ]
169
170 def digital_video_workflows(source_name: str) -> list[dict]:
171 second_path = Path("workflows") / source_name / "digital_combination.json"
172 if not second_path.exists():
173 return []
174 return [
175 {
176 "key": f"{source_name}/digital_combination.json",
177 "display_name": f"digital_combination.json - {source_name.title()}",
178 "second_workflow_path": str(second_path),
179 }
180 ]
181
182 api_image_workflows = list_api_media_workflows(pixelle_video, "image")
183 image_source_options = []
184 if digital_image_workflows("runninghub"):
185 image_source_options.append("runninghub")
186 if digital_image_workflows("selfhost"):
187 image_source_options.append("selfhost")
188 if api_image_workflows:
189 image_source_options.append("api")
190
191 workflow_config["api_image_workflow"] = None
192 if st.session_state.get("digital_human_image_service_source") not in image_source_options:
193 st.session_state.pop("digital_human_image_service_source", None)
194 image_service_source = st.radio(
195 "前置图片生成服务" if get_language() == "zh_CN" else "Pre-image generation service",
196 image_source_options,
197 format_func=lambda x: source_options[x],
198 horizontal=True,
199 key="digital_human_image_service_source",
200 help=workflow_source_help("前置图片生成" if get_language() == "zh_CN" else "pre-image generation"),
201 )
202
203 image_workflows = []
204 if image_service_source in {"runninghub", "selfhost"}:
205 if image_service_source == "runninghub" and not has_runninghub:
206 st.warning(tr("asset_based.source.runninghub_not_configured"))
207 if image_service_source == "selfhost" and not has_selfhost:
208 st.warning(tr("asset_based.source.selfhost_not_configured"))
209
210 image_workflows = digital_image_workflows(image_service_source)
211 elif image_service_source == "api":
212 if not api_image_workflows:
213 st.warning(
214 "没有找到 API 图片模型,请先配置图像模型提供商。"
215 if get_language() == "zh_CN"
216 else "No API image model found. Configure an image provider first."
217 )
218 else:
219 image_workflows = api_image_workflows
220
221 image_options = [wf["display_name"] for wf in image_workflows]
222 selected_image_workflow = st.selectbox(
223 "前置图片工作流/模型" if get_language() == "zh_CN" else "Pre-image workflow/model",
224 image_options if image_options else ["No workflow/model available"],
225 index=0,
226 key="digital_human_image_workflow",
227 disabled=not image_options,
228 help=workflow_select_help(),
229 )
230 if image_options:
231 selected_index = image_options.index(selected_image_workflow)
232 selected_workflow = image_workflows[selected_index]
233 if image_service_source == "api":
234 workflow_config["api_image_workflow"] = selected_workflow["key"]
235 else:
236 workflow_config["first_workflow_path"] = selected_workflow["first_workflow_path"]
237 workflow_config["third_workflow_path"] = selected_workflow["third_workflow_path"]
238
239 workflow_config["api_video_workflow"] = None
240 workflow_config["api_video_params"] = {}
241 api_video_workflows = list_api_media_workflows(
242 pixelle_video,
243 "video",
244 required_adapter_abilities=["digital_human"],
245 verified_only=True,
246 )
247 video_source_options = []
248 if digital_video_workflows("runninghub"):
249 video_source_options.append("runninghub")
250 if digital_video_workflows("selfhost"):
251 video_source_options.append("selfhost")
252 if api_video_workflows:
253 video_source_options.append("api")
254
255 if st.session_state.get("digital_human_video_service_source") not in video_source_options:
256 st.session_state.pop("digital_human_video_service_source", None)
257 video_service_source = st.radio(
258 "口播视频合成服务" if get_language() == "zh_CN" else "Talking-video synthesis service",
259 video_source_options,
260 format_func=lambda x: source_options[x],
261 horizontal=True,
262 key="digital_human_video_service_source",
263 help=workflow_source_help("口播视频合成" if get_language() == "zh_CN" else "talking-video synthesis"),
264 )
265
266 video_workflows = []
267 if video_service_source in {"runninghub", "selfhost"}:
268 if video_service_source == "runninghub" and not has_runninghub:
269 st.warning(tr("asset_based.source.runninghub_not_configured"))
270 if video_service_source == "selfhost" and not has_selfhost:
271 st.warning(tr("asset_based.source.selfhost_not_configured"))
272
273 video_workflows = digital_video_workflows(video_service_source)
274 elif video_service_source == "api":
275 if not api_video_workflows:
276 st.warning(
277 "没有找到已验证的 API 参考生视频模型,请先配置 DashScope 等提供商。"
278 if get_language() == "zh_CN"
279 else "No verified API reference-to-video model found. Configure a provider first."
280 )
281 else:
282 video_workflows = api_video_workflows
283
284 video_options = [wf["display_name"] for wf in video_workflows]
285 selected_video_workflow = st.selectbox(
286 "口播视频工作流/模型" if get_language() == "zh_CN" else "Talking-video workflow/model",
287 video_options if video_options else ["No workflow/model available"],
288 index=0,
289 key="digital_human_video_workflow",
290 disabled=not video_options,
291 help=workflow_select_help(),
292 )
293 if video_options:
294 selected_index = video_options.index(selected_video_workflow)
295 selected_workflow = video_workflows[selected_index]
296 if video_service_source == "api":
297 workflow_config["api_video_workflow"] = selected_workflow["key"]
298 workflow_config["api_video_params"] = render_api_video_controls(
299 selected_workflow,
300 key_prefix="digital_human",
301 default_duration=5,
302 )
303 else:
304 workflow_config["second_workflow_path"] = selected_workflow["second_workflow_path"]
305
306 missing_workflows = [
307 path for key, path in workflow_config.items()
308 if key.endswith("_workflow_path") and isinstance(path, str) and not Path(path).exists()
309 ]
310 if missing_workflows:
311 st.warning(
312 (
313 "当前选择缺少数字人口播工作流文件:"
314 + "、".join(missing_workflows)
315 )
316 if get_language() == "zh_CN"
317 else "The current selection is missing digital-human workflow files: "
318 + ", ".join(missing_workflows)
319 )
320
321 return workflow_config
322
323 def render_digital_human_mode(self, character_asset_paths: list) -> dict:
324 with st.container(border=True):
325 st.markdown(f"**{tr('digital_human.section.select_mode')}**")
326
327 with st.expander(tr("help.feature_description"), expanded=False):
328 st.markdown(f"**{tr('help.what')}**")
329 st.markdown(tr("digital_human.assets.mode_what"))
330 st.markdown(f"**{tr('help.how')}**")
331 st.markdown(tr("digital_human.assets.select_how"))
332
333 mode = st.radio(
334 "Processing Mode",
335 ["digital", "customize"],
336 horizontal=True,
337 format_func=lambda x: tr(f"mode.{x}"),
338 label_visibility="collapsed",
339 key="mode_selection"
340 )
341
342 # Text input (unified for both modes)
343 text_placeholder = tr("digital_human.input.topic_placeholder") if mode == "digital" else tr("digital_human.input.content_placeholder")
344 text_height = 120 if mode == "digital" else 200
345 text_help = tr("input.text_help_digital") if mode == "digital" else tr("input.text_help_fixed")
346
347 if mode == "digital":
348 # File uploader for multiple files
349 uploaded_files = st.file_uploader(
350 tr("digital_human.assets.upload"),
351 type=["jpg", "jpeg", "png", "webp"],
352 accept_multiple_files=True,
353 help=tr("digital_human.assets.upload_help"),
354 key="digital_files"
355 )
356
357 # Save uploaded files to temp directory with unique session ID
358 goods_asset_paths = []
359 if uploaded_files:
360 import uuid
361 session_id = str(uuid.uuid4()).replace('-', '')[:12]
362 temp_dir = Path(f"temp/assets_{session_id}")
363 temp_dir.mkdir(parents=True, exist_ok=True)
364
365 for uploaded_file in uploaded_files:
366 file_path = temp_dir / uploaded_file.name
367 with open(file_path, "wb") as f:
368 f.write(uploaded_file.getbuffer())
369 goods_asset_paths.append(str(file_path.absolute()))
370
371 st.success(tr("digital_human.assets.goods_sucess"))
372
373 # Preview uploaded assets
374 with st.expander(tr("digital_human.assets.preview"), expanded=True):
375 # Show in a grid (3 columns)
376 cols = st.columns(3)
377 for i, (file, path) in enumerate(zip(uploaded_files, goods_asset_paths)):
378 with cols[i % 3]:
379 # Check if image
380 ext = Path(path).suffix.lower()
381 if ext in [".jpg", ".jpeg", ".png", ".webp"]:
382 st.image(file, caption=file.name, use_container_width=True)
383 else:
384 st.info(tr("digital_human.assets.goods_empty_hint"))
385 # Text input
386 goods_text = st.text_area(
387 tr("digital_human.input_text"),
388 placeholder=text_placeholder,
389 height=text_height,
390 help=text_help,
391 key="digital_box"
392 )
393
394 goods_title = st.text_input(
395 tr("digital_human.goods_title"),
396 placeholder=tr("digital_human.goods_title_placeholder"),
397 help=tr("digital_human.goods_title_help"),
398 key="goods_title"
399 )
400
401 return {
402 "character_assets": character_asset_paths,
403 "goods_title": goods_title,
404 "goods_assets": goods_asset_paths,
405 "goods_text": goods_text,
406 "mode": mode
407 }
408
409 else:
410 goods_text = st.text_area(
411 tr("digital_human.customize_text"),
412 placeholder=text_placeholder,
413 height=text_height,
414 help=text_help,
415 key="customize_box"
416 )
417
418 return {
419 "character_assets": character_asset_paths,
420 "goods_text": goods_text,
421 "mode": mode
422 }
423
424 def _render_output_preview(self, pixelle_video: Any, video_params: dict):
425 """Render output preview section"""
426 with st.container(border=True):
427 st.markdown(f"**{tr('section.video_generation')}**")
428
429 # Check configuration
430 if not config_manager.validate():
431 st.warning(tr("settings.not_configured"))
432
433 # Get input data
434 character_assets = video_params.get("character_assets", [])
435 goods_assets = video_params.get("goods_assets", [])
436 goods_title = video_params.get("goods_title", "")
437 goods_text = video_params.get("goods_text", "")
438 mode = video_params.get("mode")
439 tts_voice = video_params.get("tts_voice", "zh-CN-YunjianNeural")
440 tts_speed = video_params.get("tts_speed", 1.2)
441
442 logger.info(f"🔧 The obtained TTS parameters:")
443 logger.info(f" - tts_voice: {tts_voice}")
444 logger.info(f" - tts_speed: {tts_speed}")
445 logger.info(f" - video_params中的tts_voice: {video_params.get('tts_voice', 'NOT_FOUND')}")
446 logger.info(f" - video_params: {video_params}")
447
448 # Validation
449 if not character_assets:
450 st.info(tr("digital_human.assets.character_warning"))
451 st.button(
452 tr("btn.generate"),
453 type="primary",
454 use_container_width=True,
455 disabled=True,
456 key="digital_human_generate_disabled"
457 )
458 return
459
460 if mode == "digital" and not goods_assets:
461 st.info(tr("digital_human.assets.goods_warning"))
462 st.button(
463 tr("btn.generate"),
464 type="primary",
465 use_container_width=True,
466 disabled=True,
467 key="digital_human_goods_vaiidation"
468 )
469 return
470
471 if mode == "digital" and not (goods_text or goods_title):
472 st.info(tr("digital_human.assets.digital_mode"))
473 st.button(
474 tr("btn.generate"),
475 type="primary",
476 use_container_width=True,
477 disabled=True,
478 key="digital_human_digital_disable"
479 )
480 return
481
482 if mode == "digital" and (goods_text or goods_title):
483 st.warning(tr("digital_human.assets.digital_mode_warning"))
484
485 if mode == "customize" and not goods_text:
486 st.info(tr("digital_human.assets.customize_mode"))
487 st.button(
488 tr("btn.generate"),
489 type="primary",
490 use_container_width=True,
491 disabled=True,
492 key="digital_human_customize_disable"
493 )
494 return
495
496 # Generate button
497 if st.button(tr("btn.generate"), type="primary", use_container_width=True, key="digital_human_generate"):
498 # Validate
499 if not config_manager.validate():
500 st.error(tr("settings.not_configured"))
501 st.stop()
502
503 # Show progress
504 progress_bar = st.progress(0)
505 status_text = st.empty()
506
507 start_time = time.time()
508
509 try:
510 # Define async generation function
511 async def generate_digital_human_video():
512 task_dir, task_id = create_task_output_dir()
513 workflow_path = video_params["workflow_path"]
514 api_video_workflow = workflow_path.get("api_video_workflow")
515 api_video_params = dict(workflow_path.get("api_video_params") or {})
516
517 import json
518 from pathlib import Path
519
520 async def generate_tts_reference(text: str) -> str:
521 audio_path = os.path.join(task_dir, "narration.mp3")
522 tts_inference_mode = video_params.get("tts_inference_mode", "local")
523 tts_voice = video_params.get("tts_voice")
524 tts_speed = video_params.get("tts_speed")
525 tts_workflow = video_params.get("tts_workflow")
526 ref_audio = video_params.get("ref_audio")
527
528 tts_kwargs = {
529 "text": text,
530 "output_path": audio_path,
531 "inference_mode": tts_inference_mode,
532 }
533 if tts_inference_mode == "local":
534 tts_kwargs["voice"] = tts_voice
535 tts_kwargs["speed"] = tts_speed
536 elif tts_inference_mode == "comfyui":
537 if tts_workflow:
538 tts_kwargs["workflow"] = tts_workflow
539 if ref_audio:
540 tts_kwargs["ref_audio"] = ref_audio
541
542 await pixelle_video.tts(**tts_kwargs)
543 return audio_path
544
545 async def generate_api_digital_human(text: str) -> str:
546 status_text.text(tr("progress.step_audio"))
547 progress_bar.progress(25)
548 audio_path = await generate_tts_reference(text)
549
550 reference_image_paths = [character_assets[0]]
551 if mode == "digital" and goods_assets:
552 reference_image_paths.append(goods_assets[0])
553
554 subject_prompt = (
555 "参考图1中的人物面对镜头自然口播。"
556 if get_language() == "zh_CN"
557 else "The person in reference image 1 speaks naturally to camera."
558 )
559 if mode == "digital" and goods_assets:
560 subject_prompt += (
561 "结合参考图2中的商品,生成竖屏商业口播视频。"
562 if get_language() == "zh_CN"
563 else "Use the product in reference image 2 and create a vertical product-promotion talking video."
564 )
565 prompt = f"{subject_prompt} 口播文案:{text}"
566
567 final_video_path = os.path.join(task_dir, "final.mp4")
568 duration = int(api_video_params.pop("duration", 5))
569 media_params = {
570 **api_video_params,
571 "prompt": prompt,
572 "workflow": api_video_workflow,
573 "media_type": "video",
574 "output_path": final_video_path,
575 "duration": duration,
576 "reference_image_paths": reference_image_paths,
577 "reference_audio_path": audio_path,
578 "audio": True,
579 "video_ratio": api_video_params.get("video_ratio", "9:16"),
580 }
581 progress_bar.progress(60)
582 status_text.text(tr("progress.generation"))
583 media_result = await pixelle_video.media(**media_params)
584 progress_bar.progress(100)
585 status_text.text(tr("status.success"))
586 return media_result.url
587
588 if api_video_workflow:
589 if mode == "customize":
590 generated_text = goods_text
591 elif goods_text and goods_text.strip():
592 generated_text = goods_text
593 else:
594 generated_text = await pixelle_video.llm(
595 prompt=(
596 f"请为商品“{goods_title}”写一段适合数字人口播短视频的中文推广文案。"
597 "要求自然、有吸引力,控制在80字以内,只输出文案正文。"
598 ),
599 temperature=0.7,
600 max_tokens=300,
601 )
602 return await generate_api_digital_human(generated_text)
603
604 kit = await pixelle_video._get_or_create_comfykit()
605
606 if mode == "customize":
607 status_text.text(tr("progress.step_audio"))
608 progress_bar.progress(25)
609 generated_image_path = character_assets[0]
610 generated_text = goods_text
611
612 # TTS
613 audio_path = os.path.join(task_dir, "narration.mp3")
614 tts_inference_mode = video_params.get("tts_inference_mode", "local")
615 tts_voice = video_params.get("tts_voice")
616 tts_speed = video_params.get("tts_speed")
617 tts_workflow = video_params.get("tts_workflow")
618 ref_audio = video_params.get("ref_audio")
619
620 tts_kwargs = {
621 "text": generated_text,
622 "output_path": audio_path,
623 "inference_mode": tts_inference_mode
624 }
625 if tts_inference_mode == "local":
626 tts_kwargs["voice"] = tts_voice
627 tts_kwargs["speed"] = tts_speed
628 elif tts_inference_mode == "comfyui":
629 if tts_workflow:
630 tts_kwargs["workflow"] = tts_workflow
631 if ref_audio:
632 tts_kwargs["ref_audio"] = ref_audio
633
634 await pixelle_video.tts(**tts_kwargs)
635 progress_bar.progress(65)
636 status_text.text(tr("progress.concatenating"))
637
638 # Directly call the second workflow
639 second_workflow_path = Path(workflow_path.get("second_workflow_path"))
640 if not second_workflow_path.exists():
641 raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
642 with open(second_workflow_path, 'r', encoding='utf-8') as f:
643 second_workflow_config = json.load(f)
644 second_workflow_params = {
645 "videoimage": generated_image_path,
646 "audio": audio_path
647 }
648 if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
649 workflow_input = second_workflow_config["workflow_id"]
650 else:
651 workflow_input = str(second_workflow_config)
652 second_result = await kit.execute(workflow_input, second_workflow_params)
653 # Video Link Extraction
654 generated_video_url = None
655 if hasattr(second_result, 'videos') and second_result.videos:
656 generated_video_url = second_result.videos[0]
657 elif hasattr(second_result, 'outputs') and second_result.outputs:
658 for node_id, node_output in second_result.outputs.items():
659 if isinstance(node_output, dict) and 'videos' in node_output:
660 videos = node_output['videos']
661 if videos and len(videos) > 0:
662 generated_video_url = videos[0]
663 break
664 if not generated_video_url:
665 raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
666
667 final_video_path = os.path.join(task_dir, "final.mp4")
668 timeout = httpx.Timeout(300.0)
669 async with httpx.AsyncClient(timeout=timeout) as client:
670 response = await client.get(generated_video_url)
671 response.raise_for_status()
672 with open(final_video_path, 'wb') as f:
673 f.write(response.content)
674 progress_bar.progress(100)
675 status_text.text(tr("status.success"))
676 return final_video_path
677
678 else:
679 #Initialization and parameter preparation
680 task_dir, task_id = create_task_output_dir()
681 logger.info(f"[Initialization] Task Directory: {task_dir}")
682
683 first_workflow_path = Path(workflow_path.get("first_workflow_path"))
684 third_workflow_path = Path(workflow_path.get("third_workflow_path"))
685 second_workflow_path = Path(workflow_path.get("second_workflow_path"))
686 api_image_workflow = workflow_path.get("api_image_workflow")
687 assert first_workflow_path.exists(), "The first_workflow file does not exist."
688 assert third_workflow_path.exists(), "The third_workflow file does not exist."
689 assert second_workflow_path.exists(), "The second_workflow file does not exist."
690
691 if goods_text and goods_text.strip():
692 generated_text = goods_text
693
694 status_text.text(tr("progress.step_image"))
695 if api_image_workflow:
696 image_prompt = (
697 f"Create a polished digital-human product promotion image. "
698 f"Use the first reference image as the person/character, the second reference image as the product, "
699 f"and make the scene suitable for a short spoken ad. Script: {goods_text}"
700 )
701 generated_image_path = os.path.join(task_dir, "generated_digital_image.png")
702 media_result = await pixelle_video.media(
703 prompt=image_prompt,
704 workflow=api_image_workflow,
705 media_type="image",
706 image_paths=[character_assets[0], goods_assets[0]],
707 output_path=generated_image_path,
708 width=1080,
709 height=1920,
710 )
711 generated_image_url = media_result.url
712 else:
713 workflow_path = third_workflow_path
714 workflow_params = {"firstimage": character_assets[0], "secondimage": goods_assets[0]}
715 kit = await pixelle_video._get_or_create_comfykit()
716 workflow_config = json.load(open(workflow_path, 'r', encoding='utf8'))
717 if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
718 workflow_input = workflow_config["workflow_id"]
719 else:
720 workflow_input = str(workflow_config)
721 combine_image = await kit.execute(workflow_input, workflow_params)
722 if combine_image.status != "completed":
723 raise Exception(f"workflow execution failed: {combine_image.msg}")
724 generated_image_url = getattr(combine_image, "images", [None])[0]
725 status_text.text(tr("progress.step_audio"))
726 audio_path = os.path.join(task_dir, "narration.mp3")
727 tts_inference_mode = video_params.get("tts_inference_mode", "local")
728 tts_voice = video_params.get("tts_voice")
729 tts_speed = video_params.get("tts_speed")
730 tts_workflow = video_params.get("tts_workflow")
731 ref_audio = video_params.get("ref_audio")
732
733 tts_kwargs = {
734 "text": generated_text,
735 "output_path": audio_path,
736 "inference_mode": tts_inference_mode
737 }
738 if tts_inference_mode == "local":
739 tts_kwargs["voice"] = tts_voice
740 tts_kwargs["speed"] = tts_speed
741 elif tts_inference_mode == "comfyui":
742 if tts_workflow:
743 tts_kwargs["workflow"] = tts_workflow
744 if ref_audio:
745 tts_kwargs["ref_audio"] = ref_audio
746
747 await pixelle_video.tts(**tts_kwargs)
748 progress_bar.progress(65)
749 status_text.text(tr("progress.concatenating"))
750
751 if not second_workflow_path.exists():
752 raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
753 with open(second_workflow_path, 'r', encoding='utf-8') as f:
754 second_workflow_config = json.load(f)
755 second_workflow_params = {
756 "videoimage": generated_image_url,
757 "audio": audio_path
758 }
759 if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
760 workflow_input = second_workflow_config["workflow_id"]
761 else:
762 workflow_input = str(second_workflow_config)
763 second_result = await kit.execute(workflow_input, second_workflow_params)
764 # Video Link Extraction
765 generated_video_url = None
766 if hasattr(second_result, 'videos') and second_result.videos:
767 generated_video_url = second_result.videos[0]
768 elif hasattr(second_result, 'outputs') and second_result.outputs:
769 for node_id, node_output in second_result.outputs.items():
770 if isinstance(node_output, dict) and 'videos' in node_output:
771 videos = node_output['videos']
772 if videos and len(videos) > 0:
773 generated_video_url = videos[0]
774 break
775 if not generated_video_url:
776 raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
777
778 final_video_path = os.path.join(task_dir, "final.mp4")
779 timeout = httpx.Timeout(300.0)
780 async with httpx.AsyncClient(timeout=timeout) as client:
781 response = await client.get(generated_video_url)
782 response.raise_for_status()
783 with open(final_video_path, 'wb') as f:
784 f.write(response.content)
785 progress_bar.progress(100)
786 status_text.text(tr("status.success"))
787 return final_video_path
788
789 else:
790 status_text.text(tr("progress.step_image"))
791 if api_image_workflow:
792 image_prompt = (
793 f"Create a polished digital-human product promotion image for '{goods_title}'. "
794 f"Use the first reference image as the person/character and the second reference image as the product. "
795 f"Make it vertical, clean, commercial, and suitable for a spoken short video."
796 )
797 generated_image_path = os.path.join(task_dir, "generated_digital_image.png")
798 media_result = await pixelle_video.media(
799 prompt=image_prompt,
800 workflow=api_image_workflow,
801 media_type="image",
802 image_paths=[character_assets[0], goods_assets[0]],
803 output_path=generated_image_path,
804 width=1080,
805 height=1920,
806 )
807 generated_image_url = media_result.url
808 generated_text = await pixelle_video.llm(
809 prompt=(
810 f"请为商品“{goods_title}”写一段适合数字人口播短视频的中文推广文案。"
811 "要求自然、有吸引力,控制在80字以内,只输出文案正文。"
812 ),
813 temperature=0.7,
814 max_tokens=300,
815 )
816 else:
817 workflow_path = first_workflow_path
818 workflow_params = {"firstimage": character_assets[0], "secondimage": goods_assets[0], "goodstype": goods_title}
819 kit = await pixelle_video._get_or_create_comfykit()
820 workflow_config = json.load(open(workflow_path, 'r', encoding='utf8'))
821 if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
822 workflow_input = workflow_config["workflow_id"]
823 else:
824 workflow_input = str(workflow_config)
825 synthesis_result = await kit.execute(workflow_input, workflow_params)
826 if synthesis_result.status != "completed":
827 raise Exception(f"workflow execution failed: {synthesis_result.msg}")
828 generated_image_url = getattr(synthesis_result, "images", [None])[0]
829 generated_text = getattr(synthesis_result, "texts", [None])[0]
830
831 status_text.text(tr("progress.step_audio"))
832 audio_path = os.path.join(task_dir, "narration.mp3")
833 tts_inference_mode = video_params.get("tts_inference_mode", "local")
834 tts_voice = video_params.get("tts_voice")
835 tts_speed = video_params.get("tts_speed")
836 tts_workflow = video_params.get("tts_workflow")
837 ref_audio = video_params.get("ref_audio")
838
839 tts_kwargs = {
840 "text": generated_text,
841 "output_path": audio_path,
842 "inference_mode": tts_inference_mode
843 }
844 if tts_inference_mode == "local":
845 tts_kwargs["voice"] = tts_voice
846 tts_kwargs["speed"] = tts_speed
847 elif tts_inference_mode == "comfyui":
848 if tts_workflow:
849 tts_kwargs["workflow"] = tts_workflow
850 if ref_audio:
851 tts_kwargs["ref_audio"] = ref_audio
852
853 await pixelle_video.tts(**tts_kwargs)
854 progress_bar.progress(65)
855 status_text.text(tr("progress.concatenating"))
856
857 if not second_workflow_path.exists():
858 raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
859 with open(second_workflow_path, 'r', encoding='utf-8') as f:
860 second_workflow_config = json.load(f)
861 second_workflow_params = {
862 "videoimage": generated_image_url,
863 "audio": audio_path
864 }
865 if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
866 workflow_input = second_workflow_config["workflow_id"]
867 else:
868 workflow_input = str(second_workflow_config)
869 second_result = await kit.execute(workflow_input, second_workflow_params)
870 # Video Link Extraction
871 generated_video_url = None
872 if hasattr(second_result, 'videos') and second_result.videos:
873 generated_video_url = second_result.videos[0]
874 elif hasattr(second_result, 'outputs') and second_result.outputs:
875 for node_id, node_output in second_result.outputs.items():
876 if isinstance(node_output, dict) and 'videos' in node_output:
877 videos = node_output['videos']
878 if videos and len(videos) > 0:
879 generated_video_url = videos[0]
880 break
881 if not generated_video_url:
882 raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
883
884 final_video_path = os.path.join(task_dir, "final.mp4")
885 timeout = httpx.Timeout(300.0)
886 async with httpx.AsyncClient(timeout=timeout) as client:
887 response = await client.get(generated_video_url)
888 response.raise_for_status()
889 with open(final_video_path, 'wb') as f:
890 f.write(response.content)
891 progress_bar.progress(100)
892 status_text.text(tr("status.success"))
893 return final_video_path
894
895 # Execute async generation
896 final_video_path = run_async(generate_digital_human_video())
897 run_async(save_web_generation_history(
898 pixelle_video,
899 task_id=Path(final_video_path).parent.name,
900 video_path=final_video_path,
901 pipeline="digital_human",
902 title="数字人口播" if get_language() == "zh_CN" else "Digital Human",
903 input_params={
904 "text": goods_text or goods_title,
905 "mode": mode,
906 "goods_title": goods_title,
907 "goods_text": goods_text,
908 "character_assets": character_assets,
909 "goods_assets": goods_assets,
910 "workflow_path": video_params.get("workflow_path"),
911 "tts_voice": video_params.get("tts_voice"),
912 "tts_speed": video_params.get("tts_speed"),
913 "tts_inference_mode": video_params.get("tts_inference_mode"),
914 },
915 ))
916
917 total_time = time.time() - start_time
918 progress_bar.progress(100)
919 status_text.text(tr("status.success"))
920
921 # Display result
922 st.success(tr("status.video_generated", path=final_video_path))
923
924 st.markdown("---")
925
926 # Video info
927 if os.path.exists(final_video_path):
928 file_size_mb = os.path.getsize(final_video_path) / (1024 * 1024)
929
930 info_text = (
931 f"⏱️ {tr('info.generation_time')} {total_time:.1f}s "
932 f"📦 {file_size_mb:.2f}MB"
933 )
934 st.caption(info_text)
935
936 st.markdown("---")
937
938 # Video preview
939 st.video(final_video_path)
940
941 # Download button
942 with open(final_video_path, "rb") as video_file:
943 video_bytes = video_file.read()
944 video_filename = os.path.basename(final_video_path)
945 st.download_button(
946 label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
947 data=video_bytes,
948 file_name=video_filename,
949 mime="video/mp4",
950 use_container_width=True
951 )
952 else:
953 st.error(tr("status.video_not_found", path=final_video_path))
954
955 except Exception as e:
956 status_text.text("")
957 progress_bar.empty()
958 st.error(tr("status.error", error=str(e)))
959 logger.exception(e)
960 st.stop()
961
962
963 # Register self
964 register_pipeline_ui(DigitalHumanPipelineUI)
965
965 lines PYTHON