import os import tempfile import subprocess from pathlib import Path from fastapi import UploadFile _QUALITY = {"l": "l", "m": "m", "h": "h", "u": "u"} def _save_upload(tmpdir: Path, file: UploadFile) -> Path: ext = Path(file.filename or "file").suffix.lower() out = tmpdir / f"input{ext}" with out.open("wb") as f: f.write(file.file.read()) return out def _gen_scene_py(path: Path, anim: str, duration: float, bg_color: str) -> Path: is_svg = path.suffix.lower() == ".svg" safe_path = repr(str(path)) if anim == "draw" and not is_svg: raise ValueError("Draw modu yalnızca SVG kabul eder.") head = f"""from manim import * class LogoScene(Scene): def construct(self): self.camera.background_color = "{bg_color}" """ if anim == "draw": # Fill'leri kapat + stroke yoksa hafif bir stroke ata + parçalı çiz body = f""" m = SVGMobject({safe_path}, should_center=True).set_height(5).move_to(ORIGIN) # family_members_with_points: gerçekten çizilebilir alt-objeler for sm in m.family_members_with_points(): # fill kapalı başlasın ki Create sırasında sadece çizgi görünsün sm.set_fill(opacity=0) # stroke yoksa otomatik ver (birçok SVG'de yalnızca fill var) if sm.get_stroke_width() == 0: sm.set_stroke(color=WHITE, width=3, opacity=1) # parçalı çizim (birbirinin üstüne binecek şekilde) self.play( LaggedStartMap(Create, m.family_members_with_points(), lag_ratio=0.04, rate_func=linear), run_time={float(duration)} ) # çizim bittikten sonra dolguları geri getir self.play(m.animate.set_fill(opacity=1), run_time=0.6) self.wait(0.2) """ else: loader = ( f'SVGMobject({safe_path}, should_center=True).set_height(5)' if is_svg else f'ImageMobject({safe_path}).scale(0.9)' ) body_start = f"""m = {loader} m.move_to(ORIGIN) """ if anim == "fade": body_anim = f""" self.play(FadeIn(m), run_time=max(0.3, {float(duration)}*0.5)) self.wait(0.1) self.play(FadeOut(m), run_time=max(0.2, {float(duration)}*0.3)) """ elif anim == "spin": body_anim = f""" self.play(FadeIn(m, scale=0.8), run_time=max(0.2, {float(duration)}*0.2)) self.play(Rotate(m, angle=2*PI), rate_func=linear, run_time=max(0.4, {float(duration)}*0.6)) self.play(FadeOut(m), run_time=max(0.1, {float(duration)}*0.2)) """ elif anim == "bounce": body_anim = f""" from manim import UP, DOWN, there_and_back self.play(FadeIn(m, shift=UP*3), run_time=max(0.2, {float(duration)}*0.25)) self.play(m.animate.shift(DOWN*3).scale(1.02), rate_func=there_and_back, run_time=max(0.4, {float(duration)}*0.55)) self.play(FadeOut(m), run_time=max(0.15, {float(duration)}*0.2)) """ else: raise ValueError("Geçersiz animasyon tipi.") body = body_start + body_anim code = head + body + "\n" scene_py = path.parent / "logo_scene.py" scene_py.write_text(code, encoding="utf-8") return scene_py def _run_manim(scene_py: Path, quality: str) -> Path: q = _QUALITY.get(quality, "m") tmpdir = scene_py.parent out_name = "out.mp4" cmd = [ "manim", "-q", q, str(scene_py), "LogoScene", "-o", out_name, "--format=mp4", "--media_dir", str(tmpdir), ] proc = subprocess.run(cmd, cwd=tmpdir, capture_output=True, text=True) if proc.returncode != 0: raise RuntimeError(f"Manim failed: {proc.stderr or proc.stdout}") for root, _, files in os.walk(tmpdir): if out_name in files: return Path(root) / out_name raise RuntimeError("Çıktı mp4 bulunamadı.") async def render_logo(file: UploadFile, animation: str, duration: float, bg_color: str, quality: str) -> Path: with tempfile.TemporaryDirectory() as tdir: tdir = Path(tdir) input_path = _save_upload(tdir, file) scene_py = _gen_scene_py(input_path, animation, duration, bg_color) mp4 = _run_manim(scene_py, quality) final_dir = Path("/tmp/manim_out") final_dir.mkdir(parents=True, exist_ok=True) final_path = final_dir / mp4.name final_path.write_bytes(mp4.read_bytes()) return final_path