File size: 7,581 Bytes
c5fce8d
 
23ca8b2
47ad655
712f7dc
c5fce8d
0ab57e4
c5fce8d
4a85e93
c5fce8d
4a85e93
9743092
75b886f
4a85e93
0ab57e4
c5fce8d
d5b5aa3
70e00bc
 
d5b5aa3
c5fce8d
 
d5b5aa3
c5fce8d
 
 
 
 
 
 
 
 
 
 
 
 
70e00bc
c5fce8d
 
70e00bc
c5fce8d
70e00bc
494fb2c
c5fce8d
b30ca71
40692fd
2905477
 
 
 
 
 
40692fd
 
 
 
 
c5fce8d
 
70e00bc
 
 
 
 
2905477
70e00bc
 
 
 
 
 
 
 
c5fce8d
 
 
 
 
 
 
23ca8b2
c5fce8d
 
 
 
 
 
 
 
0ab57e4
 
 
70e00bc
0ab57e4
c5fce8d
 
 
 
 
 
0af78ac
c5fce8d
4a85e93
 
 
 
 
 
 
c5fce8d
 
70e00bc
 
 
 
 
 
c5fce8d
 
 
 
d5b5aa3
712f7dc
 
d5b5aa3
70e00bc
d5b5aa3
 
c5fce8d
d5b5aa3
70e00bc
 
4a85e93
 
 
 
 
 
b30ca71
70e00bc
 
 
 
4a85e93
 
 
 
 
 
 
47ad655
4a85e93
 
47ad655
70e00bc
4a85e93
70e00bc
4a85e93
 
c2a1189
70e00bc
 
23ca8b2
70e00bc
 
 
4a85e93
70e00bc
 
 
 
d5b5aa3
 
 
 
4a85e93
70e00bc
 
4a85e93
70e00bc
d5b5aa3
712f7dc
c5fce8d
70e00bc
712f7dc
70e00bc
b30ca71
d5b5aa3
 
 
4e8e2ef
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
import ffmpeg
import os
import numpy as np
import cv2
import json
import gradio as gr
from concurrent.futures import ThreadPoolExecutor, TimeoutError
import tempfile
from PIL import Image

# Thêm đường dẫn FFmpeg
os.environ["PATH"] += os.pathsep + r"D:\Downloads\ffmpeg-7.1.1-essentials_build\ffmpeg-7.1.1-essentials_build\bin"

TIMEOUT = 300  # 5 phút

def wrap_text(text, max_width=1060, font_size=20):
    words = text.split()
    lines = []
    current_line = ""
    for word in words:
        if len(current_line + " " + word) * font_size <= max_width:
            current_line += " " + word
        else:
            lines.append(current_line.strip())
            current_line = word
    if current_line:
        lines.append(current_line.strip())
    return "\n".join(lines)

def calculate_text_height(text, font_size=20, line_spacing=10):
    lines = text.split("\n")
    return len(lines) * font_size + (len(lines) - 1) * line_spacing

def create_single_video(args):
    img, script, dur, output_path, width, height, fps = args
    temp_img_path = tempfile.NamedTemporaryFile(suffix='.png', delete=False).name
    Image.fromarray(img).save(temp_img_path)

    d_frames = int(dur * fps)
    wrapped_text = wrap_text(script, width + 460, font_size=20)
    wrapped_text = wrapped_text.replace(":", "\\:").replace("'", "\\'")
    text_height = calculate_text_height(wrapped_text, font_size=20, line_spacing=10)
    y_position = height - text_height - 36

    font_path = "fonts/Roboto-VariableFont_wdth\,wght.ttf"

    # vf = (
    #     f"scale=2400:-1,"
    #     f"zoompan=z='min(zoom+0.0001,1.5)':x='floor(iw/2-(iw/zoom/2))':y='floor(ih/2-(ih/zoom/2))':d={d_frames}:s={width}x{height}:fps={fps},"
    #     f"drawtext=text='{wrapped_text}':fontsize=20:fontcolor=white:x=(w-text_w)/2:y={y_position}:"
    #     f"fontfile={font_path}:box=1:[email protected]:boxborderw=10:line_spacing=10"
    # )
    vf = (
        f"drawtext=text='{wrapped_text}':fontsize=20:fontcolor=white:x=(w-text_w)/2:y={y_position}:"
        f"fontfile={font_path}:box=1:[email protected]:boxborderw=10:line_spacing=10"
    )

    try:
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        stream = ffmpeg.input(temp_img_path, loop=1, t=dur)
        stream = stream.output(output_path, **{
            'vf': vf,
            't': dur,
            'pix_fmt': 'yuv420p',
            'crf': '17',
            'c:v': 'libx264',
            'an': None
        })
        print("FFmpeg command:", stream.compile())
        out, err = stream.run(capture_stdout=True, capture_stderr=True)
    except ffmpeg.Error as e:
        print('FFmpeg Error:', e.stderr.decode('utf-8'))
        raise
    finally:
        if os.path.exists(temp_img_path):
            os.remove(temp_img_path)

    return output_path

