explainer-video 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +35 -0
- package/package.json +56 -4
- package/plugin.json +13 -0
- package/skills/coursework-author/.synced-from +5 -0
- package/skills/coursework-author/SKILL.md +209 -0
- package/skills/craft-codeflow-flows/SKILL.md +116 -0
- package/skills/craft-codeflow-mine/SKILL.md +207 -0
- package/skills/craft-codeflow-plan/SKILL.md +65 -0
- package/skills/craft-codeflow-scene/SKILL.md +168 -0
- package/skills/craft-video-align/SKILL.md +109 -0
- package/skills/craft-video-assemble/SKILL.md +69 -0
- package/skills/craft-video-audio/SKILL.md +59 -0
- package/skills/craft-video-enrich/SKILL.md +46 -0
- package/skills/craft-video-plan/SKILL.md +57 -0
- package/skills/craft-video-publish/SKILL.md +79 -0
- package/skills/craft-video-render/SKILL.md +68 -0
- package/skills/craft-video-scene/SKILL.md +112 -0
- package/skills/craft-video-script/SKILL.md +80 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +659 -0
- package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
- package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
- package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
- package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
- package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
- package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
- package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
- package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
- package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
- package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
- package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
- package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
- package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
- package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
- package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
- package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
- package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
- package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
- package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
- package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
- package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
- package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
- package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
- package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
- package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
- package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
- package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
- package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
- package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
- package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
- package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
- package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
- package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
- package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
- package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
- package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
- package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
- package/skills/explainer-video-from-coursework/SERIES.md +232 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +858 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
- package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
- package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
- package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
- package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
- package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
- package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
- package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
- package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
- package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
- package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
- package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
- package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
- package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
- package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
- package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
- package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
- package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
- package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
- package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
- package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
- package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
- package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
- package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
- package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
- package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
- package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
- package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
- package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
- package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
- package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
- package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
frame-dedup.mjs — capture each DISTINCT frame once; hardlink the repeats.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS
|
|
5
|
+
---------------
|
|
6
|
+
renderAt(t) is a pure function of time, so consecutive output frames are very
|
|
7
|
+
often byte-identical (measured on this pipeline: 82.1% on a declarative diagram
|
|
8
|
+
scene, 90.3% on an svg scene, 99.7% on a held card). Chromium cannot know that,
|
|
9
|
+
so the old capture loop asked for a full 1920x1080 PNG screenshot — roughly 63 ms,
|
|
10
|
+
~94% of render wall time — for every one of the 30 frames per second, and threw
|
|
11
|
+
away the ones that were mere re-photographs.
|
|
12
|
+
|
|
13
|
+
The loop here asks the PAGE instead: it hashes the serialized #stage SVG after each
|
|
14
|
+
renderAt(t) (~3.7 ms) and, when the markup is unchanged, hardlinks the previous
|
|
15
|
+
frame to this frame number — same inode, zero extra bytes — so the existing ffmpeg
|
|
16
|
+
encode path runs unchanged.
|
|
17
|
+
|
|
18
|
+
THE ONE WAY THIS CAN BE WRONG
|
|
19
|
+
-----------------------------
|
|
20
|
+
The probe compares MARKUP. Anything that moves pixels without changing the SVG
|
|
21
|
+
markup — a CSS @keyframes animation, a <canvas>, WebGL — would read as "unchanged"
|
|
22
|
+
and freeze. Two independent guards make that impossible to ship silently:
|
|
23
|
+
|
|
24
|
+
1. GATE before frame 1, if `document.getAnimations().length > 0` the segment
|
|
25
|
+
takes the plain capture path (cheap, decisive, no sampling involved).
|
|
26
|
+
2. AUDIT every AUDIT_EVERY frames, take a REAL screenshot and diff it against
|
|
27
|
+
the frame that was written. On disagreement: write the truthful frame,
|
|
28
|
+
REWIND and re-capture every frame since the last audit, then disable
|
|
29
|
+
dedup for the remainder of the segment.
|
|
30
|
+
|
|
31
|
+
So the fallback is the old behaviour, and it self-triggers. Worst case we pay the
|
|
32
|
+
original price and stay correct.
|
|
33
|
+
|
|
34
|
+
Measured: 2 real videos (72 scenes, ~54,000 frames) re-rendered to SSIM 1.000000
|
|
35
|
+
against renders that photographed every frame, at 3.28x and 4.32x the speed.
|
|
36
|
+
========================================================================== */
|
|
37
|
+
|
|
38
|
+
import { writeFileSync, linkSync, copyFileSync, existsSync } from 'node:fs';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
|
|
41
|
+
/* FNV-1a over the serialized stage + its length. Cheap, deterministic, and it covers
|
|
42
|
+
every attribute scene-core mutates (opacity, transform, stroke, fill, text, count). */
|
|
43
|
+
const HASH_EXPR = `(() => {
|
|
44
|
+
const el = document.getElementById('stage') || document.body;
|
|
45
|
+
const s = el.outerHTML;
|
|
46
|
+
let h = 2166136261;
|
|
47
|
+
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
48
|
+
return (h >>> 0).toString(16) + ':' + s.length;
|
|
49
|
+
})()`;
|
|
50
|
+
|
|
51
|
+
const ANIM_EXPR = '(document.getAnimations ? document.getAnimations().length : 0)';
|
|
52
|
+
const AUDIT_EVERY = 30; // ~1 s of video; 1 extra screenshot per 30 frames when active
|
|
53
|
+
|
|
54
|
+
const framePath = (dir, i) => join(dir, `frame_${String(i).padStart(5, '0')}.png`);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render [tStart, tEnd) at `fps`, writing frame_%05d.png, capturing only distinct states.
|
|
58
|
+
* Returns stats; throws on a missing page (the caller already validated VIDEO_META).
|
|
59
|
+
*/
|
|
60
|
+
export async function runFrames({ dir, fps, tStart, tEnd, evaluate, screenshot, log = console.log }) {
|
|
61
|
+
const total = Math.max(1, Math.round((tEnd - tStart) * fps));
|
|
62
|
+
let screenshots = 0, reused = 0, audits = 0, mismatches = 0;
|
|
63
|
+
let prevHash = null, lastAudit = 0;
|
|
64
|
+
let off = false, offAt = null, offWhy = null;
|
|
65
|
+
const hashes = [];
|
|
66
|
+
|
|
67
|
+
const disable = (why, at) => {
|
|
68
|
+
if (off) return;
|
|
69
|
+
off = true; offAt = at; offWhy = why;
|
|
70
|
+
log(`[dedup] dedup off from frame ${at} (${why}) — plain capture for the rest of this segment`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const t0 = Date.now();
|
|
74
|
+
for (let i = 0; i < total; i++) {
|
|
75
|
+
const t = tStart + i / fps;
|
|
76
|
+
await evaluate(`window.renderAt(${t})`);
|
|
77
|
+
|
|
78
|
+
/* GATE — after frame 0 draws, because a scene may inject its CSS on first render. */
|
|
79
|
+
if (i === 0) {
|
|
80
|
+
const anims = await evaluate(ANIM_EXPR);
|
|
81
|
+
if (anims > 0) disable(`scene declares ${anims} CSS/web animation(s)`, 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hash = await evaluate(HASH_EXPR);
|
|
85
|
+
hashes.push(hash);
|
|
86
|
+
|
|
87
|
+
const canReuse = !off && prevHash !== null && hash === prevHash && existsSync(framePath(dir, i - 1));
|
|
88
|
+
if (canReuse) {
|
|
89
|
+
try { linkSync(framePath(dir, i - 1), framePath(dir, i)); }
|
|
90
|
+
catch { copyFileSync(framePath(dir, i - 1), framePath(dir, i)); } // different filesystem
|
|
91
|
+
reused++;
|
|
92
|
+
} else {
|
|
93
|
+
writeFileSync(framePath(dir, i), await screenshot());
|
|
94
|
+
screenshots++;
|
|
95
|
+
}
|
|
96
|
+
prevHash = hash;
|
|
97
|
+
|
|
98
|
+
/* AUDIT — only meaningful while dedup is live. Once it is off every frame is a real
|
|
99
|
+
capture, so a disagreement could only mean the page animates on its own clock
|
|
100
|
+
between the capture and the audit; re-capturing then would burn time for nothing. */
|
|
101
|
+
if (!off && i > 0 && i % AUDIT_EVERY === 0) {
|
|
102
|
+
audits++;
|
|
103
|
+
const real = await screenshot();
|
|
104
|
+
const { readFileSync } = await import('node:fs');
|
|
105
|
+
if (!real.equals(readFileSync(framePath(dir, i)))) {
|
|
106
|
+
mismatches++;
|
|
107
|
+
log(`[dedup] audit mismatch at frame ${i} (t=${t.toFixed(2)}s): probe said unchanged, pixels differ`);
|
|
108
|
+
writeFileSync(framePath(dir, i), real);
|
|
109
|
+
const from = lastAudit + 1;
|
|
110
|
+
for (let j = from; j < i; j++) {
|
|
111
|
+
await evaluate(`window.renderAt(${tStart + j / fps})`);
|
|
112
|
+
writeFileSync(framePath(dir, j), await screenshot());
|
|
113
|
+
screenshots++; reused--;
|
|
114
|
+
}
|
|
115
|
+
if (i > from) log(`[dedup] re-captured frames ${from}..${i - 1} truthfully`);
|
|
116
|
+
disable('audit caught a change the markup probe missed', i);
|
|
117
|
+
}
|
|
118
|
+
lastAudit = i;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (i % 120 === 0 || i === total - 1) {
|
|
122
|
+
const el = (Date.now() - t0) / 1000;
|
|
123
|
+
const rate = (i + 1) / Math.max(el, 0.001);
|
|
124
|
+
process.stdout.write(`\r[render] ${i + 1}/${total} ${rate.toFixed(1)} fps ` +
|
|
125
|
+
`shots=${screenshots} reused=${reused} eta ${((total - i - 1) / Math.max(rate, 0.001)).toFixed(0)}s `);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write('\n');
|
|
129
|
+
return { total, screenshots, reused, audits, mismatches, distinct: new Set(hashes).size,
|
|
130
|
+
disabledAt: offAt, disabledWhy: offWhy, captureSec: (Date.now() - t0) / 1000 };
|
|
131
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Resolve a Google API key from the OS's native secret store — never from a
|
|
2
|
+
// plaintext file and never from the environment. The secret stays protected
|
|
3
|
+
// from the shell, the harness, and anyone reading /proc, `ps`, or `env`.
|
|
4
|
+
//
|
|
5
|
+
// The store, per platform:
|
|
6
|
+
// macOS Keychain security find-generic-password -s gemini-tts -w
|
|
7
|
+
// Linux Secret Service secret-tool lookup service gemini-tts (libsecret)
|
|
8
|
+
// Windows Credential Manager read via CredRead (advapi32) — the "generic credential"
|
|
9
|
+
// whose Internet/network address is "gemini-tts"
|
|
10
|
+
//
|
|
11
|
+
// Resolution order (first hit wins):
|
|
12
|
+
// 1. --key <value> explicit CLI arg — transient; visible in the
|
|
13
|
+
// process list, so one-off runs only
|
|
14
|
+
// 2. the platform secret store
|
|
15
|
+
//
|
|
16
|
+
// Nothing here ever logs, echoes or returns the key to stdout.
|
|
17
|
+
// Error text is scrubbed before it is printed.
|
|
18
|
+
//
|
|
19
|
+
// NOTE: the Linux and Windows branches are written from documented behaviour
|
|
20
|
+
// and have not been run on those platforms (ROADMAP §2.1 — unverified off macOS).
|
|
21
|
+
// Windows retrieval: https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credreadw
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
|
|
25
|
+
const SERVICE = 'gemini-tts';
|
|
26
|
+
|
|
27
|
+
// Windows CredMan read. CRED_TYPE_GENERIC = 1; the blob is UTF-16LE. The target
|
|
28
|
+
// name is the "Internet or network address" typed in Credential Manager's GUI.
|
|
29
|
+
const CRED_READ = `
|
|
30
|
+
Add-Type @'
|
|
31
|
+
using System;
|
|
32
|
+
using System.Runtime.InteropServices;
|
|
33
|
+
public static class Cred {
|
|
34
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
35
|
+
private struct CREDENTIAL {
|
|
36
|
+
public int Flags; public int Type; public IntPtr TargetName; public IntPtr Comment;
|
|
37
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
38
|
+
public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist;
|
|
39
|
+
public int AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;
|
|
40
|
+
}
|
|
41
|
+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
42
|
+
private static extern bool CredRead(string target, int type, int flags, out IntPtr credential);
|
|
43
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
44
|
+
private static extern void CredFree(IntPtr credential);
|
|
45
|
+
public static string Get(string target) {
|
|
46
|
+
IntPtr p;
|
|
47
|
+
if (!CredRead(target, 1, 0, out p)) return null; // 1 = CRED_TYPE_GENERIC
|
|
48
|
+
try {
|
|
49
|
+
var c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));
|
|
50
|
+
if (c.CredentialBlobSize == 0) return "";
|
|
51
|
+
var b = new byte[c.CredentialBlobSize];
|
|
52
|
+
Marshal.Copy(c.CredentialBlob, b, 0, b.Length);
|
|
53
|
+
return System.Text.Encoding.Unicode.GetString(b);
|
|
54
|
+
} finally { CredFree(p); }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
'@
|
|
58
|
+
$key = [Cred]::Get('${SERVICE}')
|
|
59
|
+
if ($null -eq $key) { exit 44 }
|
|
60
|
+
[Console]::Write($key)
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
function run(cmd, args) {
|
|
64
|
+
return execFileSync(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The secret-store backend for this platform, or null if there is none. */
|
|
68
|
+
function backend() {
|
|
69
|
+
switch (process.platform) {
|
|
70
|
+
case 'darwin':
|
|
71
|
+
return {
|
|
72
|
+
name: 'Keychain',
|
|
73
|
+
retrieve: () => run('security', ['find-generic-password', '-s', SERVICE, '-w']),
|
|
74
|
+
storeHint: `security add-generic-password -U -s ${SERVICE} -a "$USER" -w (prompts for the key)`,
|
|
75
|
+
};
|
|
76
|
+
case 'linux':
|
|
77
|
+
return {
|
|
78
|
+
name: 'Secret Service (secret-tool)',
|
|
79
|
+
retrieve: () => run('secret-tool', ['lookup', 'service', SERVICE]),
|
|
80
|
+
storeHint: `secret-tool store --label="Gemini TTS" service ${SERVICE} (reads the key from stdin — paste it, then Ctrl-D)`,
|
|
81
|
+
};
|
|
82
|
+
case 'win32':
|
|
83
|
+
return {
|
|
84
|
+
name: 'Credential Manager',
|
|
85
|
+
retrieve: () => run('powershell.exe', ['-NoProfile', '-Command', CRED_READ]),
|
|
86
|
+
storeHint: `Control Panel → User Accounts → Credential Manager → Windows Credentials → "Add a generic credential" — Internet or network address: ${SERVICE}, Password: your API key`,
|
|
87
|
+
};
|
|
88
|
+
default:
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @returns {{status:'ok'|'empty'|'missing'|'unsupported', key?:string}}
|
|
95
|
+
*/
|
|
96
|
+
export function fromSecretStore() {
|
|
97
|
+
const b = backend();
|
|
98
|
+
if (!b) return { status: 'unsupported' };
|
|
99
|
+
let out;
|
|
100
|
+
try {
|
|
101
|
+
out = b.retrieve();
|
|
102
|
+
} catch {
|
|
103
|
+
return { status: 'missing' }; // macOS exits 44 when the item is absent
|
|
104
|
+
}
|
|
105
|
+
const key = out.trim();
|
|
106
|
+
// An entry that exists but was stored empty has a zero-length password.
|
|
107
|
+
return key ? { status: 'ok', key } : { status: 'empty' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @returns {{key: string|null, source: string, store: string}}
|
|
112
|
+
*/
|
|
113
|
+
export function loadKey(explicit) {
|
|
114
|
+
const st = fromSecretStore();
|
|
115
|
+
|
|
116
|
+
if (explicit && typeof explicit === 'string' && explicit !== 'true') {
|
|
117
|
+
return { key: explicit, source: 'cli --key', store: st.status };
|
|
118
|
+
}
|
|
119
|
+
if (st.status === 'ok') {
|
|
120
|
+
return { key: st.key, source: `secret store (${backend().name})`, store: st.status };
|
|
121
|
+
}
|
|
122
|
+
return { key: null, source: 'none', store: st.status };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Remove anything that looks like a key from a string before printing it. */
|
|
126
|
+
export function scrub(text) {
|
|
127
|
+
if (!text) return text;
|
|
128
|
+
return String(text)
|
|
129
|
+
.replace(/AIza[0-9A-Za-z_\-]{10,}/g, 'AIza***REDACTED***')
|
|
130
|
+
.replace(/([?&]key=)[^&\s"']+/gi, '$1***REDACTED***')
|
|
131
|
+
.replace(/(x-goog-api-key["':\s]+)[0-9A-Za-z_\-]{10,}/gi, '$1***REDACTED***');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Human-readable description of where a key came from, with no secret in it. */
|
|
135
|
+
export function describe(src, key) {
|
|
136
|
+
if (!key) return 'not found';
|
|
137
|
+
return `${src}, ${key.length} chars`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** User-facing "here is how to store your key", per platform and status. */
|
|
141
|
+
export function storeHint(status) {
|
|
142
|
+
const b = backend();
|
|
143
|
+
if (!b) return '';
|
|
144
|
+
if (status === 'empty') {
|
|
145
|
+
return `\n A ${b.name} credential exists but is empty — store it again:\n ${b.storeHint}\n`;
|
|
146
|
+
}
|
|
147
|
+
if (status === 'missing') {
|
|
148
|
+
return `\n Store the key in ${b.name}:\n ${b.storeHint}\n`;
|
|
149
|
+
}
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
model.mjs — the ONE place the duration/estimate constants live.
|
|
3
|
+
|
|
4
|
+
These feed ONLY the estimation layer: planner packing, script-check budget,
|
|
5
|
+
and pacing-check's fallback. The audio/video stages never read them — they
|
|
6
|
+
measure real durations with ffprobe (build-audio → timeline.json → render).
|
|
7
|
+
|
|
8
|
+
Both numbers are MEASURED, not guessed (video 6, the one fully-produced run):
|
|
9
|
+
WPM = 153 Gemini Aoede narration pace: 508 words → 199.0s of audio
|
|
10
|
+
EXPANSION = 1.10 picture-over-narration overhead: 205.7s picture / 199.0s
|
|
11
|
+
narration = 1.03, plus a small buffer for code-heavy
|
|
12
|
+
videos we have not measured yet.
|
|
13
|
+
|
|
14
|
+
If a future run measures a different pace, change it HERE, not in the tools.
|
|
15
|
+
|
|
16
|
+
MAX_PAUSES = 3 pause prompts per code video (design §5.5): script-check and the
|
|
17
|
+
codebase skill's excerpt-check enforce the same cap; a fourth forces a split.
|
|
18
|
+
========================================================================== */
|
|
19
|
+
export const WPM = 153;
|
|
20
|
+
export const EXPANSION = 1.10;
|
|
21
|
+
export const MAX_PAUSES = 3;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
naming.mjs — one stable, order-preserving folder name per finished video.
|
|
3
|
+
|
|
4
|
+
The finished series lands as one folder per video:
|
|
5
|
+
|
|
6
|
+
<destination>/<NN>-<slug>/video.mp4
|
|
7
|
+
<destination>/<NN>-<slug>/description.md
|
|
8
|
+
<destination>/<NN>-<slug>/captions.srt
|
|
9
|
+
|
|
10
|
+
`NN` is the plan's `videoIndex` — the consumption order, stable and monotonic —
|
|
11
|
+
and `slug` is a filesystem-safe form of the video title. Because each video owns
|
|
12
|
+
its own folder, the fixed sidecar filenames (`description.md`, `captions.srt`)
|
|
13
|
+
can never collide across videos, and listing the destination sorts it into
|
|
14
|
+
watch order. `publish.mjs` and `assemble.mjs` both import this so the two agree
|
|
15
|
+
on the folder, regardless of the ad-hoc `--name` the agent may pick.
|
|
16
|
+
========================================================================== */
|
|
17
|
+
|
|
18
|
+
export function slugify(s) {
|
|
19
|
+
return String(s || '')
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.normalize('NFKD') // decompose, then strip combining accents
|
|
22
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
23
|
+
.replace(/[^a-z0-9]+/g, '-') // any run of non-alphanumerics → one dash
|
|
24
|
+
.replace(/^-+|-+$/g, '') // no leading/trailing dashes
|
|
25
|
+
.slice(0, 64); // keep folders and paths short
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Derive the per-video folder from the script. Falls back to `fallback` (the work
|
|
29
|
+
dir's `<NN>` basename) when the script is missing or carries no index/title. */
|
|
30
|
+
export function videoDir(script, fallback) {
|
|
31
|
+
const hasIdx = script && script.videoIndex != null;
|
|
32
|
+
const idx = hasIdx ? String(script.videoIndex) : '';
|
|
33
|
+
const slug = slugify(script && script.title);
|
|
34
|
+
if (idx && slug) return `${idx}-${slug}`;
|
|
35
|
+
if (idx) return idx;
|
|
36
|
+
if (slug) return slug;
|
|
37
|
+
return fallback || 'video';
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* onset-audit.mjs — the earliest visual event in each scene, in seconds after the
|
|
2
|
+
narration starts. A scene whose first visual lands more than ~2.5s in opens on a
|
|
3
|
+
near-empty stage (header + badge only), which reads as a dead cut. */
|
|
4
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const off = v => {
|
|
9
|
+
if (typeof v === 'number') return v;
|
|
10
|
+
if (typeof v === 'string') {
|
|
11
|
+
const m = /^[a-z][a-z0-9-]*@(-?[0-9.]+)$/.exec(v);
|
|
12
|
+
if (m) return parseFloat(m[1]);
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
};
|
|
16
|
+
const push = (arr, v) => { const o = off(v); if (o != null) arr.push(o); };
|
|
17
|
+
|
|
18
|
+
const roots = process.argv.slice(2);
|
|
19
|
+
const rows = [];
|
|
20
|
+
for (const root of roots) {
|
|
21
|
+
const dir = resolve(root, 'scenes');
|
|
22
|
+
let files;
|
|
23
|
+
try { files = readdirSync(dir).filter(f => f.endsWith('.js')); } catch { continue; }
|
|
24
|
+
for (const f of files) {
|
|
25
|
+
global.window = {};
|
|
26
|
+
await import(pathToFileURL(resolve(dir, f)).href);
|
|
27
|
+
const cfg = global.window.SCENE_CONFIG;
|
|
28
|
+
if (!cfg) { rows.push({ scene: f, onset: null, note: 'no SCENE_CONFIG' }); continue; }
|
|
29
|
+
const onsets = [];
|
|
30
|
+
Object.values(cfg.reveal || {}).forEach(a => push(onsets, a[0]));
|
|
31
|
+
Object.values(cfg.draw || {}).forEach(d => push(onsets, d.at && d.at[0]));
|
|
32
|
+
Object.values(cfg.hide || {}).forEach(a => push(onsets, a[0]));
|
|
33
|
+
(cfg.banners || []).forEach(b => push(onsets, b.from));
|
|
34
|
+
(cfg.flows || []).forEach(fl => push(onsets, fl.from));
|
|
35
|
+
const OV = cfg.overlays || {};
|
|
36
|
+
if (OV.cards) push(onsets, OV.cards.inAt);
|
|
37
|
+
if (OV.chips) push(onsets, OV.chips.at);
|
|
38
|
+
if (OV.title) push(onsets, OV.title.from);
|
|
39
|
+
if (OV.finalLine) push(onsets, OV.finalLine.at);
|
|
40
|
+
const finite = onsets.filter(n => Number.isFinite(n));
|
|
41
|
+
rows.push({ scene: f, onset: finite.length ? Math.min(...finite) : null });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
rows.sort((a, b) => (b.onset ?? -1) - (a.onset ?? -1));
|
|
45
|
+
const bad = rows.filter(r => (r.onset ?? 99) > 2.5);
|
|
46
|
+
console.log('scene first visual (s after narration start)');
|
|
47
|
+
console.log('─'.repeat(72));
|
|
48
|
+
rows.forEach(r => console.log(` ${r.scene.padEnd(30)} ${r.onset == null ? 'NONE' : r.onset.toFixed(1)}`));
|
|
49
|
+
console.log('─'.repeat(72));
|
|
50
|
+
console.log(`${rows.length} scenes · ${bad.length} open later than 2.5s`);
|
|
51
|
+
bad.forEach(r => console.log(` ✗ ${r.scene} first visual at ${r.onset.toFixed(1)}s`));
|