File size: 13,948 Bytes
71db123
63c2270
71db123
 
 
 
 
 
 
3321ce1
 
0cfcaa5
 
71db123
 
63c2270
71db123
 
 
 
 
 
 
 
63c2270
 
 
0cfcaa5
a26baec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4221935
987fe9c
71db123
987fe9c
a26baec
987fe9c
71db123
 
 
 
b165e2e
71db123
 
987fe9c
b165e2e
71db123
a26baec
71db123
 
 
a26baec
c92c393
a26baec
987fe9c
 
a26baec
71db123
 
a26baec
71db123
 
987fe9c
71db123
 
 
 
 
 
 
987fe9c
a26baec
987fe9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a26baec
 
71db123
 
 
ea4372e
a26baec
987fe9c
71db123
 
 
 
 
 
a26baec
71db123
987fe9c
 
 
a26baec
 
 
 
 
 
 
 
987fe9c
 
a26baec
71db123
 
 
 
 
a26baec
71db123
987fe9c
 
a26baec
71db123
987fe9c
 
 
 
 
 
 
 
 
 
 
a26baec
987fe9c
 
 
a26baec
987fe9c
 
a26baec
71db123
987fe9c
71db123
 
a26baec
71db123
987fe9c
 
71db123
 
 
c367b6d
a26baec
71db123
 
 
 
 
 
 
 
 
a26baec
71db123
987fe9c
 
71db123
a26baec
71db123
 
 
 
987fe9c
a26baec
ea4372e
71db123
a26baec
65183ad
71db123
63c2270
71db123
63c2270
71db123
bb94333
71db123
 
f6e5a20
bb94333
f0758ac
 
fd488e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c07a2d
 
fd488e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0758ac
fd488e9
 
 
 
0c07a2d
fd488e9
a26baec
71db123
63c2270
020a99a
71db123
63c2270
 
71db123
 
 
 
 
 
 
d8536ff
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
import os
import subprocess
import tempfile
import shutil
from datetime import datetime
import traceback
import json
import ast 
import re
import textwrap
from manim import *

app = Flask(__name__)
CORS(app)  # Enable CORS for all routes

# Configuration
BASE_DIR = "/app"
MEDIA_DIR = os.path.join(BASE_DIR, "media")
TEMP_DIR = os.path.join(BASE_DIR, "temp")
os.makedirs(MEDIA_DIR, exist_ok=True)
os.makedirs(TEMP_DIR, exist_ok=True)

# API Key for security (optional)
API_KEY = "rkmentormindzofficaltokenkey12345"



def make_wrapped_paragraph(content, max_width, color, font, font_size, line_spacing, align_left=True):
    """
    Build a vertically stacked group of Text lines that together form a paragraph.
    It splits content into lines that fit within max_width by measuring rendered width.
    Each line is a separate Text object joined into a VGroup and arranged downward.
    """
    words = content.split()
    lines = []
    current = ""

    # Create a temporary Text to measure width; use the same font/size as final lines
    temp = Text("", color=color, font=font, font_size=font_size)

    for w in words:
        test = w if not current else current + " " + w
        test_obj = Text(test, color=color, font=font, font_size=font_size)
        if test_obj.width <= max_width:
            current = test
        else:
            # flush the current line
            line = Text(current, color=color, font=font, font_size=font_size)
            lines.append(line)
            current = w
    if current:
        lines.append(Text(current, color=color, font=font, font_size=font_size))

    if not lines:
        return VGroup()

    para = VGroup(*lines)
    # Space lines vertically; arrange them as a column
    para.arrange(DOWN, buff=line_spacing)
    if align_left:
        para = para.align_to(LEFT)
    return para.strip()

