youtube_auto_image1 / logic_image.py
PLXR's picture
Update logic_image.py
a0c3aea verified
# logic_image.py
import json
import logging
import re
logger = logging.getLogger(__name__)
class ImageExtractionError(RuntimeError):
"""Raised when image bytes cannot be extracted from a model response."""
def _summarize_response_structure(response):
if response is None:
return "response=None"
summary = {
"type": type(response).__name__,
"has_candidates": hasattr(response, "candidates"),
"has_content": hasattr(response, "content"),
"has_image": hasattr(response, "image"),
"has_images": hasattr(response, "images"),
"has_data": hasattr(response, "data"),
}
return ", ".join(f"{k}={v}" for k, v in summary.items())
def _get_style_prompt(selected_style, custom_style_input, style_definitions):
if selected_style == "์ง์ ‘ ์ž…๋ ฅ":
return (custom_style_input or "").strip()
v = style_definitions.get(selected_style, "")
if isinstance(v, dict):
return (v.get("prompt") or "").strip()
return str(v).strip()
def _safe_extract_image_bytes(response):
cands = getattr(response, "candidates", None) or []
if cands:
for cand in cands:
content = getattr(cand, "content", None)
parts = getattr(content, "parts", None) or []
for p in parts:
inline = getattr(p, "inline_data", None)
data = getattr(inline, "data", None) if inline else None
if data:
return data
text_parts = [getattr(p, "text", None) for p in parts if getattr(p, "text", None)]
if text_parts:
snippet = re.sub(r"\s+", " ", " ".join(text_parts)).strip()[:200]
raise ImageExtractionError(f"ํ…์ŠคํŠธ๋งŒ ๋ฐ˜ํ™˜๋˜์–ด ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ์ผ๋ถ€: {snippet}")
raise ImageExtractionError("์ด๋ฏธ์ง€ ํŒŒํŠธ๊ฐ€ ์—†์–ด bytes๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
for key in ["image", "images", "data"]:
v = getattr(response, key, None)
if isinstance(v, (bytes, bytearray)):
return bytes(v)
summary = _summarize_response_structure(response)
raise ImageExtractionError(f"์ด๋ฏธ์ง€ bytes ์ถ”์ถœ ์‹คํŒจ: {summary}")
def _generate_thumbnail_texts(strategy_key, script_text, title_hint, client, text_model_id):
default_payload = {"top": "", "layout": "single", "left": "", "right": ""}
if strategy_key.startswith("A."):
instruction = """
๋Œ€๋ณธ์„ ์ฐธ๊ณ ํ•ด์„œ ์ธ๋„ค์ผ์šฉ ์งง์€ ๋ฌธ๊ตฌ 2๊ฐœ๋ฅผ ์ž‘์„ฑํ•ด.
- ์ขŒ์ธก ์ƒ๋‹จ: ๋ถ€์ •์ /๊ณผ๊ฑฐ/์•ฝ์ž ๋ถ„์œ„๊ธฐ์˜ ์งง์€ ๋ฌธ๊ตฌ (6์ž ์ด๋‚ด)
- ์šฐ์ธก ์ƒ๋‹จ: ๊ธ์ •์ /๋ฏธ๋ž˜/๊ฐ•์ž ๋ถ„์œ„๊ธฐ์˜ ์งง์€ ๋ฌธ๊ตฌ (6์ž ์ด๋‚ด)
์ถœ๋ ฅ ํ˜•์‹(JSON ONLY):
{"left": "...", "right": "..."}
""".strip()
elif strategy_key.startswith("B."):
instruction = """
๋Œ€๋ณธ์„ ์ฐธ๊ณ ํ•ด์„œ ์ธ๋„ค์ผ ์ƒ๋‹จ์— ๋“ค์–ด๊ฐˆ ์„œ๋ธŒ ๋ฌธ๊ตฌ๋ฅผ ์ž‘์„ฑํ•ด.
- "์†๋ณด/๊ธด๊ธ‰/๋‹จ๋…" ๊ฐ™์€ ํ†ค
- 6์ž ์ด๋‚ด
์ถœ๋ ฅ ํ˜•์‹(JSON ONLY):
{"top": "..."}
""".strip()
else:
instruction = """
๋Œ€๋ณธ์„ ์ฐธ๊ณ ํ•ด์„œ ์ธ๋„ค์ผ ์ƒ๋‹จ์— ๋“ค์–ด๊ฐˆ ์„œ๋ธŒ ๋ฌธ๊ตฌ๋ฅผ ์ž‘์„ฑํ•ด.
- 6์ž ์ด๋‚ด (์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)
๊ทธ๋ฆฌ๊ณ  ๊ตฌ์„ฑ ์œ ํ˜•์„ ์„ ํƒํ•ด.
- ๋น„๊ต/๋Œ€์กฐ๋ฉด split
- ์Šคํ† ๋ฆฌ ํ๋ฆ„์ด๋ฉด single
์ถœ๋ ฅ ํ˜•์‹(JSON ONLY):
{"top": "...", "layout": "split|single"}
""".strip()
prompt = f"""
๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ ์นดํ”ผ๋ผ์ดํ„ฐ์•ผ.
[๋Œ€๋ณธ]
{script_text[:8000]}
[์ฐธ๊ณ  ์ œ๋ชฉ]
{title_hint}
[์š”๊ตฌ์‚ฌํ•ญ]
{instruction}
""".strip()
try:
response = client.models.generate_content(model=text_model_id, contents=prompt)
text = (getattr(response, "text", "") or "").strip()
except Exception as exc:
logger.exception("์ธ๋„ค์ผ ์„œ๋ธŒ ๋ฌธ๊ตฌ ์ƒ์„ฑ ์‹คํŒจ")
raise RuntimeError("์ธ๋„ค์ผ ์„œ๋ธŒ ๋ฌธ๊ตฌ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
payload = None
try:
payload = json.loads(text)
except json.JSONDecodeError:
payload = None
if not isinstance(payload, dict):
payload = {}
merged = {**default_payload, **payload}
layout = str(merged.get("layout", "single")).lower().strip()
if layout not in {"split", "single"}:
layout = "single"
merged["layout"] = layout
if strategy_key.startswith("A."):
left_text = str(merged.get("left") or "").strip()
right_text = str(merged.get("right") or "").strip()
if not left_text:
left_text = "์œ„๊ธฐ"
if not right_text:
right_text = "๊ธฐํšŒ"
return {"left_text": left_text, "right_text": right_text}
if strategy_key.startswith("B."):
top_text = str(merged.get("top") or "").strip()
if not top_text:
top_text = "์†๋ณด"
return {"top_text": top_text}
top_text = str(merged.get("top") or "").strip()
return {"top_text": top_text, "layout": merged["layout"]}
def _generate_video_prompt(scene_text, image_prompt, client, text_model_id):
prompt = f"""
๋„ˆ๋Š” Google Flow(Veo3)์šฉ ๋น„๋””์˜ค ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋‹ค.
์•„๋ž˜ ์žฅ๋ฉด ์„ค๋ช…๊ณผ ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ, 1๊ฐœ ์žฅ๋ฉด์œผ๋กœ ์งง์€ ๋น„๋””์˜ค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ผ.
[์žฅ๋ฉด ํ…์ŠคํŠธ]
{scene_text}
[์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ]
{image_prompt}
[๊ทœ์น™]
- ์ถœ๋ ฅ์€ ํ•œ๊ตญ์–ด๋งŒ.
- ์˜์ƒ์€ ํ•˜๋‚˜์˜ ์žฅ๋ฉด์œผ๋กœ ๊ตฌ์„ฑ.
- ์นด๋ฉ”๋ผ ๋™์ž‘(ํŒจ๋‹/์คŒ/ํŠธ๋ž˜ํ‚น)๊ณผ ํ”ผ์‚ฌ์ฒด ์›€์ง์ž„์„ ํฌํ•จ.
- ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด/์ž๋ง‰/๋กœ๊ณ  ๊ธˆ์ง€.
- ์ตœ์ข… ์ถœ๋ ฅ์€ ํ”„๋กฌํ”„ํŠธ ํ…์ŠคํŠธ 1๊ฐœ๋งŒ. JSON/๋งˆํฌ๋‹ค์šด/๋ฒˆํ˜ธ ๊ธˆ์ง€.
""".strip()
try:
response = client.models.generate_content(model=text_model_id, contents=prompt)
return (getattr(response, "text", "") or "").strip()
except Exception as exc:
logger.exception("๋น„๋””์˜ค ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ")
return (scene_text or "").strip()
def process_scene_task(
index,
scene,
selected_style,
custom_style_input,
client,
text_model_id,
image_model_id,
aspect_ratio,
reference_image=None,
):
scene_text = (scene.get("text") or "").strip()
full_script = (scene.get("full_script") or "").strip()
from config_style import STYLE_DEFINITIONS
style_prompt = _get_style_prompt(selected_style, custom_style_input, STYLE_DEFINITIONS)
# ================= [๊ฐ•๋ ฅ ์ˆ˜์ •๋œ ๋ถ€๋ถ„] =================
# '๋งŒํ™” ์นธ', 'ํ…Œ๋‘๋ฆฌ'๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๊ธˆ์ง€ํ•˜๋Š” ๊ทœ์น™ ์ถ”๊ฐ€
brain_prompt = f"""
๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์˜์ƒ์šฉ '์žฅ๋ฉด ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ'๋ฅผ ๋งŒ๋“œ๋Š” ์ „๋ฌธ๊ฐ€์•ผ.
์•„๋ž˜ ๋Œ€๋ณธ์„ ๋ณด๊ณ , ๋”ฑ "ํ•œ ์žฅ๋ฉด"์„ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋งŒ๋“ค์–ด.
[์Šคํƒ€์ผ ์ง€์‹œ]
{style_prompt}
[ํ™”๋ฉด๋น„]
{aspect_ratio}
[๋Œ€๋ณธ]
{scene_text}
[์ „์ฒด ํ๋ฆ„ ์š”์•ฝ ์ฐธ๊ณ ]
{full_script[:1200]}
[์ ˆ๋Œ€ ๊ทœ์น™]
1) ์ถœ๋ ฅ์€ ๋ฌด์กฐ๊ฑด ํ•œ๊ตญ์–ด๋กœ๋งŒ ์ž‘์„ฑ.
2) ๋Œ€๋ณธ์˜ ์˜๋ฏธ๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•˜์ง€ ๋งˆ๋ผ.
3) ํ™”๋ฉด ๋ถ„ํ• (Split Screen), ๋งŒํ™” ์นธ(Panel) ๋‚˜๋ˆ„๊ธฐ ๊ธˆ์ง€.
4) ์ด๋ฏธ์ง€์— ๊ธ€์ž, ์ž๋ง‰, ๋งํ’์„  ์ ˆ๋Œ€ ๋„ฃ์ง€ ๋งˆ๋ผ. (No Text, No Speech Bubble)
5) ์ตœ์ข… ์ถœ๋ ฅ์€ "ํ”„๋กฌํ”„ํŠธ ํ•œ ๋ฉ์–ด๋ฆฌ ํ…์ŠคํŠธ"๋งŒ.
6) ์„ค๋ช…์ด๋‚˜ ๋ถ„์„ ํ…์ŠคํŠธ ์ถœ๋ ฅ ๊ธˆ์ง€.
7) 16:9 (1280x720) ๋น„์œจ๋กœ ์ƒ์„ฑ.
8) ๋Œ€๋ณธ์— ๊ตฌ์ฒด์  ๋Œ€์ƒ์ด ์žˆ์œผ๋ฉด ์‹œ๊ฐ์  ๋ฌ˜์‚ฌ๋กœ ํฌํ•จ.
9) ์ด๋ฏธ์ง€์˜ ์ƒํ•˜์ขŒ์šฐ ๋ชจ๋“  ๊ฐ€์žฅ์ž๋ฆฌ์— ํ…Œ๋‘๋ฆฌ(Border), ํ”„๋ ˆ์ž„, ์—ฌ๋ฐฑ์„ ์ ˆ๋Œ€ ๋งŒ๋“ค์ง€ ๋งˆ๋ผ. (Borderless)
10) ๋งŒํ™”์ฑ…์ด๋‚˜ ์›นํˆฐ ํ˜•์‹์ฒ˜๋Ÿผ ๋„ค๋ชจ๋‚œ ์นธ ์•ˆ์— ๊ทธ๋ฆผ์„ ๊ฐ€๋‘์ง€ ๋ง๊ณ , ์บ”๋ฒ„์Šค ์ „์ฒด๋ฅผ ํ•˜๋‚˜์˜ ๊ทธ๋ฆผ์œผ๋กœ ๊ฝ‰ ์ฑ„์›Œ๋ผ (Full Shot).
11) ํ•˜๋‹จ๋ถ€๋‚˜ ์ƒ๋‹จ์— ๊ฒ€์€ ๋ (Letterbox)๋‚˜ ํฐ์ƒ‰ ๊ณต๋ฐฑ์„ ์ ˆ๋Œ€ ๋งŒ๋“ค์ง€ ๋งˆ๋ผ.
์ด์ œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ถœ๋ ฅํ•ด.
""".strip()
# ================= [๊ฐ•๋ ฅ ์ˆ˜์ •๋œ ๋ถ€๋ถ„ ๋] =================
try:
brain_res = client.models.generate_content(
model=text_model_id,
contents=brain_prompt
)
except Exception as exc:
logger.exception("์žฅ๋ฉด ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ")
raise RuntimeError("์žฅ๋ฉด ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
final_prompt = getattr(brain_res, "text", None) or scene_text
final_prompt = re.sub(r"\s+", " ", final_prompt).strip()
uses_gemini_image = "gemini" in (image_model_id or "").lower()
last_scene_response = {"value": None}
def _render_scene_image(prompt_text):
contents = prompt_text
if reference_image is not None:
try:
from google.genai import types
import io
buf = io.BytesIO()
reference_image.save(buf, format="PNG")
ref_part = types.Part.from_bytes(data=buf.getvalue(), mime_type="image/png")
contents = [prompt_text, ref_part]
except Exception as exc:
logger.exception("์ฐธ์กฐ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์‹คํŒจ")
raise RuntimeError("์ฐธ์กฐ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
if uses_gemini_image:
try:
from google.genai import types
except Exception as exc:
logger.exception("GenerateContentConfig ๋กœ๋“œ ์‹คํŒจ")
raise RuntimeError("์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
img_res = client.models.generate_content(
model=image_model_id,
contents=contents,
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(image_size="1K", aspect_ratio=aspect_ratio)
)
)
last_scene_response["value"] = img_res
return _safe_extract_image_bytes(img_res)
if hasattr(client.models, "generate_images"):
img_res = client.models.generate_images(
model=image_model_id,
prompt=prompt_text
)
last_scene_response["value"] = img_res
return _safe_extract_image_bytes(img_res)
raise RuntimeError("์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: ๋ชจ๋ธ ํ˜ธํ™˜์„ฑ ํ™•์ธ ํ•„์š”")
try:
img_bytes = _render_scene_image(final_prompt)
video_prompt = _generate_video_prompt(scene_text, final_prompt, client, text_model_id)
return index, final_prompt, img_bytes, video_prompt
except ImageExtractionError as exc:
response = last_scene_response["value"]
logger.exception("์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ถ”์ถœ ์‹คํŒจ (1์ฐจ)")
if "ํ…์ŠคํŠธ๋งŒ ๋ฐ˜ํ™˜" in str(exc):
fallback_prompt = f"""
ํ•ต์‹ฌ ์žฅ๋ฉด: {scene_text}
16:9 ํ…Œ๋‘๋ฆฌ ์—†๋Š” ์ „์ฒด ํ™”๋ฉด(Borderless Full Shot). ์—ฌ๋ฐฑ ์—†์Œ. ์ด๋ฏธ์ง€ ์ƒ์„ฑ๋งŒ ์ถœ๋ ฅ.
""".strip()
try:
img_bytes = _render_scene_image(fallback_prompt)
video_prompt = _generate_video_prompt(scene_text, fallback_prompt, client, text_model_id)
return index, fallback_prompt, img_bytes, video_prompt
except ImageExtractionError:
raise
raise
except Exception as exc:
logger.exception("์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ")
raise RuntimeError("์žฅ๋ฉด ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ") from exc
def process_thumbnail_task(
index,
strategy_key,
strategy_text,
script_text,
title_hint,
client,
text_model_id,
image_model_id,
aspect_ratio,
reference_image=None,
):
# ================= [๊ฐ•๋ ฅ ์ˆ˜์ •๋œ ๋ถ€๋ถ„] =================
brain_prompt = f"""
๋„ˆ๋Š” ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ์ „๋ฌธ๊ฐ€๋‹ค.
์•„๋ž˜ ๋Œ€๋ณธ๊ณผ ์ธ๋„ค์ผ ์ „๋žต์„ ๋ฐ”ํƒ•์œผ๋กœ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ชจ๋ธ์— ๋„ฃ์„ 'ํ•œ ์žฅ์˜ ์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ'๋ฅผ ์ž‘์„ฑํ•˜๋ผ.
[์ „๋žต ํ‚ค]
{strategy_key}
[์ „๋žต ๋‚ด์šฉ]
{strategy_text}
[๋Œ€๋ณธ]
{script_text[:8000]}
[์ฐธ๊ณ  ์ œ๋ชฉ(๊ทธ๋ฆฌ์ง€ ๋ง ๊ฒƒ)]
{title_hint}
[ํ•„์ˆ˜ ์กฐ๊ฑด]
- ์ถœ๋ ฅ์€ ๋ฌด์กฐ๊ฑด ํ•œ๊ตญ์–ด๋งŒ. ์˜์–ด/๋กœ๋งˆ์ž ๊ธˆ์ง€.
- ํŠน์ • ๊ตญ๊ฐ€ ์ƒ์ง• ์ž๋™์ƒ์„ฑ ๊ธˆ์ง€: ํƒœ๊ทน๊ธฐ, ์ฒญ์™€๋Œ€ ๋“ฑ.
- ์ •์น˜์ธ ๋“ฑ ์‹ค์กด ์ธ๋ฌผ์€ ์ต๋ช… ์บ๋ฆญํ„ฐ/์‹ค๋ฃจ์—ฃ์œผ๋กœ ์ฒ˜๋ฆฌ.
- ํ™”๋ฉด๋น„: 16:9 (1280x720)
- ์ด๋ฏธ์ง€๋Š” ์บ”๋ฒ„์Šค ์ „์ฒด๋ฅผ ๊ฝ‰ ์ฑ„์›Œ์•ผ ํ•จ.
- ๋งŒํ™”์ฑ… ํ”„๋ ˆ์ž„, ํ…Œ๋‘๋ฆฌ, ๋งํ’์„ , ์—ฌ๋ฐฑ์„ ์ ˆ๋Œ€ ๊ทธ๋ฆฌ์ง€ ๋งˆ๋ผ (Borderless).
- ์ตœ์ข… ์ถœ๋ ฅ์€ ํ”„๋กฌํ”„ํŠธ ํ…์ŠคํŠธ 1๊ฐœ๋งŒ.
- ๋Œ€๋ณธ์— ๊ตฌ์ฒด์  ๋Œ€์ƒ์ด ์žˆ์œผ๋ฉด ์‹œ๊ฐ์ ์œผ๋กœ ํฌํ•จ.
""".strip()
# ================= [๊ฐ•๋ ฅ ์ˆ˜์ •๋œ ๋ถ€๋ถ„ ๋] =================
try:
brain_res = client.models.generate_content(model=text_model_id, contents=brain_prompt)
scene_prompt = (getattr(brain_res, "text", "") or "").strip()
except Exception as exc:
logger.exception("์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ")
raise RuntimeError("์ธ๋„ค์ผ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
overlay_texts = _generate_thumbnail_texts(strategy_key, script_text, title_hint, client, text_model_id)
# ================= [๊ฐ•๋ ฅ ์ˆ˜์ •๋œ ๋ถ€๋ถ„] =================
common_rules = f"""
- ํฌ๋งท: 16:9 (1280x720).
- ํ…Œ๋‘๋ฆฌ(Border)๋‚˜ ํ”„๋ ˆ์ž„ ์—†์ด ์ด๋ฏธ์ง€๊ฐ€ ์บ”๋ฒ„์Šค ๋๊นŒ์ง€ ๊ฝ‰ ์ฐจ์•ผ ํ•œ๋‹ค (Full Bleed).
- ์ƒํ•˜์ขŒ์šฐ์— ํฐ์ƒ‰ ์—ฌ๋ฐฑ์ด๋‚˜ ๊ฒ€์€ ๋ ˆํ„ฐ๋ฐ•์Šค๋ฅผ ์ ˆ๋Œ€ ๋งŒ๋“ค์ง€ ๋งˆ๋ผ.
- ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด ๋ฐ˜๋“œ์‹œ ํฌํ•จ.
- ํฐํŠธ: ๊ตต์€ ๊ณ ๋”•, ํฐ ๊ธ€์”จ + ๊ฒ€์ • ์ŠคํŠธ๋กœํฌ, ๋†’์€ ๋Œ€๋น„.
- ๋ฉ”์ธ ํƒ€์ดํ‹€ \"{title_hint}\"๋Š” ์ค‘๋‹จ ์ด์ƒ ์œ„์น˜์— ๋ฐฐ์น˜.
""".strip()
# ================= [๊ฐ•๋ ฅ ์ˆ˜์ •๋œ ๋ถ€๋ถ„ ๋] =================
if strategy_key.startswith("A."):
overlay_rules = f"""
- ์ขŒ์ธก ์ƒ๋‹จ ๋ฌธ๊ตฌ: \"{overlay_texts['left_text']}\".
- ์šฐ์ธก ์ƒ๋‹จ ๋ฌธ๊ตฌ: \"{overlay_texts['right_text']}\".
- ๋ฉ”์ธ ํƒ€์ดํ‹€: \"{title_hint}\"๋Š” ์ค‘๋‹จ ๋˜๋Š” ์ƒ๋‹จ์— ๋ฐฐ์น˜.
""".strip()
elif strategy_key.startswith("B."):
overlay_rules = f"""
- ์ƒ๋‹จ ์„œ๋ธŒ ๋ฌธ๊ตฌ: \"{overlay_texts['top_text']}\".
- ๋ฉ”์ธ ํƒ€์ดํ‹€: \"{title_hint}\"๋Š” ์ค‘๋‹จ ๋˜๋Š” ์ƒ๋‹จ์— ๋ฐฐ์น˜.
""".strip()
else:
layout_text = "Split Screen" if overlay_texts["layout"] == "split" else "Single Scene"
overlay_rules = f"""
- ๊ตฌ์„ฑ: {layout_text}.
- ์ƒ๋‹จ ์„œ๋ธŒ ๋ฌธ๊ตฌ: \"{overlay_texts['top_text']}\".
- ๋ฉ”์ธ ํƒ€์ดํ‹€: \"{title_hint}\"๋Š” ์ค‘๋‹จ ๋˜๋Š” ์ƒ๋‹จ์— ๋ฐฐ์น˜.
""".strip()
final_prompt = f"""
[์ „๋žต ์„ค๋ช…]
{strategy_text}
[์žฅ๋ฉด ๊ตฌ์„ฑ]
{scene_prompt}
[๊ณตํ†ต ๊ทœ์น™]
{common_rules}
[ํ…์ŠคํŠธ ๋ฐฐ์น˜]
{overlay_rules}
""".strip()
uses_gemini_image = "gemini" in (image_model_id or "").lower()
last_response = {"value": None}
def _render_image(prompt_text):
if uses_gemini_image:
try:
from google.genai import types
except Exception as exc:
logger.exception("GenerateContentConfig ๋กœ๋“œ ์‹คํŒจ")
raise RuntimeError("์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.") from exc
img_res = client.models.generate_content(
model=image_model_id,
contents=prompt_text,
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(image_size="1K", aspect_ratio=aspect_ratio)
)
)
last_response["value"] = img_res
return _safe_extract_image_bytes(img_res)
if hasattr(client.models, "generate_images"):
img_res = client.models.generate_images(
model=image_model_id,
prompt=prompt_text
)
last_response["value"] = img_res
return _safe_extract_image_bytes(img_res)
raise RuntimeError("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: ๋ชจ๋ธ ํ˜ธํ™˜์„ฑ ํ™•์ธ ํ•„์š”")
try:
img_bytes = _render_image(final_prompt)
return index, final_prompt, img_bytes
except ImageExtractionError as exc:
response = last_response["value"]
logger.exception("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ถ”์ถœ ์‹คํŒจ (1์ฐจ)")
if not strategy_key.startswith("B."):
raise
fallback_prompt = f"""
16:9 ์œ ํŠœ๋ธŒ ์ธ๋„ค์ผ. ํ…Œ๋‘๋ฆฌ ์—†๋Š” ๊ฝ‰ ์ฐฌ ํ™”๋ฉด(Borderless Full Bleed).
์ „๊ฒฝ์— 2D ์Šคํ‹ฑ๋งจ ๋ฆฌ์•ก์…˜.
ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด: ์ƒ๋‹จ \"{overlay_texts.get('top_text', '')}\", ํ•˜๋‹จ ์ค‘์•™ \"{title_hint}\".
""".strip()
try:
img_bytes = _render_image(fallback_prompt)
return index, fallback_prompt, img_bytes
except ImageExtractionError as retry_exc:
logger.exception("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ถ”์ถœ ์‹คํŒจ (ํด๋ฐฑ)")
raise retry_exc from exc
except Exception as exc:
logger.exception("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ")
raise RuntimeError("์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ") from exc