demobite 1.0.9 → 1.2.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/README.md +125 -79
- package/launcher/index.mjs +52 -9
- package/package.json +7 -3
- package/recorder/scripts/mux.mjs +4 -11
- package/recorder/scripts/post.sh +6 -3
- package/recorder/scripts/tts.mjs +3 -9
- package/scripts/calibrate.mjs +4 -2
- package/scripts/check-aliases.mjs +42 -0
- package/scripts/media-tools.mjs +93 -0
- package/scripts/trim.mjs +28 -13
- package/skill/SKILL.md +84 -3
- package/skill/scripts/briefs.mjs +164 -0
- package/skill/scripts/cleanup.mjs +104 -0
- package/skill/scripts/manifest.mjs +3 -2
- package/skill/scripts/stage-wait.mjs +87 -0
- package/skill/scripts/status.mjs +72 -0
- package/skill/scripts/upload.mjs +75 -79
- package/skill/scripts/vocab.mjs +88 -0
package/README.md
CHANGED
|
@@ -1,115 +1,161 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DemoBites Agentic Recorder
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
and
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
**Ask your coding agent to record your product. Edit the result. Download the MP4.**
|
|
4
|
+
|
|
5
|
+
Your agent drives a real browser on your machine and records the flow you describe. Connect a free DemoBites account to turn the take into an editable video, with text-to-speech, zooms, captions, and MP4 export included.
|
|
6
|
+
|
|
7
|
+
**Sign in to your app locally. You do not hand DemoBites your app password or upload your recording browser profile.**
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx demobite
|
|
10
|
+
npx demobite@latest
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
**Free Studio · Text-to-speech included · Unlimited exports · No watermark · No credit card**
|
|
14
|
+
|
|
15
|
+
[Website](https://agentic-recorder.com/) · [npm](https://www.npmjs.com/package/demobite) · [Report an issue](https://github.com/demobites/agentic-recorder/issues)
|
|
16
|
+
|
|
17
|
+
## See the workflow
|
|
18
|
+
|
|
19
|
+
https://github.com/user-attachments/assets/18293b85-82e2-475f-8d94-543f1f9bbc1d
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
An illustrated walkthrough of the flow, with waiting time compressed.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
## Start with your own agent
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
(register the management MCP with your agent) · `npx demobite logout`.
|
|
26
|
-
Anything public — publishing a release, taking a language offline — always
|
|
27
|
-
shows you a preview to approve first. Managing by agent requires the Grow plan;
|
|
28
|
-
recording works on every plan.
|
|
25
|
+
You need a coding agent that can read skill instructions, run terminal commands, and control a local browser. A browser-only chat session cannot run this local workflow.
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
for your approval inside DemoBites. You approve in the product; the platform
|
|
32
|
-
does the rest.
|
|
27
|
+
Before installing:
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
- **Node.js 18 or newer** and npm.
|
|
30
|
+
- **Google Chrome**, recommended. The recorder also supports Playwright's Chromium.
|
|
31
|
+
- A **free DemoBites account** for the Studio workflow below.
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
MCP** — a control plane your agent uses to run your Update Center and Demo
|
|
38
|
-
Center. Your customers' agents read your centers; your agent runs them.
|
|
33
|
+
Run these commands from your project directory:
|
|
39
34
|
|
|
40
35
|
```bash
|
|
41
|
-
|
|
36
|
+
# Install the skill and check your setup.
|
|
37
|
+
npx demobite@latest
|
|
38
|
+
|
|
39
|
+
# Connect this project to your DemoBites workspace.
|
|
40
|
+
npx demobite login
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
The installer installs Playwright, ffmpeg and ffprobe beside the skill if they are not already there. It uses your own ffmpeg when a compatible build is on your PATH. It checks for Chrome and downloads Chromium on the first take if Chrome is missing.
|
|
44
|
+
|
|
45
|
+
To connect, open the link printed in your terminal and approve the connection in your browser. If you are signed out of DemoBites, sign in there first. You do not paste a password into the terminal.
|
|
46
|
+
|
|
47
|
+
Then ask your agent:
|
|
48
|
+
|
|
49
|
+
> Record a demo of inviting a teammate in my app at http://localhost:3000. Open Team settings, invite alex@example.com as an Admin, and show the confirmation. Show me the storyboard before recording.
|
|
50
|
+
|
|
51
|
+
Use your own app URL and a demo account. The agent performs real actions in your app, including sending an invitation if that is part of the approved flow.
|
|
52
|
+
|
|
53
|
+
### Which agent?
|
|
54
|
+
|
|
55
|
+
The installer currently places the skill at `~/.claude/skills/agentic-recorder/`, the Claude Code skill directory.
|
|
56
|
+
|
|
57
|
+
For Cursor or Codex, make the [recorder instructions](skill/SKILL.md) available to your agent and ensure it can run the installed scripts. The launcher currently does not install into separate Cursor or Codex skill directories. Connecting an MCP client alone does not install the recorder skill.
|
|
58
|
+
|
|
59
|
+
## From prompt to finished video
|
|
60
|
+
|
|
61
|
+
1. **Describe the flow.** Give your agent the app URL and what you want to demonstrate.
|
|
62
|
+
2. **Approve the storyboard.** Review the steps and estimated length before filming. If your app needs a login, sign in yourself in the recorder's local browser window.
|
|
63
|
+
3. **Let it record.** The agent checks the flow and films it. Recording normally runs in the background.
|
|
64
|
+
4. **Review the raw take.** Open the review link printed in the terminal. Check the footage, then approve it to create a Bite, an editable video project.
|
|
65
|
+
5. **Tweak and download.** In the Studio, change a sentence, generate its voice, adjust the zooms or timing, and export your MP4.
|
|
66
|
+
|
|
67
|
+
The storyboard's narration is a starting point. DemoBites rewrites and fits it to the footage; you can edit the final wording in the Studio.
|
|
68
|
+
|
|
69
|
+
## What connecting to DemoBites adds
|
|
70
|
+
|
|
71
|
+
The recorder supplies footage, action timings, cursor movements, and camera targets. The Studio uses that information to create a video you can keep editing.
|
|
72
|
+
|
|
73
|
+
| Included in the free Studio | What you can do |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| Script and text-to-speech | Edit the words and generate narration without recording your own voice. |
|
|
76
|
+
| Camera and cursor editing | Adjust automatic zooms, cursor appearance, and timing. |
|
|
77
|
+
| Video finishing | Trim the take, edit captions, and add your background, branding, intro, and outro. |
|
|
78
|
+
| MP4 export | Download Full HD video with narration and no DemoBites watermark. |
|
|
48
79
|
|
|
49
|
-
|
|
80
|
+
**The free Studio holds up to 8 Bites at a time, with unlimited exports. Invite three people who sign up and the Studio becomes unlimited, still free.** It has no trial clock and requires no credit card. You bring your own coding agent; its subscription or model usage is separate. An ElevenLabs key is optional, not required for the Studio's included text-to-speech.
|
|
50
81
|
|
|
51
|
-
|
|
82
|
+
You can use the exported video in your docs, website, release notes, or wherever you need it. Paid publishing and management features are optional. See [current plan details](https://www.demobites.com/pricing).
|
|
52
83
|
|
|
53
|
-
|
|
84
|
+
## One prompt, one result
|
|
54
85
|
|
|
55
|
-
|
|
56
|
-
|---|---|
|
|
57
|
-
| Connect | `get_started` · `connect_demobites` · `check_connection` · `get_recording_options` |
|
|
58
|
-
| Read | `get_status` · `list_bites` · `list_releases` · `get_release` · `list_languages` |
|
|
59
|
-
| Draft & edit | `create_release` · `update_release` · `assign_bites_to_release` · `update_center_settings` · `add_language` · `update_demo_center` |
|
|
60
|
-
| Publish — with your approval | `publish_release` · `unpublish_release` · `remove_language` · `publish_demo_center` |
|
|
86
|
+
> Record a demo of Funnels: where customers drop off, which channels work. Show the sidebar and graphs.
|
|
61
87
|
|
|
62
|
-
The server describes itself: any client's `tools/list` returns every tool with
|
|
63
|
-
its full input schema, straight from the running code.
|
|
64
88
|
|
|
65
|
-
### You stay in charge
|
|
66
89
|
|
|
67
|
-
|
|
68
|
-
anything that touches a **public** page is two-phase: the tool returns a
|
|
69
|
-
human-readable preview plus a single-use confirmation token, your agent shows
|
|
70
|
-
you the preview, and only your go-ahead executes it. Tokens expire in ten
|
|
71
|
-
minutes, are bound to the exact action and arguments, and every management
|
|
72
|
-
call lands in an audit log.
|
|
90
|
+
https://github.com/user-attachments/assets/02eb7d3e-fe74-4c0d-b532-e16bfc3fa614
|
|
73
91
|
|
|
74
|
-
### Plans
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
|
|
94
|
+
Recorded by the agent, then edited and narrated in the free Studio. This prompt assumes the agent already knows the app URL and can access the Funnels page.
|
|
78
95
|
|
|
79
|
-
|
|
96
|
+
## Your app login stays local
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
**You do not need to hand DemoBites your app password to record a signed-in flow.** Sign in yourself in the recorder's browser on your machine.
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
|---|---|
|
|
85
|
-
| `launcher/` | The `npx demobite` entry — environment checks, skill install, login |
|
|
86
|
-
| `skill/` | The DemoBites recorder skill for coding agents (staging, preview, approval flow) |
|
|
87
|
-
| `recorder/` | The open recorder — same filming engine, no account, ends at a polished `demo.mp4` |
|
|
88
|
-
| `scripts/` | The shared engine: filming, clock calibration, cutting |
|
|
100
|
+
The recorder uses a separate, persistent Chrome profile at `.recorder/profile/`. Your app session is saved there for later takes. It does not automatically inherit the sessions in your everyday Chrome profile.
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
| Data | Where it goes |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| Your app's browser session | Stays in the local recording profile; the standard local workflow does not upload that profile to DemoBites. |
|
|
105
|
+
| DemoBites recorder key | Saved in `.recorder/config.json` and used to authenticate recorder requests to DemoBites. This is separate from your app login. |
|
|
106
|
+
| Footage and recording metadata | Uploaded to DemoBites for review and processing, including the storyboard, narration intent, cursor events, and camera targets. |
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
hover-anchor clock calibration and camera work, delivering a finished, styled
|
|
94
|
-
`demo.mp4` on your disk — no account, no upload. Point your coding agent at it and
|
|
95
|
-
film. When you want narration, zooms, an editable timeline and hosting, the
|
|
96
|
-
sibling skill in `skill/` is one login away.
|
|
108
|
+
Recording happens locally. Studio processing and editing use DemoBites online. Anything visible or typed into the recorded flow can appear in the footage or recording metadata, so use suitable demo data. Your coding agent's own data handling still follows its provider and your settings.
|
|
97
109
|
|
|
98
|
-
|
|
110
|
+
Keep `.recorder/` and take directories out of version control. To disconnect the recorder, run `npx demobite logout`.
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
the skill you install always matches the DemoBites platform it talks to.
|
|
102
|
-
Releases are published from GitHub Actions with npm provenance: every version
|
|
103
|
-
is cryptographically tied to a public commit in this repository.
|
|
112
|
+
## Local output without a DemoBites account
|
|
104
113
|
|
|
105
|
-
|
|
114
|
+
The repository also contains a [standalone recorder skill](recorder/SKILL.md) and local finishing scripts. These produce a local MP4; optional voiceover uses your own ElevenLabs key.
|
|
106
115
|
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
-
|
|
116
|
+
This is an advanced, manual workflow. The default `npx demobite@latest` command installs the Studio-connected skill, not the standalone skill. The standalone finishing reference contains legacy instructions; use the standalone skill as the entry point rather than treating it as a second ending of the installed Studio skill.
|
|
117
|
+
|
|
118
|
+
## A few boundaries
|
|
119
|
+
|
|
120
|
+
- The agentic recorder films **browser workflows**. It does not record native desktop apps or phone screens.
|
|
121
|
+
- The Studio-connected skill is designed for short demos, usually **30–45 seconds**, with a **90-second maximum per Bite**. Split longer stories into separate videos.
|
|
122
|
+
- Some sites block automated browsers. The workflow may need a human sign-in or verification step, and some sites may refuse recording.
|
|
123
|
+
- Filming does not publish anything. You review the take before creating the Bite; public publishing is a separate action.
|
|
124
|
+
|
|
125
|
+
## Optional: Retake and management
|
|
126
|
+
|
|
127
|
+
When your UI changes, **Retake** can refilm an existing Bite from its saved recipe. It is a paid capability; see [plan details](https://www.demobites.com/pricing).
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx demobite retake <biteId> --note "Export moved to the header"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The package also includes a DemoBites management MCP for releases and centers:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx demobite mcp
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The default installer also attempts to register that MCP with Claude Code when the current project is already connected. Management access depends on your account and key permissions. You do not need paid management to record, edit, or export a free Bite.
|
|
140
|
+
|
|
141
|
+
[Management documentation](https://www.demobites.com/docs/bites/manage-from-your-agent)
|
|
142
|
+
|
|
143
|
+
## Repository
|
|
144
|
+
|
|
145
|
+
| Directory | Purpose |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `launcher/` | CLI setup, connection commands, and MCP registration. |
|
|
148
|
+
| `skill/` | Studio-connected recorder instructions, login, upload, and Retake. |
|
|
149
|
+
| `scripts/` | Shared browser recording, trimming, and timing calibration. |
|
|
150
|
+
| `recorder/` | Standalone skill and local video finishing tools. |
|
|
151
|
+
| `aliases/` | `agentic-recorder` and `demobites` aliases for the same launcher. |
|
|
152
|
+
|
|
153
|
+
Run `npx demobite@latest` again to update the installed skill. For publishing instructions, see [RELEASING.md](RELEASING.md).
|
|
154
|
+
|
|
155
|
+
Found something confusing or have an example to share? [Open an issue](https://github.com/demobites/agentic-recorder/issues). Include your operating system, agent, and recorder version. Remove keys, cookies, and private app data from logs or recordings before attaching them.
|
|
112
156
|
|
|
113
157
|
## License
|
|
114
158
|
|
|
115
|
-
|
|
159
|
+
The code in this repository is [MIT licensed](LICENSE). The hosted DemoBites Studio is a separate service; its source is not included here.
|
|
160
|
+
|
|
161
|
+
Built and maintained by [DemoBites](https://www.demobites.com/).
|
package/launcher/index.mjs
CHANGED
|
@@ -45,10 +45,9 @@ const chromePaths = [
|
|
|
45
45
|
"C:/Program Files/Google/Chrome/Application/chrome.exe",
|
|
46
46
|
"/usr/bin/google-chrome",
|
|
47
47
|
];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
else warn("ffmpeg not found — install it (macOS: brew install ffmpeg) before recording");
|
|
48
|
+
const haveChrome = chromePaths.some((p) => fs.existsSync(p)) || hasBin("google-chrome") || hasBin("google-chrome-stable");
|
|
49
|
+
if (haveChrome) ok("Google Chrome (films with the real browser)");
|
|
50
|
+
else console.log(" · Google Chrome not found — installing Playwright's Chromium for the recorder");
|
|
52
51
|
if (hasBin("claude")) ok("Claude Code (drives the recorder; MCP registers automatically)");
|
|
53
52
|
else warn("Claude Code not found — using Cursor or Codex? They drive the recorder too; MCP setup prints below");
|
|
54
53
|
|
|
@@ -63,12 +62,41 @@ for (const f of fs.readdirSync(path.join(pkgRoot, "skill/scripts"))) copy(`skill
|
|
|
63
62
|
for (const f of fs.readdirSync(path.join(pkgRoot, "scripts"))) copy(`scripts/${f}`, `scripts/${f}`);
|
|
64
63
|
ok(`Skill installed → ${dest}`);
|
|
65
64
|
|
|
66
|
-
// Playwright
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
// Playwright and the media tools live with the skill so takes can film and
|
|
66
|
+
// finish on any machine. ffmpeg and ffprobe are packaged per platform
|
|
67
|
+
// (@ffmpeg-installer, @ffprobe-installer); a compatible system build is preferred when present
|
|
68
|
+
// (scripts/media-tools.mjs decides). One line installs everything.
|
|
69
|
+
const needed = ["playwright", "@ffmpeg-installer/ffmpeg", "@ffprobe-installer/ffprobe"].filter(
|
|
70
|
+
(m) => !fs.existsSync(path.join(dest, "node_modules", m)),
|
|
71
|
+
);
|
|
72
|
+
if (needed.length) {
|
|
73
|
+
console.log(`\n Installing ${needed.join(", ")} (one-time)…`);
|
|
74
|
+
const r = spawnSync("npm", ["install", "--prefix", dest, "--silent", "--no-audit", "--no-fund", ...needed], { stdio: "inherit" });
|
|
70
75
|
if (r.status === 0) ok("Playwright ready");
|
|
71
|
-
else warn(
|
|
76
|
+
else warn(`Install failed — run: npm install --prefix ~/.claude/skills/agentic-recorder ${needed.join(" ")}`);
|
|
77
|
+
}
|
|
78
|
+
// No Chrome on this machine: fetch Playwright's Chromium once so the first
|
|
79
|
+
// take has a browser. Linux needs the shared libraries too (--with-deps).
|
|
80
|
+
if (!haveChrome) {
|
|
81
|
+
const pw = path.join(dest, "node_modules", "playwright", "cli.js");
|
|
82
|
+
const marker = path.join(dest, ".chromium-ready");
|
|
83
|
+
if (!fs.existsSync(marker) && fs.existsSync(pw)) {
|
|
84
|
+
console.log(" Installing Chromium (one-time)…");
|
|
85
|
+
const args = [pw, "install", "chromium", ...(process.platform === "linux" ? ["--with-deps"] : [])];
|
|
86
|
+
const r = spawnSync("node", args, { stdio: "inherit", cwd: dest });
|
|
87
|
+
if (r.status === 0) { fs.writeFileSync(marker, new Date().toISOString()); ok("Chromium ready"); }
|
|
88
|
+
else warn(`Chromium install failed — run: node ${pw} install chromium${process.platform === "linux" ? " --with-deps" : ""}`);
|
|
89
|
+
} else if (fs.existsSync(marker)) ok("Chromium ready");
|
|
90
|
+
}
|
|
91
|
+
{
|
|
92
|
+
// Verify both tools actually run (compatibility, not presence).
|
|
93
|
+
const r = spawnSync("node", [path.join(dest, "scripts", "media-tools.mjs")], { encoding: "utf8" });
|
|
94
|
+
const rows = (r.stdout || "").trim().split("\n").filter(Boolean).map((l) => l.split("\t"));
|
|
95
|
+
for (const [tool, source, info] of rows) {
|
|
96
|
+
if (source === "missing") warn(`${tool}: no working build (${info})`);
|
|
97
|
+
else ok(`${tool} ready (${source === "packaged" ? "packaged with the recorder" : source === "system" ? "your system build" : "from .recorder/config.json"})`);
|
|
98
|
+
}
|
|
99
|
+
if (r.status !== 0) warn("Recording will fail until ffmpeg and ffprobe both work. Re-run this command, or install them on your system.");
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
// ── 3. Subcommands ─────────────────────────────────────────────────────────
|
|
@@ -128,6 +156,21 @@ if (arg === "retake") {
|
|
|
128
156
|
process.exit(r.status ?? 1);
|
|
129
157
|
}
|
|
130
158
|
|
|
159
|
+
// Batch of briefs (2026-09-13): `npx demobite briefs list <batchId>` etc. and
|
|
160
|
+
// `npx demobite status <takeDir|stagingId>` hand straight to the skill scripts.
|
|
161
|
+
if (arg === "briefs" || arg === "status") {
|
|
162
|
+
let cfg = readCfg();
|
|
163
|
+
if (!cfg?.api_key) {
|
|
164
|
+
console.log("\n Not connected yet — linking this machine to DemoBites first…\n");
|
|
165
|
+
const r = spawnSync("node", [path.join(dest, "scripts", "login.mjs")], { stdio: "inherit", cwd: process.cwd() });
|
|
166
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
167
|
+
cfg = readCfg();
|
|
168
|
+
}
|
|
169
|
+
if (!cfg?.api_key) { warn("Login did not complete — run: npx demobite login"); process.exit(1); }
|
|
170
|
+
const r = spawnSync("node", [path.join(dest, "scripts", `${arg}.mjs`), ...process.argv.slice(3)], { stdio: "inherit", cwd: process.cwd() });
|
|
171
|
+
process.exit(r.status ?? 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
131
174
|
if (arg === "mcp") {
|
|
132
175
|
let cfg = readCfg();
|
|
133
176
|
if (!cfg?.api_key) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "demobite",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "The DemoBites agentic recorder
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "The DemoBites agentic recorder \u2014 you prompt, it films a real browser, and DemoBites turns the take into an editable demo bite.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"demobite": "launcher/index.mjs"
|
|
7
7
|
},
|
|
@@ -34,5 +34,9 @@
|
|
|
34
34
|
"url": "https://github.com/demobites/agentic-recorder/issues"
|
|
35
35
|
},
|
|
36
36
|
"license": "MIT",
|
|
37
|
-
"type": "module"
|
|
37
|
+
"type": "module",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"check-aliases": "node scripts/check-aliases.mjs",
|
|
40
|
+
"prepublishOnly": "node scripts/check-aliases.mjs"
|
|
41
|
+
}
|
|
38
42
|
}
|
package/recorder/scripts/mux.mjs
CHANGED
|
@@ -22,15 +22,8 @@ if (!dir) {
|
|
|
22
22
|
console.error("Usage: node mux.mjs <takeDir>");
|
|
23
23
|
process.exit(2);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
catch {
|
|
28
|
-
console.error(`${tool} is required on PATH. Install it (macOS: brew install ffmpeg) and rerun.`);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
requireTool("ffmpeg");
|
|
33
|
-
requireTool("ffprobe");
|
|
25
|
+
import { ffmpeg as FFMPEG, ffprobe as FFPROBE, requireMediaTools } from "../../scripts/media-tools.mjs";
|
|
26
|
+
requireMediaTools(["ffmpeg","ffprobe"]);
|
|
34
27
|
|
|
35
28
|
const demoPath = path.join(dir, "demo.mp4");
|
|
36
29
|
const manPath = path.join(dir, "manifest.json");
|
|
@@ -66,7 +59,7 @@ const delays = tts
|
|
|
66
59
|
const mix = tts.map((_, i) => `[a${i}]`).join("") + `amix=inputs=${tts.length}:normalize=0[aout]`;
|
|
67
60
|
|
|
68
61
|
const outPath = path.join(dir, "demo-voiced.mp4");
|
|
69
|
-
execFileSync(
|
|
62
|
+
execFileSync(FFMPEG(), [
|
|
70
63
|
"-y", "-loglevel", "error",
|
|
71
64
|
"-i", demoPath,
|
|
72
65
|
...inputArgs,
|
|
@@ -75,7 +68,7 @@ execFileSync("ffmpeg", [
|
|
|
75
68
|
"-c:v", "copy", "-c:a", "aac", "-b:a", "160k",
|
|
76
69
|
outPath,
|
|
77
70
|
], { stdio: "inherit" });
|
|
78
|
-
const dur = execFileSync(
|
|
71
|
+
const dur = execFileSync(FFPROBE(), [
|
|
79
72
|
"-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", outPath,
|
|
80
73
|
]).toString().trim();
|
|
81
74
|
console.log(`demo-voiced.mp4 ready, ${parseFloat(dur).toFixed(2)}s`);
|
package/recorder/scripts/post.sh
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
#
|
|
11
11
|
# Usage: post.sh <takeDir> [backdrop-hex-no-#] (default backdrop 0f1420)
|
|
12
12
|
set -euo pipefail
|
|
13
|
-
|
|
13
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
FFMPEG="$(node "$HERE/../../scripts/media-tools.mjs" 2>/dev/null | awk -F"\t" '$1=="ffmpeg"{print $3}')"
|
|
15
|
+
FFPROBE="$(node "$HERE/../../scripts/media-tools.mjs" 2>/dev/null | awk -F"\t" '$1=="ffprobe"{print $3}')"
|
|
16
|
+
[ -n "$FFMPEG" ] && [ -n "$FFPROBE" ] || { echo "ffmpeg/ffprobe: no working build found. Run npx demobite@latest again, or install them on your system." >&2; exit 1; }
|
|
14
17
|
command -v node >/dev/null 2>&1 || { echo "node is required on PATH." >&2; exit 1; }
|
|
15
18
|
D=${1:?Usage: post.sh <takeDir> [backdrop-hex-no-#]}
|
|
16
19
|
# Backdrop resolution mirrors frame.mjs exactly: CLI arg, then the remembered
|
|
@@ -20,7 +23,7 @@ BG=${2:-$(node -p "try{(JSON.parse(require('fs').readFileSync('.recorder/config.
|
|
|
20
23
|
[ -f "$D/raw.webm" ] || { echo "$D/raw.webm not found. Run record.mjs first." >&2; exit 1; }
|
|
21
24
|
[ -f "$D/shadow.png" ] && [ -f "$D/mask.png" ] || { echo "Overlays missing. Run: node frame.mjs $D" >&2; exit 1; }
|
|
22
25
|
TRIM=$(node -p "JSON.parse(require('fs').readFileSync('$D/manifest.json','utf8')).record_from||0")
|
|
23
|
-
|
|
26
|
+
"$FFMPEG" -y -loglevel error -i "$D/raw.webm" -i "$D/shadow.png" -i "$D/mask.png" -filter_complex \
|
|
24
27
|
"color=c=0x${BG}:s=1920x1080:r=30[bg];[0:v]trim=start=${TRIM},setpts=PTS-STARTPTS,scale=1728:972:flags=lanczos[v];[2:v]format=gray[m];[v][m]alphamerge[va];[bg][1:v]overlay=0:0[b1];[b1][va]overlay=96:54:shortest=1,fps=30,format=yuv420p[out]" \
|
|
25
28
|
-map "[out]" -c:v libx264 -preset medium -crf 19 -movflags +faststart "$D/demo.mp4"
|
|
26
|
-
echo "demo.mp4 duration: $(
|
|
29
|
+
echo "demo.mp4 duration: $("$FFPROBE" -v error -show_entries format=duration -of csv=p=0 "$D/demo.mp4")s"
|
package/recorder/scripts/tts.mjs
CHANGED
|
@@ -20,14 +20,8 @@ if (!dir) {
|
|
|
20
20
|
console.error("Usage: node tts.mjs <takeDir>");
|
|
21
21
|
process.exit(2);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
catch {
|
|
26
|
-
console.error(`${tool} is required on PATH. Install it (macOS: brew install ffmpeg) and rerun.`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
requireTool("ffprobe");
|
|
23
|
+
import { ffmpeg as FFMPEG, ffprobe as FFPROBE, requireMediaTools } from "../../scripts/media-tools.mjs";
|
|
24
|
+
requireMediaTools(["ffprobe"]);
|
|
31
25
|
|
|
32
26
|
let KEY = process.env.ELEVENLABS_API_KEY || "";
|
|
33
27
|
if (!KEY) {
|
|
@@ -68,7 +62,7 @@ for (const step of narrated) {
|
|
|
68
62
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
69
63
|
const file = `seg${step.n}.mp3`;
|
|
70
64
|
fs.writeFileSync(path.join(outDir, file), buf);
|
|
71
|
-
const dur = parseFloat(execFileSync(
|
|
65
|
+
const dur = parseFloat(execFileSync(FFPROBE(), [
|
|
72
66
|
"-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path.join(outDir, file),
|
|
73
67
|
]).toString());
|
|
74
68
|
out.push({ n: step.n, text: step.narration.text, file, duration: Math.round(dur * 100) / 100 });
|
package/scripts/calibrate.mjs
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
import fs from "node:fs";
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { ffmpeg as FFMPEG, requireMediaTools } from "./media-tools.mjs";
|
|
32
|
+
requireMediaTools(["ffmpeg"]);
|
|
31
33
|
|
|
32
34
|
const dir = process.argv[2];
|
|
33
35
|
if (!dir) { console.error("Usage: node calibrate.mjs <takeDir>"); process.exit(2); }
|
|
@@ -97,7 +99,7 @@ function roiChanges(bb) {
|
|
|
97
99
|
const pad = 6;
|
|
98
100
|
const x0 = Math.max(0, bb.x - pad), y0 = Math.max(0, bb.y - pad);
|
|
99
101
|
const w = Math.min(FRAME.w - x0, bb.w + 2 * pad), h = Math.min(FRAME.h - y0, bb.h + 2 * pad);
|
|
100
|
-
const res = spawnSync(
|
|
102
|
+
const res = spawnSync(FFMPEG(), [
|
|
101
103
|
"-loglevel", "info", "-i", clean,
|
|
102
104
|
"-vf", `crop=${w}:${h}:${x0}:${y0},select='gte(scene,0)',metadata=print`, "-f", "null", "-",
|
|
103
105
|
], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
@@ -218,7 +220,7 @@ if (clicks.length === 0) {
|
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
function sceneChanges(threshold) {
|
|
221
|
-
const res = spawnSync(
|
|
223
|
+
const res = spawnSync(FFMPEG(), [
|
|
222
224
|
"-loglevel", "info", "-i", clean,
|
|
223
225
|
"-vf", `select='gt(scene,${threshold})',showinfo`, "-f", "null", "-",
|
|
224
226
|
], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ALIAS GUARD (founder law, 2026-09-09): `agentic-recorder` and `demobites` on
|
|
3
|
+
// npm are aliases of this package. They depend on `demobite` with a caret
|
|
4
|
+
// range, so minor and patch releases flow through automatically, but a MAJOR
|
|
5
|
+
// bump silently strands them on the old major. This guard runs before every
|
|
6
|
+
// publish of demobite (prepublishOnly) and in CI, and refuses to continue if
|
|
7
|
+
// any alias's dependency range no longer covers the version about to ship.
|
|
8
|
+
//
|
|
9
|
+
// When it fails: bump the range in aliases/<name>/package.json (and the alias
|
|
10
|
+
// version), publish each alias from the founder's own terminal, then publish
|
|
11
|
+
// demobite. See RELEASING.md.
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const main = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
18
|
+
const aliasesDir = path.join(root, "aliases");
|
|
19
|
+
const [major] = main.version.split(".").map(Number);
|
|
20
|
+
|
|
21
|
+
let failed = false;
|
|
22
|
+
for (const name of fs.readdirSync(aliasesDir)) {
|
|
23
|
+
const pkgPath = path.join(aliasesDir, name, "package.json");
|
|
24
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
25
|
+
const alias = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
26
|
+
const range = alias.dependencies?.demobite ?? "";
|
|
27
|
+
const m = /^\^(\d+)\./.exec(range);
|
|
28
|
+
const covered = m && Number(m[1]) === major;
|
|
29
|
+
const line = `${alias.name.padEnd(18)} depends on demobite ${range.padEnd(8)} → ${covered ? "covers" : "DOES NOT COVER"} ${main.version}`;
|
|
30
|
+
console.log((covered ? " ✓ " : " ✗ ") + line);
|
|
31
|
+
if (!covered) failed = true;
|
|
32
|
+
const bin = fs.readFileSync(path.join(aliasesDir, name, "index.mjs"), "utf8");
|
|
33
|
+
if (!bin.includes('import "demobite/launcher/index.mjs"')) {
|
|
34
|
+
console.log(` ✗ ${alias.name} does not import the demobite launcher`);
|
|
35
|
+
failed = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (failed) {
|
|
39
|
+
console.error("\nAlias guard failed: an npm alias would strand on the old major. Update aliases/*/package.json (range + version), publish the aliases, then publish demobite. See RELEASING.md.");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log("alias guard: ok");
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// media-tools — the ONE place the recorder resolves ffmpeg and ffprobe.
|
|
2
|
+
//
|
|
3
|
+
// Order (founder design 2026-09-09):
|
|
4
|
+
// 1. an explicit override in <project>/.recorder/config.json { "ffmpeg": "...", "ffprobe": "..." }
|
|
5
|
+
// 2. a SYSTEM binary on PATH that passes the compatibility probe (below)
|
|
6
|
+
// 3. the packaged binaries from @ffmpeg-installer/ffmpeg + @ffprobe-installer/ffprobe
|
|
7
|
+
// (one platform each), installed by the launcher beside Playwright in the skill folder
|
|
8
|
+
//
|
|
9
|
+
// Compatibility, not presence: trim/mux need libx264 and the aac encoder. An old
|
|
10
|
+
// or stripped system build fails the probe and the packaged binary wins.
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
|
|
17
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
// scripts/ lives directly under the skill folder; node_modules sits beside it.
|
|
19
|
+
const skillRoot = path.resolve(here, "..");
|
|
20
|
+
|
|
21
|
+
function probe(bin, tool) {
|
|
22
|
+
try {
|
|
23
|
+
const out = execFileSync(bin, ["-version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString();
|
|
24
|
+
if (!/^ff(mpeg|probe) version/m.test(out)) return { ok: false, why: "not an ffmpeg build" };
|
|
25
|
+
if (tool === "ffmpeg") {
|
|
26
|
+
const enc = execFileSync(bin, ["-hide_banner", "-encoders"], { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString();
|
|
27
|
+
if (!/\blibx264\b/.test(enc)) return { ok: false, why: "no libx264 encoder" };
|
|
28
|
+
if (!/\baac\b/.test(enc)) return { ok: false, why: "no aac encoder" };
|
|
29
|
+
}
|
|
30
|
+
return { ok: true };
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { ok: false, why: e?.code === "ENOENT" ? "not found" : "did not run" };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fromOverride(tool) {
|
|
37
|
+
try {
|
|
38
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(process.cwd(), ".recorder", "config.json"), "utf8"));
|
|
39
|
+
const v = cfg?.[tool];
|
|
40
|
+
return typeof v === "string" && v ? v : null;
|
|
41
|
+
} catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fromPackage(tool) {
|
|
45
|
+
// Per-platform packages: only the current platform's binary is downloaded.
|
|
46
|
+
const pkg = tool === "ffmpeg" ? "@ffmpeg-installer/ffmpeg" : "@ffprobe-installer/ffprobe";
|
|
47
|
+
for (const base of [skillRoot, process.cwd()]) {
|
|
48
|
+
try {
|
|
49
|
+
const req = createRequire(pathToFileURL(path.join(base, "package.json")));
|
|
50
|
+
const p = req(pkg)?.path;
|
|
51
|
+
if (typeof p === "string" && fs.existsSync(p)) return p;
|
|
52
|
+
} catch { /* not installed here */ }
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cache = {};
|
|
58
|
+
/** Resolve a tool. Returns { path, source, why } — path null when nothing works. */
|
|
59
|
+
export function resolveTool(tool) {
|
|
60
|
+
if (cache[tool]) return cache[tool];
|
|
61
|
+
const tried = [];
|
|
62
|
+
const o = fromOverride(tool);
|
|
63
|
+
if (o) { const r = probe(o, tool); if (r.ok) return (cache[tool] = { path: o, source: "config" }); tried.push(`config ${o}: ${r.why}`); }
|
|
64
|
+
const sys = probe(tool, tool);
|
|
65
|
+
if (sys.ok) return (cache[tool] = { path: tool, source: "system" });
|
|
66
|
+
tried.push(`system: ${sys.why}`);
|
|
67
|
+
const pk = fromPackage(tool);
|
|
68
|
+
if (pk) { const r = probe(pk, tool); if (r.ok) return (cache[tool] = { path: pk, source: "packaged" }); tried.push(`packaged ${pk}: ${r.why}`); }
|
|
69
|
+
else tried.push("packaged: not installed");
|
|
70
|
+
return (cache[tool] = { path: null, source: null, why: tried.join("; ") });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const ffmpeg = () => resolveTool("ffmpeg").path;
|
|
74
|
+
export const ffprobe = () => resolveTool("ffprobe").path;
|
|
75
|
+
|
|
76
|
+
/** Exit with a clear message unless both tools resolve. Used by every script that films or finishes. */
|
|
77
|
+
export function requireMediaTools(tools = ["ffmpeg", "ffprobe"]) {
|
|
78
|
+
const missing = tools.map((t) => [t, resolveTool(t)]).filter(([, r]) => !r.path);
|
|
79
|
+
if (missing.length) {
|
|
80
|
+
for (const [t, r] of missing) console.error(`${t}: no working build found (${r.why}).`);
|
|
81
|
+
console.error("Run `npx demobite@latest` again to install the packaged ffmpeg and ffprobe, or install them on your system.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `node media-tools.mjs` prints what would be used — the launcher calls this.
|
|
87
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
88
|
+
for (const t of ["ffmpeg", "ffprobe"]) {
|
|
89
|
+
const r = resolveTool(t);
|
|
90
|
+
console.log(r.path ? `${t}\t${r.source}\t${r.path}` : `${t}\tmissing\t${r.why}`);
|
|
91
|
+
}
|
|
92
|
+
process.exit(["ffmpeg", "ffprobe"].every((t) => resolveTool(t).path) ? 0 : 1);
|
|
93
|
+
}
|