@homie-rocks/studio 0.51.4 → 0.51.5
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/CHANGELOG.md +29 -0
- package/README.md +10 -29
- package/apps/APPS.md +1 -1
- package/bin/homie-studio.mjs +28 -30
- package/guides/app/SKILL.md +1 -1
- package/guides/art/SKILL.md +68 -140
- package/guides/art/scripts/art.mjs +6 -79
- package/guides/models/SKILL.md +73 -177
- package/guides/models/references/RIGHTS.md +1 -1
- package/guides/models/scripts/models.mjs +7 -288
- package/guides/music/SKILL.md +74 -183
- package/guides/music/references/COMPOSITION.md +21 -26
- package/guides/music/references/GAME-AUDIO.md +3 -3
- package/guides/music/references/RIGHTS.md +12 -15
- package/guides/music/scripts/music.mjs +11 -202
- package/guides/parts/SKILL.md +1 -1
- package/guides/shop/SKILL.md +68 -77
- package/guides/sound/SKILL.md +70 -1
- package/guides/sound/scripts/player/sound.js +233 -0
- package/guides/studio-setup/SKILL.md +11 -27
- package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
- package/guides/style/SKILL.md +66 -3
- package/guides/video/SKILL.md +83 -0
- package/guides/video/references/DELIVERY.md +73 -0
- package/guides/video/references/HONESTY.md +40 -0
- package/guides/video/references/METHOD.md +144 -0
- package/guides/video/references/RECORD.md +137 -0
- package/guides/video/references/TRAILER.md +154 -0
- package/guides/video/references/examples/site-walkthrough.json +14 -0
- package/guides/video/references/examples/studio-play-phone.json +15 -0
- package/guides/video/references/examples/studio-play.json +17 -0
- package/guides/video/scripts/capture-game.mjs +175 -0
- package/guides/video/scripts/clock.js +136 -0
- package/guides/video/scripts/drawover/film.js +58 -0
- package/guides/video/scripts/drawover/index.html +9 -0
- package/guides/video/scripts/drawover/look.js +115 -0
- package/guides/video/scripts/drawover/shots.json +12 -0
- package/guides/video/scripts/lib/browser.mjs +87 -0
- package/guides/video/scripts/lib/frames.mjs +170 -0
- package/guides/video/scripts/lib/remix.mjs +159 -0
- package/guides/video/scripts/qa.mjs +127 -0
- package/guides/video/scripts/record-fixed.mjs +403 -0
- package/guides/video/scripts/record-page.mjs +477 -0
- package/guides/video/scripts/tap.js +108 -0
- package/guides/video/scripts/video.mjs +888 -0
- package/lib/build.mjs +12 -9
- package/lib/cloudflare.mjs +6 -11
- package/lib/dev.mjs +1 -2
- package/lib/doctor.mjs +10 -54
- package/lib/mcp-tools.mjs +42 -21
- package/lib/mcp.mjs +1 -1
- package/lib/media-import.mjs +41 -0
- package/lib/media-workflows.mjs +13 -0
- package/lib/official-media.mjs +15 -0
- package/lib/rules-build.mjs +13 -5
- package/lib/scaffold.mjs +2 -2
- package/lib/shop-links.mjs +54 -18
- package/lib/shop-listen.mjs +43 -0
- package/lib/shop-local.mjs +1 -1
- package/lib/shop-mcp.mjs +58 -0
- package/lib/stripe-connect.mjs +36 -15
- package/lib/template-history.json +45 -0
- package/lib/upgrade-media.mjs +13 -0
- package/lib/upgrade.mjs +10 -0
- package/package.json +1 -1
- package/parts/PARTS.md +1 -1
- package/rules/view.ts +4 -0
- package/shop/SHOP.md +66 -34
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +5 -1
- package/worker/purchase-reconciliation.mjs +16 -1
- package/worker/shop-subscriptions.mjs +2 -0
- package/worker/shop.mjs +7 -5
- package/worker/stripe-ownership.mjs +7 -0
- package/worker/version.mjs +1 -1
- package/guides/models/references/models.json +0 -175
- package/guides/music/scripts/lib/eleven.mjs +0 -260
- package/lib/projects-env.mjs +0 -68
- package/lib/projects.mjs +0 -311
- package/lib/provider-key.mjs +0 -21
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* sound.js: a game's sound effects and music, from the files the Homie plugin's sound skill made
|
|
3
|
+
* (sound.json beside them lists what exists). Plain JavaScript, no dependencies.
|
|
4
|
+
*
|
|
5
|
+
* import { createSound } from './sound.js'; // bundle games: from src/; static games: from sound/
|
|
6
|
+
* const sound = createSound({ base: 'sound/' }); // where sound.json and the files are served, relative to the page
|
|
7
|
+
* sound.play('jump'); // a random variant, a little pitch jitter, never more than 4 at once
|
|
8
|
+
* sound.play('coin', { pitch: combo, volume: 0.8, pan: -0.3 });
|
|
9
|
+
* sound.music('theme'); // the theme's first loop, from the first touch on
|
|
10
|
+
* sound.section('chase'); // the next loop, starting exactly on the next bar line
|
|
11
|
+
* sound.duck(0.4, 0.5); // music down to 40% for half a second (a big hit, a line of speech)
|
|
12
|
+
* sound.volume({ sfx: 1, music: 0.6 }); sound.mute(true);
|
|
13
|
+
*
|
|
14
|
+
* Sound starts on the first touch, click or key, with no "tap for sound" screen: browsers keep audio
|
|
15
|
+
* suspended until a person has touched the page, so the first gesture resumes it (a held stick counts
|
|
16
|
+
* when the finger lifts). A sound asked for before that is dropped, not queued: a late hit is worse than none.
|
|
17
|
+
* window.__homieSound holds counters for a playtest (what played, when); it never controls anything.
|
|
18
|
+
*
|
|
19
|
+
* THE CAPTURE LOG (off unless a recorder asks). A recorder that renders the page frame by frame cannot
|
|
20
|
+
* record the speaker, so it asks for a log instead: before the page's scripts run it sets
|
|
21
|
+
*
|
|
22
|
+
* window.__homieSoundCapture = { events: [] };
|
|
23
|
+
*
|
|
24
|
+
* and from then on every sound this player schedules is pushed onto `events` as it is scheduled, stamped
|
|
25
|
+
* with performance.now() (`t`, ms), enough to mix the same sound again offline from the same files:
|
|
26
|
+
*
|
|
27
|
+
* { t, type: 'start', id, bus: 'sfx' | 'music', name, url, gain, rate, pan, loop, delay, fadeIn, bar? }
|
|
28
|
+
* { t, type: 'stop', id, delay, fade, curve: 'cut' | 'linear' | 'target' }
|
|
29
|
+
* { t, type: 'duck', to, seconds } music down to `to`, back after `seconds`
|
|
30
|
+
* { t, type: 'levels', master, sfx, music, muted }
|
|
31
|
+
* { t, type: 'drop', name, why } asked for and not played: 'locked', 'missing', 'loading', 'voices'
|
|
32
|
+
*
|
|
33
|
+
* `delay` is seconds after `t`; `url` is absolute; `rate` is the playback rate after pitch and jitter (the
|
|
34
|
+
* random choices are in the log, so the mix repeats them). With the log on, bar lines are counted on the page's
|
|
35
|
+
* clock (performance.now) instead of the audio clock, so a recorder's virtual clock moves them too.
|
|
36
|
+
* A game with its own audio code can push the same events itself; the video skill's `trailer` reads them.
|
|
37
|
+
* Without that global nothing is logged and nothing here costs anything.
|
|
38
|
+
*/
|
|
39
|
+
export function createSound({ base = 'sound/', manifest = 'sound.json', maxPerName = 4, maxVoices = 24 } = {}) {
|
|
40
|
+
const AC = window.AudioContext || window.webkitAudioContext;
|
|
41
|
+
const ctx = AC ? new AC({ latencyHint: 'interactive' }) : null;
|
|
42
|
+
const url = (f) => new URL(f, new URL(base, document.baseURI)).toString();
|
|
43
|
+
const buses = {};
|
|
44
|
+
const buffers = new Map();
|
|
45
|
+
const live = new Map();
|
|
46
|
+
const stats = { unlocked: false, unlockedAt: null, plays: [], missing: [], errors: [], music: null };
|
|
47
|
+
let meta = { sfx: {}, music: {} };
|
|
48
|
+
let muted = false;
|
|
49
|
+
const levels = { master: 1, sfx: 1, music: 0.7 };
|
|
50
|
+
let track = null;
|
|
51
|
+
// The capture log: only when a recorder put the global there before this ran (see the head of this file).
|
|
52
|
+
let cap = null;
|
|
53
|
+
try { const c = window.__homieSoundCapture; if (c && Array.isArray(c.events)) cap = c; } catch (e) { /* not ours to read */ }
|
|
54
|
+
let capId = 0;
|
|
55
|
+
const logEv = (e) => { if (cap) { e.t = performance.now(); cap.events.push(e); } return e; };
|
|
56
|
+
const fileOf = new Map(); // a variant's key -> the file that decoded (what the log names)
|
|
57
|
+
|
|
58
|
+
if (ctx) {
|
|
59
|
+
buses.master = ctx.createGain();
|
|
60
|
+
// A gentle safety limiter: many hits at once must not clip.
|
|
61
|
+
const comp = ctx.createDynamicsCompressor();
|
|
62
|
+
comp.threshold.value = -6; comp.knee.value = 6; comp.ratio.value = 8; comp.attack.value = 0.003; comp.release.value = 0.15;
|
|
63
|
+
buses.master.connect(comp).connect(ctx.destination);
|
|
64
|
+
buses.sfx = ctx.createGain(); buses.sfx.connect(buses.master);
|
|
65
|
+
buses.music = ctx.createGain(); buses.music.connect(buses.master);
|
|
66
|
+
buses.duck = ctx.createGain(); buses.duck.connect(buses.music);
|
|
67
|
+
applyLevels();
|
|
68
|
+
try { if (navigator.audioSession) navigator.audioSession.type = 'playback'; } catch (e) { /* not iOS */ }
|
|
69
|
+
const unlock = () => {
|
|
70
|
+
if (ctx.state !== 'running') ctx.resume().catch(() => {});
|
|
71
|
+
try { const b = ctx.createBuffer(1, 1, 22050); const s = ctx.createBufferSource(); s.buffer = b; s.connect(ctx.destination); s.start(0); } catch (e) { /* closed */ }
|
|
72
|
+
if (!stats.unlocked && ctx.state === 'running') { stats.unlocked = true; stats.unlockedAt = performance.now(); }
|
|
73
|
+
if (track && !track.started && ctx.state === 'running') startTrack();
|
|
74
|
+
};
|
|
75
|
+
for (const ev of ['pointerdown', 'pointerup', 'touchstart', 'touchend', 'keydown', 'click']) window.addEventListener(ev, unlock, { capture: true, passive: true });
|
|
76
|
+
ctx.addEventListener('statechange', () => { if (ctx.state === 'running') { stats.unlocked = true; stats.unlockedAt = stats.unlockedAt || performance.now(); if (track && !track.started) startTrack(); } });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ready = fetch(url(manifest)).then((r) => { if (!r.ok) throw new Error(`${manifest}: HTTP ${r.status}`); return r.json(); }).then((m) => {
|
|
80
|
+
meta = { sfx: m.sfx || {}, music: m.music || {} };
|
|
81
|
+
const variants = Object.values(meta.sfx).flat();
|
|
82
|
+
const loops = Object.values(meta.music).flatMap((t) => Object.values(t.loops || {}));
|
|
83
|
+
return Promise.all([...variants, ...loops].map(loadFirst));
|
|
84
|
+
}).catch((e) => { stats.errors.push(String(e && e.message || e)); });
|
|
85
|
+
|
|
86
|
+
function load(file) {
|
|
87
|
+
if (!ctx || buffers.has(file)) return buffers.get(file);
|
|
88
|
+
const p = fetch(url(file)).then((r) => { if (!r.ok) throw new Error(`${file}: HTTP ${r.status}`); return r.arrayBuffer(); })
|
|
89
|
+
.then((b) => new Promise((ok, no) => ctx.decodeAudioData(b, ok, no)))
|
|
90
|
+
.then((buf) => { buffers.set(file, buf); return buf; })
|
|
91
|
+
.catch((e) => { stats.errors.push(String(e && e.message || e)); buffers.delete(file); return null; });
|
|
92
|
+
buffers.set(file, p);
|
|
93
|
+
return p;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A list's own key, never a file's name: a list of ONE file ("jump.wav", or ["jump.wav"]) used to be kept under
|
|
97
|
+
// that file's name, so loading the file found the list's own unfinished promise and waited on itself for ever
|
|
98
|
+
// (`ready` never resolved, and music asked for before it never started).
|
|
99
|
+
const keyOf = (list) => `=${Array.isArray(list) ? list.join('|') : list}`;
|
|
100
|
+
/** A file listed as [ogg, wav]: the first this browser decodes wins (older Safari cannot decode Ogg). */
|
|
101
|
+
function loadFirst(list) {
|
|
102
|
+
const files = Array.isArray(list) ? list : [list];
|
|
103
|
+
const key = keyOf(list);
|
|
104
|
+
if (buffers.has(key)) return buffers.get(key);
|
|
105
|
+
const p = files.reduce((prev, f) => prev.then((buf) => buf || load(f).then((b) => (b instanceof AudioBuffer ? b : null))), Promise.resolve(null))
|
|
106
|
+
.then((buf) => { if (buf) { buffers.set(key, buf); fileOf.set(key, files.find((f) => buffers.get(f) === buf) ?? files[0]); } else buffers.delete(key); return buf; });
|
|
107
|
+
buffers.set(key, p);
|
|
108
|
+
return p;
|
|
109
|
+
}
|
|
110
|
+
const loopBuffer = (list) => buffers.get(keyOf(list));
|
|
111
|
+
|
|
112
|
+
function applyLevels() {
|
|
113
|
+
if (!ctx) return;
|
|
114
|
+
const t = ctx.currentTime;
|
|
115
|
+
buses.master.gain.setTargetAtTime(muted ? 0 : levels.master, t, 0.02);
|
|
116
|
+
buses.sfx.gain.setTargetAtTime(levels.sfx, t, 0.02);
|
|
117
|
+
buses.music.gain.setTargetAtTime(levels.music, t, 0.05);
|
|
118
|
+
logEv({ type: 'levels', master: levels.master, sfx: levels.sfx, music: levels.music, muted });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function play(name, { volume = 1, pitch = 0, pan = 0, jitter = 0.35 } = {}) {
|
|
122
|
+
stats.plays.push({ name, t: Math.round(performance.now()) });
|
|
123
|
+
if (stats.plays.length > 500) stats.plays.splice(0, 250);
|
|
124
|
+
if (!ctx || ctx.state !== 'running') { logEv({ type: 'drop', name, why: 'locked' }); return null; }
|
|
125
|
+
const variants = meta.sfx[name];
|
|
126
|
+
if (!variants || !variants.length) { if (!stats.missing.includes(name)) stats.missing.push(name); logEv({ type: 'drop', name, why: 'missing' }); return null; }
|
|
127
|
+
const variant = variants[Math.floor(Math.random() * variants.length)];
|
|
128
|
+
const buf = loopBuffer(variant);
|
|
129
|
+
if (!(buf instanceof AudioBuffer)) { logEv({ type: 'drop', name, why: 'loading' }); return null; }
|
|
130
|
+
const mine = live.get(name) || [];
|
|
131
|
+
while (mine.length >= maxPerName) { const old = mine.shift(); try { old.stop(); } catch (e) { /* ended */ } logEv({ type: 'stop', id: old.capId, delay: 0, fade: 0, curve: 'cut' }); }
|
|
132
|
+
let total = 0; for (const v of live.values()) total += v.length;
|
|
133
|
+
if (total >= maxVoices) { logEv({ type: 'drop', name, why: 'voices' }); return null; }
|
|
134
|
+
const src = ctx.createBufferSource();
|
|
135
|
+
src.buffer = buf;
|
|
136
|
+
src.playbackRate.value = 2 ** ((pitch + (Math.random() * 2 - 1) * jitter) / 12);
|
|
137
|
+
if (cap) { src.capId = ++capId; logEv({ type: 'start', id: src.capId, bus: 'sfx', name, url: url(fileOf.get(keyOf(variant)) ?? (Array.isArray(variant) ? variant[0] : variant)), gain: volume, rate: src.playbackRate.value, pan: Math.max(-1, Math.min(1, pan || 0)), loop: false, delay: 0, fadeIn: 0 }); }
|
|
138
|
+
const g = ctx.createGain(); g.gain.value = volume;
|
|
139
|
+
let node = src.connect(g);
|
|
140
|
+
if (pan && ctx.createStereoPanner) { const p = ctx.createStereoPanner(); p.pan.value = Math.max(-1, Math.min(1, pan)); node = node.connect(p); }
|
|
141
|
+
node.connect(buses.sfx);
|
|
142
|
+
src.onended = () => { const i = mine.indexOf(src); if (i >= 0) mine.splice(i, 1); };
|
|
143
|
+
mine.push(src); live.set(name, mine);
|
|
144
|
+
src.start();
|
|
145
|
+
return src;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function startTrack() {
|
|
149
|
+
if (!track || !ctx || ctx.state !== 'running') return;
|
|
150
|
+
const buf = loopBuffer(track.loops[track.section]);
|
|
151
|
+
if (!(buf instanceof AudioBuffer)) { ready.then(() => { if (track && !track.started) startTrack(); }); return; }
|
|
152
|
+
track.started = true;
|
|
153
|
+
track.t0 = ctx.currentTime + 0.05;
|
|
154
|
+
track.c0 = performance.now() / 1000 + 0.05; // the same moment on the page's clock, for the capture log's bar lines
|
|
155
|
+
track.node = loopNode(buf, track.t0, track.fade, track.section);
|
|
156
|
+
stats.music = { name: track.name, section: track.section, since: Math.round(performance.now()) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loopNode(buf, at, fade, sectionName) {
|
|
160
|
+
const src = ctx.createBufferSource(); src.buffer = buf; src.loop = true;
|
|
161
|
+
if (cap) {
|
|
162
|
+
const list = track.loops[sectionName];
|
|
163
|
+
src.capId = ++capId;
|
|
164
|
+
logEv({ type: 'start', id: src.capId, bus: 'music', name: `${track.name}/${sectionName}`, url: url(fileOf.get(keyOf(list)) ?? (Array.isArray(list) ? list[0] : list)), gain: 1, rate: 1, pan: 0, loop: true, delay: Math.max(0, at - ctx.currentTime), fadeIn: Math.max(0.005, fade), bar: track.bar });
|
|
165
|
+
}
|
|
166
|
+
const g = ctx.createGain(); g.gain.setValueAtTime(0, at); g.gain.linearRampToValueAtTime(1, at + Math.max(0.005, fade));
|
|
167
|
+
src.connect(g).connect(buses.duck);
|
|
168
|
+
src.start(at);
|
|
169
|
+
return { src, g };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let loaded = false;
|
|
173
|
+
ready.then(() => { loaded = true; });
|
|
174
|
+
function music(name, opts = {}) {
|
|
175
|
+
// Called before sound.json has arrived (the usual case: at the top of the game): start it once it has.
|
|
176
|
+
if (!loaded) { ready.then(() => music(name, opts)); return; }
|
|
177
|
+
const { section = null, fade = 0.4 } = opts;
|
|
178
|
+
const t = meta.music[name];
|
|
179
|
+
if (!t) { stats.errors.push(`no music "${name}" in ${manifest}`); return; }
|
|
180
|
+
stopMusic({ fade: 0.2 });
|
|
181
|
+
const first = section || Object.keys(t.loops || {})[0];
|
|
182
|
+
track = { name, loops: t.loops, bar: t.barSeconds, section: first, started: false, fade };
|
|
183
|
+
startTrack();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Switch loops on the next bar line (every loop shares the tempo, so bar lines line up). */
|
|
187
|
+
function section(name, { fade = 0.03 } = {}) {
|
|
188
|
+
if (!track || !track.loops[name] || name === track.section) return;
|
|
189
|
+
track.section = name;
|
|
190
|
+
if (!track.started) return;
|
|
191
|
+
const buf = loopBuffer(track.loops[name]);
|
|
192
|
+
if (!(buf instanceof AudioBuffer)) return;
|
|
193
|
+
const now = ctx.currentTime;
|
|
194
|
+
let at = track.t0 + Math.ceil((now + 0.02 - track.t0) / track.bar) * track.bar;
|
|
195
|
+
if (cap) {
|
|
196
|
+
// With the capture log on, the next bar line is counted on the page's clock: a recorder's virtual clock is the
|
|
197
|
+
// one the film is on, and the audio clock runs at the wall's speed beside it.
|
|
198
|
+
const cnow = performance.now() / 1000;
|
|
199
|
+
const wait = track.c0 + Math.ceil((cnow + 0.02 - track.c0) / track.bar) * track.bar - cnow;
|
|
200
|
+
at = now + wait; track.c0 = cnow + wait;
|
|
201
|
+
}
|
|
202
|
+
const old = track.node;
|
|
203
|
+
old.g.gain.setValueAtTime(1, at - fade); old.g.gain.linearRampToValueAtTime(0, at);
|
|
204
|
+
try { old.src.stop(at + 0.05); } catch (e) { /* */ }
|
|
205
|
+
logEv({ type: 'stop', id: old.src.capId, delay: Math.max(0, at - fade - now), fade, curve: 'linear' });
|
|
206
|
+
track.node = loopNode(buf, at - fade, fade, name);
|
|
207
|
+
track.t0 = at;
|
|
208
|
+
stats.music = { name: track.name, section: name, since: Math.round(performance.now()) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stopMusic({ fade = 0.5 } = {}) {
|
|
212
|
+
if (!track) return;
|
|
213
|
+
if (track.node && ctx) { const t = ctx.currentTime; track.node.g.gain.setTargetAtTime(0, t, fade / 3); try { track.node.src.stop(t + fade + 0.1); } catch (e) { /* */ } logEv({ type: 'stop', id: track.node.src.capId, delay: 0, fade, curve: 'target' }); }
|
|
214
|
+
track = null; stats.music = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function duck(to = 0.4, seconds = 0.4) {
|
|
218
|
+
if (!ctx) return;
|
|
219
|
+
const t = ctx.currentTime; const g = buses.duck.gain;
|
|
220
|
+
g.cancelScheduledValues(t); g.setTargetAtTime(to, t, 0.015); g.setTargetAtTime(1, t + seconds, 0.12);
|
|
221
|
+
logEv({ type: 'duck', to, seconds });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const api = {
|
|
225
|
+
ready, play, music, section, stopMusic, duck,
|
|
226
|
+
volume(v) { Object.assign(levels, v || {}); applyLevels(); },
|
|
227
|
+
mute(on = true) { muted = !!on; applyLevels(); },
|
|
228
|
+
get context() { return ctx; },
|
|
229
|
+
state: () => ({ unlocked: stats.unlocked, running: ctx ? ctx.state === 'running' : false, loaded: new Set([...buffers.values()].filter((b) => b instanceof AudioBuffer)).size, music: stats.music, missing: stats.missing.slice(), errors: stats.errors.slice(0, 10) }),
|
|
230
|
+
};
|
|
231
|
+
try { window.__homieSound = { get stats() { return { ...api.state(), plays: stats.plays.slice(-100), unlockedAt: stats.unlockedAt }; } }; } catch (e) { /* frozen global */ }
|
|
232
|
+
return api;
|
|
233
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: studio-setup
|
|
3
3
|
description: "Set up a Homie studio (one repository with games/, apps/, music/, videos/ and posts/, and a site with public game rooms on the studio's own Cloudflare) with the AI handling setup and defaults: setup status first (accounts and tools, what each unlocks, the exact fix), choose editable defaults, plan the requested game or app, build it with progress the person can watch, playtest, put it online and list it in the homie.rocks directory. Use when someone asks to set up, create or start a studio or a game studio, asks what they need or whether they are set up, or says \"set up a game studio called X\" (with or without \"and make a multiplayer game\")."
|
|
4
|
-
compatibility: Node 22. Reads, and offers only when a feature needs them, each provider's own tools (Wrangler (Cloudflare, pinned in the studio), the GitHub CLI,
|
|
4
|
+
compatibility: Node 22. Reads, and offers only when a feature needs them, each provider's own tools (Wrangler (Cloudflare, pinned in the studio), the GitHub CLI, official fal and ElevenLabs MCP connections, Cloudflare's official plugin, Stripe for the optional shop, and Ollama for Clef on this computer). The plugin's providers.json lists them.
|
|
5
5
|
metadata:
|
|
6
6
|
providers: cloudflare github elevenlabs fal stripe ollama
|
|
7
7
|
---
|
|
@@ -303,32 +303,16 @@ session: say nothing was sent and carry on. Never make help wait on it, never of
|
|
|
303
303
|
a file, code or anyone's name in it, and a reply address only if they typed it. When they ask to tell Homie
|
|
304
304
|
something themselves, draft it with `offered: false`.
|
|
305
305
|
|
|
306
|
-
###
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
2. `npx --no-install homie-studio setup --via stripe-projects [--with elevenlabs] --dry-run`: what it would do.
|
|
317
|
-
3. Run it without `--dry-run`. It stops at each step that is the person's and says which, as `needs`:
|
|
318
|
-
- `stripe-cli` / `projects-plugin`: run the command it names (the person approves the install);
|
|
319
|
-
- `projects-init`: run `stripe projects init --yes`; Stripe opens its sign-in in their browser. It must be **their
|
|
320
|
-
own** Stripe account (a new free one is fine). If the Stripe CLI on this computer is signed in to some other
|
|
321
|
-
account (a work or client one), stop and ask; never use it;
|
|
322
|
-
- `accept-terms`: ask the person; only after their yes, run it again with `--accept-tos`;
|
|
323
|
-
- `link-cloudflare`: run `stripe projects link cloudflare`; their browser shows Cloudflare's approval page;
|
|
324
|
-
- `paid`: stop. Nothing paid is ever added; this is theirs to decide in Stripe Projects, not yours.
|
|
325
|
-
4. When it answers ok, `npm run deploy` as usual: Wrangler deploys with the Projects token.
|
|
326
|
-
|
|
327
|
-
What it keeps where: the Cloudflare account id goes in `studio.json` (`cloudflare.auth: "stripe-projects"`); the token
|
|
328
|
-
stays in Projects' vault and the git-ignored `.env` it syncs, and the toolkit hands it to Wrangler itself. Follow
|
|
329
|
-
Projects' own rules: never print, read out or `cat` an env value (`stripe projects env --json` lists names only), never
|
|
330
|
-
commit `.env`, `.env.*` or `.projects/vault/` (the command checks `.gitignore`), never hand-edit `.projects/`. If a
|
|
331
|
-
later deploy says the token is not accepted: `stripe projects env --pull` in the studio folder, then deploy again.
|
|
306
|
+
### Official provider connections
|
|
307
|
+
|
|
308
|
+
Read [official integrations](references/OFFICIAL-MEDIA.md). The AI discovers the
|
|
309
|
+
client's official fal, ElevenLabs and Cloudflare tools and verifies an authenticated
|
|
310
|
+
read. Pass `integrations: {fal: "connected", elevenlabs: "unavailable", cloudflare:
|
|
311
|
+
"unknown"}` to `setup_status` using actual session evidence. Omit unknown providers;
|
|
312
|
+
never infer a connection from a local executable or credentials. The card shows the
|
|
313
|
+
shortest browser connection flow for each. Optional media connections never block setup.
|
|
314
|
+
Wrangler login remains the deployment connection. Do not provision provider credentials
|
|
315
|
+
through Stripe Projects; its old Homie setup wrapper has been removed.
|
|
332
316
|
|
|
333
317
|
The live address `deploy` prints is on `workers.dev`, which names the person's Cloudflare account;
|
|
334
318
|
`deploy` keeps it in `.studio/local.json` (git-ignored). Never write it into a committed file. A custom
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Official integrations — checked 2026-10-14
|
|
2
|
+
|
|
3
|
+
Homie owns studio direction, placement, provenance and checks. The AI client owns
|
|
4
|
+
provider connections. Never copy credentials from an old Homie key file or a CLI login.
|
|
5
|
+
|
|
6
|
+
| Provider | Official integration | What the person does |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| fal | [fal plugin and model-generation MCP](https://fal.ai/docs/documentation/setting-up/mcp), `https://mcp.fal.ai/mcp-relay` | Install/connect in the AI client; sign in and approve once in the browser. OAuth, no API key. |
|
|
9
|
+
| ElevenLabs | [elevenlabs/plugin](https://github.com/elevenlabs/plugin), [hosted MCP](https://elevenlabs.io/docs/eleven-agents/operate/hosted-mcp), `https://api.elevenlabs.io/v1/mcp` | Install/connect; sign in and approve workspace access once in the browser. OAuth, no API key. |
|
|
10
|
+
| Cloudflare | [cloudflare/skills plugin](https://github.com/cloudflare/skills), [Code Mode MCP](https://github.com/cloudflare/mcp), `https://mcp.cloudflare.com/mcp` | Install/connect and approve account access in the browser. Wrangler deployment has its own browser login; MCP authorization does not sign Wrangler in. |
|
|
11
|
+
|
|
12
|
+
For Codex CLI the AI can add a remote server (`codex mcp add <name> --url <URL>`)
|
|
13
|
+
and start `codex mcp login <name>`; the person completes the browser page. For
|
|
14
|
+
Claude Code add the HTTP server and authenticate from `/mcp`. In chat clients,
|
|
15
|
+
choose the official connector in Settings/Connectors and Connect. Prefer an already
|
|
16
|
+
installed official plugin. Cloudflare's Codex marketplace is `cloudflare/skills`
|
|
17
|
+
and plugin `cloudflare@cloudflare`; ElevenLabs' marketplace is `elevenlabs/plugin`
|
|
18
|
+
(install from `/plugins`). Start a new session if the client requires it.
|
|
19
|
+
|
|
20
|
+
fal's docs explicitly distinguish the OAuth generation relay from its documentation
|
|
21
|
+
server and key-based terminal clients. The generation relay exposes `search_models`,
|
|
22
|
+
`recommend_model`, `get_model_schema`, `get_pricing`, `run_model`, `submit_job`,
|
|
23
|
+
`check_job`, `get_job_result` and `upload_file`. Discover current schemas in-session.
|
|
24
|
+
Do not install genmedia or make a local key for Homie.
|
|
25
|
+
|
|
26
|
+
ElevenLabs' [October 12 announcement](https://elevenlabs.io/blog/introducing-voice-music-image-and-video-generation-in-the-elevenlabs-mcp)
|
|
27
|
+
confirms hosted music and sound effects through the same OAuth connector. Workspace
|
|
28
|
+
admins control tool access. Its [creative MCP skill](https://github.com/elevenlabs/plugin/blob/main/skills/mcp/creative-studio/SKILL.md)
|
|
29
|
+
documents `creative_get_flow_node_types`, `creative_get_model_guide` and
|
|
30
|
+
`creative_get_flow_run_status`. Discover the connected workspace's current generation
|
|
31
|
+
schemas instead of inventing a music/sound submit tool name. The older
|
|
32
|
+
[local MCP](https://github.com/elevenlabs/elevenlabs-mcp) is deprecated in favor of the
|
|
33
|
+
hosted OAuth server; its key-based setup and tool names are not the remote contract.
|
|
34
|
+
If a future official plugin requires a key, its own AI-client setup owns that flow;
|
|
35
|
+
Homie never handles the value. In this proof session ElevenLabs tools were not exposed,
|
|
36
|
+
so the owner still needs to choose ElevenLabs in Connectors, Connect, and approve the
|
|
37
|
+
workspace in the browser. The previously signed-in CLI is not this MCP connection.
|
|
38
|
+
|
|
39
|
+
Cloudflare's [server catalog](https://github.com/cloudflare/mcp-server-cloudflare)
|
|
40
|
+
provides Observability (`https://observability.mcp.cloudflare.com/mcp`) for logs and
|
|
41
|
+
analytics and Workers Bindings (`https://bindings.mcp.cloudflare.com/mcp`) for D1
|
|
42
|
+
and other bindings. Prefer the main plugin's `search` then `execute` for current API
|
|
43
|
+
operations, including Workers Issues events and D1 inspection, or discover typed tools
|
|
44
|
+
on a product server. Scope reads to the studio's account and Worker. Each additional
|
|
45
|
+
server may require its own consent; do not promise a single shared token. Use official
|
|
46
|
+
MCP reads instead of adding Homie account/log/issue clients.
|
|
47
|
+
|
|
48
|
+
[Wrangler login](https://developers.cloudflare.com/workers/wrangler/commands/general/#login)
|
|
49
|
+
is Cloudflare's own OAuth browser flow. Keep `cloudflare_login` and pinned Wrangler
|
|
50
|
+
for builds/deploys, D1 migrations and R2 upload. Homie does not read Projects env files.
|
|
51
|
+
|
|
52
|
+
## Existing studios
|
|
53
|
+
|
|
54
|
+
Update the toolkit and Homie plugin together; run `homie-studio upgrade --apply`.
|
|
55
|
+
Use the matching toolkit guides if an installed plugin is older. Stop running copied
|
|
56
|
+
Homie provider scripts. The removed generation commands have no compatibility path.
|
|
57
|
+
Connect the official plugins in the AI client and report successful authenticated
|
|
58
|
+
reads to `setup_status.integrations`. A local CLI installation or a key file is not
|
|
59
|
+
connection evidence. Unknown stays unknown; optional connections never block local work.
|
|
60
|
+
|
|
61
|
+
Remove old credential instructions from studio notes and copied skills. Homie upgrade
|
|
62
|
+
replaces its exact retired credential-guidance section in AGENTS.md/CLAUDE.md/README.md;
|
|
63
|
+
review custom instructions separately. Never open old credential files to migrate them.
|
|
64
|
+
The owner can revoke retired credentials in provider settings and remove old files
|
|
65
|
+
through their own credential manager. Existing outputs and receipts remain valid records;
|
|
66
|
+
importing them is local and does not charge again. Stripe's shop integration is separate.
|
package/guides/style/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: style
|
|
3
3
|
description: "A game's look as a set of decisions that fit together — render style, palette, shape language, proportions, materials, light, camera, fonts and effects; then the cast, its library family, scale and phone budgets — each picked automatically from the person's words with a one-line why, which the person can steer (\"warmer\", \"closer\"), lock, or explore on a style board of three directions drawn by the game engine itself (free; a painted mood image only on their own fal account). Locked and pinned decisions keep every asset coherent: change one and the blast radius lists exactly what went stale and what remaking it would cost, and nothing is remade without a yes. Use when someone talks about a game's look or style, \"make it look like…\", \"show me other looks\", \"keep that palette\", \"darker\", \"the art doesn't fit together\", or before making any models or art for a game."
|
|
4
|
-
compatibility: Node 22 and Chrome. The style board is free; a painted mood image uses the creator's own fal account
|
|
4
|
+
compatibility: Node 22 and Chrome. The style board is free; a painted mood image uses the creator's own fal account through the official fal MCP.
|
|
5
5
|
metadata:
|
|
6
6
|
providers: fal
|
|
7
7
|
---
|
|
@@ -57,8 +57,8 @@ before you show them (file_read), and say in one line each what is different.
|
|
|
57
57
|
with `style set` within them.
|
|
58
58
|
- **Lock** when the person says so: `style lock <id> style.palette --words "<what they said>"`, or the whole
|
|
59
59
|
style: `style lock <id> style --words "…"`.
|
|
60
|
-
- **A painted mood image** per direction is optional and PAID (
|
|
61
|
-
account,
|
|
60
|
+
- **A painted mood image** per direction is optional and PAID (quoted through the official MCP, the person's own fal
|
|
61
|
+
account, the official fal MCP): a picture the engine cannot draw is a target, never a promise, and the card says
|
|
62
62
|
so. Offer it only when the engine's swatches leave the person unsure.
|
|
63
63
|
- **Golden images**: once the style is locked, two to six approved pictures (a chosen mood image, a concept they
|
|
64
64
|
loved, their own art) become the references of every generated concept: `style golden <id> add <image>`.
|
|
@@ -103,3 +103,66 @@ would cost. Nothing is remade by itself: remaking is a new, priced job they appr
|
|
|
103
103
|
- Never present a painted mood image as what the game looks like.
|
|
104
104
|
- Never name a living artist in a style ("in the style of …"): describe the look in words.
|
|
105
105
|
- Never "upgrade" a game whose procedural look is its strength to generated meshes without the owner's ask.
|
|
106
|
+
|
|
107
|
+
## Painted style references
|
|
108
|
+
|
|
109
|
+
Use the art skill and official fal MCP for optional painted mood images. Build the prompt
|
|
110
|
+
from the derived style and approved references; record the style revision, cost and rights
|
|
111
|
+
with each result. Keep boards in the codex and mark targets distinct from playable frames.
|
|
112
|
+
|
|
113
|
+
## Official generation workflow
|
|
114
|
+
|
|
115
|
+
The owner's AI uses the provider's official plugin/MCP in its own client. Homie never
|
|
116
|
+
asks for, stores, reads or prints provider credentials. Read the current provider
|
|
117
|
+
skill and discover its available tools and input schemas; host prefixes vary.
|
|
118
|
+
Connection instructions and verified sources: [official integrations](../studio-setup/references/OFFICIAL-MEDIA.md).
|
|
119
|
+
If tools are missing, connect that official plugin once in the client's browser flow.
|
|
120
|
+
Do not fall back to a Homie client, key file, environment key or CLI generation wrapper.
|
|
121
|
+
|
|
122
|
+
Before spending, state the model, count/duration, estimated cost and remaining approved
|
|
123
|
+
budget. Use the owner's existing authorization; ask only if the spend is outside it.
|
|
124
|
+
Record the request ID as soon as accepted. Poll that same request; never resubmit to
|
|
125
|
+
retry a slow job. Unknown cost is unknown, never zero. Provider billing is authoritative;
|
|
126
|
+
Homie's local receipt cannot enforce spending in another client.
|
|
127
|
+
|
|
128
|
+
## Place and prove the result
|
|
129
|
+
|
|
130
|
+
Download the completed tool's output using the AI client's file/download facilities.
|
|
131
|
+
Keep temporary signed URLs out of committed records. Keep the original in the studio's
|
|
132
|
+
art/job folder; put the optimized delivered asset beside its game/app, or in music/ or
|
|
133
|
+
videos/ for a published work. Record a receipt with these fields:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"provider": "fal",
|
|
138
|
+
"tool": "run_model",
|
|
139
|
+
"model": "exact endpoint returned by the provider",
|
|
140
|
+
"requestId": "actual request ID",
|
|
141
|
+
"prompt": "the actual prompt, including style constraints",
|
|
142
|
+
"styleRevision": "revision from the studio's style",
|
|
143
|
+
"rights": {"source": "provider/model terms URL", "checkedAt": "YYYY-MM-DD", "commercial": false, "note": "record the actual plan and permitted use"},
|
|
144
|
+
"cost": {"status": "unknown"}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For ElevenLabs use its actual discovered tool and model, and its flow/session or request
|
|
149
|
+
identifier. A measured cost uses `{"status":"actual","amount":1,"unit":"credits"}`;
|
|
150
|
+
a quote uses `estimate`. Never claim commercial rights from the fact that generation
|
|
151
|
+
succeeded. Read the provider and model terms for the account's plan; record attribution,
|
|
152
|
+
reference-image permissions and any restrictions. Mark test fixtures `"fixture": true`.
|
|
153
|
+
|
|
154
|
+
Wire the delivered path into the scene or manifest, then run:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
npx --no-install homie-studio media import --file <downloaded-file> --receipt <receipt.json> --out games/<id>/assets/<name> --used-by games/<id>/view.ts
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This copies bytes, verifies a static reference, hashes the asset and records allowlisted
|
|
161
|
+
provenance in `media/provenance.json`. Reimporting the same output is idempotent; it
|
|
162
|
+
never generates or charges. Include each generation step's receipt for derived assets.
|
|
163
|
+
Run the scene and look/listen: a file and static reference are not proof that it renders
|
|
164
|
+
or plays. Report the placed path, usage evidence, rights and actual/estimated cost.
|
|
165
|
+
|
|
166
|
+
The toolkit’s `lib/media-workflows.mjs` declares this skill’s verified tool references.
|
|
167
|
+
Validate them against the host’s current tool inventory before execution; tool discovery
|
|
168
|
+
is required for generation capabilities that have no stable documented name.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video
|
|
3
|
+
description: "Capture and edit studio videos locally, and generate requested shots through the official fal MCP."
|
|
4
|
+
compatibility: Node 22 and the official fal plugin in the AI client. Local media processing uses ffmpeg and Chrome where needed.
|
|
5
|
+
metadata:
|
|
6
|
+
providers: fal
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Video from the studio
|
|
10
|
+
|
|
11
|
+
Start with the actual running game/app. Plan a short sequence that shows its central
|
|
12
|
+
action, one progression moment and a truthful call to action. Capture gameplay locally;
|
|
13
|
+
do not pass generated footage off as gameplay. Derive generated shots from the locked
|
|
14
|
+
style and approved reference frames, preserving composition and character continuity.
|
|
15
|
+
|
|
16
|
+
For requested generated footage, use fal's `recommend_model`/`search_models`,
|
|
17
|
+
`get_model_schema`, `get_pricing`, then `submit_job`, `check_job` and `get_job_result`.
|
|
18
|
+
Specify count, duration, resolution and whether audio is needed. Inspect a first shot
|
|
19
|
+
before chaining more. The provider plugin owns uploads and generation.
|
|
20
|
+
|
|
21
|
+
Local `scripts/video.mjs` keeps capture/record/trailer, beat grids, slices, timing checks,
|
|
22
|
+
editing and delivery. Read `references/RECORD.md` for page capture. Check contact sheets,
|
|
23
|
+
then watch with audio at speed; verify sync, text legibility, transitions and both phone
|
|
24
|
+
and landscape framing. Keep masters in `videos/<slug>/`, export compressed deliverables
|
|
25
|
+
and register them in `videos/manifest.json`. Check that the site page plays the delivered
|
|
26
|
+
file and credits its inputs. Deployment is a separate requested action.
|
|
27
|
+
|
|
28
|
+
## Official generation workflow
|
|
29
|
+
|
|
30
|
+
The owner's AI uses the provider's official plugin/MCP in its own client. Homie never
|
|
31
|
+
asks for, stores, reads or prints provider credentials. Read the current provider
|
|
32
|
+
skill and discover its available tools and input schemas; host prefixes vary.
|
|
33
|
+
Connection instructions and verified sources: [official integrations](../studio-setup/references/OFFICIAL-MEDIA.md).
|
|
34
|
+
If tools are missing, connect that official plugin once in the client's browser flow.
|
|
35
|
+
Do not fall back to a Homie client, key file, environment key or CLI generation wrapper.
|
|
36
|
+
|
|
37
|
+
Before spending, state the model, count/duration, estimated cost and remaining approved
|
|
38
|
+
budget. Use the owner's existing authorization; ask only if the spend is outside it.
|
|
39
|
+
Record the request ID as soon as accepted. Poll that same request; never resubmit to
|
|
40
|
+
retry a slow job. Unknown cost is unknown, never zero. Provider billing is authoritative;
|
|
41
|
+
Homie's local receipt cannot enforce spending in another client.
|
|
42
|
+
|
|
43
|
+
## Place and prove the result
|
|
44
|
+
|
|
45
|
+
Download the completed tool's output using the AI client's file/download facilities.
|
|
46
|
+
Keep temporary signed URLs out of committed records. Keep the original in the studio's
|
|
47
|
+
art/job folder; put the optimized delivered asset beside its game/app, or in music/ or
|
|
48
|
+
videos/ for a published work. Record a receipt with these fields:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"provider": "fal",
|
|
53
|
+
"tool": "run_model",
|
|
54
|
+
"model": "exact endpoint returned by the provider",
|
|
55
|
+
"requestId": "actual request ID",
|
|
56
|
+
"prompt": "the actual prompt, including style constraints",
|
|
57
|
+
"styleRevision": "revision from the studio's style",
|
|
58
|
+
"rights": {"source": "provider/model terms URL", "checkedAt": "YYYY-MM-DD", "commercial": false, "note": "record the actual plan and permitted use"},
|
|
59
|
+
"cost": {"status": "unknown"}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For ElevenLabs use its actual discovered tool and model, and its flow/session or request
|
|
64
|
+
identifier. A measured cost uses `{"status":"actual","amount":1,"unit":"credits"}`;
|
|
65
|
+
a quote uses `estimate`. Never claim commercial rights from the fact that generation
|
|
66
|
+
succeeded. Read the provider and model terms for the account's plan; record attribution,
|
|
67
|
+
reference-image permissions and any restrictions. Mark test fixtures `"fixture": true`.
|
|
68
|
+
|
|
69
|
+
Wire the delivered path into the scene or manifest, then run:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
npx --no-install homie-studio media import --file <downloaded-file> --receipt <receipt.json> --out games/<id>/assets/<name> --used-by games/<id>/view.ts
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This copies bytes, verifies a static reference, hashes the asset and records allowlisted
|
|
76
|
+
provenance in `media/provenance.json`. Reimporting the same output is idempotent; it
|
|
77
|
+
never generates or charges. Include each generation step's receipt for derived assets.
|
|
78
|
+
Run the scene and look/listen: a file and static reference are not proof that it renders
|
|
79
|
+
or plays. Report the placed path, usage evidence, rights and actual/estimated cost.
|
|
80
|
+
|
|
81
|
+
The toolkit’s `lib/media-workflows.mjs` declares this skill’s verified tool references.
|
|
82
|
+
Validate them against the host’s current tool inventory before execution; tool discovery
|
|
83
|
+
is required for generation capabilities that have no stable documented name.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Delivering a video, and the traps on the way
|
|
2
|
+
|
|
3
|
+
## Check the file, not the command
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
node <skill folder>/scripts/qa.mjs videos/<slug>/<slug>.mp4 [--kind trailer|clip|music-video]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It decodes every frame and reports: decode errors, H.264 and yuv420p (what every phone plays), even
|
|
10
|
+
frame sizes, BT.709 colour tags, faststart (the index at the front, so a phone starts playing before
|
|
11
|
+
it has downloaded everything), black stretches, frozen stretches (held frames from a capture that
|
|
12
|
+
could not keep up), silent stretches, loudness (about -14 LUFS for the web) and true peak (under -1
|
|
13
|
+
dBTP), and the 25 MiB a studio site serves itself. Then open the contact sheet and watch it once,
|
|
14
|
+
start to end: QA is not a review.
|
|
15
|
+
|
|
16
|
+
## Shapes
|
|
17
|
+
|
|
18
|
+
- One 1920x1080 master and its contact sheet is the default delivery.
|
|
19
|
+
- A 9:16 cut (1080x1920) when a vertical feed is named: the whole game frame over a blurred fill, or
|
|
20
|
+
a crop that follows the action; never a fixed centre crop that hides it.
|
|
21
|
+
- Social feeds autoplay muted: burn the key words into the picture (the film draw-over's kinetic type
|
|
22
|
+
does it) or ship `captions.vtt`, which the studio's video page loads.
|
|
23
|
+
|
|
24
|
+
- A captured frame is a JPEG: full range, BT.601. `format=yuv420p` and `-colorspace bt709` change
|
|
25
|
+
neither (the file reads back `yuvj420p`, primaries and transfer unset, and phones show it washed
|
|
26
|
+
out or crushed). The skill's encodes end in `scale=out_range=tv:out_color_matrix=bt709,
|
|
27
|
+
format=yuv420p,setparams=…` with `-color_range tv`: converted, then tagged. A file made another
|
|
28
|
+
way: check it with `qa.mjs`.
|
|
29
|
+
|
|
30
|
+
## Capture
|
|
31
|
+
|
|
32
|
+
- A game too heavy to paint 30 frames a second is filmed frame by frame instead (`trailer`, or
|
|
33
|
+
`record-fixed.mjs` directly): the page's clock is stepped 1/30 s a frame, so nothing is held.
|
|
34
|
+
Its sound is rebuilt from the game's sound log, not recorded (`references/TRAILER.md`, section 0).
|
|
35
|
+
|
|
36
|
+
- Wait about three seconds after the page loads before the first frame: fonts, shaders and the first
|
|
37
|
+
round settle.
|
|
38
|
+
- A 3D game in headless Chrome paints 12 to 30 fps depending on the machine; the capture records the
|
|
39
|
+
real presentation time of every frame and holds a frame the page did not paint. Many held frames
|
|
40
|
+
(`heldFrames` in `capture.json`) look like stutter: capture again with fewer programs running, or at
|
|
41
|
+
a smaller size.
|
|
42
|
+
- Headless Chrome needs the GPU flags; a software renderer makes a game look frozen.
|
|
43
|
+
- If the browser path in the environment points at another Chromium-based app (an Electron shell, a
|
|
44
|
+
full-screen app browser), capture fails with "Target.createTarget: Not supported" or opens no page: point it at
|
|
45
|
+
Chrome or Chrome for Testing.
|
|
46
|
+
- Keep the raw frames until the edit is locked, then delete them (a minute of 1080p frames is gigabytes;
|
|
47
|
+
check free disk space before a capture and keep at least 10 GB free).
|
|
48
|
+
|
|
49
|
+
## Honesty
|
|
50
|
+
|
|
51
|
+
- **Interpolated frames are fabrication.** Frame interpolation (`minterpolate`, "AI smoothing") invents
|
|
52
|
+
gameplay that never happened. Recapture instead.
|
|
53
|
+
- Rebuild a constant frame rate from the saved frames and their real timestamps, never from a rounded
|
|
54
|
+
frame clock.
|
|
55
|
+
- Mask anything private in a capture before it is public: addresses, ports, a local development URL,
|
|
56
|
+
location text, and QR codes. Decode every QR code in a public video (a QR that points at a local
|
|
57
|
+
address or a private room is a leak and a dead end); re-cutting stills from the footage brings back
|
|
58
|
+
anything masked only in the edit.
|
|
59
|
+
- Show real elapsed times and real results; no claim the game does not back up.
|
|
60
|
+
- Bots are bots: never imply a crowd that was not there.
|
|
61
|
+
|
|
62
|
+
## Generated footage
|
|
63
|
+
|
|
64
|
+
- Model strengths, measured: Veo for acting and hands, Kling for locked-off shots and a face that
|
|
65
|
+
stays the same person; Seedance refuses stills that look like real people; some models smear faces.
|
|
66
|
+
- **Continuity is the first thing a viewer notices.** Write the cast and where everyone is into every
|
|
67
|
+
prompt, and check every face on every re-roll: takes silently recast people.
|
|
68
|
+
- A punch-in over about 1.3x on a 1080p base goes soft; generate at the framing you need.
|
|
69
|
+
- Real UI inside generated footage (a phone screen, a monitor): have an image-edit model paint the
|
|
70
|
+
screen pure green, key it, find the screen's four corners, and warp a frame-exact render of the real
|
|
71
|
+
UI onto it; matte to the tracked corners because some lights key as green too.
|
|
72
|
+
- A generated room tone can carry a steady hum: check the spectrum (the `sound` skill's `analyze`
|
|
73
|
+
lists narrow tones).
|