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