Bandrik0 commited on
Commit
8fd6f6f
·
1 Parent(s): d775c67

fix(renderer): valid Scene code + robust CLI call

Browse files
Files changed (1) hide show
  1. backend/renderer.py +127 -108
backend/renderer.py CHANGED
@@ -1,117 +1,136 @@
1
  import os
2
- import subprocess
3
  import tempfile
 
4
  from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
5
 
 
 
 
 
 
 
 
 
 
6
 
7
- def _scene_code(input_path: str, animation: str, duration: float, bg_color: str) -> str:
8
- is_svg = str(input_path).lower().endswith(".svg")
9
- return f"""
10
- from manim import *
11
- from manim import rate_functions as rf
12
 
13
  class LogoScene(Scene):
14
  def construct(self):
15
- config.background_color = "{bg_color}"
16
- path = r{input_path!r}
17
- is_svg = {str(is_svg)}
18
-
19
- if is_svg:
20
- try:
21
- mobj = SVGMobject(path)
22
- mobj.set_height(5).move_to(ORIGIN)
23
-
24
- if {animation!r} == "draw":
25
- # Fill'leri kapat, stroke'u görünür yap
26
- for sm in mobj.family_members_with_points():
27
- if hasattr(sm, "set_fill"):
28
- sm.set_fill(opacity=0)
29
- if hasattr(sm, "set_stroke"):
30
- sm.set_stroke((sm.stroke_color or WHITE),
31
- width=(sm.stroke_width or 2),
32
- opacity=1.0)
33
-
34
- parts = [sm for sm in mobj.family_members_with_points() if sm.has_points()]
35
- if len(parts) == 0:
36
- self.add(mobj)
37
- self.play(FadeIn(mobj, run_time={duration}))
38
- self.wait(0.2)
39
- return
40
-
41
- self.play(
42
- LaggedStart(*[Create(p) for p in parts], lag_ratio=0.05),
43
- run_time=max(0.6, {duration}*0.85)
44
- )
45
- # fill'leri geri getir
46
- self.play(
47
- *[p.animate.set_fill(opacity=1.0) for p in parts if hasattr(p, "set_fill")],
48
- run_time=max(0.2, {duration}*0.15)
49
- )
50
- self.wait(0.2)
51
- return
52
- except Exception:
53
- is_svg = False
54
-
55
- # SVG değilse (veya draw değilse) fallback
56
- if not is_svg:
57
- mobj = ImageMobject(path).set_height(5).move_to(ORIGIN)
58
-
59
- if {animation!r} == "spin":
60
- self.play(FadeIn(mobj, run_time=max(0.2, {duration}*0.25), scale=0.8))
61
- self.play(Rotate(mobj, angle=TAU, run_time=max(0.4, {duration}*0.75)))
62
- self.wait(0.2)
63
- elif {animation!r} == "bounce":
64
- # küçük başlat → orijinal boyuta "zıplayarak" gel
65
- mobj.scale(0.01)
66
- self.add(mobj)
67
- self.play(
68
- mobj.animate.scale(100),
69
- run_time=max(0.4, {duration}*0.6),
70
- rate_func=rf.ease_out_back
71
- )
72
- self.play(
73
- mobj.animate.scale(0.95),
74
- run_time=max(0.2, {duration}*0.2),
75
- rate_func=rf.ease_out_sine
76
- )
77
- self.wait(0.2)
78
  else:
79
- # fade
80
- self.play(FadeIn(mobj, run_time={duration}))
81
- self.wait(0.3)
82
- """
83
-
84
-
85
- def render_logo(input_path: str, output_path: str, animation: str, duration: float, bg_color: str, quality: str = "m"):
86
- qflag = {"l": "-ql", "m": "-qm", "h": "-qh", "u": "-qu"}.get(quality.lower(), "-qm")
87
-
88
- with tempfile.TemporaryDirectory() as tmpdir:
89
- scene_file = Path(tmpdir) / "logo_scene.py"
90
- scene_file.write_text(_scene_code(str(input_path), animation, duration, bg_color), encoding="utf-8")
91
-
92
- cmd = [
93
- "manim",
94
- qflag,
95
- str(scene_file),
96
- "LogoScene",
97
- "-o", "logo_render.mp4",
98
- "--format", "mp4",
99
- "--disable_caching",
100
- "--media_dir", tmpdir,
101
- ]
102
- try:
103
- subprocess.run(cmd, check=True, capture_output=True, text=True)
104
- except subprocess.CalledProcessError as e:
105
- raise RuntimeError(f"Manim failed: {e.stderr or e.stdout}")
106
-
107
- vid = None
108
- for p in Path(tmpdir).rglob("*.mp4"):
109
- if p.name.endswith(".mp4"):
110
- vid = p
111
- break
112
- if not vid:
113
- raise RuntimeError("Render output not found.")
114
-
115
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
116
- with open(vid, "rb") as src, open(output_path, "wb") as dst:
117
- dst.write(src.read())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
2
  import tempfile
3
+ import subprocess
4
  from pathlib import Path
5
+ from typing import Tuple
6
+ from fastapi import UploadFile
7
+
8
+ # Manim kalite kısayolları
9
+ _QUALITY = {"l": "l", "m": "m", "h": "h", "u": "u"}
10
+
11
+ def _save_upload(tmpdir: Path, file: UploadFile) -> Path:
12
+ ext = Path(file.filename or "file").suffix.lower()
13
+ out = tmpdir / f"input{ext}"
14
+ with out.open("wb") as f:
15
+ f.write(file.file.read())
16
+ return out
17
 
