Skip to content

Commit cdc9977

Browse files
committed
feat: add narrated promo video pipeline (edge-tts en-US-GuyNeural)
1 parent be4a107 commit cdc9977

3 files changed

Lines changed: 239 additions & 0 deletions

File tree

.github/workflows/video-build.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Build Promo Video
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
paths:
7+
- 'promo/**'
8+
release:
9+
types: [published]
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: video-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
permissions:
17+
contents: write
18+
19+
jobs:
20+
render-video:
21+
name: Render Promo Video
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.11'
30+
31+
- name: Install dependencies
32+
run: |
33+
sudo apt-get update
34+
sudo apt-get install -y ffmpeg libcairo2-dev libpango1.0-dev
35+
pip install manim==0.18.1 edge-tts mutagen
36+
37+
- name: Generate narration audio
38+
run: |
39+
cd promo
40+
python generate_audio.py
41+
cat durations.json
42+
43+
- name: Render synced promo video
44+
run: |
45+
cd promo
46+
manim render -qh --format mp4 promo_scene.py ProductPromo
47+
48+
- name: Merge video + audio
49+
run: |
50+
cd promo
51+
VIDEO=$(find media/videos -name "ProductPromo.mp4" | head -1)
52+
ffmpeg -y -i "$VIDEO" -i narration.mp3 \
53+
-c:v copy -c:a aac -b:a 192k \
54+
-map 0:v:0 -map 1:a:0 \
55+
-shortest \
56+
eApps_v1.0_promo.mp4
57+
ls -lh eApps_v1.0_promo.mp4
58+
59+
- name: Upload final video
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: eApps-promo-video
63+
path: promo/eApps_v1.0_promo.mp4
64+
retention-days: 90
65+
66+
- name: Attach to release
67+
if: github.event_name == 'release'
68+
uses: softprops/action-gh-release@v2
69+
with:
70+
files: promo/eApps_v1.0_promo.mp4
71+
tag_name: ${{ github.event.release.tag_name }}
72+
fail_on_unmatched_files: false
73+
env:
74+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

