Skip to content

Commit 1ea55e1

Browse files
committed
feat: audio-synced promo video with richer visuals and tech explanations
1 parent 106a4ee commit 1ea55e1

3 files changed

Lines changed: 186 additions & 96 deletions

File tree

.github/workflows/video-build.yml

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,25 @@ jobs:
2828
with:
2929
python-version: '3.11'
3030

31-
- name: Install Manim, gTTS & dependencies
31+
- name: Install dependencies
3232
run: |
3333
sudo apt-get update
3434
sudo apt-get install -y ffmpeg libcairo2-dev libpango1.0-dev
35-
pip install manim==0.18.1 gTTS
35+
pip install manim==0.18.1 gTTS mutagen
3636
37-
- name: Generate narration audio
37+
- name: Generate narration audio (per-segment)
3838
run: |
3939
cd promo
4040
python generate_audio.py
41-
ls -lh narration.mp3
41+
cat durations.json
42+
ls -lh *.mp3
4243
43-
- name: Render promo video (1080p)
44+
- name: Render synced promo video
4445
run: |
4546
cd promo
4647
manim render -qh --format mp4 promo_scene.py ProductPromo
4748
48-
- name: Merge video + audio into final MP4
49+
- name: Merge video + audio
4950
run: |
5051
cd promo
5152
VIDEO=$(find media/videos -name "ProductPromo.mp4" | head -1)
@@ -54,7 +55,6 @@ jobs:
5455
-map 0:v:0 -map 1:a:0 \
5556
-shortest \
5657
ebuild_v1.0_promo.mp4
57-
echo "Final video:"
5858
ls -lh ebuild_v1.0_promo.mp4
5959
6060
- name: Upload final video
@@ -73,18 +73,3 @@ jobs:
7373
fail_on_unmatched_files: false
7474
env:
7575
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76-
77-
summary:
78-
name: Video Build Summary
79-
runs-on: ubuntu-latest
80-
needs: render-video
81-
if: always()
82-
steps:
83-
- name: Report
84-
run: |
85-
echo "## Promo Video Build" >> $GITHUB_STEP_SUMMARY
86-
echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY
87-
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
88-
echo "| Product | ebuild |" >> $GITHUB_STEP_SUMMARY
89-
echo "| Audio | gTTS narration |" >> $GITHUB_STEP_SUMMARY
90-
echo "| Render | ${{ needs.render-video.result }} |" >> $GITHUB_STEP_SUMMARY

promo/generate_audio.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
1-
"""Generate narration audio using Google Text-to-Speech."""
1+
"""Generate per-segment narration audio and output durations for Manim sync."""
2+
import json
23
from gtts import gTTS
4+
from mutagen.mp3 import MP3
35

4-
NARRATION = (
5-
"Introducing ebuild. The embedded build system for EoS. Feature one: Cross-compilation toolchains target any architecture from a single host. Feature two: Incremental builds compile only what changed, saving hours of build time. Feature three: Automatic dependency resolution manages complex embedded project graphs. ebuild. Open source and production ready. Visit github dot com slash embeddedos-org slash ebuild."
6-
)
6+
SEGMENTS = [
7+
{"id": "intro", "text": "Introducing ebuild. EoS Embedded Build System."},
8+
{"id": "f1", "text": "Feature one. Cross-Compilation Toolchains. Target ARM, RISC-V, and x86 from a single host with automatic toolchain management."},
9+
{"id": "f2", "text": "Feature two. Incremental Builds. Content-addressed caching rebuilds only changed targets, cutting build times by 90 percent."},
10+
{"id": "f3", "text": "Feature three. Dependency Resolution. DAG-based resolver handles diamond dependencies and circular includes automatically."},
11+
{"id": "arch", "text": "Under the hood, ebuild is built with C, Make, CMake, LLVM. The architecture flows from Parser, to Resolver, to Scheduler, to Compiler, to Linker."},
12+
{"id": "cta", "text": "ebuild. Open source and production ready. Visit github dot com slash embeddedos-org slash ebuild."},
13+
]
714

