Magenta49 commited on
Commit
d4165da
ยท
1 Parent(s): 8ff9a44

Add knowledge-issue news style

Browse files
Files changed (5) hide show
  1. .github/workflows/deploy_to_hf.yml +22 -0
  2. app.py +235 -118
  3. config_style.py +32 -36
  4. logic_image.py +342 -89
  5. utils.py +16 -1
.github/workflows/deploy_to_hf.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout repository
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+
17
+ - name: Push to Hugging Face Space
18
+ env:
19
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
20
+ run: |
21
+ git remote add space https://PLXR:${HF_TOKEN}@huggingface.co/spaces/PLXR/youtube_auto_image1
22
+ git push --force space main
app.py CHANGED
@@ -2,6 +2,8 @@ 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
 
@@ -9,6 +11,7 @@ from google import genai
9
  import logic_image
10
  import logic_seo
11
  import logic_tts
 
12
  from config_style import STYLE_DEFINITIONS, THUMBNAIL_STRATEGIES
13
 
14
  # ==========================================
@@ -28,6 +31,10 @@ 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("""
@@ -121,15 +128,10 @@ with st.sidebar:
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
 
@@ -147,7 +149,7 @@ with st.sidebar:
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์ž ๋‹จ์œ„๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.")
@@ -228,24 +230,48 @@ with tab1:
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()
@@ -256,9 +282,46 @@ with tab1:
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])
@@ -267,15 +330,18 @@ with tab1:
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}**")
@@ -285,9 +351,29 @@ with tab1:
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
  # ----------------------------------------------------------------
@@ -310,6 +396,8 @@ with tab2:
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)
@@ -323,105 +411,132 @@ with tab2:
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)
@@ -459,15 +574,17 @@ with tab3:
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):
@@ -478,4 +595,4 @@ with tab3:
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")
 
2
  import os
3
  import io
4
  import concurrent.futures
5
+ import datetime
6
+ import zipfile
7
  from PIL import Image
8
  from google import genai
9
 
 
11
  import logic_image
12
  import logic_seo
13
  import logic_tts
14
+ import utils
15
  from config_style import STYLE_DEFINITIONS, THUMBNAIL_STRATEGIES
16
 
17
  # ==========================================
 
31
  st.session_state['final_audio'] = None
32
  if 'shared_script' not in st.session_state:
33
  st.session_state['shared_script'] = ""
34
+ if 'thumbnail_results' not in st.session_state:
35
+ st.session_state['thumbnail_results'] = []
36
+ if 'thumbnail_text' not in st.session_state:
37
+ st.session_state['thumbnail_text'] = ""
38
 
39
  # [CSS] ๋‹คํฌ๋ชจ๋“œ + ์˜ค๋ Œ์ง€ ํฌ์ธํŠธ ๋””์ž์ธ
40
  st.markdown("""
 
128
  st.divider()
129
 
130
  st.markdown("### ๐ŸŽจ Model Engine")
131
+ st.radio("์‚ฌ์šฉํ•  ๋ชจ๋ธ", ["๐Ÿš€ Pro (Imagen 3)"], label_visibility="collapsed")
132
+ # [๊ณ ์ •] Pro ๋ชจ๋ธ๋งŒ ์‚ฌ์šฉ
133
+ image_model_id = "gemini-3-pro-image-preview"
134
+ text_model_id = "gemini-3-pro-preview"
 
 
 
 
 
135
 
136
  st.divider()
137
 
 
149
  st.markdown("<br>", unsafe_allow_html=True)
150
 
151
  st.caption("Image Scene Split (์ด๋ฏธ์ง€ ์ƒ์„ฑ์šฉ)")
152
+ duration_per_scene = st.slider("์žฅ๋ฉด๋‹น ์‹œ๊ฐ„(์ดˆ)", 5, 600, 5, step=5)
153
  split_criteria = duration_per_scene * 8
154
  st.info(f"๐Ÿ’ก {duration_per_scene}์ดˆ (์•ฝ {split_criteria}์ž) ๋‹จ์œ„๋กœ ์žฅ๋ฉด์„ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.")
155
  st.caption("โ€ป TTS๋Š” ์„ค์ •๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ 500์ž ๋‹จ์œ„๋กœ ์ตœ์ ํ™”๋ฉ๋‹ˆ๋‹ค.")
 
230
 
231
  st.toast(f"๐Ÿ“œ {len(scenes_text)}๊ฐœ ์žฅ๋ฉด์œผ๋กœ ๋ถ„ํ• ํ•˜์—ฌ ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
232
 
 
233
  progress_text = "AI ํ™”๊ฐ€๊ฐ€ ๊ทธ๋ฆผ์„ ๊ทธ๋ฆฌ๋Š” ์ค‘์ž…๋‹ˆ๋‹ค..."
234
  my_bar = st.progress(0, text=progress_text)
235
+
236
  temp_results = [None] * len(scenes_text)
237
+
238
+ def run_scene_task(task_index, task_text):
239
+ client = genai.Client(api_key=api_key)
240
+ return logic_image.process_scene_task(
241
+ task_index,
242
+ {'text': task_text},
243
+ selected_style,
244
+ custom_style_input,
245
+ client,
246
+ text_model_id,
247
+ image_model_id,
248
+ aspect_ratio,
249
+ reference_image
250
+ )
251
 
252
  # ์•ˆ์ •์„ฑ ์œ„ํ•ด max_workers=2
253
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
254
  future_to_idx = {
255
+ executor.submit(run_scene_task, i, text): i
256
  for i, text in enumerate(scenes_text)
257
  }
258
  completed = 0
259
  for future in concurrent.futures.as_completed(future_to_idx):
260
+ idx = future_to_idx[future]
261
+ try:
262
+ _, prompt, img = future.result()
263
+ temp_results[idx] = {
264
+ "prompt": prompt,
265
+ "img_bytes": img,
266
+ "script_text": scenes_text[idx],
267
+ }
268
+ completed += 1
269
+ my_bar.progress(completed / len(scenes_text), text=f"Scene {idx+1} ์™„๋ฃŒ!")
270
+ except Exception as exc:
271
+ completed += 1
272
+ my_bar.progress(completed / len(scenes_text), text=f"Scene {idx+1} ์‹คํŒจ")
273
+ st.error(f"Scene {idx+1} ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
274
+ st.exception(exc)
275
 
276
  st.session_state['scene_results'] = temp_results
277
  my_bar.empty()
 
282
  st.divider()
283
  st.subheader("๐ŸŽฌ Scene Results")
284
 
285
+ def _normalize_scene_item(item):
286
+ if isinstance(item, dict):
287
+ return item
288
+ if isinstance(item, (list, tuple)) and len(item) == 3:
289
+ prompt_text, img_bytes, script_text = item
290
+ return {
291
+ "prompt": prompt_text,
292
+ "img_bytes": img_bytes,
293
+ "script_text": script_text,
294
+ }
295
+ return {}
296
+
297
+ scene_zip_buffer = io.BytesIO()
298
+ with zipfile.ZipFile(scene_zip_buffer, "w") as zip_file:
299
+ for i, item in enumerate(st.session_state['scene_results']):
300
+ item = _normalize_scene_item(item)
301
+ if not item:
302
+ continue
303
+ img_data = item.get("img_bytes")
304
+ if img_data:
305
+ snippet = utils.make_scene_snippet(item.get("script_text", ""))
306
+ zip_file.writestr(f"scene_{i+1:02d}_{snippet}.png", img_data)
307
+ scene_zip_buffer.seek(0)
308
+ if scene_zip_buffer.getbuffer().nbytes > 0:
309
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M")
310
+ st.download_button(
311
+ label="โฌ‡๏ธ ๋ชจ๋“  ์ด๋ฏธ์ง€ ํ•œ๋ฒˆ์— ๋‹ค์šด๋กœ๋“œ",
312
+ data=scene_zip_buffer.getvalue(),
313
+ file_name=f"scenes_{timestamp}.zip",
314
+ mime="application/zip",
315
+ use_container_width=True
316
+ )
317
+
318
  for i, item in enumerate(st.session_state['scene_results']):
319
+ item = _normalize_scene_item(item)
320
+ if not item:
321
+ continue
322
+ p_text = item.get("prompt", "")
323
+ img_data = item.get("img_bytes")
324
+ s_text = item.get("script_text", "")
325
 
326
  with st.container():
327
  c1, c2 = st.columns([1, 2])
 
330
  try:
331
  image = Image.open(io.BytesIO(img_data))
332
  st.image(image, use_container_width=True)
333
+ snippet = utils.make_scene_snippet(s_text)
334
  st.download_button(
335
  label="โฌ‡๏ธ ๋‹ค์šด๋กœ๋“œ",
336
  data=img_data,
337
+ file_name=f"scene_{i+1:02d}_{snippet}.png",
338
  mime="image/png",
339
  key=f"dl_btn_{i}",
340
  use_container_width=True
341
  )
342
+ except Exception as exc:
343
+ st.error("์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.")
344
+ st.exception(exc)
345
  else: st.warning("์ด๋ฏธ์ง€ ์—†์Œ")
346
  with c2:
347
  st.markdown(f"**Scene {i+1}**")
 
351
  else:
352
  client = genai.Client(api_key=api_key)
353
  with st.spinner("๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ์ค‘..."):
354
+ try:
355
+ _, new_p, new_img = logic_image.process_scene_task(
356
+ i,
357
+ {'text': s_text},
358
+ selected_style,
359
+ custom_style_input,
360
+ client,
361
+ text_model_id,
362
+ image_model_id,
363
+ aspect_ratio,
364
+ reference_image
365
+ )
366
+ st.session_state['scene_results'][i] = {
367
+ "prompt": new_p,
368
+ "img_bytes": new_img,
369
+ "script_text": s_text,
370
+ }
371
+ st.rerun()
372
+ except Exception as exc:
373
+ st.error("์ด๋ฏธ์ง€ ์žฌ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
374
+ st.exception(exc)
375
+ with st.expander("ํ”„๋กฌํ”„ํŠธ ๋ณด๊ธฐ"):
376
+ st.write(p_text)
377
  st.divider()
378
 
379
  # ----------------------------------------------------------------
 
396
  seo_result = logic_seo.generate_seo_content(client, text_model_id, seo_script)
397
 
398
  st.session_state["seo_result"] = seo_result
399
+ st.session_state["thumbnail_results"] = []
400
+ st.session_state["thumbnail_text"] = seo_result["titles"][0] if seo_result.get("titles") else ""
401
  st.success("๋ถ„์„ ์™„๋ฃŒ!")
402
 
403
  c_seo1, c_seo2 = st.columns(2)
 
411
  st.markdown("##### ๐Ÿ“ ์„ค๋ช…๋ž€ (Description)")
412
  st.text_area("์„ค๋ช…๋ž€ ๊ฒฐ๊ณผ", seo_result['description'], height=200)
413
  st.divider()
 
414
 
415
+ st.subheader("๐Ÿ–ผ๏ธ ์ธ๋„ค์ผ ์ƒ์„ฑ")
416
 
417
+ seo_cached = st.session_state.get("seo_result")
 
 
 
 
418
 
419
+ if not seo_cached:
420
+ st.info("SEO ๋ถ„์„์„ ๋จผ์ € ์‹คํ–‰ํ•˜๋ฉด ์ธ๋„ค์ผ ์ƒ์„ฑ ๋ฒ„ํŠผ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.")
421
+ else:
422
+ default_thumb_text = seo_cached["titles"][0] if seo_cached.get("titles") else ""
423
+ if not st.session_state["thumbnail_text"]:
424
+ st.session_state["thumbnail_text"] = default_thumb_text
425
+ thumb_text = st.text_input(
426
+ "์ธ๋„ค์ผ ๋ฉ”์ธ ๋ฌธ๊ตฌ(์ฐธ๊ณ ์šฉ)",
427
+ value=st.session_state["thumbnail_text"]
428
+ )
429
+ st.session_state["thumbnail_text"] = thumb_text
430
+ thumbnail_aspect_ratio = "16:9"
431
+
432
+ if st.button("๐ŸŽจ ์ธ๋„ค์ผ 3์ข… ๋™์‹œ ์ƒ์„ฑ", key="thumb_img_btn", type="primary"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  if not api_key:
434
  st.error("API Key ํ•„์š”")
435
+ elif not seo_script:
436
+ st.warning("๋Œ€๋ณธ ํ•„์š”")
437
  else:
438
+ progress_text = "์ธ๋„ค์ผ 3์ข… ์ƒ์„ฑ ์ค‘..."
439
+ my_bar = st.progress(0, text=progress_text)
440
+
441
+ strategy_items = list(THUMBNAIL_STRATEGIES.items())
442
+ temp_results = [None] * len(strategy_items)
443
+
444
+ def run_thumbnail_task(task_index, strategy_key, strategy_text):
445
+ client = genai.Client(api_key=api_key)
446
+ return logic_image.process_thumbnail_task(
447
+ task_index,
448
+ strategy_key,
449
+ strategy_text,
450
+ seo_script,
451
+ thumb_text,
452
+ client,
453
+ text_model_id,
454
+ image_model_id,
455
+ thumbnail_aspect_ratio
456
+ )
457
 
458
+ with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
459
+ future_to_idx = {
460
+ executor.submit(
461
+ run_thumbnail_task,
462
+ i,
463
+ key,
464
+ strategy_text
465
+ ): i
466
+ for i, (key, strategy_text) in enumerate(strategy_items)
467
+ }
468
+ completed = 0
469
+ for future in concurrent.futures.as_completed(future_to_idx):
470
+ idx = future_to_idx[future]
471
+ strategy_key = strategy_items[idx][0]
472
  try:
473
+ _, prompt, img = future.result()
474
+ temp_results[idx] = (strategy_key, prompt, img)
475
+ completed += 1
476
+ my_bar.progress(completed / len(strategy_items), text=f"{strategy_key} ์™„๋ฃŒ!")
477
+ except Exception as exc:
478
+ completed += 1
479
+ my_bar.progress(completed / len(strategy_items), text=f"{strategy_key} ์‹คํŒจ")
480
+ st.error(f"{strategy_key} ์ธ๋„ค์ผ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
481
+ st.exception(exc)
482
+
483
+ st.session_state['thumbnail_results'] = temp_results
484
+ my_bar.empty()
485
+ st.rerun()
486
+
487
+ if st.session_state.get('thumbnail_results'):
488
+ cols = st.columns(3)
489
+ for idx, (col, item) in enumerate(zip(cols, st.session_state['thumbnail_results'])):
490
+ if item is None:
491
+ continue
492
+ strategy_key, prompt_text, img_data = item
493
+ with col:
494
+ st.markdown(f"**{strategy_key}**")
495
+ if img_data:
496
  try:
497
+ image = Image.open(io.BytesIO(img_data))
498
+ st.image(image, use_container_width=True)
499
+ st.download_button(
500
+ label="โฌ‡๏ธ ๋‹ค์šด๋กœ๋“œ",
501
+ data=img_data,
502
+ file_name=f"thumbnail_{strategy_key.split('.')[0].lower()}.png",
503
+ mime="image/png",
504
+ key=f"thumb_dl_{strategy_key}",
505
+ use_container_width=True
506
+ )
507
+ except Exception as exc:
508
+ st.error("์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.")
509
+ st.exception(exc)
510
+ else:
511
+ st.warning("์ด๋ฏธ์ง€ ์—†์Œ")
512
+ if st.button("๐Ÿ”„ ์žฌ์ƒ์„ฑ", key=f"thumb_regen_{strategy_key}"):
513
+ if not api_key:
514
+ st.error("API Key ํ•„์š”")
515
+ elif not seo_script:
516
+ st.warning("๋Œ€๋ณธ ํ•„์š”")
517
+ else:
518
+ client = genai.Client(api_key=api_key)
519
+ strategy_text = THUMBNAIL_STRATEGIES[strategy_key]
520
+ with st.spinner("์ธ๋„ค์ผ ์žฌ์ƒ์„ฑ ์ค‘..."):
521
+ try:
522
+ _, new_prompt, new_img = logic_image.process_thumbnail_task(
523
+ idx,
524
+ strategy_key,
525
+ strategy_text,
526
+ seo_script,
527
+ thumb_text,
528
+ client,
529
+ text_model_id,
530
+ image_model_id,
531
+ thumbnail_aspect_ratio
532
+ )
533
+ st.session_state['thumbnail_results'][idx] = (strategy_key, new_prompt, new_img)
534
+ st.rerun()
535
+ except Exception as exc:
536
+ st.error("์ธ๋„ค์ผ ์žฌ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.")
537
+ st.exception(exc)
538
+ with st.expander("ํ”„๋กฌํ”„ํŠธ ๋ณด๊ธฐ"):
539
+ st.write(prompt_text)
540
 
541
  # ----------------------------------------------------------------
542
  # [TAB 3] AI ์„ฑ์šฐ (TTS)
 
574
  if not api_key: st.error("API Key ํ•„์š”")
575
  elif not tts_script: st.warning("๋Œ€๋ณธ ํ•„์š”")
576
  else:
 
 
577
  # [ํ•ต์‹ฌ] TTS๋Š” ์Šฌ๋ผ์ด๋” ๊ฐ’(split_criteria)์„ ๋ฌด์‹œํ•˜๊ณ , ๋ฌด์กฐ๊ฑด 500์ž ๋‹จ์œ„๋กœ ๊ณ ์ •
578
  chunks = logic_tts.split_text_smartly(tts_script, limit=500)
579
 
580
  audio_res = [None] * len(chunks)
581
  with st.status("๋…น์Œ ์ง„ํ–‰ ์ค‘...", expanded=True):
582
  with concurrent.futures.ThreadPoolExecutor() as executor:
583
+ def run_tts_task(task_index, chunk):
584
+ client = genai.Client(api_key=api_key)
585
+ return logic_tts.process_tts_task(task_index, chunk, client, tts_model_id, voice_opt)
586
+
587
+ f_map = {executor.submit(run_tts_task, i, c): i for i, c in enumerate(chunks)}
588
  for f in concurrent.futures.as_completed(f_map):
589
  idx, dat = f.result()
590
  if isinstance(dat, bytes):
 
595
  if final_wav:
596
  st.success("์˜ค๋””์˜ค ์ƒ์„ฑ ์™„๋ฃŒ!")
597
  st.audio(final_wav, format="audio/wav")
598
+ st.download_button("๋‹ค์šด๋กœ๋“œ (WAV)", final_wav, "full_audio.wav", "audio/wav")
config_style.py CHANGED
@@ -16,55 +16,51 @@ STYLE_DEFINITIONS = {
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
- }
 
16
  "๊ณผํ•™/์—”์ง€๋‹ˆ์–ด๋ง": "3D Technical Animation (Fern, AiTelly Style). ํ™”ํ’: Blender Cycles / Clean Rendering, ๋ฐ์€ ์ŠคํŠœ๋””์˜ค ์กฐ๋ช…(Clean Studio Lighting). ์—ฐ์ถœ: ๊ธฐ๊ณ„/๊ฑด์ถ•๋ฌผ์˜ ๋‹จ๋ฉด๋„(Cutaway) ๋ฐ ์ž‘๋™ ์›๋ฆฌ ์‹œ๊ฐํ™”. ์ธ๋ฌผ: ์—”์ง€๋‹ˆ์–ด/๊ณผํ•™์ž/๊ต์‚ฌ/ํšŒ์‚ฌ์›/๊ตฐ์ธ ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•œ 3d ์บ๋ฆญํ„ฐ๊ฐ€ ๋“ฑ์žฅํ•˜์—ฌ ๊ธฐ๊ณ„๋ฅผ ์กฐ์ž‘ํ•˜๊ฑฐ๋‚˜ ์„ค๋ช…ํ•˜๋Š” ๊ธฐ๋Šฅ์  ์—ญํ•  ์ˆ˜ํ–‰. ๋ถ„์œ„๊ธฐ: ๊น”๋”ํ•˜๊ณ , ๊ต์œก์ ์ด๋ฉฐ, ๋ช…ํ™•ํ•จ(Clear & Educational). ๊ณผ๋„ํ•œ ๊ทธ๋ฆผ์ž ๋ฐฐ์ œ. ๋Œ€๋ณธ์˜ ์ƒํ™ฉ์„ ์ž˜ ๋‚˜ํƒ€๋‚ด๊ฒŒ ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ๋ง๊ณ  ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ.",
17
 
18
  "์‹ฌํ”Œ ๊ทธ๋ฆผํŒ/์กธ๋ผ๋งจ": "๊ตต์€ ์„ , ๋‹จ์ˆœํ•จ์˜ ๋ฏธํ•™. ํ•œ ์žฅ๋ฉด์— ํ•˜๋‚˜์˜ ์ฃผ์ œ๋งŒ.",
19
+
20
+ "๊ณผ์žฅ๋œ ์–ผ๊ตด/๋ฏธ๋‹ˆ๋ฉ€ ๋ฐ”๋””": "๊ทน๋„๋กœ ๊ณผ์žฅ๋œ 2D ์ฝ”๋ฏน ์นดํˆฐ ์Šคํƒ€์ผ (Exaggerated 2D Cartoon). **์บ๋ฆญํ„ฐ:** ๋ชธํ†ต๊ณผ ํŒ”๋‹ค๋ฆฌ๋Š” ๊ตต๊ธฐ ์—†๋Š” ์™„์ „ํ•œ '๋‹จ์ˆœ ๊ฒ€์€ ์„ (Simple Black Line Stick figure)'์œผ๋กœ ์ฒ˜๋ฆฌ. ์˜ท์ด๋‚˜ ๊ทผ์œก ๋ฌ˜์‚ฌ ์ƒ๋žต. ๋ฐ˜๋ฉด, **์–ผ๊ตด**์— ๋ชจ๋“  ์ž‘ํ™”๋ ฅ์„ ์ง‘์ค‘ํ•˜์—ฌ ์ด๋ชฉ๊ตฌ๋น„, ์ฃผ๋ฆ„, ํ‘œ์ •, ๋•€๋ฐฉ์šธ ๋“ฑ์„ ๋ถ€๋‹ด์Šค๋Ÿฌ์šธ ์ •๋„๋กœ ๋ฆฌ์–ผํ•˜๊ณ  ๊ณผ์žฅ๋˜๊ฒŒ ๋ฌ˜์‚ฌ (Hyper-expressive Face). **์žฅ๋น„:** ์ƒํ™ฉ์— ํ•„์š”ํ•œ ๋„๊ตฌ(์ด, ์Šค๋งˆํŠธํฐ, ์„œ๋ฅ˜ ๋“ฑ)๋Š” ์†์— ์ฅ์–ด์ฃผ๋ฉฐ ๋””ํ…Œ์ผํ•˜๊ฒŒ ํ‘œํ˜„. **๊ตฌ๋„:** ์บ๋ฆญํ„ฐ๋Š” ํ™”๋ฉด์˜ ์•ฝ 1/6 ํฌ๊ธฐ๋กœ ์ž‘๊ฒŒ ๋ฐฐ์น˜(Wide Shot). ๋‚˜๋จธ์ง€ ์—ฌ๋ฐฑ์€ ๋ฐฐ๊ฒฝ๊ณผ ์ƒํ™ฉ ๋ฌ˜์‚ฌ์— ํ• ์• ํ•˜์—ฌ ๋Œ€๋ณธ์˜ ์ƒํ™ฉ์„ ํ•œ๋ˆˆ์— ๋ณด์—ฌ์คŒ. **๋ฐฐ๊ฒฝ:** ์บ๋ฆญํ„ฐ์™€ ์–ด์šธ๋ฆฌ๋Š” 2D ์ผ๋Ÿฌ์ŠคํŠธ ๋ฐฐ๊ฒฝ. ๋ถ„ํ•  ํ™”๋ฉด ๊ธˆ์ง€, ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ.",
21
 
22
  "์‹ค์‚ฌ + ์ฝ”๋ฏน ํŽ˜์ด์Šค": "Hyper-Realistic Environment with Comic Elements. ๋ฐฐ๊ฒฝ๊ณผ ์‚ฌ๋ฌผ, ์‚ฌ๋žŒ/๋™๋ฌผ์˜ ๋ชธ์ฒด: '์–ธ๋ฆฌ์–ผ ์—”์ง„ 5' ์ˆ˜์ค€์˜ 8K ์‹ค์‚ฌ(Photorealistic). ํ„ธ, ํ”ผ๋ถ€ ์งˆ๊ฐ, ์กฐ๋ช… ์™„๋ฒฝ ๊ตฌํ˜„. ์‚ฌ๋žŒ ์–ผ๊ตด: ๋ชธ์€ ์‹ค์‚ฌ์ง€๋งŒ ์–ผ๊ตด๋งŒ '๋ฆญ ์•ค ๋ชจํ‹ฐ(Rick and Morty) ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ'์˜ 2D ์นดํˆฐ์œผ๋กœ ํ•ฉ์„ฑ. (์ฐธ์กฐ: ํฐ ํฐ์ƒ‰ ๋ˆˆ, ๊ฒ€์€ ์  ๋ˆˆ๋™์ž, ๊ตต์€ ๋ˆˆ์น, ๋‹จ์ˆœํ•œ ์ž…). - **ํ‘œ์ •:** ๋‹นํ™ฉ, ๊ณตํฌ, ํ˜ผ๋ž€, ์ˆ ์— ์ทจํ•œ ๋“ฏํ•œ '๋ณ‘๋ง›' ํ‘œ์ • ๊ฐ•์กฐ. ๋™๋ฌผ ๋ˆˆ: ํ„ธ๊ณผ ๋ชธ์€ ๋‹คํ๋ฉ˜ํ„ฐ๋ฆฌ๊ธ‰ ์‹ค์‚ฌ์ง€๋งŒ, ๋ˆˆ๋งŒ 'ํฐ์ƒ‰ ํฐ์ž์™€ ๊ฒ€์€ ์  ๋ˆˆ๋™์ž'๋กœ ๋œ 2D ๋งŒํ™” ๋ˆˆ์œผ๋กœ ์—ฐ์ถœ. ๋ถ„์œ„๊ธฐ: ๊ณ ํ€„๋ฆฌํ‹ฐ ๋‹คํ๋ฉ˜ํ„ฐ๋ฆฌ์ธ ์ฒ™ํ•˜๋Š” ๋ณ‘๋ง› ์ฝ”๋ฏธ๋””. ์ง„์ง€ํ•œ ์ƒํ™ฉ์ผ์ˆ˜๋ก ํ‘œ์ •์„ ๋” ๋‹จ์ˆœํ•˜๊ณ  ๋ฉ์ฒญํ•˜๊ฒŒ(Derp) ์—ฐ์ถœ. ์ ˆ๋Œ€ ์ด๋ฏธ์ง€์— ๊ธ€์”จ ์—ฐ์ถœ ์ „ํ˜€ ํ•˜์ง€ ์•Š๋Š”๋‹ค.",
23
 
24
  "ํ•œ๊ตญ ์›นํˆฐ ์Šคํƒ€์ผ": "ํ•œ๊ตญ ์ธ๊ธฐ ์›นํˆฐ ์Šคํƒ€์ผ์˜ ๊ณ ํ€„๋ฆฌํ‹ฐ 2D ์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ด์…˜ (Korean Webtoon Style). ์„ ๋ช…ํ•œ ํŽœ์„ ๊ณผ ํ™”๋ คํ•œ ์ฑ„์ƒ‰. ์ง‘์ค‘์„ (Speed lines)์€ ์ •๋ง ์ค‘์š”ํ•œ ์ˆœ๊ฐ„์—๋งŒ ๊ฐ€๋” ์‚ฌ์šฉ. ์บ๋ฆญํ„ฐ๋Š” 8๋“ฑ์‹  ์›นํˆฐ ์ฃผ์ธ๊ณต ์Šคํƒ€์ผ. ์บ๋ฆญํ„ฐ ์ฃผ๋ณ€์˜ '์ƒํ™ฉ'๊ณผ '๋ฐฐ๊ฒฝ(์žฅ์†Œ)'์„ ์•„์ฃผ ๊ตฌ์ฒด์ ์ด๊ณ  ๋ฐ€๋„ ์žˆ๊ฒŒ ๋ฌ˜์‚ฌ. ๋‹จ์ˆœ ์ธ๋ฌผ ์ปท๋ณด๋‹ค๋Š” ์ฃผ๋ณ€ ์‚ฌ๋ฌผ๊ณผ ๋ฐฐ๊ฒฝ์ด ํ•จ๊ป˜ ๋ณด์ด๋Š” ๊ตฌ๋„ ์„ ํ˜ธ. ์ „์ฒด์ ์œผ๋กœ ๋ฐฐ๊ฒฝ ๋””ํ…Œ์ผ์ด ์‚ด์•„์žˆ๋Š” ๋„ค์ด๋ฒ„ ์›นํˆฐ ์ธ๋„ค์ผ ์Šคํƒ€์ผ. (16:9)",
25
+
26
+ "์กฐ์„  ์›นํˆฐ ์Šคํƒ€์ผ": "์กฐ์„ ์‹œ๋Œ€ ๋ฐฐ๊ฒฝ์˜ ํ•œ๊ตญ ์ธ๊ธฐ ์‹œ๋Œ€๊ทน(์‚ฌ๊ทน) ์›นํˆฐ ์Šคํƒ€์ผ ๊ณ ํ€„๋ฆฌํ‹ฐ 2D ์ผ๋Ÿฌ์ŠคํŠธ๋ ˆ์ด์…˜ (Korean Historical Webtoon Style). ์„ ๋ช…ํ•œ ํŽœ์„ ๊ณผ ํ™”๋ คํ•œ ์ฑ„์ƒ‰. ์ง‘์ค‘์„ ์€ ์ค‘์š”ํ•œ ์ˆœ๊ฐ„์—๋งŒ ์‚ฌ์šฉ. ์บ๋ฆญํ„ฐ๋Š” 8๋“ฑ์‹  ์›นํˆฐ ์ฃผ์ธ๊ณต ์Šคํƒ€์ผ์ด๋ฉฐ ํ•œ๋ณต์„ ์ฐฉ์šฉ. ์บ๋ฆญํ„ฐ ์ฃผ๋ณ€ ์ƒํ™ฉ๊ณผ ๋ฐฐ๊ฒฝ์„ ์•„์ฃผ ๊ตฌ์ฒด์ ์ด๊ณ  ๋ฐ€๋„ ์žˆ๊ฒŒ ๋ฌ˜์‚ฌํ•˜๋˜, ๋ฐฐ๊ฒฝ์€ ๋ฐ˜๋“œ์‹œ ์กฐ์„ ์‹œ๋Œ€ ๊ฑด์ถ•๋ฌผ(๊ธฐ์™€์ง‘, ์ดˆ๊ฐ€์ง‘, ํ™๊ธธ)๊ณผ ์ž์—ฐ ํ’๊ฒฝ์œผ๋กœ ํ•œ์ •. ํ˜„๋Œ€์ ์ธ ๊ฑด๋ฌผ์ด๋‚˜ ๋ฌผ๊ฑด์€ ์ ˆ๋Œ€ ๋ฐฐ์ œ. ์ „์ฒด์ ์œผ๋กœ ๋ฐฐ๊ฒฝ ๋””ํ…Œ์ผ์ด ์‚ด์•„์žˆ๋Š” ๋„ค์ด๋ฒ„ ์‹œ๋Œ€๊ทน ์›นํˆฐ ์ธ๋„ค์ผ ์Šคํƒ€์ผ. (16:9)",
27
+
28
+ "์ง€์‹์ด์Šˆ ๋‰ด์Šค": "์ง€์‹/์ด์Šˆ/๋‰ด์Šค ์œ ํŠœ๋ธŒ ์ฑ„๋„ ์Šคํƒ€์ผ (High-End Explainer & Documentary). [ํ•ต์‹ฌ]: \"๋‰ด์Šค ํ™”๋ฉด(์ž๋ง‰๋ฐ”, ๋กœ๊ณ )\" ์ ˆ๋Œ€ ๊ธˆ์ง€. ์„ธ๋ จ๋œ '๋‹คํ๋ฉ˜ํ„ฐ๋ฆฌ ์ž๋ฃŒ ํ™”๋ฉด' ์Šคํƒ€์ผ. [์ƒํ™ฉ๋ณ„ ์ž๋™ ์—ฐ์ถœ ์„ ํƒ]: 1. ๋ฐ์ดํ„ฐ/ํ†ต๊ณ„ -> '3D ์ž…์ฒด ๊ทธ๋ž˜ํ”„' + '์ƒ์Šน/ํ•˜๋ฝ ํ™”์‚ดํ‘œ' + '์ˆซ์ž'. 2. ์ธ๋ฌผ/๋ฐœ์–ธ -> '์ธ๋ฌผ ์‚ฌ์ง„' + '๋งํ’์„ /์ธ์šฉ๊ตฌ'. 3. ์‚ฌ๋ฌผ/์žฅ์†Œ -> '๊ณ ํ™”์งˆ ์‹ค์‚ฌ ๊ผด๋ผ์ฃผ(Collage)'. 4. ๊ฐœ๋…/์ •๋ฆฌ -> 'ํฐ์ƒ‰ ๊ตฌ๊ฒจ์ง„ ์ข…์ด ๋ฐฐ๊ฒฝ(Top-down)' + '์‹ค์‚ฌ ์‚ฌ์ง„ ์‚ฌ๋ฌผ, ์žฅ์†Œ, ์ธํ™”๋ฌผ(๊ทธ๋ฆผ์ž ํšจ๊ณผ)' + '์„ธ๋ จ๋œ ๋„ค์ด๋น„ ์ •๋ณด ์ƒ์ž UI'. 5. ๊ตฌ์กฐ/์‹œ๊ฐํ™” -> '3D ์•„์ด์ฝ˜ ์ˆœ์„œ๋„(Flow)' ๋˜๋Š” 'VS ๋Œ€๊ฒฐ๊ตฌ๋„(Split)'. ๋ชจ๋“œ1 ๋ถ€ํ„ฐ ๋ชจ๋“œ5 ์ค‘์—์„œ ์–ด์šธ๋ฆฌ๋Š” ๋ชจ๋“œ๋ฅผ ๊ณจ๊ณ ๋ฃจ ์„ ํƒํ•œ๋‹ค. ๋ถ„ํ™œํ™”๋ฉด์œผ๋กœ ์—ฐ์ถœํ•˜์ง€ ๋ง๊ณ  ํ•˜๋‚˜์˜ ํ™”๋ฉด์œผ๋กœ ์—ฐ์ถœํ•œ๋‹ค. ํ…์ŠคํŠธ(ํ…์ŠคํŠธ์ƒ์ž)๋Š” ํ™”๋ฉด ์•„๋ž˜์˜ ์ž๋ง‰์„ ๋„ฃ์„ ๊ณต๊ฐ„์„ ๋ฐฉํ•ดํ•˜์ง€ ์•Š๋„๋ก ์ ˆ๋Œ€ ํ•˜๋‹จ์— ์—ฐ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค.",
29
 
30
  "์ง€๋ธŒ๋ฆฌ ๋Œ€์ž‘ ๊ฐ์„ฑ": "์ผ๋ณธ ๋Œ€์ž‘ ๊ท€์—ฌ์šด ์ง€๋ธŒ๋ฆฌํ’ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ (High-Budget Anime Style). ์„œ์ •์ ์ธ ๋А๋‚Œ๋ณด๋‹ค๋Š” '์ •๋ณด๋Ÿ‰์ด ๋งŽ๊ณ  ์น˜๋ฐ€ํ•œ' ๊ณ ๋ฐ€๋„ ๋ฐฐ๊ฒฝ ์ž‘ํ™” (High Detail Backgrounds). ์ง€๋ธŒ๋ฆฌ ์บ๋ฆญํ„ฐ์˜ ํ‘œ์ •๊ณผ ํ–‰๋™์„ '์ˆœ๊ฐ„ ํฌ์ฐฉ'ํ•˜๋“ฏ ์—ญ๋™์ ์œผ๋กœ ๋ฌ˜๏ฟฝ๏ฟฝ๏ฟฝ. ๋Œ€๋ณธ์˜ ์ง€๋ฌธ์„ ํ•˜๋‚˜๋„ ๋†“์น˜์ง€ ์•Š๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” '์ฒ ์ €ํ•œ ๋””ํ…Œ์ผ' ์œ„์ฃผ. (16:9) ์ „์ฒด ๋Œ€๋ณธ์— ์–ด์šธ๋ฆฌ๋Š” ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ์—ฐ์ถœ."
31
  }
32
 
33
 
34
+ # 2. ์ธ๋„ค์ผ ์ƒ์„ฑ ์ „๋žต ์ •์˜ (A/B/C ๊ณ ์ •)
35
  THUMBNAIL_STRATEGIES = {
36
  "A. ๊ทน์  ๋Œ€๋น„ (Split Screen)": """
37
+ - ํ™”๋ฉด์„ ์ •ํ™•ํžˆ 50:50 ์ˆ˜์ง ๋ถ„ํ• .
38
+ - Left: ๋ถ€์ •์ /๊ณผ๊ฑฐ/์•ฝ์ž ๋ถ„์œ„๊ธฐ (์–ด๋‘ก๊ณ  ํ˜ผ๋ž€).
39
+ - Right: ๊ธ์ •์ /๋ฏธ๋ž˜/๊ฐ•์ž ๋ถ„์œ„๊ธฐ (๋ฐ๊ณ  ์งˆ์„œ).
40
+ - ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ํ•„์ˆ˜:
41
+ * ์ขŒ์ธก ์ƒ๋‹จ: ์งง์€ ๋ถ€์ • ๋ฌธ๊ตฌ
42
+ * ์šฐ์ธก ์ƒ๋‹จ: ์งง์€ ๊ธ์ • ๋ฌธ๊ตฌ
43
+ * ํ•˜๋‹จ ์ค‘์•™: ๋ฉ”์ธ ํƒ€์ดํ‹€(thumb_text)
44
+ - Style: Realistic documentary, dramatic lighting.
 
 
 
 
45
  """,
46
 
47
+ "B. ์‹ค์‚ฌ + 2D ์Šคํ‹ฑ๋งจ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ": """
48
+ - Background: ํ˜„์‹ค ๋‹คํ ๋‰ด์Šค ์‚ฌ์ง„ ์Šคํƒ€์ผ.
49
+ - Foreground: 2D ์Šคํ‹ฑ๋งจ ์บ๋ฆญํ„ฐ๊ฐ€ ์ถฉ๊ฒฉ/๊ฐ•์กฐ ๋ฆฌ์•ก์…˜.
50
+ - ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ํ•„์ˆ˜:
51
+ * ์ƒ๋‹จ: ์†๋ณด/๊ธด๊ธ‰/๋‹จ๋… ๊ฐ™์€ ์„œ๋ธŒ ๋ฌธ๊ตฌ
52
+ * ํ•˜๋‹จ: ๋ฉ”์ธ ํƒ€์ดํ‹€(thumb_text)
53
+ - Style: News composite, high CTR.
 
 
 
 
54
  """,
55
 
56
+ "C. 2D Stickman ์ „์šฉ": """
57
+ - Flat vector, clean stickman.
58
+ - ๋งํ’์„  ์ ˆ๋Œ€ ๊ธˆ์ง€.
59
+ - ๋Œ€๋ณธ ๊ตฌ์กฐ์— ๋”ฐ๋ผ:
60
+ * ๋น„๊ต๋ฉด Split Screen
61
+ * ์Šคํ† ๋ฆฌ๋ฉด Single Scene
62
+ - ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ํ•„์ˆ˜:
63
+ * ํ•˜๋‹จ: ๋ฉ”์ธ ํƒ€์ดํ‹€(thumb_text)
64
+ * ์ƒ๋‹จ: ์„ ํƒ์  ์„œ๋ธŒ ๋ฌธ๊ตฌ
 
 
65
  """
66
+ }
logic_image.py CHANGED
@@ -1,6 +1,28 @@
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()
@@ -15,29 +37,113 @@ def _safe_extract_image_bytes(response):
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,
@@ -84,64 +190,117 @@ def process_scene_task(
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,
@@ -151,7 +310,7 @@ def process_scene_task(
151
  text_model_id,
152
  image_model_id,
153
  aspect_ratio,
154
- reference_image=None
155
  ):
156
  """
157
  return (idx, final_prompt, image_bytes)
@@ -176,43 +335,137 @@ def process_scene_task(
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # logic_image.py
2
+ import json
3
+ import logging
4
  import re
5
 
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ImageExtractionError(RuntimeError):
10
+ """Raised when image bytes cannot be extracted from a model response."""
11
+
12
+
13
+ def _summarize_response_structure(response):
14
+ if response is None:
15
+ return "response=None"
16
+ summary = {
17
+ "type": type(response).__name__,
18
+ "has_candidates": hasattr(response, "candidates"),
19
+ "has_content": hasattr(response, "content"),
20
+ "has_image": hasattr(response, "image"),
21
+ "has_images": hasattr(response, "images"),
22
+ "has_data": hasattr(response, "data"),
23
+ }
24
+ return ", ".join(f"{k}={v}" for k, v in summary.items())
25
+
26
  def _get_style_prompt(selected_style, custom_style_input, style_definitions):
27
  if selected_style == "์ง์ ‘ ์ž…๋ ฅ":
28
  return (custom_style_input or "").strip()
 
37
  (๋ชจ๋ธ/๋ฒ„์ „๋ณ„๋กœ ์‘๋‹ต ๊ตฌ์กฐ๊ฐ€ ๋‹ฌ๋ผ์„œ ๋ฐฉ์–ด์ ์œผ๋กœ ์ฒ˜๋ฆฌ)
38
  """
39
  # candidates -> content -> parts -> inline_data.data ํŒจํ„ด
40
+ cands = getattr(response, "candidates", None) or []
41
+ if cands:
42
+ for cand in cands:
43
+ content = getattr(cand, "content", None)
44
+ parts = getattr(content, "parts", None) or []
45
+ for p in parts:
46
+ inline = getattr(p, "inline_data", None)
47
+ data = getattr(inline, "data", None) if inline else None
48
+ if data:
49
+ return data
50
+ text_parts = [getattr(p, "text", None) for p in parts if getattr(p, "text", None)]
51
+ if text_parts:
52
+ snippet = re.sub(r"\s+", " ", " ".join(text_parts)).strip()[:200]
53
+ raise ImageExtractionError(f"ํ…์ŠคํŠธ๋งŒ ๋ฐ˜ํ™˜๋˜์–ด ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ์ผ๋ถ€: {snippet}")
54
+ raise ImageExtractionError("์ด๋ฏธ์ง€ ํŒŒํŠธ๊ฐ€ ์—†์–ด bytes๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
55
 
56
  # ํ˜น์‹œ response.image ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์˜ค๋Š” ๊ฒฝ์šฐ
57
  for key in ["image", "images", "data"]:
58
+ v = getattr(response, key, None)
59
+ if isinstance(v, (bytes, bytearray)):
60
+ return bytes(v)
61
+
62
+ summary = _summarize_response_structure(response)
63
+ raise ImageExtractionError(f"์ด๋ฏธ์ง€ bytes ์ถ”์ถœ ์‹คํŒจ: {summary}")
64
+
65
+
66
+ def _generate_thumbnail_texts(strategy_key, script_text, title_hint, client, text_model_id):
67
+ default_payload = {"top": "", "layout": "single", "left": "", "right": ""}
68
+ if strategy_key.startswith("A."):
69
+ instruction = """
70
+ ๋Œ€๋ณธ์„ ์ฐธ๊ณ ํ•ด์„œ ์ธ๋„ค์ผ์šฉ ์งง์€ ๋ฌธ๊ตฌ 2๊ฐœ๋ฅผ ์ž‘์„ฑํ•ด.
71
+ - ์ขŒ์ธก ์ƒ๋‹จ: ๋ถ€์ •์ /๊ณผ๊ฑฐ/์•ฝ์ž ๋ถ„์œ„๊ธฐ์˜ ์งง์€ ๋ฌธ๊ตฌ (6์ž ์ด๋‚ด)
72
+ - ์šฐ์ธก ์ƒ๋‹จ: ๊ธ์ •์ /๋ฏธ๋ž˜/๊ฐ•์ž ๋ถ„์œ„๊ธฐ์˜ ์งง์€ ๋ฌธ๊ตฌ (6์ž ์ด๋‚ด)
73
+ ์ถœ๋ ฅ ํ˜•์‹(JSON ONLY):
74
+ {"left": "...", "right": "..."}
75
+ """.strip()
76
+ elif strategy_key.startswith("B."):
77
+ instruction = """
78
+ ๋Œ€๋ณธ์„ ์ฐธ๊ณ ํ•ด์„œ ์ธ๋„ค์ผ ์ƒ๋‹จ์— ๋“ค์–ด๊ฐˆ ์„œ๋ธŒ ๋ฌธ๊ตฌ๋ฅผ ์ž‘์„ฑํ•ด.
79
+ - "์†๋ณด/๊ธด๊ธ‰/๋‹จ๋…" ๊ฐ™์€ ํ†ค
80
+ - 6์ž ์ด๋‚ด
81
+ ์ถœ๋ ฅ ํ˜•์‹(JSON ONLY):
82
+ {"top": "..."}
83
+ """.strip()
84
+ else:
85
+ instruction = """
86
+ ๋Œ€๋ณธ์„ ์ฐธ๊ณ ํ•ด์„œ ์ธ๋„ค์ผ ์ƒ๋‹จ์— ๋“ค์–ด๊ฐˆ ์„œ๋ธŒ ๋ฌธ๊ตฌ๋ฅผ ์ž‘์„ฑํ•ด.
87
+ - 6์ž ์ด๋‚ด (์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)
88
+ ๊ทธ๋ฆฌ๊ณ  ๊ตฌ์„ฑ ์œ ํ˜•์„ ์„ ํƒํ•ด.
89
+ - ๋น„๊ต/๋Œ€์กฐ๋ฉด split
90
+ - ์Šคํ† ๋ฆฌ ํ๋ฆ„์ด๋ฉด single
91
+ ์ถœ๋ ฅ ํ˜•์‹(JSON ONLY):
92
+ {"top": "...", "layout": "split|single"}
93
+ """.strip()
94
 
95
+ prompt = f"""
96
+ ๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ ์นดํ”ผ๋ผ์ดํ„ฐ์•ผ.
97
+
98
+ [๋Œ€๋ณธ]
99
+ {script_text[:8000]}
100
+
101
+ [์ฐธ๊ณ  ์ œ๋ชฉ]
102
+ {title_hint}
103
+
104
+ [์š”๊ตฌ์‚ฌํ•ญ]
105
+ {instruction}
106
+ """.strip()
107
+
108
+ try:
109
+ response = client.models.generate_content(model=text_model_id, contents=prompt)
110
+ text = (getattr(response, "text", "") or "").strip()
111
+ except Exception as exc:
112
+ logger.exception("์ธ๋„ค์ผ ์„œ๋ธŒ ๋ฌธ๊ตฌ ์ƒ์„ฑ ์‹คํŒจ")
113
+ raise RuntimeError("์ธ๋„ค์ผ ์„œ๋ธŒ ๋ฌธ๊ตฌ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
114
+
115
+ payload = None
116
+ try:
117
+ payload = json.loads(text)
118
+ except json.JSONDecodeError:
119
+ payload = None
120
+
121
+ if not isinstance(payload, dict):
122
+ payload = {}
123
+
124
+ merged = {**default_payload, **payload}
125
+ layout = str(merged.get("layout", "single")).lower().strip()
126
+ if layout not in {"split", "single"}:
127
+ layout = "single"
128
+ merged["layout"] = layout
129
+
130
+ if strategy_key.startswith("A."):
131
+ left_text = str(merged.get("left") or "").strip()
132
+ right_text = str(merged.get("right") or "").strip()
133
+ if not left_text:
134
+ left_text = "์œ„๊ธฐ"
135
+ if not right_text:
136
+ right_text = "๊ธฐํšŒ"
137
+ return {"left_text": left_text, "right_text": right_text}
138
+
139
+ if strategy_key.startswith("B."):
140
+ top_text = str(merged.get("top") or "").strip()
141
+ if not top_text:
142
+ top_text = "์†๋ณด"
143
+ return {"top_text": top_text}
144
+
145
+ top_text = str(merged.get("top") or "").strip()
146
+ return {"top_text": top_text, "layout": merged["layout"]}
147
 
148
  def process_scene_task(
149
  index,
 
190
  3) ํ™”๋ฉด ๋ถ„ํ• (Split Screen) ๊ธˆ์ง€. ํ•œ ์žฅ๋ฉด์œผ๋กœ๋งŒ ๊ตฌ์„ฑ.
191
  4) ์ด๋ฏธ์ง€์— ๊ธ€์ž/์ž๋ง‰ ๋„ฃ์ง€ ๋งˆ๋ผ. (ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ๊ธˆ์ง€)
192
  5) ์ตœ์ข… ์ถœ๋ ฅ์€ "ํ”„๋กฌํ”„ํŠธ ํ•œ ๋ฉ์–ด๋ฆฌ ํ…์ŠคํŠธ"๋งŒ. JSON/๋งˆํฌ๋‹ค์šด/๋ฒˆํ˜ธ/์ œ๋ชฉ ๊ธˆ์ง€.
193
+ 6) ๊ฒฐ๊ณผ๋Š” ์ด๋ฏธ์ง€ 1์žฅ์ด์–ด์•ผ ํ•˜๋ฉฐ, ์„ค๋ช…/๋ถ„์„ ํ…์ŠคํŠธ ์ถœ๋ ฅ ๊ธˆ์ง€.
194
+ 7) 16:9 (1280x720) ๋น„์œจ๋กœ ์ƒ์„ฑ.
195
 
196
  ์ด์ œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ถœ๋ ฅํ•ด.
197
  """.strip()
198
 
199
 
200
+ try:
201
+ brain_res = client.models.generate_content(
202
+ model=text_model_id,
203
+ contents=brain_prompt
204
+ )
205
+ except Exception as exc:
206
+ logger.exception("์žฅ๋ฉด ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ")
207
+ raise RuntimeError("์žฅ๋ฉด ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
208
 
209
  # brain_res.text๊ฐ€ ์—†์„ ์ˆ˜๋„ ์žˆ์–ด ๋ฐฉ์–ด
210
  final_prompt = getattr(brain_res, "text", None) or scene_text
211
  final_prompt = re.sub(r"\s+", " ", final_prompt).strip()
212
 
213
+ # 2) Painter: ์ธ๋„ค์ผ๊ณผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ชจ๋ธ ํƒ€์ž…์— ๋งž์ถฐ ์ƒ์„ฑ
214
+ uses_gemini_image = "gemini" in (image_model_id or "").lower()
215
+
216
+ last_scene_response = {"value": None}
217
+
218
+ def _render_scene_image(prompt_text):
219
+ contents = prompt_text
220
+ if reference_image is not None:
221
+ try:
222
+ from google.genai import types
223
+ import io
224
+ buf = io.BytesIO()
225
+ reference_image.save(buf, format="PNG")
226
+ ref_part = types.Part.from_bytes(data=buf.getvalue(), mime_type="image/png")
227
+ contents = [prompt_text, ref_part]
228
+ except Exception as exc:
229
+ logger.exception("์ฐธ์กฐ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์‹คํŒจ")
230
+ raise RuntimeError("์ฐธ์กฐ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
231
+
232
+ if uses_gemini_image:
233
+ try:
234
+ from google.genai import types
235
+ except Exception as exc:
236
+ logger.exception("GenerateContentConfig ๋กœ๋“œ ์‹คํŒจ")
237
+ raise RuntimeError("์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
238
+ img_res = client.models.generate_content(
239
  model=image_model_id,
240
+ contents=contents,
241
+ config=types.GenerateContentConfig(
242
+ response_modalities=["IMAGE"],
243
+ image_config=types.ImageConfig(image_size="1K")
244
+ )
245
  )
246
+ last_scene_response["value"] = img_res
247
+ return _safe_extract_image_bytes(img_res)
 
 
248
 
249
+ if hasattr(client.models, "generate_images"):
250
+ img_res = client.models.generate_images(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  model=image_model_id,
252
+ prompt=prompt_text
253
  )
254
+ last_scene_response["value"] = img_res
255
+ return _safe_extract_image_bytes(img_res)
256
+
257
+ raise RuntimeError(
258
+ "์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. "
259
+ "๋ชจ๋ธ/ํ˜ธ์ถœ๋ฐฉ์‹ ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
260
+ "Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ์€ generateContent, Imagen ๋ชจ๋ธ์€ generate_images๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค."
261
+ )
262
 
263
+ try:
264
+ img_bytes = _render_scene_image(final_prompt)
265
+ return index, final_prompt, img_bytes
266
+ except ImageExtractionError as exc:
267
+ response = last_scene_response["value"]
268
+ prompt_feedback = getattr(response, "prompt_feedback", None)
269
+ finish_reason = None
270
+ candidates = getattr(response, "candidates", None) or []
271
+ if candidates:
272
+ finish_reason = getattr(candidates[0], "finish_reason", None)
273
+ logger.exception(
274
+ "์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ถ”์ถœ ์‹คํŒจ (1์ฐจ) finish_reason=%s prompt_feedback=%s",
275
+ finish_reason,
276
+ prompt_feedback
277
+ )
278
+ if "ํ…์ŠคํŠธ๋งŒ ๋ฐ˜ํ™˜" in str(exc):
279
+ fallback_prompt = f"""
280
+ ํ•ต์‹ฌ ์žฅ๋ฉด: {scene_text}
281
+ 16:9 (1280x720). ํ•œ ์žฅ๋ฉด์œผ๋กœ๋งŒ ๊ตฌ์„ฑ. ์ด๋ฏธ์ง€ ์ƒ์„ฑ๋งŒ ์ถœ๋ ฅ.
282
+ """.strip()
283
+ try:
284
+ img_bytes = _render_scene_image(fallback_prompt)
285
+ return index, fallback_prompt, img_bytes
286
+ except ImageExtractionError:
287
+ raise
288
+ raise
289
+ except Exception as exc:
290
+ logger.exception("์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ")
291
+ if uses_gemini_image:
292
+ raise RuntimeError(
293
+ "์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. "
294
+ "๋ชจ๋ธ/ํ˜ธ์ถœ๋ฐฉ์‹ ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
295
+ "Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ์€ generateContent ๋ฐฉ์‹์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."
296
+ ) from exc
297
+ raise RuntimeError(
298
+ "์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. "
299
+ "๋ชจ๋ธ/ํ˜ธ์ถœ๋ฐฉ์‹ ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
300
+ "Imagen ๊ณ„์—ด ๋ชจ๋ธ์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜ Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ์„ ์„ ํƒํ•˜์„ธ์š”."
301
+ ) from exc
302
+
303
+ def process_thumbnail_task(
304
  index,
305
  strategy_key,
306
  strategy_text,
 
310
  text_model_id,
311
  image_model_id,
312
  aspect_ratio,
313
+ reference_image=None,
314
  ):
315
  """
316
  return (idx, final_prompt, image_bytes)
 
335
 
336
  [ํ•„์ˆ˜ ์กฐ๊ฑด]
337
  - ์ถœ๋ ฅ์€ ๋ฌด์กฐ๊ฑด ํ•œ๊ตญ์–ด๋งŒ. ์˜์–ด/๋กœ๋งˆ์ž ๊ธˆ์ง€.
 
 
338
  - ํŠน์ • ๊ตญ๊ฐ€ ์ƒ์ง• ์ž๋™์ƒ์„ฑ ๊ธˆ์ง€: ํƒœ๊ทน๊ธฐ, ์ฒญ์™€๋Œ€, ๊ตญํšŒ์˜์‚ฌ๋‹น, ๋Œ€ํ†ต๋ น, ํ›ˆ์žฅ/ํœ˜์žฅ, ์„ ๊ฑฐ ํฌ์Šคํ„ฐ, ๊ตญ๊ธฐ๋ฅ˜ ์ „๋ถ€ ๊ธˆ์ง€.
339
  - ์ •์น˜์ธ์„ ์‹ค์กด ์ธ๋ฌผ์ฒ˜๋Ÿผ ํŠน์ •ํ•˜์ง€ ๋ง๊ณ , ์ต๋ช… ์บ๋ฆญํ„ฐ/์‹ค๋ฃจ์—ฃ/์ƒ์ง•์  ์—ฐ์ถœ๋กœ ์ฒ˜๋ฆฌ.
340
  - ํ™”๋ฉด ๋ถ„ํ• ์€ ์ „๋žต์— ํฌํ•จ๋œ ๊ฒฝ์šฐ์—๋งŒ ํ—ˆ์šฉ.
341
+ - ํ™”๋ฉด๋น„: 16:9 (1280x720)
342
  - ์ตœ์ข… ์ถœ๋ ฅ์€ ํ”„๋กฌํ”„ํŠธ ํ…์ŠคํŠธ 1๊ฐœ๋งŒ. (JSON/๋งˆํฌ๋‹ค์šด/๋ฒˆํ˜ธ ๊ธˆ์ง€)
343
  """.strip()
344
 
345
+ try:
346
+ brain_res = client.models.generate_content(model=text_model_id, contents=brain_prompt)
347
+ scene_prompt = (getattr(brain_res, "text", "") or "").strip()
348
+ except Exception as exc:
349
+ logger.exception("์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ")
350
+ raise RuntimeError("์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
351
+
352
+ overlay_texts = _generate_thumbnail_texts(strategy_key, script_text, title_hint, client, text_model_id)
353
+
354
+ common_rules = f"""
355
+ - ํฌ๋งท: 16:9 (1280x720). ์ „์ฒด ํ”„๋ ˆ์ž„ ์œ ์ง€, ํฌ๋กญ/์ž˜๋ฆผ ๊ธˆ์ง€.
356
+ - ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ๋ฐ˜๋“œ์‹œ ํฌํ•จ.
357
+ - ํฐํŠธ: ๊ตต์€ ๊ณ ๋”•, ํฐ ๊ธ€์”จ + ๊ฒ€์ • ์ŠคํŠธ๋กœํฌ, ๋†’์€ ๋Œ€๋น„.
358
+ - ์ƒ๋‹จ: ์งง์€ ์„œ๋ธŒ ๋ฌธ๊ตฌ.
359
+ - ํ•˜๋‹จ ์ค‘์•™: ๋ฉ”์ธ ํƒ€์ดํ‹€ \"{title_hint}\" ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ.
360
+ """.strip()
361
 
362
+ if strategy_key.startswith("A."):
363
+ overlay_rules = f"""
364
+ - ์ขŒ์ธก ์ƒ๋‹จ ๋ฌธ๊ตฌ: \"{overlay_texts['left_text']}\".
365
+ - ์šฐ์ธก ์ƒ๋‹จ ๋ฌธ๊ตฌ: \"{overlay_texts['right_text']}\".
366
+ - ํ•˜๋‹จ ์ค‘์•™ ๋ฉ”์ธ ํƒ€์ดํ‹€: \"{title_hint}\".
367
+ """.strip()
368
+ elif strategy_key.startswith("B."):
369
+ overlay_rules = f"""
370
+ - ์ƒ๋‹จ ์„œ๋ธŒ ๋ฌธ๊ตฌ: \"{overlay_texts['top_text']}\".
371
+ - ํ•˜๋‹จ ์ค‘์•™ ๋ฉ”์ธ ํƒ€์ดํ‹€: \"{title_hint}\".
372
+ """.strip()
373
+ else:
374
+ layout_text = "Split Screen" if overlay_texts["layout"] == "split" else "Single Scene"
375
+ overlay_rules = f"""
376
+ - ๊ตฌ์„ฑ: {layout_text}.
377
+ - ์ƒ๋‹จ ์„œ๋ธŒ ๋ฌธ๊ตฌ: \"{overlay_texts['top_text']}\".
378
+ - ํ•˜๋‹จ ์ค‘์•™ ๋ฉ”์ธ ๏ฟฝ๏ฟฝ์ดํ‹€: \"{title_hint}\".
379
+ """.strip()
380
 
381
+ final_prompt = f"""
382
+ [์ „๋žต ์„ค๋ช…]
383
+ {strategy_text}
 
 
 
 
 
 
 
384
 
385
+ [์žฅ๋ฉด ๊ตฌ์„ฑ]
386
+ {scene_prompt}
387
+
388
+ [๊ณตํ†ต ๊ทœ์น™]
389
+ {common_rules}
390
+
391
+ [ํ…์ŠคํŠธ ๋ฐฐ์น˜]
392
+ {overlay_rules}
393
+ """.strip()
394
+
395
+ # 2) Painter: ๋ชจ๋ธ ํŠน์„ฑ์— ๋งž๊ฒŒ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
396
+ uses_gemini_image = "gemini" in (image_model_id or "").lower()
397
+
398
+ last_response = {"value": None}
399
+
400
+ def _render_image(prompt_text):
401
+ if uses_gemini_image:
402
+ try:
403
+ from google.genai import types
404
+ except Exception as exc:
405
+ logger.exception("GenerateContentConfig ๋กœ๋“œ ์‹คํŒจ")
406
+ raise RuntimeError("์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
407
  img_res = client.models.generate_content(
408
  model=image_model_id,
409
+ contents=prompt_text,
410
+ config=types.GenerateContentConfig(
411
+ response_modalities=["IMAGE"],
412
+ image_config=types.ImageConfig(image_size="1K")
413
+ )
414
  )
415
+ last_response["value"] = img_res
416
+ return _safe_extract_image_bytes(img_res)
417
+ if hasattr(client.models, "generate_images"):
418
+ img_res = client.models.generate_images(
419
+ model=image_model_id,
420
+ prompt=prompt_text
421
+ )
422
+ last_response["value"] = img_res
423
+ return _safe_extract_image_bytes(img_res)
424
+ raise RuntimeError(
425
+ "์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. "
426
+ "๋ชจ๋ธ/ํ˜ธ์ถœ๋ฐฉ์‹ ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
427
+ "Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ์€ generateContent, Imagen ๋ชจ๋ธ์€ generate_images๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค."
428
+ )
429
 
430
+ try:
431
+ img_bytes = _render_image(final_prompt)
432
+ return index, final_prompt, img_bytes
433
+ except ImageExtractionError as exc:
434
+ response = last_response["value"]
435
+ prompt_feedback = getattr(response, "prompt_feedback", None)
436
+ finish_reason = None
437
+ candidates = getattr(response, "candidates", None) or []
438
+ if candidates:
439
+ finish_reason = getattr(candidates[0], "finish_reason", None)
440
+ logger.exception(
441
+ "์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ถ”์ถœ ์‹คํŒจ (1์ฐจ) finish_reason=%s prompt_feedback=%s",
442
+ finish_reason,
443
+ prompt_feedback
444
+ )
445
+ if not strategy_key.startswith("B."):
446
+ raise
447
+ fallback_prompt = f"""
448
+ 16:9 ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ. ํ˜„์‹ค ๋‹คํ ๋‰ด์Šค ์‚ฌ์ง„ ๋ฐฐ๊ฒฝ.
449
+ ์ „๊ฒฝ์— 2D ์Šคํ‹ฑ๋งจ์ด ์ถฉ๊ฒฉ/๊ฐ•์กฐ ๋ฆฌ์•ก์…˜.
450
+ ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ํฌํ•จ: ์ƒ๋‹จ \"{overlay_texts.get('top_text', '')}\",
451
+ ํ•˜๋‹จ ์ค‘์•™ \"{title_hint}\". ํฐ ๊ธ€์”จ + ๊ฒ€์ • ์ŠคํŠธ๋กœํฌ, ๊ตต์€ ๊ณ ๋”•.
452
+ """.strip()
453
+ try:
454
+ img_bytes = _render_image(fallback_prompt)
455
+ return index, fallback_prompt, img_bytes
456
+ except ImageExtractionError as retry_exc:
457
+ logger.exception("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ถ”์ถœ ์‹คํŒจ (ํด๋ฐฑ)")
458
+ raise retry_exc from exc
459
+ except Exception as exc:
460
+ logger.exception("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ")
461
+ if uses_gemini_image:
462
+ raise RuntimeError(
463
+ "์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. "
464
+ "๋ชจ๋ธ/ํ˜ธ์ถœ๋ฐฉ์‹ ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
465
+ "Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ์€ generateContent ๋ฐฉ์‹์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."
466
+ ) from exc
467
+ raise RuntimeError(
468
+ "์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. "
469
+ "๋ชจ๋ธ/ํ˜ธ์ถœ๋ฐฉ์‹ ๋ถˆ์ผ์น˜์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "
470
+ "Imagen ๊ณ„์—ด ๋ชจ๋ธ์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜ Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ์„ ์„ ํƒํ•˜์„ธ์š”."
471
+ ) from exc
utils.py CHANGED
@@ -20,4 +20,19 @@ def group_sentences(text, target_sec=10):
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"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"
24
+
25
+ def make_scene_snippet(text, head_len=10, tail_len=10, max_len=60):
26
+ clean_text = " ".join((text or "").strip().split())
27
+ clean_text = re.sub(r'[\\/*?:"<>|]', "", clean_text)
28
+ clean_text = re.sub(r"[^0-9A-Za-z๊ฐ€-ํžฃ\s.~]", " ", clean_text)
29
+ clean_text = re.sub(r"[.]{2,}", ".", clean_text)
30
+ clean_text = re.sub(r"\s+", " ", clean_text).strip()
31
+ if not clean_text:
32
+ return "scene"
33
+ if len(clean_text) <= head_len + tail_len + 1:
34
+ snippet = clean_text
35
+ else:
36
+ snippet = f"{clean_text[:head_len]}~{clean_text[-tail_len:]}"
37
+ snippet = re.sub(r"\s+", " ", snippet).strip()
38
+ return snippet[:max_len].rstrip(" .")