@homie-rocks/studio 0.51.3 → 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +10 -29
  3. package/apps/APPS.md +1 -1
  4. package/bin/homie-studio.mjs +28 -30
  5. package/docs/mcp-events.md +68 -0
  6. package/guides/app/SKILL.md +25 -1
  7. package/guides/art/SKILL.md +68 -140
  8. package/guides/art/scripts/art.mjs +6 -79
  9. package/guides/models/SKILL.md +73 -177
  10. package/guides/models/references/RIGHTS.md +1 -1
  11. package/guides/models/scripts/models.mjs +7 -288
  12. package/guides/music/SKILL.md +74 -183
  13. package/guides/music/references/COMPOSITION.md +21 -26
  14. package/guides/music/references/GAME-AUDIO.md +3 -3
  15. package/guides/music/references/RIGHTS.md +12 -15
  16. package/guides/music/scripts/music.mjs +11 -202
  17. package/guides/office/SKILL.md +24 -0
  18. package/guides/parts/SKILL.md +1 -1
  19. package/guides/servers/SKILL.md +24 -0
  20. package/guides/shop/SKILL.md +68 -77
  21. package/guides/sound/SKILL.md +70 -1
  22. package/guides/sound/scripts/player/sound.js +233 -0
  23. package/guides/studio-setup/SKILL.md +11 -27
  24. package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
  25. package/guides/style/SKILL.md +66 -3
  26. package/guides/video/SKILL.md +83 -0
  27. package/guides/video/references/DELIVERY.md +73 -0
  28. package/guides/video/references/HONESTY.md +40 -0
  29. package/guides/video/references/METHOD.md +144 -0
  30. package/guides/video/references/RECORD.md +137 -0
  31. package/guides/video/references/TRAILER.md +154 -0
  32. package/guides/video/references/examples/site-walkthrough.json +14 -0
  33. package/guides/video/references/examples/studio-play-phone.json +15 -0
  34. package/guides/video/references/examples/studio-play.json +17 -0
  35. package/guides/video/scripts/capture-game.mjs +175 -0
  36. package/guides/video/scripts/clock.js +136 -0
  37. package/guides/video/scripts/drawover/film.js +58 -0
  38. package/guides/video/scripts/drawover/index.html +9 -0
  39. package/guides/video/scripts/drawover/look.js +115 -0
  40. package/guides/video/scripts/drawover/shots.json +12 -0
  41. package/guides/video/scripts/lib/browser.mjs +87 -0
  42. package/guides/video/scripts/lib/frames.mjs +170 -0
  43. package/guides/video/scripts/lib/remix.mjs +159 -0
  44. package/guides/video/scripts/qa.mjs +127 -0
  45. package/guides/video/scripts/record-fixed.mjs +403 -0
  46. package/guides/video/scripts/record-page.mjs +477 -0
  47. package/guides/video/scripts/tap.js +108 -0
  48. package/guides/video/scripts/video.mjs +888 -0
  49. package/lib/build.mjs +12 -9
  50. package/lib/cloudflare.mjs +6 -11
  51. package/lib/dev.mjs +1 -2
  52. package/lib/doctor.mjs +10 -54
  53. package/lib/functions-build.mjs +8 -1
  54. package/lib/mcp-tools.mjs +42 -21
  55. package/lib/mcp.mjs +1 -1
  56. package/lib/media-import.mjs +41 -0
  57. package/lib/media-workflows.mjs +13 -0
  58. package/lib/official-media.mjs +15 -0
  59. package/lib/rules-build.mjs +13 -5
  60. package/lib/scaffold.mjs +2 -2
  61. package/lib/shop-links.mjs +54 -18
  62. package/lib/shop-listen.mjs +43 -0
  63. package/lib/shop-local.mjs +1 -1
  64. package/lib/shop-mcp.mjs +58 -0
  65. package/lib/stripe-connect.mjs +36 -15
  66. package/lib/template-history.json +87 -0
  67. package/lib/upgrade-media.mjs +13 -0
  68. package/lib/upgrade.mjs +10 -0
  69. package/package.json +1 -1
  70. package/parts/PARTS.md +1 -1
  71. package/rules/view.ts +4 -0
  72. package/shop/SHOP.md +66 -34
  73. package/worker/build-id.mjs +1 -1
  74. package/worker/event-resources.mjs +12 -3
  75. package/worker/event-webhooks.mjs +14 -9
  76. package/worker/functions.mjs +11 -6
  77. package/worker/index.mjs +5 -1
  78. package/worker/mcp-event-streams.mjs +58 -0
  79. package/worker/mcp-events-wire.mjs +41 -0
  80. package/worker/mcp-events.mjs +132 -0
  81. package/worker/mcp.mjs +4 -1
  82. package/worker/purchase-reconciliation.mjs +16 -1
  83. package/worker/shop-subscriptions.mjs +2 -0
  84. package/worker/shop.mjs +7 -5
  85. package/worker/stripe-ownership.mjs +7 -0
  86. package/worker/version.mjs +1 -1
  87. package/guides/models/references/models.json +0 -175
  88. package/guides/music/scripts/lib/eleven.mjs +0 -260
  89. package/lib/projects-env.mjs +0 -68
  90. package/lib/projects.mjs +0 -311
  91. package/lib/provider-key.mjs +0 -21
