@homie-rocks/studio 0.51.4 → 0.51.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -29
- package/apps/APPS.md +1 -1
- package/bin/homie-studio.mjs +28 -30
- package/guides/app/SKILL.md +1 -1
- package/guides/art/SKILL.md +68 -140
- package/guides/art/scripts/art.mjs +6 -79
- package/guides/models/SKILL.md +73 -177
- package/guides/models/references/RIGHTS.md +1 -1
- package/guides/models/scripts/models.mjs +7 -288
- package/guides/music/SKILL.md +74 -183
- package/guides/music/references/COMPOSITION.md +21 -26
- package/guides/music/references/GAME-AUDIO.md +3 -3
- package/guides/music/references/RIGHTS.md +12 -15
- package/guides/music/scripts/music.mjs +11 -202
- package/guides/parts/SKILL.md +1 -1
- package/guides/shop/SKILL.md +68 -77
- package/guides/sound/SKILL.md +70 -1
- package/guides/sound/scripts/player/sound.js +233 -0
- package/guides/studio-setup/SKILL.md +11 -27
- package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
- package/guides/style/SKILL.md +66 -3
- package/guides/video/SKILL.md +83 -0
- package/guides/video/references/DELIVERY.md +73 -0
- package/guides/video/references/HONESTY.md +40 -0
- package/guides/video/references/METHOD.md +144 -0
- package/guides/video/references/RECORD.md +137 -0
- package/guides/video/references/TRAILER.md +154 -0
- package/guides/video/references/examples/site-walkthrough.json +14 -0
- package/guides/video/references/examples/studio-play-phone.json +15 -0
- package/guides/video/references/examples/studio-play.json +17 -0
- package/guides/video/scripts/capture-game.mjs +175 -0
- package/guides/video/scripts/clock.js +136 -0
- package/guides/video/scripts/drawover/film.js +58 -0
- package/guides/video/scripts/drawover/index.html +9 -0
- package/guides/video/scripts/drawover/look.js +115 -0
- package/guides/video/scripts/drawover/shots.json +12 -0
- package/guides/video/scripts/lib/browser.mjs +87 -0
- package/guides/video/scripts/lib/frames.mjs +170 -0
- package/guides/video/scripts/lib/remix.mjs +159 -0
- package/guides/video/scripts/qa.mjs +127 -0
- package/guides/video/scripts/record-fixed.mjs +403 -0
- package/guides/video/scripts/record-page.mjs +477 -0
- package/guides/video/scripts/tap.js +108 -0
- package/guides/video/scripts/video.mjs +888 -0
- package/lib/build.mjs +12 -9
- package/lib/cloudflare.mjs +6 -11
- package/lib/dev.mjs +1 -2
- package/lib/doctor.mjs +10 -54
- package/lib/mcp-tools.mjs +42 -21
- package/lib/mcp.mjs +1 -1
- package/lib/media-import.mjs +41 -0
- package/lib/media-workflows.mjs +13 -0
- package/lib/official-media.mjs +15 -0
- package/lib/rules-build.mjs +13 -5
- package/lib/scaffold.mjs +2 -2
- package/lib/shop-links.mjs +54 -18
- package/lib/shop-listen.mjs +43 -0
- package/lib/shop-local.mjs +1 -1
- package/lib/shop-mcp.mjs +58 -0
- package/lib/stripe-connect.mjs +36 -15
- package/lib/template-history.json +45 -0
- package/lib/upgrade-media.mjs +13 -0
- package/lib/upgrade.mjs +10 -0
- package/package.json +1 -1
- package/parts/PARTS.md +1 -1
- package/rules/view.ts +4 -0
- package/shop/SHOP.md +66 -34
- package/worker/build-id.mjs +1 -1
- package/worker/index.mjs +5 -1
- package/worker/purchase-reconciliation.mjs +16 -1
- package/worker/shop-subscriptions.mjs +2 -0
- package/worker/shop.mjs +7 -5
- package/worker/stripe-ownership.mjs +7 -0
- package/worker/version.mjs +1 -1
- package/guides/models/references/models.json +0 -175
- package/guides/music/scripts/lib/eleven.mjs +0 -260
- package/lib/projects-env.mjs +0 -68
- package/lib/projects.mjs +0 -311
- package/lib/provider-key.mjs +0 -21
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Honesty rules
|
|
2
|
+
|
|
3
|
+
## Gameplay is captured, never made
|
|
4
|
+
|
|
5
|
+
- Anything presented as the game is recorded from the game running (`capture`), in a live room,
|
|
6
|
+
with nothing pressed for the camera and nothing drawn over the play.
|
|
7
|
+
- Generated footage (a model's clip) is never cut into a trailer as if it were gameplay. A
|
|
8
|
+
trailer may open on a generated or drawn title sequence; the moment gameplay starts, it is the
|
|
9
|
+
game.
|
|
10
|
+
- No speed-ups, no reversed footage, no frame interpolation passed off as the game's frame rate.
|
|
11
|
+
Held frames are counted in `capture.json`; if the game ran slowly, say so or capture again.
|
|
12
|
+
- Bots are bots. The game's own bots fill empty seats; never caption or voice them as people,
|
|
13
|
+
and never claim player counts the room did not have.
|
|
14
|
+
- A page recording (`record`) is pressed by a script, in real time: it says so, and the cursor and
|
|
15
|
+
press rings it draws are the recorder's, not the page's. Its frames are the page's own, held when the
|
|
16
|
+
page did not repaint, never interpolated; on a software renderer or under 20 fps it is a recording
|
|
17
|
+
of a slow computer, and is not shown as the game's speed.
|
|
18
|
+
- A studio's score or player numbers shown on screen are what the game drew during the capture.
|
|
19
|
+
|
|
20
|
+
## No real people, no real brands
|
|
21
|
+
|
|
22
|
+
- No likeness, voice or name of a real person (celebrities, public figures, private people) in a
|
|
23
|
+
prompt, a reference image, a voice or the type. Invent the cast.
|
|
24
|
+
- No real logos, products, brands or copyrighted characters; no screenshots of other people's
|
|
25
|
+
posts, videos or games; no "in the style of" a living artist.
|
|
26
|
+
- References a person supplies (a photo, a sketch) are theirs to give: ask when unsure.
|
|
27
|
+
|
|
28
|
+
## Money
|
|
29
|
+
|
|
30
|
+
- A budget before the first paid call; every call priced from the provider's own pricing and
|
|
31
|
+
checked against the approved cap by the owner’s AI before submitting. Record the accepted
|
|
32
|
+
request ID and poll that job instead of submitting again. Record actual cost, estimate or
|
|
33
|
+
unknown explicitly in provenance. Homie does not enforce a provider-side spending cap.
|
|
34
|
+
|
|
35
|
+
## Rights of what is made
|
|
36
|
+
|
|
37
|
+
- Music: the plan at render time decides (the `music` skill's `references/RIGHTS.md`).
|
|
38
|
+
- fal: outputs belong to the person under fal's terms, but each model has its own licence and
|
|
39
|
+
content policy. Read the model page's licence line before commercial use and put the models in
|
|
40
|
+
the video's credits (`add` does).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# A music video (or a cutscene) from generated footage
|
|
2
|
+
|
|
3
|
+
The method: the generated clips are the motion; a hand-built JavaScript drawing is the picture;
|
|
4
|
+
the words are a main event, not subtitles; everything sits on the song's beat grid, and nothing
|
|
5
|
+
ships until a frame-by-frame check says the mouths, the words and the hits land.
|
|
6
|
+
|
|
7
|
+
## 0. The song and the grid
|
|
8
|
+
|
|
9
|
+
The song comes from the `music` skill. Use word timestamps if the official MCP returns them,
|
|
10
|
+
or obtain timings through an available official transcription capability and check them by ear.
|
|
11
|
+
The local grid needs timing metadata; generation alone does not guarantee it. Then:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
node <video.mjs> grid <slug> --song <music slug>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`videos/<slug>/grid.json`: the tempo, every beat, every bar line, the sections of the song's
|
|
18
|
+
plan with their start times, and every sung word (`w`, start `s`, end `e`, in seconds of the
|
|
19
|
+
song). The model's own word stamps ran 50 to 80 ms after what is heard on some words, so `grid`
|
|
20
|
+
moves each word to the vocal onset nearest its stamp (the original stays in `stamp`), and lyric
|
|
21
|
+
type then pops on the consonant. Plan everything in these numbers.
|
|
22
|
+
|
|
23
|
+
## 1. A style sheet
|
|
24
|
+
|
|
25
|
+
`videos/<slug>/STYLE.md`, one page, before any image is made:
|
|
26
|
+
|
|
27
|
+
- **The risk.** One real aesthetic decision the video is built around (a limited palette with a
|
|
28
|
+
meaning, a print process, a camera rule). Not "cinematic", not generic AI gloss.
|
|
29
|
+
- **Palette** as hex values and what each colour stands for; **line** (weight, texture); **type**
|
|
30
|
+
(two or three faces, each with a job: the big hits, the quiet lines, the machine voice).
|
|
31
|
+
- **What never appears**: real people, logos, brands, copyrighted characters, text in the
|
|
32
|
+
generated images (models misspell it; type is drawn later).
|
|
33
|
+
|
|
34
|
+
## 2. Character and set sheets
|
|
35
|
+
|
|
36
|
+
One image call per sheet through the official fal MCP: discover a model, inspect its schema
|
|
37
|
+
and price the requested size before calling `run_model`. A character sheet is one invented person or creature on a plain ground: front,
|
|
38
|
+
three-quarter and profile, their palette, one prop that tells who they are. A set sheet is a
|
|
39
|
+
place, empty, lit the way the style says. Look at each; redo the ones that break the style before
|
|
40
|
+
anything moves. Keep the prompts in `videos/<slug>/prompts/`.
|
|
41
|
+
|
|
42
|
+
## 3. The shot list on the grid
|
|
43
|
+
|
|
44
|
+
`videos/<slug>/SHOTS.md`: one row per shot with its bar range and times from `grid.json`, the
|
|
45
|
+
lyric it carries, what is seen, which sheets it uses, and whether it is a **performance** (a
|
|
46
|
+
character sings: needs the song as an audio reference) or a **picture** shot (motion only). Cuts
|
|
47
|
+
land on bar lines (or beats in a fast section). Characters on one side of the frame, words on the
|
|
48
|
+
other. Price the whole list before generating anything: shots x price, against the budget.
|
|
49
|
+
|
|
50
|
+
## 4. Bases
|
|
51
|
+
|
|
52
|
+
For each shot, discover a reference-to-video model through the official fal MCP and read
|
|
53
|
+
`get_model_schema`. Use `upload_file` for local references and pass its returned URLs in the
|
|
54
|
+
fields supported by that model. Never pass Homie's former `@file:` shorthand.
|
|
55
|
+
|
|
56
|
+
A performance prompt can ask for the singer from the character reference to sing the supplied
|
|
57
|
+
audio, with a still camera, closed mouth during silence, no text and one continuous shot.
|
|
58
|
+
Reference syntax, audio support and lip sync controls depend on the discovered model.
|
|
59
|
+
|
|
60
|
+
- Slice the exact shot audio locally:
|
|
61
|
+
`node <video.mjs> slice <slug> --from <shot start> --to <shot end> --out work/audio/<shot>.wav`.
|
|
62
|
+
- Choose an explicit supported duration covering the shot and quote it before submitting.
|
|
63
|
+
- Price the supported resolutions. Use a lower resolution under a draw-over when it survives
|
|
64
|
+
review; use more detail where the base itself is seen.
|
|
65
|
+
- Picture shots need image-to-video from a keyframe; disable generated audio if supported.
|
|
66
|
+
- Review the keyframe before paying for its clip. Save the accepted job ID and retrieve that
|
|
67
|
+
job's output without submitting it twice. Import outputs with provenance and their consumer.
|
|
68
|
+
|
|
69
|
+
## 5. The sync check on every base
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
node <video.mjs> lag <slug> --base work/bases/s03.mp4 --ref work/audio/s03.wav
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A reference-to-video model that sings the reference plays it back in its own soundtrack, and
|
|
76
|
+
draws the lips against that. Cross-correlating the clip's sound with the reference (GCC-PHAT)
|
|
77
|
+
gives the offset: about 0 ms with a sharp peak means the lips are timed to the song by
|
|
78
|
+
construction. No clear peak means the clip did not use the reference: regenerate it. A clear peak away from 0
|
|
79
|
+
is a shift, not a failure: one 480p clip dropped the quiet half second before the first sung word
|
|
80
|
+
of its reference, so its sound (and its lips) ran 475 ms ahead of the song; `lag` says so and
|
|
81
|
+
gives the `baseStart` that puts the lips back on the song. At 120 BPM a beat is 500 ms, so
|
|
82
|
+
check that the peak is far above the next one before trusting a shift near a beat's length. Then look:
|
|
83
|
+
`film frames` the clip and check mouths at a few sung words (open on vowels, closed on m/b/p and in
|
|
84
|
+
the gaps). A stylised mouth (a beak, a mask) at 480p opens with the phrases rather than each
|
|
85
|
+
syllable; a human-like mouth at 720p syncs closer. Choose the character with that in mind.
|
|
86
|
+
|
|
87
|
+
## 6. The draw-over and kinetic type
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
node <video.mjs> film init <slug> # film/index.html, film.js, look.js, shots.json
|
|
91
|
+
node <video.mjs> film frames <slug> --base work/bases/s03.mp4 --id s03
|
|
92
|
+
node <video.mjs> film render <slug> [--from 0 --to 12] # work/film-h.mp4
|
|
93
|
+
node <video.mjs> film render <slug> --mode v # work/film-v.mp4 (1080x1920)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `film.js` renders any frame as a pure function of time (`renderAt(t)`): the shot at that time,
|
|
97
|
+
the base frame under it, the look, the words. `film render` photographs every frame in order in
|
|
98
|
+
a headless GPU Chrome and encodes them with the song, so a render is the same film every time.
|
|
99
|
+
- `look.js` is the look, and it is meant to be rewritten for each video: the starting point
|
|
100
|
+
prints the base as two inks on paper (halftone dots, line work, a hand-made boil twelve times a
|
|
101
|
+
second, paper tooth fixed to the card) and sets the sung words big for shots marked `big`,
|
|
102
|
+
quiet for `quiet`. Push it where the style sheet says.
|
|
103
|
+
- `shots.json`: `at` and `dur` (on the grid), `base` (the frames folder), `baseStart` (where in
|
|
104
|
+
the clip to start: remap a story beat onto the lyric that needs it), `focus` (where the
|
|
105
|
+
subject is, 0..1, so a crop keeps them), `type` (`big`, `quiet`, `none`) and `side` (where the
|
|
106
|
+
words go, opposite the subject). `songStart` is where the song starts in the film.
|
|
107
|
+
- Inserts with no character (a title, a graphic moment, an object) can be drawn entirely in
|
|
108
|
+
`look.js` with no base at all: a shot with no `base` gets only the paper and the type.
|
|
109
|
+
|
|
110
|
+
## 7. The sync loop
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
node <video.mjs> words <slug> --in work/film-h.mp4 # every sung word's frame, labelled
|
|
114
|
+
node <video.mjs> sync <slug> --in work/film-h.mp4 --at 4.00,8.00,12.00
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`words` makes a sheet with one frame at each sung word's onset: the mouth and the lyric type
|
|
118
|
+
must match the word under it. A word lost at a cut (its first syllable before the cut) is carried
|
|
119
|
+
by starting that line in the preceding shot. `sync` compares the sound's onset with the
|
|
120
|
+
picture's change at each hit or cut: within one frame passes. Fix, re-render the range, check
|
|
121
|
+
again, as many times as it takes.
|
|
122
|
+
|
|
123
|
+
## 8. Contact-sheet review
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
node <video.mjs> sheet <slug> --in work/film-h.mp4 --every 1
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Look at every cell: type that runs off the frame, a face under a word, a shot that reads as a
|
|
130
|
+
different film, a frame gone black. Then a closer look at the hook and the chorus (every half
|
|
131
|
+
second). Nothing is finished until the whole video has been watched this way.
|
|
132
|
+
|
|
133
|
+
## 9. Deliver and publish
|
|
134
|
+
|
|
135
|
+
Copy the finished cuts to `videos/<slug>/<slug>.mp4` (16:9) and `<slug>-vertical.mp4` (9:16),
|
|
136
|
+
then `add … --kind music-video --for-song <music slug> --publish` and `publish`. Credits name the
|
|
137
|
+
song (and its provider) and the models the footage came from.
|
|
138
|
+
|
|
139
|
+
## A cutscene
|
|
140
|
+
|
|
141
|
+
The same steps with a script instead of a song: the audio reference is the scene's dialogue or
|
|
142
|
+
sound design (made with the `music` skill or recorded), the grid is the scene's own timing (a
|
|
143
|
+
`grid.json` you write: `beats` at the story's hits, `words` from the dialogue's timing), and it
|
|
144
|
+
is delivered at the game's own resolution for playback inside the game (`games/<id>/public/`).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Recording a page while a script drives it
|
|
2
|
+
|
|
3
|
+
**Server rules keep real time.** A trailer for a rules game uses live `capture`, not a
|
|
4
|
+
virtual browser clock: slowing the view cannot slow the server. Scripted inputs use
|
|
5
|
+
`record`, then `edl`/`cut`. Keep a real participant in the room while filming a wall/TV;
|
|
6
|
+
a spectator does not keep an empty room alive. Do not switch to a browser host for footage
|
|
7
|
+
presented as online play. Inspect the room and actual action, sound and held frames.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
`record` (scripts/record-page.mjs) records any web page in real time while a steps file drives it:
|
|
11
|
+
clicks, taps, keys, typing, scrolls, drags, and waits for a selector, a text or a state. Use it for
|
|
12
|
+
a studio's own game page (land, press Play, play a little), a site walkthrough, a product demo or a
|
|
13
|
+
tutorial. `capture` is still the one for a trailer's gameplay (the game's big screen, nothing pressed).
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run dev # background task; the site at http://127.0.0.1:8787
|
|
17
|
+
node <video.mjs> record <slug> --steps <steps.json> --url http://127.0.0.1:8787
|
|
18
|
+
node <video.mjs> record <slug> --steps <steps.json> --url http://127.0.0.1:8787 --device phone --name phone
|
|
19
|
+
node <video.mjs> sheet <slug> --in videos/<slug>/work/record/recording.mp4
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Outside a studio (any project, any address): `node <skill folder>/scripts/record-page.mjs --steps
|
|
23
|
+
<steps.json> --out <folder>`. It needs Chrome, ffmpeg and `puppeteer-core` (a studio has it; elsewhere
|
|
24
|
+
install it yourself with `npm install --no-save puppeteer-core`).
|
|
25
|
+
|
|
26
|
+
The take lands in `videos/<slug>/work/record/` (`--name <take>`: `work/record-<take>/`):
|
|
27
|
+
|
|
28
|
+
- `recording.mp4`: 1920x1080 at 30 fps for a computer, 1080x1920 for a phone (`--device phone`: a
|
|
29
|
+
360x640 page at 3x, touch on, a phone's user agent), H.264, faststart;
|
|
30
|
+
- `recording.json`: every step with the second it started and how long it took, what its own frame
|
|
31
|
+
received while it ran (`received`: keys, pointer presses, touches, or "another page opened"), the
|
|
32
|
+
page's and the game's real frame rates, frames painted and held, the GPU renderer, warnings;
|
|
33
|
+
- `captions.vtt` when steps have captions (copy it to `videos/<slug>/captions.vtt` for the page).
|
|
34
|
+
|
|
35
|
+
The command prints a few lines (`--json`: a small object of paths and numbers, never media).
|
|
36
|
+
|
|
37
|
+
## The steps file
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"path": "/gem-rush/",
|
|
42
|
+
"device": "computer",
|
|
43
|
+
"tail": 2,
|
|
44
|
+
"steps": [
|
|
45
|
+
{ "wait": 1500, "caption": "Gem Rush, on the studio's own site" },
|
|
46
|
+
{ "click": "a[data-play]", "caption": "Press Play" },
|
|
47
|
+
{ "waitFor": { "frame": "game", "selector": "canvas" }, "timeout": 30000 },
|
|
48
|
+
{ "key": "ArrowRight", "hold": 900, "frame": "game" }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Where it starts: `"url"` (any address), or `"path"` joined to `--url` (the local `dev` site or the
|
|
54
|
+
live one). The page is opened and settled (`"settle"`, 1.5 s), then the recording starts and the steps
|
|
55
|
+
run one after another, at the speed they take. It ends `"tail"` seconds after the last step, or at
|
|
56
|
+
`--seconds` (120 by default, 600 at most).
|
|
57
|
+
|
|
58
|
+
Every step is one of these (`{ "click": "a" }` is short for `{ "do": "click", "selector": "a" }`). Any
|
|
59
|
+
step takes `"caption"` (a cue from the second it starts, `"captionSeconds"` long, 3 by default),
|
|
60
|
+
`"timeout"` (ms, 20000) and `"optional": true` (a failure is noted and the steps go on).
|
|
61
|
+
|
|
62
|
+
| Step | What it does |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `{ "wait": 1500 }` | Waits, in milliseconds. |
|
|
65
|
+
| `{ "waitFor": "<selector>" }` | Until it is visible. `{ "waitFor": { "text": "Round 2" } }`, `{ "waitFor": { "js": "window.game?.started" } }` (any expression, true when it is), `{ "waitFor": { "frame": "game" } }` (until the frame exists). |
|
|
66
|
+
| `{ "click": "<selector>" }` | The cursor glides there (`"ms"`, 450), a press ring, a real click. `"at": [x, y]` instead of a selector (CSS pixels, or 0..1 of the page), `"offset": [0.5, 0.7]` inside the element. On a phone, a tap. |
|
|
67
|
+
| `{ "tap": "<selector>" }` | A touch tap, with a ring. |
|
|
68
|
+
| `{ "drag": { "selector": "canvas", "offset": [0.5, 0.7], "to": [90, 0], "ms": 500, "hold": 900 } }` | A held press that moves by `to` (CSS pixels) over `ms`, then held: a virtual stick on a phone, a drag on a computer. |
|
|
69
|
+
| `{ "hover": "<selector>" }` | The cursor glides there; hover styles answer. |
|
|
70
|
+
| `{ "key": "ArrowRight", "hold": 900 }` | One key held for `hold` ms. `"Shift+ArrowUp"` holds both. `"times": 3` repeats it. |
|
|
71
|
+
| `{ "keys": ["ArrowUp", "ArrowLeft"], "hold": 300, "gap": 120 }` | Several, one after another. |
|
|
72
|
+
| `{ "type": "Owl", "into": "input[name=nick]" }` | Clicks into the field, then types at `"delay"` ms a letter (70). |
|
|
73
|
+
| `{ "scroll": 600 }` | Scrolls by wheel over time (`"ms"`), never a jump. `{ "scroll": { "to": "#pricing" } }` brings it to the top quarter. |
|
|
74
|
+
| `{ "focus": "game" }` | Gives a frame the keyboard (a play page usually does it itself). |
|
|
75
|
+
| `{ "goto": "/games/" }` | Opens another address (a path joins `--url`). |
|
|
76
|
+
| `{ "caption": "…" }` | A caption on its own, with no action. |
|
|
77
|
+
|
|
78
|
+
`"frame"` on any step names where its selector, keys or check are: `"game"` (a studio game's own
|
|
79
|
+
frame, `/<id>/__game/`), a piece of a frame's address, or the page itself (the default). Keys go to
|
|
80
|
+
the frame that has the keyboard; `"frame": "game"` gives it to the game first.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
|
|
84
|
+
- `references/examples/studio-play.json`: a studio's own game from its landing: Play, then the arrow
|
|
85
|
+
keys, on a computer.
|
|
86
|
+
- `references/examples/studio-play-phone.json`: the same on a phone: a tap on Play, then the game's own
|
|
87
|
+
touch stick, dragged and held.
|
|
88
|
+
- `references/examples/site-walkthrough.json`: an ordinary page (example.com): a scroll and a hover.
|
|
89
|
+
|
|
90
|
+
The two studio examples are tested: `scripts/studio-check.mjs --record` records both against a new
|
|
91
|
+
studio's Gem Rush under `homie-studio dev` (CI runs it on Linux), and fails when a step fails or the
|
|
92
|
+
game's frame received none of the input.
|
|
93
|
+
|
|
94
|
+
For another game: its id in `path`, and its own controls (`games/<id>/src`: what its keydown and
|
|
95
|
+
pointer handlers listen for).
|
|
96
|
+
|
|
97
|
+
## Honest frames, at real speed
|
|
98
|
+
|
|
99
|
+
- **Real time.** The steps run at the speed the page answers; the film lasts exactly as long as the
|
|
100
|
+
recording. Nothing is sped up, slowed down, cut or reordered in the take; any edit comes after, in
|
|
101
|
+
the open.
|
|
102
|
+
- **Every frame is one the page drew.** Chrome's compositor frames are kept with the time each was
|
|
103
|
+
presented and laid onto 30 fps by holding the newest one. A moment the page did not repaint is held,
|
|
104
|
+
and counted (`heldFrames`); a still page lasts as long as it showed. Never interpolated, never
|
|
105
|
+
blended (the test suite guards it): **interpolated frames are fabrication.**
|
|
106
|
+
- **A GPU, or say so.** Headless Chrome runs on the GPU flags; a software renderer (SwiftShader, on a
|
|
107
|
+
machine without a GPU) draws a WebGL game at a few frames a second and makes it look stuck. The
|
|
108
|
+
result names the renderer and the page's own frame rate (`pageFps`, measured with
|
|
109
|
+
requestAnimationFrame in the page and in the game's frame); under 20, or on a software renderer, it
|
|
110
|
+
warns, and the recording shows a slow computer, not the game. Record on a computer with a GPU, or at
|
|
111
|
+
`--scale 0.67`. `--min-fps 20` makes such a take fail instead.
|
|
112
|
+
- **Delivered is not correct.** `received` says the keys and taps reached the game's frame; it does not
|
|
113
|
+
say the game did the right thing with them. Look at the take (`sheet`) before anyone else does.
|
|
114
|
+
- **The cursor is the recorder's.** The arrow and the press rings are drawn over the page to show where
|
|
115
|
+
the script pointed and pressed (`--no-cursor` leaves them out). The take's honesty line says so, and
|
|
116
|
+
`add --file <the take>` puts it on the video page.
|
|
117
|
+
- **Sound.** A studio game's own WebAudio (its `/__game/` frame), on the picture's clock: steady
|
|
118
|
+
presses land within a frame or two of their sound. Any other page records silent; a bed or a voice-over
|
|
119
|
+
is added in the edit.
|
|
120
|
+
- **Bots are bots.** A studio game may fill empty seats with its own bots: never caption them as people.
|
|
121
|
+
- **Mask before it is public.** A recording shows no address bar, but a page can print its own
|
|
122
|
+
address: a local `dev` address, a room code, a QR code that joins a room (the big screen's). Decode
|
|
123
|
+
every QR in a public video. The recorder's visits are tagged as the studio's own QA, so the studio's
|
|
124
|
+
stats never count them as visitors.
|
|
125
|
+
|
|
126
|
+
## Frame by frame instead of real time
|
|
127
|
+
|
|
128
|
+
`record-fixed.mjs` (the older browser-game trailer path) takes the same steps file but steps the page's clock
|
|
129
|
+
1/fps a frame instead of filming it live: no held frames on a slow page, and waits and holds are in
|
|
130
|
+
film time. It has wait, waitFor, click, tap, key, keys, type, focus and caption only, draws no cursor,
|
|
131
|
+
films a computer's page only, and its sound comes from the game's sound log, not from the speaker.
|
|
132
|
+
`references/TRAILER.md` section 0 says what its clock does and does not reach.
|
|
133
|
+
|
|
134
|
+
## Disk and size
|
|
135
|
+
|
|
136
|
+
A take keeps every painted frame as a JPEG until it is encoded (about 150 KB each at 1080p, up to 60
|
|
137
|
+
a second), then deletes them. It stops below 10 GB free (`--min-free-gb`). Check `df -h .` first.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# A gameplay trailer (15 to 60 seconds)
|
|
2
|
+
|
|
3
|
+
**Server rules keep real time.** A trailer for a rules game uses live `capture`, not a
|
|
4
|
+
virtual browser clock: slowing the view cannot slow the server. Scripted inputs use
|
|
5
|
+
`record`, then `edl`/`cut`. Keep a real participant in the room while filming a wall/TV;
|
|
6
|
+
a spectator does not keep an empty room alive. Do not switch to a browser host for footage
|
|
7
|
+
presented as online play. Inspect the room and actual action, sound and held frames.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Real footage of the studio's own game, cut on the beat of the studio's own music. Free: no
|
|
11
|
+
provider, no model, nothing generated.
|
|
12
|
+
|
|
13
|
+
## 0. The short way: `trailer`
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run dev # as a background task
|
|
17
|
+
node <video.mjs> trailer <slug> --game <id> --url http://127.0.0.1:8787 --seconds 45 --length 20 --title "GAME NAME"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For a server-rules game this captures live picture and speaker audio, picks shots by
|
|
21
|
+
motion, and makes cards and three deliveries. It defaults to `--view play`: an idle
|
|
22
|
+
camera occupies a seat and keeps the room awake. `--view tv` needs another participant
|
|
23
|
+
in that same room. Inspect actual action; an idle seat is not gameplay.
|
|
24
|
+
The output says frames were recorded live, including held frames; it does not claim
|
|
25
|
+
reconstructed audio stems or beat-aligned shots without a sound log. Use record for
|
|
26
|
+
scripted inputs. The virtual-clock flags are refused with that alternative.
|
|
27
|
+
|
|
28
|
+
For an older browser game, the frame-by-frame path below needs no song:
|
|
29
|
+
|
|
30
|
+
1. **Films frame by frame** (`record-fixed.mjs`): the page's clock is the recorder's. Each film frame,
|
|
31
|
+
`requestAnimationFrame`, `performance.now`, `Date`, `setTimeout`, `setInterval`, CSS animations and
|
|
32
|
+
transitions and Web Animations move on by exactly 1/fps, the page draws, the frame is copied. A
|
|
33
|
+
countdown on a timer, a callout faded by CSS and the game loop stay in step, and no frame is held.
|
|
34
|
+
`capture.json` gives `realSeconds` and `speed` (film seconds per real second).
|
|
35
|
+
2. **Rebuilds the sound** from the capture log (`events.json`): the `sound` skill's `sound.js` pushes
|
|
36
|
+
every sound it schedules onto `window.__homieSoundCapture.events` when a recorder set that global
|
|
37
|
+
before the page ran (file, gain, rate, pan, loop, delay; stops, ducks, fader moves). The files are
|
|
38
|
+
fetched from the page's own origin and mixed at 48 kHz: `sound.wav`, `sfx.wav`, `music.wav`.
|
|
39
|
+
The player's compressor is stood in for by a peak limiter at -1 dBFS.
|
|
40
|
+
3. **Picks the shots** from that log: each effect's start scores its tenth of a second, divided by the
|
|
41
|
+
square root of how often that sound played, plus 0.35 of the picture's motion. With music in the
|
|
42
|
+
log, a shot is one bar (two when a bar is under 1.2 s, half when over 2.6 s), the bed is the game's
|
|
43
|
+
own `music.wav` from one of its bar lines, and only `sfx.wav` is cut with the picture.
|
|
44
|
+
4. **Cards and three deliveries**: an end card (`--end`, `--sub`, `--small`), a title card with
|
|
45
|
+
`--title`, in the game's look; 16:9, 1:1 and 9:16, each -14 LUFS.
|
|
46
|
+
|
|
47
|
+
Limits of the older frame-by-frame path:
|
|
48
|
+
|
|
49
|
+
- Sound that does not go through `sound.js` (the game's own oscillators, an `<audio>` element, speech):
|
|
50
|
+
not in the mix. A game with its own audio code can push the same events (the head of `sound.js`
|
|
51
|
+
lists them). No events at all: a silent film and a warning; use `capture` (it records the speaker).
|
|
52
|
+
- The audio clock, `<video>`/`<audio>` elements, Web Workers and the network are not on the virtual
|
|
53
|
+
clock. The big screen (`/<id>/tv`, the default) follows a live room on a relay that keeps real time:
|
|
54
|
+
at a `speed` near 1 the film is right; well under 1 the room runs fast in the film, and the result
|
|
55
|
+
warns. Render smaller (`--scale`), or use `capture`. Rendering is never run faster than real time
|
|
56
|
+
unless `--no-pace` (for a page with no server behind it).
|
|
57
|
+
- Steps (`--steps`) are wait, waitFor, click, tap, key, keys, type, focus and caption, timed on the
|
|
58
|
+
film's clock; no cursor is drawn, and hover, drag, scroll and goto belong to `record`.
|
|
59
|
+
- A computer's page only (no phone emulation); the 9:16 and 1:1 files are made from the 16:9 film.
|
|
60
|
+
- The highlights are a score, not a judgement: read `shotsPlayed`, look at the sheet, edit `work/edl.json`.
|
|
61
|
+
|
|
62
|
+
`--keep-capture` re-edits the film already made. Sections 5 and 6 below still apply (look, check, publish).
|
|
63
|
+
|
|
64
|
+
## 1. The music
|
|
65
|
+
|
|
66
|
+
The bed is a song from the `music` skill (`--song <slug>` everywhere below): its plan gives the
|
|
67
|
+
exact tempo and its bar lines. The best trailer bed has a clear beat and a lift in its first few
|
|
68
|
+
bars; `--bed-from-bar <k>` starts the trailer at a later bar (a chorus). With no song, the
|
|
69
|
+
trailer runs on the game's own sound and cuts every two seconds.
|
|
70
|
+
|
|
71
|
+
## 2. The capture
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npm run dev # as a background task: the studio's site at http://127.0.0.1:8787
|
|
75
|
+
node <video.mjs> capture <slug> --game <id> --view play --url http://127.0.0.1:8787 --seconds 60
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- If another site already holds port 8787, start this one with
|
|
79
|
+
`npx --no-install homie-studio dev --port <free port>`, and check the address answers with
|
|
80
|
+
this studio's name before capturing.
|
|
81
|
+
- Stop it afterwards with `npx --no-install homie-studio dev --stop` (this studio's server and
|
|
82
|
+
nothing else).
|
|
83
|
+
- Without `--view play`, capture opens the game's big screen (`/<id>/tv`): a spectator
|
|
84
|
+
in a live public room. A server room needs a participant to tick. Bots may fill empty seats
|
|
85
|
+
according to the game's policy; do not assume there is always action. Nothing is pressed.
|
|
86
|
+
- The picture is Chrome's own frame stream with the time each frame was shown; it is laid onto
|
|
87
|
+
30 fps, and a frame the page did not paint in time is held and counted (`heldFrames`). The
|
|
88
|
+
sound is copied off the game's WebAudio output on the audio clock, which is fitted to the page
|
|
89
|
+
clock, so a hit and its sound stay on the same frame.
|
|
90
|
+
- Capture two or three times the trailer's length: the edit picks the best moments.
|
|
91
|
+
- `--view play` films a seat instead of the big screen, but that seat is taken by the capture and
|
|
92
|
+
stands still (its avatar is idle, and the room counts it). Prefer the big screen when
|
|
93
|
+
other people are playing in that room. Never script a
|
|
94
|
+
player to look good for the camera: what is filmed is the game as it plays.
|
|
95
|
+
- A 3D game on a laptop: check `sourceFps` in `capture.json`; under 30, capture again with fewer
|
|
96
|
+
other programs running, or a smaller `--width`/`--height`.
|
|
97
|
+
|
|
98
|
+
## 3. Cards
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
node <video.mjs> card <slug> --name title --text "GAME NAME" --sub "a one-line promise"
|
|
102
|
+
node <video.mjs> card <slug> --name end --text "Play free" --sub "<the play link>" --small "Real gameplay, recorded live."
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Cards are drawn in Chrome at 1920x1080 and 1080x1920 (no font setup needed). Keep the title to a
|
|
106
|
+
word or three; the end card is where people go next.
|
|
107
|
+
|
|
108
|
+
## 4. The edit
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
node <video.mjs> edl <slug> --length 30 --song <music slug> --title "GAME NAME"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`work/edl.json`: a title card of one bar, shots of one bar (two beats when a bar is longer than
|
|
115
|
+
2.6 s), the end card on what is left (at least 2.5 s). Shots are the busiest non-overlapping
|
|
116
|
+
windows of the capture, measured as motion, that do not look like a shot already picked, shown in
|
|
117
|
+
the order they happened. Each shot pushes in slowly (to 1.25x by default, `--push 1` for none)
|
|
118
|
+
toward where the picture changes most: a camera move on the real footage, nothing added. Every
|
|
119
|
+
cut is on a bar line. Edit it by hand: swap a shot for a better moment (`in` is seconds into the capture),
|
|
120
|
+
make one shot two bars long (and shorten another), move the song's start. Keep the total.
|
|
121
|
+
|
|
122
|
+
## 5. Cut, check, look
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
node <video.mjs> cut <slug>
|
|
126
|
+
node <video.mjs> sync <slug>
|
|
127
|
+
node <video.mjs> sheet <slug> --in videos/<slug>/<slug>.mp4 --every 0.5
|
|
128
|
+
node <video.mjs> sheet <slug> --in videos/<slug>/<slug>-vertical.mp4
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- 16:9 is 1920x1080; 9:16 is 1080x1920 with the whole game frame across the middle over a
|
|
132
|
+
blurred, darkened fill of itself (a crop would hide the action at the edges).
|
|
133
|
+
- Sound: the song, with the game's own sound 8 dB under it where the game made any, both
|
|
134
|
+
deliveries at -14 LUFS and -1.5 dBTP; H.264 High, yuv420p in limited range, converted to and tagged
|
|
135
|
+
BT.709, faststart. `--square` adds 1:1 (1080x1080; `card … --square` draws its cards).
|
|
136
|
+
- Cuts are counted in frames: each cut is on the frame nearest its bar line (`cuts` in the result
|
|
137
|
+
gives the frame, the time and how far from the bar it is, never over half a frame), every frame's
|
|
138
|
+
timestamp is its number over 30, and the film is exactly its frames long. A bar of 1.875 s is
|
|
139
|
+
56.25 frames: the shots run 56, 57, 56, 56 frames, and nothing drifts.
|
|
140
|
+
- `sync` must pass (every cut within a frame of its beat), and the contact sheets must show the
|
|
141
|
+
game, readable, at every cell. A beat with no onset right on it (a rest, a pickup note just
|
|
142
|
+
before the bar) is listed, not judged: never move a cut off its bar to make the check pass.
|
|
143
|
+
- A fixed camera on a whole arena reads small on a phone: prefer moments where players are close
|
|
144
|
+
together, or a bigger push (`--push 1.5`).
|
|
145
|
+
|
|
146
|
+
## 6. Publish
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
node <video.mjs> add <slug> --title "GAME NAME trailer" --kind trailer --for-game <id> --for-song <music slug> --publish
|
|
150
|
+
node <video.mjs> publish <slug>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The page gets a Play button for the game and the capture's honesty line ("recorded from the
|
|
154
|
+
game running in a live public room; seats without a person are the game's own bots").
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "A walkthrough of an ordinary page: no game, a scroll, a hover, a click and some typing. \"url\" is any address; captions become captions.vtt for the video page.",
|
|
3
|
+
"url": "https://example.com/",
|
|
4
|
+
"device": "computer",
|
|
5
|
+
"tail": 2,
|
|
6
|
+
"steps": [
|
|
7
|
+
{ "wait": 1200, "caption": "The page as a visitor sees it" },
|
|
8
|
+
{ "scroll": 400 },
|
|
9
|
+
{ "hover": "a", "caption": "Its one link" },
|
|
10
|
+
{ "wait": 800 },
|
|
11
|
+
{ "scroll": -400 },
|
|
12
|
+
{ "wait": 800 }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "The same game on a phone (a 9:16 film, 1080x1920): a tap on Play, then the game's own touch stick, dragged and held. Tested on the Gem Rush starter. A phone page has touch only: tap and drag, never a key.",
|
|
3
|
+
"path": "/gem-rush/",
|
|
4
|
+
"device": "phone",
|
|
5
|
+
"tail": 2,
|
|
6
|
+
"steps": [
|
|
7
|
+
{ "wait": 1200, "caption": "Gem Rush on a phone" },
|
|
8
|
+
{ "click": "a[data-play]", "caption": "Tap Play" },
|
|
9
|
+
{ "waitFor": { "frame": "game", "selector": "canvas" }, "timeout": 30000 },
|
|
10
|
+
{ "wait": 2500 },
|
|
11
|
+
{ "drag": { "frame": "game", "selector": "canvas", "offset": [0.5, 0.7], "to": [90, 0], "ms": 500, "hold": 900 }, "caption": "Drag to move" },
|
|
12
|
+
{ "drag": { "frame": "game", "selector": "canvas", "offset": [0.5, 0.7], "to": [0, -90], "ms": 500, "hold": 900 } },
|
|
13
|
+
{ "wait": 1500 }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "A studio's own game, recorded from its landing page: Play, then a few moves with the arrow keys. Tested on the Gem Rush starter (scripts/studio-check.mjs --record). For another game: its id in \"path\", and its own keys (games/<id>/src, the keydown handler).",
|
|
3
|
+
"path": "/gem-rush/",
|
|
4
|
+
"device": "computer",
|
|
5
|
+
"tail": 2,
|
|
6
|
+
"steps": [
|
|
7
|
+
{ "wait": 1500, "caption": "Gem Rush, on the studio's own site" },
|
|
8
|
+
{ "click": "a[data-play]", "caption": "Press Play" },
|
|
9
|
+
{ "waitFor": { "frame": "game", "selector": "canvas" }, "timeout": 30000 },
|
|
10
|
+
{ "wait": 2500, "caption": "A public room at once; bots fill the empty seats" },
|
|
11
|
+
{ "key": "ArrowRight", "hold": 900, "frame": "game", "caption": "The arrow keys move" },
|
|
12
|
+
{ "key": "ArrowUp", "hold": 700, "frame": "game" },
|
|
13
|
+
{ "key": "ArrowLeft", "hold": 900, "frame": "game" },
|
|
14
|
+
{ "key": "ArrowDown", "hold": 700, "frame": "game" },
|
|
15
|
+
{ "wait": 1500 }
|
|
16
|
+
]
|
|
17
|
+
}
|