Spaces:
Running
Running
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import streamlit as st
|
3 |
+
import replicate
|
4 |
+
from PIL import Image
|
5 |
+
import io
|
6 |
+
import base64
|
7 |
+
import tempfile
|
8 |
+
|
9 |
+
# ํ์ด์ง ์ค์
|
10 |
+
st.set_page_config(
|
11 |
+
page_title="AI Video Generator",
|
12 |
+
page_icon="๐ฌ",
|
13 |
+
layout="wide"
|
14 |
+
)
|
15 |
+
|
16 |
+
# ์คํ์ผ ์ ์ฉ
|
17 |
+
st.markdown("""
|
18 |
+
<style>
|
19 |
+
.main {
|
20 |
+
padding-top: 2rem;
|
21 |
+
}
|
22 |
+
.stButton>button {
|
23 |
+
width: 100%;
|
24 |
+
background-color: #4CAF50;
|
25 |
+
color: white;
|
26 |
+
font-weight: bold;
|
27 |
+
padding: 0.5rem;
|
28 |
+
border-radius: 0.5rem;
|
29 |
+
}
|
30 |
+
.stButton>button:hover {
|
31 |
+
background-color: #45a049;
|
32 |
+
}
|
33 |
+
</style>
|
34 |
+
""", unsafe_allow_html=True)
|
35 |
+
|
36 |
+
# ํ์ดํ
|
37 |
+
st.title("๐ฌ AI Video Generator")
|
38 |
+
st.markdown("**Replicate API**๋ฅผ ์ฌ์ฉํ์ฌ ํ
์คํธ๋ ์ด๋ฏธ์ง๋ก๋ถํฐ ๋น๋์ค๋ฅผ ์์ฑํฉ๋๋ค.")
|
39 |
+
|
40 |
+
# API ํ ํฐ ์ค์
|
41 |
+
api_token = os.getenv("RAPI_TOKEN")
|
42 |
+
|
43 |
+
# ์ฌ์ด๋๋ฐ ์ค์
|
44 |
+
with st.sidebar:
|
45 |
+
st.header("โ๏ธ ์ค์ ")
|
46 |
+
|
47 |
+
# API ํ ํฐ ์
๋ ฅ (ํ๊ฒฝ๋ณ์๊ฐ ์๋ ๊ฒฝ์ฐ)
|
48 |
+
if not api_token:
|
49 |
+
api_token_input = st.text_input(
|
50 |
+
"Replicate API Token",
|
51 |
+
type="password",
|
52 |
+
help="ํ๊ฒฝ๋ณ์ RAPI_TOKEN์ด ์ค์ ๋์ง ์์์ต๋๋ค. API ํ ํฐ์ ์
๋ ฅํ์ธ์."
|
53 |
+
)
|
54 |
+
if api_token_input:
|
55 |
+
api_token = api_token_input
|
56 |
+
os.environ["REPLICATE_API_TOKEN"] = api_token
|
57 |
+
else:
|
58 |
+
st.success("โ
API ํ ํฐ์ด ํ๊ฒฝ๋ณ์์์ ๋ก๋๋์์ต๋๋ค.")
|
59 |
+
os.environ["REPLICATE_API_TOKEN"] = api_token
|
60 |
+
|
61 |
+
st.divider()
|
62 |
+
|
63 |
+
# ํ๋ฉด ๋น์จ ์ค์
|
64 |
+
st.subheader("๐ ํ๋ฉด ๋น์จ")
|
65 |
+
aspect_ratios = {
|
66 |
+
"16:9": "16:9 (YouTube, ์ผ๋ฐ ๋์์)",
|
67 |
+
"4:3": "4:3 (์ ํต์ ์ธ TV ํ์)",
|
68 |
+
"1:1": "1:1 (Instagram ํผ๋)",
|
69 |
+
"3:4": "3:4 (Instagram ํฌํธ๋ ์ดํธ)",
|
70 |
+
"9:16": "9:16 (Instagram ๋ฆด์ค, TikTok)",
|
71 |
+
"21:9": "21:9 (์๋ค๋งํฑ ์์ด๋)",
|
72 |
+
"9:21": "9:21 (์ธํธ๋ผ ์ธ๋กํ)"
|
73 |
+
}
|
74 |
+
|
75 |
+
selected_ratio = st.selectbox(
|
76 |
+
"๋น์จ ์ ํ",
|
77 |
+
options=list(aspect_ratios.keys()),
|
78 |
+
format_func=lambda x: aspect_ratios[x],
|
79 |
+
index=0
|
80 |
+
)
|
81 |
+
|
82 |
+
st.divider()
|
83 |
+
|
84 |
+
# Seed ์ค์
|
85 |
+
st.subheader("๐ฒ ๋๋ค ์๋")
|
86 |
+
seed = st.number_input(
|
87 |
+
"Seed ๊ฐ",
|
88 |
+
min_value=0,
|
89 |
+
max_value=999999,
|
90 |
+
value=42,
|
91 |
+
help="๋์ผํ ์๋๊ฐ์ผ๋ก ๋์ผํ ๊ฒฐ๊ณผ๋ฅผ ์ฌํํ ์ ์์ต๋๋ค."
|
92 |
+
)
|
93 |
+
|
94 |
+
st.divider()
|
95 |
+
|
96 |
+
# ๊ณ ์ ์ค์ ํ์
|
97 |
+
st.subheader("๐ ๊ณ ์ ์ค์ ")
|
98 |
+
st.info("""
|
99 |
+
- **์ฌ์ ์๊ฐ**: 5์ด
|
100 |
+
- **ํด์๋**: 480p
|
101 |
+
""")
|
102 |
+
|
103 |
+
# ๋ฉ์ธ ์ปจํ
์ธ
|
104 |
+
col1, col2 = st.columns([1, 1])
|
105 |
+
|
106 |
+
with col1:
|
107 |
+
st.header("๐ฏ ์์ฑ ๋ชจ๋ ์ ํ")
|
108 |
+
mode = st.radio(
|
109 |
+
"๋ชจ๋๋ฅผ ์ ํํ์ธ์:",
|
110 |
+
["ํ
์คํธ to ๋น๋์ค", "์ด๋ฏธ์ง to ๋น๋์ค"],
|
111 |
+
help="ํ
์คํธ ์ค๋ช
์ด๋ ์ด๋ฏธ์ง๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋น๋์ค๋ฅผ ์์ฑํฉ๋๋ค."
|
112 |
+
)
|
113 |
+
|
114 |
+
# ์ด๋ฏธ์ง ์
๋ก๋ (์ด๋ฏธ์ง to ๋น๋์ค ๋ชจ๋)
|
115 |
+
uploaded_image = None
|
116 |
+
image_base64 = None
|
117 |
+
|
118 |
+
if mode == "์ด๋ฏธ์ง to ๋น๋์ค":
|
119 |
+
st.subheader("๐ท ์ด๋ฏธ์ง ์
๋ก๋")
|
120 |
+
uploaded_file = st.file_uploader(
|
121 |
+
"์ด๋ฏธ์ง๋ฅผ ์ ํํ์ธ์",
|
122 |
+
type=['png', 'jpg', 'jpeg', 'webp'],
|
123 |
+
help="์
๋ก๋ํ ์ด๋ฏธ์ง๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋น๋์ค๊ฐ ์์ฑ๋ฉ๋๋ค."
|
124 |
+
)
|
125 |
+
|
126 |
+
if uploaded_file is not None:
|
127 |
+
# ์ด๋ฏธ์ง ํ์
|
128 |
+
uploaded_image = Image.open(uploaded_file)
|
129 |
+
st.image(uploaded_image, caption="์
๋ก๋๋ ์ด๋ฏธ์ง", use_column_width=True)
|
130 |
+
|
131 |
+
# ์ด๋ฏธ์ง๋ฅผ base64๋ก ๋ณํ
|
132 |
+
buffered = io.BytesIO()
|
133 |
+
uploaded_image.save(buffered, format="PNG")
|
134 |
+
image_base64 = base64.b64encode(buffered.getvalue()).decode()
|
135 |
+
|
136 |
+
with col2:
|
137 |
+
st.header("โ๏ธ ํ๋กฌํํธ ์
๋ ฅ")
|
138 |
+
|
139 |
+
if mode == "ํ
์คํธ to ๋น๋์ค":
|
140 |
+
prompt_placeholder = "์์ฑํ ๋น๋์ค๋ฅผ ์ค๋ช
ํด์ฃผ์ธ์.\n์: The sun rises slowly between tall buildings. [Ground-level follow shot] Bicycle tires roll over a dew-covered street at dawn."
|
141 |
+
else:
|
142 |
+
prompt_placeholder = "์ด๋ฏธ์ง๋ฅผ ์ด๋ป๊ฒ ์์ง์ด๊ฒ ํ ์ง ์ค๋ช
ํด์ฃผ์ธ์.\n์: Camera slowly zooms in while clouds move across the sky. The subject's hair gently moves in the wind."
|
143 |
+
|
144 |
+
prompt = st.text_area(
|
145 |
+
"ํ๋กฌํํธ",
|
146 |
+
height=150,
|
147 |
+
placeholder=prompt_placeholder,
|
148 |
+
help="์์ธํ๊ณ ๊ตฌ์ฒด์ ์ธ ์ค๋ช
์ผ์๋ก ๋ ์ข์ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์์ต๋๋ค."
|
149 |
+
)
|
150 |
+
|
151 |
+
# ์์ฑ ๋ฒํผ
|
152 |
+
st.divider()
|
153 |
+
|
154 |
+
if st.button("๐ฌ ๋น๋์ค ์์ฑ", type="primary", use_container_width=True):
|
155 |
+
# ์
๋ ฅ ๊ฒ์ฆ
|
156 |
+
if not api_token:
|
157 |
+
st.error("โ API ํ ํฐ์ด ํ์ํฉ๋๋ค. ์ฌ์ด๋๋ฐ์์ ์ค์ ํด์ฃผ์ธ์.")
|
158 |
+
elif not prompt:
|
159 |
+
st.error("โ ํ๋กฌํํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.")
|
160 |
+
elif mode == "์ด๋ฏธ์ง to ๋น๋์ค" and uploaded_image is None:
|
161 |
+
st.error("โ ์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ํด์ฃผ์ธ์.")
|
162 |
+
else:
|
163 |
+
try:
|
164 |
+
# ํ๋ก๊ทธ๋ ์ค ๋ฐ
|
165 |
+
progress_text = "๋น๋์ค ์์ฑ ์ค... ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์."
|
166 |
+
progress_bar = st.progress(0, text=progress_text)
|
167 |
+
|
168 |
+
# ์
๋ ฅ ํ๋ผ๋ฏธํฐ ์ค์
|
169 |
+
input_params = {
|
170 |
+
"prompt": prompt,
|
171 |
+
"duration": 5,
|
172 |
+
"resolution": "480p",
|
173 |
+
"aspect_ratio": selected_ratio,
|
174 |
+
"seed": seed
|
175 |
+
}
|
176 |
+
|
177 |
+
# ์ด๋ฏธ์ง to ๋น๋์ค ๋ชจ๋์ธ ๊ฒฝ์ฐ
|
178 |
+
if mode == "์ด๋ฏธ์ง to ๋น๋์ค" and image_base64:
|
179 |
+
input_params["image"] = f"data:image/png;base64,{image_base64}"
|
180 |
+
|
181 |
+
# ์งํ๋ฅ ์
๋ฐ์ดํธ
|
182 |
+
progress_bar.progress(25, text="Replicate API ํธ์ถ ์ค...")
|
183 |
+
|
184 |
+
# Replicate ์คํ
|
185 |
+
output = replicate.run(
|
186 |
+
"bytedance/seedance-1-lite",
|
187 |
+
input=input_params
|
188 |
+
)
|
189 |
+
|
190 |
+
# ์งํ๋ฅ ์
๋ฐ์ดํธ
|
191 |
+
progress_bar.progress(75, text="๋น๋์ค ๋ค์ด๋ก๋ ์ค...")
|
192 |
+
|
193 |
+
# ๋น๋์ค ์ ์ฅ
|
194 |
+
if hasattr(output, 'read'):
|
195 |
+
video_data = output.read()
|
196 |
+
else:
|
197 |
+
# URL์ธ ๊ฒฝ์ฐ ๋ค์ด๋ก๋
|
198 |
+
import requests
|
199 |
+
response = requests.get(output)
|
200 |
+
video_data = response.content
|
201 |
+
|
202 |
+
# ์์ ํ์ผ๋ก ์ ์ฅ
|
203 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
|
204 |
+
tmp_file.write(video_data)
|
205 |
+
tmp_filename = tmp_file.name
|
206 |
+
|
207 |
+
# ๋ก์ปฌ ํ์ผ๋ก๋ ์ ์ฅ
|
208 |
+
output_filename = "output.mp4"
|
209 |
+
with open(output_filename, "wb") as file:
|
210 |
+
file.write(video_data)
|
211 |
+
|
212 |
+
# ์งํ๋ฅ ์๋ฃ
|
213 |
+
progress_bar.progress(100, text="์๋ฃ!")
|
214 |
+
|
215 |
+
# ์ฑ๊ณต ๋ฉ์์ง
|
216 |
+
st.success(f"โ
๋น๋์ค๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋์์ต๋๋ค! ({output_filename})")
|
217 |
+
|
218 |
+
# ๋น๋์ค ํ์
|
219 |
+
st.subheader("๐น ์์ฑ๋ ๋น๋์ค")
|
220 |
+
st.video(tmp_filename)
|
221 |
+
|
222 |
+
# ๋ค์ด๋ก๋ ๋ฒํผ
|
223 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
224 |
+
with col2:
|
225 |
+
st.download_button(
|
226 |
+
label="โฌ๏ธ ๋น๋์ค ๋ค์ด๋ก๋",
|
227 |
+
data=video_data,
|
228 |
+
file_name="generated_video.mp4",
|
229 |
+
mime="video/mp4",
|
230 |
+
use_container_width=True
|
231 |
+
)
|
232 |
+
|
233 |
+
# ์์ฑ ์ ๋ณด ํ์
|
234 |
+
with st.expander("๐ ์์ฑ ์ ๋ณด"):
|
235 |
+
st.json({
|
236 |
+
"mode": mode,
|
237 |
+
"aspect_ratio": selected_ratio,
|
238 |
+
"seed": seed,
|
239 |
+
"duration": "5์ด",
|
240 |
+
"resolution": "480p",
|
241 |
+
"prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt
|
242 |
+
})
|
243 |
+
|
244 |
+
except Exception as e:
|
245 |
+
st.error(f"โ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}")
|
246 |
+
st.info("๐ก API ํ ํฐ์ด ์ฌ๋ฐ๋ฅธ์ง, ๋ชจ๋ธ์ด ์ฌ์ฉ ๊ฐ๋ฅํ์ง ํ์ธํด์ฃผ์ธ์.")
|
247 |
+
|
248 |
+
# ์ฌ์ฉ ๋ฐฉ๋ฒ
|
249 |
+
with st.expander("๐ ์ฌ์ฉ ๋ฐฉ๋ฒ"):
|
250 |
+
st.markdown("""
|
251 |
+
### ์ค์น ๋ฐ ์คํ
|
252 |
+
|
253 |
+
1. **ํ์ํ ํจํค์ง ์ค์น**:
|
254 |
+
```bash
|
255 |
+
pip install streamlit replicate pillow requests
|
256 |
+
```
|
257 |
+
|
258 |
+
2. **ํ๊ฒฝ๋ณ์ ์ค์ ** (์ ํ์ฌํญ):
|
259 |
+
```bash
|
260 |
+
export RAPI_TOKEN="your-replicate-api-token"
|
261 |
+
```
|
262 |
+
|
263 |
+
3. **์ ํ๋ฆฌ์ผ์ด์
์คํ**:
|
264 |
+
```bash
|
265 |
+
streamlit run video_generator.py
|
266 |
+
```
|
267 |
+
|
268 |
+
### ๊ธฐ๋ฅ ์ค๋ช
|
269 |
+
|
270 |
+
- **ํ
์คํธ to ๋น๋์ค**: ํ
์คํธ ์ค๋ช
๋ง์ผ๋ก ๋น๋์ค๋ฅผ ์์ฑํฉ๋๋ค.
|
271 |
+
- **์ด๋ฏธ์ง to ๋น๋์ค**: ์
๋ก๋ํ ์ด๋ฏธ์ง๋ฅผ ์์ง์ด๋ ๋น๋์ค๋ก ๋ณํํฉ๋๋ค.
|
272 |
+
- **ํ๋ฉด ๋น์จ**: ๋ค์ํ SNS ํ๋ซํผ์ ์ต์ ํ๋ ๋น์จ์ ์ ํํ ์ ์์ต๋๋ค.
|
273 |
+
- **Seed ๊ฐ**: ๋์ผํ ์๋๊ฐ์ผ๋ก ๋์ผํ ๊ฒฐ๊ณผ๋ฅผ ์ฌํํ ์ ์์ต๋๋ค.
|
274 |
+
|
275 |
+
### ํ
|
276 |
+
|
277 |
+
- ๊ตฌ์ฒด์ ์ด๊ณ ์์ธํ ํ๋กฌํํธ์ผ์๋ก ๋ ์ข์ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์์ต๋๋ค.
|
278 |
+
- ์นด๋ฉ๋ผ ์์ง์, ์กฐ๋ช
, ๋ถ์๊ธฐ ๋ฑ์ ์ค๋ช
์ ํฌํจ์์ผ๋ณด์ธ์.
|
279 |
+
- ์ด๋ฏธ์ง to ๋น๋์ค ๋ชจ๋์์๋ ์ด๋ฏธ์ง์ ์ด๋ค ๋ถ๋ถ์ ์ด๋ป๊ฒ ์์ง์ผ์ง ์ค๋ช
ํ์ธ์.
|
280 |
+
""")
|
281 |
+
|
282 |
+
# ํธํฐ
|
283 |
+
st.divider()
|
284 |
+
st.markdown(
|
285 |
+
"""
|
286 |
+
<div style='text-align: center; color: gray;'>
|
287 |
+
<p>Powered by Replicate AI and bytedance/seedance-1-lite model</p>
|
288 |
+
</div>
|
289 |
+
""",
|
290 |
+
unsafe_allow_html=True
|
291 |
+
)
|