def create_manim_script(problem_data, script_path):
    """Generate Manim script from problem data with robust wrapping for title, text, and equations."""

    # Defaults
    settings = problem_data.get("video_settings", {
        "background_color": "#0f0f23",
        "text_color": "WHITE",
        "highlight_color": "YELLOW",
        "font": "",
        "text_size": 36,
        "equation_size": 42,
        "title_size": 48,
        "wrap_width": 18.0  # in scene width units; adjust to taste
    })

    slides = problem_data.get("slides", [])
    if not slides:
        raise ValueError("No slides provided in input data")

    slides_repr = repr(slides)

    # Use a dedicated wrap width in scene units; you can adapt how max_width is computed
    wrap_width = float(settings.get("wrap_width", 12.0))

    manim_code = f'''
from manim import *
import textwrap
class GeneratedMathScene(Scene):
    def construct(self):
        # Scene settings
        self.camera.background_color = "{settings.get('background_color', '#0f0f23')}"
        default_color = {settings.get('text_color', 'WHITE')}
        highlight_color = {settings.get('highlight_color', 'YELLOW')}
        default_font = "{settings.get('font', 'CMU Serif')}"
        text_size = {settings.get('text_size', 36)}
        equation_size = {settings.get('equation_size', 42)}
        title_size = {settings.get('title_size', 48)}
        wrap_width = {wrap_width}
        
        # Helper to wrap text into lines that fit within max width
        def make_wrapped_paragraph(content, color, font, font_size, line_spacing=0.2):
            lines = []
            words = content.split()
            current = ""
            for w in words:
                test = w if not current else current + " " + w
                test_obj = Text(test, color=color, font=font, font_size=font_size)
                if test_obj.width <= wrap_width * 0.9:  # a bit of padding
                    current = test
                else:
                    lines.append(Text(current, color=color, font=font, font_size=font_size))
                    current = w
            if current:
                lines.append(Text(current, color=color, font=font, font_size=font_size))
            if not lines:
                return VGroup()
            para = VGroup(*lines).arrange(DOWN, buff=line_spacing)
            return para
        class GeneratedMathSceneInner(Scene):
            pass
        content_group = VGroup()
        current_y = 3.0
        line_spacing = 0.8
        slides = {slides_repr}
        
        # Build each slide
        for idx, slide in enumerate(slides):
            obj = None
            content = slide.get("content", "")
            animation = slide.get("animation", "write_left")
            duration = slide.get("duration", 1.0)
            slide_type = slide.get("type", "text")
            
            if slide_type == "title":
                # Wrap title text
                title_text = content
                # Use paragraph wrapping to keep multi-line titles readable
                lines = []
                if title_text:
                    lines = []
                    # Reuse make_wrapped_paragraph by simulating a single paragraph
                    lines_group = make_wrapped_paragraph(title_text, highlight_color, default_font, title_size, line_spacing=0.2)
                    obj = lines_group if len(lines_group) > 0 else Text(title_text, color=highlight_color, font=default_font, font_size=title_size)
                else:
                    obj = Text("", color=highlight_color, font=default_font, font_size=title_size)
                if obj.width > wrap_width:
                    obj.scale_to_fit_width(wrap_width)
                
                obj.move_to(ORIGIN)
                self.play(FadeIn(obj), run_time=duration * 0.8)
                self.wait(duration * 0.3)
                self.play(FadeOut(obj), run_time=duration * 0.3)
                continue
            
            elif slide_type == "text":
                # Use wrapping for normal text
                obj = make_wrapped_paragraph(content, default_color, default_font, text_size, line_spacing=0.25)
            
            elif slide_type == "equation":
                # Wrap long equations by splitting content into lines if needed
                # Heuristic: if content is too wide, create a multi-line TeX using \\ line breaks
                eq_content = content
                # Optional: insert line breaks at common math breakpoints if needed
                test = MathTex(eq_content, color=default_color, font_size=equation_size)
                if test.width > wrap_width:
                    # naive wrap: insert line breaks at spaces near the middle
                    parts = eq_content.split(" ")
                    mid = len(parts)//2
                    line1 = " ".join(parts[:mid])
                    line2 = " ".join(parts[mid:])
                    wrapped_eq = f"{{line1}} \\\\\\\\ {{line2}}"
                    obj = MathTex(wrapped_eq, color=default_color, font_size=equation_size)
                else:
                    obj = MathTex(eq_content, color=default_color, font_size=equation_size)
                
                if obj.width > wrap_width:
                    obj.scale_to_fit_width(wrap_width)
            
            if obj:
                # Position and animate
                obj.to_edge(LEFT, buff=0.3)
                obj.shift(UP * (current_y - obj.height/2))
                
                obj_bottom = obj.get_bottom()[1]
                if obj_bottom < -3.5:
                    scroll_amount = abs(obj_bottom - (-3.5)) + 0.3
                    self.play(content_group.animate.shift(UP * scroll_amount), run_time=0.5)
                    current_y += scroll_amount
                    obj.shift(UP * scroll_amount)
                    obj.to_edge(LEFT, buff=0.3)
                
                if animation == "write_left":
                    self.play(Write(obj), run_time=duration)
                elif animation == "fade_in":
                    self.play(FadeIn(obj), run_time=duration)
                elif animation == "highlight_left":
                    self.play(Write(obj), run_time=duration * 0.6)
                    self.play(obj.animate.set_color(highlight_color), run_time=duration * 0.4)
                else:
                    self.play(Write(obj), run_time=duration)
                
                content_group.add(obj)
                # Decrease y for next item
                current_y -= (getattr(obj, "height", 0) + line_spacing)
                self.wait(0.3)
        
        if len(content_group) > 0:
            final_box = SurroundingRectangle(content_group[-1], color=highlight_color, buff=0.2)
            self.play(Create(final_box), run_time=0.8)
            self.wait(1.5)
    '''

    with open(script_path, 'w', encoding='utf-8') as f:
        f.write(manim_code)
    
    print(f"Generated script preview (first 500 chars):{manim_code[:500]}...")

