PLXR commited on
Commit
8ff9a44
ยท
1 Parent(s): 864a1c1

initial commit

Browse files
Files changed (9) hide show
  1. README.md +36 -0
  2. app.py +481 -0
  3. config_style.py +70 -0
  4. gitattributes +35 -0
  5. logic_image.py +218 -0
  6. logic_seo.py +80 -0
  7. logic_tts.py +100 -0
  8. requirements.txt +2 -0
  9. utils.py +23 -0
README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Nano Banana Studio
3
+ emoji: ๐ŸŒ
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: streamlit
7
+ sdk_version: 1.41.0
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+
13
+ # ๐ŸŒ Nano Banana Studio Pro
14
+
15
+ ์œ ํŠœ๋ฒ„๋ฅผ ์œ„ํ•œ ์˜ฌ์ธ์› AI ์ŠคํŠœ๋””์˜ค์ž…๋‹ˆ๋‹ค. ๊ตฌ๊ธ€ Gemini ๋ชจ๋ธ์„ ํ™œ์šฉํ•ด ์˜์ƒ ์žฅ๋ฉด์„ ์ƒ์„ฑํ•˜๊ณ , ์œ ํŠœ๋ธŒ SEO(์ œ๋ชฉ, ํƒœ๊ทธ, ์„ค๋ช…)๋ฅผ ๊ธฐํšํ•ฉ๋‹ˆ๋‹ค.
16
+
17
+ ## ๐Ÿ“‚ ํŒŒ์ผ ๊ตฌ์กฐ ์„ค๋ช… (Modular Structure)
18
+
19
+ ์ด ํ”„๋กœ์ ํŠธ๋Š” ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ชจ๋“ˆํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜์ •์ด ํ•„์š”ํ•  ๋•Œ ํ•ด๋‹น ํŒŒ์ผ์„ ์ฐธ๊ณ ํ•˜์„ธ์š”.
20
+
21
+ * **`app.py`**: ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค. Streamlit UI(ํ™”๋ฉด)์™€ ์ „์ฒด์ ์ธ ํ๋ฆ„์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
22
+ * **`config_style.py`**: **[์ˆ˜์ • ๊ถŒ์žฅ]** ํ”„๋กฌํ”„ํŠธ ์Šคํƒ€์ผ๊ณผ ์ธ๋„ค์ผ ์ „๋žต์ด ์ €์žฅ๋œ ๊ณณ์ž…๋‹ˆ๋‹ค. ๋กœ์ง ๊ฑด๋“œ๋ฆด ํ•„์š” ์—†์ด ์Šคํƒ€์ผ๋งŒ ์ถ”๊ฐ€ํ•  ๋•Œ ์ด๊ณณ์„ ์—ฝ๋‹ˆ๋‹ค.
23
+ * **`logic_image.py`**: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋กœ์ง(Brain & Painter)๊ณผ ์บ๋ฆญํ„ฐ ์ฐธ์กฐ ๊ธฐ๋Šฅ์ด ๋“ค์–ด์žˆ์Šต๋‹ˆ๋‹ค.
24
+ * **`logic_seo.py`**: ์œ ํŠœ๋ธŒ ๊ธฐํš(SEO) ๋ฐ ์ธ๋„ค์ผ ๊ธฐํš ๋กœ์ง์ด ๋“ค์–ด์žˆ์Šต๋‹ˆ๋‹ค.
25
+ * **`utils.py`**: ํŒŒ์ผ๋ช… ์ •๋ฆฌ, ํ…์ŠคํŠธ ์ž๋ฅด๊ธฐ ๋“ฑ ๋‹จ์ˆœ ๋„๊ตฌ ํ•จ์ˆ˜๋“ค์ด ๋ชจ์—ฌ์žˆ์Šต๋‹ˆ๋‹ค.
26
+
27
+ ## ๐Ÿš€ ์ฃผ์š” ๊ธฐ๋Šฅ
28
+ 1. **AI ์žฅ๋ฉด ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ)**: ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 5์žฅ์˜ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ์ƒ์„ฑํ•˜์—ฌ ์†๋„๊ฐ€ ๋น ๋ฆ…๋‹ˆ๋‹ค.
29
+ 2. **์บ๋ฆญํ„ฐ ์ฐธ์กฐ (Character Consistency)**: ์บ๋ฆญํ„ฐ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๋ฉด ์ผ๊ด€๋œ ์บ๋ฆญํ„ฐ๊ฐ€ ๋“ฑ์žฅํ•˜๋„๋ก ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.
30
+ 3. **์œ ํŠœ๋ธŒ SEO ๊ธฐํš**: ๋Œ€๋ณธ์„ ๋ถ„์„ํ•ด ํด๋ฆญ๋ฅ  ๋†’์€ ์ œ๋ชฉ, ์„ค๋ช…๋ž€, ํƒœ๊ทธ๋ฅผ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.
31
+
32
+ ## ๐Ÿ› ๏ธ ์„ค์น˜ ๋ฐ ์‹คํ–‰ (๋กœ์ปฌ)
33
+
34
+ ```bash
35
+ pip install -r requirements.txt
36
+ streamlit run app.py
app.py ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import io
4
+ import concurrent.futures
5
+ from PIL import Image
6
+ from google import genai
7
+
8
+ # ๋กœ์ง ๋ชจ๋“ˆ ์ž„ํฌํŠธ
9
+ import logic_image
10
+ import logic_seo
11
+ import logic_tts
12
+ from config_style import STYLE_DEFINITIONS, THUMBNAIL_STRATEGIES
13
+
14
+ # ==========================================
15
+ # 1. ํŽ˜์ด์ง€ ์„ค์ • ๋ฐ CSS ๋””์ž์ธ
16
+ # ==========================================
17
+ st.set_page_config(
18
+ page_title="Nano Banana Studio Pro",
19
+ page_icon="๐ŸŒ",
20
+ layout="wide",
21
+ initial_sidebar_state="expanded"
22
+ )
23
+
24
+ # ์„ธ์…˜ ์ดˆ๊ธฐํ™”
25
+ if 'scene_results' not in st.session_state:
26
+ st.session_state['scene_results'] = []
27
+ if 'final_audio' not in st.session_state:
28
+ st.session_state['final_audio'] = None
29
+ if 'shared_script' not in st.session_state:
30
+ st.session_state['shared_script'] = ""
31
+
32
+ # [CSS] ๋‹คํฌ๋ชจ๋“œ + ์˜ค๋ Œ์ง€ ํฌ์ธํŠธ ๋””์ž์ธ
33
+ st.markdown("""
34
+ <style>
35
+ @import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
36
+
37
+ html, body, [class*="css"] {
38
+ font-family: 'Pretendard', sans-serif;
39
+ }
40
+ .stApp {
41
+ background-color: #121212;
42
+ color: #E0E0E0;
43
+ }
44
+
45
+ /* ์‚ฌ์ด๋“œ๋ฐ” */
46
+ section[data-testid="stSidebar"] {
47
+ background-color: #0A0A0A;
48
+ border-right: 1px solid #333;
49
+ }
50
+
51
+ /* ์ž…๋ ฅ์ฐฝ ๊ฐ€๋…์„ฑ ๊ฐœ์„  */
52
+ .stTextInput input, .stTextArea textarea, .stSelectbox div[data-baseweb="select"] > div {
53
+ background-color: #252525 !important;
54
+ color: #FFFFFF !important;
55
+ border: 1px solid #404040 !important;
56
+ border-radius: 8px !important;
57
+ }
58
+ .stTextArea textarea[disabled] {
59
+ background-color: #1E1E1E !important;
60
+ color: #AAAAAA !important;
61
+ border: 1px solid #333 !important;
62
+ }
63
+ ::placeholder {
64
+ color: #AAAAAA !important;
65
+ opacity: 1;
66
+ }
67
+
68
+ /* ํƒญ ๋””์ž์ธ */
69
+ button[data-baseweb="tab"] {
70
+ font-size: 16px;
71
+ font-weight: 700;
72
+ color: #888;
73
+ }
74
+ button[data-baseweb="tab"][aria-selected="true"] {
75
+ color: #FFC107 !important;
76
+ background-color: transparent !important;
77
+ border-bottom: 2px solid #FFC107 !important;
78
+ }
79
+
80
+ /* ๋ฒ„ํŠผ ๋””์ž์ธ */
81
+ div.stButton > button[kind="primary"] {
82
+ background: linear-gradient(135deg, #FFC107 0%, #FF9800 100%);
83
+ color: #000000;
84
+ border: none;
85
+ padding: 14px 24px;
86
+ font-size: 16px;
87
+ font-weight: 800;
88
+ border-radius: 8px;
89
+ transition: all 0.3s ease;
90
+ }
91
+ div.stButton > button[kind="primary"]:hover {
92
+ transform: translateY(-2px);
93
+ box-shadow: 0 4px 15px rgba(255, 193, 7, 0.4);
94
+ }
95
+ div.stButton > button[kind="secondary"] {
96
+ border-color: #555;
97
+ color: #ddd;
98
+ }
99
+
100
+ h1, h2, h3, h4, h5 { color: #FFFFFF !important; }
101
+ .highlight { color: #FFC107; }
102
+
103
+ div[data-testid="stExpander"] {
104
+ background-color: #1E1E1E;
105
+ border: 1px solid #333;
106
+ border-radius: 8px;
107
+ }
108
+ div[data-testid="stFileUploader"] section {
109
+ background-color: #1E1E1E;
110
+ border: 2px dashed #444;
111
+ }
112
+ </style>
113
+ """, unsafe_allow_html=True)
114
+
115
+ # ==========================================
116
+ # 2. ์‚ฌ์ด๋“œ๋ฐ” (ํ†ตํ•ฉ ์„ค์ •)
117
+ # ==========================================
118
+ with st.sidebar:
119
+ st.markdown("## โš™๏ธ ๊ธฐ๋ณธ ์„ค์ • (Basic)")
120
+ api_key = st.text_input("Google API Key", type="password", placeholder="API ํ‚ค๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...", label_visibility="collapsed")
121
+ st.divider()
122
+
123
+ st.markdown("### ๐ŸŽจ Model Engine")
124
+ model_choice = st.radio("์‚ฌ์šฉํ•  ๋ชจ๋ธ", ["โšก Fast (Gemini 2.0)", "๐Ÿš€ Pro (Imagen 3)"], label_visibility="collapsed")
125
+
126
+ if "Fast" in model_choice:
127
+ image_model_id = "gemini-2.0-flash"
128
+ text_model_id = "gemini-2.0-flash"
129
+ else:
130
+ # [๋ณต๊ตฌ] ์‚ฌ์šฉ์ž๋‹˜์ด ์›ํ•˜์‹œ๋˜ ๊ทธ ๋ชจ๋ธ ID!
131
+ image_model_id = "gemini-3-pro-image-preview"
132
+ text_model_id = "gemini-3-pro-preview"
133
+
134
+ st.divider()
135
+
136
+ st.markdown("### ๐Ÿ“ Canvas Ratio")
137
+ ar_radio = st.radio("๋น„์œจ", ["16:9", "9:16"], label_visibility="collapsed", horizontal=True)
138
+ aspect_ratio = ar_radio
139
+
140
+ st.divider()
141
+
142
+ st.markdown("### ๐Ÿ› ๏ธ ๊ณ ๊ธ‰ ์„ค์ • (TTS & Split)")
143
+
144
+ st.caption("TTS Model ID")
145
+ tts_model_id = st.text_input("TTS ID", value="gemini-2.5-pro-preview-tts", label_visibility="collapsed")
146
+
147
+ st.markdown("<br>", unsafe_allow_html=True)
148
+
149
+ st.caption("Image Scene Split (์ด๋ฏธ์ง€ ์ƒ์„ฑ์šฉ)")
150
+ duration_per_scene = st.slider("์žฅ๋ฉด๋‹น ์‹œ๊ฐ„(์ดˆ)", 3, 30, 5)
151
+ split_criteria = duration_per_scene * 8
152
+ st.info(f"๐Ÿ’ก {duration_per_scene}์ดˆ (์•ฝ {split_criteria}์ž) ๋‹จ์œ„๋กœ ์žฅ๋ฉด์„ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.")
153
+ st.caption("โ€ป TTS๋Š” ์„ค์ •๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ 500์ž ๋‹จ์œ„๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.")
154
+
155
+ st.divider()
156
+
157
+ st.markdown("## ๐ŸŽจ ์Šคํƒ€์ผ ๋ฐ ์บ๋ฆญํ„ฐ ์„ค์ •")
158
+ st.caption("์˜์ƒ ์ „์ฒด์˜ ๋ถ„์œ„๊ธฐ์™€ ์บ๋ฆญํ„ฐ๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.")
159
+ st.markdown("<br>", unsafe_allow_html=True)
160
+ st.markdown("#### ๐Ÿ–Œ๏ธ ํ™”ํ’(Style) ์„ ํƒ")
161
+
162
+ style_options = list(STYLE_DEFINITIONS.keys()) + ["์ง์ ‘ ์ž…๋ ฅ"]
163
+ selected_style = st.selectbox("์Šคํƒ€์ผ ์„ ํƒ", style_options, label_visibility="collapsed")
164
+
165
+ custom_style_input = ""
166
+ current_prompt = ""
167
+
168
+ if selected_style == "์ง์ ‘ ์ž…๋ ฅ":
169
+ custom_style_input = st.text_input("์Šคํƒ€์ผ ํ”„๋กฌํ”„ํŠธ", placeholder="์˜ˆ: ์ง€๋ธŒ๋ฆฌ ์Šคํƒ€์ผ, ์ˆ˜์ฑ„ํ™”ํ’")
170
+ current_prompt = custom_style_input if custom_style_input else "(ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”)"
171
+ else:
172
+ style_value = STYLE_DEFINITIONS[selected_style]
173
+ if isinstance(style_value, dict):
174
+ current_prompt = style_value.get('prompt', str(style_value))
175
+ else:
176
+ current_prompt = str(style_value)
177
+
178
+ st.markdown("#### ๐Ÿ“œ ์ ์šฉ๋  ํ”„๋กฌํ”„ํŠธ (๋ฏธ๋ฆฌ๋ณด๊ธฐ)")
179
+ st.text_area("ํ”„๋กฌํ”„ํŠธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", value=current_prompt, height=150, disabled=True, label_visibility="collapsed")
180
+
181
+ st.divider()
182
+
183
+ st.markdown("#### ๐Ÿ‘ค ์บ๋ฆญํ„ฐ ์ฐธ์กฐ (Reference)")
184
+ uploaded_file = st.file_uploader("์ด๋ฏธ์ง€ ์—…๋กœ๋“œ", type=["png", "jpg", "jpeg"], label_visibility="collapsed")
185
+ reference_image = None
186
+ if uploaded_file:
187
+ reference_image = Image.open(uploaded_file)
188
+ st.image(reference_image, caption="์ฐธ์กฐ ์ด๋ฏธ์ง€", use_container_width=True)
189
+
190
+
191
+ # ==========================================
192
+ # 3. ๋ฉ”์ธ ์ฝ˜ํ…์ธ 
193
+ # ==========================================
194
+
195
+ st.title("Nano Banana <span class='highlight'>Studio Pro</span>", anchor=False)
196
+ st.markdown("<div style='margin-top: -15px; color: #888; margin-bottom: 20px;'>AI Powered All-in-One Workspace</div>", unsafe_allow_html=True)
197
+
198
+ tab1, tab2, tab3 = st.tabs(["๐ŸŽฌ ์žฅ๋ฉด ์ƒ์„ฑ (Scenes)", "๐Ÿ“ˆ ๊ธฐํš/SEO (Analysis)", "๐ŸŽ™๏ธ ์„ฑ์šฐ/TTS (Voice)"])
199
+
200
+ # ----------------------------------------------------------------
201
+ # [TAB 1] ์žฅ๋ฉด/์ด๋ฏธ์ง€ ์ƒ์„ฑ (๋ฉ”์ธ ๊ธฐ๋Šฅ)
202
+ # ----------------------------------------------------------------
203
+ with tab1:
204
+ st.markdown("<br>", unsafe_allow_html=True)
205
+
206
+ st.markdown("#### โถ ๋Œ€๋ณธ ์ž…๋ ฅ (Script)")
207
+ script_input = st.text_area(
208
+ "๋Œ€๋ณธ",
209
+ height=300,
210
+ placeholder="๋Œ€๋ณธ์„ ์ค„๊ธ€๋กœ ๋ถ™์—ฌ๋„ฃ์œผ์„ธ์š”. ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ์„ค์ •ํ•œ ์‹œ๊ฐ„(์ดˆ) ๋‹จ์œ„๋กœ AI๊ฐ€ ์žฅ๋ฉด์„ ์ž๋™์œผ๋กœ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.",
211
+ label_visibility="collapsed",
212
+ key="scene_script_input"
213
+ )
214
+
215
+ if script_input:
216
+ st.session_state['shared_script'] = script_input
217
+
218
+ st.markdown("<br>", unsafe_allow_html=True)
219
+
220
+ # ์ƒ์„ฑ ๋ฒ„ํŠผ
221
+ if st.button("๐Ÿš€ ์žฅ๋ฉด ์ƒ์„ฑ ์‹œ์ž‘ (Generate Scenes)", type="primary", use_container_width=True):
222
+ if not api_key: st.error("โš ๏ธ ์‚ฌ์ด๋“œ๋ฐ”์— API Key๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
223
+ elif not script_input: st.warning("โš ๏ธ ๋Œ€๋ณธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
224
+ else:
225
+ # [ํ•ต์‹ฌ] ์ด๋ฏธ์ง€ ์ƒ์„ฑ์€ ์Šฌ๋ผ์ด๋” ๊ฐ’(split_criteria)์„ ์‚ฌ์šฉ
226
+ raw_scenes = logic_tts.split_text_smartly(script_input, limit=split_criteria)
227
+ scenes_text = raw_scenes[:10]
228
+
229
+ st.toast(f"๐Ÿ“œ {len(scenes_text)}๊ฐœ ์žฅ๋ฉด์œผ๋กœ ๋ถ„ํ• ํ•˜์—ฌ ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
230
+
231
+ client = genai.Client(api_key=api_key)
232
+ progress_text = "AI ํ™”๊ฐ€๊ฐ€ ๊ทธ๋ฆผ์„ ๊ทธ๋ฆฌ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค..."
233
+ my_bar = st.progress(0, text=progress_text)
234
+
235
+ temp_results = [None] * len(scenes_text)
236
+
237
+ # ์•ˆ์ •์„ฑ ์œ„ํ•ด max_workers=2
238
+ with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
239
+ future_to_idx = {
240
+ 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
241
+ for i, text in enumerate(scenes_text)
242
+ }
243
+ completed = 0
244
+ for future in concurrent.futures.as_completed(future_to_idx):
245
+ idx, prompt, img = future.result()
246
+ temp_results[idx] = (prompt, img, scenes_text[idx])
247
+ completed += 1
248
+ my_bar.progress(completed / len(scenes_text), text=f"Scene {idx+1} ์™„๋ฃŒ!")
249
+
250
+ st.session_state['scene_results'] = temp_results
251
+ my_bar.empty()
252
+ st.rerun()
253
+
254
+ # ๊ฒฐ๊ณผ ํ‘œ์‹œ
255
+ if st.session_state['scene_results']:
256
+ st.divider()
257
+ st.subheader("๐ŸŽฌ Scene Results")
258
+
259
+ for i, item in enumerate(st.session_state['scene_results']):
260
+ if item is None: continue
261
+ p_text, img_data, s_text = item
262
+
263
+ with st.container():
264
+ c1, c2 = st.columns([1, 2])
265
+ with c1:
266
+ if img_data:
267
+ try:
268
+ image = Image.open(io.BytesIO(img_data))
269
+ st.image(image, use_container_width=True)
270
+ st.download_button(
271
+ label="โฌ‡๏ธ ๋‹ค์šด๋กœ๋“œ",
272
+ data=img_data,
273
+ file_name=f"scene_{i+1}.png",
274
+ mime="image/png",
275
+ key=f"dl_btn_{i}",
276
+ use_container_width=True
277
+ )
278
+ except: st.error("์ด๋ฏธ์ง€ ์˜ค๋ฅ˜")
279
+ else: st.warning("์ด๋ฏธ์ง€ ์—†์Œ")
280
+ with c2:
281
+ st.markdown(f"**Scene {i+1}**")
282
+ st.info(s_text)
283
+ if st.button(f"๐Ÿ”„ ์žฌ์ƒ์„ฑ", key=f"regen_{i}"):
284
+ if not api_key: st.error("API Key ํ•„์š”")
285
+ else:
286
+ client = genai.Client(api_key=api_key)
287
+ with st.spinner("๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ์ค‘..."):
288
+ _, 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)
289
+ st.session_state['scene_results'][i] = (new_p, new_img, s_text)
290
+ st.rerun()
291
+ st.divider()
292
+
293
+ # ----------------------------------------------------------------
294
+ # [TAB 2] ์œ ํŠœ๋ธŒ ๊ธฐํš (SEO)
295
+ # ----------------------------------------------------------------
296
+ with tab2:
297
+ st.markdown("<br>", unsafe_allow_html=True)
298
+ st.markdown("#### ๐Ÿ“ˆ ์œ ํŠœ๋ธŒ SEO ๋ถ„์„๊ธฐ")
299
+ st.caption("๋Œ€๋ณธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ œ๋ชฉ, ํƒœ๊ทธ, ์„ค๋ช…๋ž€์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")
300
+
301
+ default_seo_script = st.session_state.get('shared_script', "")
302
+ seo_script = st.text_area("๋ถ„์„ํ•  ๋Œ€๋ณธ ์ž…๋ ฅ", value=default_seo_script, height=150, placeholder="SEO ๋ถ„์„์„ ์›ํ•˜๋Š” ๋Œ€๋ณธ์„ ์ž…๋ ฅํ•˜์„ธ์š”.", key="seo_input")
303
+
304
+ if st.button("โœจ SEO ๊ธฐํš์„œ ์ƒ์„ฑํ•˜๊ธฐ", key="seo_btn", type="primary"):
305
+ if not api_key: st.error("API Key ํ•„์š”")
306
+ elif not seo_script: st.warning("๋Œ€๋ณธ ํ•„์š”")
307
+ else:
308
+ with st.spinner("Brain ๋ชจ๋ธ์ด ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค..."):
309
+ client = genai.Client(api_key=api_key)
310
+ seo_result = logic_seo.generate_seo_content(client, text_model_id, seo_script)
311
+
312
+ st.session_state["seo_result"] = seo_result
313
+ st.success("๋ถ„์„ ์™„๋ฃŒ!")
314
+
315
+ c_seo1, c_seo2 = st.columns(2)
316
+ with c_seo1:
317
+ st.markdown("##### ๐Ÿ“Œ ์ถ”์ฒœ ์ œ๋ชฉ")
318
+ for t in seo_result['titles']: st.info(f"{t}")
319
+ with c_seo2:
320
+ st.markdown("##### ๐Ÿท๏ธ ์ถ”์ฒœ ํƒœ๊ทธ")
321
+ st.code(", ".join(seo_result['tags']))
322
+
323
+ st.markdown("##### ๐Ÿ“ ์„ค๋ช…๋ž€ (Description)")
324
+ st.text_area("์„ค๋ช…๋ž€ ๊ฒฐ๊ณผ", seo_result['description'], height=200)
325
+ st.divider()
326
+ st.subheader("๐Ÿ–ผ๏ธ ์ธ๋„ค์ผ ์ƒ์„ฑ")
327
+
328
+ seo_cached = st.session_state.get("seo_result")
329
+
330
+ if not seo_cached:
331
+ st.info("SEO ๋ถ„์„์„ ๋จผ์ € ์‹คํ–‰ํ•˜๋ฉด ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฒ„ํŠผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.")
332
+ else:
333
+ strat_keys = list(THUMBNAIL_STRATEGIES.keys())
334
+ sel_strat = st.selectbox("์ธ๋„ค์ผ ์ „๋žต ์„ ํƒ", strat_keys, index=0)
335
+
336
+ # ์ธ๋„ค์ผ ํ…์ŠคํŠธ(๋ฉ”์ธ ํƒ€์ดํ‹€) ํ›„๋ณด: SEO ์ถ”์ฒœ ์ œ๋ชฉ 1๊ฐœ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ
337
+ default_thumb_text = seo_cached["titles"][0] if seo_cached.get("titles") else ""
338
+ thumb_text = st.text_input("์ธ๋„ค์ผ ๋ฉ”์ธ ๋ฌธ๊ตฌ(์›ํ•˜๋ฉด ์ˆ˜์ •)", value=default_thumb_text)
339
+
340
+ if st.button("๐Ÿง  ์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ", key="thumb_prompt_btn"):
341
+ if not api_key:
342
+ st.error("API Key ํ•„์š”")
343
+ else:
344
+ client = genai.Client(api_key=api_key)
345
+
346
+ strategy_block = THUMBNAIL_STRATEGIES[sel_strat]
347
+
348
+ prompt = f"""
349
+ ๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ ๊ธฐํš์ž๋‹ค.
350
+ ์•„๋ž˜ '๋Œ€๋ณธ'๊ณผ '์ „๋žต'์„ ์ฐธ๊ณ ํ•ด์„œ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ชจ๋ธ์— ๋„ฃ์„ '์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ'๋ฅผ ๋งŒ๋“ ๋‹ค.
351
+
352
+ [๋Œ€๋ณธ]
353
+ {seo_script[:8000]}
354
+
355
+ [์ „๋žต]
356
+ {strategy_block}
357
+
358
+ [์ถ”๊ฐ€ ์กฐ๊ฑด]
359
+ - ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์•ˆ์— ํ…์ŠคํŠธ๋ฅผ ์ง์ ‘ ๊ทธ๋ ค ๋„ฃ์ง€ ๋งˆ๋ผ(๊ธ€์ž ์ƒ์„ฑ ๊ธˆ์ง€).
360
+ - ๋Œ€์‹  'ํ…์ŠคํŠธ๋ฅผ ๋„ฃ์„ ์ž๋ฆฌ'๋ฅผ ๊ตฌ๋„๋กœ ํ™•๋ณดํ•ด๋ผ(์ƒ๋‹จ/ํ•˜๋‹จ ์—ฌ๋ฐฑ, ์•ˆ์ „์˜์—ญ).
361
+ - ๊ตญ๊ฐ€/๊ตญ๊ธฐ/๋Œ€ํ†ต๋ น/์ฒญ์™€๋Œ€/๊ตญํšŒ์˜์‚ฌ๋‹น ๋“ฑ ํŠน์ • ๊ตญ๊ฐ€ ์ƒ์ง•์ด ์ž๋™์œผ๋กœ ๋‚˜์˜ค์ง€ ์•Š๊ฒŒ,
362
+ ์ •์น˜ ์ƒ์ง•๋ฌผ์€ ์ถ”์ƒ์  ์€์œ (์‹ค๋ฃจ์—ฃ, ์กฐ๋ช…, ๊ตฐ์ค‘, ๋ฌด๋Œ€)๋กœ ์ฒ˜๋ฆฌํ•ด๋ผ.
363
+ - ํ™”๋ฉด๋น„๋Š” {aspect_ratio}.
364
+ - ์ถœ๋ ฅ์€ "ํ”„๋กฌํ”„ํŠธ ํ…์ŠคํŠธ 1๊ฐœ"๋งŒ. JSON/๋งˆํฌ๋‹ค์šด ๊ธˆ์ง€.
365
+
366
+ [์‚ฌ์šฉ์ž๊ฐ€ ๋„ฃ์„ ๋ฉ”์ธ ๋ฌธ๊ตฌ(์ฐธ๊ณ ๋งŒ)]
367
+ {thumb_text}
368
+ """.strip()
369
+
370
+ res = client.models.generate_content(model=text_model_id, contents=prompt)
371
+ thumb_prompt = (getattr(res, "text", "") or "").strip()
372
+ st.session_state["thumb_prompt"] = thumb_prompt
373
+ st.success("์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์™„๋ฃŒ!")
374
+ st.text_area("์ƒ์„ฑ๋œ ์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ", value=thumb_prompt, height=180)
375
+
376
+ # ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฒ„ํŠผ ๋…ธ์ถœ
377
+ thumb_prompt_cached = st.session_state.get("thumb_prompt", "")
378
+ if thumb_prompt_cached:
379
+ if st.button("๐ŸŽจ ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ 1์žฅ ์ƒ์„ฑ", key="thumb_img_btn", type="primary"):
380
+ if not api_key:
381
+ st.error("API Key ํ•„์š”")
382
+ else:
383
+ client = genai.Client(api_key=api_key)
384
+
385
+ # ์ด๋ฏธ์ง€ ์ƒ์„ฑ (generate_images ์šฐ์„ , ์—†์œผ๋ฉด generate_content fallback)
386
+ img_bytes = None
387
+ try:
388
+ if hasattr(client.models, "generate_images"):
389
+ img_res = client.models.generate_images(
390
+ model=image_model_id,
391
+ prompt=thumb_prompt_cached
392
+ )
393
+ # logic_image์— ์žˆ๋Š” ์•ˆ์ „ ์ถ”์ถœ ํ•จ์ˆ˜๊ฐ€ ์—†๋‹ค๋ฉด ๊ฐ„๋‹จํžˆ parts์—์„œ ๋ฝ‘๊ธฐ
394
+ try:
395
+ cand = img_res.candidates[0]
396
+ part = cand.content.parts[0]
397
+ img_bytes = part.inline_data.data if part.inline_data else None
398
+ except:
399
+ img_bytes = None
400
+ else:
401
+ img_res = client.models.generate_content(
402
+ model=image_model_id,
403
+ contents=thumb_prompt_cached
404
+ )
405
+ try:
406
+ cand = img_res.candidates[0]
407
+ part = cand.content.parts[0]
408
+ img_bytes = part.inline_data.data if part.inline_data else None
409
+ except:
410
+ img_bytes = None
411
+ except Exception as e:
412
+ st.error(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: {e}")
413
+
414
+ if img_bytes:
415
+ st.image(Image.open(io.BytesIO(img_bytes)), use_container_width=True)
416
+ st.download_button(
417
+ "โฌ‡๏ธ ์ธ๋„ค์ผ ๋‹ค์šด๋กœ๋“œ",
418
+ data=img_bytes,
419
+ file_name="thumbnail.png",
420
+ mime="image/png",
421
+ use_container_width=True
422
+ )
423
+ else:
424
+ st.error("์ด๋ฏธ์ง€ ๋ฐ”์ดํŠธ๋ฅผ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. (๋ชจ๋ธ ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ ํ•„์š”)")
425
+
426
+ # ----------------------------------------------------------------
427
+ # [TAB 3] AI ์„ฑ์šฐ (TTS)
428
+ # ----------------------------------------------------------------
429
+ with tab3:
430
+ st.markdown("<br>", unsafe_allow_html=True)
431
+ st.markdown("#### ๐ŸŽ™๏ธ AI ์„ฑ์šฐ ์ŠคํŠœ๋””์˜ค")
432
+ st.caption("์žฅ๋ฉด ์ƒ์„ฑ ์—†์ด ์˜ค๋””์˜ค๋งŒ ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•˜์„ธ์š”.")
433
+
434
+ default_tts_script = st.session_state.get('shared_script', "")
435
+ tts_script = st.text_area("๋‚ญ๋…ํ•  ๋Œ€๋ณธ ์ž…๋ ฅ", value=default_tts_script, height=150, placeholder="์ฝ๊ณ  ์‹ถ์€ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.", key="tts_input")
436
+
437
+ voice_map = {
438
+ "Charon (๋‚จ์„ฑ/๋‹คํ)": "Charon", "Puck (๋‚จ์„ฑ/์พŒํ™œ)": "Puck",
439
+ "Kore (์—ฌ์„ฑ/์ฐจ๋ถ„)": "Kore", "Fenrir (๋‚จ์„ฑ/๊ฐ•ํ•จ)": "Fenrir",
440
+ "Aoede (์—ฌ์„ฑ/๋†’์Œ)": "Aoede", "Orus (๋‚จ์„ฑ/์ค‘ํ›„)": "Orus"
441
+ }
442
+
443
+ c_voice1, c_voice2 = st.columns([2, 1])
444
+ with c_voice1:
445
+ v_sel = st.selectbox("์„ฑ์šฐ ์„ ํƒ", list(voice_map.keys()), key="tts_voice_sel")
446
+ voice_opt = voice_map[v_sel]
447
+ with c_voice2:
448
+ if st.button("โ–ถ ๋ฏธ๋ฆฌ๋“ฃ๊ธฐ", key="tts_preview"):
449
+ if not api_key: st.error("API Key ํ•„์š”")
450
+ else:
451
+ client = genai.Client(api_key=api_key)
452
+ prev_audio = logic_tts.generate_speech_chunk(client, tts_model_id, "์•ˆ๋…•ํ•˜์„ธ์š”, ์ œ ๋ชฉ์†Œ๋ฆฌ์ž…๋‹ˆ๋‹ค.", voice_opt)
453
+ if isinstance(prev_audio, bytes):
454
+ if not prev_audio.startswith(b'RIFF') and hasattr(logic_tts, 'raw_pcm_to_wav'):
455
+ prev_audio = logic_tts.raw_pcm_to_wav(prev_audio)
456
+ st.audio(prev_audio, format="audio/wav", autoplay=True)
457
+
458
+ if st.button("๐ŸŽ™๏ธ ์ „์ฒด ๋…น์Œ ๋ฐ ๋ณ‘ํ•ฉ ์‹œ์ž‘", key="tts_full_btn", type="primary"):
459
+ if not api_key: st.error("API Key ํ•„์š”")
460
+ elif not tts_script: st.warning("๋Œ€๋ณธ ํ•„์š”")
461
+ else:
462
+ client = genai.Client(api_key=api_key)
463
+
464
+ # [ํ•ต์‹ฌ] TTS๋Š” ์Šฌ๋ผ์ด๋” ๊ฐ’(split_criteria)์„ ๋ฌด์‹œํ•˜๊ณ , ๋ฌด์กฐ๊ฑด 500์ž ๋‹จ์œ„๋กœ ๊ณ ์ •
465
+ chunks = logic_tts.split_text_smartly(tts_script, limit=500)
466
+
467
+ audio_res = [None] * len(chunks)
468
+ with st.status("๋…น์Œ ์ง„ํ–‰ ์ค‘...", expanded=True):
469
+ with concurrent.futures.ThreadPoolExecutor() as executor:
470
+ f_map = {executor.submit(logic_tts.process_tts_task, i, c, client, tts_model_id, voice_opt): i for i, c in enumerate(chunks)}
471
+ for f in concurrent.futures.as_completed(f_map):
472
+ idx, dat = f.result()
473
+ if isinstance(dat, bytes):
474
+ audio_res[idx] = dat
475
+ st.write(f"โœ… Part {idx+1} ์™„๋ฃŒ")
476
+
477
+ final_wav = logic_tts.merge_wav_bytes(audio_res)
478
+ if final_wav:
479
+ st.success("์˜ค๋””์˜ค ์ƒ์„ฑ ์™„๋ฃŒ!")
480
+ st.audio(final_wav, format="audio/wav")
481
+ st.download_button("๋‹ค์šด๋กœ๋“œ (WAV)", final_wav, "full_audio.wav", "audio/wav")
config_style.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================
2
+ # Nano Banana Studio - Style & Prompt Configuration
3
+ # ==============================================================
4
+
5
+ # 1. ์˜์ƒ/์ด๋ฏธ์ง€ ์ƒ์„ฑ ์Šคํƒ€์ผ ์ •์˜
6
+ # logic_image.py ์—์„œ ์ด ๋”•์…”๋„ˆ๋ฆฌ๋ฅผ ์ฐธ์กฐํ•˜์—ฌ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
7
+ STYLE_DEFINITIONS = {
8
+ "๋ฐ์€ ์ •๋ณด/์ด์Šˆ": "๋Œ€์‚ฌ์— ์–ด์šธ๋ฆฌ๋Š” 2d ์–ผ๊ตด์ด ๋‘ฅ๊ทผ ํ•˜์–€์ƒ‰ ์Šคํ‹ฑ๋งจ ์—ฐ์ถœ๋กœ ์„ค๋ช…๊ณผ ์ดํ•ด๊ฐ€ ์ž˜๋˜๋Š” ๋А๋‚Œ์œผ๋กœ ๊ทธ๋ ค์ค˜ ์ƒํ™ฉ์„ ์ž˜ ๋‚˜ํƒ€๋‚ด๊ฒŒ ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ๋ง๊ณ  ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ๋„ˆ๋ฌด ์–ด์ง€๋Ÿฝ์ง€ ์•Š๊ฒŒ, ๊ธ€์”จ๋Š” ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ 2~3๋งŒ ๋‚˜์˜ค๊ฒŒ ํ•œ๋‹ค. ๊ธ€์”จ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์ง€ ์•Š๊ฒŒ ํ•ต์‹ฌ๋งŒ. 2D ์Šคํ‹ฑ๋งจ์„ ํ™œ์šฉํ•ด ๋Œ€๋ณธ์„ ์„ค๋ช…์ด ์ž˜๋˜๊ฒŒ ์„ค๋ช…ํ•˜๋Š” ์—ฐ์ถœ์„ ํ•œ๋‹ค. ์ž๋ง‰ ์Šคํƒ€์ผ ์—ฐ์ถœ์€ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ธ€์”จ๊ฐ€ ๋‚˜์˜ฌ๊ฒฝ์šฐ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ ์ค‘์‹ฌ์œผ๋กœ๋งŒ ๋‚˜์˜ค๊ฒŒ ๋„ˆ๋ฌด ๊ธ€์ด ๋งŽ์ง€ ์•Š๋„๋ก ํ•œ๋‹ค, ๊ธ€์ž๋Š” ๋ฐฐ๊ฒฝ๊ณผ ์‚ฌ๋ฌผ์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์—ฐ์ถœ, ์ „์ฒด ๋ฐฐ๊ฒฝ ์—ฐ์ถœ์€ 2D๋กœ ํ•˜๊ณ  ๋””ํ…Œ์ผํ•˜๊ฒŒ ์ž…์ฒด์ ์ด๊ณ  ๋ชฐ์ž…๊ฐ ์žˆ๊ฒŒ ์—ฐ์ถœํ•ด์„œ ๊ทธ๋ ค์ค˜ (16:9). ๋‹ค์–‘ํ•œ ์žฅ์†Œ์™€ ์ƒํ™ฉ ์—ฐ์ถœ๋กœ ๋ฐฐ๊ฒฝ์„ ๋””ํ…Œ์ผํ•˜๊ฒŒ ํ•œ๋‹ค. ๋ฌด์กฐ๊ฑด 2D ์Šคํ‹ฑ๋งจ ์—ฐ์ถœ. 2d ์Šคํ‹ฑ๋งจ ๊ต์œก์šฉ ์ผ๋Ÿฌ์ŠคํŠธ",
9
+
10
+ "์Šคํ‹ฑ๋งจ ๋“œ๋ผ๋งˆ/์‚ฌ์‹ค์ ": "๊ณ ํ€„๋ฆฌํ‹ฐ ์–ผ๊ตฌ์ด ๋‘ฅ๊ทผ 2D ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ, ์‚ฌ์‹ค์ ์ธ ๋ฐฐ๊ฒฝ๊ณผ ์กฐ๋ช… ์—ฐ์ถœ. ์บ๋ฆญํ„ฐ: ์–ผ๊ตด์ด ๋‘ฅ๊ทผ ํ•˜์–€์ƒ‰ 2D ์Šคํ‹ฑ๋งจ๋“ค. ๋‹จ์ˆœํ•œ ๋‚™์„œ๊ฐ€ ์•„๋‹ˆ๋ผ, ๋ช…์•”๊ณผ ๋ฉ์–ด๋ฆฌ๊ฐ์ด ๋А๊ปด์ง€๋Š” '๊ณ ๊ธ‰ ์Šคํ‹ฑ๋งจ' ์Šคํƒ€์ผ. ์–ผ๊ตด์ด ํฌ๊ฒŒ ์ž˜๋ณด์ด๊ฒŒ ์—ฐ์ถœ. ๋ฐฐ๊ฒฝ: ๋‹จ์ˆœํ•œ ๋‹จ์ƒ‰ ๋ฐฐ๊ฒฝ ๊ธˆ์ง€. ๋Œ€๋ณธ์˜ ์žฅ์†Œ(์‚ฌ๋ฌด์‹ค, ๊ฑฐ๋ฆฌ, ๋ฐฉ ์•ˆ, ์ „์žฅ ๋“ฑ)๋ฅผ '์‚ฌ์ง„'์ฒ˜๋Ÿผ ๋””ํ…Œ์ผํ•˜๊ณ  ์ž…์ฒด์ ์œผ๋กœ 2d ๋ฌ˜์‚ฌ. ๋ถ„์œ„๊ธฐ: ์ •๋ณด ์ „๋‹ฌ๋ณด๋‹ค๋Š” '์ƒํ™ฉ๊ทน(Drama)'์— ์ง‘์ค‘. ์˜ํ™”์ ์ธ ์กฐ๋ช…(Cinematic Lighting)๊ณผ ์‹ฌ๋„(Depth) ํ‘œํ˜„. ์—ฐ์ถœ: ์Šคํ‹ฑ๋งจ ์—ฌ๋Ÿฌ ์บ๋ฆญํ„ฐ๋“ค์ด ๋Œ€๋ณธ ์† ํ–‰๋™์„ ๋ฆฌ์–ผํ•˜๊ฒŒ ์—ฐ๊ธฐ(Acting). ๊ฐ์ • ํ‘œํ˜„์€ ํ‘œ์ •๋ณด๋‹ค๋Š” ์—ญ๋™์ ์ธ ๋ชธ์ง“(Body Language)์œผ๋กœ ๊ทน๋Œ€ํ™”. ์ ˆ๋Œ€ ๊ธˆ์ง€: ํ™”๋ฉด ๋ถ„ํ• (Split Screen), ํ…์ŠคํŠธ ๋‚˜์—ด, ๋‹จ์ˆœ ์ธํฌ๊ทธ๋ž˜ํ”ฝ ์Šคํƒ€์ผ. ๋Œ€๋ณธ์˜ ์ƒํ™ฉ์„ ์ž˜ ๋‚˜ํƒ€๋‚ด๊ฒŒ ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ๋ง๊ณ  ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ.",
11
+
12
+ "์—ญ์‚ฌ/๋‹คํ (Dark)": "์—ญ์‚ฌ์  ์‚ฌ์‹ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ '2D ์‹œ๋„ค๋งˆํ‹ฑ ์–ผ๊ตด์ด ๋‘ฅ๊ทผ ํ•˜์–€์ƒ‰ ์Šคํ‹ฑ๋งจ ์• ๋‹ˆ๋ฉ”์ด์…˜' ์Šคํƒ€์ผ. ๊นŠ์ด ์žˆ๋Š” ์ƒ‰๊ฐ(Dark & Rich Tone)๊ณผ ๊ทน์ ์ธ ์กฐ๋ช… ์‚ฌ์šฉ. ์บ๋ฆญํ„ฐ๋Š” 2D ์‹ค๋ฃจ์—ฃ์ด๋‚˜ ์Šคํ‹ฑ๋งจ์ด์ง€๋งŒ ์‹œ๋Œ€์— ๋งž๋Š” ์˜์ƒ๊ณผ ํ—ค์–ด์Šคํƒ€์ผ์„ ์ฐฉ์šฉ. 2D ์Šคํ‹ฑ๋งจ์„ ํ™œ์šฉํ•ด ๋Œ€๋ณธ์„ ์„ค๋ช…์ด ์ž˜๋˜๊ฒŒ ์„ค๋ช…ํ•˜๋Š” ์—ฐ์ถœ์„ ํ•œ๋‹ค. ์ž๋ง‰ ์Šคํƒ€์ผ ์—ฐ์ถœ์€ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ „์Ÿ, ๊ธฐ๊ทผ ๋“ฑ์˜ ๋ฌ˜์‚ฌ๋Š” ์ƒ์ง•์ ์ด๊ณ  ์€์œ ์ ์œผ๋กœ ํ‘œํ˜„. ๋„ˆ๋ฌด ๊ณ ์–ดํ‹ฑํ•œ ์—ฐ์ถœ์€ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ฐฐ๊ฒฝ ๋ฌ˜์‚ฌ์— ๋””ํ…Œ์ผ์„ ์‚ด๋ ค ์‹œ๋Œ€์  ๋ถ„์œ„๊ธฐ๋ฅผ ๊ฐ•์กฐ. ๋ฌด์กฐ๊ฑด ์–ผ๊ตด์ด ๋‘ฅ๊ทผ 2D ์Šคํ‹ฑ๋งจ ์—ฐ์ถœ. ๋Œ€๋ณธ์˜ ์ƒํ™ฉ์„ ์ž˜ ๋‚˜ํƒ€๋‚ด๊ฒŒ ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ๋ง๊ณ  ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ.",
13
+
14
+ "3D ๋‹คํ๋ฉ˜ํ„ฐ๋ฆฌ": "Unreal Engine 5 render style, Realistic 3D game cinematic screenshot. ํ”ผ์‚ฌ์ฒด: ๋งค๋ˆํ•˜๊ณ  ํ•˜์–€ ์ด๋ชฉ๊ตฌ๋น„ ์—†๋Š” ๋งˆ๋„คํ‚น ๋จธ๋ฆฌ (Smooth white featureless mannequin head). ๋ˆˆ์ฝ”์ž… ์—†์Œ. ๋ณต์žฅ: ๊ฐ€๋””๊ฑด, ์ฒญ๋ฐ”์ง€, ์ •์žฅ ๋“ฑ ํ˜„์‹ค์ ์ธ ์˜์ƒ์„ ์ž…ํ˜€ ๊ธฐ๋ฌ˜ํ•œ ๋А๋‚Œ ๊ฐ•์กฐ. ์กฐ๋ช…: ์˜ํ™” ๊ฐ™์€ ์กฐ๋ช… (Cinematic lighting), ๋‹ค์†Œ ์–ด๋‘ก๊ณ  ๋ถ„์œ„๊ธฐ ์žˆ๋Š”(Moody) ์—ฐ์ถœ. ๋ฐฐ๊ฒฝ: ๋‚ก์€ ์†ŒํŒŒ, ์–ด์ง€๋Ÿฌ์ง„ ๋ฐฉ ๋“ฑ ์‚ฌ์‹ค์ ์ธ ํ…์Šค์ฒ˜์™€ ๋””ํ…Œ์ผ(8k resolution), ํ˜„์‹ค์ ์ธ ๋‹ค์–‘ํ•œ ์žฅ์†Œ. ๋Œ€๋ณธ์˜ ์ƒํ™ฉ์„ ์ž˜ ๋‚˜ํƒ€๋‚ด๊ฒŒ ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ๋ง๊ณ  ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ.",
15
+
16
+ "๊ณผํ•™/์—”์ง€๋‹ˆ์–ด๋ง": "3D Technical Animation (Fern, AiTelly Style). ํ™”ํ’: Blender Cycles / Clean Rendering, ๋ฐ์€ ์ŠคํŠœ๋””์˜ค ์กฐ๋ช…(Clean Studio Lighting). ์—ฐ์ถœ: ๊ธฐ๊ณ„/๊ฑด์ถ•๋ฌผ์˜ ๋‹จ๋ฉด๋„(Cutaway) ๋ฐ ์ž‘๋™ ์›๋ฆฌ ์‹œ๊ฐํ™”. ์ธ๋ฌผ: ์—”์ง€๋‹ˆ์–ด/๊ณผํ•™์ž/๊ต์‚ฌ/ํšŒ์‚ฌ์›/๊ตฐ์ธ ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•œ 3d ์บ๋ฆญํ„ฐ๊ฐ€ ๋“ฑ์žฅํ•˜์—ฌ ๊ธฐ๊ณ„๋ฅผ ์กฐ์ž‘ํ•˜๊ฑฐ๋‚˜ ์„ค๋ช…ํ•˜๋Š” ๊ธฐ๋Šฅ์  ์—ญํ•  ์ˆ˜ํ–‰. ๋ถ„์œ„๊ธฐ: ๊น”๋”ํ•˜๊ณ , ๊ต์œก์ ์ด๋ฉฐ, ๋ช…ํ™•ํ•จ(Clear & Educational). ๊ณผ๋„ํ•œ ๊ทธ๋ฆผ์ž ๋ฐฐ์ œ. ๋Œ€๋ณธ์˜ ์ƒํ™ฉ์„ ์ž˜ ๋‚˜ํƒ€๋‚ด๊ฒŒ ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ๋ง๊ณ  ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ.",
17
+
18
+ "์‹ฌํ”Œ ๊ทธ๋ฆผํŒ/์กธ๋ผ๋งจ": "๊ตต์€ ์„ , ๋‹จ์ˆœํ•จ์˜ ๋ฏธํ•™. ํ•œ ์žฅ๋ฉด์— ํ•˜๋‚˜์˜ ์ฃผ์ œ๋งŒ.",
19
+
20
+ "์‹ค์‚ฌ + ์ฝ”๋ฏน ํŽ˜์ด์Šค": "Hyper-Realistic Environment with Comic Elements. ๋ฐฐ๊ฒฝ๊ณผ ์‚ฌ๋ฌผ, ์‚ฌ๋žŒ/๋™๋ฌผ์˜ ๋ชธ์ฒด: '์–ธ๋ฆฌ์–ผ ์—”์ง„ 5' ์ˆ˜์ค€์˜ 8K ์‹ค์‚ฌ(Photorealistic). ํ„ธ, ํ”ผ๋ถ€ ์งˆ๊ฐ, ์กฐ๋ช… ์™„๋ฒฝ ๊ตฌํ˜„. ์‚ฌ๋žŒ ์–ผ๊ตด: ๋ชธ์€ ์‹ค์‚ฌ์ง€๋งŒ ์–ผ๊ตด๋งŒ '๋ฆญ ์•ค ๋ชจํ‹ฐ(Rick and Morty) ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ'์˜ 2D ์นดํˆฐ์œผ๋กœ ํ•ฉ์„ฑ. (์ฐธ์กฐ: ํฐ ํฐ์ƒ‰ ๋ˆˆ, ๊ฒ€์€ ์  ๋ˆˆ๋™์ž, ๊ตต์€ ๋ˆˆ์น, ๋‹จ์ˆœํ•œ ์ž…). - **ํ‘œ์ •:** ๋‹นํ™ฉ, ๊ณตํฌ, ํ˜ผ๋ž€, ์ˆ ์— ์ทจํ•œ ๋“ฏํ•œ '๋ณ‘๋ง›' ํ‘œ์ • ๊ฐ•์กฐ. ๋™๋ฌผ ๋ˆˆ: ํ„ธ๊ณผ ๋ชธ์€ ๋‹คํ๋ฉ˜ํ„ฐ๋ฆฌ๊ธ‰ ์‹ค์‚ฌ์ง€๋งŒ, ๋ˆˆ๋งŒ 'ํฐ์ƒ‰ ํฐ์ž์™€ ๊ฒ€์€ ์  ๋ˆˆ๋™์ž'๋กœ ๋œ 2D ๋งŒํ™” ๋ˆˆ์œผ๋กœ ์—ฐ์ถœ. ๋ถ„์œ„๊ธฐ: ๊ณ ํ€„๋ฆฌํ‹ฐ ๏ฟฝ๏ฟฝ๏ฟฝํ๋ฉ˜ํ„ฐ๋ฆฌ์ธ ์ฒ™ํ•˜๋Š” ๋ณ‘๋ง› ์ฝ”๋ฏธ๋””. ์ง„์ง€ํ•œ ์ƒํ™ฉ์ผ์ˆ˜๋ก ํ‘œ์ •์„ ๋” ๋‹จ์ˆœํ•˜๊ณ  ๋ฉ์ฒญํ•˜๊ฒŒ(Derp) ์—ฐ์ถœ. ์ ˆ๋Œ€ ์ด๋ฏธ์ง€์— ๊ธ€์”จ ์—ฐ์ถœ ์ „ํ˜€ ํ•˜์ง€ ์•Š๋Š”๋‹ค.",
21
+
22
+ "ํ•œ๊ตญ ์›นํˆฐ ์Šคํƒ€์ผ": "ํ•œ๊ตญ ์ธ๊ธฐ ์›นํˆฐ ์Šคํƒ€์ผ์˜ ๊ณ ํ€„๋ฆฌํ‹ฐ 2D ์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ด์…˜ (Korean Webtoon Style). ์„ ๋ช…ํ•œ ํŽœ์„ ๊ณผ ํ™”๋ คํ•œ ์ฑ„์ƒ‰. ์ง‘์ค‘์„ (Speed lines)์€ ์ •๋ง ์ค‘์š”ํ•œ ์ˆœ๊ฐ„์—๋งŒ ๊ฐ€๋” ์‚ฌ์šฉ. ์บ๋ฆญํ„ฐ๋Š” 8๋“ฑ์‹  ์›นํˆฐ ์ฃผ์ธ๊ณต ์Šคํƒ€์ผ. ์บ๋ฆญํ„ฐ ์ฃผ๋ณ€์˜ '์ƒํ™ฉ'๊ณผ '๋ฐฐ๊ฒฝ(์žฅ์†Œ)'์„ ์•„์ฃผ ๊ตฌ์ฒด์ ์ด๊ณ  ๋ฐ€๋„ ์žˆ๊ฒŒ ๋ฌ˜์‚ฌ. ๋‹จ์ˆœ ์ธ๋ฌผ ์ปท๋ณด๋‹ค๋Š” ์ฃผ๋ณ€ ์‚ฌ๋ฌผ๊ณผ ๋ฐฐ๊ฒฝ์ด ํ•จ๊ป˜ ๋ณด์ด๋Š” ๊ตฌ๋„ ์„ ํ˜ธ. ์ „์ฒด์ ์œผ๋กœ ๋ฐฐ๊ฒฝ ๋””ํ…Œ์ผ์ด ์‚ด์•„์žˆ๋Š” ๋„ค์ด๋ฒ„ ์›นํˆฐ ์ธ๋„ค์ผ ์Šคํƒ€์ผ. (16:9)",
23
+
24
+ "์ง€๋ธŒ๋ฆฌ ๋Œ€์ž‘ ๊ฐ์„ฑ": "์ผ๋ณธ ๋Œ€์ž‘ ๊ท€์—ฌ์šด ์ง€๋ธŒ๋ฆฌํ’ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ (High-Budget Anime Style). ์„œ์ •์ ์ธ ๋А๋‚Œ๋ณด๋‹ค๋Š” '์ •๋ณด๋Ÿ‰์ด ๋งŽ๊ณ  ์น˜๋ฐ€ํ•œ' ๊ณ ๋ฐ€๋„ ๋ฐฐ๊ฒฝ ์ž‘ํ™” (High Detail Backgrounds). ์ง€๋ธŒ๋ฆฌ ์บ๋ฆญํ„ฐ์˜ ํ‘œ์ •๊ณผ ํ–‰๋™์„ '์ˆœ๊ฐ„ ํฌ์ฐฉ'ํ•˜๋“ฏ ์—ญ๋™์ ์œผ๋กœ ๋ฌ˜์‚ฌ. ๋Œ€๋ณธ์˜ ์ง€๋ฌธ์„ ํ•˜๋‚˜๋„ ๋†“์น˜์ง€ ์•Š๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” '์ฒ ์ €ํ•œ ๋””ํ…Œ์ผ' ์œ„์ฃผ. (16:9) ์ „์ฒด ๋Œ€๋ณธ์— ์–ด์šธ๋ฆฌ๋Š” ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ."
25
+ }
26
+
27
+
28
+ # 2. ์ธ๋„ค์ผ ์ƒ์„ฑ ์ „๋žต ์ •์˜ (์ถ”ํ›„ ์ธ๋„ค์ผ ์ƒ์„ฑ ๊ธฐ๋Šฅ ํ™•์žฅ ์‹œ ์‚ฌ์šฉ)
29
+ THUMBNAIL_STRATEGIES = {
30
+ "A. ๊ทน์  ๋Œ€๋น„ (Split Screen)": """
31
+ [Strategy Goal]
32
+ Create a thumbnail with a 50:50 vertical split.
33
+ Left Side: Negative/Past/Weak (Dark tone).
34
+ Right Side: Positive/Future/Strong (Bright tone).
35
+
36
+ [CRITICAL RULES]
37
+ 1. **Text Language:** THE TEXT OVERLAY MUST BE IN KOREAN. Extract key phrases from the script in KOREAN. Do not translate them.
38
+ 2. **Composition:** Split screen vertically. Distinct visual separation.
39
+ 3. **Style:** Realistic, high-definition, YouTube documentary style, dramatic lighting.
40
+
41
+ [Prompt Structure to Generate]
42
+ "Split screen. Left: [Description of A], dark lighting, text: '[ํ•œ๊ตญ์–ด ํ…์ŠคํŠธ]'. Right: [Description of B], bright lighting, text: '[ํ•œ๊ตญ์–ด ํ…์ŠคํŠธ]'. Bottom: Big bold text '[ํ•œ๊ตญ์–ด ๋ฉ”์ธ ํƒ€์ดํ‹€]'. Realistic style, 8k."
43
+ """,
44
+
45
+ "B. ํ•˜์ด๋ธŒ๋ฆฌ๋“œ (Real + 2D)": """
46
+ [Strategy Goal]
47
+ Composite image. Realistic photo background + 2D vector stickman foreground.
48
+
49
+ [CRITICAL RULES]
50
+ 1. **Text Language:** THE TEXT OVERLAY MUST BE IN KOREAN.
51
+ 2. **Background:** Realistic photo of key subject/location. Documentary news style.
52
+ 3. **Foreground:** Simple 2D stickman character reacting/pointing.
53
+
54
+ [Prompt Structure to Generate]
55
+ "Composite image. Background: Realistic photo of [Key Subject], news style. Foreground: 2D stickman [Action/Reaction]. Text at bottom: '[ํ•œ๊ตญ์–ด ๋ฉ”์ธ ํƒ€์ดํ‹€]', bold font, yellow/red color."
56
+ """,
57
+
58
+ "C. ๋‚˜๋…ธ๋ฐ”๋‚˜๋‚˜ (2D Stickman)": """
59
+ [Strategy Goal]
60
+ Flat 2D vector art, simple stickman character.
61
+
62
+ [CRITICAL RULES]
63
+ 1. **Text Language:** THE TEXT OVERLAY MUST BE IN KOREAN.
64
+ 2. **Style:** Flat 2D vector art, clean lines. NO speech bubbles.
65
+ 3. **Logic:** If comparing -> Split screen. If explaining -> Single wide shot.
66
+
67
+ [Prompt Structure to Generate]
68
+ "Flat 2D vector art. Stickman character in [Pose]. Background: [Elements]. Text at bottom: '[ํ•œ๊ตญ์–ด ๋ฉ”์ธ ํƒ€์ดํ‹€]'. No speech bubbles."
69
+ """
70
+ }
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
logic_image.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # logic_image.py
2
+ import re
3
+
4
+ def _get_style_prompt(selected_style, custom_style_input, style_definitions):
5
+ if selected_style == "์ง์ ‘ ์ž…๋ ฅ":
6
+ return (custom_style_input or "").strip()
7
+ v = style_definitions.get(selected_style, "")
8
+ if isinstance(v, dict):
9
+ return (v.get("prompt") or "").strip()
10
+ return str(v).strip()
11
+
12
+ def _safe_extract_image_bytes(response):
13
+ """
14
+ google-genai ์‘๋‹ต์—์„œ ์ด๋ฏธ์ง€ bytes๋ฅผ ์ตœ๋Œ€ํ•œ ์•ˆ์ „ํ•˜๊ฒŒ ๊บผ๋‚ด๋Š” ํ•จ์ˆ˜
15
+ (๋ชจ๋ธ/๋ฒ„์ „๋ณ„๋กœ ์‘๋‹ต ๊ตฌ์กฐ๊ฐ€ ๋‹ฌ๋ผ์„œ ๋ฐฉ์–ด์ ์œผ๋กœ ์ฒ˜๋ฆฌ)
16
+ """
17
+ # candidates -> content -> parts -> inline_data.data ํŒจํ„ด
18
+ try:
19
+ cands = getattr(response, "candidates", None) or []
20
+ if not cands:
21
+ return None
22
+ content = getattr(cands[0], "content", None)
23
+ parts = getattr(content, "parts", None) or []
24
+ for p in parts:
25
+ inline = getattr(p, "inline_data", None)
26
+ if inline and getattr(inline, "data", None):
27
+ return inline.data
28
+ except:
29
+ pass
30
+
31
+ # ํ˜น์‹œ response.image ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์˜ค๋Š” ๊ฒฝ์šฐ
32
+ for key in ["image", "images", "data"]:
33
+ try:
34
+ v = getattr(response, key, None)
35
+ if isinstance(v, (bytes, bytearray)):
36
+ return bytes(v)
37
+ except:
38
+ pass
39
+
40
+ return None
41
+
42
+ def process_scene_task(
43
+ index,
44
+ scene,
45
+ selected_style,
46
+ custom_style_input,
47
+ client,
48
+ text_model_id,
49
+ image_model_id,
50
+ aspect_ratio,
51
+ reference_image=None,
52
+ ):
53
+ """
54
+ app.py๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์‹œ๊ทธ๋‹ˆ์ฒ˜:
55
+ return (idx, prompt_text, image_bytes)
56
+
57
+ - scene: {'text': '...'}
58
+ - reference_image: PIL.Image (์˜ต์…˜)
59
+ """
60
+ scene_text = (scene.get("text") or "").strip()
61
+
62
+ # 1) Brain: ์žฅ๋ฉด ํ…์ŠคํŠธ -> ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
63
+ # (์Šคํƒ€์ผ์€ config_style.py์˜ STYLE_DEFINITIONS ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ์ž…๋จ)
64
+ from config_style import STYLE_DEFINITIONS
65
+
66
+ style_prompt = _get_style_prompt(selected_style, custom_style_input, STYLE_DEFINITIONS)
67
+
68
+ brain_prompt = f"""
69
+ ๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์˜์ƒ์šฉ '์žฅ๋ฉด ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ'๋ฅผ ๋งŒ๋“œ๋Š” ์ „๋ฌธ๊ฐ€์•ผ.
70
+ ์•„๋ž˜ ๋Œ€๋ณธ์„ ๋ณด๊ณ , ๋”ฑ "ํ•œ ์žฅ๋ฉด"์„ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋งŒ๋“ค์–ด.
71
+
72
+ [์Šคํƒ€์ผ ์ง€์‹œ]
73
+ {style_prompt}
74
+
75
+ [ํ™”๋ฉด๋น„]
76
+ {aspect_ratio}
77
+
78
+ [๋Œ€๋ณธ]
79
+ {scene_text}
80
+
81
+ [์ ˆ๋Œ€ ๊ทœ์น™]
82
+ 1) ์ถœ๋ ฅ์€ ๋ฌด์กฐ๊ฑด ํ•œ๊ตญ์–ด๋กœ๋งŒ ์ž‘์„ฑ. ์˜์–ด/๋กœ๋งˆ์ž ๋‹จ์–ด ์‚ฌ์šฉ ๊ธˆ์ง€.
83
+ 2) ๋Œ€๋ณธ์˜ ์˜๋ฏธ๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•˜์ง€ ๋งˆ๋ผ. (๊ณผ์žฅ/๊ฐ์ƒ‰/์ถ”์ธก ๊ธˆ์ง€)
84
+ 3) ํ™”๋ฉด ๋ถ„ํ• (Split Screen) ๊ธˆ์ง€. ํ•œ ์žฅ๋ฉด์œผ๋กœ๋งŒ ๊ตฌ์„ฑ.
85
+ 4) ์ด๋ฏธ์ง€์— ๊ธ€์ž/์ž๋ง‰ ๋„ฃ์ง€ ๋งˆ๋ผ. (ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ๊ธˆ์ง€)
86
+ 5) ์ตœ์ข… ์ถœ๋ ฅ์€ "ํ”„๋กฌํ”„ํŠธ ํ•œ ๋ฉ์–ด๋ฆฌ ํ…์ŠคํŠธ"๋งŒ. JSON/๋งˆํฌ๋‹ค์šด/๋ฒˆํ˜ธ/์ œ๋ชฉ ๊ธˆ์ง€.
87
+
88
+ ์ด์ œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ถœ๋ ฅํ•ด.
89
+ """.strip()
90
+
91
+
92
+ brain_res = client.models.generate_content(
93
+ model=text_model_id,
94
+ contents=brain_prompt
95
+ )
96
+
97
+ # brain_res.text๊ฐ€ ์—†์„ ์ˆ˜๋„ ์žˆ์–ด ๋ฐฉ์–ด
98
+ final_prompt = getattr(brain_res, "text", None) or scene_text
99
+ final_prompt = re.sub(r"\s+", " ", final_prompt).strip()
100
+
101
+ # 2) Painter: ํ”„๋กฌํ”„ํŠธ -> ์ด๋ฏธ์ง€ ์ƒ์„ฑ
102
+ # google-genai ๋ฒ„์ „์— ๋”ฐ๋ผ generate_images / generate_content ๋‘˜ ๋‹ค ๋Œ€์‘
103
+ img_bytes = None
104
+
105
+ # (A) generate_images๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ
106
+ if hasattr(client.models, "generate_images"):
107
+ try:
108
+ img_res = client.models.generate_images(
109
+ model=image_model_id,
110
+ prompt=final_prompt
111
+ )
112
+ # ์‘๋‹ต ๊ตฌ์กฐ๊ฐ€ ๋ฒ„์ „๋งˆ๋‹ค ๋‹ฌ๋ผ์„œ ์ตœ๋Œ€ํ•œ ๋ฐฉ์–ด์ ์œผ๋กœ ์ถ”์ถœ
113
+ img_bytes = _safe_extract_image_bytes(img_res)
114
+ except:
115
+ img_bytes = None
116
+
117
+ # (B) ์—†์œผ๋ฉด generate_content๋กœ ์‹œ๋„
118
+ if img_bytes is None:
119
+ try:
120
+ # reference_image๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๋กœ ๊ฐ™์ด ์ „๋‹ฌ ์‹œ๋„
121
+ contents = final_prompt
122
+ if reference_image is not None:
123
+ # genai.types.Part ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฑธ ์‚ฌ์šฉ
124
+ try:
125
+ from google.genai import types
126
+ import io
127
+ buf = io.BytesIO()
128
+ reference_image.save(buf, format="PNG")
129
+ ref_part = types.Part.from_bytes(data=buf.getvalue(), mime_type="image/png")
130
+ contents = [final_prompt, ref_part]
131
+ except:
132
+ # types๊ฐ€ ์—†์œผ๋ฉด ๊ทธ๋ƒฅ ํ…์ŠคํŠธ๋งŒ
133
+ contents = final_prompt
134
+
135
+ img_res = client.models.generate_content(
136
+ model=image_model_id,
137
+ contents=contents
138
+ )
139
+ img_bytes = _safe_extract_image_bytes(img_res)
140
+ except:
141
+ img_bytes = None
142
+
143
+ return index, final_prompt, img_bytes
144
+ def process_thumbnail_task(
145
+ index,
146
+ strategy_key,
147
+ strategy_text,
148
+ script_text,
149
+ title_hint,
150
+ client,
151
+ text_model_id,
152
+ image_model_id,
153
+ aspect_ratio,
154
+ reference_image=None
155
+ ):
156
+ """
157
+ return (idx, final_prompt, image_bytes)
158
+ Tab1(์žฅ๋ฉด ์ƒ์„ฑ)๊ณผ ๋™์ผํ•œ ๋ฐ˜ํ™˜ํ˜• ์œ ์ง€
159
+ """
160
+ # 1) Brain: ์ „๋žต(A/B/C) + ๋Œ€๋ณธ -> ์ธ๋„ค์ผ์šฉ ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ(ํ•œ๊ธ€) ์ƒ์„ฑ
161
+ brain_prompt = f"""
162
+ ๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ์ „๋ฌธ๊ฐ€๋‹ค.
163
+ ์•„๋ž˜ ๋Œ€๋ณธ๊ณผ ์ธ๋„ค์ผ ์ „๋žต์„ ๋ฐ”ํƒ•์œผ๋กœ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ชจ๋ธ์— ๋„ฃ์„ 'ํ•œ ์žฅ์˜ ์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ'๋ฅผ ์ž‘์„ฑํ•˜๋ผ.
164
+
165
+ [์ „๋žต ํ‚ค]
166
+ {strategy_key}
167
+
168
+ [์ „๋žต ๋‚ด์šฉ]
169
+ {strategy_text}
170
+
171
+ [๋Œ€๋ณธ]
172
+ {script_text[:8000]}
173
+
174
+ [์ฐธ๊ณ  ์ œ๋ชฉ(๊ทธ๋ฆฌ์ง€ ๋ง ๊ฒƒ)]
175
+ {title_hint}
176
+
177
+ [ํ•„์ˆ˜ ์กฐ๊ฑด]
178
+ - ์ถœ๋ ฅ์€ ๋ฌด์กฐ๊ฑด ํ•œ๊ตญ์–ด๋งŒ. ์˜์–ด/๋กœ๋งˆ์ž ๊ธˆ์ง€.
179
+ - ์ด๋ฏธ์ง€ ์•ˆ์— ๊ธ€์ž/์ž๋ง‰/๋ฐฐ๋„ˆ/๋กœ๊ณ  ์ ˆ๋Œ€ ์ƒ์„ฑ ๊ธˆ์ง€.
180
+ - ๋Œ€์‹  ํ…์ŠคํŠธ๋ฅผ ๋‚˜์ค‘์— ๋„ฃ์„ ์ˆ˜ ์žˆ๊ฒŒ ์ƒ๋‹จ ๋˜๋Š” ํ•˜๋‹จ์— ๋„“์€ ์—ฌ๋ฐฑ(์•ˆ์ „์˜์—ญ)์„ ํ™•๋ณด.
181
+ - ํŠน์ • ๊ตญ๊ฐ€ ์ƒ์ง• ์ž๋™์ƒ์„ฑ ๊ธˆ์ง€: ํƒœ๊ทน๊ธฐ, ์ฒญ์™€๋Œ€, ๊ตญํšŒ์˜์‚ฌ๋‹น, ๋Œ€ํ†ต๋ น, ํ›ˆ์žฅ/ํœ˜์žฅ, ์„ ๊ฑฐ ํฌ์Šคํ„ฐ, ๊ตญ๊ธฐ๋ฅ˜ ์ „๋ถ€ ๊ธˆ์ง€.
182
+ - ์ •์น˜์ธ์„ ์‹ค์กด ์ธ๋ฌผ์ฒ˜๋Ÿผ ํŠน์ •ํ•˜์ง€ ๋ง๊ณ , ์ต๋ช… ์บ๋ฆญํ„ฐ/์‹ค๋ฃจ์—ฃ/์ƒ์ง•์  ์—ฐ์ถœ๋กœ ์ฒ˜๋ฆฌ.
183
+ - ํ™”๋ฉด ๋ถ„ํ• ์€ ์ „๋žต์— ํฌํ•จ๋œ ๊ฒฝ์šฐ์—๋งŒ ํ—ˆ์šฉ.
184
+ - ํ™”๋ฉด๋น„: {aspect_ratio}
185
+ - ์ตœ์ข… ์ถœ๋ ฅ์€ ํ”„๋กฌํ”„ํŠธ ํ…์ŠคํŠธ 1๊ฐœ๋งŒ. (JSON/๋งˆํฌ๋‹ค์šด/๋ฒˆํ˜ธ ๊ธˆ์ง€)
186
+ """.strip()
187
+
188
+ brain_res = client.models.generate_content(model=text_model_id, contents=brain_prompt)
189
+ final_prompt = (getattr(brain_res, "text", "") or "").strip()
190
+
191
+ # 2) Painter: Tab1๊ณผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
192
+ img_bytes = None
193
+
194
+ # (A) generate_images ์šฐ์„ 
195
+ if hasattr(client.models, "generate_images"):
196
+ try:
197
+ img_res = client.models.generate_images(
198
+ model=image_model_id,
199
+ prompt=final_prompt
200
+ )
201
+ img_bytes = _safe_extract_image_bytes(img_res)
202
+ except:
203
+ img_bytes = None
204
+
205
+ # (B) fallback: generate_content
206
+ if img_bytes is None:
207
+ try:
208
+ contents = final_prompt
209
+ img_res = client.models.generate_content(
210
+ model=image_model_id,
211
+ contents=contents
212
+ )
213
+ img_bytes = _safe_extract_image_bytes(img_res)
214
+ except:
215
+ img_bytes = None
216
+
217
+ return index, final_prompt, img_bytes
218
+
logic_seo.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from google import genai
4
+
5
+ def generate_seo_content(client, model_id, script_text):
6
+ """
7
+ ๋Œ€๋ณธ์„ ๋ถ„์„ํ•˜์—ฌ ์œ ํŠœ๋ธŒ ์ œ๋ชฉ, ํƒœ๊ทธ, ์„ค๋ช…๋ž€์„ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜
8
+ """
9
+ try:
10
+ # ํ”„๋กฌํ”„ํŠธ ์„ค๊ณ„: JSON ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅ์„ ๊ฐ•์ œํ•จ
11
+ prompt = f"""
12
+ You are a YouTube SEO Expert.
13
+ Analyze the following script and generate optimized metadata for a YouTube video.
14
+
15
+ [Script]
16
+ {script_text[:10000]} # ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ž๋ฆ„
17
+
18
+ [Requirements]
19
+ 1. Titles: Create 5 click-bait, high-performing titles (Korean).
20
+ 2. Tags: Generate 15 relevant tags separated by commas (Korean).
21
+ 3. Description: Write a compelling video description including hashtags (Korean).
22
+
23
+ [Output Format]
24
+ You MUST output a valid JSON object with the following structure:
25
+ {{
26
+ "titles": ["title1", "title2", ...],
27
+ "tags": ["tag1", "tag2", ...],
28
+ "description": "description text..."
29
+ }}
30
+
31
+ OUTPUT JSON ONLY. NO MARKDOWN.
32
+ """
33
+
34
+ # ํ…์ŠคํŠธ ๋ชจ๋ธ ํ˜ธ์ถœ
35
+ response = client.models.generate_content(
36
+ model=model_id,
37
+ contents=prompt
38
+ )
39
+
40
+ response_text = response.text.strip()
41
+
42
+ # ๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋ก ์ œ๊ฑฐ (```json ... ```)
43
+ if response_text.startswith("```"):
44
+ response_text = re.sub(r"^```json\s*", "", response_text)
45
+ response_text = re.sub(r"^```\s*", "", response_text)
46
+ response_text = re.sub(r"\s*```$", "", response_text)
47
+
48
+ # JSON ํŒŒ์‹ฑ
49
+ try:
50
+ result = json.loads(response_text)
51
+
52
+ # ํƒœ๊ทธ๊ฐ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌธ์ž์—ด๋กœ ์™”์„ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
53
+ if isinstance(result.get("tags"), str):
54
+ result["tags"] = [t.strip() for t in result["tags"].split(",")]
55
+
56
+ return result
57
+
58
+ except json.JSONDecodeError:
59
+ # JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ํ…์ŠคํŠธ ํŒŒ์‹ฑ ์‹œ๋„ (๋น„์ƒ ๋Œ€์ฑ…)
60
+ print("SEO JSON Parsing Failed. Falling back to text parsing.")
61
+ return _fallback_text_parsing(response_text)
62
+
63
+ except Exception as e:
64
+ print(f"SEO Generation Error: {e}")
65
+ # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋นˆ ๊นกํ†ต ๋ฐ˜ํ™˜ (์•ฑ์ด ์ฃฝ์ง€ ์•Š๊ฒŒ)
66
+ return {
67
+ "titles": ["๋ถ„์„ ์‹คํŒจ (๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”)"],
68
+ "tags": [],
69
+ "description": f"์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
70
+ }
71
+
72
+ def _fallback_text_parsing(text):
73
+ """
74
+ JSON ํ˜•์‹์ด ์•„๋‹ ๊ฒฝ์šฐ ์–ต์ง€๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜
75
+ """
76
+ return {
77
+ "titles": ["์ƒ์„ฑ๋œ ํ…์ŠคํŠธ์—์„œ ์ œ๋ชฉ์„ ์ถ”์ถœํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."],
78
+ "tags": ["์˜ค๋ฅ˜"],
79
+ "description": text
80
+ }
logic_tts.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import wave
3
+ import re
4
+
5
+ def split_text_smartly(text, limit=500):
6
+ """
7
+ ํ…์ŠคํŠธ๋ฅผ ๋ฌธ์žฅ ๋ถ€ํ˜ธ(.?!) ๊ธฐ์ค€์œผ๋กœ ์ž๋ฅด๋˜,
8
+ limit(๊ธ€์ž์ˆ˜)๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ์ตœ๋Œ€ํ•œ ๊ฝ‰ ์ฑ„์›Œ์„œ ํ•ฉ์น˜๋Š” ๋˜‘๋˜‘ํ•œ ํ•จ์ˆ˜
9
+ """
10
+ # 1. ๋จผ์ € ๋ฌธ์žฅ ๋ถ€ํ˜ธ ๊ธฐ์ค€์œผ๋กœ ์ž˜๊ฒŒ ์ชผ๊ฐฌ
11
+ sentences = re.split(r'(?<=[.?!])\s+', text)
12
+ chunks = []
13
+ current_chunk = ""
14
+
15
+ for sentence in sentences:
16
+ # ๋นˆ ๋ฌธ์žฅ ๋ฌด์‹œ
17
+ if not sentence.strip():
18
+ continue
19
+
20
+ # ํ•ฉ์ณค์„ ๋•Œ ์ œํ•œ์„ ๋„˜์œผ๋ฉด -> ์ง€๊ธˆ๊นŒ์ง€ ๋ชจ์€๊ฑฐ ์ €์žฅํ•˜๊ณ  ์ƒˆ ์ถœ๋ฐœ
21
+ if len(current_chunk) + len(sentence) > limit:
22
+ if current_chunk:
23
+ chunks.append(current_chunk.strip())
24
+ current_chunk = sentence
25
+ else:
26
+ # ์ œํ•œ ์•ˆ ๋„˜์œผ๋ฉด -> ๊ณ„์† ์ด์–ด ๋ถ™์ž„
27
+ current_chunk += " " + sentence
28
+
29
+ # ๋งˆ์ง€๋ง‰ ๋‚จ์€ ์กฐ๊ฐ ์ €์žฅ
30
+ if current_chunk:
31
+ chunks.append(current_chunk.strip())
32
+
33
+ return chunks
34
+
35
+ def raw_pcm_to_wav(pcm_data, sample_rate=24000):
36
+ """
37
+ [ํ•ต์‹ฌ] Gemini๊ฐ€ ์ฃผ๋Š” ๋‚ ๊ฒƒ์˜ PCM ๋ฐ์ดํ„ฐ๋ฅผ
38
+ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋“ค์„ ์ˆ˜ ์žˆ๋Š” WAV ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
39
+ """
40
+ try:
41
+ wav_io = io.BytesIO()
42
+ with wave.open(wav_io, "wb") as wav_file:
43
+ wav_file.setnchannels(1) # ๋ชจ๋…ธ
44
+ wav_file.setsampwidth(2) # 16-bit (2 bytes)
45
+ wav_file.setframerate(sample_rate)
46
+ wav_file.writeframes(pcm_data)
47
+ return wav_io.getvalue()
48
+ except Exception as e:
49
+ print(f"PCM to WAV Error: {e}")
50
+ return None
51
+
52
+ def generate_speech_chunk(client, model_id, text, voice_name):
53
+ """
54
+ ์งง์€ ํ…์ŠคํŠธ๋ฅผ ์˜ค๋””์˜ค๋กœ ๋ฐ”๊พธ๋Š” ํ•จ์ˆ˜ (๋ฏธ๋ฆฌ๋“ฃ๊ธฐ์šฉ)
55
+ """
56
+ try:
57
+ response = client.models.generate_content(
58
+ model=model_id,
59
+ contents=text,
60
+ config={
61
+ "response_modalities": ["AUDIO"],
62
+ "speech_config": {
63
+ "voice_config": {
64
+ "prebuilt_voice_config": {"voice_name": voice_name}
65
+ }
66
+ }
67
+ }
68
+ )
69
+
70
+ # ์˜ค๋””์˜ค ๋ฐ์ดํ„ฐ ์ถ”์ถœ
71
+ if response.candidates and response.candidates[0].content.parts:
72
+ part = response.candidates[0].content.parts[0]
73
+ if part.inline_data:
74
+ return part.inline_data.data
75
+ return None
76
+ except Exception as e:
77
+ print(f"TTS Chunk Error: {e}")
78
+ return None
79
+
80
+ def process_tts_task(index, text, client, model_id, voice_name):
81
+ """
82
+ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ž‘์—… ํ•จ์ˆ˜
83
+ """
84
+ audio_data = generate_speech_chunk(client, model_id, text, voice_name)
85
+ if audio_data:
86
+ # ๋ฐ”๋กœ WAV ํ—ค๋”๋ฅผ ์”Œ์šฐ์ง€ ์•Š๊ณ , ๋‚˜์ค‘์— ํ•ฉ์น˜๊ธฐ ์œ„ํ•ด PCM ์ƒํƒœ๋กœ ๋ฐ˜ํ™˜ํ•  ์ˆ˜๋„ ์žˆ์Œ
87
+ # ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„  ๊ฐ„๋‹จํ•˜๊ฒŒ PCM ๋ฐ”์ด๋„ˆ๋ฆฌ ์ž์ฒด๋ฅผ ๋ฐ˜ํ™˜
88
+ return index, audio_data
89
+ return index, None
90
+
91
+ def merge_wav_bytes(audio_chunks_pcm, sample_rate=24000):
92
+ """
93
+ ์—ฌ๋Ÿฌ ๊ฐœ์˜ PCM ์กฐ๊ฐ์„ ํ•˜๋‚˜์˜ ๊ธด WAV ํŒŒ์ผ๋กœ ํ•ฉ์นจ
94
+ """
95
+ try:
96
+ full_pcm = b"".join([chunk for chunk in audio_chunks_pcm if chunk])
97
+ return raw_pcm_to_wav(full_pcm, sample_rate)
98
+ except Exception as e:
99
+ print(f"Merge Error: {e}")
100
+ return None
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit
2
+ google-genai
utils.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import math
3
+
4
+ def group_sentences(text, target_sec=10):
5
+ raw_sentences = re.split(r'(?<=[.?!])\s+', text.strip())
6
+ scenes = []
7
+ curr_text, curr_time = "", 0
8
+ for s in raw_sentences:
9
+ if not s: continue
10
+ s_time = max(5, math.ceil(len(s)/35)*5)
11
+ if curr_time + s_time > target_sec and curr_text:
12
+ scenes.append({"text": curr_text, "time": curr_time})
13
+ curr_text, curr_time = s, s_time
14
+ else:
15
+ curr_text = (curr_text + " " + s).strip()
16
+ curr_time += s_time
17
+ if curr_text: scenes.append({"text": curr_text, "time": curr_time})
18
+ return scenes
19
+
20
+ def get_smart_filename(index, text):
21
+ clean_text = re.sub(r'[\\/*?:"<>|]', "", text).strip()
22
+ name_part = clean_text[:12] if len(clean_text) <= 12 else f"{clean_text[:6]}~{clean_text[-4:]}"
23
+ return f"{index+1:02d}_{name_part}.png"