# Copyright (C) 2025 AIDC-AI # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Style configuration components for web UI (middle column) """ import os from pathlib import Path import streamlit as st from loguru import logger from web.i18n import tr, get_language from web.utils.async_helpers import run_async from web.utils.streamlit_helpers import check_and_warn_selfhost_workflow from pixelle_video.config import config_manager def render_style_config(pixelle_video): """Render style configuration section (middle column)""" # TTS Section (moved from left column) # ==================================================================== with st.container(border=True): st.markdown(f"**{tr('section.tts')}**") with st.expander(tr("help.feature_description"), expanded=False): st.markdown(f"**{tr('help.what')}**") st.markdown(tr("tts.what")) st.markdown(f"**{tr('help.how')}**") st.markdown(tr("tts.how")) # Get TTS config comfyui_config = config_manager.get_comfyui_config() tts_config = comfyui_config["tts"] # Inference mode selection tts_mode = st.radio( tr("tts.inference_mode"), ["local", "comfyui"], horizontal=True, format_func=lambda x: tr(f"tts.mode.{x}"), index=0 if tts_config.get("inference_mode", "local") == "local" else 1, key="tts_inference_mode" ) # Show hint based on mode if tts_mode == "local": st.caption(tr("tts.mode.local_hint")) else: st.caption(tr("tts.mode.comfyui_hint")) # ================================================================ # Local Mode UI # ================================================================ if tts_mode == "local": # Import voice configuration from pixelle_video.tts_voices import EDGE_TTS_VOICES, get_voice_display_name # Get saved voice from config local_config = tts_config.get("local", {}) saved_voice = local_config.get("voice", "zh-CN-YunjianNeural") saved_speed = local_config.get("speed", 1.2) # Build voice options with i18n voice_options = [] voice_ids = [] default_voice_index = 0 for idx, voice_config in enumerate(EDGE_TTS_VOICES): voice_id = voice_config["id"] display_name = get_voice_display_name(voice_id, tr, get_language()) voice_options.append(display_name) voice_ids.append(voice_id) # Set default index if matches saved voice if voice_id == saved_voice: default_voice_index = idx # Two-column layout: Voice | Speed voice_col, speed_col = st.columns([1, 1]) with voice_col: # Voice selector selected_voice_display = st.selectbox( tr("tts.voice_selector"), voice_options, index=default_voice_index, key="tts_local_voice" ) # Get actual voice ID selected_voice_index = voice_options.index(selected_voice_display) selected_voice = voice_ids[selected_voice_index] with speed_col: # Speed slider tts_speed = st.slider( tr("tts.speed"), min_value=0.5, max_value=2.0, value=saved_speed, step=0.1, format="%.1fx", key="tts_local_speed" ) st.caption(tr("tts.speed_label", speed=f"{tts_speed:.1f}")) # Variables for video generation tts_workflow_key = None ref_audio_path = None # ================================================================ # ComfyUI Mode UI # ================================================================ else: # comfyui mode # Get available TTS workflows tts_workflows = pixelle_video.tts.list_workflows() # Build options for selectbox tts_workflow_options = [wf["display_name"] for wf in tts_workflows] tts_workflow_keys = [wf["key"] for wf in tts_workflows] # Default to saved workflow if exists default_tts_index = 0 saved_tts_workflow = tts_config.get("comfyui", {}).get("default_workflow") if saved_tts_workflow and saved_tts_workflow in tts_workflow_keys: default_tts_index = tts_workflow_keys.index(saved_tts_workflow) tts_workflow_display = st.selectbox( "TTS Workflow", tts_workflow_options if tts_workflow_options else ["No TTS workflows found"], index=default_tts_index, label_visibility="collapsed", key="tts_workflow_select" ) # Get the actual workflow key if tts_workflow_options: tts_selected_index = tts_workflow_options.index(tts_workflow_display) tts_workflow_key = tts_workflow_keys[tts_selected_index] else: tts_workflow_key = "selfhost/tts_edge.json" # fallback # Check and warn for selfhost TTS workflow (auto popup if not confirmed) check_and_warn_selfhost_workflow(tts_workflow_key) # Reference audio upload (optional, for voice cloning) ref_audio_file = st.file_uploader( tr("tts.ref_audio"), type=["mp3", "wav", "flac", "m4a", "aac", "ogg"], help=tr("tts.ref_audio_help"), key="ref_audio_upload" ) # Save uploaded ref_audio to temp file if provided ref_audio_path = None if ref_audio_file is not None: # Audio preview player (directly play uploaded file) st.audio(ref_audio_file) # Save to temp directory temp_dir = Path("temp") temp_dir.mkdir(exist_ok=True) ref_audio_path = temp_dir / f"ref_audio_{ref_audio_file.name}" with open(ref_audio_path, "wb") as f: f.write(ref_audio_file.getbuffer()) # Variables for video generation selected_voice = None tts_speed = None # ================================================================ # TTS Preview (works for both modes) # ================================================================ with st.expander(tr("tts.preview_title"), expanded=False): # Preview text input preview_text = st.text_input( tr("tts.preview_text"), value="大家好,这是一段测试语音。", placeholder=tr("tts.preview_text_placeholder"), key="tts_preview_text" ) # Preview button if st.button(tr("tts.preview_button"), key="preview_tts", use_container_width=True): with st.spinner(tr("tts.previewing")): try: # Build TTS params based on mode tts_params = { "text": preview_text, "inference_mode": tts_mode } if tts_mode == "local": tts_params["voice"] = selected_voice tts_params["speed"] = tts_speed else: # comfyui tts_params["workflow"] = tts_workflow_key if ref_audio_path: tts_params["ref_audio"] = str(ref_audio_path) audio_path = run_async(pixelle_video.tts(**tts_params)) # Play the audio if audio_path: st.success(tr("tts.preview_success")) if os.path.exists(audio_path): st.audio(audio_path, format="audio/mp3") elif audio_path.startswith('http'): st.audio(audio_path) else: st.error("Failed to generate preview audio") # Show file path st.caption(f"📁 {audio_path}") else: st.error("Failed to generate preview audio") except Exception as e: st.error(tr("tts.preview_failed", error=str(e))) logger.exception(e) # ==================================================================== # Storyboard Template Section # ==================================================================== def get_template_preview_path(template_path: str, language: str = "zh_CN") -> str: """ Get the preview image path for a template based on language. Args: template_path: Template path like "1080x1920/image_default.html" language: Language code, either "zh_CN" or "en" Returns: Path to preview image in docs/images/ """ # Extract size and template name from path # e.g., "1080x1920/image_default.html" -> size="1080x1920", name="image_default" path_parts = template_path.split('/') if len(path_parts) >= 2: size = path_parts[0] # e.g., "1080x1920" template_file = path_parts[1] # e.g., "image_default.html" template_name = template_file.replace('.html', '') # e.g., "image_default" # Build preview image path # Format: docs/images/{size}/{template_name}.jpg or {template_name}_en.jpg # Chinese uses Chinese preview, all other languages use English preview for better i18n suffix = "" if language == "zh_CN" else "_en" # Try different image extensions for ext in ['.jpg', '.png']: preview_path = f"docs/images/{size}/{template_name}{suffix}{ext}" if os.path.exists(preview_path): return preview_path # Fallback: try without language suffix (for templates with only one version) for ext in ['.jpg', '.png']: preview_path = f"docs/images/{size}/{template_name}{ext}" if os.path.exists(preview_path): return preview_path # If no preview found, return empty string return "" with st.container(border=True): st.markdown(f"**{tr('section.template')}**") with st.expander(tr("help.feature_description"), expanded=False): st.markdown(f"**{tr('help.what')}**") st.markdown(tr("template.what")) st.markdown(f"**{tr('help.how')}**") st.markdown(tr("template.how")) # Template preview link (based on language) current_lang = get_language() # Import template utilities from pixelle_video.utils.template_util import get_templates_grouped_by_size_and_type, get_template_type # Template type selector st.markdown(f"**{tr('template.type_selector')}**") template_type_options = { 'static': tr('template.type.static'), 'image': tr('template.type.image'), 'video': tr('template.type.video') } # Radio buttons in horizontal layout selected_template_type = st.radio( tr('template.type_selector'), options=list(template_type_options.keys()), format_func=lambda x: template_type_options[x], index=1, # Default to 'image' key="template_type_selector", label_visibility="collapsed", horizontal=True ) # Display hint based on selected type (below radio buttons) if selected_template_type == 'static': st.info(tr('template.type.static_hint')) elif selected_template_type == 'image': st.info(tr('template.type.image_hint')) elif selected_template_type == 'video': st.info(tr('template.type.video_hint')) # Get templates grouped by size, filtered by selected type grouped_templates = get_templates_grouped_by_size_and_type(selected_template_type) if not grouped_templates: st.warning(f"No {template_type_options[selected_template_type]} templates found. Please select a different type or add templates.") st.stop() # Build orientation i18n mapping ORIENTATION_I18N = { 'portrait': tr('orientation.portrait'), 'landscape': tr('orientation.landscape'), 'square': tr('orientation.square') } # Get default template from config template_config = pixelle_video.config.get("template", {}) config_default_template = template_config.get("default_template", "1080x1920/image_default.html") # Backward compatibility if config_default_template == "1080x1920/default.html": config_default_template = "1080x1920/image_default.html" # Determine type-specific default template type_default_templates = { 'static': '1080x1920/static_default.html', 'image': '1080x1920/image_default.html', 'video': '1080x1920/video_default.html' } type_specific_default = type_default_templates.get(selected_template_type, config_default_template) # Initialize selected template in session state if not exists if 'selected_template' not in st.session_state: st.session_state['selected_template'] = type_specific_default # Track last selected template type to detect type changes last_template_type = st.session_state.get('last_template_type', None) if last_template_type != selected_template_type: # Template type changed, reset to type-specific default st.session_state['selected_template'] = type_specific_default st.session_state['last_template_type'] = selected_template_type # Collect size groups and prepare tabs size_groups = [] size_labels = [] for size, templates in grouped_templates.items(): if not templates: continue # Filter templates to only include those with proper naming convention # Only show templates starting with static_, image_, or video_ valid_templates = [] for template in templates: template_name = template.display_info.name if template_name.startswith(('static_', 'image_', 'video_')): valid_templates.append(template) # Skip if no valid templates after filtering if not valid_templates: continue # Separate templates into two groups: with preview and without preview templates_with_preview = [] templates_without_preview = [] for template in valid_templates: preview_path = get_template_preview_path(template.template_path, current_lang) if preview_path and os.path.exists(preview_path): templates_with_preview.append(template) else: templates_without_preview.append(template) # Skip this group if no templates at all if not templates_with_preview and not templates_without_preview: continue # Combine: templates with preview first, then without preview all_templates = templates_with_preview + templates_without_preview # Get orientation from first template in group orientation = ORIENTATION_I18N.get( all_templates[0].display_info.orientation, all_templates[0].display_info.orientation ) width = all_templates[0].display_info.width height = all_templates[0].display_info.height # Create tab label tab_label = f"{orientation} {width}×{height}" size_labels.append(tab_label) size_groups.append(all_templates) # Create tabs for each size group (wrapped in expander) with st.expander(tr("template.gallery_view"), expanded=True): if size_groups: tabs = st.tabs(size_labels) for tab, all_templates in zip(tabs, size_groups): with tab: # Create grid layout (5 columns) num_cols = 5 cols = st.columns(num_cols) for idx, template in enumerate(all_templates): col_idx = idx % num_cols with cols[col_idx]: # Get preview image path preview_path = get_template_preview_path(template.template_path, current_lang) # Display preview image or placeholder if preview_path and os.path.exists(preview_path): st.image(preview_path, use_container_width=True) else: # Placeholder for templates without preview (fixed height, compact layout) st.markdown( f"""