8-
tts = gTTS(text=NARRATION, lang="en", slow=False)
9-
tts.save("narration.mp3")
10-
print(f"Generated narration.mp3 ({len(NARRATION)} chars)")
15+
durations = {}
16+
audio_files = []
17+
18+
for seg in SEGMENTS:
19+
filename = f"seg_{seg['id']}.mp3"
20+
tts = gTTS(text=seg["text"], lang="en", slow=False)
21+
tts.save(filename)
22+
dur = MP3(filename).info.length
23+
durations[seg["id"]] = round(dur + 0.5, 1) # add 0.5s padding
24+
audio_files.append(filename)
25+
print(f" {seg['id']}: {dur:.1f}s -> padded {durations[seg['id']]}s")
26+
27+
# Write durations JSON for Manim to read
28+
with open("durations.json", "w") as f:
29+
json.dump(durations, f, indent=2)
30+
31+
# Concatenate all segments into single narration.mp3
32+
import subprocess
33+
list_file = "concat_list.txt"
34+
with open(list_file, "w") as f:
35+
for af in audio_files:
36+
f.write(f"file '{af}'\n")
37+
38+
subprocess.run([
39+
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
40+
"-i", list_file, "-c", "copy", "narration.mp3"
41+
], check=True)
42+
43+
total = sum(durations.values())
44+
print(f"\nTotal narration: {total:.1f}s")
45+
print(f"Durations: {json.dumps(durations)}")

promo/promo_scene.py

Lines changed: 137 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,167 @@
1+
"""ebuild — production promo video with synced narration."""
12
from manim import *
3+
import json
4+
import os
25

6+
# Load durations from generate_audio.py
7+
dur_path = os.path.join(os.path.dirname(__file__), "durations.json")
8+
if os.path.exists(dur_path):
9+
with open(dur_path) as f:
10+
DUR = json.load(f)
11+
else:
12+
DUR = {"intro": 4, "f1": 6, "f2": 6, "f3": 6, "arch": 7, "cta": 5}
13+
14+
ACCENT = "#16a34a"
15+
BG = "#0f172a"
16+
DARK = "#1e293b"
317

4-
class ProductPromo(Scene):
5-
"""ebuild — promotional video."""
618

19+
class ProductPromo(Scene):
720
def construct(self):
8-
self.camera.background_color = "#0f172a"
9-
accent = "#16a34a"
21+
self.camera.background_color = BG
1022

11-
# ── Scene 1: Logo Reveal ──
23+
# ═══ INTRO ═══
1224
title = Text("ebuild", font_size=96, color=WHITE, weight=BOLD)
13-
tagline = Text(
14-
"EoS Embedded Build System",
15-
font_size=32, color=GRAY_B,
16-
)
17-
tagline.next_to(title, DOWN, buff=0.6)
25+
underline = Line(LEFT * 3, RIGHT * 3, color=ACCENT, stroke_width=4)
26+
underline.next_to(title, DOWN, buff=0.3)
27+
tagline = Text("EoS Embedded Build System", font_size=28, color=GRAY_B)
28+
tagline.next_to(underline, DOWN, buff=0.4)
29+
# Tech badges
30+
techs = "C, Make, CMake, LLVM".split(", ")
31+
badges = VGroup()
32+
for t in techs:
33+
badge = VGroup(
34+
RoundedRectangle(corner_radius=0.1, width=len(t)*0.18+0.6, height=0.4,
35+
stroke_color=ACCENT, fill_color=DARK, fill_opacity=1),
36+
Text(t, font_size=14, color=WHITE),
37+
)
38+
badge[1].move_to(badge[0])
39+
badges.add(badge)
40+
badges.arrange(RIGHT, buff=0.3).next_to(tagline, DOWN, buff=0.5)
1841

19-
self.play(Write(title), run_time=1.5)
20-
self.play(FadeIn(tagline, shift=UP * 0.3), run_time=0.8)
21-
self.wait(2)
22-
self.play(FadeOut(title), FadeOut(tagline))
42+
self.play(Write(title), run_time=0.8)
43+
self.play(Create(underline), FadeIn(tagline, shift=UP*0.2), run_time=0.6)
44+
self.play(LaggedStart(*[FadeIn(b, scale=0.8) for b in badges], lag_ratio=0.1), run_time=0.6)
45+
self.wait(DUR["intro"] - 2.0)
46+
self.play(FadeOut(VGroup(title, underline, tagline, badges)), run_time=0.4)
2347