@@ -1,11 +1,14 @@
1
1
  ---
2
2
  name: sound
3
3
  description: Make a game's sound effects and synthesized music on the creator's own computer, free, with no provider or account — a set of effects (jump, coin, hit, explosion, win, lose, UI) rendered from presets, a short synthesized theme or score from chords and patterns with stems and seamless section loops, a mix of any audio files and expressions — measured for loudness, clipping, late starts and what a phone speaker loses, then wired into the game with a small player that starts on the first touch and switches music on bar lines. Use when someone asks for sound effects, SFX, game audio, a chiptune or synth theme, background music without a paid service, music that changes with the game, or says a game is silent.
4
+ compatibility: Node 22 and ffmpeg for local synthesis; official ElevenLabs MCP for requested generated sound.
5
+ metadata:
6
+ providers: elevenlabs
4
7
  ---
5
8
 
6
9
  # Sound for a studio's game
7
10
 
8
- Everything here runs on this computer with Node 22 and ffmpeg: nothing is sent anywhere and nothing
11
+ Local synthesis runs on this computer with Node 22 and ffmpeg: nothing is sent anywhere and nothing
9
12
  costs money. Sung songs and produced music from a model are the `music` skill (ElevenLabs, paid);
10
13
  this skill is effects and synthesized music. They share the studio's `music/` folder and manifest, so
11
14
  a synthesized theme gets a song page just like a rendered one.
@@ -154,3 +157,69 @@ numbers, the warnings you fixed and any you did not, and that it cost nothing.
154
157
  - Never use a mp3 for an effect or a loop: encoders pad the start with silence (a late hit) and the
155
158
  ends (a gap at the loop). WAV or Ogg.
156
159
  - Never imitate a real song, artist or brand jingle.
