digital_human.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. import os
  2. import time
  3. from pathlib import Path
  4. from typing import Any
  5. import streamlit as st
  6. from loguru import logger
  7. import httpx
  8. from web.i18n import tr, get_language
  9. from web.pipelines.base import PipelineUI, register_pipeline_ui
  10. from web.components.content_input import render_version_info
  11. from web.components.digital_tts_config import render_style_config
  12. from web.utils.async_helpers import run_async
  13. from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow
  14. from pixelle_video.config import config_manager
  15. from pixelle_video.utils.os_util import create_task_output_dir
  16. class DigitalHumanPipelineUI(PipelineUI):
  17. """
  18. UI for the Digital_Human Video Generation Pipeline.
  19. Generates videos from user-provided assets (images&videos&audio).
  20. """
  21. name = "digital_human"
  22. icon = "🤖"
  23. @property
  24. def display_name(self):
  25. return tr("pipeline.digital_human.name")
  26. @property
  27. def description(self):
  28. return tr("pipeline.digital_human.description")
  29. def render(self, pixelle_video: Any):
  30. # Three-column layout
  31. left_col, middle_col, right_col = st.columns([1, 1, 1])
  32. # ====================================================================
  33. # Left Column: Asset Upload
  34. # ====================================================================
  35. with left_col:
  36. asset_params = self.render_digital_human_input()
  37. style_params = render_style_config(pixelle_video)
  38. # bgm_params = render_bgm_section(key_prefix="asset_")
  39. render_version_info()
  40. # ====================================================================
  41. # Middle Column: Video Configuration
  42. # ====================================================================
  43. with middle_col:
  44. # Style configuration ()
  45. workflow_path = self.workflow_path_config()
  46. mode_params = self.render_digital_human_mode(asset_params["character_assets"])
  47. # ====================================================================
  48. # Right Column: Output Preview
  49. # ====================================================================
  50. with right_col:
  51. # Combine all parameters
  52. video_params = {
  53. **mode_params,
  54. **asset_params,
  55. **style_params,
  56. "workflow_path": workflow_path
  57. }
  58. self._render_output_preview(pixelle_video, video_params)
  59. def render_digital_human_input(self) -> dict:
  60. """Render digital human character image upload section"""
  61. with st.container(border=True):
  62. st.markdown(f"**{tr('digital_human.section.character_assets')}**")
  63. with st.expander(tr("help.feature_description"), expanded=False):
  64. st.markdown(f"**{tr('help.what')}**")
  65. st.markdown(tr("digital_human.assets.character_what"))
  66. st.markdown(f"**{tr('help.how')}**")
  67. st.markdown(tr("digital_human.assets.how"))
  68. # File uploader for multiple files
  69. uploaded_files = st.file_uploader(
  70. tr("digital_human.assets.upload"),
  71. type=["jpg", "jpeg", "png", "webp"],
  72. accept_multiple_files=True,
  73. help=tr("digital_human.assets.upload_help"),
  74. key="character_files"
  75. )
  76. # Save uploaded files to temp directory with unique session ID
  77. character_asset_paths = []
  78. if uploaded_files:
  79. import uuid
  80. session_id = str(uuid.uuid4()).replace('-', '')[:12]
  81. temp_dir = Path(f"temp/assets_{session_id}")
  82. temp_dir.mkdir(parents=True, exist_ok=True)
  83. for uploaded_file in uploaded_files:
  84. file_path = temp_dir / uploaded_file.name
  85. with open(file_path, "wb") as f:
  86. f.write(uploaded_file.getbuffer())
  87. character_asset_paths.append(str(file_path.absolute()))
  88. st.success(tr("digital_human.assets.character_sucess"))
  89. # Preview uploaded assets
  90. with st.expander(tr("digital_human.assets.preview"), expanded=True):
  91. # Show in a grid (3 columns)
  92. cols = st.columns(3)
  93. for i, (file, path) in enumerate(zip(uploaded_files, character_asset_paths)):
  94. with cols[i % 3]:
  95. # Check if image
  96. ext = Path(path).suffix.lower()
  97. if ext in [".jpg", ".jpeg", ".png", ".webp"]:
  98. st.image(file, caption=file.name, use_container_width=True)
  99. else:
  100. st.info(tr("digital_human.assets.character_empty_hint"))
  101. return {"character_assets": character_asset_paths}
  102. def workflow_path_config(self) -> dict:
  103. # Workflow source selection
  104. with st.container(border=True):
  105. st.markdown(f"**{tr('asset_based.section.source')}**")
  106. with st.expander(tr("help.feature_description"), expanded=False):
  107. st.markdown(f"**{tr('help.what')}**")
  108. st.markdown(tr("asset_based.source.what"))
  109. st.markdown(f"**{tr('help.how')}**")
  110. st.markdown(tr("asset_based.source.how"))
  111. source_options = {
  112. "runninghub": tr("asset_based.source.runninghub"),
  113. "selfhost": tr("asset_based.source.selfhost")
  114. }
  115. # Check if RunningHub API key is configured
  116. comfyui_config = config_manager.get_comfyui_config()
  117. has_runninghub = bool(comfyui_config.get("runninghub_api_key"))
  118. has_selfhost = bool(comfyui_config.get("comfyui_url"))
  119. # Default to runninghub always
  120. default_source_index = 0
  121. source = st.radio(
  122. tr("asset_based.source.select"),
  123. options=list(source_options.keys()),
  124. format_func=lambda x: source_options[x],
  125. index=default_source_index,
  126. horizontal=True,
  127. key="digital_human_workflow_source",
  128. label_visibility="collapsed"
  129. )
  130. # Initialize workflow_config with default value based on source selection
  131. # This ensures the variable is always defined even if the backend is not configured
  132. if source == "runninghub":
  133. workflow_config = {
  134. "first_workflow_path": "workflows/runninghub/digital_image.json",
  135. "second_workflow_path": "workflows/runninghub/digital_combination.json",
  136. "third_workflow_path": "workflows/runninghub/digital_customize.json"
  137. }
  138. if not has_runninghub:
  139. st.warning(tr("asset_based.source.runninghub_not_configured"))
  140. else:
  141. st.info(tr("asset_based.source.runninghub_hint"))
  142. else:
  143. workflow_config = {
  144. "first_workflow_path": "workflows/selfhost/digital_image.json",
  145. "second_workflow_path": "workflows/selfhost/digital_combination.json",
  146. "third_workflow_path": "workflows/selfhost/digital_customize.json"
  147. }
  148. if not has_selfhost:
  149. st.warning(tr("asset_based.source.selfhost_not_configured"))
  150. else:
  151. st.info(tr("asset_based.source.selfhost_hint"))
  152. # Check and warn for selfhost workflows (auto popup if not confirmed)
  153. # Warn for the first workflow as representative
  154. # TODO: need to check if the workflow is valid
  155. # check_and_warn_selfhost_workflow("selfhost/digital_image.json")
  156. return workflow_config
  157. def render_digital_human_mode(self, character_asset_paths: list) -> dict:
  158. with st.container(border=True):
  159. st.markdown(f"**{tr('digital_human.section.select_mode')}**")
  160. with st.expander(tr("help.feature_description"), expanded=False):
  161. st.markdown(f"**{tr('help.what')}**")
  162. st.markdown(tr("digital_human.assets.mode_what"))
  163. st.markdown(f"**{tr('help.how')}**")
  164. st.markdown(tr("digital_human.assets.select_how"))
  165. mode = st.radio(
  166. "Processing Mode",
  167. ["digital", "customize"],
  168. horizontal=True,
  169. format_func=lambda x: tr(f"mode.{x}"),
  170. label_visibility="collapsed",
  171. key="mode_selection"
  172. )
  173. # Text input (unified for both modes)
  174. text_placeholder = tr("digital_human.input.topic_placeholder") if mode == "digital" else tr("digital_human.input.content_placeholder")
  175. text_height = 120 if mode == "digital" else 200
  176. text_help = tr("input.text_help_digital") if mode == "digital" else tr("input.text_help_fixed")
  177. if mode == "digital":
  178. # File uploader for multiple files
  179. uploaded_files = st.file_uploader(
  180. tr("digital_human.assets.upload"),
  181. type=["jpg", "jpeg", "png", "webp"],
  182. accept_multiple_files=True,
  183. help=tr("digital_human.assets.upload_help"),
  184. key="digital_files"
  185. )
  186. # Save uploaded files to temp directory with unique session ID
  187. goods_asset_paths = []
  188. if uploaded_files:
  189. import uuid
  190. session_id = str(uuid.uuid4()).replace('-', '')[:12]
  191. temp_dir = Path(f"temp/assets_{session_id}")
  192. temp_dir.mkdir(parents=True, exist_ok=True)
  193. for uploaded_file in uploaded_files:
  194. file_path = temp_dir / uploaded_file.name
  195. with open(file_path, "wb") as f:
  196. f.write(uploaded_file.getbuffer())
  197. goods_asset_paths.append(str(file_path.absolute()))
  198. st.success(tr("digital_human.assets.goods_sucess"))
  199. # Preview uploaded assets
  200. with st.expander(tr("digital_human.assets.preview"), expanded=True):
  201. # Show in a grid (3 columns)
  202. cols = st.columns(3)
  203. for i, (file, path) in enumerate(zip(uploaded_files, goods_asset_paths)):
  204. with cols[i % 3]:
  205. # Check if image
  206. ext = Path(path).suffix.lower()
  207. if ext in [".jpg", ".jpeg", ".png", ".webp"]:
  208. st.image(file, caption=file.name, use_container_width=True)
  209. else:
  210. st.info(tr("digital_human.assets.goods_empty_hint"))
  211. # Text input
  212. goods_text = st.text_area(
  213. tr("digital_human.input_text"),
  214. placeholder=text_placeholder,
  215. height=text_height,
  216. help=text_help,
  217. key="digital_box"
  218. )
  219. goods_title = st.text_input(
  220. tr("digital_human.goods_title"),
  221. placeholder=tr("digital_human.goods_title_placeholder"),
  222. help=tr("digital_human.goods_title_help"),
  223. key="goods_title"
  224. )
  225. return {
  226. "character_assets": character_asset_paths,
  227. "goods_title": goods_title,
  228. "goods_assets": goods_asset_paths,
  229. "goods_text": goods_text,
  230. "mode": mode
  231. }
  232. else:
  233. goods_text = st.text_area(
  234. tr("digital_human.customize_text"),
  235. placeholder=text_placeholder,
  236. height=text_height,
  237. help=text_help,
  238. key="customize_box"
  239. )
  240. return {
  241. "character_assets": character_asset_paths,
  242. "goods_text": goods_text,
  243. "mode": mode
  244. }
  245. def _render_output_preview(self, pixelle_video: Any, video_params: dict):
  246. """Render output preview section"""
  247. with st.container(border=True):
  248. st.markdown(f"**{tr('section.video_generation')}**")
  249. # Check configuration
  250. if not config_manager.validate():
  251. st.warning(tr("settings.not_configured"))
  252. # Get input data
  253. character_assets = video_params.get("character_assets", [])
  254. goods_assets = video_params.get("goods_assets", [])
  255. goods_title = video_params.get("goods_title", "")
  256. goods_text = video_params.get("goods_text", "")
  257. mode = video_params.get("mode")
  258. tts_voice = video_params.get("tts_voice", "zh-CN-YunjianNeural")
  259. tts_speed = video_params.get("tts_speed", 1.2)
  260. logger.info(f"🔧 The obtained TTS parameters:")
  261. logger.info(f" - tts_voice: {tts_voice}")
  262. logger.info(f" - tts_speed: {tts_speed}")
  263. logger.info(f" - video_params中的tts_voice: {video_params.get('tts_voice', 'NOT_FOUND')}")
  264. logger.info(f" - video_params: {video_params}")
  265. # Validation
  266. if not character_assets:
  267. st.info(tr("digital_human.assets.character_warning"))
  268. st.button(
  269. tr("btn.generate"),
  270. type="primary",
  271. use_container_width=True,
  272. disabled=True,
  273. key="digital_human_generate_disabled"
  274. )
  275. return
  276. if mode == "digital" and not goods_assets:
  277. st.info(tr("digital_human.assets.goods_warning"))
  278. st.button(
  279. tr("btn.generate"),
  280. type="primary",
  281. use_container_width=True,
  282. disabled=True,
  283. key="digital_human_goods_vaiidation"
  284. )
  285. return
  286. if mode == "digital" and not (goods_text or goods_title):
  287. st.info(tr("digital_human.assets.digital_mode"))
  288. st.button(
  289. tr("btn.generate"),
  290. type="primary",
  291. use_container_width=True,
  292. disabled=True,
  293. key="digital_human_digital_disable"
  294. )
  295. return
  296. if mode == "digital" and (goods_text or goods_title):
  297. st.warning(tr("digital_human.assets.digital_mode_warning"))
  298. if mode == "customize" and not goods_text:
  299. st.info(tr("digital_human.assets.customize_mode"))
  300. st.button(
  301. tr("btn.generate"),
  302. type="primary",
  303. use_container_width=True,
  304. disabled=True,
  305. key="digital_human_customize_disable"
  306. )
  307. return
  308. # Generate button
  309. if st.button(tr("btn.generate"), type="primary", use_container_width=True, key="digital_human_generate"):
  310. # Validate
  311. if not config_manager.validate():
  312. st.error(tr("settings.not_configured"))
  313. st.stop()
  314. # Show progress
  315. progress_bar = st.progress(0)
  316. status_text = st.empty()
  317. start_time = time.time()
  318. try:
  319. # Define async generation function
  320. async def generate_digital_human_video():
  321. task_dir, task_id = create_task_output_dir()
  322. kit = await pixelle_video._get_or_create_comfykit()
  323. workflow_path = video_params["workflow_path"]
  324. import json
  325. from pathlib import Path
  326. if mode == "customize":
  327. status_text.text(tr("progress.step_audio"))
  328. progress_bar.progress(25)
  329. generated_image_path = character_assets[0]
  330. generated_text = goods_text
  331. # TTS
  332. audio_path = os.path.join(task_dir, "narration.mp3")
  333. tts_inference_mode = video_params.get("tts_inference_mode", "local")
  334. tts_voice = video_params.get("tts_voice")
  335. tts_speed = video_params.get("tts_speed")
  336. tts_workflow = video_params.get("tts_workflow")
  337. ref_audio = video_params.get("ref_audio")
  338. tts_kwargs = {
  339. "text": generated_text,
  340. "output_path": audio_path,
  341. "inference_mode": tts_inference_mode
  342. }
  343. if tts_inference_mode == "local":
  344. tts_kwargs["voice"] = tts_voice
  345. tts_kwargs["speed"] = tts_speed
  346. elif tts_inference_mode == "comfyui":
  347. if tts_workflow:
  348. tts_kwargs["workflow"] = tts_workflow
  349. if ref_audio:
  350. tts_kwargs["ref_audio"] = ref_audio
  351. await pixelle_video.tts(**tts_kwargs)
  352. progress_bar.progress(65)
  353. status_text.text(tr("progress.concatenating"))
  354. # Directly call the second workflow
  355. second_workflow_path = Path(workflow_path.get("second_workflow_path"))
  356. if not second_workflow_path.exists():
  357. raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
  358. with open(second_workflow_path, 'r', encoding='utf-8') as f:
  359. second_workflow_config = json.load(f)
  360. second_workflow_params = {
  361. "videoimage": generated_image_path,
  362. "audio": audio_path
  363. }
  364. if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
  365. workflow_input = second_workflow_config["workflow_id"]
  366. else:
  367. workflow_input = str(second_workflow_config)
  368. second_result = await kit.execute(workflow_input, second_workflow_params)
  369. # Video Link Extraction
  370. generated_video_url = None
  371. if hasattr(second_result, 'videos') and second_result.videos:
  372. generated_video_url = second_result.videos[0]
  373. elif hasattr(second_result, 'outputs') and second_result.outputs:
  374. for node_id, node_output in second_result.outputs.items():
  375. if isinstance(node_output, dict) and 'videos' in node_output:
  376. videos = node_output['videos']
  377. if videos and len(videos) > 0:
  378. generated_video_url = videos[0]
  379. break
  380. if not generated_video_url:
  381. raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
  382. final_video_path = os.path.join(task_dir, "final.mp4")
  383. timeout = httpx.Timeout(300.0)
  384. async with httpx.AsyncClient(timeout=timeout) as client:
  385. response = await client.get(generated_video_url)
  386. response.raise_for_status()
  387. with open(final_video_path, 'wb') as f:
  388. f.write(response.content)
  389. progress_bar.progress(100)
  390. status_text.text(tr("status.success"))
  391. return final_video_path
  392. else:
  393. #Initialization and parameter preparation
  394. task_dir, task_id = create_task_output_dir()
  395. logger.info(f"[Initialization] Task Directory: {task_dir}")
  396. first_workflow_path = Path(workflow_path.get("first_workflow_path"))
  397. third_workflow_path = Path(workflow_path.get("third_workflow_path"))
  398. second_workflow_path = Path(workflow_path.get("second_workflow_path"))
  399. assert first_workflow_path.exists(), "The first_workflow file does not exist."
  400. assert third_workflow_path.exists(), "The third_workflow file does not exist."
  401. assert second_workflow_path.exists(), "The second_workflow file does not exist."
  402. if goods_text and goods_text.strip():
  403. workflow_path = third_workflow_path
  404. workflow_params = {"firstimage": character_assets[0], "secondimage": goods_assets[0]}
  405. generated_text = goods_text
  406. status_text.text(tr("progress.step_image"))
  407. kit = await pixelle_video._get_or_create_comfykit()
  408. workflow_config = json.load(open(workflow_path, 'r', encoding='utf8'))
  409. if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
  410. workflow_input = workflow_config["workflow_id"]
  411. else:
  412. workflow_input = str(workflow_config)
  413. combine_image = await kit.execute(workflow_input, workflow_params)
  414. if combine_image.status != "completed":
  415. raise Exception(f"workflow execution failed: {combine_image.msg}")
  416. generated_image_url = getattr(combine_image, "images", [None])[0]
  417. status_text.text(tr("progress.step_audio"))
  418. audio_path = os.path.join(task_dir, "narration.mp3")
  419. tts_inference_mode = video_params.get("tts_inference_mode", "local")
  420. tts_voice = video_params.get("tts_voice")
  421. tts_speed = video_params.get("tts_speed")
  422. tts_workflow = video_params.get("tts_workflow")
  423. ref_audio = video_params.get("ref_audio")
  424. tts_kwargs = {
  425. "text": generated_text,
  426. "output_path": audio_path,
  427. "inference_mode": tts_inference_mode
  428. }
  429. if tts_inference_mode == "local":
  430. tts_kwargs["voice"] = tts_voice
  431. tts_kwargs["speed"] = tts_speed
  432. elif tts_inference_mode == "comfyui":
  433. if tts_workflow:
  434. tts_kwargs["workflow"] = tts_workflow
  435. if ref_audio:
  436. tts_kwargs["ref_audio"] = ref_audio
  437. await pixelle_video.tts(**tts_kwargs)
  438. progress_bar.progress(65)
  439. status_text.text(tr("progress.concatenating"))
  440. if not second_workflow_path.exists():
  441. raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
  442. with open(second_workflow_path, 'r', encoding='utf-8') as f:
  443. second_workflow_config = json.load(f)
  444. second_workflow_params = {
  445. "videoimage": generated_image_url,
  446. "audio": audio_path
  447. }
  448. if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
  449. workflow_input = second_workflow_config["workflow_id"]
  450. else:
  451. workflow_input = str(second_workflow_config)
  452. second_result = await kit.execute(workflow_input, second_workflow_params)
  453. # Video Link Extraction
  454. generated_video_url = None
  455. if hasattr(second_result, 'videos') and second_result.videos:
  456. generated_video_url = second_result.videos[0]
  457. elif hasattr(second_result, 'outputs') and second_result.outputs:
  458. for node_id, node_output in second_result.outputs.items():
  459. if isinstance(node_output, dict) and 'videos' in node_output:
  460. videos = node_output['videos']
  461. if videos and len(videos) > 0:
  462. generated_video_url = videos[0]
  463. break
  464. if not generated_video_url:
  465. raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
  466. final_video_path = os.path.join(task_dir, "final.mp4")
  467. timeout = httpx.Timeout(300.0)
  468. async with httpx.AsyncClient(timeout=timeout) as client:
  469. response = await client.get(generated_video_url)
  470. response.raise_for_status()
  471. with open(final_video_path, 'wb') as f:
  472. f.write(response.content)
  473. progress_bar.progress(100)
  474. status_text.text(tr("status.success"))
  475. return final_video_path
  476. else:
  477. workflow_path = first_workflow_path
  478. workflow_params = {"firstimage": character_assets[0], "secondimage": goods_assets[0], "goodstype": goods_title}
  479. status_text.text(tr("progress.step_image"))
  480. kit = await pixelle_video._get_or_create_comfykit()
  481. workflow_config = json.load(open(workflow_path, 'r', encoding='utf8'))
  482. if workflow_config.get("source") == "runninghub" and "workflow_id" in workflow_config:
  483. workflow_input = workflow_config["workflow_id"]
  484. else:
  485. workflow_input = str(workflow_config)
  486. synthesis_result = await kit.execute(workflow_input, workflow_params)
  487. if synthesis_result.status != "completed":
  488. raise Exception(f"workflow execution failed: {synthesis_result.msg}")
  489. generated_image_url = getattr(synthesis_result, "images", [None])[0]
  490. generated_text = getattr(synthesis_result, "texts", [None])[0]
  491. status_text.text(tr("progress.step_audio"))
  492. audio_path = os.path.join(task_dir, "narration.mp3")
  493. tts_inference_mode = video_params.get("tts_inference_mode", "local")
  494. tts_voice = video_params.get("tts_voice")
  495. tts_speed = video_params.get("tts_speed")
  496. tts_workflow = video_params.get("tts_workflow")
  497. ref_audio = video_params.get("ref_audio")
  498. tts_kwargs = {
  499. "text": generated_text,
  500. "output_path": audio_path,
  501. "inference_mode": tts_inference_mode
  502. }
  503. if tts_inference_mode == "local":
  504. tts_kwargs["voice"] = tts_voice
  505. tts_kwargs["speed"] = tts_speed
  506. elif tts_inference_mode == "comfyui":
  507. if tts_workflow:
  508. tts_kwargs["workflow"] = tts_workflow
  509. if ref_audio:
  510. tts_kwargs["ref_audio"] = ref_audio
  511. await pixelle_video.tts(**tts_kwargs)
  512. progress_bar.progress(65)
  513. status_text.text(tr("progress.concatenating"))
  514. if not second_workflow_path.exists():
  515. raise Exception(f"The second step workflow file does not exist:{second_workflow_path}")
  516. with open(second_workflow_path, 'r', encoding='utf-8') as f:
  517. second_workflow_config = json.load(f)
  518. second_workflow_params = {
  519. "videoimage": generated_image_url,
  520. "audio": audio_path
  521. }
  522. if second_workflow_config.get("source") == "runninghub" and "workflow_id" in second_workflow_config:
  523. workflow_input = second_workflow_config["workflow_id"]
  524. else:
  525. workflow_input = str(second_workflow_config)
  526. second_result = await kit.execute(workflow_input, second_workflow_params)
  527. # Video Link Extraction
  528. generated_video_url = None
  529. if hasattr(second_result, 'videos') and second_result.videos:
  530. generated_video_url = second_result.videos[0]
  531. elif hasattr(second_result, 'outputs') and second_result.outputs:
  532. for node_id, node_output in second_result.outputs.items():
  533. if isinstance(node_output, dict) and 'videos' in node_output:
  534. videos = node_output['videos']
  535. if videos and len(videos) > 0:
  536. generated_video_url = videos[0]
  537. break
  538. if not generated_video_url:
  539. raise Exception("The second step of the workflow did not return a video. Please check the workflow configuration.")
  540. final_video_path = os.path.join(task_dir, "final.mp4")
  541. timeout = httpx.Timeout(300.0)
  542. async with httpx.AsyncClient(timeout=timeout) as client:
  543. response = await client.get(generated_video_url)
  544. response.raise_for_status()
  545. with open(final_video_path, 'wb') as f:
  546. f.write(response.content)
  547. progress_bar.progress(100)
  548. status_text.text(tr("status.success"))
  549. return final_video_path
  550. # Execute async generation
  551. final_video_path = run_async(generate_digital_human_video())
  552. total_time = time.time() - start_time
  553. progress_bar.progress(100)
  554. status_text.text(tr("status.success"))
  555. # Display result
  556. st.success(tr("status.video_generated", path=final_video_path))
  557. st.markdown("---")
  558. # Video info
  559. if os.path.exists(final_video_path):
  560. file_size_mb = os.path.getsize(final_video_path) / (1024 * 1024)
  561. info_text = (
  562. f"⏱️ {tr('info.generation_time')} {total_time:.1f}s "
  563. f"📦 {file_size_mb:.2f}MB"
  564. )
  565. st.caption(info_text)
  566. st.markdown("---")
  567. # Video preview
  568. st.video(final_video_path)
  569. # Download button
  570. with open(final_video_path, "rb") as video_file:
  571. video_bytes = video_file.read()
  572. video_filename = os.path.basename(final_video_path)
  573. st.download_button(
  574. label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
  575. data=video_bytes,
  576. file_name=video_filename,
  577. mime="video/mp4",
  578. use_container_width=True
  579. )
  580. else:
  581. st.error(tr("status.video_not_found", path=final_video_path))
  582. except Exception as e:
  583. status_text.text("")
  584. progress_bar.empty()
  585. st.error(tr("status.error", error=str(e)))
  586. logger.exception(e)
  587. st.stop()
  588. # Register self
  589. register_pipeline_ui(DigitalHumanPipelineUI)