style_config.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  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. Style configuration components for web UI (middle column)
  14. """
  15. import os
  16. from pathlib import Path
  17. import streamlit as st
  18. from loguru import logger
  19. from web.i18n import tr, get_language
  20. from web.utils.async_helpers import run_async
  21. from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow
  22. from pixelle_video.config import config_manager
  23. def render_style_config(pixelle_video):
  24. """Render style configuration section (middle column)"""
  25. # TTS Section (moved from left column)
  26. # ====================================================================
  27. with st.container(border=True):
  28. st.markdown(f"**{tr('section.tts')}**")
  29. with st.expander(tr("help.feature_description"), expanded=False):
  30. st.markdown(f"**{tr('help.what')}**")
  31. st.markdown(tr("tts.what"))
  32. st.markdown(f"**{tr('help.how')}**")
  33. st.markdown(tr("tts.how"))
  34. # Get TTS config
  35. comfyui_config = config_manager.get_comfyui_config()
  36. tts_config = comfyui_config["tts"]
  37. # Inference mode selection
  38. tts_mode = st.radio(
  39. tr("tts.inference_mode"),
  40. ["local", "comfyui"],
  41. horizontal=True,
  42. format_func=lambda x: tr(f"tts.mode.{x}"),
  43. index=0 if tts_config.get("inference_mode", "local") == "local" else 1,
  44. key="tts_inference_mode"
  45. )
  46. # Show hint based on mode
  47. if tts_mode == "local":
  48. st.caption(tr("tts.mode.local_hint"))
  49. else:
  50. st.caption(tr("tts.mode.comfyui_hint"))
  51. # ================================================================
  52. # Local Mode UI
  53. # ================================================================
  54. if tts_mode == "local":
  55. # Import voice configuration
  56. from pixelle_video.tts_voices import EDGE_TTS_VOICES, get_voice_display_name
  57. # Get saved voice from config
  58. local_config = tts_config.get("local", {})
  59. saved_voice = local_config.get("voice", "zh-CN-YunjianNeural")
  60. saved_speed = local_config.get("speed", 1.2)
  61. # Build voice options with i18n
  62. voice_options = []
  63. voice_ids = []
  64. default_voice_index = 0
  65. for idx, voice_config in enumerate(EDGE_TTS_VOICES):
  66. voice_id = voice_config["id"]
  67. display_name = get_voice_display_name(voice_id, tr, get_language())
  68. voice_options.append(display_name)
  69. voice_ids.append(voice_id)
  70. # Set default index if matches saved voice
  71. if voice_id == saved_voice:
  72. default_voice_index = idx
  73. # Two-column layout: Voice | Speed
  74. voice_col, speed_col = st.columns([1, 1])
  75. with voice_col:
  76. # Voice selector
  77. selected_voice_display = st.selectbox(
  78. tr("tts.voice_selector"),
  79. voice_options,
  80. index=default_voice_index,
  81. key="tts_local_voice"
  82. )
  83. # Get actual voice ID
  84. selected_voice_index = voice_options.index(selected_voice_display)
  85. selected_voice = voice_ids[selected_voice_index]
  86. with speed_col:
  87. # Speed slider
  88. tts_speed = st.slider(
  89. tr("tts.speed"),
  90. min_value=0.5,
  91. max_value=2.0,
  92. value=saved_speed,
  93. step=0.1,
  94. format="%.1fx",
  95. key="tts_local_speed"
  96. )
  97. st.caption(tr("tts.speed_label", speed=f"{tts_speed:.1f}"))
  98. # Variables for video generation
  99. tts_workflow_key = None
  100. ref_audio_path = None
  101. # ================================================================
  102. # ComfyUI Mode UI
  103. # ================================================================
  104. else: # comfyui mode
  105. # Get available TTS workflows
  106. tts_workflows = pixelle_video.tts.list_workflows()
  107. # Build options for selectbox
  108. tts_workflow_options = [wf["display_name"] for wf in tts_workflows]
  109. tts_workflow_keys = [wf["key"] for wf in tts_workflows]
  110. # Default to saved workflow if exists
  111. default_tts_index = 0
  112. saved_tts_workflow = tts_config.get("comfyui", {}).get("default_workflow")
  113. if saved_tts_workflow and saved_tts_workflow in tts_workflow_keys:
  114. default_tts_index = tts_workflow_keys.index(saved_tts_workflow)
  115. tts_workflow_display = st.selectbox(
  116. "TTS Workflow",
  117. tts_workflow_options if tts_workflow_options else ["No TTS workflows found"],
  118. index=default_tts_index,
  119. label_visibility="collapsed",
  120. key="tts_workflow_select"
  121. )
  122. # Get the actual workflow key
  123. if tts_workflow_options:
  124. tts_selected_index = tts_workflow_options.index(tts_workflow_display)
  125. tts_workflow_key = tts_workflow_keys[tts_selected_index]
  126. else:
  127. tts_workflow_key = "selfhost/tts_edge.json" # fallback
  128. # Check and warn for selfhost TTS workflow (auto popup if not confirmed)
  129. check_and_warn_selfhost_workflow(tts_workflow_key)
  130. # Reference audio upload (optional, for voice cloning)
  131. ref_audio_file = st.file_uploader(
  132. tr("tts.ref_audio"),
  133. type=["mp3", "wav", "flac", "m4a", "aac", "ogg"],
  134. help=tr("tts.ref_audio_help"),
  135. key="ref_audio_upload"
  136. )
  137. # Save uploaded ref_audio to temp file if provided
  138. ref_audio_path = None
  139. if ref_audio_file is not None:
  140. # Audio preview player (directly play uploaded file)
  141. st.audio(ref_audio_file)
  142. # Save to temp directory
  143. temp_dir = Path("temp")
  144. temp_dir.mkdir(exist_ok=True)
  145. ref_audio_path = temp_dir / f"ref_audio_{ref_audio_file.name}"
  146. with open(ref_audio_path, "wb") as f:
  147. f.write(ref_audio_file.getbuffer())
  148. # Variables for video generation
  149. selected_voice = None
  150. tts_speed = None
  151. # ================================================================
  152. # TTS Preview (works for both modes)
  153. # ================================================================
  154. with st.expander(tr("tts.preview_title"), expanded=False):
  155. # Preview text input
  156. preview_text = st.text_input(
  157. tr("tts.preview_text"),
  158. value="大家好,这是一段测试语音。",
  159. placeholder=tr("tts.preview_text_placeholder"),
  160. key="tts_preview_text"
  161. )
  162. # Preview button
  163. if st.button(tr("tts.preview_button"), key="preview_tts", use_container_width=True):
  164. with st.spinner(tr("tts.previewing")):
  165. try:
  166. # Build TTS params based on mode
  167. tts_params = {
  168. "text": preview_text,
  169. "inference_mode": tts_mode
  170. }
  171. if tts_mode == "local":
  172. tts_params["voice"] = selected_voice
  173. tts_params["speed"] = tts_speed
  174. else: # comfyui
  175. tts_params["workflow"] = tts_workflow_key
  176. if ref_audio_path:
  177. tts_params["ref_audio"] = str(ref_audio_path)
  178. audio_path = run_async(pixelle_video.tts(**tts_params))
  179. # Play the audio
  180. if audio_path:
  181. st.success(tr("tts.preview_success"))
  182. if os.path.exists(audio_path):
  183. st.audio(audio_path, format="audio/mp3")
  184. elif audio_path.startswith('http'):
  185. st.audio(audio_path)
  186. else:
  187. st.error("Failed to generate preview audio")
  188. # Show file path
  189. st.caption(f"📁 {audio_path}")
  190. else:
  191. st.error("Failed to generate preview audio")
  192. except Exception as e:
  193. st.error(tr("tts.preview_failed", error=str(e)))
  194. logger.exception(e)
  195. # ====================================================================
  196. # Storyboard Template Section
  197. # ====================================================================
  198. def get_template_preview_path(template_path: str, language: str = "zh_CN") -> str:
  199. """
  200. Get the preview image path for a template based on language.
  201. Args:
  202. template_path: Template path like "1080x1920/image_default.html"
  203. language: Language code, either "zh_CN" or "en"
  204. Returns:
  205. Path to preview image in docs/images/
  206. """
  207. # Extract size and template name from path
  208. # e.g., "1080x1920/image_default.html" -> size="1080x1920", name="image_default"
  209. path_parts = template_path.split('/')
  210. if len(path_parts) >= 2:
  211. size = path_parts[0] # e.g., "1080x1920"
  212. template_file = path_parts[1] # e.g., "image_default.html"
  213. template_name = template_file.replace('.html', '') # e.g., "image_default"
  214. # Build preview image path
  215. # Format: docs/images/{size}/{template_name}.jpg or {template_name}_en.jpg
  216. # Chinese uses Chinese preview, all other languages use English preview for better i18n
  217. suffix = "" if language == "zh_CN" else "_en"
  218. # Try different image extensions
  219. for ext in ['.jpg', '.png']:
  220. preview_path = f"docs/images/{size}/{template_name}{suffix}{ext}"
  221. if os.path.exists(preview_path):
  222. return preview_path
  223. # Fallback: try without language suffix (for templates with only one version)
  224. for ext in ['.jpg', '.png']:
  225. preview_path = f"docs/images/{size}/{template_name}{ext}"
  226. if os.path.exists(preview_path):
  227. return preview_path
  228. # If no preview found, return empty string
  229. return ""
  230. with st.container(border=True):
  231. st.markdown(f"**{tr('section.template')}**")
  232. with st.expander(tr("help.feature_description"), expanded=False):
  233. st.markdown(f"**{tr('help.what')}**")
  234. st.markdown(tr("template.what"))
  235. st.markdown(f"**{tr('help.how')}**")
  236. st.markdown(tr("template.how"))
  237. # Template preview link (based on language)
  238. current_lang = get_language()
  239. # Import template utilities
  240. from pixelle_video.utils.template_util import get_templates_grouped_by_size_and_type, get_template_type
  241. # Template type selector
  242. st.markdown(f"**{tr('template.type_selector')}**")
  243. template_type_options = {
  244. 'static': tr('template.type.static'),
  245. 'image': tr('template.type.image'),
  246. 'video': tr('template.type.video')
  247. }
  248. # Radio buttons in horizontal layout
  249. selected_template_type = st.radio(
  250. tr('template.type_selector'),
  251. options=list(template_type_options.keys()),
  252. format_func=lambda x: template_type_options[x],
  253. index=1, # Default to 'image'
  254. key="template_type_selector",
  255. label_visibility="collapsed",
  256. horizontal=True
  257. )
  258. # Display hint based on selected type (below radio buttons)
  259. if selected_template_type == 'static':
  260. st.info(tr('template.type.static_hint'))
  261. elif selected_template_type == 'image':
  262. st.info(tr('template.type.image_hint'))
  263. elif selected_template_type == 'video':
  264. st.info(tr('template.type.video_hint'))
  265. # Get templates grouped by size, filtered by selected type
  266. grouped_templates = get_templates_grouped_by_size_and_type(selected_template_type)
  267. if not grouped_templates:
  268. st.warning(f"No {template_type_options[selected_template_type]} templates found. Please select a different type or add templates.")
  269. st.stop()
  270. # Build orientation i18n mapping
  271. ORIENTATION_I18N = {
  272. 'portrait': tr('orientation.portrait'),
  273. 'landscape': tr('orientation.landscape'),
  274. 'square': tr('orientation.square')
  275. }
  276. # Get default template from config
  277. template_config = pixelle_video.config.get("template", {})
  278. config_default_template = template_config.get("default_template", "1080x1920/image_default.html")
  279. # Backward compatibility
  280. if config_default_template == "1080x1920/default.html":
  281. config_default_template = "1080x1920/image_default.html"
  282. # Determine type-specific default template
  283. type_default_templates = {
  284. 'static': '1080x1920/static_default.html',
  285. 'image': '1080x1920/image_default.html',
  286. 'video': '1080x1920/video_default.html'
  287. }
  288. type_specific_default = type_default_templates.get(selected_template_type, config_default_template)
  289. # Initialize selected template in session state if not exists
  290. if 'selected_template' not in st.session_state:
  291. st.session_state['selected_template'] = type_specific_default
  292. # Track last selected template type to detect type changes
  293. last_template_type = st.session_state.get('last_template_type', None)
  294. if last_template_type != selected_template_type:
  295. # Template type changed, reset to type-specific default
  296. st.session_state['selected_template'] = type_specific_default
  297. st.session_state['last_template_type'] = selected_template_type
  298. # Collect size groups and prepare tabs
  299. size_groups = []
  300. size_labels = []
  301. for size, templates in grouped_templates.items():
  302. if not templates:
  303. continue
  304. # Filter templates to only include those with proper naming convention
  305. # Only show templates starting with static_, image_, or video_
  306. valid_templates = []
  307. for template in templates:
  308. template_name = template.display_info.name
  309. if template_name.startswith(('static_', 'image_', 'video_')):
  310. valid_templates.append(template)
  311. # Skip if no valid templates after filtering
  312. if not valid_templates:
  313. continue
  314. # Separate templates into two groups: with preview and without preview
  315. templates_with_preview = []
  316. templates_without_preview = []
  317. for template in valid_templates:
  318. preview_path = get_template_preview_path(template.template_path, current_lang)
  319. if preview_path and os.path.exists(preview_path):
  320. templates_with_preview.append(template)
  321. else:
  322. templates_without_preview.append(template)
  323. # Skip this group if no templates at all
  324. if not templates_with_preview and not templates_without_preview:
  325. continue
  326. # Combine: templates with preview first, then without preview
  327. all_templates = templates_with_preview + templates_without_preview
  328. # Get orientation from first template in group
  329. orientation = ORIENTATION_I18N.get(
  330. all_templates[0].display_info.orientation,
  331. all_templates[0].display_info.orientation
  332. )
  333. width = all_templates[0].display_info.width
  334. height = all_templates[0].display_info.height
  335. # Create tab label
  336. tab_label = f"{orientation} {width}×{height}"
  337. size_labels.append(tab_label)
  338. size_groups.append(all_templates)
  339. # Create tabs for each size group (wrapped in expander)
  340. with st.expander(tr("template.gallery_view"), expanded=True):
  341. if size_groups:
  342. tabs = st.tabs(size_labels)
  343. for tab, all_templates in zip(tabs, size_groups):
  344. with tab:
  345. # Create grid layout (5 columns)
  346. num_cols = 5
  347. cols = st.columns(num_cols)
  348. for idx, template in enumerate(all_templates):
  349. col_idx = idx % num_cols
  350. with cols[col_idx]:
  351. # Get preview image path
  352. preview_path = get_template_preview_path(template.template_path, current_lang)
  353. # Display preview image or placeholder
  354. if preview_path and os.path.exists(preview_path):
  355. st.image(preview_path, use_container_width=True)
  356. else:
  357. # Placeholder for templates without preview (fixed height, compact layout)
  358. st.markdown(
  359. f"""
  360. <div style="
  361. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  362. height: 150px;
  363. display: flex;
  364. align-items: center;
  365. justify-content: center;
  366. text-align: center;
  367. border-radius: 8px;
  368. color: white;
  369. margin-bottom: 15px;
  370. padding: 10px;
  371. ">
  372. <div style="
  373. font-size: 14px;
  374. opacity: 0.95;
  375. overflow: hidden;
  376. text-overflow: ellipsis;
  377. display: -webkit-box;
  378. -webkit-line-clamp: 5;
  379. -webkit-box-orient: vertical;
  380. word-break: break-all;
  381. ">{template.display_info.name}</div>
  382. </div>
  383. """,
  384. unsafe_allow_html=True
  385. )
  386. # Select button (unified label)
  387. is_selected = (st.session_state['selected_template'] == template.template_path)
  388. button_label = f"{tr('template.selected')}" if is_selected else tr('template.select_button')
  389. button_type = "primary" if is_selected else "secondary"
  390. if st.button(
  391. button_label,
  392. key=f"template_{template.template_path}",
  393. use_container_width=True,
  394. type=button_type,
  395. ):
  396. st.session_state['selected_template'] = template.template_path
  397. st.rerun()
  398. else:
  399. st.warning(tr("template.no_templates_with_preview"))
  400. # Display selected template name (inside expander, below tabs)
  401. frame_template = st.session_state['selected_template']
  402. # Find the selected template's display name
  403. selected_template_name = None
  404. for size, templates in grouped_templates.items():
  405. for template in templates:
  406. if template.template_path == frame_template:
  407. selected_template_name = template.display_info.name
  408. break
  409. if selected_template_name:
  410. break
  411. if selected_template_name:
  412. st.info(f"📋 {tr('template.selected_template')}: **{selected_template_name}**")
  413. # Display video size from template
  414. from pixelle_video.utils.template_util import parse_template_size
  415. video_width, video_height = parse_template_size(frame_template)
  416. st.caption(tr("template.video_size_info", width=video_width, height=video_height))
  417. # Custom template parameters (for video generation)
  418. from pixelle_video.services.frame_html import HTMLFrameGenerator
  419. # Resolve template path to support both data/templates/ and templates/
  420. from pixelle_video.utils.template_util import resolve_template_path
  421. template_path_for_params = resolve_template_path(frame_template)
  422. generator_for_params = HTMLFrameGenerator(template_path_for_params)
  423. custom_params_for_video = generator_for_params.parse_template_parameters()
  424. # Get media size from template (for image/video generation)
  425. media_width, media_height = generator_for_params.get_media_size()
  426. st.session_state['template_media_width'] = media_width
  427. st.session_state['template_media_height'] = media_height
  428. # Detect template media type
  429. from pixelle_video.utils.template_util import get_template_type
  430. template_name = Path(frame_template).name
  431. template_media_type = get_template_type(template_name)
  432. template_requires_media = (template_media_type in ["image", "video"])
  433. # Store in session state for workflow filtering
  434. st.session_state['template_media_type'] = template_media_type
  435. st.session_state['template_requires_media'] = template_requires_media
  436. # Backward compatibility
  437. st.session_state['template_requires_image'] = (template_media_type == "image")
  438. custom_values_for_video = {}
  439. if custom_params_for_video:
  440. st.markdown("📝 " + tr("template.custom_parameters"))
  441. # Render custom parameter inputs in 2 columns
  442. video_custom_col1, video_custom_col2 = st.columns(2)
  443. param_items = list(custom_params_for_video.items())
  444. mid_point = (len(param_items) + 1) // 2
  445. # Left column parameters
  446. with video_custom_col1:
  447. for param_name, config in param_items[:mid_point]:
  448. param_type = config['type']
  449. default = config['default']
  450. label = config['label']
  451. if param_type == 'text':
  452. custom_values_for_video[param_name] = st.text_input(
  453. label,
  454. value=default,
  455. key=f"video_custom_{param_name}"
  456. )
  457. elif param_type == 'number':
  458. custom_values_for_video[param_name] = st.number_input(
  459. label,
  460. value=default,
  461. key=f"video_custom_{param_name}"
  462. )
  463. elif param_type == 'color':
  464. custom_values_for_video[param_name] = st.color_picker(
  465. label,
  466. value=default,
  467. key=f"video_custom_{param_name}"
  468. )
  469. elif param_type == 'bool':
  470. custom_values_for_video[param_name] = st.checkbox(
  471. label,
  472. value=default,
  473. key=f"video_custom_{param_name}"
  474. )
  475. # Right column parameters
  476. with video_custom_col2:
  477. for param_name, config in param_items[mid_point:]:
  478. param_type = config['type']
  479. default = config['default']
  480. label = config['label']
  481. if param_type == 'text':
  482. custom_values_for_video[param_name] = st.text_input(
  483. label,
  484. value=default,
  485. key=f"video_custom_{param_name}"
  486. )
  487. elif param_type == 'number':
  488. custom_values_for_video[param_name] = st.number_input(
  489. label,
  490. value=default,
  491. key=f"video_custom_{param_name}"
  492. )
  493. elif param_type == 'color':
  494. custom_values_for_video[param_name] = st.color_picker(
  495. label,
  496. value=default,
  497. key=f"video_custom_{param_name}"
  498. )
  499. elif param_type == 'bool':
  500. custom_values_for_video[param_name] = st.checkbox(
  501. label,
  502. value=default,
  503. key=f"video_custom_{param_name}"
  504. )
  505. # Template preview expander
  506. with st.expander(tr("template.preview_title"), expanded=False):
  507. col1, col2 = st.columns(2)
  508. with col1:
  509. preview_title = st.text_input(
  510. tr("template.preview_param_title"),
  511. value=tr("template.preview_default_title"),
  512. key="preview_title"
  513. )
  514. preview_image = st.text_input(
  515. tr("template.preview_param_image"),
  516. value="resources/example.png",
  517. help=tr("template.preview_image_help"),
  518. key="preview_image"
  519. )
  520. with col2:
  521. preview_text = st.text_area(
  522. tr("template.preview_param_text"),
  523. value=tr("template.preview_default_text"),
  524. height=100,
  525. key="preview_text"
  526. )
  527. # Info: Size is auto-determined from template
  528. from pixelle_video.utils.template_util import parse_template_size, resolve_template_path
  529. template_width, template_height = parse_template_size(resolve_template_path(frame_template))
  530. st.info(f"📐 {tr('template.size_info')}: {template_width} × {template_height}")
  531. # Preview button
  532. if st.button(tr("template.preview_button"), key="btn_preview_template", use_container_width=True):
  533. with st.spinner(tr("template.preview_generating")):
  534. try:
  535. from pixelle_video.services.frame_html import HTMLFrameGenerator
  536. # Use the currently selected template (size is auto-parsed)
  537. from pixelle_video.utils.template_util import resolve_template_path
  538. template_path = resolve_template_path(frame_template)
  539. generator = HTMLFrameGenerator(template_path)
  540. # Build ext dict with auto-injected parameters (same as FrameProcessor)
  541. ext = {
  542. "index": 1, # Preview uses index 1
  543. }
  544. # Add custom parameters from user input
  545. if custom_values_for_video:
  546. ext.update(custom_values_for_video)
  547. # Generate preview
  548. preview_path = run_async(generator.generate_frame(
  549. title=preview_title,
  550. text=preview_text,
  551. image=preview_image,
  552. ext=ext
  553. ))
  554. # Display preview
  555. if preview_path:
  556. st.success(tr("template.preview_success"))
  557. st.image(
  558. preview_path,
  559. caption=tr("template.preview_caption", template=frame_template),
  560. )
  561. # Show file path
  562. st.caption(f"📁 {preview_path}")
  563. else:
  564. st.error("Failed to generate preview")
  565. except Exception as e:
  566. st.error(tr("template.preview_failed", error=str(e)))
  567. logger.exception(e)
  568. # ====================================================================
  569. # Media Generation Section (conditional based on template)
  570. # ====================================================================
  571. # Check if current template requires media generation
  572. template_media_type = st.session_state.get('template_media_type', 'image')
  573. template_requires_media = st.session_state.get('template_requires_media', True)
  574. if template_requires_media:
  575. # Template requires media - show Media Generation Section
  576. with st.container(border=True):
  577. # Dynamic section title based on template type
  578. if template_media_type == "video":
  579. section_title = tr('section.video')
  580. else:
  581. section_title = tr('section.image')
  582. st.markdown(f"**{section_title}**")
  583. # 1. ComfyUI Workflow selection
  584. with st.expander(tr("help.feature_description"), expanded=False):
  585. st.markdown(f"**{tr('help.what')}**")
  586. if template_media_type == "video":
  587. st.markdown(tr('style.video_workflow_what'))
  588. else:
  589. st.markdown(tr("style.workflow_what"))
  590. st.markdown(f"**{tr('help.how')}**")
  591. if template_media_type == "video":
  592. st.markdown(tr('style.video_workflow_how'))
  593. else:
  594. st.markdown(tr("style.workflow_how"))
  595. # Get available workflows and filter by template type
  596. all_workflows = pixelle_video.media.list_workflows()
  597. # Filter workflows based on template media type
  598. if template_media_type == "video":
  599. # Only show video_ workflows
  600. workflows = [wf for wf in all_workflows if "video_" in wf["key"].lower()]
  601. else:
  602. # Only show image_ workflows (exclude video_)
  603. workflows = [wf for wf in all_workflows if "video_" not in wf["key"].lower()]
  604. # Build options for selectbox
  605. # Display: "image_flux.json - Runninghub"
  606. # Value: "runninghub/image_flux.json"
  607. workflow_options = [wf["display_name"] for wf in workflows]
  608. workflow_keys = [wf["key"] for wf in workflows]
  609. # Default to first option (should be runninghub by sorting)
  610. default_workflow_index = 0
  611. # If user has a saved preference in config, try to match it
  612. comfyui_config = config_manager.get_comfyui_config()
  613. # Select config based on template type (image or video)
  614. media_config_key = "video" if template_media_type == "video" else "image"
  615. saved_workflow = comfyui_config.get(media_config_key, {}).get("default_workflow", "")
  616. if saved_workflow and saved_workflow in workflow_keys:
  617. default_workflow_index = workflow_keys.index(saved_workflow)
  618. workflow_display = st.selectbox(
  619. "Workflow",
  620. workflow_options if workflow_options else ["No workflows found"],
  621. index=default_workflow_index,
  622. label_visibility="collapsed",
  623. key="media_workflow_select"
  624. )
  625. # Get the actual workflow key (e.g., "runninghub/image_flux.json")
  626. if workflow_options:
  627. workflow_selected_index = workflow_options.index(workflow_display)
  628. workflow_key = workflow_keys[workflow_selected_index]
  629. else:
  630. workflow_key = "runninghub/image_flux.json" # fallback
  631. # Check and warn for selfhost media workflow (auto popup if not confirmed)
  632. check_and_warn_selfhost_workflow(workflow_key)
  633. # Get media size from template
  634. media_width = st.session_state.get('template_media_width')
  635. media_height = st.session_state.get('template_media_height')
  636. # Display media size info (read-only)
  637. if template_media_type == "video":
  638. size_info_text = tr('style.video_size_info', width=media_width, height=media_height)
  639. else:
  640. size_info_text = tr('style.image_size_info', width=media_width, height=media_height)
  641. st.info(f"📐 {size_info_text}")
  642. # Prompt prefix input
  643. # Get current prompt_prefix from config (based on media type)
  644. current_prefix = comfyui_config.get(media_config_key, {}).get("prompt_prefix", "")
  645. # Prompt prefix input (temporary, not saved to config)
  646. prompt_prefix = st.text_area(
  647. tr('style.prompt_prefix'),
  648. value=current_prefix,
  649. placeholder=tr("style.prompt_prefix_placeholder"),
  650. height=80,
  651. label_visibility="visible",
  652. help=tr("style.prompt_prefix_help")
  653. )
  654. # Media preview expander
  655. preview_title = tr("style.video_preview_title") if template_media_type == "video" else tr("style.preview_title")
  656. with st.expander(preview_title, expanded=False):
  657. # Test prompt input
  658. if template_media_type == "video":
  659. test_prompt_label = tr("style.test_video_prompt")
  660. test_prompt_value = "a dog running in the park"
  661. else:
  662. test_prompt_label = tr("style.test_prompt")
  663. test_prompt_value = "a dog"
  664. test_prompt = st.text_input(
  665. test_prompt_label,
  666. value=test_prompt_value,
  667. help=tr("style.test_prompt_help"),
  668. key="style_test_prompt"
  669. )
  670. # Preview button
  671. preview_button_label = tr("style.video_preview") if template_media_type == "video" else tr("style.preview")
  672. if st.button(preview_button_label, key="preview_style", use_container_width=True):
  673. previewing_text = tr("style.video_previewing") if template_media_type == "video" else tr("style.previewing")
  674. with st.spinner(previewing_text):
  675. try:
  676. from pixelle_video.utils.prompt_helper import build_image_prompt
  677. # Build final prompt with prefix
  678. final_prompt = build_image_prompt(test_prompt, prompt_prefix)
  679. # Generate preview media (use user-specified size and media type)
  680. media_result = run_async(pixelle_video.media(
  681. prompt=final_prompt,
  682. workflow=workflow_key,
  683. media_type=template_media_type,
  684. width=int(media_width),
  685. height=int(media_height)
  686. ))
  687. preview_media_path = media_result.url
  688. # Display preview (support both URL and local path)
  689. if preview_media_path:
  690. success_text = tr("style.video_preview_success") if template_media_type == "video" else tr("style.preview_success")
  691. st.success(success_text)
  692. if template_media_type == "video":
  693. # Display video
  694. st.video(preview_media_path)
  695. else:
  696. # Display image
  697. if preview_media_path.startswith('http'):
  698. # URL - use directly
  699. img_html = f'<div class="preview-image"><img src="{preview_media_path}" alt="Style Preview"/></div>'
  700. else:
  701. # Local file - encode as base64
  702. with open(preview_media_path, 'rb') as f:
  703. img_data = base64.b64encode(f.read()).decode()
  704. img_html = f'<div class="preview-image"><img src="data:image/png;base64,{img_data}" alt="Style Preview"/></div>'
  705. st.markdown(img_html, unsafe_allow_html=True)
  706. # Show the final prompt used
  707. st.info(f"**{tr('style.final_prompt_label')}**\n{final_prompt}")
  708. # Show file path
  709. st.caption(f"📁 {preview_media_path}")
  710. else:
  711. st.error(tr("style.preview_failed_general"))
  712. except Exception as e:
  713. st.error(tr("style.preview_failed", error=str(e)))
  714. logger.exception(e)
  715. else:
  716. # Template doesn't need images - show simplified message
  717. with st.container(border=True):
  718. st.markdown(f"**{tr('section.image')}**")
  719. st.info("ℹ️ " + tr("image.not_required"))
  720. st.caption(tr("image.not_required_hint"))
  721. # Get media size from template (even though not used, for consistency)
  722. media_width = st.session_state.get('template_media_width')
  723. media_height = st.session_state.get('template_media_height')
  724. # Set default values for later use
  725. workflow_key = None
  726. prompt_prefix = ""
  727. # Return all style configuration parameters
  728. return {
  729. "tts_inference_mode": tts_mode,
  730. "tts_voice": selected_voice if tts_mode == "local" else None,
  731. "tts_speed": tts_speed if tts_mode == "local" else None,
  732. "tts_workflow": tts_workflow_key if tts_mode == "comfyui" else None,
  733. "ref_audio": str(ref_audio_path) if ref_audio_path else None,
  734. "frame_template": frame_template,
  735. "template_params": custom_values_for_video if custom_values_for_video else None,
  736. "media_workflow": workflow_key,
  737. "prompt_prefix": prompt_prefix if prompt_prefix else "",
  738. "media_width": media_width,
  739. "media_height": media_height
  740. }