| 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 | Style configuration components for web UI (middle column) |
| 15 | """ |
| 16 | |
| 17 | import os |
| 18 | from pathlib import Path |
| 19 | |
| 20 | import streamlit as st |
| 21 | from loguru import logger |
| 22 | |
| 23 | from web.i18n import tr, get_language |
| 24 | from web.utils.async_helpers import run_async |
| 25 | from pixelle_video.config import config_manager |
| 26 | |
| 27 | |
| 28 | def render_style_config(pixelle_video): |
| 29 | """Render style configuration section (middle column)""" |
| 30 | # TTS Section (moved from left column) |
| 31 | # ==================================================================== |
| 32 | with st.container(border=True): |
| 33 | st.markdown(f"**{tr('section.tts')}**") |
| 34 | |
| 35 | with st.expander(tr("help.feature_description"), expanded=False): |
| 36 | st.markdown(f"**{tr('help.what')}**") |
| 37 | st.markdown(tr("tts.what")) |
| 38 | st.markdown(f"**{tr('help.how')}**") |
| 39 | st.markdown(tr("tts.how")) |
| 40 | |
| 41 | # Get TTS config |
| 42 | comfyui_config = config_manager.get_comfyui_config() |
| 43 | tts_config = comfyui_config["tts"] |
| 44 | |
| 45 | # Inference mode selection |
| 46 | tts_mode = st.radio( |
| 47 | tr("tts.inference_mode"), |
| 48 | ["local", "comfyui"], |
| 49 | horizontal=True, |
| 50 | format_func=lambda x: tr(f"tts.mode.{x}"), |
| 51 | index=0 if tts_config.get("inference_mode", "local") == "local" else 1, |
| 52 | key="digital_tts_inference_mode" |
| 53 | ) |
| 54 | |
| 55 | # Show hint based on mode |
| 56 | if tts_mode == "local": |
| 57 | st.caption(tr("tts.mode.local_hint")) |
| 58 | else: |
| 59 | st.caption(tr("tts.mode.comfyui_hint")) |
| 60 | |
| 61 | # ================================================================ |
| 62 | # Local Mode UI |
| 63 | # ================================================================ |
| 64 | if tts_mode == "local": |
| 65 | # Import voice configuration |
| 66 | from pixelle_video.tts_voices import EDGE_TTS_VOICES, get_voice_display_name |
| 67 | |
| 68 | # Get saved voice from config |
| 69 | local_config = tts_config.get("local", {}) |
| 70 | saved_voice = local_config.get("voice", "zh-CN-YunjianNeural") |
| 71 | saved_speed = local_config.get("speed", 1.2) |
| 72 | |
| 73 | # Build voice options with i18n |
| 74 | voice_options = [] |
| 75 | voice_ids = [] |
| 76 | default_voice_index = 0 |
| 77 | |
| 78 | for idx, voice_config in enumerate(EDGE_TTS_VOICES): |
| 79 | voice_id = voice_config["id"] |
| 80 | display_name = get_voice_display_name(voice_id, tr, get_language()) |
| 81 | voice_options.append(display_name) |
| 82 | voice_ids.append(voice_id) |
| 83 | |
| 84 | # Set default index if matches saved voice |
| 85 | if voice_id == saved_voice: |
| 86 | default_voice_index = idx |
| 87 | |
| 88 | # Two-column layout: Voice | Speed |
| 89 | voice_col, speed_col = st.columns([1, 1]) |
| 90 | |
| 91 | with voice_col: |
| 92 | # Voice selector |
| 93 | selected_voice_display = st.selectbox( |
| 94 | tr("tts.voice_selector"), |
| 95 | voice_options, |
| 96 | index=default_voice_index, |
| 97 | key="digital_tts_local_voice" |
| 98 | ) |
| 99 | |
| 100 | # Get actual voice ID |
| 101 | selected_voice_index = voice_options.index(selected_voice_display) |
| 102 | selected_voice = voice_ids[selected_voice_index] |
| 103 | |
| 104 | with speed_col: |
| 105 | # Speed slider |
| 106 | tts_speed = st.slider( |
| 107 | tr("tts.speed"), |
| 108 | min_value=0.5, |
| 109 | max_value=2.0, |
| 110 | value=saved_speed, |
| 111 | step=0.1, |
| 112 | format="%.1fx", |
| 113 | key="digital_tts_local_speed" |
| 114 | ) |
| 115 | st.caption(tr("tts.speed_label", speed=f"{tts_speed:.1f}")) |
| 116 | |
| 117 | # Variables for video generation |
| 118 | tts_workflow_key = None |
| 119 | ref_audio_path = None |
| 120 | |
| 121 | # ================================================================ |
| 122 | # ComfyUI Mode UI |
| 123 | # ================================================================ |
| 124 | else: # comfyui mode |
| 125 | tts_workflow_key = "runninghub/tts_index2.json" # fallback |
| 126 | |
| 127 | # Reference audio upload (optional, for voice cloning) |
| 128 | ref_audio_file = st.file_uploader( |
| 129 | tr("tts.ref_audio"), |
| 130 | type=["mp3", "wav", "flac", "m4a", "aac", "ogg"], |
| 131 | help=tr("tts.ref_audio_help"), |
| 132 | key="digital_ref_audio_upload" |
| 133 | ) |
| 134 | |
| 135 | # Save uploaded ref_audio to temp file if provided |
| 136 | ref_audio_path = None |
| 137 | if ref_audio_file is not None: |
| 138 | # Audio preview player (directly play uploaded file) |
| 139 | st.audio(ref_audio_file) |
| 140 | |
| 141 | # Save to temp directory |
| 142 | temp_dir = Path("temp") |
| 143 | temp_dir.mkdir(exist_ok=True) |
| 144 | ref_audio_path = temp_dir / f"ref_audio_{ref_audio_file.name}" |
| 145 | with open(ref_audio_path, "wb") as f: |
| 146 | f.write(ref_audio_file.getbuffer()) |
| 147 | |
| 148 | # Variables for video generation |
| 149 | selected_voice = None |
| 150 | tts_speed = None |
| 151 | |
| 152 | # ================================================================ |
| 153 | # TTS Preview (works for both modes) |
| 154 | # ================================================================ |
| 155 | with st.expander(tr("tts.preview_title"), expanded=False): |
| 156 | # Preview text input |
| 157 | preview_text = st.text_input( |
| 158 | tr("tts.preview_text"), |
| 159 | value="大家好,这是一段测试语音。", |
| 160 | placeholder=tr("tts.preview_text_placeholder"), |
| 161 | key="digital_tts_preview_text" |
| 162 | ) |
| 163 | |
| 164 | # Preview button |
| 165 | if st.button(tr("tts.preview_button"), key="gidital_preview_tts", use_container_width=True): |
| 166 | with st.spinner(tr("tts.previewing")): |
| 167 | try: |
| 168 | # Build TTS params based on mode |
| 169 | tts_params = { |
| 170 | "text": preview_text, |
| 171 | "inference_mode": tts_mode |
| 172 | } |
| 173 | |
| 174 | if tts_mode == "local": |
| 175 | tts_params["voice"] = selected_voice |
| 176 | tts_params["speed"] = tts_speed |
| 177 | else: # comfyui |
| 178 | tts_params["workflow"] = tts_workflow_key |
| 179 | if ref_audio_path: |
| 180 | tts_params["ref_audio"] = str(ref_audio_path) |
| 181 | |
| 182 | audio_path = run_async(pixelle_video.tts(**tts_params)) |
| 183 | |
| 184 | # Play the audio |
| 185 | if audio_path: |
| 186 | st.success(tr("tts.preview_success")) |
| 187 | if os.path.exists(audio_path): |
| 188 | st.audio(audio_path, format="audio/mp3") |
| 189 | elif audio_path.startswith('http'): |
| 190 | st.audio(audio_path) |
| 191 | else: |
| 192 | st.error("Failed to generate preview audio") |
| 193 | |
| 194 | # Show file path |
| 195 | st.caption(f"📁 {audio_path}") |
| 196 | else: |
| 197 | st.error("Failed to generate preview audio") |
| 198 | except Exception as e: |
| 199 | st.error(tr("tts.preview_failed", error=str(e))) |
| 200 | logger.exception(e) |
| 201 | |
| 202 | # Return all style configuration parameters (Simplified version only local TTS) |
| 203 | return { |
| 204 | "tts_inference_mode": tts_mode, |
| 205 | "tts_voice": selected_voice if tts_mode == "local" else None, |
| 206 | "tts_speed": tts_speed if tts_mode == "local" else None, |
| 207 | "tts_workflow": tts_workflow_key if tts_mode == "comfyui" else None, |
| 208 | "ref_audio": str(ref_audio_path) if ref_audio_path else None, |
| 209 | } |