| @@ -0,0 +1,9 @@ | ||
| + | # heavy media + transient render artifacts (scripts are kept) | |
| + | node_modules/ | |
| + | shots/ | |
| + | build/ | |
| + | *.wav | |
| + | *.mp3 | |
| + | *.mp4 | |
| + | voiceover/kvenv/ | |
| + | __pycache__/ |
| @@ -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 )") |
| @@ -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") |
| @@ -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"); |
| @@ -0,0 +1 @@ | ||
| + | /root/.npm/_npx/361ceb562f3b3235/node_modules |
| @@ -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(); |
| @@ -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(); |
| @@ -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}") |
| @@ -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." | |
| + | } | |
| + | ] | |
| + | } |