160
+
161
+ ## Generated sound on request
162
+
163
+ Use the official ElevenLabs MCP for requested generated effects. Discover its current
164
+ sound capabilities and schema through the official plugin; never assume a tool exists.
165
+ If the hosted connection lacks sound generation, say so; keep local synthesis available
166
+ as Homie-owned, free functionality. Specify duration, loopability, envelope and material
167
+ from the scene. Audition on phone speakers, remove clicks, and trigger it only on the
168
+ intended game event after the browser audio unlock gesture.
169
+
170
+ ## Official generation workflow
171
+
172
+ The owner's AI uses the provider's official plugin/MCP in its own client. Homie never
173
+ asks for, stores, reads or prints provider credentials. Read the current provider
174
+ skill and discover its available tools and input schemas; host prefixes vary.
175
+ Connection instructions and verified sources: [official integrations](../studio-setup/references/OFFICIAL-MEDIA.md).
176
+ If tools are missing, connect that official plugin once in the client's browser flow.
177
+ Do not fall back to a Homie client, key file, environment key or CLI generation wrapper.
178
+
179
+ Before spending, state the model, count/duration, estimated cost and remaining approved
180
+ budget. Use the owner's existing authorization; ask only if the spend is outside it.
181
+ Record the request ID as soon as accepted. Poll that same request; never resubmit to
182
+ retry a slow job. Unknown cost is unknown, never zero. Provider billing is authoritative;
183
+ Homie's local receipt cannot enforce spending in another client.
184
+
185
+ ## Place and prove the result
186
+
187
+ Download the completed tool's output using the AI client's file/download facilities.
188
+ Keep temporary signed URLs out of committed records. Keep the original in the studio's
189
+ art/job folder; put the optimized delivered asset beside its game/app, or in music/ or
190
+ videos/ for a published work. Record a receipt with these fields:
191
+
192
+ ```json
193
+ {
194
+ "provider": "elevenlabs",
195
+ "tool": "actual discovered tool",
196
+ "model": "exact endpoint returned by the provider",
197
+ "requestId": "actual request ID",
198
+ "prompt": "the actual prompt, including style constraints",
199
+ "styleRevision": "revision from the studio's style",
200
+ "rights": {"source": "provider/model terms URL", "checkedAt": "YYYY-MM-DD", "commercial": false, "note": "record the actual plan and permitted use"},
201
+ "cost": {"status": "unknown"}
202
+ }
203
+ ```
204
+
205
+ For ElevenLabs use its actual discovered tool and model, and its flow/session or request
206
+ identifier. A measured cost uses `{"status":"actual","amount":1,"unit":"credits"}`;
207
+ a quote uses `estimate`. Never claim commercial rights from the fact that generation
208
+ succeeded. Read the provider and model terms for the account's plan; record attribution,
209
+ reference-image permissions and any restrictions. Mark test fixtures `"fixture": true`.
210
+
211
+ Wire the delivered path into the scene or manifest, then run:
212
+
213
+ ```sh
214
+ npx --no-install homie-studio media import --file <downloaded-file> --receipt <receipt.json> --out games/<id>/assets/<name> --used-by games/<id>/view.ts
215
+ ```
216
+
217
+ This copies bytes, verifies a static reference, hashes the asset and records allowlisted
218
+ provenance in `media/provenance.json`. Reimporting the same output is idempotent; it
219
+ never generates or charges. Include each generation step's receipt for derived assets.
220
+ Run the scene and look/listen: a file and static reference are not proof that it renders
221
+ or plays. Report the placed path, usage evidence, rights and actual/estimated cost.
222
+
223
+ The toolkit’s `lib/media-workflows.mjs` declares this skill’s verified tool references.
224
+ Validate them against the host’s current tool inventory before execution; tool discovery
225
+ is required for generation capabilities that have no stable documented name.
@@ -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, the ElevenLabs CLI, a fal key, Ollama for Clef on this computer, and as an option the stripe CLI's Stripe Projects). The plugin's providers.json lists them.
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
- ### Another way: Cloudflare (and ElevenLabs) through Stripe Projects
307
-
308
- `npx wrangler login` stays the way. Offer this only when the person has **no Cloudflare account** (and wants songs but
309
- has no ElevenLabs either), and would rather have both made for them in one go: Stripe Projects
310
- (docs.stripe.com/projects), Stripe's CLI plugin, makes or links provider accounts on the person's own Stripe sign-in
311
- and hands the credentials to the studio folder. It is a prototype in this version (`setup --via stripe-projects`).
312
-
313
- 1. Say it in two lines: Stripe signs them in; Cloudflare makes a free account for their Stripe email (or, if they
314
- have one, shows its own approval page once); on their own accounts; they accept Cloudflare's terms
315
- (and ElevenLabs': whose free plan has **no commercial licence**, so songs made on it are not for sale).
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.
@@ -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 (FAL_KEY), through the models skill.
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 (about US$0.035 each, the person's own fal
61
- account, `models.mjs mood`): a picture the engine cannot draw is a target, never a promise, and the card says
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.