Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import os | |
| import io | |
| import concurrent.futures | |
| from PIL import Image | |
| from google import genai | |
| # ๋ก์ง ๋ชจ๋ ์ํฌํธ | |
| import logic_image | |
| import logic_seo | |
| import logic_tts | |
| from config_style import STYLE_DEFINITIONS, THUMBNAIL_STRATEGIES | |
| # ========================================== | |
| # 1. ํ์ด์ง ์ค์ ๋ฐ CSS ๋์์ธ | |
| # ========================================== | |
| st.set_page_config( | |
| page_title="Nano Banana Studio Pro", | |
| page_icon="๐", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # ์ธ์ ์ด๊ธฐํ | |
| if 'scene_results' not in st.session_state: | |
| st.session_state['scene_results'] = [] | |
| if 'final_audio' not in st.session_state: | |
| st.session_state['final_audio'] = None | |
| if 'shared_script' not in st.session_state: | |
| st.session_state['shared_script'] = "" | |
| # [CSS] ๋คํฌ๋ชจ๋ + ์ค๋ ์ง ํฌ์ธํธ ๋์์ธ | |
| st.markdown(""" | |
| <style> | |
| @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'); | |
| html, body, [class*="css"] { | |
| font-family: 'Pretendard', sans-serif; | |
| } | |
| .stApp { | |
| background-color: #121212; | |
| color: #E0E0E0; | |
| } | |
| /* ์ฌ์ด๋๋ฐ */ | |
| section[data-testid="stSidebar"] { | |
| background-color: #0A0A0A; | |
| border-right: 1px solid #333; | |
| } | |
| /* ์ ๋ ฅ์ฐฝ ๊ฐ๋ ์ฑ ๊ฐ์ */ | |
| .stTextInput input, .stTextArea textarea, .stSelectbox div[data-baseweb="select"] > div { | |
| background-color: #252525 !important; | |
| color: #FFFFFF !important; | |
| border: 1px solid #404040 !important; | |
| border-radius: 8px !important; | |
| } | |
| .stTextArea textarea[disabled] { | |
| background-color: #1E1E1E !important; | |
| color: #AAAAAA !important; | |
| border: 1px solid #333 !important; | |
| } | |
| ::placeholder { | |
| color: #AAAAAA !important; | |
| opacity: 1; | |
| } | |
| /* ํญ ๋์์ธ */ | |
| button[data-baseweb="tab"] { | |
| font-size: 16px; | |
| font-weight: 700; | |
| color: #888; | |
| } | |
| button[data-baseweb="tab"][aria-selected="true"] { | |
| color: #FFC107 !important; | |
| background-color: transparent !important; | |
| border-bottom: 2px solid #FFC107 !important; | |
| } | |
| /* ๋ฒํผ ๋์์ธ */ | |
| div.stButton > button[kind="primary"] { | |
| background: linear-gradient(135deg, #FFC107 0%, #FF9800 100%); | |
| color: #000000; | |
| border: none; | |
| padding: 14px 24px; | |
| font-size: 16px; | |
| font-weight: 800; | |
| border-radius: 8px; | |
| transition: all 0.3s ease; | |
| } | |
| div.stButton > button[kind="primary"]:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(255, 193, 7, 0.4); | |
| } | |
| div.stButton > button[kind="secondary"] { | |
| border-color: #555; | |
| color: #ddd; | |
| } | |
| h1, h2, h3, h4, h5 { color: #FFFFFF !important; } | |
| .highlight { color: #FFC107; } | |
| div[data-testid="stExpander"] { | |
| background-color: #1E1E1E; | |
| border: 1px solid #333; | |
| border-radius: 8px; | |
| } | |
| div[data-testid="stFileUploader"] section { | |
| background-color: #1E1E1E; | |
| border: 2px dashed #444; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ========================================== | |
| # 2. ์ฌ์ด๋๋ฐ (ํตํฉ ์ค์ ) | |
| # ========================================== | |
| with st.sidebar: | |
| st.markdown("## โ๏ธ ๊ธฐ๋ณธ ์ค์ (Basic)") | |
| api_key = st.text_input("Google API Key", type="password", placeholder="API ํค๋ฅผ ์ ๋ ฅํ์ธ์...", label_visibility="collapsed") | |
| st.divider() | |
| st.markdown("### ๐จ Model Engine") | |
| model_choice = st.radio("์ฌ์ฉํ ๋ชจ๋ธ", ["โก Fast (Gemini 2.0)", "๐ Pro (Imagen 3)"], label_visibility="collapsed") | |
| if "Fast" in model_choice: | |
| image_model_id = "gemini-2.0-flash" | |
| text_model_id = "gemini-2.0-flash" | |
| else: | |
| # [๋ณต๊ตฌ] ์ฌ์ฉ์๋์ด ์ํ์๋ ๊ทธ ๋ชจ๋ธ ID! | |
| image_model_id = "gemini-3-pro-image-preview" | |
| text_model_id = "gemini-3-pro-preview" | |
| st.divider() | |
| st.markdown("### ๐ Canvas Ratio") | |
| ar_radio = st.radio("๋น์จ", ["16:9", "9:16"], label_visibility="collapsed", horizontal=True) | |
| aspect_ratio = ar_radio | |
| st.divider() | |
| st.markdown("### ๐ ๏ธ ๊ณ ๊ธ ์ค์ (TTS & Split)") | |
| st.caption("TTS Model ID") | |
| tts_model_id = st.text_input("TTS ID", value="gemini-2.5-pro-preview-tts", label_visibility="collapsed") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.caption("Image Scene Split (์ด๋ฏธ์ง ์์ฑ์ฉ)") | |
| duration_per_scene = st.slider("์ฅ๋ฉด๋น ์๊ฐ(์ด)", 3, 30, 5) | |
| split_criteria = duration_per_scene * 8 | |
| st.info(f"๐ก {duration_per_scene}์ด (์ฝ {split_criteria}์) ๋จ์๋ก ์ฅ๋ฉด์ ๋๋๋๋ค.") | |
| st.caption("โป TTS๋ ์ค์ ๊ณผ ๋ฌด๊ดํ๊ฒ 500์ ๋จ์๋ก ์ต์ ํ๋ฉ๋๋ค.") | |
| st.divider() | |
| st.markdown("## ๐จ ์คํ์ผ ๋ฐ ์บ๋ฆญํฐ ์ค์ ") | |
| st.caption("์์ ์ ์ฒด์ ๋ถ์๊ธฐ์ ์บ๋ฆญํฐ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown("#### ๐๏ธ ํํ(Style) ์ ํ") | |
| style_options = list(STYLE_DEFINITIONS.keys()) + ["์ง์ ์ ๋ ฅ"] | |
| selected_style = st.selectbox("์คํ์ผ ์ ํ", style_options, label_visibility="collapsed") | |
| custom_style_input = "" | |
| current_prompt = "" | |
| if selected_style == "์ง์ ์ ๋ ฅ": | |
| custom_style_input = st.text_input("์คํ์ผ ํ๋กฌํํธ", placeholder="์: ์ง๋ธ๋ฆฌ ์คํ์ผ, ์์ฑํํ") | |
| current_prompt = custom_style_input if custom_style_input else "(ํ๋กฌํํธ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์)" | |
| else: | |
| style_value = STYLE_DEFINITIONS[selected_style] | |
| if isinstance(style_value, dict): | |
| current_prompt = style_value.get('prompt', str(style_value)) | |
| else: | |
| current_prompt = str(style_value) | |
| st.markdown("#### ๐ ์ ์ฉ๋ ํ๋กฌํํธ (๋ฏธ๋ฆฌ๋ณด๊ธฐ)") | |
| st.text_area("ํ๋กฌํํธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", value=current_prompt, height=150, disabled=True, label_visibility="collapsed") | |
| st.divider() | |
| st.markdown("#### ๐ค ์บ๋ฆญํฐ ์ฐธ์กฐ (Reference)") | |
| uploaded_file = st.file_uploader("์ด๋ฏธ์ง ์ ๋ก๋", type=["png", "jpg", "jpeg"], label_visibility="collapsed") | |
| reference_image = None | |
| if uploaded_file: | |
| reference_image = Image.open(uploaded_file) | |
| st.image(reference_image, caption="์ฐธ์กฐ ์ด๋ฏธ์ง", use_container_width=True) | |
| # ========================================== | |
| # 3. ๋ฉ์ธ ์ฝํ ์ธ | |
| # ========================================== | |
| st.title("Nano Banana <span class='highlight'>Studio Pro</span>", anchor=False) | |
| st.markdown("<div style='margin-top: -15px; color: #888; margin-bottom: 20px;'>AI Powered All-in-One Workspace</div>", unsafe_allow_html=True) | |
| tab1, tab2, tab3 = st.tabs(["๐ฌ ์ฅ๋ฉด ์์ฑ (Scenes)", "๐ ๊ธฐํ/SEO (Analysis)", "๐๏ธ ์ฑ์ฐ/TTS (Voice)"]) | |
| # ---------------------------------------------------------------- | |
| # [TAB 1] ์ฅ๋ฉด/์ด๋ฏธ์ง ์์ฑ (๋ฉ์ธ ๊ธฐ๋ฅ) | |
| # ---------------------------------------------------------------- | |
| with tab1: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown("#### โถ ๋๋ณธ ์ ๋ ฅ (Script)") | |
| script_input = st.text_area( | |
| "๋๋ณธ", | |
| height=300, | |
| placeholder="๋๋ณธ์ ์ค๊ธ๋ก ๋ถ์ฌ๋ฃ์ผ์ธ์. ์ฌ์ด๋๋ฐ์์ ์ค์ ํ ์๊ฐ(์ด) ๋จ์๋ก AI๊ฐ ์ฅ๋ฉด์ ์๋์ผ๋ก ๋๋๋๋ค.", | |
| label_visibility="collapsed", | |
| key="scene_script_input" | |
| ) | |
| if script_input: | |
| st.session_state['shared_script'] = script_input | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| # ์์ฑ ๋ฒํผ | |
| if st.button("๐ ์ฅ๋ฉด ์์ฑ ์์ (Generate Scenes)", type="primary", use_container_width=True): | |
| if not api_key: st.error("โ ๏ธ ์ฌ์ด๋๋ฐ์ API Key๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| elif not script_input: st.warning("โ ๏ธ ๋๋ณธ์ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| else: | |
| # [ํต์ฌ] ์ด๋ฏธ์ง ์์ฑ์ ์ฌ๋ผ์ด๋ ๊ฐ(split_criteria)์ ์ฌ์ฉ | |
| raw_scenes = logic_tts.split_text_smartly(script_input, limit=split_criteria) | |
| scenes_text = raw_scenes[:10] | |
| st.toast(f"๐ {len(scenes_text)}๊ฐ ์ฅ๋ฉด์ผ๋ก ๋ถํ ํ์ฌ ์์ฑ์ ์์ํฉ๋๋ค.") | |
| client = genai.Client(api_key=api_key) | |
| progress_text = "AI ํ๊ฐ๊ฐ ๊ทธ๋ฆผ์ ๊ทธ๋ฆฌ๋ ์ค์ ๋๋ค..." | |
| my_bar = st.progress(0, text=progress_text) | |
| temp_results = [None] * len(scenes_text) | |
| # ์์ ์ฑ ์ํด max_workers=2 | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: | |
| future_to_idx = { | |
| executor.submit(logic_image.process_scene_task, i, {'text': text}, selected_style, custom_style_input, client, text_model_id, image_model_id, aspect_ratio, reference_image): i | |
| for i, text in enumerate(scenes_text) | |
| } | |
| completed = 0 | |
| for future in concurrent.futures.as_completed(future_to_idx): | |
| idx, prompt, img = future.result() | |
| temp_results[idx] = (prompt, img, scenes_text[idx]) | |
| completed += 1 | |
| my_bar.progress(completed / len(scenes_text), text=f"Scene {idx+1} ์๋ฃ!") | |
| st.session_state['scene_results'] = temp_results | |
| my_bar.empty() | |
| st.rerun() | |
| # ๊ฒฐ๊ณผ ํ์ | |
| if st.session_state['scene_results']: | |
| st.divider() | |
| st.subheader("๐ฌ Scene Results") | |
| for i, item in enumerate(st.session_state['scene_results']): | |
| if item is None: continue | |
| p_text, img_data, s_text = item | |
| with st.container(): | |
| c1, c2 = st.columns([1, 2]) | |
| with c1: | |
| if img_data: | |
| try: | |
| image = Image.open(io.BytesIO(img_data)) | |
| st.image(image, use_container_width=True) | |
| st.download_button( | |
| label="โฌ๏ธ ๋ค์ด๋ก๋", | |
| data=img_data, | |
| file_name=f"scene_{i+1}.png", | |
| mime="image/png", | |
| key=f"dl_btn_{i}", | |
| use_container_width=True | |
| ) | |
| except: st.error("์ด๋ฏธ์ง ์ค๋ฅ") | |
| else: st.warning("์ด๋ฏธ์ง ์์") | |
| with c2: | |
| st.markdown(f"**Scene {i+1}**") | |
| st.info(s_text) | |
| if st.button(f"๐ ์ฌ์์ฑ", key=f"regen_{i}"): | |
| if not api_key: st.error("API Key ํ์") | |
| else: | |
| client = genai.Client(api_key=api_key) | |
| with st.spinner("๋ค์ ๊ทธ๋ฆฌ๋ ์ค..."): | |
| _, new_p, new_img = logic_image.process_scene_task(i, {'text': s_text}, selected_style, custom_style_input, client, text_model_id, image_model_id, aspect_ratio, reference_image) | |
| st.session_state['scene_results'][i] = (new_p, new_img, s_text) | |
| st.rerun() | |
| st.divider() | |
| # ---------------------------------------------------------------- | |
| # [TAB 2] ์ ํ๋ธ ๊ธฐํ (SEO) | |
| # ---------------------------------------------------------------- | |
| with tab2: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown("#### ๐ ์ ํ๋ธ SEO ๋ถ์๊ธฐ") | |
| st.caption("๋๋ณธ์ ๊ธฐ๋ฐ์ผ๋ก ์ ๋ชฉ, ํ๊ทธ, ์ค๋ช ๋์ ์๋์ผ๋ก ์์ฑํฉ๋๋ค.") | |
| default_seo_script = st.session_state.get('shared_script', "") | |
| seo_script = st.text_area("๋ถ์ํ ๋๋ณธ ์ ๋ ฅ", value=default_seo_script, height=150, placeholder="SEO ๋ถ์์ ์ํ๋ ๋๋ณธ์ ์ ๋ ฅํ์ธ์.", key="seo_input") | |
| if st.button("โจ SEO ๊ธฐํ์ ์์ฑํ๊ธฐ", key="seo_btn", type="primary"): | |
| if not api_key: st.error("API Key ํ์") | |
| elif not seo_script: st.warning("๋๋ณธ ํ์") | |
| else: | |
| with st.spinner("Brain ๋ชจ๋ธ์ด ๋ถ์ ์ค์ ๋๋ค..."): | |
| client = genai.Client(api_key=api_key) | |
| seo_result = logic_seo.generate_seo_content(client, text_model_id, seo_script) | |
| st.session_state["seo_result"] = seo_result | |
| st.success("๋ถ์ ์๋ฃ!") | |
| c_seo1, c_seo2 = st.columns(2) | |
| with c_seo1: | |
| st.markdown("##### ๐ ์ถ์ฒ ์ ๋ชฉ") | |
| for t in seo_result['titles']: st.info(f"{t}") | |
| with c_seo2: | |
| st.markdown("##### ๐ท๏ธ ์ถ์ฒ ํ๊ทธ") | |
| st.code(", ".join(seo_result['tags'])) | |
| st.markdown("##### ๐ ์ค๋ช ๋ (Description)") | |
| st.text_area("์ค๋ช ๋ ๊ฒฐ๊ณผ", seo_result['description'], height=200) | |
| st.divider() | |
| st.subheader("๐ผ๏ธ ์ธ๋ค์ผ ์์ฑ") | |
| seo_cached = st.session_state.get("seo_result") | |
| if not seo_cached: | |
| st.info("SEO ๋ถ์์ ๋จผ์ ์คํํ๋ฉด ์ธ๋ค์ผ ์์ฑ ๋ฒํผ์ด ๋ํ๋ฉ๋๋ค.") | |
| else: | |
| strat_keys = list(THUMBNAIL_STRATEGIES.keys()) | |
| sel_strat = st.selectbox("์ธ๋ค์ผ ์ ๋ต ์ ํ", strat_keys, index=0) | |
| # ์ธ๋ค์ผ ํ ์คํธ(๋ฉ์ธ ํ์ดํ) ํ๋ณด: SEO ์ถ์ฒ ์ ๋ชฉ 1๊ฐ๋ฅผ ๊ธฐ๋ณธ๊ฐ์ผ๋ก | |
| default_thumb_text = seo_cached["titles"][0] if seo_cached.get("titles") else "" | |
| thumb_text = st.text_input("์ธ๋ค์ผ ๋ฉ์ธ ๋ฌธ๊ตฌ(์ํ๋ฉด ์์ )", value=default_thumb_text) | |
| if st.button("๐ง ์ธ๋ค์ผ ํ๋กฌํํธ ์์ฑ", key="thumb_prompt_btn"): | |
| if not api_key: | |
| st.error("API Key ํ์") | |
| else: | |
| client = genai.Client(api_key=api_key) | |
| strategy_block = THUMBNAIL_STRATEGIES[sel_strat] | |
| prompt = f""" | |
| ๋๋ ์ ํ๋ธ ์ธ๋ค์ผ ๊ธฐํ์๋ค. | |
| ์๋ '๋๋ณธ'๊ณผ '์ ๋ต'์ ์ฐธ๊ณ ํด์, ์ด๋ฏธ์ง ์์ฑ ๋ชจ๋ธ์ ๋ฃ์ '์ธ๋ค์ผ ํ๋กฌํํธ'๋ฅผ ๋ง๋ ๋ค. | |
| [๋๋ณธ] | |
| {seo_script[:8000]} | |
| [์ ๋ต] | |
| {strategy_block} | |
| [์ถ๊ฐ ์กฐ๊ฑด] | |
| - ์ธ๋ค์ผ ์ด๋ฏธ์ง ์์ ํ ์คํธ๋ฅผ ์ง์ ๊ทธ๋ ค ๋ฃ์ง ๋ง๋ผ(๊ธ์ ์์ฑ ๊ธ์ง). | |
| - ๋์ 'ํ ์คํธ๋ฅผ ๋ฃ์ ์๋ฆฌ'๋ฅผ ๊ตฌ๋๋ก ํ๋ณดํด๋ผ(์๋จ/ํ๋จ ์ฌ๋ฐฑ, ์์ ์์ญ). | |
| - ๊ตญ๊ฐ/๊ตญ๊ธฐ/๋ํต๋ น/์ฒญ์๋/๊ตญํ์์ฌ๋น ๋ฑ ํน์ ๊ตญ๊ฐ ์์ง์ด ์๋์ผ๋ก ๋์ค์ง ์๊ฒ, | |
| ์ ์น ์์ง๋ฌผ์ ์ถ์์ ์์ (์ค๋ฃจ์ฃ, ์กฐ๋ช , ๊ตฐ์ค, ๋ฌด๋)๋ก ์ฒ๋ฆฌํด๋ผ. | |
| - ํ๋ฉด๋น๋ {aspect_ratio}. | |
| - ์ถ๋ ฅ์ "ํ๋กฌํํธ ํ ์คํธ 1๊ฐ"๋ง. JSON/๋งํฌ๋ค์ด ๊ธ์ง. | |
| [์ฌ์ฉ์๊ฐ ๋ฃ์ ๋ฉ์ธ ๋ฌธ๊ตฌ(์ฐธ๊ณ ๋ง)] | |
| {thumb_text} | |
| """.strip() | |
| res = client.models.generate_content(model=text_model_id, contents=prompt) | |
| thumb_prompt = (getattr(res, "text", "") or "").strip() | |
| st.session_state["thumb_prompt"] = thumb_prompt | |
| st.success("์ธ๋ค์ผ ํ๋กฌํํธ ์์ฑ ์๋ฃ!") | |
| st.text_area("์์ฑ๋ ์ธ๋ค์ผ ํ๋กฌํํธ", value=thumb_prompt, height=180) | |
| # ํ๋กฌํํธ๊ฐ ์์ ๋๋ง ์ด๋ฏธ์ง ์์ฑ ๋ฒํผ ๋ ธ์ถ | |
| thumb_prompt_cached = st.session_state.get("thumb_prompt", "") | |
| if thumb_prompt_cached: | |
| if st.button("๐จ ์ธ๋ค์ผ ์ด๋ฏธ์ง 1์ฅ ์์ฑ", key="thumb_img_btn", type="primary"): | |
| if not api_key: | |
| st.error("API Key ํ์") | |
| else: | |
| client = genai.Client(api_key=api_key) | |
| # ์ด๋ฏธ์ง ์์ฑ (generate_images ์ฐ์ , ์์ผ๋ฉด generate_content fallback) | |
| img_bytes = None | |
| try: | |
| if hasattr(client.models, "generate_images"): | |
| img_res = client.models.generate_images( | |
| model=image_model_id, | |
| prompt=thumb_prompt_cached | |
| ) | |
| # logic_image์ ์๋ ์์ ์ถ์ถ ํจ์๊ฐ ์๋ค๋ฉด ๊ฐ๋จํ parts์์ ๋ฝ๊ธฐ | |
| try: | |
| cand = img_res.candidates[0] | |
| part = cand.content.parts[0] | |
| img_bytes = part.inline_data.data if part.inline_data else None | |
| except: | |
| img_bytes = None | |
| else: | |
| img_res = client.models.generate_content( | |
| model=image_model_id, | |
| contents=thumb_prompt_cached | |
| ) | |
| try: | |
| cand = img_res.candidates[0] | |
| part = cand.content.parts[0] | |
| img_bytes = part.inline_data.data if part.inline_data else None | |
| except: | |
| img_bytes = None | |
| except Exception as e: | |
| st.error(f"์ด๋ฏธ์ง ์์ฑ ์คํจ: {e}") | |
| if img_bytes: | |
| st.image(Image.open(io.BytesIO(img_bytes)), use_container_width=True) | |
| st.download_button( | |
| "โฌ๏ธ ์ธ๋ค์ผ ๋ค์ด๋ก๋", | |
| data=img_bytes, | |
| file_name="thumbnail.png", | |
| mime="image/png", | |
| use_container_width=True | |
| ) | |
| else: | |
| st.error("์ด๋ฏธ์ง ๋ฐ์ดํธ๋ฅผ ์ถ์ถํ์ง ๋ชปํ์ต๋๋ค. (๋ชจ๋ธ ์๋ต ๊ตฌ์กฐ ํ์ธ ํ์)") | |
| # ---------------------------------------------------------------- | |
| # [TAB 3] AI ์ฑ์ฐ (TTS) | |
| # ---------------------------------------------------------------- | |
| with tab3: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| st.markdown("#### ๐๏ธ AI ์ฑ์ฐ ์คํ๋์ค") | |
| st.caption("์ฅ๋ฉด ์์ฑ ์์ด ์ค๋์ค๋ง ํ์ํ ๋ ์ฌ์ฉํ์ธ์.") | |
| default_tts_script = st.session_state.get('shared_script', "") | |
| tts_script = st.text_area("๋ญ๋ ํ ๋๋ณธ ์ ๋ ฅ", value=default_tts_script, height=150, placeholder="์ฝ๊ณ ์ถ์ ํ ์คํธ๋ฅผ ์ ๋ ฅํ์ธ์.", key="tts_input") | |
| voice_map = { | |
| "Charon (๋จ์ฑ/๋คํ)": "Charon", "Puck (๋จ์ฑ/์พํ)": "Puck", | |
| "Kore (์ฌ์ฑ/์ฐจ๋ถ)": "Kore", "Fenrir (๋จ์ฑ/๊ฐํจ)": "Fenrir", | |
| "Aoede (์ฌ์ฑ/๋์)": "Aoede", "Orus (๋จ์ฑ/์คํ)": "Orus" | |
| } | |
| c_voice1, c_voice2 = st.columns([2, 1]) | |
| with c_voice1: | |
| v_sel = st.selectbox("์ฑ์ฐ ์ ํ", list(voice_map.keys()), key="tts_voice_sel") | |
| voice_opt = voice_map[v_sel] | |
| with c_voice2: | |
| if st.button("โถ ๋ฏธ๋ฆฌ๋ฃ๊ธฐ", key="tts_preview"): | |
| if not api_key: st.error("API Key ํ์") | |
| else: | |
| client = genai.Client(api_key=api_key) | |
| prev_audio = logic_tts.generate_speech_chunk(client, tts_model_id, "์๋ ํ์ธ์, ์ ๋ชฉ์๋ฆฌ์ ๋๋ค.", voice_opt) | |
| if isinstance(prev_audio, bytes): | |
| if not prev_audio.startswith(b'RIFF') and hasattr(logic_tts, 'raw_pcm_to_wav'): | |
| prev_audio = logic_tts.raw_pcm_to_wav(prev_audio) | |
| st.audio(prev_audio, format="audio/wav", autoplay=True) | |
| if st.button("๐๏ธ ์ ์ฒด ๋ น์ ๋ฐ ๋ณํฉ ์์", key="tts_full_btn", type="primary"): | |
| if not api_key: st.error("API Key ํ์") | |
| elif not tts_script: st.warning("๋๋ณธ ํ์") | |
| else: | |
| client = genai.Client(api_key=api_key) | |
| # [ํต์ฌ] TTS๋ ์ฌ๋ผ์ด๋ ๊ฐ(split_criteria)์ ๋ฌด์ํ๊ณ , ๋ฌด์กฐ๊ฑด 500์ ๋จ์๋ก ๊ณ ์ | |
| chunks = logic_tts.split_text_smartly(tts_script, limit=500) | |
| audio_res = [None] * len(chunks) | |
| with st.status("๋ น์ ์งํ ์ค...", expanded=True): | |
| with concurrent.futures.ThreadPoolExecutor() as executor: | |
| f_map = {executor.submit(logic_tts.process_tts_task, i, c, client, tts_model_id, voice_opt): i for i, c in enumerate(chunks)} | |
| for f in concurrent.futures.as_completed(f_map): | |
| idx, dat = f.result() | |
| if isinstance(dat, bytes): | |
| audio_res[idx] = dat | |
| st.write(f"โ Part {idx+1} ์๋ฃ") | |
| final_wav = logic_tts.merge_wav_bytes(audio_res) | |
| if final_wav: | |
| st.success("์ค๋์ค ์์ฑ ์๋ฃ!") | |
| st.audio(final_wav, format="audio/wav") | |
| st.download_button("๋ค์ด๋ก๋ (WAV)", final_wav, "full_audio.wav", "audio/wav") |