Spaces:
Sleeping
Sleeping
Magenta49 commited on
Commit ยท
d4165da
1
Parent(s): 8ff9a44
Add knowledge-issue news style
Browse files- .github/workflows/deploy_to_hf.yml +22 -0
- app.py +235 -118
- config_style.py +32 -36
- logic_image.py +342 -89
- 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 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 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("์ฅ๋ฉด๋น ์๊ฐ(์ด)",
|
| 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(
|
| 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
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 329 |
|
| 330 |
-
|
| 331 |
-
st.info("SEO ๋ถ์์ ๋จผ์ ์คํํ๋ฉด ์ธ๋ค์ผ ์์ฑ ๋ฒํผ์ด ๋ํ๋ฉ๋๋ค.")
|
| 332 |
-
else:
|
| 333 |
-
strat_keys = list(THUMBNAIL_STRATEGIES.keys())
|
| 334 |
-
sel_strat = st.selectbox("์ธ๋ค์ผ ์ ๋ต ์ ํ", strat_keys, index=0)
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
)
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
try:
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
try:
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 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. ํ์ด๋ธ๋ฆฌ๋
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 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.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 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 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# ํน์ response.image ๊ฐ์ ํํ๋ก ์ค๋ ๊ฒฝ์ฐ
|
| 32 |
for key in ["image", "images", "data"]:
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 93 |
-
|
| 94 |
-
|
| 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 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
model=image_model_id,
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
except:
|
| 115 |
-
img_bytes = None
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 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 |
-
|
| 138 |
)
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
- ํ๋ฉด๋น:
|
| 185 |
- ์ต์ข
์ถ๋ ฅ์ ํ๋กฌํํธ ํ
์คํธ 1๊ฐ๋ง. (JSON/๋งํฌ๋ค์ด/๋ฒํธ ๊ธ์ง)
|
| 186 |
""".strip()
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
img_res = client.models.generate_content(
|
| 210 |
model=image_model_id,
|
| 211 |
-
contents=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
)
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(" .")
|