@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.
- package/CHANGELOG.md +45 -0
- package/README.md +10 -29
- package/apps/APPS.md +1 -1
- package/bin/homie-studio.mjs +28 -30
- package/docs/mcp-events.md +68 -0
- package/guides/app/SKILL.md +25 -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/office/SKILL.md +24 -0
- package/guides/parts/SKILL.md +1 -1
- package/guides/servers/SKILL.md +24 -0
- 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/functions-build.mjs +8 -1
- 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 +87 -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/event-resources.mjs +12 -3
- package/worker/event-webhooks.mjs +14 -9
- package/worker/functions.mjs +11 -6
- package/worker/index.mjs +5 -1
- package/worker/mcp-event-streams.mjs +58 -0
- package/worker/mcp-events-wire.mjs +41 -0
- package/worker/mcp-events.mjs +132 -0
- package/worker/mcp.mjs +4 -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
package/guides/sound/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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.
|