18
+ def _gen_scene_py(
19
+ path: Path, anim: str, duration: float, bg_color: str
20
+ ) -> Path:
21
+ """
22
+ logo_scene.py dosyasını üretir ve yolunu döner.
23
+ 'draw' yalnızca SVG kabul eder.
24
+ """
25
+ is_svg = path.suffix.lower() == ".svg"
26
+ safe_path = repr(str(path)) # path'i güvenli biçimde göm
27
 
28
+ if anim == "draw" and not is_svg:
29
+ raise ValueError("Draw animasyonu yalnızca SVG ile çalışır.")
30
+
31
+ # Ortak başlangıç
32
+ head = f"""from manim import *
33
 
34
  class LogoScene(Scene):
35
  def construct(self):
36
+ self.camera.background_color = "{bg_color}"
37
+ """
38
+
39
+ if anim == "draw":
40
+ body = f"""
41
+ m = SVGMobject({safe_path}).set_height(5).move_to(ORIGIN)
42
+ self.play(Create(m), run_time={float(duration)}, rate_func=linear)
43
+ self.wait(0.2)
44
+ """
45
+ else:
46
+ # Raster ve SVG ikisi de desteklensin
47
+ loader = (
48
+ f'SVGMobject({safe_path}).set_height(5)'
49
+ if is_svg else
50
+ f'ImageMobject({safe_path}).scale(0.9)'
51
+ )
52
+ body_start = f"""m = {loader}
53
+ m.move_to(ORIGIN)
54
+ """
55
+
56
+ if anim == "fade":
57
+ body_anim = f"""
58
+ self.play(FadeIn(m), run_time=max(0.3, {float(duration)}*0.5))
59
+ self.wait(0.1)
60
+ self.play(FadeOut(m), run_time=max(0.2, {float(duration)}*0.3))
61
+ """
62
+ elif anim == "spin":
63
+ body_anim = f"""
64
+ self.play(FadeIn(m, scale=0.8), run_time=max(0.2, {float(duration)}*0.2))
65
+ self.play(Rotate(m, angle=2*PI), rate_func=linear, run_time=max(0.4, {float(duration)}*0.6))
66
+ self.play(FadeOut(m), run_time=max(0.1, {float(duration)}*0.2))
67
+ """
68
+ elif anim == "bounce":
69
+ body_anim = f"""
70
+ from manim import UP, DOWN, there_and_back
71
+ self.play(FadeIn(m, shift=UP*3), run_time=max(0.2, {float(duration)}*0.25))
72
+ self.play(m.animate.shift(DOWN*3).scale(1.02), rate_func=there_and_back, run_time=max(0.4, {float(duration)}*0.55))
73
+ self.play(FadeOut(m), run_time=max(0.15, {float(duration)}*0.2))
74
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  else:
76
+ raise ValueError("Geçersiz animasyon tipi.")
77
+
78
+ body = body_start + body_anim
79
+
80
+ code = head + body + "\n"
81
+ scene_py = path.parent / "logo_scene.py"
82
+ scene_py.write_text(code, encoding="utf-8")
83
+ return scene_py
84
+
85
+ def _run_manim(scene_py: Path, quality: str) -> Path:
86
+ """
87
+ Manim'i çalıştırır ve üretilen mp4 yolunu döndürür.
88
+ """
89
+ q = _QUALITY.get(quality, "m")
90
+ tmpdir = scene_py.parent
91
+ out_name = "out.mp4"
92
+
93
+ cmd = [
94
+ "manim",
95
+ "-q", q,
96
+ str(scene_py),
97
+ "LogoScene",
98
+ "-o", out_name,
99
+ "--format=mp4",
100
+ "--media_dir", str(tmpdir),
101
+ ]
102
+
103
+ proc = subprocess.run(
104
+ cmd, cwd=tmpdir, capture_output=True, text=True
105
+ )
106
+ if proc.returncode != 0:
107
+ raise RuntimeError(f"Manim failed: {proc.stderr or proc.stdout}")
108
+
109
+ # media_dir altında mp4'ü bul
110
+ for root, _, files in os.walk(tmpdir):
111
+ if out_name in files:
112
+ return Path(root) / out_name
113
+
114
+ raise RuntimeError("Çıktı mp4 bulunamadı.")
115
+
116
+ async def render_logo(
117
+ file: UploadFile,
118
+ animation: str,
119
+ duration: float,
120
+ bg_color: str,
121
+ quality: str,
122
+ ) -> Path:
123
+ """
124
+ FastAPI endpoint'inin kullandığı ana fonksiyon.
125
+ """
126
+ with tempfile.TemporaryDirectory() as tdir:
127
+ tdir = Path(tdir)
128
+ input_path = _save_upload(tdir, file)
129
+ scene_py = _gen_scene_py(input_path, animation, duration, bg_color)
130
+ mp4 = _run_manim(scene_py, quality)
131
+ # mp4’ü kalıcı path’e taşı (tmp kapanınca silinmesin diye)
132
+ final_dir = Path("/tmp/manim_out")
133
+ final_dir.mkdir(parents=True, exist_ok=True)
134
+ final_path = final_dir / mp4.name
135
+ final_path.write_bytes(mp4.read_bytes())
136
+ return final_path