24-
# ── Scene 2: Feature Showcase ──
48+
# ═══ FEATURES ═══
2549
features = [
26-
"Cross-Compilation Toolchains",
27-
"Incremental Builds",
28-
"Dependency Resolution",
50+
("01", "Cross-Compilation Toolchains", "Target ARM, RISC-V, and x86 from a single host with automatic toolchain management", DUR["f1"]),
51+
("02", "Incremental Builds", "Content-addressed caching rebuilds only changed targets, cutting build times by 90 percent", DUR["f2"]),
52+
("03", "Dependency Resolution", "DAG-based resolver handles diamond dependencies and circular includes automatically", DUR["f3"]),
2953
]
30-
for i, feat in enumerate(features):
31-
num = Text(
32-
f"0{i+1}", font_size=120, color=accent, weight=BOLD, font="Monospace",
33-
).set_opacity(0.15)
34-
num.to_edge(LEFT, buff=1.5).shift(UP * 0.5)
54+
for num, feat_name, feat_desc, dur in features:
55+
# Large number watermark
56+
num_text = Text(num, font_size=200, color=ACCENT, weight=BOLD,
57+
font="Monospace").set_opacity(0.08)
58+
num_text.to_edge(LEFT, buff=0.5)
59+
60+
# Feature title
61+
feat_title = Text(feat_name, font_size=48, color=WHITE, weight=BOLD)
62+
feat_title.to_edge(UP, buff=1.5).shift(RIGHT * 0.5)
3563

36-
feat_text = Text(feat, font_size=52, color=WHITE, weight=BOLD)
37-
feat_text.next_to(num, RIGHT, buff=0.8).align_to(num, UP)
64+
# Accent bar
65+
bar = Rectangle(width=6, height=0.05, color=ACCENT, fill_opacity=1)
66+
bar.next_to(feat_title, DOWN, buff=0.2, aligned_edge=LEFT)
3867