def create_video_from_images(images, scripts, durations, audio_path, output_path, fps=60):
    height, width, _ = images[0].shape
    temp_dir = tempfile.mkdtemp()
    video_paths = []

    with ThreadPoolExecutor(max_workers=2) as executor:
        tasks = [
            (img, script, dur, os.path.join(temp_dir, f"temp_{i}.mp4"), width, height, fps)
            for i, (img, script, dur) in enumerate(zip(images, scripts, durations))
        ]
        try:
            video_paths = list(executor.map(create_single_video, tasks, timeout=TIMEOUT))
        except TimeoutError:
            print("Timeout Error: Operation took too long to complete.")
            return None

    concat_file = os.path.join(temp_dir, "concat.txt")
    with open(concat_file, 'w') as f:
        for path in video_paths:
            f.write(f"file '{path}'\n")

    ffmpeg.input(concat_file, format='concat', safe=0).output(output_path, c='copy', an=None).overwrite_output().run()

    if audio_path:
        final_output_path = output_path.replace(".mp4", "_with_audio.mp4")
        video_input = ffmpeg.input(output_path)
        audio_input = ffmpeg.input(audio_path)
        ffmpeg.output(video_input, audio_input, final_output_path, vcodec='libx264', acodec='aac', shortest=None).overwrite_output().run()
    else:
        final_output_path = output_path

    for path in video_paths:
        if os.path.exists(path):
            os.remove(path)
    if os.path.exists(concat_file):
        os.remove(concat_file)
    if os.path.exists(temp_dir):
        os.rmdir(temp_dir)

    return final_output_path

def generate_video(image_files, script_input, duration_input, audio_file, fps=60):
    try:
        scripts = json.loads(script_input)
        durations = json.loads(duration_input)
    except Exception as e:
        return None, f"❌ Lỗi khi phân tích đầu vào:\n{e}"

    if len(image_files) != len(scripts) or len(scripts) != len(durations):
        return None, "❌ Số lượng ảnh, scripts và durations phải bằng nhau!"

    try:
        os.makedirs("outputs", exist_ok=True)
        # Xử lý audio_file
        audio_path = None
        if audio_file:
            audio_path = audio_file.name
            if not os.path.exists(audio_path):
                raise ValueError(f"Tệp âm thanh không tồn tại: {audio_path}")
        output_video_path = os.path.join("outputs", "temp_output.mp4")

        images = []
        for idx, img_file in enumerate(image_files):
            try:
                # Lấy đường dẫn tệp từ NamedString
                img_path = img_file.name
                if not os.path.exists(img_path):
                    raise ValueError(f"Tệp ảnh không tồn tại tại index {idx+1}: {img_path}")

                # Đọc ảnh từ đường dẫn
                img = cv2.imread(img_path)
                if img is None:
                    raise ValueError(f"Không đọc được ảnh tại index {idx+1}: {img_path}")

                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                images.append(img)
                print(f"✅ Ảnh {idx+1}: {img_path} - kích thước {img.shape}")
            except Exception as e:
                print(f"❌ Không đọc được ảnh {idx+1} - lỗi: {e}")
                return None, f"❌ Lỗi đọc ảnh tại index {idx+1}: {e}"

        if not images:
            return None, "❌ Không có ảnh nào hợp lệ để tạo video!"

        final_video_path = create_video_from_images(images, scripts, durations, audio_path, output_video_path, fps)
        if final_video_path is None:
            return None, "❌ Quá trình tạo video đã bị timeout!"

        return final_video_path, "✅ Video tạo thành công!"
    except Exception as e:
        import traceback
        return None, f"❌ Lỗi khi tạo video:\n{traceback.format_exc()}"

demo = gr.Interface(
    fn=generate_video,
    inputs=[
        gr.File(file_types=None, label="Ảnh (nhiều)", file_count="multiple"),
        gr.Textbox(label="Scripts (danh sách)", placeholder="['Chào bạn', 'Video demo']"),
        gr.Textbox(label="Durations (giây)", placeholder="[3, 4]"),
        gr.File(file_types=None, label="Nhạc nền (bất kỳ định dạng)"),  # Bỏ kiểm tra định dạng
        gr.Slider(minimum=1, maximum=120, step=1, label="FPS (frame/giây)", value=60),
    ],
    outputs=[
        gr.Video(label="Video kết quả"),
        gr.Textbox(label="Trạng thái", interactive=False),
    ],
    title="Tạo video từ ảnh, chữ và nhạc",
    description="Upload nhiều ảnh + đoạn chữ + nhạc nền để tạo video tự động."
)

if __name__ == "__main__":
    demo.launch(show_error=True, share=True)