Spaces:
Sleeping
Sleeping
| # 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 |