output_preview.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # Copyright (C) 2025 AIDC-AI
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. """
  13. Output preview components for web UI (right column)
  14. """
  15. import base64
  16. import os
  17. from pathlib import Path
  18. import streamlit as st
  19. from loguru import logger
  20. from web.i18n import tr, get_language
  21. from web.utils.async_helpers import run_async
  22. from pixelle_video.models.progress import ProgressEvent
  23. from pixelle_video.config import config_manager
  24. def render_output_preview(pixelle_video, video_params):
  25. """Render output preview section (right column)"""
  26. # Check if batch mode
  27. is_batch = video_params.get("batch_mode", False)
  28. if is_batch:
  29. # Batch generation mode
  30. render_batch_output(pixelle_video, video_params)
  31. else:
  32. # Single video generation mode (original logic)
  33. render_single_output(pixelle_video, video_params)
  34. def render_single_output(pixelle_video, video_params):
  35. """Render single video generation output (original logic, unchanged)"""
  36. # Extract parameters from video_params dict
  37. text = video_params.get("text", "")
  38. mode = video_params.get("mode", "generate")
  39. title = video_params.get("title")
  40. n_scenes = video_params.get("n_scenes", 5)
  41. split_mode = video_params.get("split_mode", "paragraph")
  42. bgm_path = video_params.get("bgm_path")
  43. bgm_volume = video_params.get("bgm_volume", 0.2)
  44. tts_mode = video_params.get("tts_inference_mode", "local")
  45. selected_voice = video_params.get("tts_voice")
  46. tts_speed = video_params.get("tts_speed")
  47. tts_workflow_key = video_params.get("tts_workflow")
  48. ref_audio_path = video_params.get("ref_audio")
  49. frame_template = video_params.get("frame_template")
  50. custom_values_for_video = video_params.get("template_params", {})
  51. workflow_key = video_params.get("media_workflow")
  52. prompt_prefix = video_params.get("prompt_prefix", "")
  53. with st.container(border=True):
  54. st.markdown(f"**{tr('section.video_generation')}**")
  55. # Check if system is configured
  56. if not config_manager.validate():
  57. st.warning(tr("settings.not_configured"))
  58. # Generate Button
  59. if st.button(tr("btn.generate"), type="primary", use_container_width=True):
  60. # Validate system configuration
  61. if not config_manager.validate():
  62. st.error(tr("settings.not_configured"))
  63. st.stop()
  64. # Validate input
  65. if not text:
  66. st.error(tr("error.input_required"))
  67. st.stop()
  68. # Show progress
  69. progress_bar = st.progress(0)
  70. status_text = st.empty()
  71. # Record start time for generation
  72. import time
  73. start_time = time.time()
  74. try:
  75. # Progress callback to update UI
  76. def update_progress(event: ProgressEvent):
  77. """Update progress bar and status text from ProgressEvent"""
  78. # Translate event to user-facing message
  79. if event.event_type == "frame_step":
  80. # Frame step: "分镜 3/5 - 步骤 2/4: 生成插图"
  81. action_key = f"progress.step_{event.action}"
  82. action_text = tr(action_key)
  83. message = tr(
  84. "progress.frame_step",
  85. current=event.frame_current,
  86. total=event.frame_total,
  87. step=event.step,
  88. action=action_text
  89. )
  90. elif event.event_type == "processing_frame":
  91. # Processing frame: "分镜 3/5"
  92. message = tr(
  93. "progress.frame",
  94. current=event.frame_current,
  95. total=event.frame_total
  96. )
  97. else:
  98. # Simple events: use i18n key directly
  99. message = tr(f"progress.{event.event_type}")
  100. # Append extra_info if available (e.g., batch progress)
  101. if event.extra_info:
  102. message = f"{message} - {event.extra_info}"
  103. status_text.text(message)
  104. progress_bar.progress(min(int(event.progress * 100), 99)) # Cap at 99% until complete
  105. # Generate video (directly pass parameters)
  106. # Note: media_width and media_height are auto-determined from template
  107. video_params = {
  108. "text": text,
  109. "mode": mode,
  110. "title": title if title else None,
  111. "n_scenes": n_scenes,
  112. "split_mode": split_mode,
  113. "media_workflow": workflow_key,
  114. "frame_template": frame_template,
  115. "prompt_prefix": prompt_prefix,
  116. "bgm_path": bgm_path,
  117. "bgm_volume": bgm_volume if bgm_path else 0.2,
  118. "progress_callback": update_progress,
  119. "media_width": st.session_state.get('template_media_width'),
  120. "media_height": st.session_state.get('template_media_height'),
  121. }
  122. # Add TTS parameters based on mode
  123. video_params["tts_inference_mode"] = tts_mode
  124. if tts_mode == "local":
  125. video_params["tts_voice"] = selected_voice
  126. video_params["tts_speed"] = tts_speed
  127. else: # comfyui
  128. video_params["tts_workflow"] = tts_workflow_key
  129. if ref_audio_path:
  130. video_params["ref_audio"] = str(ref_audio_path)
  131. # Add custom template parameters if any
  132. if custom_values_for_video:
  133. video_params["template_params"] = custom_values_for_video
  134. result = run_async(pixelle_video.generate_video(**video_params))
  135. # Calculate total generation time
  136. total_generation_time = time.time() - start_time
  137. progress_bar.progress(100)
  138. status_text.text(tr("status.success"))
  139. # Display success message
  140. st.success(tr("status.video_generated", path=result.video_path))
  141. st.markdown("---")
  142. # Video information (compact display)
  143. file_size_mb = result.file_size / (1024 * 1024)
  144. # Parse video size from template path
  145. from pixelle_video.utils.template_util import parse_template_size, resolve_template_path
  146. template_path = resolve_template_path(result.storyboard.config.frame_template)
  147. video_width, video_height = parse_template_size(template_path)
  148. info_text = (
  149. f"⏱️ {tr('info.generation_time')} {total_generation_time:.1f}s "
  150. f"📦 {file_size_mb:.2f}MB "
  151. f"🎬 {len(result.storyboard.frames)}{tr('info.scenes_unit')} "
  152. f"📐 {video_width}x{video_height}"
  153. )
  154. st.caption(info_text)
  155. st.markdown("---")
  156. # Video preview
  157. if os.path.exists(result.video_path):
  158. st.video(result.video_path)
  159. # Download button
  160. with open(result.video_path, "rb") as video_file:
  161. video_bytes = video_file.read()
  162. video_filename = os.path.basename(result.video_path)
  163. st.download_button(
  164. label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
  165. data=video_bytes,
  166. file_name=video_filename,
  167. mime="video/mp4",
  168. use_container_width=True
  169. )
  170. else:
  171. st.error(tr("status.video_not_found", path=result.video_path))
  172. except Exception as e:
  173. status_text.text("")
  174. progress_bar.empty()
  175. st.error(tr("status.error", error=str(e)))
  176. logger.exception(e)
  177. st.stop()
  178. def render_batch_output(pixelle_video, video_params):
  179. """Render batch generation output (minimal, redirect to History)"""
  180. topics = video_params.get("topics", [])
  181. with st.container(border=True):
  182. st.markdown(f"**{tr('batch.section_generation')}**")
  183. # Check if topics are provided
  184. if not topics:
  185. st.warning(tr("batch.no_topics"))
  186. return
  187. # Check system configuration
  188. if not config_manager.validate():
  189. st.warning(tr("settings.not_configured"))
  190. return
  191. batch_count = len(topics)
  192. # Display batch info
  193. st.info(tr("batch.prepare_info", count=batch_count))
  194. # Estimated time (optional)
  195. estimated_minutes = batch_count * 3 # Assume 3 minutes per video
  196. st.caption(tr("batch.estimated_time", minutes=estimated_minutes))
  197. # Generate button with batch semantics
  198. if st.button(
  199. tr("batch.generate_button", count=batch_count),
  200. type="primary",
  201. use_container_width=True,
  202. help=tr("batch.generate_help")
  203. ):
  204. # Prepare shared config
  205. shared_config = {
  206. "title_prefix": video_params.get("title_prefix"),
  207. "n_scenes": video_params.get("n_scenes") or 5,
  208. "media_workflow": video_params.get("media_workflow"),
  209. "frame_template": video_params.get("frame_template"),
  210. "prompt_prefix": video_params.get("prompt_prefix") or "",
  211. "bgm_path": video_params.get("bgm_path"),
  212. "bgm_volume": video_params.get("bgm_volume") or 0.2,
  213. "tts_inference_mode": video_params.get("tts_inference_mode") or "local",
  214. "media_width": video_params.get("media_width"),
  215. "media_height": video_params.get("media_height"),
  216. }
  217. # Add TTS parameters based on mode (only add non-None values)
  218. if shared_config["tts_inference_mode"] == "local":
  219. tts_voice = video_params.get("tts_voice")
  220. tts_speed = video_params.get("tts_speed")
  221. if tts_voice:
  222. shared_config["tts_voice"] = tts_voice
  223. if tts_speed:
  224. shared_config["tts_speed"] = tts_speed
  225. else: # comfyui
  226. tts_workflow = video_params.get("tts_workflow")
  227. if tts_workflow:
  228. shared_config["tts_workflow"] = tts_workflow
  229. ref_audio = video_params.get("ref_audio")
  230. if ref_audio:
  231. shared_config["ref_audio"] = str(ref_audio)
  232. # Add template parameters
  233. if video_params.get("template_params"):
  234. shared_config["template_params"] = video_params["template_params"]
  235. # UI containers
  236. overall_progress_container = st.container()
  237. current_task_container = st.container()
  238. # Overall progress UI
  239. overall_progress_bar = overall_progress_container.progress(0)
  240. overall_status = overall_progress_container.empty()
  241. # Current task progress UI
  242. current_task_title = current_task_container.empty()
  243. current_task_progress = current_task_container.progress(0)
  244. current_task_status = current_task_container.empty()
  245. # Overall progress callback
  246. def update_overall_progress(current, total, topic):
  247. progress = (current - 1) / total
  248. overall_progress_bar.progress(progress)
  249. overall_status.markdown(
  250. f"📊 **{tr('batch.overall_progress')}**: {current}/{total} ({int(progress * 100)}%)"
  251. )
  252. # Single task progress callback factory
  253. def make_task_progress_callback(task_idx, topic):
  254. def callback(event: ProgressEvent):
  255. # Display current task title
  256. current_task_title.markdown(f"🎬 **{tr('batch.current_task')} {task_idx}**: {topic}")
  257. # Update task detailed progress
  258. if event.event_type == "frame_step":
  259. action_key = f"progress.step_{event.action}"
  260. action_text = tr(action_key)
  261. message = tr(
  262. "progress.frame_step",
  263. current=event.frame_current,
  264. total=event.frame_total,
  265. step=event.step,
  266. action=action_text
  267. )
  268. elif event.event_type == "processing_frame":
  269. message = tr(
  270. "progress.frame",
  271. current=event.frame_current,
  272. total=event.frame_total
  273. )
  274. else:
  275. message = tr(f"progress.{event.event_type}")
  276. current_task_progress.progress(event.progress)
  277. current_task_status.text(message)
  278. return callback
  279. # Execute batch generation
  280. from web.utils.batch_manager import SimpleBatchManager
  281. import time
  282. batch_manager = SimpleBatchManager()
  283. start_time = time.time()
  284. batch_result = batch_manager.execute_batch(
  285. pixelle_video=pixelle_video,
  286. topics=topics,
  287. shared_config=shared_config,
  288. overall_progress_callback=update_overall_progress,
  289. task_progress_callback_factory=make_task_progress_callback
  290. )
  291. total_time = time.time() - start_time
  292. # Clear progress displays
  293. overall_progress_bar.progress(1.0)
  294. overall_status.markdown(f"✅ **{tr('batch.completed')}**")
  295. current_task_title.empty()
  296. current_task_progress.empty()
  297. current_task_status.empty()
  298. # Display results summary
  299. st.markdown("---")
  300. st.markdown(f"**{tr('batch.results_title')}**")
  301. col1, col2, col3 = st.columns(3)
  302. col1.metric(tr("batch.total"), batch_result["total_count"])
  303. col2.metric(f"✅ {tr('batch.success')}", batch_result["success_count"])
  304. col3.metric(f"❌ {tr('batch.failed')}", batch_result["failed_count"])
  305. # Display total time
  306. minutes = int(total_time / 60)
  307. seconds = int(total_time % 60)
  308. st.caption(f"⏱️ {tr('batch.total_time')}: {minutes}{tr('batch.minutes')}{seconds}{tr('batch.seconds')}")
  309. # Redirect to History page
  310. st.markdown("---")
  311. st.success(tr("batch.success_message"))
  312. st.info(tr("batch.view_in_history"))
  313. # Button to go to History page using JavaScript URL navigation
  314. st.markdown(
  315. f"""
  316. <a href="/History" target="_blank">
  317. <button style="
  318. width: 100%;
  319. padding: 0.5rem 1rem;
  320. background-color: white;
  321. color: rgb(49, 51, 63);
  322. border: 1px solid rgba(49, 51, 63, 0.2);
  323. border-radius: 0.5rem;
  324. cursor: pointer;
  325. font-size: 1rem;
  326. font-weight: 400;
  327. text-align: center;
  328. ">
  329. 📚 {tr('batch.goto_history')}
  330. </button>
  331. </a>
  332. """,
  333. unsafe_allow_html=True
  334. )
  335. # Show failed tasks if any
  336. if batch_result["errors"]:
  337. st.markdown("---")
  338. st.markdown(f"#### {tr('batch.failed_list')}")
  339. for item in batch_result["errors"]:
  340. with st.expander(f"🔴 {tr('batch.task')} {item['index']}: {item['topic']}", expanded=False):
  341. st.error(f"**{tr('batch.error')}**: {item['error']}")
  342. # Detailed error (collapsed)
  343. with st.expander(tr("batch.error_detail")):
  344. st.code(item['traceback'], language="python")