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