39-
bar = Rectangle(
40-
width=8, height=0.06, color=accent, fill_opacity=1,
68+
# Description text (wrapped)
69+
desc_text = Paragraph(
70+
feat_desc, font_size=22, color=GRAY_B,
71+
line_spacing=1.2, alignment="left",
72+
).scale(0.9)
73+
desc_text.next_to(bar, DOWN, buff=0.4, aligned_edge=LEFT)
74+
if desc_text.width > 10:
75+
desc_text.scale(10 / desc_text.width)
76+
77+
# Visual element: tech diagram box
78+
diagram = VGroup(
79+
RoundedRectangle(corner_radius=0.15, width=4, height=2.5,
80+
stroke_color=ACCENT, stroke_width=1,
81+
fill_color=DARK, fill_opacity=0.5),
4182
)
42-
bar.next_to(feat_text, DOWN, buff=0.3, aligned_edge=LEFT)
83+
# Add icon-like dots inside
84+
for row in range(3):
85+
for col in range(4):
86+
dot = Dot(radius=0.04, color=ACCENT).set_opacity(0.3 + row*0.2)
87+
dot.move_to(diagram[0].get_center() + RIGHT*(col-1.5)*0.6 + DOWN*(row-1)*0.5)
88+
diagram.add(dot)
89+
diagram.to_edge(RIGHT, buff=1).shift(DOWN * 0.3)
4390

91+
grp = VGroup(num_text, feat_title, bar, desc_text, diagram)
4492
self.play(
45-
FadeIn(num, shift=RIGHT * 0.5),
46-
Write(feat_text),
47-
GrowFromEdge(bar, LEFT),
48-
run_time=1.0,
93+
FadeIn(num_text),
94+
Write(feat_title), GrowFromEdge(bar, LEFT),
95+
run_time=0.7,
4996
)
50-
self.wait(1.5)
51-
self.play(FadeOut(num), FadeOut(feat_text), FadeOut(bar), run_time=0.5)
97+
self.play(FadeIn(desc_text, shift=UP*0.2), FadeIn(diagram, scale=0.9), run_time=0.6)
98+
self.wait(dur - 1.7)
99+
self.play(FadeOut(grp), run_time=0.4)
52100

53-
# ── Scene 3: Architecture Flash ──
54-
arch_title = Text("Architecture", font_size=28, color=GRAY_B)
55-
arch_title.to_edge(UP, buff=0.8)
101+
# ═══ ARCHITECTURE ═══
102+
arch_label = Text("Architecture", font_size=20, color=GRAY_B)
103+
arch_label.to_edge(UP, buff=0.6)
56104

105+
components = ["Parser", "Resolver", "Scheduler", "Compiler", "Linker"]
57106
boxes = VGroup()
58-
labels = ["Core", "API", "Runtime"]
59-
for j, lbl in enumerate(labels):
60-
box = RoundedRectangle(
61-
corner_radius=0.15, width=2.5, height=1.2,
62-
stroke_color=accent, fill_color="#1e293b", fill_opacity=1,
107+
for i, comp in enumerate(components):
108+
box = VGroup(
109+
RoundedRectangle(
110+
corner_radius=0.12, width=2.2, height=1.0,
111+
stroke_color=ACCENT, fill_color=DARK, fill_opacity=1, stroke_width=2,
112+
),
113+
Text(comp, font_size=16, color=WHITE),
63114
)
64-
box_label = Text(lbl, font_size=22, color=WHITE)
65-
box_label.move_to(box)
66-
grp = VGroup(box, box_label)
67-
boxes.add(grp)
68-
boxes.arrange(RIGHT, buff=0.6)
115+
box[1].move_to(box[0])
116+
boxes.add(box)
117+
boxes.arrange(RIGHT, buff=0.4)
69118

70119
arrows = VGroup()
71-
for j in range(len(boxes) - 1):
120+
for i in range(len(boxes) - 1):
72121
arr = Arrow(
73-
boxes[j].get_right(), boxes[j + 1].get_left(),
74-
color=accent, buff=0.1, stroke_width=2,
122+
boxes[i].get_right(), boxes[i+1].get_left(),
123+
color=ACCENT, buff=0.08, stroke_width=2,
124+
max_tip_length_to_length_ratio=0.15,
75125
)
76126
arrows.add(arr)
77127

78-
self.play(FadeIn(arch_title))
79-
self.play(LaggedStart(*[FadeIn(b, shift=UP * 0.3) for b in boxes], lag_ratio=0.2))
80-
self.play(LaggedStart(*[GrowArrow(a) for a in arrows], lag_ratio=0.15))
81-
self.wait(2)
82-
self.play(FadeOut(arch_title), FadeOut(boxes), FadeOut(arrows))
128+
# Data flow dots
129+
flow_dots = VGroup()
130+
for arr in arrows:
131+
for t in [0.3, 0.5, 0.7]:
132+
dot = Dot(radius=0.03, color=ACCENT).set_opacity(0.6)
133+
dot.move_to(arr.point_from_proportion(t))
134+
flow_dots.add(dot)
83135

84-
# ── Scene 4: CTA ──
85-
cta = Text("ebuild", font_size=72, color=WHITE, weight=BOLD)
86-
url = Text(
136+
self.play(FadeIn(arch_label), run_time=0.3)
137+
self.play(
138+
LaggedStart(*[FadeIn(b, shift=UP*0.3) for b in boxes], lag_ratio=0.12),
139+
run_time=0.8,
140+
)
141+
self.play(
142+
LaggedStart(*[GrowArrow(a) for a in arrows], lag_ratio=0.1),
143+
run_time=0.5,
144+
)
145+
self.play(LaggedStart(*[FadeIn(d, scale=0) for d in flow_dots], lag_ratio=0.05), run_time=0.4)
146+
self.wait(DUR["arch"] - 2.4)
147+
self.play(FadeOut(VGroup(arch_label, boxes, arrows, flow_dots)), run_time=0.4)
148+
149+
# ═══ CTA ═══
150+
cta_name = Text("ebuild", font_size=72, color=WHITE, weight=BOLD)
151+
cta_line = Line(LEFT*2, RIGHT*2, color=ACCENT, stroke_width=3)
152+
cta_line.next_to(cta_name, DOWN, buff=0.3)
153+
cta_url = Text(
87154
"github.com/embeddedos-org/ebuild",
88-
font_size=24, color=accent,
155+
font_size=22, color=ManimColor(ACCENT),
89156
)
90-
url.next_to(cta, DOWN, buff=0.5)
91-
badge = Text("Open Source", font_size=18, color=GRAY_B)
92-
badge.next_to(url, DOWN, buff=0.4)
93-
94-
self.play(Write(cta), run_time=1.0)
95-
self.play(FadeIn(url, shift=UP * 0.2))
96-
self.play(FadeIn(badge, shift=UP * 0.2))
97-
self.wait(3)
157+
cta_url.next_to(cta_line, DOWN, buff=0.3)
158+
cta_badge = Text("Open Source · MIT License · Production Ready",
159+
font_size=16, color=GRAY_B)
160+
cta_badge.next_to(cta_url, DOWN, buff=0.3)
161+
star = Text("★ Star us on GitHub", font_size=18, color=YELLOW).set_opacity(0.8)
162+
star.next_to(cta_badge, DOWN, buff=0.4)
163+
164+
self.play(Write(cta_name), Create(cta_line), run_time=0.7)
165+
self.play(FadeIn(cta_url, shift=UP*0.2), run_time=0.4)
166+
self.play(FadeIn(cta_badge), FadeIn(star), run_time=0.4)
167+
self.wait(DUR["cta"] - 1.9)

0 commit comments

Comments
 (0)