@app.route("/")
def home():
    return "Flask Manim Video Generator is Running"

@app.route("/generate", methods=["POST"])
def generate_video():
    try:
        raw_data = request.get_json()
        raw_body=raw_data.get("jsondata" , '')
        #print(f"Raw body length: {len(raw_body)}")
        #print(f"First 200 chars: {raw_body[:200]}")
        lst = raw_body.split("&&&&")
        cleaned = re.sub(r'(\d)\s*\.\s*(\d)', r'\1.\2', lst[0])
        nlist = ast.literal_eval(cleaned)
        datalst=[]
        for line in range(len(nlist)):
          datalst.append({
                            "type": nlist[line][0].strip(),
                            "content": nlist[line][1].strip(),
                            "animation": nlist[line][2].strip().replace(" ",""),
                            "duration": nlist[line][3]
                        })
        
        data={
    "video_settings": {
        "background_color": "#0f0f23",
        "text_color": "WHITE",
        "highlight_color": "YELLOW",
        "font": "CMU Serif",
        "text_size": 36,
        "equation_size": 42,
        "title_size": 48
    },
    "slides":datalst}
        # Now proceed with video generation using 'data'
        print(json.dumps(data, indent=2))  # For debugging
        # ✅ Final validation
        if "slides" not in data or not data["slides"]:
            return jsonify({"error": "No slides provided in request"}), 400
        
        print(f"✅ Parsed {len(data['slides'])} slides successfully.")
        
        # Validate input
        if "slides" not in data or not data["slides"]:
            return jsonify({"error": "No slides provided in request"}), 400
        
        print(f"Received request with {len(data['slides'])} slides")
        
        # Create unique temporary directory
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        temp_work_dir = os.path.join(TEMP_DIR, f"manim_{timestamp}")
        os.makedirs(temp_work_dir, exist_ok=True)
        
        # Generate Manim script
        script_path = os.path.join(temp_work_dir, "scene.py")
        create_manim_script(data, script_path)
        print(f"Created Manim script at {script_path}")
        
        # Render video using subprocess
        quality = 'l'  # l=low, m=medium, h=high
        render_command = [
            "manim",
            f"-q{quality}",
            "--disable_caching",
            "--media_dir", temp_work_dir,
            script_path,
            "GeneratedMathScene"
        ]
        
        print(f"Running command: {' '.join(render_command)}")
        
        result = subprocess.run(
            render_command,
            capture_output=True,
            text=True,
            cwd=temp_work_dir,
            timeout=120
        )
        
        if result.returncode != 0:
            error_msg = result.stderr or result.stdout
            print(f"Manim rendering failed: {error_msg}")
            return jsonify({
                "error": "Manim rendering failed",
                "details": error_msg
            }), 500
        
        print("Manim rendering completed successfully")
        
        # Find generated video
        quality_map = {'l': '480p15', 'm': '720p30', 'h': '1080p60'}
        video_quality = quality_map.get(quality, '480p15')
        
        video_path = os.path.join(
            temp_work_dir,
            "videos",
            "scene",
            video_quality,
            "GeneratedMathScene.mp4"
        )
        
        if not os.path.exists(video_path):
            print(f"Video not found at expected path: {video_path}")
            return jsonify({
                "error": "Video file not found after rendering",
                "expected_path": video_path
            }), 500
        
        print(f"Video found at: {video_path}")
        
        # Copy to media directory
        output_filename = f"math_video_{timestamp}.mp4"
        output_path = os.path.join(MEDIA_DIR, output_filename)
        shutil.copy(video_path, output_path)
        print(f"Video copied to: {output_path}")
        
        # Clean up temp directory
        try:
            shutil.rmtree(temp_work_dir)
            print("Cleaned up temp directory")
        except Exception as e:
            print(f"Failed to clean temp dir: {e}")

        return send_file(
            output_path,
            mimetype='video/mp4',
            as_attachment=False,
            download_name=output_filename
        )
        
    except subprocess.TimeoutExpired:
        print("Video rendering timeout")
        return jsonify({"error": "Video rendering timeout (120s)"}), 504
    except Exception as e:
        print(f"Error: {str(e)}")
        traceback.print_exc()
        return jsonify({
            "error": str(e),
            "traceback": traceback.format_exc()
        }), 500

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 7860))
    app.run(host='0.0.0.0', port=port, debug=False)