● Zion Boggan zionboggan.com ↗

Add walkthrough video pipeline (Kokoro VO + NVENC render scripts)

84dadc0   Zion Boggan committed on Jun 25, 2026 (1 month ago)
promo/.gitignore +9 -0
@@ -0,0 +1,9 @@
+# heavy media + transient render artifacts (scripts are kept)
+node_modules/
+shots/
+build/
+*.wav
+*.mp3
+*.mp4
+voiceover/kvenv/
+__pycache__/
promo/build_video.py +114 -0
@@ -0,0 +1,114 @@
+import json, os, subprocess, shlex
+
+ROOT = "/shared/projects/SteppingApp/promo"
+SHOTS = f"{ROOT}/shots"
+CLIPS = f"{ROOT}/build/clips"
+os.makedirs(CLIPS, exist_ok=True)
+FPS = 30
+W, H = 1920, 1080
+def pick(paths):
+ for p in paths:
+ if os.path.exists(p):
+ return p
+ return paths[-1]
+MONO = pick([
+ "/usr/share/fonts/truetype/liberation2/LiberationMono-Bold.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
+])
+SANS = pick([
+ "/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf",
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
+])
+GOLD = "0xC8A24C"
+BONE = "0xF2ECE0"
+STAGE = "0x0F0E0C"
+
+meta = json.load(open(f"{ROOT}/voiceover/scene_times.json"))
+ss = meta["scene_start"]
+total = meta["total"]
+TAIL = 1.6
+
+order = ["s1","s2","s3","s4","s5","s6","s7","s8","s9","s10"]
+cfg = {
+ "s1": ("hero.png","wide","STEPPING / DIVINE NINE STEP CULTURE"),
+ "s2": ("cities.png","wide","CHOOSE YOUR CITY"),
+ "s3": ("city.png","wide","THE LINE-UP / UPCOMING SHOWS"),
+ "s4": ("event.png","wide","EVERY SHOW, IN DETAIL"),
+ "s5": ("yard.png","tall","THE YARD / THE COMMUNITY"),
+ "s6": ("profile.png","wide","PROFILES / WHO'S WHO"),
+ "s7": ("route.png","tall","THE ROUTE / PLAN THE TRIP"),
+ "s8": ("mobile.png","phone","RIGHT IN YOUR PHONE"),
+ "s9": ("hero.png","wide","MORE ON THE WAY"),
+ "s10": (None,"end",""),
+}
+
+def dur(i):
+ s = order[i]
+ if i < len(order)-1:
+ return round(ss[order[i+1]] - ss[s], 3)
+ return round(total - ss[s] + TAIL, 3)
+
+def esc(t):
+ return t.replace(":", "\\:").replace("'", "’").replace(",", "\\,")
+
+def caption(text):
+ if not text:
+ return ""
+ t = esc(text)
+ return (f",drawtext=fontfile={MONO}:text='{t}':fontcolor={GOLD}:fontsize=30"
+ f":x=88:y=h-96:box=1:boxcolor={STAGE}@0.55:boxborderw=22")
+
+def run(cmd):
+ r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ if r.returncode != 0:
+ print("FFMPEG ERR:\n", r.stderr[-1500:])
+ raise SystemExit(1)
+
+clip_files = []
+for i, s in enumerate(order):
+ img, kind, cap = cfg[s]
+ d = dur(i)
+ out = f"{CLIPS}/{s}.mp4"
+ frames = int(round(d*FPS))
+ fin = "fade=t=in:st=0:d=0.4,fade=t=out:st=%.3f:d=0.4" % max(d-0.4, 0)
+ if kind == "wide":
+ z = f"scale=2400:-1,zoompan=z='min(zoom+0.00035,1.06)':d={frames}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={W}x{H}:fps={FPS}"
+ vf = f"{z},{fin}{caption(cap)}"
+ run(f"ffmpeg -y -loop 1 -t {d} -i {shlex.quote(SHOTS+'/'+img)} -vf \"{vf}\" -r {FPS} -c:v libx264 -pix_fmt yuv420p -an {out}")
+ elif kind == "tall":
+ vf = f"scale={W}:-1,crop={W}:{H}:0:'(ih-{H})*t/{d}',{fin}{caption(cap)}"
+ run(f"ffmpeg -y -loop 1 -t {d} -i {shlex.quote(SHOTS+'/'+img)} -vf \"{vf}\" -r {FPS} -c:v libx264 -pix_fmt yuv420p -an {out}")
+ elif kind == "phone":
+ fc = (f"color={STAGE}:s={W}x{H}:d={d}:r={FPS}[bg];"
+ f"[0:v]scale=-1:1000[ph];"
+ f"[bg][ph]overlay=(W-w)/2:(H-h)/2,{fin}{caption(cap)}")
+ run(f"ffmpeg -y -loop 1 -t {d} -i {shlex.quote(SHOTS+'/'+img)} -filter_complex \"{fc}\" -r {FPS} -c:v libx264 -pix_fmt yuv420p -an {out}")
+ else: # end card
+ vf = (f"drawtext=fontfile={SANS}:text='STEPPING':fontcolor={BONE}:fontsize=120:x=(w-text_w)/2:y=(h-text_h)/2-40,"
+ f"drawtext=fontfile={MONO}:text='BUILT FOR THE CULTURE':fontcolor={GOLD}:fontsize=30:x=(w-text_w)/2:y=(h/2)+70,"
+ f"fade=t=in:st=0:d=0.5")
+ run(f"ffmpeg -y -f lavfi -i color={STAGE}:s={W}x{H}:d={d}:r={FPS} -vf \"{vf}\" -c:v libx264 -pix_fmt yuv420p -an {out}")
+ clip_files.append(out)
+ print(f" {s} {d}s -> {out}")
+
+# concat
+with open(f"{CLIPS}/list.txt","w") as f:
+ for c in clip_files:
+ f.write(f"file '{c}'\n")
+run(f"ffmpeg -y -f concat -safe 0 -i {CLIPS}/list.txt -c:v libx264 -pix_fmt yuv420p -r {FPS} {ROOT}/build/video_silent.mp4")
+
+# audio: VO + ducked CC-BY music, padded to video length
+vlen = round(total + TAIL, 3)
+fadest = round(total - 0.3, 3)
+af = (f"[0:a]aresample=44100,aformat=channel_layouts=stereo,loudnorm=I=-16:TP=-1.5:LRA=11,apad=whole_dur={vlen},asplit=2[vo][key];"
+ f"[1:a]atrim=0:{vlen},asetpts=N/SR/TB,aresample=44100,aformat=channel_layouts=stereo,volume=0.22,afade=t=in:st=0:d=2,afade=t=out:st={fadest}:d=2.0[m];"
+ f"[m][key]sidechaincompress=threshold=0.035:ratio=9:attack=15:release=350:makeup=1[md];"
+ f"[vo][md]amix=inputs=2:duration=longest:normalize=0,alimiter=limit=0.97[out]")
+run(f"ffmpeg -y -i {ROOT}/voiceover/stepping_vo.wav -i {ROOT}/assets/music_inspired.mp3 "
+ f"-filter_complex \"{af}\" -map \"[out]\" -ar 44100 -ac 2 -c:a aac -b:a 192k {ROOT}/build/audio_final.m4a")
+
+# mux
+run(f"ffmpeg -y -i {ROOT}/build/video_silent.mp4 -i {ROOT}/build/audio_final.m4a "
+ f"-map 0:v -map 1:a -c:v copy -c:a copy -shortest {ROOT}/build/stepping_walkthrough.mp4")
+print("DONE -> build/stepping_walkthrough.mp4 (", vlen, "s )")
promo/build_video_win.py +99 -0
@@ -0,0 +1,99 @@
+import json, os, shutil, subprocess
+
+FF = r"C:\Users\homelab\ffbuild71\ffmpeg-n7.1-latest-win64-gpl-7.1\bin\ffmpeg.exe"
+ROOT = r"C:\Users\homelab\stepping-promo"
+SHOTS = ROOT + r"\shots"
+CLIPS = ROOT + r"\build\clips"
+os.makedirs(CLIPS, exist_ok=True)
+os.makedirs(ROOT + r"\fonts", exist_ok=True)
+os.chdir(ROOT)
+
+def cpfont(cands, dst):
+ for s in cands:
+ if os.path.exists(s):
+ shutil.copy(s, dst); return
+ raise SystemExit("no font: " + dst)
+cpfont([r"C:\Windows\Fonts\consolab.ttf", r"C:\Windows\Fonts\consola.ttf"], ROOT + r"\fonts\mono.ttf")
+cpfont([r"C:\Windows\Fonts\arialbd.ttf", r"C:\Windows\Fonts\arial.ttf"], ROOT + r"\fonts\sans.ttf")
+
+FPS = 30
+W, H = 1920, 1080
+# relative font paths (cwd=ROOT) avoid the Windows drive-colon escaping trap in drawtext
+MONO = "fonts/mono.ttf"
+SANS = "fonts/sans.ttf"
+GOLD, BONE, STAGE = "0xC8A24C", "0xF2ECE0", "0x0F0E0C"
+VCODEC = "-c:v h264_nvenc -pix_fmt yuv420p -preset p5 -rc vbr -cq 20 -b:v 0"
+
+meta = json.load(open(ROOT + r"\voiceover\scene_times.json"))
+ss, total, TAIL = meta["scene_start"], meta["total"], 1.6
+order = ["s1","s2","s3","s4","s5","s6","s7","s8","s9","s10"]
+cfg = {
+ "s1": ("hero.png","wide","STEPPING / DIVINE NINE STEP CULTURE"),
+ "s2": ("cities.png","wide","CHOOSE YOUR CITY"),
+ "s3": ("city.png","wide","THE LINE-UP / UPCOMING SHOWS"),
+ "s4": ("event.png","wide","EVERY SHOW, IN DETAIL"),
+ "s5": ("yard.png","tall","THE YARD / THE COMMUNITY"),
+ "s6": ("profile.png","wide","PROFILES / WHO'S WHO"),
+ "s7": ("route.png","tall","THE ROUTE / PLAN THE TRIP"),
+ "s8": ("mobile.png","phone","RIGHT IN YOUR PHONE"),
+ "s9": ("hero.png","wide","MORE ON THE WAY"),
+ "s10": (None,"end",""),
+}
+
+def dur(i):
+ s = order[i]
+ return round((ss[order[i+1]] - ss[s]) if i < len(order)-1 else (total - ss[s] + TAIL), 3)
+
+def esc(t):
+ return t.replace(":", "\\:").replace("'", "’").replace(",", "\\,")
+
+def cap(text):
+ if not text: return ""
+ return (f",drawtext=fontfile={MONO}:text='{esc(text)}':fontcolor={GOLD}:fontsize=30"
+ f":x=88:y=h-96:box=1:boxcolor={STAGE}@0.55:boxborderw=22")
+
+def q(p): return '"' + p + '"'
+
+def run(cmd):
+ r = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ if r.returncode != 0:
+ print("ERR:", r.stderr[-1400:]); raise SystemExit(1)
+
+clips = []
+for i, s in enumerate(order):
+ img, kind, c = cfg[s]
+ d = dur(i); frames = int(round(d*FPS)); out = f"{CLIPS}\\{s}.mp4"
+ fin = "fade=t=in:st=0:d=0.4,fade=t=out:st=%.3f:d=0.4" % max(d-0.4, 0)
+ ip = q(f"{SHOTS}\\{img}") if img else ""
+ if kind == "wide":
+ vf = (f"scale=2112:-2,crop={W}:{H}:'(iw-{W})*t/{d}':(ih-{H})/2,{fin}{cap(c)}")
+ run(f'{q(FF)} -y -loop 1 -t {d} -i {ip} -vf "{vf}" -r {FPS} {VCODEC} -an {q(out)}')
+ elif kind == "tall":
+ vf = f"scale={W}:-1,crop={W}:{H}:0:'(ih-{H})*t/{d}',{fin}{cap(c)}"
+ run(f'{q(FF)} -y -loop 1 -t {d} -i {ip} -vf "{vf}" -r {FPS} {VCODEC} -an {q(out)}')
+ elif kind == "phone":
+ fc = (f"color={STAGE}:s={W}x{H}:d={d}:r={FPS}[bg];[0:v]scale=-1:1000[ph];"
+ f"[bg][ph]overlay=(W-w)/2:(H-h)/2,{fin}{cap(c)}")
+ run(f'{q(FF)} -y -loop 1 -t {d} -i {ip} -filter_complex "{fc}" -r {FPS} {VCODEC} -an {q(out)}')
+ else:
+ vf = (f"drawtext=fontfile={SANS}:text='STEPPING':fontcolor={BONE}:fontsize=120:x=(w-text_w)/2:y=(h-text_h)/2-40,"
+ f"drawtext=fontfile={MONO}:text='BUILT FOR THE CULTURE':fontcolor={GOLD}:fontsize=30:x=(w-text_w)/2:y=(h/2)+70,"
+ f"fade=t=in:st=0:d=0.5")
+ run(f'{q(FF)} -y -f lavfi -i color={STAGE}:s={W}x{H}:d={d}:r={FPS} -vf "{vf}" {VCODEC} -an {q(out)}')
+ clips.append(out); print(f" {s} {d}s ok")
+
+with open(f"{CLIPS}\\list.txt","w") as f:
+ for c in clips: f.write("file '" + c.replace("\\","/") + "'\n")
+run(f'{q(FF)} -y -f concat -safe 0 -i {q(CLIPS + chr(92) + "list.txt")} -c copy {q(ROOT + chr(92) + "build" + chr(92) + "video_silent.mp4")}')
+
+vlen = round(total + TAIL, 3); fadest = round(total - 0.3, 3)
+af = (f"[0:a]aresample=44100,aformat=channel_layouts=stereo,loudnorm=I=-16:TP=-1.5:LRA=11,apad=whole_dur={vlen},asplit=2[vo][key];"
+ f"[1:a]atrim=0:{vlen},asetpts=N/SR/TB,aresample=44100,aformat=channel_layouts=stereo,volume=0.22,afade=t=in:st=0:d=2,afade=t=out:st={fadest}:d=2.0[m];"
+ f"[m][key]sidechaincompress=threshold=0.035:ratio=9:attack=15:release=350:makeup=1[md];"
+ f"[vo][md]amix=inputs=2:duration=longest:normalize=0,alimiter=limit=0.97[out]")
+run(f'{q(FF)} -y -i {q(ROOT + chr(92) + "voiceover" + chr(92) + "stepping_vo.wav")} -i {q(ROOT + chr(92) + "assets" + chr(92) + "music_inspired.mp3")} '
+ f'-filter_complex "{af}" -map "[out]" -ar 44100 -ac 2 -c:a aac -b:a 192k {q(ROOT + chr(92) + "build" + chr(92) + "audio_final.m4a")}')
+
+run(f'{q(FF)} -y -i {q(ROOT + chr(92) + "build" + chr(92) + "video_silent.mp4")} -i {q(ROOT + chr(92) + "build" + chr(92) + "audio_final.m4a")} '
+ f'-map 0:v -map 1:a -c:v copy -c:a copy -shortest {q(ROOT + chr(92) + "build" + chr(92) + "stepping_walkthrough.mp4")}')
+print("DONE", vlen, "s")
promo/capture.mjs +49 -0
@@ -0,0 +1,49 @@
+import { chromium } from "playwright-core";
+import { readFileSync } from "fs";
+
+const EXE = "/root/.npm/_npx/361ceb562f3b3235/node_modules/playwright-core/.local-browsers/chromium-1228/chrome-linux64/chrome";
+const OUT = "/shared/projects/SteppingApp/promo/shots";
+const SCR = "/tmp/claude-0/-shared-projects/26d7af11-61c4-4183-85c3-70d9aac72ef6/scratchpad";
+const BASE = "http://REDACTED-IP:3000";
+const DJ = readFileSync(`${SCR}/.djid`, "utf8").trim();
+const EV = readFileSync(`${SCR}/.evid`, "utf8").trim();
+
+const browser = await chromium.launch({
+ executablePath: EXE,
+ args: ["--no-sandbox", "--disable-dev-shm-usage", "--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"],
+});
+
+// desktop viewport shots (16:9, crisp)
+const d = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 2 });
+const view = async (path, name, settle = 1500, scrollTo = null) => {
+ await d.goto(`${BASE}${path}`, { waitUntil: "networkidle" });
+ if (scrollTo) await d.evaluate((sel) => document.querySelector(sel)?.scrollIntoView({ block: "center" }), scrollTo);
+ await d.waitForTimeout(settle);
+ await d.screenshot({ path: `${OUT}/${name}.png` });
+};
+await d.mouse.move(1250, 470);
+await view("/", "hero", 2600);
+await view("/", "cities", 1400, "#route");
+await view(`/city/atlanta/profile/dj/${DJ}`, "profile", 1200);
+
+// desktop full-page (tall, for vertical pan)
+const f = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 1 });
+const full = async (path, name, settle = 1300) => {
+ await f.goto(`${BASE}${path}`, { waitUntil: "networkidle" });
+ await f.waitForTimeout(settle);
+ await f.screenshot({ path: `${OUT}/${name}.png`, fullPage: true });
+};
+await full("/city/atlanta", "city");
+await full(`/city/atlanta/event/${EV}`, "event");
+await full("/city/atlanta/yard", "yard");
+await full("/city/atlanta/route", "route");
+await full("/about", "about");
+
+// mobile (phone)
+const m = await browser.newPage({ viewport: { width: 430, height: 932 }, deviceScaleFactor: 3, isMobile: true, hasTouch: true });
+await m.goto(`${BASE}/`, { waitUntil: "networkidle" });
+await m.waitForTimeout(1800);
+await m.screenshot({ path: `${OUT}/mobile.png`, fullPage: false });
+
+await browser.close();
+console.log("shots done");
promo/node_modules +1 -0
@@ -0,0 +1 @@
+/root/.npm/_npx/361ceb562f3b3235/node_modules
promo/recap.mjs +18 -0
@@ -0,0 +1,18 @@
+import { chromium } from "playwright-core";
+import { readFileSync } from "fs";
+const EXE="/root/.npm/_npx/361ceb562f3b3235/node_modules/playwright-core/.local-browsers/chromium-1228/chrome-linux64/chrome";
+const SCR="/tmp/claude-0/-shared-projects/26d7af11-61c4-4183-85c3-70d9aac72ef6/scratchpad";
+const EV=readFileSync(`${SCR}/.evid`,"utf8").trim();
+const b=await chromium.launch({executablePath:EXE,args:["--no-sandbox","--disable-dev-shm-usage"]});
+const f=await b.newPage({viewport:{width:1920,height:1080},deviceScaleFactor:1});
+const full=async(path,name,sel)=>{
+ await f.goto(`http://REDACTED-IP:3000${path}`,{waitUntil:"load"});
+ await f.waitForSelector(sel,{timeout:8000}).catch(()=>{});
+ await f.waitForFunction(()=>document.body.scrollHeight>1300,{timeout:8000}).catch(()=>{});
+ await f.waitForTimeout(1200);
+ await f.screenshot({path:`/shared/projects/SteppingApp/promo/shots/${name}.png`,fullPage:true});
+ console.log(name, await f.evaluate(()=>document.body.scrollHeight));
+};
+await full("/city/atlanta","city","text=The Line-Up");
+await full(`/city/atlanta/event/${EV}`,"event","text=The Crowd");
+await b.close();
promo/recap2.mjs +18 -0
@@ -0,0 +1,18 @@
+import { chromium } from "playwright-core";
+import { readFileSync } from "fs";
+const EXE="/root/.npm/_npx/361ceb562f3b3235/node_modules/playwright-core/.local-browsers/chromium-1228/chrome-linux64/chrome";
+const SCR="/tmp/claude-0/-shared-projects/26d7af11-61c4-4183-85c3-70d9aac72ef6/scratchpad";
+const EV=readFileSync(`${SCR}/.evid`,"utf8").trim();
+const b=await chromium.launch({executablePath:EXE,args:["--no-sandbox","--disable-dev-shm-usage"]});
+const ctx=await b.newContext({viewport:{width:1920,height:1080},deviceScaleFactor:1,reducedMotion:"reduce"});
+const f=await ctx.newPage();
+const full=async(path,name,sel)=>{
+ await f.goto(`http://REDACTED-IP:3000${path}`,{waitUntil:"networkidle"});
+ await f.waitForSelector(sel,{timeout:8000}).catch(()=>{});
+ await f.waitForTimeout(1000);
+ await f.screenshot({path:`/shared/projects/SteppingApp/promo/shots/${name}.png`,fullPage:true});
+ console.log(name, await f.evaluate(()=>document.documentElement.scrollHeight));
+};
+await full("/city/atlanta","city","text=The Line-Up");
+await full(`/city/atlanta/event/${EV}`,"event","text=The Crowd");
+await b.close();
promo/voiceover/gen_vo.py +73 -0
@@ -0,0 +1,73 @@
+import json, numpy as np, soundfile as sf
+from kokoro import KPipeline
+
+SR = 24000
+VOICE = "af_heart"
+SPEED = 0.95 # slightly slower, calm and clear for an older audience
+pipe = KPipeline(lang_code="a")
+
+# (scene, text, gap_after_seconds) - neutral product voice, full walkthrough.
+segs = [
+ ("s1", "This is Stepping.", 0.55),
+ ("s1", "The home for Divine Nine step show culture.", 0.70),
+ ("s1", "Step shows, strolls, and the people behind them, all in one place.", 0.90),
+
+ ("s2", "Start by choosing your city.", 0.50),
+ ("s2", "Stepping is launching in Atlanta and Washington, D.C.", 0.85),
+
+ ("s3", "Inside each city is The Line-Up.", 0.50),
+ ("s3", "Every step show, stroll, and competition coming up.", 0.65),
+ ("s3", "Tap any one to see the date, the time, and the details.", 0.85),
+
+ ("s4", "On a show's page, you'll find everything you need to know.", 0.55),
+ ("s4", "And you can join the conversation in the comments.", 0.85),
+
+ ("s5", "Then there's The Yard, the community.", 0.50),
+ ("s5", "Chapters, step teams, DJs, promoters, and classes.", 0.65),
+ ("s5", "Everyone who makes the culture move.", 0.85),
+
+ ("s6", "Each one has its own page,", 0.35),
+ ("s6", "so you always know who's who.", 0.85),
+
+ ("s7", "Traveling in for a show?", 0.45),
+ ("s7", "The Route helps you plan the trip.", 0.50),
+ ("s7", "Hotels, restaurants, places to go, and the nearest hospital, for every city.", 0.90),
+
+ ("s8", "Stepping works right in your phone's browser.", 0.55),
+ ("s8", "No app store. No accounts to figure out.", 0.55),
+ ("s8", "Just open it, and start.", 0.90),
+
+ ("s9", "And there's more on the way.", 0.50),
+ ("s9", "Live streaming of shows, more cities across the country, and video from every set.", 0.95),
+
+ ("s10", "Stepping.", 0.45),
+ ("s10", "Built for the culture.", 0.0),
+]
+
+def synth(text):
+ parts = [a for _, _, a in pipe(text, voice=VOICE, speed=SPEED)]
+ return np.concatenate(parts).astype("float32")
+
+out = []
+t = 0.0
+scene_start = {}
+seg_log = []
+for scene, text, gap in segs:
+ if scene not in scene_start:
+ scene_start[scene] = round(t, 3)
+ w = synth(text)
+ out.append(w)
+ seg_log.append({"scene": scene, "start": round(t, 3), "dur": round(len(w) / SR, 3), "text": text})
+ t += len(w) / SR
+ if gap > 0:
+ out.append(np.zeros(int(gap * SR), dtype="float32"))
+ t += gap
+
+full = np.concatenate(out)
+sf.write("stepping_vo.wav", full, SR)
+meta = {"voice": VOICE, "sr": SR, "speed": SPEED, "total": round(len(full) / SR, 3),
+ "scene_start": scene_start, "segments": seg_log}
+json.dump(meta, open("scene_times.json", "w"), indent=2)
+print("TOTAL", meta["total"])
+for s, st in scene_start.items():
+ print(f" {s}: {st}")
promo/voiceover/scene_times.json +170 -0
@@ -0,0 +1,170 @@
+{
+ "voice": "af_heart",
+ "sr": 24000,
+ "speed": 0.95,
+ "total": 90.725,
+ "scene_start": {
+ "s1": 0.0,
+ "s2": 11.325,
+ "s3": 18.775,
+ "s4": 30.925,
+ "s5": 39.1,
+ "s6": 50.4,
+ "s7": 55.95,
+ "s8": 67.475,
+ "s9": 77.725,
+ "s10": 86.925
+ },
+ "segments": [
+ {
+ "scene": "s1",
+ "start": 0.0,
+ "dur": 1.675,
+ "text": "This is Stepping."
+ },
+ {
+ "scene": "s1",
+ "start": 2.225,
+ "dur": 3.075,
+ "text": "The home for Divine Nine step show culture."
+ },
+ {
+ "scene": "s1",
+ "start": 6.0,
+ "dur": 4.425,
+ "text": "Step shows, strolls, and the people behind them, all in one place."
+ },
+ {
+ "scene": "s2",
+ "start": 11.325,
+ "dur": 2.225,
+ "text": "Start by choosing your city."
+ },
+ {
+ "scene": "s2",
+ "start": 14.05,
+ "dur": 3.875,
+ "text": "Stepping is launching in Atlanta and Washington, D.C."
+ },
+ {
+ "scene": "s3",
+ "start": 18.775,
+ "dur": 2.475,
+ "text": "Inside each city is The Line-Up."
+ },
+ {
+ "scene": "s3",
+ "start": 21.75,
+ "dur": 3.875,
+ "text": "Every step show, stroll, and competition coming up."
+ },
+ {
+ "scene": "s3",
+ "start": 26.275,
+ "dur": 3.8,
+ "text": "Tap any one to see the date, the time, and the details."
+ },
+ {
+ "scene": "s4",
+ "start": 30.925,
+ "dur": 3.625,
+ "text": "On a show's page, you'll find everything you need to know."
+ },
+ {
+ "scene": "s4",
+ "start": 35.1,
+ "dur": 3.15,
+ "text": "And you can join the conversation in the comments."
+ },
+ {
+ "scene": "s5",
+ "start": 39.1,
+ "dur": 2.575,
+ "text": "Then there's The Yard, the community."
+ },
+ {
+ "scene": "s5",
+ "start": 42.175,
+ "dur": 4.15,
+ "text": "Chapters, step teams, DJs, promoters, and classes."
+ },
+ {
+ "scene": "s5",
+ "start": 46.975,
+ "dur": 2.575,
+ "text": "Everyone who makes the culture move."
+ },
+ {
+ "scene": "s6",
+ "start": 50.4,
+ "dur": 2.2,
+ "text": "Each one has its own page,"
+ },
+ {
+ "scene": "s6",
+ "start": 52.95,
+ "dur": 2.15,
+ "text": "so you always know who's who."
+ },
+ {
+ "scene": "s7",
+ "start": 55.95,
+ "dur": 2.05,
+ "text": "Traveling in for a show?"
+ },
+ {
+ "scene": "s7",
+ "start": 58.45,
+ "dur": 2.25,
+ "text": "The Route helps you plan the trip."
+ },
+ {
+ "scene": "s7",
+ "start": 61.2,
+ "dur": 5.375,
+ "text": "Hotels, restaurants, places to go, and the nearest hospital, for every city."
+ },
+ {
+ "scene": "s8",
+ "start": 67.475,
+ "dur": 2.925,
+ "text": "Stepping works right in your phone's browser."
+ },
+ {
+ "scene": "s8",
+ "start": 70.95,
+ "dur": 3.275,
+ "text": "No app store. No accounts to figure out."
+ },
+ {
+ "scene": "s8",
+ "start": 74.775,
+ "dur": 2.05,
+ "text": "Just open it, and start."
+ },
+ {
+ "scene": "s9",
+ "start": 77.725,
+ "dur": 1.925,
+ "text": "And there's more on the way."
+ },
+ {
+ "scene": "s9",
+ "start": 80.15,
+ "dur": 5.825,
+ "text": "Live streaming of shows, more cities across the country, and video from every set."
+ },
+ {
+ "scene": "s10",
+ "start": 86.925,
+ "dur": 1.5,
+ "text": "Stepping."
+ },
+ {
+ "scene": "s10",
+ "start": 88.875,
+ "dur": 1.85,
+ "text": "Built for the culture."
+ }
+ ]
+}