Seedance-Free / app.py
ginipick's picture
Update app.py
ac825a1 verified
raw
history blame
11.1 kB
#!/usr/bin/env python3
"""
AI Video Generator with Gradio
Single file application - app.py
"""
import os
import gradio as gr
import replicate
import base64
from PIL import Image
import io
import requests
from datetime import datetime
import tempfile
# API ํ† ํฐ ์„ค์ •
api_token = os.getenv("RAPI_TOKEN")
if api_token:
os.environ["REPLICATE_API_TOKEN"] = api_token
# ํ™”๋ฉด ๋น„์œจ ์˜ต์…˜
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 (์šธํŠธ๋ผ ์„ธ๋กœํ˜•)"
}
def update_prompt_placeholder(mode):
"""๋ชจ๋“œ์— ๋”ฐ๋ผ ํ”„๋กฌํ”„ํŠธ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์—…๋ฐ์ดํŠธ"""
if mode == "ํ…์ŠคํŠธ to ๋น„๋””์˜ค":
return gr.update(placeholder="์ƒ์„ฑํ•  ๋น„๋””์˜ค๋ฅผ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.\n์˜ˆ: The sun rises slowly between tall buildings. [Ground-level follow shot] Bicycle tires roll over a dew-covered street at dawn.")
else:
return gr.update(placeholder="์ด๋ฏธ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ์›€์ง์ด๊ฒŒ ํ• ์ง€ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.\n์˜ˆ: Camera slowly zooms in while clouds move across the sky. The subject's hair gently moves in the wind.")
def update_image_input(mode):
"""๋ชจ๋“œ์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€ ์ž…๋ ฅ ํ‘œ์‹œ/์ˆจ๊น€"""
if mode == "์ด๋ฏธ์ง€ to ๋น„๋””์˜ค":
return gr.update(visible=True)
else:
return gr.update(visible=False)
def generate_video(mode, prompt, image, aspect_ratio, seed, api_key_input, progress=gr.Progress()):
"""๋น„๋””์˜ค ์ƒ์„ฑ ๋ฉ”์ธ ํ•จ์ˆ˜"""
# API ํ† ํฐ ํ™•์ธ
token = api_key_input or api_token
if not token:
return None, "โŒ API ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ™˜๊ฒฝ๋ณ€์ˆ˜ RAPI_TOKEN์„ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ API ํ‚ค๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”."
os.environ["REPLICATE_API_TOKEN"] = token
# ์ž…๋ ฅ ๊ฒ€์ฆ
if not prompt:
return None, "โŒ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
if mode == "์ด๋ฏธ์ง€ to ๋น„๋””์˜ค" and image is None:
return None, "โŒ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”."
try:
progress(0, desc="๋น„๋””์˜ค ์ƒ์„ฑ ์ค€๋น„ ์ค‘...")
# ์ž…๋ ฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
input_params = {
"prompt": prompt,
"duration": 5,
"resolution": "480p",
"aspect_ratio": aspect_ratio,
"seed": seed
}
# ์ด๋ฏธ์ง€ to ๋น„๋””์˜ค ๋ชจ๋“œ
if mode == "์ด๋ฏธ์ง€ to ๋น„๋””์˜ค" and image is not None:
progress(0.1, desc="์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ค‘...")
# PIL Image๋ฅผ base64๋กœ ๋ณ€ํ™˜
if isinstance(image, str): # ํŒŒ์ผ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ
with Image.open(image) as img:
buffered = io.BytesIO()
img.save(buffered, format="PNG")
image_base64 = base64.b64encode(buffered.getvalue()).decode()
else: # PIL Image ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ
buffered = io.BytesIO()
image.save(buffered, format="PNG")
image_base64 = base64.b64encode(buffered.getvalue()).decode()
input_params["image"] = f"data:image/png;base64,{image_base64}"
progress(0.3, desc="Replicate API ํ˜ธ์ถœ ์ค‘...")
# Replicate ์‹คํ–‰
output = replicate.run(
"bytedance/seedance-1-lite",
input=input_params
)
progress(0.7, desc="๋น„๋””์˜ค ๋‹ค์šด๋กœ๋“œ ์ค‘...")
# ๋น„๋””์˜ค ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
if hasattr(output, 'read'):
video_data = output.read()
else:
# URL์ธ ๊ฒฝ์šฐ ๋‹ค์šด๋กœ๋“œ
response = requests.get(output)
video_data = response.content
# ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
tmp_file.write(video_data)
video_path = tmp_file.name
# output.mp4๋กœ๋„ ์ €์žฅ
with open("output.mp4", "wb") as file:
file.write(video_data)
progress(1.0, desc="์™„๋ฃŒ!")
# ์ƒ์„ฑ ์ •๋ณด
info = f"""โœ… ๋น„๋””์˜ค๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!
๐Ÿ“Š ์ƒ์„ฑ ์ •๋ณด:
- ๋ชจ๋“œ: {mode}
- ํ™”๋ฉด ๋น„์œจ: {aspect_ratio}
- Seed: {seed}
- ์žฌ์ƒ ์‹œ๊ฐ„: 5์ดˆ
- ํ•ด์ƒ๋„: 480p
- ํŒŒ์ผ: output.mp4"""
return video_path, info
except Exception as e:
error_msg = f"โŒ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
return None, error_msg
# Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
with gr.Blocks(title="AI Video Generator", theme=gr.themes.Soft()) as app:
gr.Markdown("""
# ๐ŸŽฌ AI Video Generator
**Replicate API**๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…์ŠคํŠธ๋‚˜ ์ด๋ฏธ์ง€๋กœ๋ถ€ํ„ฐ ๋น„๋””์˜ค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
[![Powered by Ginigen](https://img.shields.io/badge/Powered%20by-Replicate-blue)](https://ginigen.com/)
""")
with gr.Row():
with gr.Column(scale=1):
# API ์„ค์ •
with gr.Accordion("โš™๏ธ API ์„ค์ •", open=not bool(api_token)):
if api_token:
gr.Markdown("โœ… API ํ† ํฐ์ด ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
api_key_input = gr.Textbox(
label="Replicate API Token (์„ ํƒ์‚ฌํ•ญ)",
type="password",
placeholder="ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ๋ฎ์–ด์“ฐ๋ ค๋ฉด ์—ฌ๊ธฐ์— ์ž…๋ ฅ",
value=""
)
else:
gr.Markdown("โš ๏ธ ํ™˜๊ฒฝ๋ณ€์ˆ˜ RAPI_TOKEN์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
api_key_input = gr.Textbox(
label="Replicate API Token (ํ•„์ˆ˜)",
type="password",
placeholder="Replicate API ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š”",
value=""
)
# ์ƒ์„ฑ ๋ชจ๋“œ
mode = gr.Radio(
label="๐ŸŽฏ ์ƒ์„ฑ ๋ชจ๋“œ",
choices=["ํ…์ŠคํŠธ to ๋น„๋””์˜ค", "์ด๋ฏธ์ง€ to ๋น„๋””์˜ค"],
value="ํ…์ŠคํŠธ to ๋น„๋””์˜ค"
)
# ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
image_input = gr.Image(
label="๐Ÿ“ท ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ",
type="pil",
visible=False
)
# ํ™”๋ฉด ๋น„์œจ
aspect_ratio = gr.Dropdown(
label="๐Ÿ“ ํ™”๋ฉด ๋น„์œจ",
choices=list(ASPECT_RATIOS.keys()),
value="16:9",
info="SNS ํ”Œ๋žซํผ์— ์ตœ์ ํ™”๋œ ๋น„์œจ์„ ์„ ํƒํ•˜์„ธ์š”"
)
# ๋น„์œจ ์„ค๋ช… ํ‘œ์‹œ
ratio_info = gr.Markdown(value=f"์„ ํƒ๋œ ๋น„์œจ: {ASPECT_RATIOS['16:9']}")
# Seed ์„ค์ •
seed = gr.Number(
label="๐ŸŽฒ ๋žœ๋ค ์‹œ๋“œ",
value=42,
precision=0,
info="๋™์ผํ•œ ์‹œ๋“œ๊ฐ’์œผ๋กœ ๋™์ผํ•œ ๊ฒฐ๊ณผ๋ฅผ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"
)
# ๊ณ ์ • ์„ค์ • ํ‘œ์‹œ
gr.Markdown("""
### ๐Ÿ“‹ ๊ณ ์ • ์„ค์ •
- **์žฌ์ƒ ์‹œ๊ฐ„**: 5์ดˆ
- **ํ•ด์ƒ๋„**: 480p
""")
with gr.Column(scale=2):
# ํ”„๋กฌํ”„ํŠธ ์ž…๋ ฅ
prompt = gr.Textbox(
label="โœ๏ธ ํ”„๋กฌํ”„ํŠธ",
lines=5,
placeholder="์ƒ์„ฑํ•  ๋น„๋””์˜ค๋ฅผ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.\n์˜ˆ: The sun rises slowly between tall buildings. [Ground-level follow shot] Bicycle tires roll over a dew-covered street at dawn."
)
# ์ƒ์„ฑ ๋ฒ„ํŠผ
generate_btn = gr.Button("๐ŸŽฌ ๋น„๋””์˜ค ์ƒ์„ฑ", variant="primary", size="lg")
# ๊ฒฐ๊ณผ ํ‘œ์‹œ
with gr.Column():
output_video = gr.Video(
label="๐Ÿ“น ์ƒ์„ฑ๋œ ๋น„๋””์˜ค",
autoplay=True
)
output_info = gr.Textbox(
label="์ •๋ณด",
lines=8,
interactive=False
)
# ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
with gr.Accordion("๐Ÿ“– ์‚ฌ์šฉ ๋ฐฉ๋ฒ•", open=False):
gr.Markdown("""
### ์„ค์น˜ ๋ฐฉ๋ฒ•
1. **ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜**:
```bash
pip install gradio replicate pillow requests
```
2. **ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •** (์„ ํƒ์‚ฌํ•ญ):
```bash
export RAPI_TOKEN="your-replicate-api-token"
```
3. **์‹คํ–‰**:
```bash
python app.py
```
### ๊ธฐ๋Šฅ ์„ค๋ช…
- **ํ…์ŠคํŠธ to ๋น„๋””์˜ค**: ํ…์ŠคํŠธ ์„ค๋ช…๋งŒ์œผ๋กœ ๋น„๋””์˜ค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
- **์ด๋ฏธ์ง€ to ๋น„๋””์˜ค**: ์—…๋กœ๋“œํ•œ ์ด๋ฏธ์ง€๋ฅผ ์›€์ง์ด๋Š” ๋น„๋””์˜ค๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
- **ํ™”๋ฉด ๋น„์œจ**: ๋‹ค์–‘ํ•œ SNS ํ”Œ๋žซํผ์— ์ตœ์ ํ™”๋œ ๋น„์œจ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
- **Seed ๊ฐ’**: ๋™์ผํ•œ ์‹œ๋“œ๊ฐ’์œผ๋กœ ๋™์ผํ•œ ๊ฒฐ๊ณผ๋ฅผ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
### ํ”„๋กฌํ”„ํŠธ ์ž‘์„ฑ ํŒ
- ๊ตฌ์ฒด์ ์ด๊ณ  ์ƒ์„ธํ•œ ์„ค๋ช…์„ ์‚ฌ์šฉํ•˜์„ธ์š”
- ์นด๋ฉ”๋ผ ์›€์ง์ž„์„ ๋ช…์‹œํ•˜์„ธ์š” (์˜ˆ: zoom in, pan left, tracking shot)
- ์กฐ๋ช…๊ณผ ๋ถ„์œ„๊ธฐ๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š” (์˜ˆ: golden hour, dramatic lighting)
- ์›€์ง์ž„์˜ ์†๋„๋ฅผ ์ง€์ •ํ•˜์„ธ์š” (์˜ˆ: slowly, rapidly, gently)
""")
# ์˜ˆ์‹œ
gr.Examples(
examples=[
["ํ…์ŠคํŠธ to ๋น„๋””์˜ค", "A serene lake at sunrise with mist rolling over the water. Camera slowly pans across the landscape as birds fly overhead.", None, "16:9", 42],
["ํ…์ŠคํŠธ to ๋น„๋””์˜ค", "Urban street scene at night with neon lights reflecting on wet pavement. People walking with umbrellas, camera tracking forward.", None, "9:16", 123],
["ํ…์ŠคํŠธ to ๋น„๋””์˜ค", "Close-up of a flower blooming in time-lapse, soft natural lighting, shallow depth of field.", None, "1:1", 789],
],
inputs=[mode, prompt, image_input, aspect_ratio, seed],
label="์˜ˆ์‹œ ํ”„๋กฌํ”„ํŠธ"
)
# ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
mode.change(
fn=update_prompt_placeholder,
inputs=[mode],
outputs=[prompt]
)
mode.change(
fn=update_image_input,
inputs=[mode],
outputs=[image_input]
)
aspect_ratio.change(
fn=lambda x: f"์„ ํƒ๋œ ๋น„์œจ: {ASPECT_RATIOS[x]}",
inputs=[aspect_ratio],
outputs=[ratio_info]
)
generate_btn.click(
fn=generate_video,
inputs=[mode, prompt, image_input, aspect_ratio, seed, api_key_input],
outputs=[output_video, output_info]
)
# ์•ฑ ์‹คํ–‰
if __name__ == "__main__":
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
inbrowser=True
)