PLXR's picture
Update app.py
406d5a5 verified
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")