| @@ -7,6 +7,7 @@ export function SmoothScroll({ children }: { children: ReactNode }) { | ||
| useEffect(() => { | ||
| if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; | ||
| const lenis = new Lenis({ duration: 1.1, smoothWheel: true }); | ||
| + | (window as unknown as { __lenis?: Lenis }).__lenis = lenis; | |
| let raf = 0; | ||
| const loop = (t: number) => { | ||
| lenis.raf(t); |
| @@ -10,3 +10,4 @@ __pycache__/ | ||
| node_modules | ||
| recap*.mjs | ||
| + | rec/ |
| @@ -0,0 +1,88 @@ | ||
| + | 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" | |
| + | REC = ROOT + r"\rec" | |
| + | CLIPS = ROOT + r"\build\clips2" | |
| + | 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, W, H = 30, 1920, 1080 | |
| + | MONO, SANS = "fonts/mono.ttf", "fonts/sans.ttf" | |
| + | GOLD, BONE, STAGE = "0xC8A24C", "0xF2ECE0", "0x0F0E0C" | |
| + | VC = "-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": ("desk", "STEPPING / DIVINE NINE STEP CULTURE"), | |
| + | "s2": ("desk", "CHOOSE YOUR CITY"), | |
| + | "s3": ("desk", "THE LINE-UP / UPCOMING SHOWS"), | |
| + | "s4": ("desk", "EVERY SHOW, IN DETAIL"), | |
| + | "s5": ("desk", "THE YARD / THE COMMUNITY"), | |
| + | "s6": ("desk", "PROFILES / WHO'S WHO"), | |
| + | "s7": ("desk", "THE ROUTE / PLAN THE TRIP"), | |
| + | "s8": ("phone", "RIGHT IN YOUR PHONE"), | |
| + | "s9": ("desk", "MORE ON THE WAY"), | |
| + | "s10": ("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(t): | |
| + | if not t: return "" | |
| + | return (f",drawtext=fontfile={MONO}:text='{esc(t)}':fontcolor={GOLD}:fontsize=30" | |
| + | f":x=88:y=h-96:box=1:boxcolor={STAGE}@0.6: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): | |
| + | kind, c = cfg[s]; d = dur(i); 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) | |
| + | src = q(f"{REC}\\{s}.webm") | |
| + | if kind == "desk": | |
| + | vf = f"scale={W}:{H}:force_original_aspect_ratio=increase,crop={W}:{H},fps={FPS},{fin}{cap(c)}" | |
| + | run(f'{q(FF)} -y -sseof -{d} -t {d} -i {src} -vf "{vf}" -r {FPS} {VC} -an {q(out)}') | |
| + | elif kind == "phone": | |
| + | fc = (f"color={STAGE}:s={W}x{H}:d={d}:r={FPS}[bg];[0:v]scale=-1:1000,fps={FPS}[ph];" | |
| + | f"[bg][ph]overlay=(W-w)/2:(H-h)/2,{fin}{cap(c)}") | |
| + | run(f'{q(FF)} -y -sseof -{d} -t {d} -i {src} -filter_complex "{fc}" -r {FPS} {VC} -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}" {VC} -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) + "video2_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) + "audio2.m4a")}') | |
| + | ||
| + | run(f'{q(FF)} -y -i {q(ROOT + chr(92) + "build" + chr(92) + "video2_silent.mp4")} -i {q(ROOT + chr(92) + "build" + chr(92) + "audio2.m4a")} ' | |
| + | f'-map 0:v -map 1:a -c:v copy -c:a copy -shortest {q(ROOT + chr(92) + "build" + chr(92) + "stepping_walkthrough_v2.mp4")}') | |
| + | print("DONE", vlen) |
| @@ -0,0 +1,71 @@ | ||
| + | import { chromium } from "playwright-core"; | |
| + | import { readFileSync, readdirSync, renameSync, mkdirSync } 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 REC = "/shared/projects/SteppingApp/promo/rec"; | |
| + | const BASE = "http://REDACTED-IP:3000"; | |
| + | const DJ = readFileSync(`${SCR}/.djid`, "utf8").trim(); | |
| + | const EV = readFileSync(`${SCR}/.evid`, "utf8").trim(); | |
| + | ||
| + | const scenes = [ | |
| + | { id: "s1", url: "/", vp: [1920, 1080], dur: 11.3, act: "hero" }, | |
| + | { id: "s2", url: "/", vp: [1920, 1080], dur: 7.45, act: "toRoute" }, | |
| + | { id: "s3", url: "/city/atlanta", vp: [1920, 1080], dur: 12.15, act: "scroll" }, | |
| + | { id: "s4", url: `/city/atlanta/event/${EV}`, vp: [1920, 1080], dur: 8.18, act: "scroll" }, | |
| + | { id: "s5", url: "/city/atlanta/yard", vp: [1920, 1080], dur: 11.3, act: "scroll" }, | |
| + | { id: "s6", url: `/city/atlanta/profile/dj/${DJ}`, vp: [1920, 1080], dur: 5.55, act: "hold" }, | |
| + | { id: "s7", url: "/city/atlanta/route", vp: [1920, 1080], dur: 11.5, act: "scroll" }, | |
| + | { id: "s8", url: "/", vp: [430, 932], dur: 10.25, act: "scroll", mobile: true }, | |
| + | { id: "s9", url: "/", vp: [1920, 1080], dur: 9.2, act: "hero" }, | |
| + | ]; | |
| + | ||
| + | const args = ["--no-sandbox", "--disable-dev-shm-usage", "--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"]; | |
| + | const browser = await chromium.launch({ executablePath: EXE, args }); | |
| + | ||
| + | for (const s of scenes) { | |
| + | const dir = `${REC}/${s.id}`; | |
| + | mkdirSync(dir, { recursive: true }); | |
| + | const ctx = await browser.newContext({ | |
| + | viewport: { width: s.vp[0], height: s.vp[1] }, | |
| + | deviceScaleFactor: 1, | |
| + | recordVideo: { dir, size: { width: s.vp[0], height: s.vp[1] } }, | |
| + | ...(s.mobile ? { isMobile: true, hasTouch: true } : {}), | |
| + | }); | |
| + | const page = await ctx.newPage(); | |
| + | await page.goto(`${BASE}${s.url}`, { waitUntil: "networkidle" }); | |
| + | await page.waitForTimeout(s.act === "hero" ? 1800 : 900); | |
| + | const ms = s.dur * 1000; | |
| + | ||
| + | if (s.act === "hero") { | |
| + | const [W, H] = s.vp; | |
| + | const pts = [[W * 0.6, H * 0.42], [W * 0.72, H * 0.55], [W * 0.54, H * 0.5], [W * 0.68, H * 0.46]]; | |
| + | const per = ms / pts.length; | |
| + | for (const [x, y] of pts) { | |
| + | await page.mouse.move(x, y, { steps: 24 }); | |
| + | await page.mouse.click(x, y); | |
| + | await page.waitForTimeout(Math.max(per - 250, 200)); | |
| + | } | |
| + | } else if (s.act === "toRoute") { | |
| + | await page.evaluate(() => { | |
| + | const l = window.__lenis; const el = document.querySelector("#route"); | |
| + | if (el && l) l.scrollTo(el.offsetTop - 90, { duration: 4.5 }); | |
| + | }); | |
| + | await page.waitForTimeout(ms); | |
| + | } else if (s.act === "scroll") { | |
| + | await page.evaluate((d) => { | |
| + | const l = window.__lenis; const t = document.body.scrollHeight - window.innerHeight; | |
| + | if (l) l.scrollTo(t, { duration: d }); else window.scrollTo({ top: t, behavior: "smooth" }); | |
| + | }, s.dur - 1.5); | |
| + | await page.waitForTimeout(ms); | |
| + | } else { | |
| + | await page.waitForTimeout(ms); | |
| + | } | |
| + | ||
| + | await ctx.close(); | |
| + | const f = readdirSync(dir).find((x) => x.endsWith(".webm")); | |
| + | renameSync(`${dir}/${f}`, `${REC}/${s.id}.webm`); | |
| + | console.log(s.id, "recorded"); | |
| + | } | |
| + | await browser.close(); | |
| + | console.log("ALL RECORDED"); |
| @@ -0,0 +1,46 @@ | ||
| + | import { chromium } from "playwright-core"; | |
| + | import { readFileSync, readdirSync, renameSync, mkdirSync } 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 REC = "/shared/projects/SteppingApp/promo/rec"; | |
| + | const BASE = "http://REDACTED-IP:3000"; | |
| + | const DJ = readFileSync(`${SCR}/.djid`, "utf8").trim(); | |
| + | const EV = readFileSync(`${SCR}/.evid`, "utf8").trim(); | |
| + | ||
| + | const tours = { | |
| + | s3: { url: "/city/atlanta", dur: 12.15, pts: [[600,275],[455,745],[1245,755],[455,845],[1245,852],[1165,527],[345,272],[700,668]] }, | |
| + | s4: { url: `/city/atlanta/event/${EV}`, dur: 8.18, pts: [[600,200],[600,463],[690,533],[820,625],[895,864],[660,403]] }, | |
| + | s6: { url: `/city/atlanta/profile/dj/${DJ}`, dur: 5.55, pts: [[210,130],[520,300],[820,430],[430,500],[300,200]] }, | |
| + | }; | |
| + | ||
| + | const args = ["--no-sandbox","--disable-dev-shm-usage","--use-gl=angle","--use-angle=swiftshader","--enable-unsafe-swiftshader","--ignore-gpu-blocklist"]; | |
| + | const browser = await chromium.launch({ executablePath: EXE, args }); | |
| + | ||
| + | for (const [id, t] of Object.entries(tours)) { | |
| + | const dir = `${REC}/${id}`; | |
| + | mkdirSync(dir, { recursive: true }); | |
| + | const ctx = await browser.newContext({ | |
| + | viewport: { width: 1920, height: 1080 }, deviceScaleFactor: 1, | |
| + | recordVideo: { dir, size: { width: 1920, height: 1080 } }, | |
| + | }); | |
| + | const page = await ctx.newPage(); | |
| + | await page.goto(`${BASE}${t.url}`, { waitUntil: "networkidle" }); | |
| + | await page.mouse.move(960, 540); | |
| + | await page.waitForTimeout(900); | |
| + | const end = Date.now() + t.dur * 1000; | |
| + | let i = 0; | |
| + | while (Date.now() < end) { | |
| + | const [x, y] = t.pts[i % t.pts.length]; | |
| + | await page.mouse.move(x, y, { steps: 28 }); | |
| + | if (i % 3 === 2) await page.mouse.click(x, y); | |
| + | await page.waitForTimeout(420); | |
| + | i++; | |
| + | } | |
| + | await ctx.close(); | |
| + | const f = readdirSync(dir).find((x) => x.endsWith(".webm")); | |
| + | renameSync(`${dir}/${f}`, `${REC}/${id}.webm`); | |
| + | console.log(id, "re-recorded (cursor tour)"); | |
| + | } | |
| + | await browser.close(); | |
| + | console.log("DONE"); |