reelkit-cli 0.10.6 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +12 -2
- package/package.json +2 -1
- package/skill/SKILL.md +41 -5
- package/skill/reference/delivery.md +39 -0
- package/skill/reference/hebrew-rtl.md +10 -1
- package/skill/reference/reference-recreation.md +33 -0
- package/skill/reference/revisions.md +36 -0
- package/skill/reference/rights.md +27 -0
- package/skill/reference/voice-fixes.md +33 -0
- package/skill/reference/voice-sync.md +4 -0
- package/src/cli.ts +40 -2
- package/src/clock/clock.ts +76 -0
- package/src/commands/build.ts +98 -7
- package/src/commands/clock.ts +37 -0
- package/src/commands/diff.ts +81 -0
- package/src/commands/export.ts +51 -0
- package/src/commands/lint.ts +94 -0
- package/src/commands/template.ts +217 -0
- package/src/contract/index.ts +60 -0
- package/src/diff/framediff.ts +121 -0
- package/src/export/presets.ts +147 -0
- package/src/lint/pixel-rules.ts +149 -0
- package/src/lint/source-rules.ts +186 -0
- package/src/media/ffmpeg.ts +84 -0
- package/src/render/chunked.ts +139 -0
- package/src/render/render.ts +22 -4
- package/src/render/worker.ts +98 -54
- package/src/testing/fake-api.ts +49 -1
- package/tools/voice/README.md +26 -0
- package/tools/voice/_audio.py +118 -0
- package/tools/voice/ab_video.py +93 -0
- package/tools/voice/credits.py +35 -0
- package/tools/voice/onsets.py +70 -0
- package/tools/voice/phonemes.py +85 -0
- package/tools/voice/pitch_check.py +51 -0
- package/tools/voice/place_line.py +67 -0
- package/tools/voice/splice.py +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
What changed in each published version of `reelkit-cli`, newest first. A push to `main` publishes the version in
|
|
4
4
|
`package.json` when it is not on npm yet, and it must have an entry here.
|
|
5
5
|
|
|
6
|
+
## 0.12.0 - 2026-10-09
|
|
7
|
+
|
|
8
|
+
- Templates: `reelkit template push` saves a whole project in your private files (the plan, the composition, everything under `assets/` and the rendered video), and `reelkit template clone <id>` unpacks one into a new project to continue or remix. `reelkit template list` and `reelkit template delete <id>` manage them.
|
|
9
|
+
- Your templates page on reelkit.cc shows each one: its video, a timeline of its scenes, text, voice, music, beats and sound effects, and its files.
|
|
10
|
+
- `reelkit render` writes `out/sounds.json`: which sound plays when. A template pushed after that names each sound effect on its timeline.
|
|
11
|
+
|
|
12
|
+
## 0.11.0 - 2026-10-09
|
|
13
|
+
|
|
14
|
+
Ported from the reelkit-cli 0.9 toolset (written against the Remotion 0.8.0 CLI) onto the HyperFrames renderer.
|
|
15
|
+
|
|
16
|
+
- `reelkit export` with presets `iphone`, `phone-720` and `chat`, plus `--verify-only`. A full-range source is converted to TV-range `yuv420p` once, tagged BT.709, pinned to the preset's profile and level 4.0, with the index at the front. The command then prints a pass/fail table from ffprobe and a full decode. A source that is already TV range is not converted again.
|
|
17
|
+
- `reelkit diff` compares picture and audio and, with `--allow`, fails when a change falls outside the given frames.
|
|
18
|
+
- `reelkit render` keeps the file it replaces as `out/previous.mp4`, retries a browser crash, and reports success only after the file is synced, fully decoded and the right length. `--chunk` renders resumable pieces and mixes their audio once. `--guard` and `--allow` refuse a render that changed more than it was allowed to, and then share nothing.
|
|
19
|
+
- `reelkit lint` reports lettering cut by a frame edge, a short flash over a held shot, a shot that is never drawn, right-to-left text with no direction, and vowel points in on-screen text. A wide plate no longer hides the letters beside it.
|
|
20
|
+
- `reelkit clock` maps a reference frame to a film frame, and the other way.
|
|
21
|
+
- `reelkit tools` prints the voice scripts in `tools/voice`. Their extra Python packages stay optional: a missing one exits with a message instead of a traceback.
|
|
22
|
+
- The skill now includes the acceptance habits and the reference notes for close recreations, revisions, delivery, voice fixes and rights.
|
|
23
|
+
|
|
6
24
|
## 0.10.6 - 2026-10-09
|
|
7
25
|
|
|
8
26
|
- Corrects this changelog. The 0.10.5 entry said 0.10.4 had been published without the `reelkit` command; it was published complete.
|
package/README.md
CHANGED
|
@@ -76,7 +76,15 @@ reelkit render
|
|
|
76
76
|
| `reelkit plan check` | Validate `plan.json` |
|
|
77
77
|
| `reelkit check` | Check the composition without rendering |
|
|
78
78
|
| `reelkit preview` | Preview frames, and a report of which scene changes carry something across |
|
|
79
|
-
| `reelkit render` | Render `out/video.mp4` |
|
|
79
|
+
| `reelkit render` | Render `out/video.mp4`. The file it replaces is kept as `out/previous.mp4` |
|
|
80
|
+
| `reelkit render --chunk` | Render a long film in pieces that are kept if the run stops; the same command continues |
|
|
81
|
+
| `reelkit render --allow <ranges>` | After rendering, fail if a frame or the sound changed outside those ranges |
|
|
82
|
+
| `reelkit render --guard previous` | Compare the new render with the one it replaced |
|
|
83
|
+
| `reelkit export --preset iphone` | A phone-safe copy: TV-range `yuv420p`, BT.709, level 4.0, then a pass/fail table (`phone-720`, `chat`) |
|
|
84
|
+
| `reelkit diff <old> <new> --allow <ranges>` | Picture and audio diff; fails when a change lies outside the ranges |
|
|
85
|
+
| `reelkit lint` | Advice: lettering cut by an edge, flashes over a held shot, shots never drawn, RTL text, vowel points |
|
|
86
|
+
| `reelkit clock` | Where a reference frame lands in a film cut down from a longer video |
|
|
87
|
+
| `reelkit tools` | Where the voice scripts are (options video, splice, place a line, onsets, phonemes, pitch, credits) |
|
|
80
88
|
|
|
81
89
|
## What is shared
|
|
82
90
|
|
|
@@ -98,7 +106,9 @@ node bin/reelkit.mjs --help # or `npm link` to get the `reelkit` command
|
|
|
98
106
|
pnpm test
|
|
99
107
|
```
|
|
100
108
|
|
|
101
|
-
Tests run against an in-process fake of the API (`src/testing/fake-api.ts`), so they need no account and no network.
|
|
109
|
+
Tests run against an in-process fake of the API (`src/testing/fake-api.ts`), so they need no account and no network. `pnpm test` runs them.
|
|
110
|
+
|
|
111
|
+
A checkout is a different install from the published package. `node bin/reelkit.mjs` runs this copy, and `reelkit init` and `reelkit auth login` then install this copy's skill into every coding agent they find, in place of the skill those agents already have. Set `REELKIT_NO_AUTO_INSTALL=1` when you do not want that.
|
|
102
112
|
|
|
103
113
|
## Licence
|
|
104
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelkit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "CLI and Claude skill for making short-form video with a shared asset library.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Daniel Livshin",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"bin",
|
|
41
41
|
"src",
|
|
42
42
|
"skill",
|
|
43
|
+
"tools",
|
|
43
44
|
"CHANGELOG.md"
|
|
44
45
|
],
|
|
45
46
|
"engines": {
|
package/skill/SKILL.md
CHANGED
|
@@ -15,7 +15,21 @@ For image or video cutouts, editable Blender product shots, custom 3D materials,
|
|
|
15
15
|
|
|
16
16
|
To implement a chosen style, read its row in `reference/style-recipes.md` for action, timing, sound and proof. `reference/style-components.md` maps the shared components and built-in kit to shot roles; inspect the selected source before using its props.
|
|
17
17
|
|
|
18
|
-
This skill targets `reelkit-cli` 0.
|
|
18
|
+
This skill targets `reelkit-cli` 0.12.0 and its HyperFrames kit. Check the installed version with `reelkit --version`; in a source checkout, invoke its `bin/reelkit.mjs` with Node. Use the matching CLI before authoring with `reelkit/frame`.
|
|
19
|
+
|
|
20
|
+
## What decides whether the film is accepted
|
|
21
|
+
|
|
22
|
+
A render that looks finished can still be refused. These are the checks that decide it:
|
|
23
|
+
|
|
24
|
+
1. A close recreation is measured from the reference's frames, not from memory (`reference/reference-recreation.md`). The reference and the film are different clocks: `reelkit clock` converts one into the other.
|
|
25
|
+
2. What a word names is on screen when that word is said. After a line is replaced, measure the new onset (`reference/voice-sync.md`) and move only that caption.
|
|
26
|
+
3. `reelkit lint`, and `reelkit lint --no-video` before a render, reports lettering cut by the frame, a flash that hides a held shot, a shot that is never drawn, Hebrew with no direction, and vowel points in on-screen text. Open the frames it names. Lettering that sits on a photograph is a blind spot of the edge check.
|
|
27
|
+
4. After the first render, a revision changes only what was asked. `reelkit render --allow <frames>` fails when anything else moved (`reference/revisions.md`). `reelkit diff old new --allow <frames>` is the same check after the fact.
|
|
28
|
+
5. The file you send is an export, not the master. Use `reelkit export --preset iphone`, `reelkit export --preset phone-720` or `reelkit export --preset chat`, and only when every row of the table passes (`reference/delivery.md`).
|
|
29
|
+
6. A wrong word in the narration is fixed in that line, not by recording the film again (`reference/voice-fixes.md`). `reelkit tools` prints the scripts.
|
|
30
|
+
7. A file you did not make gets a row in `ASSET_SOURCES.md` when it enters the project (`reference/rights.md`).
|
|
31
|
+
|
|
32
|
+
`reelkit render --chunk` keeps finished pieces of a long film and continues from them. `reelkit render --guard previous` compares the new render with the one it replaced.
|
|
19
33
|
|
|
20
34
|
Every command takes `--json` for machine-readable output and exits non-zero with a one-line reason when something is wrong. Read the reason and act on it.
|
|
21
35
|
|
|
@@ -37,7 +51,9 @@ For each file the user gives, look at it, then register it with a description of
|
|
|
37
51
|
`reelkit assets upload ./logo.png --describe "Acme logo, white wordmark on blue"`
|
|
38
52
|
Add `--footage` for a video the motion design should be laid over. User files stay on this machine. A video of someone or something on a plain background can be made transparent: `--green` keys a green background locally for free, and `--cutout` removes any background on the server (the video is sent to Reelkit, so ask first; see `reference/clips.md`).
|
|
39
53
|
|
|
40
|
-
If the user points at an existing video ("make one like this", a link or a file), it is a reference: you learn how it is built and make something new in that spirit. Read `reference/references.md`, then `reelkit ref download <url-or-file>` and `reelkit ref analyze <id>`, and look at the frames it saves. Take its structure, pace and motion; never its footage, music or words. A link is fetched on the user's machine and they are responsible for the right to download it; ask before running `reelkit ref analyze` without `--no-transcript`, because the audio (never the video) is sent to Reelkit to be transcribed.
|
|
54
|
+
If the user points at an existing video ("make one like this", a link or a file), it is a reference: you learn how it is built and make something new in that spirit. Read `reference/references.md`, then `reelkit ref download <url-or-file>` and `reelkit ref analyze <id>`, and look at the frames it saves. Take its structure, pace and motion; never its footage, music or words. A link is fetched on the user's machine and they are responsible for the right to download it; ask before running `reelkit ref analyze` without `--no-transcript`, because the audio (never the video) is sent to Reelkit to be transcribed. When the brief is to follow that video cut for cut, also read `reference/reference-recreation.md` and keep the two clocks apart with `reelkit clock`.
|
|
55
|
+
|
|
56
|
+
When a file that neither you nor the user made enters the project (a clip, a photo, a logo, music), add its row to `ASSET_SOURCES.md` then, not at the end (`reference/rights.md`).
|
|
41
57
|
|
|
42
58
|
### 2. Look
|
|
43
59
|
Read `reference/styles.md` for techniques that fit the material, and `reference/art-styles.md` when the picture should be drawn. Recommend a direction with a short reason; present a catalogue only if the user asks to compare looks. If they already described what they want, use it. Bundle only consequential open questions. If any on-screen text will be Hebrew, read `reference/hebrew-rtl.md` now: it changes how words may enter and how lines are written.
|
|
@@ -118,20 +134,33 @@ Read `reference/hyperframes-composition.md` for the seekable HyperFrames timing
|
|
|
118
134
|
- For what sits behind the whole film (a video, a picture that changes, or an animated ground), read `reference/backgrounds.md`; pick one ground and keep it.
|
|
119
135
|
- Before writing a new component, read `reference/component-authoring.md`.
|
|
120
136
|
- Write `src/Video.tsx` and any component files beside it. `Video.tsx` may import only `react`, `reelkit/frame`, `reelkit/kit` and sibling components (`./Name`).
|
|
137
|
+
- Run `reelkit lint --no-video` together with `reelkit check`. It reads the source: right-to-left text with no direction, vowel points in on-screen text, a sequence that starts after its parent ends, and a shot written on a range the film never draws.
|
|
121
138
|
- `manifest.json` is the exact object passed to `Video` as the `manifest` prop. Media is referenced as `urls[path]`, where `path` is the file's path in the project, such as `urls[scene.voiceoverKey]` or `urls["assets/lib/<id>/clip.mp3"]`.
|
|
122
139
|
|
|
123
140
|
Run `reelkit check` and fix every error. Treat "Worth improving" notes as diagnostics: a deliberate cut or a calm shot can be correct. Run `reelkit preview` (the first preview on a machine may download a browser), inspect the contact sheet and each scene's early, late, word-timed and boundary frames. Check cropped or overlapping text, phone readability, missing media, hidden layers, false claims and identity changes. A mid-animation frame is not a defect by itself; inspect the landing and hold. Fix observed problems, then recheck changed shots. Repeat only for a change or unresolved defect; finish with `reference/delivery-review.md`. Continuity notes should prompt a reason for a cut, not force every shot to morph.
|
|
124
141
|
|
|
125
142
|
Use frames to inspect layout and the moving video to judge timing and sound when playback is available. `reelkit preview` writes `out/preview/sheet.jpg`, one labelled picture of all of them: look at it first, then at every frame singly. And whoever built a video is the worst judge of it. If independent review is available and authorized, give the reviewer the original request and the preview frames without your explanations. Ask for observable defects and concrete fixes: a cropped logo, unreadable label, missed landing or unintended empty state. Fix those defects and recheck the changed shots; stop iterating when the requested result is achieved.
|
|
126
143
|
|
|
144
|
+
For a close recreation, score each scene against the reference on timing, layout, type, motion and legibility, and keep that table in the project. A scene under 80 is not finished: say which part is short (`reference/reference-recreation.md`). That score is the recreation habit. It is not a quota for an ordinary film (`reference/delivery-review.md`).
|
|
145
|
+
|
|
127
146
|
**Checkpoint.** Show the preview frames when the user is choosing the final treatment. If they have already approved rendering or explicitly requested this edit and render, continue within that scope. For requested changes, edit the code, run `reelkit check` and `reelkit preview`, and inspect the updated frames.
|
|
128
147
|
|
|
129
148
|
### 7. Render
|
|
130
|
-
`reelkit render`.
|
|
149
|
+
`reelkit render`. A browser that fails to start or drops its connection is tried again; an error in the composition is not. The render counts as done only when the file is on disk, decodes from end to end and holds every frame. For a film of about a minute or longer, `reelkit render --chunk` renders in pieces of 300 frames (pass a number to change the size). Finished pieces are kept: run the same command again to continue. The render this one replaces is kept as `out/previous.mp4`.
|
|
150
|
+
|
|
151
|
+
Run a long render as one background job and stop that job by its process id. Do not use `pkill -f`, which also kills the shell that started it.
|
|
152
|
+
|
|
153
|
+
Then `reelkit lint` on the finished film and open every frame it names. It does not see lettering that sits on a photograph: look at those shots yourself.
|
|
154
|
+
|
|
155
|
+
If the render fails, read the error, fix the composition, and run `reelkit check` before rendering again. Give the user the path it prints.
|
|
156
|
+
|
|
157
|
+
Verify the finished film using `reference/delivery-review.md`; previews alone do not prove audio presence or synchronization. Report the deliverable path, the main creative choices and any verification limitation. Create extra platform variants only when requested or necessary for the stated destinations.
|
|
131
158
|
|
|
132
|
-
|
|
159
|
+
### 8. Deliver
|
|
160
|
+
`out/video.mp4` is the master. HyperFrames already writes it as `yuv420p` in TV range, tagged BT.709. It is still not the file to send: the profile and level are not pinned, and the size is not a chat copy. `reelkit export --preset iphone` is the full-quality phone copy, `reelkit export --preset phone-720` a lighter one, `reelkit export --preset chat` a small one. Send a file only when every row of its table passes. The checks are in `reference/delivery.md`. What to look at in the picture and the sound stays in `reference/delivery-review.md`.
|
|
133
161
|
|
|
134
|
-
|
|
162
|
+
## After the first render
|
|
163
|
+
A later version is read against the one before it. Restate the request as the frames it may touch, then `reelkit render --allow <frames>` (`reference/revisions.md`). A narration line that is wrong is replaced in that line only (`reference/voice-fixes.md`): do not record the rest of the film again, and do not pick a take the user asked to hear.
|
|
135
164
|
|
|
136
165
|
## The studio pane
|
|
137
166
|
|
|
@@ -152,5 +181,12 @@ The pane never changes the project by itself. Its buttons (Edit this scene, Chan
|
|
|
152
181
|
- Keep components driven by props, not hardcoded, so they can be reused: nothing of this user's text in them, and every new component starts with a one or two sentence comment saying what it shows and when to use it (`reference/component-authoring.md`).
|
|
153
182
|
- Resolve open creative decisions before committing to them. Preserve earlier approvals and explicit requests to edit or render; do not add a new approval step for work already authorized.
|
|
154
183
|
- Never say a video is done without having looked at its frames and checked that the file exists.
|
|
184
|
+
- Report what the checks actually said: the lint frames, the diff result, the export table. A command that exited is not the same as a check that passed.
|
|
185
|
+
- A revision changes only what was asked. `reelkit render --allow <frames>` is how that is proved. Do not tidy, retime or recolour anything outside the list.
|
|
186
|
+
- Never regenerate a voice the user did not ask you to touch, and never pick a take when they asked to hear the options.
|
|
187
|
+
- On-screen text never has vowel points. Pointing belongs in the voice request only.
|
|
188
|
+
- `reelkit template push` saves the whole project in the user's private files: the plan, the composition, everything under `assets/` (their own pictures, footage and recorded voice too) and the rendered video. Run it only when the user asks to save, back up or reuse the project, and say in one sentence what it sends. Only they can see it, on their templates page and with `reelkit template list`. `reelkit template clone <id> [folder]` unpacks one into a new project to continue or remix; `reelkit template delete <id>` removes it.
|
|
189
|
+
- A private project means `reelkit render --no-share` on every render, and neither `reelkit components share` nor `reelkit assets upload --share`.
|
|
190
|
+
- Never print an API key, a token, or the contents of a credentials file.
|
|
155
191
|
- If a command reports that a quota is used up, tell the user what ran out and when it resets. Do not work around it. A message that says to wait a minute or an hour ("Too many searches", "Too many uploads started") is a throttle, not a used-up quota: wait and retry once instead of stopping.
|
|
156
192
|
- Reply in the user's language.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delivery
|
|
3
|
+
description: Use when a render is to be sent to someone or opened on a phone - which export preset to use, what makes a file phone-safe, and the checks that must pass before saying it is delivered.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Delivering a film
|
|
7
|
+
|
|
8
|
+
`out/video.mp4` from this CLI is a master, not a file to send. HyperFrames already writes it as H.264 `yuv420p` in TV range, tagged BT.709: the encoder converts Chrome's full-range capture once, inside its own filter. It is still the wrong file to hand someone. The profile and level are not pinned, so a 1080p master can come out at level 5.0, which older phones refuse, and the size is not a copy that fits in a chat.
|
|
9
|
+
|
|
10
|
+
`reelkit export` makes the copy. It scales the short side, pins the profile and level 4.0, puts the index at the front, and converts full range to TV range only when the source is full range. A second conversion of a file that is already TV range lifts the blacks, so a master from this CLI is re-encoded without that step. The same command is what fixes an older or foreign file that really is full-range `yuvj420p` with no tags: that is the failure the presets exist for.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
reelkit export --preset iphone # 1080p, High profile level 4.0, CRF 18: the full-quality copy
|
|
14
|
+
reelkit export --preset phone-720 # 720p, High level 4.0, CRF 20 capped at 4 Mb/s: a lighter phone copy
|
|
15
|
+
reelkit export --preset chat # 720p, Main level 4.0, about 900 kb/s, AAC 128k: small enough for a chat (about 8 MB a minute)
|
|
16
|
+
reelkit export --preset chat --out ../film_v14.mp4
|
|
17
|
+
reelkit export other.mp4 --verify-only --preset chat # check a file someone else encoded
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`reelkit export --preset iphone`, `reelkit export --preset phone-720` and `reelkit export --preset chat` each encode and then verify. `reelkit export other.mp4 --verify-only --preset chat` only checks. The export is good only when every row passes:
|
|
21
|
+
|
|
22
|
+
| check | why |
|
|
23
|
+
|---|---|
|
|
24
|
+
| h264, the preset's profile, level at most 4.0 | a level left to the encoder comes out 5.0 at 1080p with many reference frames, which older phones refuse |
|
|
25
|
+
| yuv420p, range tv | full range is what plays washed out |
|
|
26
|
+
| matrix, transfer, primaries all bt709 | untagged transfer and primaries are guessed by the player |
|
|
27
|
+
| frame count equals the source | a dropped or added frame moves every caption after it |
|
|
28
|
+
| full decode with zero error lines | the file is whole, not cut off by a crash |
|
|
29
|
+
| moov before mdat | the file starts playing before it has fully downloaded |
|
|
30
|
+
| aac at 48000 Hz | what every phone plays, when the source has sound |
|
|
31
|
+
|
|
32
|
+
## If you encode by hand
|
|
33
|
+
- Convert the range exactly once. `in_range=full:out_range=tv` applied per segment and again at the join lifts black from #101010 to #1d1d1d. Skip it when the source is already TV range.
|
|
34
|
+
- `-color_trc` and `-color_primaries` on the command line are dropped when a `-vf` chain is present. Put `setparams=range=tv:color_primaries=bt709:color_trc=bt709:colorspace=bt709` at the end of the chain, and `-x264-params colorprim=bt709:transfer=bt709:colormatrix=bt709` as well.
|
|
35
|
+
- Pin `-profile:v` and `-level:v`; add `-movflags +faststart`; always `-nostdin -y` (an overwrite question hangs a scripted run).
|
|
36
|
+
- "Encode exactly like the last version" means reading the last file's settings (ffprobe, and the x264 options string stored in the file), not remembering them.
|
|
37
|
+
|
|
38
|
+
## Before you say "sent"
|
|
39
|
+
The file exists, its size is what you expect, the table passed, and you have looked at one frame from it. Never report a delivery from a command's exit code alone. What to look at in the picture and the sound is in `reference/delivery-review.md`.
|
|
@@ -24,10 +24,19 @@ Most broken Hebrew videos break in the same few ways. These rules are for text d
|
|
|
24
24
|
## How words enter
|
|
25
25
|
- **No masked reveals.** A Hebrew word uncovered by a moving edge (a clip, a wipe, an overflow-hidden box it rises out of) reads for a moment as a row of dashes or as a different word, because so much of the letter's identity is in its top. The frame edge counts as a mask too: a word sliding in from off screen shows a lone letter first.
|
|
26
26
|
- Enter whole words instead: a short rise (1 to 2 percent of the frame height) with opacity reaching 1 within two frames, or a scale down from slightly larger (at most 1.25 times) onto its place.
|
|
27
|
-
- At most one word in three gets the big scale entrance. The rest rise, or widen
|
|
27
|
+
- At most one word in three gets the big scale entrance. The rest rise, or widen by spacing the letters (one span each; see below), or slide a short distance from inside the frame.
|
|
28
28
|
- Letter by letter: each letter appears whole, starting from the rightmost (the first one read).
|
|
29
29
|
- This overrides the masked rise that `reference/motion-design.md` recommends for Latin headlines.
|
|
30
30
|
|
|
31
|
+
## Letter spacing that moves
|
|
32
|
+
CSS `letter-spacing` on right-to-left text reorders glyphs or clips the last one. Spread a word with one span per letter and grow the gap from the right, which is the first letter read. Do this only for a script whose letters do not join (Hebrew, not Arabic). When a reference shows the spread, measure the gap on every frame and use that list, normalised from 0 to 1. One measured curve is 0, .26, .45, .60, .72, .81, .89, .94, .97, 1 over ten frames.
|
|
33
|
+
|
|
34
|
+
## Vowel points
|
|
35
|
+
Niqqud is for the voice request, never for text on screen. A pointed word is a different picture from the one the user wrote, and `reelkit lint --no-video` reports it. Keep the user's wording unpointed in the composition.
|
|
36
|
+
|
|
37
|
+
## Edges
|
|
38
|
+
Check lettering on the rendered film, not only on the two preview frames. A title's words leave the frame together; one letter left behind reads as a fragment. A flash of a few frames can hide a caption that is already drifting off an edge: `reelkit lint` reports the cut lettering and the flash, with the frame numbers to open.
|
|
39
|
+
|
|
31
40
|
## Hyphens and punctuation
|
|
32
41
|
- For newly written display copy, prefer clear punctuation and breaks rather than joining words with decorative dashes. Preserve the user's exact supplied wording; do not silently remove a hyphen or punctuation from a quote.
|
|
33
42
|
- A prefix letter before a number or a Latin word ("ב-2026") is correct Hebrew, but on screen prefer wording that avoids it.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reference-recreation
|
|
3
|
+
description: Use when the user wants a film that follows an existing video closely (a recreation in another language, "exactly like this one") - measuring the reference frame by frame, keeping two clocks straight, scoring each scene, and taking clean shots from source footage.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Following a reference closely
|
|
7
|
+
|
|
8
|
+
`reference/references.md` covers learning from a video's structure. This file is for the harder brief: the user wants the same moves, cut for cut, with their own words. The right to use any of the reference's footage, music or words is theirs to settle; see `reference/rights.md`.
|
|
9
|
+
|
|
10
|
+
## Measure, do not guess
|
|
11
|
+
Every move you rebuild from memory of how it "looks" comes back as a revision. Before writing a shot:
|
|
12
|
+
1. Extract every frame of the stretch (`ffmpeg -i ref.mp4 -vf "select='between(n,232,245)'" -vsync 0 f_%04d.png`) and lay them on one sheet.
|
|
13
|
+
2. Write down, per frame: what is on screen, where (as a share of the width and height), how big, and what changed since the frame before. Hard cut or blend? One fat frame, a blurred frame, a flash?
|
|
14
|
+
3. For a move, measure the quantity that moves on every frame and normalise it to 0..1. That list is the easing; use it as written rather than naming a curve. Example, a word whose letters spread: the glyph size was constant, only the gap grew, with progress 0, .26, .45, .60, .72, .81, .89, .94, .97, 1 over ten frames, a heavy blurred frame before and a heavy frame after.
|
|
15
|
+
4. Compare your render to the reference on the same sheet, same frames, before showing anyone.
|
|
16
|
+
|
|
17
|
+
What looked like "the word scales up" was letter spacing; what looked like "bars close" was bars closing part-way and then a cut. The frames say which.
|
|
18
|
+
|
|
19
|
+
## Two clocks
|
|
20
|
+
If your film is shorter than the reference, it has two clocks: the reference's frames, where every shot was measured, and the film's frames, where the sound sits. Mixing them puts things a hundred frames out.
|
|
21
|
+
- Keep one table of the reference ranges the film keeps, in the composition (`const SEGS = [[152, 653], [782, 1028]]`, with `const HOOK_END = 179` if the opening has its own clock) or in `clock.json`: `{ "opening": 179, "keep": [[152, 653], [782, 1028]] }`.
|
|
22
|
+
- `reelkit clock 802 1581` prints where reference frames land in the film; `reelkit clock --film 623` goes the other way. Use it every time; do not do the subtraction in your head.
|
|
23
|
+
- Write shots on the reference clock and let one helper place them. A shot that lies in a cut range, or in the wrong scene section, is silently never drawn: `reelkit lint --no-video` lists both.
|
|
24
|
+
- Audio is placed in film seconds. A caption frame in the code and a word time in the voice are on different clocks until you convert one.
|
|
25
|
+
|
|
26
|
+
## Score every scene
|
|
27
|
+
After each preview, score each scene out of 100 against the reference on the same things every time: timing (cuts and entrances within a frame or two), layout (position and size), type (face, weight, direction), motion (the measured curve), and legibility. Keep the table in the project. A scene under 80 is not done; say which part is short and by how much. Have a separate reviewer score from the frames alone when you can: whoever built a scene scores it high.
|
|
28
|
+
|
|
29
|
+
## Clean shots from source footage
|
|
30
|
+
When the user supplies footage to cut from (their own, or footage they have cleared):
|
|
31
|
+
1. Detect the cuts (`reelkit ref analyze`, or ffmpeg's scene filter) and make a contact sheet with one frame per shot, labelled with frame ranges.
|
|
32
|
+
2. Reject frames with burned-in captions, handles or logos; crop the rest to the clean part and note the crop. A shot that is clean for five frames is a five-frame clip; play it slowly rather than use dirty frames.
|
|
33
|
+
3. Write a `SHOTS.md` beside the clips: file, source frames, crop, what it shows, where it may be used. Write the source and licence into the asset log the moment the file enters the project.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: revisions
|
|
3
|
+
description: Use for every change to a film that has already been shown to the user - how to change only what was asked, prove it with a frame diff, and keep each version recoverable.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Revising a film
|
|
7
|
+
|
|
8
|
+
After the first render the user reads every later version against the one before. Their request is a list; anything else that moves is a defect, even if you think it is better. "Change only what I asked" is the default contract of every revision, whether or not they say it.
|
|
9
|
+
|
|
10
|
+
## Before touching anything
|
|
11
|
+
1. Restate the request as a numbered list of changes, each with the frames it may touch. If an item is ambiguous or two items conflict (a demo they approved shows one thing, the written instruction another), build what is written and say so in the report; do not pick silently.
|
|
12
|
+
2. Save the state you are leaving: copy `src/Video.tsx` (and any file you will edit) to a `backup/` folder under a version name, and keep the current render. `reelkit render` copies the render it replaces to `out/previous.mp4`; copy that somewhere with a version name if you need more than one step back.
|
|
13
|
+
3. Work out the allowed frames. A change to a shot is its frames; a change to a voice line is its seconds; a retimed caption is a few frames either side. For a film on a reference clock, `reelkit clock <frames>` converts (see `reference/reference-recreation.md`).
|
|
14
|
+
|
|
15
|
+
## Making the change
|
|
16
|
+
- Edit the smallest thing that does it. A wrapper that returns its children untouched when it has nothing to do (scale 1, blur 0) keeps every other frame identical; a restructure does not.
|
|
17
|
+
- Do not tidy, retime, recolour or "also fix" anything outside the list. Write what you noticed into the report instead and let the user decide.
|
|
18
|
+
- If the user limits renders ("render once"), do every check you can before it: `reelkit check`, `reelkit lint --no-video`, the arithmetic of positions and frames. Removing something can expose what it hid (a flash that covered a caption drifting off the edge): look at what becomes visible.
|
|
19
|
+
|
|
20
|
+
## Proving it
|
|
21
|
+
```
|
|
22
|
+
reelkit render --no-share --allow 623-679,1637-1644 --allow-audio 8.4s-10.5s
|
|
23
|
+
```
|
|
24
|
+
`--allow` compares the new render with the one it replaced and fails when a frame outside the ranges changed (mean grey difference above 1.0 at 320x180), or when the sound's level changed outside them. Or after the fact: `reelkit diff out/previous.mp4 out/video.mp4 --allow ...`.
|
|
25
|
+
|
|
26
|
+
Read the result:
|
|
27
|
+
- **Changed outside the allowed ranges**: you broke the contract. Find the cause before anything else.
|
|
28
|
+
- **Allowed but identical**: a change that was asked for is not in the film.
|
|
29
|
+
- **Largest difference on an unchanged frame**: small values next to a changed range (0.2 to 0.6) are the video codec carrying the change into neighbouring frames, not your code. State the number in the report.
|
|
30
|
+
- A changed range wider than you expected is information, not noise (a word that used to linger changes every frame it lingered on). Report it with the reason.
|
|
31
|
+
|
|
32
|
+
## Reporting
|
|
33
|
+
Lead with whether every change is in and whether anything else moved. Then one line per requested change with the frames you checked, then the diff result, then anything you saw and did not touch. Say plainly what you did not do and why.
|
|
34
|
+
|
|
35
|
+
## Versions
|
|
36
|
+
Name outputs by version and never overwrite a delivered file without keeping it (`*_prev`). Keep, per version: the composition source, the render, the voice track if it changed, and one line in a log saying what went in. A box can reboot mid-render: after writing anything that matters, check the file exists and reads back (`reelkit render` and `reelkit export` do this for their own output).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rights
|
|
3
|
+
description: Use whenever a file that neither you nor the user made enters a project (a clip from a video, a photo, a logo, music) and before anything is shared with the library - what to log, and what may and may not be shared.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rights and the asset log
|
|
7
|
+
|
|
8
|
+
Every project keeps one `ASSET_SOURCES.md` at its root. A file gets its row when it enters the project, not at the end: by the end nobody remembers where the fourth photo came from.
|
|
9
|
+
|
|
10
|
+
| file | used in | what it shows | source | author | licence |
|
|
11
|
+
|---|---|---|---|---|---|
|
|
12
|
+
|
|
13
|
+
- **Source** is a link that opens the page the file came from, or "generated here with <tool>", or "the user's own".
|
|
14
|
+
- **Licence** is the name on that page (CC0, CC BY 4.0, public domain) or, when there is none, "third-party, not licensed, local use only; needs clearance before any posting". Write that sentence in full. It is what stops the file being posted later.
|
|
15
|
+
- A CC BY or CC BY-SA file needs its author, the licence, the link and "modified: <what you did>" wherever it is shown or shared.
|
|
16
|
+
- A photo of an identifiable person carries more than copyright. A free licence on the picture does not cover the person in it. Say so in the row.
|
|
17
|
+
- Brand logos and product screenshots are their owners'. Use them only as the user instructs, and never share them.
|
|
18
|
+
|
|
19
|
+
## What may be shared with the library
|
|
20
|
+
Sharing is publishing: there is no unshare. Only when the user has said yes, in this conversation, to that item:
|
|
21
|
+
- components you wrote that carry none of the user's text, names, numbers or brand;
|
|
22
|
+
- media you generated here with code, or that is CC0 or public domain, with no identifiable person and no logo;
|
|
23
|
+
- CC BY or CC BY-SA media only with the full attribution in its description.
|
|
24
|
+
|
|
25
|
+
Never: clips cut from someone else's video, music stems, film or broadcast footage, photos of real people, portraits made to look like a real person, logos, screenshots of products, the user's voice or narration, anything whose row says "not licensed".
|
|
26
|
+
|
|
27
|
+
If the user said the project is private (`reelkit init --private`, `--no-share`, `REELKIT_NO_SHARE=1`), nothing leaves the machine: render with `--no-share` every time, and do not run `reelkit components share` or `reelkit assets upload --share`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: voice-fixes
|
|
3
|
+
description: Use when a narration line is wrong (a mispronounced word, the wrong stress, an odd melody) or the user wants a different take - checking takes by phonemes and pitch, letting the user choose from a numbered options video, splicing, and replacing one line inside the voice track.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fixing a line of narration
|
|
7
|
+
|
|
8
|
+
`reelkit assets voiceover` records a film's narration. This file is for what happens after: one word is wrong, and the rest must not move. The scripts ship with the package: `reelkit tools` prints their folder and each one's `--help` line (the folder's `README.md` gives the order). They are plain Python and generate nothing themselves.
|
|
9
|
+
|
|
10
|
+
## Find out what is wrong, in sounds
|
|
11
|
+
Speech-to-text writes the word it expects, so a transcript that reads correctly proves little. Check what was said:
|
|
12
|
+
- `phonemes.py take.wav --want "m e a f j e n i m"`: the phonemes of the take, and whether the wanted ones are there.
|
|
13
|
+
- `pitch_check.py take.wav`: the range, the largest step between frames (a ratio near 2 is an octave jump) and the pitch over the last 0.6 s (rising: it sounds like a question).
|
|
14
|
+
- Duration: a word that takes 0.8 s when it is naturally 0.45 s has been stretched.
|
|
15
|
+
Check the clean voice, and say so if you could only check the mix: music degrades all three.
|
|
16
|
+
|
|
17
|
+
## Get better takes
|
|
18
|
+
1. `credits.py --takes 6 --chars 28 --check` first. If the balance is short, stop and tell the user; half a set of takes is worse than none.
|
|
19
|
+
2. Change the spelling, not just the seed. For Hebrew: pointed text (niqqud, from a pointing tool, hand-checked), unpointed, a hyphen at the syllable break, a different letter with the same sound. Pointing a whole sentence can change its neighbours; sometimes the unpointed line is the one that reads right. Two or three spellings, two takes each.
|
|
20
|
+
3. Run the checks on every take and drop the failures before anyone listens.
|
|
21
|
+
4. Speech-to-speech from the user's own recording gives their stress and melody in the film's voice. Use it as recorded: speeding it up or shifting its pitch afterwards is audible, and the phoneme and duration checks show it.
|
|
22
|
+
|
|
23
|
+
## Let the user choose
|
|
24
|
+
`ab_video.py --title "<the line>" --out options.mp4 current.mp3:"in the film now" a.mp3:"new spelling, take 1" ...` makes one phone-safe video that plays each take under a big number with 0.7 s between them, and a key file. Put the take in use first. Send the video; wait for a number. Never pick for them when they asked to hear options, and never regenerate a voice they did not ask you to touch.
|
|
25
|
+
|
|
26
|
+
## Put the choice in
|
|
27
|
+
- If only part of a take is wanted, `splice.py`: it cuts at the quietest point near where you ask (a pause, the closure of a stop, just before a fricative), at a zero crossing, with a 10 ms crossfade, and reports the level and pitch either side and the exact length.
|
|
28
|
+
- The film's voice track is rarely the line files laid end to end. `place_line.py locate track.wav line.mp3 --near <seconds>` shows where each part of the line sits; pauses were often shortened when the track was built, so one line can be in two pieces with different offsets.
|
|
29
|
+
- `place_line.py replace ... --at --until --from --match-old`: overwrite only the stretch that changed, matched to the level of what it replaces, every other sample identical. It reports the margin to the next line. If the new take is longer than its slot, trim a pause inside the take; never move later lines.
|
|
30
|
+
- Update the line's record (which take, where it sits, the margin) and keep the old file.
|
|
31
|
+
|
|
32
|
+
## Then the captions
|
|
33
|
+
Measure where the changed word now starts (`onsets.py`, see `reference/voice-sync.md`). Move a caption only if its word moved, and by the same number of frames. Finish with `reelkit diff old new --allow <caption frames> --allow-audio <the line's seconds>`.
|
|
@@ -106,3 +106,7 @@ export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
|
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
`reelkit preview` then shows, for this film, four word frames (`thousand ▸ hook`, `tasks ▸ list`, `deadlines ▸ list`, `energy ▸ list`): the number is whole in the first, and each row is already on screen in the others.
|
|
109
|
+
|
|
110
|
+
## Measured onsets
|
|
111
|
+
|
|
112
|
+
Word times in `manifest.json` come from the recording that was made for the plan. After a line is replaced, those times are no longer the file. `reelkit tools` prints the folder of `onsets.py`, which measures where a word now starts in the new take. Place the caption one or two frames before that onset, and move a caption only when its word moved, by the same number of frames. A film that also follows a reference has two clocks: convert with `reelkit clock` before writing the frame (`reference/reference-recreation.md`). Finish with `reelkit diff old new --allow <frames>` so only that caption, and that stretch of sound, changed (`reference/revisions.md`).
|
package/src/cli.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { templateClone, templateDelete, templateList, templatePush } from "./commands/template";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { ApiFailure } from "./api/client";
|
|
3
4
|
import { assetsGenClip, assetsGenImage, assetsGenSvg, assetsLayers, assetsPull, assetsSearch, assetsUpload, assetsVoiceover, assetsVoices } from "./commands/assets";
|
|
4
5
|
import { authLogin, authLogout, whoami } from "./commands/auth";
|
|
5
6
|
import { check, preview, render, soundCommand } from "./commands/build";
|
|
7
|
+
import { clockCommand } from "./commands/clock";
|
|
6
8
|
import { componentsShare } from "./commands/components";
|
|
9
|
+
import { diffCommand } from "./commands/diff";
|
|
10
|
+
import { exportCommand } from "./commands/export";
|
|
11
|
+
import { lintCommand } from "./commands/lint";
|
|
7
12
|
import { init } from "./commands/init";
|
|
8
13
|
import { install } from "./commands/install";
|
|
9
14
|
import { openInBrowser } from "./open";
|
|
10
15
|
import { planCheck } from "./commands/plan";
|
|
11
16
|
import { refAnalyze, refAudio, refDownload, refList } from "./commands/ref";
|
|
12
17
|
import type { Ctx, Result } from "./context";
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
18
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
14
20
|
|
|
15
21
|
const ctx: Ctx = {
|
|
16
22
|
cwd: process.cwd(),
|
|
@@ -117,10 +123,42 @@ program.command("check").description("Check the composition in src/ without rend
|
|
|
117
123
|
program.command("install").description("Install the Reelkit skill into your coding agents").option("--agent <id>", "claude, codex, cursor, gemini, agents or all").option("--force", "reinstall even when up to date").action(run((ctx, opts) => install(ctx, opts)));
|
|
118
124
|
program.command("preview").description("Render preview frames into out/preview/: two per scene (at 30% and 90%) and the frames either side of each scene change, with a report of what carries across").action(run((ctx) => preview(ctx)));
|
|
119
125
|
program.command("render").description("Render the video to out/video.mp4; new components written in this project are then shared with the library for review").option("--no-share", "do not share new components with the library after this render (also: REELKIT_NO_SHARE=1)")
|
|
120
|
-
.
|
|
126
|
+
.option("--chunk [frames]", "render in pieces (default 300 frames) that are kept if the render is interrupted; run the same command again to continue from them")
|
|
127
|
+
.option("--guard <film>", "after rendering, compare with this film (or `previous`, the render this one replaces) and fail if anything outside --allow changed")
|
|
128
|
+
.option("--allow <ranges>", "frames that may change, such as 623-679,1432-1436 (seconds as 20.7s-22.6s); implies --guard previous")
|
|
129
|
+
.option("--allow-audio <ranges>", "where the sound may change, if not the same as --allow").option("--threshold <n>", "mean grey-level difference above which a frame counts as changed (default 1.0)")
|
|
130
|
+
.action(run((ctx, opts: { share?: boolean; chunk?: string | boolean; guard?: string; allow?: string; allowAudio?: string; threshold?: string }) => render(ctx, { noShare: opts.share === false, chunk: opts.chunk === undefined ? undefined : opts.chunk === true ? true : Number(opts.chunk), guard: opts.guard, allow: opts.allow, allowAudio: opts.allowAudio, threshold: opts.threshold })));
|
|
131
|
+
program.command("export [file]").description("Encode a finished render (default out/video.mp4) for a phone and verify it: one full-to-TV range conversion when the source is full range, yuv420p tagged BT.709, profile and level pinned, AAC 48 kHz, index at the front; then a pass/fail table")
|
|
132
|
+
.option("--preset <id>", "iphone (1080p High, default), phone-720 (720p High, capped at 4 Mb/s) or chat (720p Main, about 900 kb/s)").option("--out <path>", "where to write it (default: beside the input, named <input>.<preset>.mp4)")
|
|
133
|
+
.option("--list", "list the presets").option("--verify-only", "do not encode: check the given file against the preset")
|
|
134
|
+
.action(run((ctx, file: string | undefined, opts: { preset?: string; out?: string; list?: boolean; verifyOnly?: boolean }) => exportCommand(ctx, file, opts)));
|
|
135
|
+
program.command("diff <old> <new>").description("Compare two renders frame by frame (and their sound): lists what changed, and with --allow fails when anything else did")
|
|
136
|
+
.option("--allow <ranges>", "frames that may change, such as 623-679,1432-1436 (seconds as 20.7s-22.6s)").option("--allow-audio <ranges>", "where the sound may change, if not the same as --allow")
|
|
137
|
+
.option("--threshold <n>", "mean grey-level difference (at 320x180) above which a frame counts as changed (default 1.0)").option("--audio-threshold <db>", "level difference of a 0.1 s window above which the sound counts as changed (default 1.5)").option("--no-audio", "compare the picture only")
|
|
138
|
+
.action(run((ctx, oldFile: string, newFile: string, opts: { allow?: string; allowAudio?: string; threshold?: string; audioThreshold?: string; audio?: boolean }) => diffCommand(ctx, oldFile, newFile, opts)));
|
|
139
|
+
program.command("lint [file]").description("Advice from the source and from the rendered film (default out/video.mp4): lettering cut by an edge of the frame, flashes that cover a held shot, right-to-left text with no direction, vowel points in on-screen text, sequences and clock shots that are never drawn")
|
|
140
|
+
.option("--no-video", "check the source only").option("--min-frames <n>", "how many frames in a row lettering must be cut before it is reported (default 4)").option("--from <frame>", "first frame to read").option("--to <frame>", "last frame to read").option("--allow-niqqud", "do not report vowel points")
|
|
141
|
+
.action(run((ctx, file: string | undefined, opts: { video?: boolean; minFrames?: string; from?: string; to?: string; allowNiqqud?: boolean }) => lintCommand(ctx, file, opts)));
|
|
142
|
+
program.command("clock [frames...]").description("For a film cut down from a longer reference: where a reference frame lands in the film (or, with --film, which reference frame a film frame shows). The clock comes from --keep, clock.json or SEGS in src/Video.tsx")
|
|
143
|
+
.option("--keep <ranges>", "the reference ranges the film keeps, end exclusive, such as 152-653,782-1028").option("--opening <frames>", "frames at the start of the film on their own clock (default 0)").option("--film", "the frames given are film frames").option("--fps <n>", "frames a second, for the times printed (default 30)")
|
|
144
|
+
.action(run((ctx, frames: string[], opts: { keep?: string; opening?: string; film?: boolean; fps?: string }) => clockCommand(ctx, frames, opts)));
|
|
121
145
|
program.command("sound [file]").description("Measure the sound of a finished film against its picture: hits, swells and how many picture changes have a sound (default out/video.mp4)")
|
|
122
146
|
.option("--detail", "list every picture change with the nearest hit and its distance, every swell (start, peak, length) and the first sound")
|
|
123
147
|
.action(run((ctx, file: string | undefined, o: { detail?: boolean }) => soundCommand(ctx, file, { detail: o.detail })));
|
|
148
|
+
program.command("tools").description("Print where the voice scripts that ship with Reelkit are (tools/voice: options video, splice, place a line, onsets, phonemes, pitch, credits)")
|
|
149
|
+
.action(run(async () => {
|
|
150
|
+
const dir = fileURLToPath(new URL("../tools/voice", import.meta.url));
|
|
151
|
+
const names = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("_")).sort() : [];
|
|
152
|
+
return { ok: names.length > 0, data: { dir, scripts: names }, summary: names.length ? `${dir}\n${names.map((n) => ` python3 ${dir}/${n} --help`).join("\n")}\nRead ${dir}/README.md for the order to use them in.` : "The voice scripts are missing from this installation. Reinstall reelkit." };
|
|
153
|
+
}));
|
|
154
|
+
const template = program.command("template").description("Whole projects kept in your private files, to continue later or to start a new video from");
|
|
155
|
+
template.command("push").description("Save this project as a template in your private files: the plan, the composition, everything under assets/ (your own files too), the rendered video when there is one, and a picture of each scene. Only you can see it")
|
|
156
|
+
.option("--name <name>", "what to call the template (default: the project's name)")
|
|
157
|
+
.action(run((ctx, opts: { name?: string }) => templatePush(ctx, opts)));
|
|
158
|
+
template.command("list").description("List your templates").action(run((ctx) => templateList(ctx)));
|
|
159
|
+
template.command("clone <id> [folder]").description("Unpack one of your templates into a new project folder, as a copy of its own to continue or to remix")
|
|
160
|
+
.action(run((ctx, id: string, folder: string | undefined) => templateClone(ctx, id, folder)));
|
|
161
|
+
template.command("delete <id>").description("Delete one of your templates from your private files").action(run((ctx, id: string) => templateDelete(ctx, id)));
|
|
124
162
|
const components = program.command("components").description("Components written in this project");
|
|
125
163
|
components.command("share [names...]").description("Share components written in this project with the library for review (all of them with no name): their source, a description and an example are sent, nothing else")
|
|
126
164
|
.option("--describe <text>", "what it shows and when to use it (one component)").option("--example <jsx>", "an example use with plain values, such as '<StatRing value={42} />' (one component)").option("--tags <tags>", "comma-separated tags, 3 to 6 (one component)")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// A film cut down from a longer reference keeps two clocks: the reference's frames (where every shot was measured) and the film's frames (where the
|
|
2
|
+
// sound sits). This is the one place that converts between them. Pure.
|
|
3
|
+
|
|
4
|
+
export type Clock = {
|
|
5
|
+
// Frames at the start of the film that run on their own clock, 0 to opening (0 when the film starts inside the first kept range).
|
|
6
|
+
opening: number;
|
|
7
|
+
// The ranges of the reference that the film keeps, in film order, end exclusive.
|
|
8
|
+
keep: [number, number][];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Piece = { refFrom: number; refTo: number; filmFrom: number; filmTo: number };
|
|
12
|
+
|
|
13
|
+
// Where each kept range lands in the film.
|
|
14
|
+
export function pieces(clock: Clock): Piece[] {
|
|
15
|
+
let n = clock.opening;
|
|
16
|
+
return clock.keep.map(([a, b]) => { const p = { refFrom: a, refTo: b, filmFrom: n, filmTo: n + (b - a) }; n += b - a; return p; });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const filmLength = (clock: Clock): number => clock.opening + clock.keep.reduce((s, [a, b]) => s + (b - a), 0);
|
|
20
|
+
|
|
21
|
+
// Reference frame to film frame; undefined inside a range that was cut out.
|
|
22
|
+
export function toFilm(clock: Clock, ref: number): number | undefined {
|
|
23
|
+
for (const p of pieces(clock)) if (ref >= p.refFrom && ref < p.refTo) return p.filmFrom + (ref - p.refFrom);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Film frame to reference frame; the opening is on its own clock and has no reference frame.
|
|
28
|
+
export function toRef(clock: Clock, film: number): number | undefined {
|
|
29
|
+
for (const p of pieces(clock)) if (film >= p.filmFrom && film < p.filmTo) return p.refFrom + (film - p.filmFrom);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The film ranges a reference range [a, b) is shown in: none when it lies wholly in a cut, several when it spans one.
|
|
34
|
+
export function filmRanges(clock: Clock, a: number, b: number): { from: number; to: number }[] {
|
|
35
|
+
return pieces(clock).flatMap((p) => { const s = Math.max(a, p.refFrom), e = Math.min(b, p.refTo); return e > s ? [{ from: p.filmFrom + (s - p.refFrom), to: p.filmFrom + (e - p.refFrom) }] : []; });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateClock(clock: Clock): string[] {
|
|
39
|
+
const problems: string[] = [];
|
|
40
|
+
if (!Number.isInteger(clock.opening) || clock.opening < 0) problems.push("opening must be a whole number of frames, 0 or more.");
|
|
41
|
+
clock.keep.forEach(([a, b], i) => { if (!(Number.isInteger(a) && Number.isInteger(b) && b > a)) problems.push(`keep range ${i + 1} (${a}-${b}) must be two whole frames with the end after the start.`); });
|
|
42
|
+
return problems;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// "152-653,782-1028" as kept ranges (end exclusive).
|
|
46
|
+
export function parseKeep(text: string): [number, number][] {
|
|
47
|
+
return text.split(",").map((part) => {
|
|
48
|
+
const m = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(part);
|
|
49
|
+
if (!m) throw new Error(`"${part.trim()}" is not a range. Write the kept ranges as 152-653,782-1028.`);
|
|
50
|
+
return [Number(m[1]), Number(m[2])] as [number, number];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A clock written in a composition as literal constants: `const SEGS = [[152, 653], [782, 1028]]` and, optionally, `const HOOK_END = 179`.
|
|
55
|
+
export function clockFromSource(source: string): Clock | undefined {
|
|
56
|
+
const segs = /\bconst\s+(?:SEGS|KEEP_RANGES|KEPT)\b[^=]*=\s*(\[\s*(?:\[\s*\d+\s*,\s*\d+\s*\]\s*,?\s*)+\])/.exec(source);
|
|
57
|
+
if (!segs) return undefined;
|
|
58
|
+
const keep = [...segs[1]!.matchAll(/\[\s*(\d+)\s*,\s*(\d+)\s*\]/g)].map((m) => [Number(m[1]), Number(m[2])] as [number, number]);
|
|
59
|
+
const opening = /\bconst\s+(?:HOOK_END|OPENING)\b[^=]*=\s*(\d+)/.exec(source);
|
|
60
|
+
return { opening: opening ? Number(opening[1]) : 0, keep };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fmt = (frames: number, fps: number) => `${frames} (${(frames / fps).toFixed(3)} s)`;
|
|
64
|
+
|
|
65
|
+
// One line per value asked about.
|
|
66
|
+
export function describe(clock: Clock, values: number[], from: "ref" | "film", fps: number): string[] {
|
|
67
|
+
return values.map((v) => {
|
|
68
|
+
if (from === "ref") {
|
|
69
|
+
const f = toFilm(clock, v);
|
|
70
|
+
return f === undefined ? `reference ${v}: cut out, not in the film${v < (clock.keep[0]?.[0] ?? 0) ? " (before the first kept range; the opening has its own clock)" : ""}` : `reference ${v} -> film ${fmt(f, fps)}`;
|
|
71
|
+
}
|
|
72
|
+
if (v < clock.opening) return `film ${fmt(v, fps)}: in the opening, which runs on its own clock (opening frame ${v})`;
|
|
73
|
+
const r = toRef(clock, v);
|
|
74
|
+
return r === undefined ? `film ${v}: after the end of the film (${filmLength(clock)} frames)` : `film ${fmt(v, fps)} -> reference ${r}`;
|
|
75
|
+
});
|
|
76
|
+
}
|