File size: 5,862 Bytes
8cba2a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import os
import uuid
from PIL import Image, ImageOps

# Pillow μ΅œμ‹  버전 ν˜Έν™˜ 패치
if not hasattr(Image, "ANTIALIAS"):
    Image.ANTIALIAS = Image.Resampling.LANCZOS

import gradio as gr
from moviepy.editor import ImageClip, concatenate_videoclips


OUTPUT_DIR = "outputs"
os.makedirs(OUTPUT_DIR, exist_ok=True)

WIDTH, HEIGHT = 1080, 1920


def get_file_path(file_obj):
    if file_obj is None:
        return None
    if isinstance(file_obj, str):
        return file_obj
    if hasattr(file_obj, "name"):
        return file_obj.name
    return str(file_obj)


def resize_crop_image(image_path):
    img = Image.open(image_path).convert("RGB")
    img = ImageOps.exif_transpose(img)

    target_ratio = WIDTH / HEIGHT
    img_ratio = img.width / img.height

    if img_ratio > target_ratio:
        new_h = HEIGHT
        new_w = int(new_h * img_ratio)
    else:
        new_w = WIDTH
        new_h = int(new_w / img_ratio)

    img = img.resize((new_w, new_h), Image.LANCZOS)

    left = (new_w - WIDTH) // 2
    top = (new_h - HEIGHT) // 2

    return img.crop((left, top, left + WIDTH, top + HEIGHT))


def save_scene_image(file_obj):
    image_path = get_file_path(file_obj)
    img = resize_crop_image(image_path)

    path = os.path.join(OUTPUT_DIR, f"scene_{uuid.uuid4()}.jpg")
    img.save(path, quality=95)
    return path


def apply_motion(clip, motion_type, duration):
    if motion_type == "μžλ™ μΆ”μ²œ":
        motion_type = "천천히 쀌인"

    if motion_type == "천천히 쀌인":
        clip = clip.resize(lambda t: 1 + 0.14 * (t / duration))
        clip = clip.crop(
            x_center=WIDTH / 2,
            y_center=HEIGHT / 2,
            width=WIDTH,
            height=HEIGHT
        )

    elif motion_type == "천천히 μ€Œμ•„μ›ƒ":
        clip = clip.resize(1.16)
        clip = clip.resize(lambda t: 1.16 - 0.14 * (t / duration))
        clip = clip.crop(
            x_center=WIDTH / 2,
            y_center=HEIGHT / 2,
            width=WIDTH,
            height=HEIGHT
        )

    elif motion_type == "쒌우 이동":
        clip = clip.resize(1.20)
        clip = clip.crop(
            x1=lambda t: int(180 * (t / duration)),
            y_center=HEIGHT / 2,
            width=WIDTH,
            height=HEIGHT
        )

    elif motion_type == "μœ„μ—μ„œ μ•„λž˜λ‘œ 이동":
        clip = clip.resize(1.20)
        clip = clip.crop(
            x_center=WIDTH / 2,
            y1=lambda t: int(200 * (t / duration)),
            width=WIDTH,
            height=HEIGHT
        )

    elif motion_type == "μ•„λž˜μ—μ„œ μœ„λ‘œ 이동":
        clip = clip.resize(1.20)
        clip = clip.crop(
            x_center=WIDTH / 2,
            y1=lambda t: int(200 - 200 * (t / duration)),
            width=WIDTH,
            height=HEIGHT
        )

    elif motion_type == "ν•˜λŠ˜μ—μ„œ 내렀닀보기":
        clip = clip.resize(lambda t: 1.22 - 0.12 * (t / duration))
        clip = clip.crop(
            x_center=WIDTH / 2,
            y_center=lambda t: HEIGHT / 2 - 160 + 240 * (t / duration),
            width=WIDTH,
            height=HEIGHT
        )

    return clip


def make_video(img1, img2, img3, img4, motion_type, seconds_per_scene):
    images = [img1, img2, img3, img4]
    images = [img for img in images if img is not None]

    if not images:
        raise gr.Error("사진을 1μž₯ 이상 μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.")

    clips = []

    for img_file in images:
        scene_path = save_scene_image(img_file)
        clip = ImageClip(scene_path).set_duration(int(seconds_per_scene))
        clip = apply_motion(clip, motion_type, int(seconds_per_scene))
        clips.append(clip)

    final = concatenate_videoclips(clips, method="compose")

    output_path = os.path.join(OUTPUT_DIR, f"shorts_{uuid.uuid4()}.mp4")

    final.write_videofile(
        output_path,
        fps=24,
        codec="libx264",
        audio=False,
        preset="ultrafast",
        threads=2
    )

    return output_path, output_path


with gr.Blocks(title="ShortsVideoGenerater") as demo:
    gr.Markdown("# ShortsVideoGenerater")
    gr.Markdown(
        """
        사진 4μž₯을 μ—…λ‘œλ“œν•˜λ©΄ **유튜브 μ‡ΌμΈ μš© 9:16 μ˜μƒ**으둜 λ³€ν™˜ν•©λ‹ˆλ‹€.

        - 사진은 ν•œ μž₯μ”© μˆœμ„œλŒ€λ‘œ μž¬μƒλ©λ‹ˆλ‹€.
        - κΈ°λ³Έκ°’: 사진 1μž₯λ‹Ή 5초
        - 사진 4μž₯ μ—…λ‘œλ“œ μ‹œ 총 20초 μ˜μƒ
        - μžλ§‰ 없이 사진 μ˜μƒλ§Œ μƒμ„±λ©λ‹ˆλ‹€.
        """
    )

    with gr.Row():
        img1 = gr.File(label="μž₯λ©΄ 1 사진", file_types=["image"])
        img2 = gr.File(label="μž₯λ©΄ 2 사진", file_types=["image"])

    with gr.Row():
        img3 = gr.File(label="μž₯λ©΄ 3 사진", file_types=["image"])
        img4 = gr.File(label="μž₯λ©΄ 4 사진", file_types=["image"])

    with gr.Row():
        motion_type = gr.Radio(
            choices=[
                "μžλ™ μΆ”μ²œ",
                "천천히 쀌인",
                "천천히 μ€Œμ•„μ›ƒ",
                "쒌우 이동",
                "μœ„μ—μ„œ μ•„λž˜λ‘œ 이동",
                "μ•„λž˜μ—μ„œ μœ„λ‘œ 이동",
                "ν•˜λŠ˜μ—μ„œ 내렀닀보기",
            ],
            value="μžλ™ μΆ”μ²œ",
            label="ν™”λ©΄ μ›€μ§μž„"
        )

        seconds_per_scene = gr.Radio(
            choices=[4, 5, 6, 7],
            value=5,
            label="사진 1μž₯λ‹Ή μ‹œκ°„"
        )

    create_btn = gr.Button("유튜브 μ‡ΌμΈ  μƒμ„±ν•˜κΈ°", variant="primary")

    output_video = gr.Video(label="μƒμ„±λœ μ‡ΌμΈ  μ˜μƒ")
    download_file = gr.File(label="MP4 λ‹€μš΄λ‘œλ“œ")

    create_btn.click(
        fn=make_video,
        inputs=[
            img1, img2, img3, img4,
            motion_type,
            seconds_per_scene
        ],
        outputs=[
            output_video,
            download_file
        ]
    )


if __name__ == "__main__":
    demo.launch()