File size: 5,232 Bytes
d970572
 
 
 
 
2e31ab2
 
d970572
 
2e31ab2
 
 
 
 
d970572
 
 
 
2e31ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d970572
2e31ab2
 
 
 
d970572
2e31ab2
d970572
 
2e31ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d970572
2e31ab2
d970572
 
2e31ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d970572
2e31ab2
 
 
 
d970572
2e31ab2
 
 
 
 
 
 
 
 
d970572
2e31ab2
d970572
2e31ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d970572
2e31ab2
 
 
d970572
 
 
 
2e31ab2
 
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
import re
import subprocess
import os
import glob
import logging
import platform


def get_scene_name(manim_code):
    """Extracts the scene class name from Manim code."""
    # This regex looks for 'class YourSceneName(Scene):' or 'class YourSceneName(ThreeDScene):'
    match = re.search(
        r"class\s+(\w+)\s*\(\s*(?:ThreeD|Multi)?[Ss]cene\s*\)", manim_code
    )
    if match:
        return match.group(1)
    raise ValueError("No Scene class found in generated code")


def sanitize_path_for_ffmpeg(path: str) -> str:
    if platform.system() == "Windows":
        # For Windows
        return path.replace("\\", "\\\\").replace(":", "\\:")
    else:
        # For Linux/macOS
        return (
            path.replace("'", "'\\''")
            .replace(":", "\\:")
            .replace(",", "\\,")
            .replace("[", "\\[")
            .replace("]", "\\]")
        )


def create_manim_video(video_data, manim_code, audio_file=None, subtitle_file=None):
    logging.info("Starting to create Manim video")
    with open("generated_video.py", "w", encoding="utf-8") as f:
        f.write(manim_code)

    scene_name = get_scene_name(manim_code)
    logging.info(f"Identified scene name: {scene_name}")

    command = ["manim", "-qh", "generated_video.py", scene_name]
    logging.info(f"Running Manim with command: {' '.join(command)}")

    # Use capture_output=True to get stderr for better error reporting
    manim_process = subprocess.run(command, check=True, capture_output=True, text=True)
    if manim_process.returncode != 0:
        logging.error(f"Manim failed with stderr:\n{manim_process.stderr}")
        raise subprocess.CalledProcessError(
            manim_process.returncode, command, stderr=manim_process.stderr
        )

    video_path = os.path.join(
        "media", "videos", "generated_video", "1080p60", f"{scene_name}.mp4"
    )
    if not os.path.exists(video_path):
        logging.error(f"No rendered video found at: {video_path}")
        raise FileNotFoundError(f"No rendered video found for scene {scene_name}")

    input_video = video_path
    final_output = "final_output.mp4"
    extended_video_temp = "extended_video.mp4"

    if audio_file and os.path.exists(audio_file):
        logging.info(f"Audio file found: {audio_file}")

        video_duration_cmd = [
            "ffprobe",
            "-v",
            "error",
            "-show_entries",
            "format=duration",
            "-of",
            "default=noprint_wrappers=1:nokey=1",
            input_video,
        ]
        audio_duration_cmd = [
            "ffprobe",
            "-v",
            "error",
            "-show_entries",
            "format=duration",
            "-of",
            "default=noprint_wrappers=1:nokey=1",
            audio_file,
        ]

        video_duration = float(
            subprocess.check_output(video_duration_cmd).decode("utf-8").strip()
        )
        audio_duration = float(
            subprocess.check_output(audio_duration_cmd).decode("utf-8").strip()
        )

        logging.info(
            f"Video duration: {video_duration}s, Audio duration: {audio_duration}s"
        )

        # If audio is longer, extend the video with a freeze frame of the last frame
        if audio_duration > video_duration:
            logging.info(
                "Audio is longer than video, extending video with freeze frame."
            )

            extend_cmd = [
                "ffmpeg",
                "-y",
                "-i",
                input_video,
                "-vf",
                f"tpad=stop_mode=clone:stop_duration={audio_duration - video_duration}",
                "-c:v",
                "libx264",
                extended_video_temp,
            ]

            logging.info(f"Extending video with command: {' '.join(extend_cmd)}")
            subprocess.run(extend_cmd, check=True, capture_output=True, text=True)
            input_video = extended_video_temp  # The extended video is now our input

    # merge
    merge_cmd = ["ffmpeg", "-y", "-i", input_video]

    if audio_file and os.path.exists(audio_file):
        merge_cmd.extend(["-i", audio_file])

    filter_complex = []
    maps = ["-map", "0:v:0"]
    if audio_file and os.path.exists(audio_file):
        maps.extend(["-map", "1:a:0"])

    # Add subtitle
    if subtitle_file and os.path.exists(subtitle_file):
        sanitized_path = sanitize_path_for_ffmpeg(os.path.abspath(subtitle_file))
        filter_complex.append(f"ass='{sanitized_path}'")

    if filter_complex:
        merge_cmd.extend(["-vf", ",".join(filter_complex)])

    merge_cmd.extend(maps)
    merge_cmd.extend(["-c:v", "libx264", "-c:a", "aac", "-shortest", final_output])

    logging.info(f"Merging with final command: {' '.join(merge_cmd)}")
    subprocess.run(merge_cmd, check=True, capture_output=True, text=True)

    if os.path.exists(extended_video_temp):
        os.remove(extended_video_temp)
        logging.info("Removed temporary extended video file.")
    if os.path.exists("generated_video.py"):
        os.remove("generated_video.py")
        logging.info("Removed generated_video.py")

    logging.info(f"Final video created at: {final_output}")
    return final_output