legawa / video /script.py
pebaryan
feat: add ethics guardrails, verifier, beranda + kredit tabs
00ce06b
Raw
History Blame Contribute Delete
12.7 kB
"""Legawa Explainer Video β€” Manim CE script.
Scenes:
1. The Problem β€” stack of RUU documents
2. What Legawa Does β€” 4 agent cards
3. How It Works β€” flow diagram
4. Free & Open β€” tokens + links
Rendered with: manim -qm script.py Scene1_Problem Scene2_Agents Scene3_Flow Scene4_Closing
"""
from manim import *
# ── Palette (Warm Academic) ──────────────────────────────────────────────
BG = "#1a1a2e"
TITLE = "#eaeaea"
BODY = "#cccccc"
PRIMARY = "#ff6b6b"
SECONDARY = "#ffd93d"
ACCENT = "#6bcb77"
MUTED = "#666666"
MONO = "monospace"
# ── Helpers ──────────────────────────────────────────────────────────────
def T(text, size=36, color=TITLE, weight=NORMAL):
"""Shorthand for monospace Text."""
return Text(text, font_size=size, font=MONO, color=color, weight=weight)
def card_box(width=2.2, height=1.2, color=PRIMARY):
"""Rounded rect for agent cards."""
return RoundedRectangle(
width=width, height=height,
corner_radius=0.15,
stroke_color=color, stroke_width=2.5,
fill_color=color, fill_opacity=0.12,
)
# ═══════════════════════════════════════════════════════════════════════════
# Scene 1 β€” The Problem
# ═══════════════════════════════════════════════════════════════════════════
class Scene1_Problem(Scene):
def construct(self):
self.camera.background_color = BG
# Papers piling up
papers = VGroup()
labels = ["RUU", "RUU", "RUU", "RUU", "RUU", "RUU"]
colors = [PRIMARY, SECONDARY, ACCENT, PRIMARY, SECONDARY, ACCENT]
for i, (label, c) in enumerate(zip(labels[:6], colors[:6])):
w, h = 2.6 - i * 0.15, 3.2 - i * 0.15
x_shift = i * 0.06
y_shift = i * 0.08 - 1.0
paper = Rectangle(
width=w, height=h,
stroke_color=c, stroke_width=2,
fill_color=c, fill_opacity=0.08,
).shift(RIGHT * x_shift + UP * y_shift)
text = T(label, size=18, color=c).move_to(paper)
papers.add(paper, text)
self.play(
LaggedStart(*[FadeIn(p, shift=UP * 0.3, scale=0.9) for p in papers],
lag_ratio=0.12),
run_time=2.5,
)
self.wait(0.5)
# Stressed legislator text
stress = T("?", size=72, color=PRIMARY, weight=BOLD).shift(RIGHT * 3.2 + UP * 1.0)
self.play(Write(stress), run_time=0.5)
self.wait(0.3)
# Problem text above the stack
problem_text = T(
"Ratusan RUU setiap tahun...\nberhari-hari riset per dokumen",
size=26, color=BODY,
).to_edge(UP, buff=0.5)
self.play(Write(problem_text), run_time=1.2)
self.wait(1.5)
# Fade out to Legawa logo
logo = T("πŸ›οΈ Legawa", size=56, color=SECONDARY, weight=BOLD)
subtitle = T("Multi-Agent AI untuk Legislator", size=24, color=MUTED).next_to(logo, DOWN)
self.play(
FadeOut(VGroup(*papers, stress, problem_text)),
run_time=0.8,
)
self.play(Write(logo), run_time=1.0)
self.play(Write(subtitle), run_time=0.8)
self.wait(1.5)
self.play(FadeOut(VGroup(logo, subtitle)), run_time=0.5)
# ═══════════════════════════════════════════════════════════════════════════
# Scene 2 β€” What Legawa Does
# ═══════════════════════════════════════════════════════════════════════════
class Scene2_Agents(Scene):
def construct(self):
self.camera.background_color = BG
title = T("Empat Agen dalam Satu Asisten", size=40, color=TITLE, weight=BOLD)
title.to_edge(UP, buff=0.5)
self.play(Write(title), run_time=0.8)
self.wait(0.3)
# 4 agent cards in a 2x2 grid
agents = [
("πŸ“„ Analisis\nRUU", PRIMARY, "Analisis pasal-per-pasal\nlengkap sitasi"),
("πŸ” Riset\nHukum", ACCENT, "Cari peraturan di\npasal.id"),
("✍️ Draf\nDokumen", SECONDARY, "Pidato, naskah akademik,\nmemo kebijakan"),
("πŸ“¬ Surat\nKonstituen", PRIMARY, "Triase & draft\nbalasan"),
]
positions = [
LEFT * 3 + UP * 0.5,
RIGHT * 3 + UP * 0.5,
LEFT * 3 + DOWN * 1.8,
RIGHT * 3 + DOWN * 1.8,
]
cards = VGroup()
for (emoji_name, color, desc), pos in zip(agents, positions):
box = RoundedRectangle(
width=4.5, height=2.2,
corner_radius=0.15,
stroke_color=color, stroke_width=2.5,
fill_color=color, fill_opacity=0.1,
).move_to(pos)
name_text = T(emoji_name, size=30, color=color, weight=BOLD)
name_text.move_to(box).shift(UP * 0.3)
desc_text = T(desc, size=18, color=BODY)
desc_text.next_to(name_text, DOWN, buff=0.3)
cards.add(VGroup(box, name_text, desc_text))
# Animate in one by one
for card in cards:
self.play(FadeIn(card, shift=UP * 0.3, scale=0.95), run_time=0.6)
self.wait(0.15)
self.wait(1.0)
# Tagline
tag = T("Small models, big adventure πŸ•οΈ", size=24, color=SECONDARY)
tag.to_edge(DOWN, buff=0.4)
self.play(Write(tag), run_time=0.8)
self.wait(1.5)
self.play(FadeOut(VGroup(title, cards, tag)), run_time=0.5)
# ═══════════════════════════════════════════════════════════════════════════
# Scene 3 β€” How It Works (Flow Diagram)
# ═══════════════════════════════════════════════════════════════════════════
class Scene3_Flow(Scene):
def construct(self):
self.camera.background_color = BG
title = T("Bagaimana Cara Kerjanya?", size=36, color=TITLE, weight=BOLD)
title.to_edge(UP, buff=0.4)
self.play(Write(title), run_time=0.6)
self.wait(0.2)
def make_box(text_lines, color, w=2.2, h=1.0):
"""Helper: box with centered text."""
box = RoundedRectangle(
width=w, height=h,
corner_radius=0.12,
stroke_color=color, stroke_width=2,
fill_color=color, fill_opacity=0.1,
)
txt = T(text_lines, size=18, color=TITLE)
txt.move_to(box)
return VGroup(box, txt)
def arrow_between(left_obj, right_obj, color=MUTED):
"""Arrow from right edge of left_obj to left edge of right_obj."""
start = left_obj.get_right() + RIGHT * 0.15
end = right_obj.get_left() + LEFT * 0.15
arr = Arrow(
start, end,
stroke_width=2.5,
stroke_color=color,
max_tip_length_to_length_ratio=0.08,
)
return arr
# Row 1: Input
input_box = make_box("πŸ“„\nInput RUU", ACCENT, w=2.0, h=1.2)
input_box.shift(LEFT * 5.5 + UP * 0.5)
# Row 1: BIG LLM
big_box = make_box("🧠 BIG\nQwen3.5-27B", PRIMARY, w=2.2, h=1.2)
big_box.shift(LEFT * 2.0 + UP * 0.5)
# Row 1: pasal.id
pasal_box = make_box("πŸ“œ\npasal.id", SECONDARY, w=2.0, h=1.2)
pasal_box.shift(RIGHT * 1.2 + UP * 0.5)
# Row 2: SMALL LLM
small_box = make_box("🧠 SMALL\nQwen3.5-9B", PRIMARY, w=2.2, h=1.2)
small_box.shift(LEFT * 1.0 + DOWN * 1.3)
# Row 2: Output
output_box = make_box("βœ…\nHasil Analisis", ACCENT, w=2.0, h=1.2)
output_box.shift(RIGHT * 3.5 + DOWN * 1.3)
# Animate in
self.play(FadeIn(input_box, shift=RIGHT * 0.5), run_time=0.6)
self.wait(0.15)
self.play(FadeIn(big_box, shift=RIGHT * 0.5), run_time=0.6)
self.wait(0.15)
# Arrow: input β†’ BIG
arr1 = arrow_between(input_box, big_box)
self.play(Create(arr1), run_time=0.3)
self.play(FadeIn(pasal_box, shift=RIGHT * 0.5), run_time=0.6)
arr2 = arrow_between(big_box, pasal_box)
self.play(Create(arr2), run_time=0.3)
self.wait(0.3)
self.play(FadeIn(small_box, shift=UP * 0.5), run_time=0.6)
# Arrow: BIG β†’ SMALL (down-left)
arr3 = Arrow(
big_box.get_bottom() + DOWN * 0.15,
small_box.get_top() + UP * 0.15,
stroke_width=2.5, stroke_color=MUTED,
max_tip_length_to_length_ratio=0.08,
)
self.play(Create(arr3), run_time=0.3)
# Arrow: SMALL β†’ Output (right)
arr4 = Arrow(
small_box.get_right() + RIGHT * 0.15,
output_box.get_left() + LEFT * 0.15,
stroke_width=2.5, stroke_color=MUTED,
max_tip_length_to_length_ratio=0.08,
)
self.play(FadeIn(output_box, shift=RIGHT * 0.5), run_time=0.6)
self.play(Create(arr4), run_time=0.3)
self.wait(0.5)
# Arrow: pasal.id β†’ SMALL (feed back)
arr5 = Arrow(
pasal_box.get_bottom() + DOWN * 0.15,
small_box.get_right() + RIGHT * 0.5,
stroke_width=2, stroke_color=SECONDARY,
max_tip_length_to_length_ratio=0.08,
)
# Curve it by adding a path
self.play(Create(arr5), run_time=0.4)
# Value text
value = T("Hasil lengkap dalam hitungan menit ⚑", size=28, color=SECONDARY, weight=BOLD)
value.to_edge(DOWN, buff=0.5)
self.play(Write(value), run_time=0.8)
self.wait(2.0)
self.play(FadeOut(VGroup(
title, input_box, big_box, pasal_box, small_box, output_box,
arr1, arr2, arr3, arr4, arr5, value,
)), run_time=0.5)
# ═══════════════════════════════════════════════════════════════════════════
# Scene 4 β€” Free & Open (Closing)
# ═══════════════════════════════════════════════════════════════════════════
class Scene4_Closing(Scene):
def construct(self):
self.camera.background_color = BG
# Token icons β€” spread wider
hf_token = T("πŸ”‘ HF Token", size=32, color=PRIMARY, weight=BOLD)
pasal_token = T("πŸ“œ pasal.id Token", size=32, color=SECONDARY, weight=BOLD)
hf_token.shift(LEFT * 3.5 + UP * 0.8)
pasal_token.shift(RIGHT * 3.5 + UP * 0.8)
self.play(FadeIn(hf_token, shift=UP * 0.5), run_time=0.6)
self.wait(0.2)
self.play(FadeIn(pasal_token, shift=UP * 0.5), run_time=0.6)
self.wait(0.3)
# Tagline β€” centered below both tokens
byo = T("Bawa token sendiri. Buka di GitHub.", size=26, color=BODY)
byo.shift(DOWN * 0.5)
self.play(Write(byo), run_time=0.8)
self.wait(0.3)
# Links
gh = T("github.com/pebaryan/Legawa", size=20, color=ACCENT)
hf = T("build-small-hackathon/legawa", size=20, color=ACCENT)
gh.to_edge(DOWN, buff=0.8)
hf.next_to(gh, DOWN, buff=0.2)
self.play(
Write(gh), Write(hf),
run_time=0.6,
)
self.wait(0.5)
# Final brand
final = T("πŸ›οΈ Legawa β€” small models, big adventure πŸ•οΈ", size=34, color=SECONDARY, weight=BOLD)
final.to_edge(UP, buff=1.5)
self.play(Write(final), run_time=1.0)
self.wait(2.0)
# Clean exit
self.play(*[FadeOut(mob) for mob in self.mobjects], run_time=0.5)