promo/generate_audio.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Generate per-segment narration using edge-tts (US English neural voice)."""
2+
import asyncio
3+
import json
4+
import edge_tts
5+
from mutagen.mp3 import MP3
6+
7+
VOICE = "en-US-GuyNeural"
8+
RATE = "+0%"
9+
10+
SEGMENTS = [
11+
{"id": "intro", "text": "Introducing eApps. The unified marketplace and app store for EoS."},
12+
{"id": "f1", "text": "Feature one. 50 Apps Across 8 Platforms. Native apps, desktop apps, mobile, web PWAs, browser extensions, dev tools, CLI tools, and enterprise deployments — all in one monorepo."},
13+
{"id": "f2", "text": "Feature two. Automated Build and Delivery. CI/CD pipelines compile, test, and publish every app to 188 platform targets automatically."},
14+
{"id": "f3", "text": "Feature three. Live Marketplace. A hosted web storefront where users browse, search, and install EoS apps with one click."},
15+
{"id": "arch", "text": "Under the hood, eApps is built with C, TypeScript, React, and GitHub Actions. The architecture flows from App Source, to Build Matrix, to Test Suite, to Package Registry, to Marketplace."},
16+
{"id": "cta", "text": "eApps. Open source and ready to ship. Visit github dot com slash embeddedos-org slash eApps."}
17+
]
18+
19+
20+
async def generate():
21+
durations = {}
22+
audio_files = []
23+
24+
for seg in SEGMENTS:
25+
filename = f"seg_{seg['id']}.mp3"
26+
communicate = edge_tts.Communicate(seg["text"], VOICE, rate=RATE)
27+
await communicate.save(filename)
28+
dur = MP3(filename).info.length
29+
durations[seg["id"]] = round(dur + 0.5, 1)
30+
audio_files.append(filename)
31+
print(f" {seg['id']}: {dur:.1f}s -> padded {durations[seg['id']]}s")
32+
33+
with open("durations.json", "w") as f:
34+
json.dump(durations, f, indent=2)
35+
36+
import subprocess
37+
with open("concat_list.txt", "w") as f:
38+
for af in audio_files:
39+
f.write(f"file '{af}'\n")
40+
41+
subprocess.run([
42+
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
43+
"-i", "concat_list.txt", "-c", "copy", "narration.mp3"
44+
], check=True)
45+
46+
total = sum(durations.values())
47+
print(f"\nVoice: {VOICE}")
48+
print(f"Total narration: {total:.1f}s")
49+
50+
asyncio.run(generate())

promo/promo_scene.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""eApps - production promo video with synced narration."""
2+
from manim import *
3+
import json
4+
import os
5+
6+
dur_path = os.path.join(os.path.dirname(__file__), "durations.json")
7+
if os.path.exists(dur_path):
8+
with open(dur_path) as f:
9+
DUR = json.load(f)
10+
else:
11+
DUR = {"intro": 4, "f1": 6, "f2": 6, "f3": 6, "arch": 7, "cta": 5}
12+
13+
ACCENT = "#3b82f6"
14+
BG = "#0f172a"
15+
DARK = "#1e293b"
16+
17+
18+
class ProductPromo(Scene):
19+
def construct(self):
20+
self.camera.background_color = BG
21+
22+
title = Text("eApps", font_size=96, color=WHITE, weight=BOLD)
23+
underline = Line(LEFT * 3, RIGHT * 3, color=ACCENT, stroke_width=4)
24+
underline.next_to(title, DOWN, buff=0.3)
25+
tagline = Text("EoS Unified Marketplace & App Store", font_size=28, color=GRAY_B)
26+
tagline.next_to(underline, DOWN, buff=0.4)
27+
techs = "C, TypeScript, React, GitHub Actions".split(", ")
28+
badges = VGroup()
29+
for t in techs:
30+
badge = VGroup(
31+
RoundedRectangle(corner_radius=0.1, width=len(t)*0.18+0.6, height=0.4,
32+
stroke_color=ACCENT, fill_color=DARK, fill_opacity=1),
33+
Text(t, font_size=14, color=WHITE),
34+
)
35+
badge[1].move_to(badge[0])
36+
badges.add(badge)
37+
badges.arrange(RIGHT, buff=0.3).next_to(tagline, DOWN, buff=0.5)
38+
39+
self.play(Write(title), run_time=0.8)
40+
self.play(Create(underline), FadeIn(tagline, shift=UP*0.2), run_time=0.6)
41+
self.play(LaggedStart(*[FadeIn(b, scale=0.8) for b in badges], lag_ratio=0.1), run_time=0.6)
42+
self.wait(DUR["intro"] - 2.0)
43+
self.play(FadeOut(VGroup(title, underline, tagline, badges)), run_time=0.4)
44+
45+
features = [
46+
("01", "50 Apps, 8 Platforms", "Native, desktop, mobile, web PWAs, extensions, dev tools, CLI, and enterprise — all in one monorepo", DUR["f1"]),
47+
("02", "Automated Build & Delivery", "CI/CD pipelines compile, test, and publish every app to 188 platform targets automatically", DUR["f2"]),
48+
("03", "Live Marketplace", "Hosted web storefront where users browse, search, and install EoS apps with one click", DUR["f3"]),
49+
]
50+
for num, feat_name, feat_desc, dur in features:
51+
num_text = Text(num, font_size=200, color=ACCENT, weight=BOLD, font="Monospace").set_opacity(0.08)
52+
num_text.to_edge(LEFT, buff=0.5)
53+
feat_title = Text(feat_name, font_size=48, color=WHITE, weight=BOLD)
54+
feat_title.to_edge(UP, buff=1.5).shift(RIGHT * 0.5)
55+
bar = Rectangle(width=6, height=0.05, color=ACCENT, fill_opacity=1)
56+
bar.next_to(feat_title, DOWN, buff=0.2, aligned_edge=LEFT)
57+
desc_text = Paragraph(feat_desc, font_size=22, color=GRAY_B, line_spacing=1.2, alignment="left").scale(0.9)
58+
desc_text.next_to(bar, DOWN, buff=0.4, aligned_edge=LEFT)
59+
if desc_text.width > 10:
60+
desc_text.scale(10 / desc_text.width)
61+
diagram = VGroup(RoundedRectangle(corner_radius=0.15, width=4, height=2.5, stroke_color=ACCENT, stroke_width=1, fill_color=DARK, fill_opacity=0.5))
62+
for row in range(3):
63+
for col in range(4):
64+
dot = Dot(radius=0.04, color=ACCENT).set_opacity(0.3 + row*0.2)
65+
dot.move_to(diagram[0].get_center() + RIGHT*(col-1.5)*0.6 + DOWN*(row-1)*0.5)
66+
diagram.add(dot)
67+
diagram.to_edge(RIGHT, buff=1).shift(DOWN * 0.3)
68+
grp = VGroup(num_text, feat_title, bar, desc_text, diagram)
69+
self.play(FadeIn(num_text), Write(feat_title), GrowFromEdge(bar, LEFT), run_time=0.7)
70+
self.play(FadeIn(desc_text, shift=UP*0.2), FadeIn(diagram, scale=0.9), run_time=0.6)
71+
self.wait(dur - 1.7)
72+
self.play(FadeOut(grp), run_time=0.4)
73+
74+
arch_label = Text("Architecture", font_size=20, color=GRAY_B)
75+
arch_label.to_edge(UP, buff=0.6)
76+
components = ["App Source", "Build Matrix", "Test Suite", "Package Registry", "Marketplace"]
77+
boxes = VGroup()
78+
for comp in components:
79+
box = VGroup(
80+
RoundedRectangle(corner_radius=0.12, width=2.2, height=1.0, stroke_color=ACCENT, fill_color=DARK, fill_opacity=1, stroke_width=2),
81+
Text(comp, font_size=16, color=WHITE),
82+
)
83+
box[1].move_to(box[0])
84+
boxes.add(box)
85+
boxes.arrange(RIGHT, buff=0.4)
86+
arrows = VGroup()
87+
for i in range(len(boxes) - 1):
88+
arr = Arrow(boxes[i].get_right(), boxes[i+1].get_left(), color=ACCENT, buff=0.08, stroke_width=2, max_tip_length_to_length_ratio=0.15)
89+
arrows.add(arr)
90+
flow_dots = VGroup()
91+
for arr in arrows:
92+
for t in [0.3, 0.5, 0.7]:
93+
dot = Dot(radius=0.03, color=ACCENT).set_opacity(0.6)
94+
dot.move_to(arr.point_from_proportion(t))
95+
flow_dots.add(dot)
96+
self.play(FadeIn(arch_label), run_time=0.3)
97+
self.play(LaggedStart(*[FadeIn(b, shift=UP*0.3) for b in boxes], lag_ratio=0.12), run_time=0.8)
98+
self.play(LaggedStart(*[GrowArrow(a) for a in arrows], lag_ratio=0.1), run_time=0.5)
99+
self.play(LaggedStart(*[FadeIn(d, scale=0) for d in flow_dots], lag_ratio=0.05), run_time=0.4)
100+
self.wait(DUR["arch"] - 2.4)
101+
self.play(FadeOut(VGroup(arch_label, boxes, arrows, flow_dots)), run_time=0.4)
102+
103+
cta_name = Text("eApps", font_size=72, color=WHITE, weight=BOLD)
104+
cta_line = Line(LEFT*2, RIGHT*2, color=ACCENT, stroke_width=3)
105+
cta_line.next_to(cta_name, DOWN, buff=0.3)
106+
cta_url = Text("github.com/embeddedos-org/eApps", font_size=22, color=ManimColor(ACCENT))
107+
cta_url.next_to(cta_line, DOWN, buff=0.3)
108+
cta_badge = Text("Open Source | Apache 2.0 | Production Ready", font_size=16, color=GRAY_B)
109+
cta_badge.next_to(cta_url, DOWN, buff=0.3)
110+
star = Text("Star us on GitHub", font_size=18, color=YELLOW).set_opacity(0.8)
111+
star.next_to(cta_badge, DOWN, buff=0.4)
112+
self.play(Write(cta_name), Create(cta_line), run_time=0.7)
113+
self.play(FadeIn(cta_url, shift=UP*0.2), run_time=0.4)
114+
self.play(FadeIn(cta_badge), FadeIn(star), run_time=0.4)
115+
self.wait(DUR["cta"] - 1.9)

0 commit comments

Comments
 (0)