Spaces:
Running
Running
import os | |
import streamlit as st | |
import replicate | |
from PIL import Image | |
import io | |
import base64 | |
import tempfile | |
# νμ΄μ§ μ€μ | |
st.set_page_config( | |
page_title="AI Video Generator", | |
page_icon="π¬", | |
layout="wide" | |
) | |
# μ€νμΌ μ μ© | |
st.markdown(""" | |
<style> | |
.main { | |
padding-top: 2rem; | |
} | |
.stButton>button { | |
width: 100%; | |
background-color: #4CAF50; | |
color: white; | |
font-weight: bold; | |
padding: 0.5rem; | |
border-radius: 0.5rem; | |
} | |
.stButton>button:hover { | |
background-color: #45a049; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# νμ΄ν | |
st.title("π¬ AI Video Generator") | |
st.markdown("**Replicate API**λ₯Ό μ¬μ©νμ¬ ν μ€νΈλ μ΄λ―Έμ§λ‘λΆν° λΉλμ€λ₯Ό μμ±ν©λλ€.") | |
# API ν ν° μ€μ | |
api_token = os.getenv("RAPI_TOKEN") | |
# μ¬μ΄λλ° μ€μ | |
with st.sidebar: | |
st.header("βοΈ μ€μ ") | |
# API ν ν° μ λ ₯ (νκ²½λ³μκ° μλ κ²½μ°) | |
if not api_token: | |
api_token_input = st.text_input( | |
"Replicate API Token", | |
type="password", | |
help="νκ²½λ³μ RAPI_TOKENμ΄ μ€μ λμ§ μμμ΅λλ€. API ν ν°μ μ λ ₯νμΈμ." | |
) | |
if api_token_input: | |
api_token = api_token_input | |
os.environ["REPLICATE_API_TOKEN"] = api_token | |
else: | |
st.success("β API ν ν°μ΄ νκ²½λ³μμμ λ‘λλμμ΅λλ€.") | |
os.environ["REPLICATE_API_TOKEN"] = api_token | |
st.divider() | |
# νλ©΄ λΉμ¨ μ€μ | |
st.subheader("π νλ©΄ λΉμ¨") | |
aspect_ratios = { | |
"16:9": "16:9 (YouTube, μΌλ° λμμ)", | |
"4:3": "4:3 (μ ν΅μ μΈ TV νμ)", | |
"1:1": "1:1 (Instagram νΌλ)", | |
"3:4": "3:4 (Instagram ν¬νΈλ μ΄νΈ)", | |
"9:16": "9:16 (Instagram 릴μ€, TikTok)", | |
"21:9": "21:9 (μλ€λ§ν± μμ΄λ)", | |
"9:21": "9:21 (μΈνΈλΌ μΈλ‘ν)" | |
} | |
selected_ratio = st.selectbox( | |
"λΉμ¨ μ ν", | |
options=list(aspect_ratios.keys()), | |
format_func=lambda x: aspect_ratios[x], | |
index=0 | |
) | |
st.divider() | |
# Seed μ€μ | |
st.subheader("π² λλ€ μλ") | |
seed = st.number_input( | |
"Seed κ°", | |
min_value=0, | |
max_value=999999, | |
value=42, | |
help="λμΌν μλκ°μΌλ‘ λμΌν κ²°κ³Όλ₯Ό μ¬νν μ μμ΅λλ€." | |
) | |
st.divider() | |
# κ³ μ μ€μ νμ | |
st.subheader("π κ³ μ μ€μ ") | |
st.info(""" | |
- **μ¬μ μκ°**: 5μ΄ | |
- **ν΄μλ**: 480p | |
""") | |
# λ©μΈ 컨ν μΈ | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
st.header("π― μμ± λͺ¨λ μ ν") | |
mode = st.radio( | |
"λͺ¨λλ₯Ό μ ννμΈμ:", | |
["ν μ€νΈ to λΉλμ€", "μ΄λ―Έμ§ to λΉλμ€"], | |
help="ν μ€νΈ μ€λͺ μ΄λ μ΄λ―Έμ§λ₯Ό κΈ°λ°μΌλ‘ λΉλμ€λ₯Ό μμ±ν©λλ€." | |
) | |
# μ΄λ―Έμ§ μ λ‘λ (μ΄λ―Έμ§ to λΉλμ€ λͺ¨λ) | |
uploaded_image = None | |
image_base64 = None | |
if mode == "μ΄λ―Έμ§ to λΉλμ€": | |
st.subheader("π· μ΄λ―Έμ§ μ λ‘λ") | |
uploaded_file = st.file_uploader( | |
"μ΄λ―Έμ§λ₯Ό μ ννμΈμ", | |
type=['png', 'jpg', 'jpeg', 'webp'], | |
help="μ λ‘λν μ΄λ―Έμ§λ₯Ό κΈ°λ°μΌλ‘ λΉλμ€κ° μμ±λ©λλ€." | |
) | |
if uploaded_file is not None: | |
# μ΄λ―Έμ§ νμ | |
uploaded_image = Image.open(uploaded_file) | |
st.image(uploaded_image, caption="μ λ‘λλ μ΄λ―Έμ§", use_column_width=True) | |
# μ΄λ―Έμ§λ₯Ό base64λ‘ λ³ν | |
buffered = io.BytesIO() | |
uploaded_image.save(buffered, format="PNG") | |
image_base64 = base64.b64encode(buffered.getvalue()).decode() | |
with col2: | |
st.header("βοΈ ν둬ννΈ μ λ ₯") | |
if mode == "ν μ€νΈ to λΉλμ€": | |
prompt_placeholder = "μμ±ν λΉλμ€λ₯Ό μ€λͺ ν΄μ£ΌμΈμ.\nμ: The sun rises slowly between tall buildings. [Ground-level follow shot] Bicycle tires roll over a dew-covered street at dawn." | |
else: | |
prompt_placeholder = "μ΄λ―Έμ§λ₯Ό μ΄λ»κ² μμ§μ΄κ² ν μ§ μ€λͺ ν΄μ£ΌμΈμ.\nμ: Camera slowly zooms in while clouds move across the sky. The subject's hair gently moves in the wind." | |
prompt = st.text_area( | |
"ν둬ννΈ", | |
height=150, | |
placeholder=prompt_placeholder, | |
help="μμΈνκ³ κ΅¬μ²΄μ μΈ μ€λͺ μΌμλ‘ λ μ’μ κ²°κ³Όλ₯Ό μ»μ μ μμ΅λλ€." | |
) | |
# μμ± λ²νΌ | |
st.divider() | |
if st.button("π¬ λΉλμ€ μμ±", type="primary", use_container_width=True): | |
# μ λ ₯ κ²μ¦ | |
if not api_token: | |
st.error("β API ν ν°μ΄ νμν©λλ€. μ¬μ΄λλ°μμ μ€μ ν΄μ£ΌμΈμ.") | |
elif not prompt: | |
st.error("β ν둬ννΈλ₯Ό μ λ ₯ν΄μ£ΌμΈμ.") | |
elif mode == "μ΄λ―Έμ§ to λΉλμ€" and uploaded_image is None: | |
st.error("β μ΄λ―Έμ§λ₯Ό μ λ‘λν΄μ£ΌμΈμ.") | |
else: | |
try: | |
# νλ‘κ·Έλ μ€ λ° | |
progress_text = "λΉλμ€ μμ± μ€... μ μλ§ κΈ°λ€λ €μ£ΌμΈμ." | |
progress_bar = st.progress(0, text=progress_text) | |
# μ λ ₯ νλΌλ―Έν° μ€μ | |
input_params = { | |
"prompt": prompt, | |
"duration": 5, | |
"resolution": "480p", | |
"aspect_ratio": selected_ratio, | |
"seed": seed | |
} | |
# μ΄λ―Έμ§ to λΉλμ€ λͺ¨λμΈ κ²½μ° | |
if mode == "μ΄λ―Έμ§ to λΉλμ€" and image_base64: | |
input_params["image"] = f"data:image/png;base64,{image_base64}" | |
# μ§νλ₯ μ λ°μ΄νΈ | |
progress_bar.progress(25, text="Replicate API νΈμΆ μ€...") | |
# Replicate μ€ν | |
output = replicate.run( | |
"bytedance/seedance-1-lite", | |
input=input_params | |
) | |
# μ§νλ₯ μ λ°μ΄νΈ | |
progress_bar.progress(75, text="λΉλμ€ λ€μ΄λ‘λ μ€...") | |
# λΉλμ€ μ μ₯ | |
if hasattr(output, 'read'): | |
video_data = output.read() | |
else: | |
# URLμΈ κ²½μ° λ€μ΄λ‘λ | |
import requests | |
response = requests.get(output) | |
video_data = response.content | |
# μμ νμΌλ‘ μ μ₯ | |
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file: | |
tmp_file.write(video_data) | |
tmp_filename = tmp_file.name | |
# λ‘컬 νμΌλ‘λ μ μ₯ | |
output_filename = "output.mp4" | |
with open(output_filename, "wb") as file: | |
file.write(video_data) | |
# μ§νλ₯ μλ£ | |
progress_bar.progress(100, text="μλ£!") | |
# μ±κ³΅ λ©μμ§ | |
st.success(f"β λΉλμ€κ° μ±κ³΅μ μΌλ‘ μμ±λμμ΅λλ€! ({output_filename})") | |
# λΉλμ€ νμ | |
st.subheader("πΉ μμ±λ λΉλμ€") | |
st.video(tmp_filename) | |
# λ€μ΄λ‘λ λ²νΌ | |
col1, col2, col3 = st.columns([1, 2, 1]) | |
with col2: | |
st.download_button( | |
label="β¬οΈ λΉλμ€ λ€μ΄λ‘λ", | |
data=video_data, | |
file_name="generated_video.mp4", | |
mime="video/mp4", | |
use_container_width=True | |
) | |
# μμ± μ 보 νμ | |
with st.expander("π μμ± μ 보"): | |
st.json({ | |
"mode": mode, | |
"aspect_ratio": selected_ratio, | |
"seed": seed, | |
"duration": "5μ΄", | |
"resolution": "480p", | |
"prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt | |
}) | |
except Exception as e: | |
st.error(f"β μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}") | |
st.info("π‘ API ν ν°μ΄ μ¬λ°λ₯Έμ§, λͺ¨λΈμ΄ μ¬μ© κ°λ₯νμ§ νμΈν΄μ£ΌμΈμ.") | |
# μ¬μ© λ°©λ² | |
with st.expander("π μ¬μ© λ°©λ²"): | |
st.markdown(""" | |
### μ€μΉ λ° μ€ν | |
1. **νμν ν¨ν€μ§ μ€μΉ**: | |
```bash | |
pip install streamlit replicate pillow requests | |
``` | |
2. **νκ²½λ³μ μ€μ ** (μ νμ¬ν): | |
```bash | |
export RAPI_TOKEN="your-replicate-api-token" | |
``` | |
3. **μ ν리μΌμ΄μ μ€ν**: | |
```bash | |
streamlit run video_generator.py | |
``` | |
### κΈ°λ₯ μ€λͺ | |
- **ν μ€νΈ to λΉλμ€**: ν μ€νΈ μ€λͺ λ§μΌλ‘ λΉλμ€λ₯Ό μμ±ν©λλ€. | |
- **μ΄λ―Έμ§ to λΉλμ€**: μ λ‘λν μ΄λ―Έμ§λ₯Ό μμ§μ΄λ λΉλμ€λ‘ λ³νν©λλ€. | |
- **νλ©΄ λΉμ¨**: λ€μν SNS νλ«νΌμ μ΅μ νλ λΉμ¨μ μ νν μ μμ΅λλ€. | |
- **Seed κ°**: λμΌν μλκ°μΌλ‘ λμΌν κ²°κ³Όλ₯Ό μ¬νν μ μμ΅λλ€. | |
### ν | |
- ꡬ체μ μ΄κ³ μμΈν ν둬ννΈμΌμλ‘ λ μ’μ κ²°κ³Όλ₯Ό μ»μ μ μμ΅λλ€. | |
- μΉ΄λ©λΌ μμ§μ, μ‘°λͺ , λΆμκΈ° λ±μ μ€λͺ μ ν¬ν¨μμΌλ³΄μΈμ. | |
- μ΄λ―Έμ§ to λΉλμ€ λͺ¨λμμλ μ΄λ―Έμ§μ μ΄λ€ λΆλΆμ μ΄λ»κ² μμ§μΌμ§ μ€λͺ νμΈμ. | |
""") | |
# νΈν° | |
st.divider() | |
st.markdown( | |
""" | |
<div style='text-align: center; color: gray;'> | |
<p>Powered by Replicate AI and bytedance/seedance-1-lite model</p> | |
</div> | |
""", | |
unsafe_allow_html=True | |
) |