@autoaiai/aicut 0.0.0-stage → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/MANUAL.md +84 -0
  3. package/README.md +36 -2
  4. package/SPEC.md +221 -0
  5. package/THIRD_PARTY_NOTICES.md +10 -0
  6. package/TRADEMARK.md +25 -0
  7. package/bin/aicut.mjs +264 -0
  8. package/package.json +89 -6
  9. package/product.json +5 -0
  10. package/skills/aicut/LICENSE +21 -0
  11. package/skills/aicut/SKILL.md +66 -0
  12. package/skills/aicut/references/commands.md +72 -0
  13. package/skills/aicut/references/film-format.md +119 -0
  14. package/skills/aicut/references/recipes.md +184 -0
  15. package/skills/aicut/references/troubleshooting.md +57 -0
  16. package/skills/aicut-motion/LICENSE +21 -0
  17. package/skills/aicut-motion/SKILL.md +255 -0
  18. package/skills/aicut-motion/references/design-systems/README.md +25 -0
  19. package/skills/aicut-motion/references/design-systems/bold-poster.md +62 -0
  20. package/skills/aicut-motion/references/design-systems/data-instrument.md +71 -0
  21. package/skills/aicut-motion/references/design-systems/drafting-table.md +76 -0
  22. package/skills/aicut-motion/references/design-systems/editorial-ink.md +83 -0
  23. package/skills/aicut-motion/references/design-systems/riso-halftone.md +70 -0
  24. package/skills/aicut-motion/references/design-systems/storybook-scene.md +75 -0
  25. package/skills/aicut-motion/references/design-systems/swiss-kinetic.md +67 -0
  26. package/skills/aicut-motion/references/pages.md +258 -0
  27. package/src/film-doc.d.mts +204 -0
  28. package/src/film-doc.mjs +1225 -0
  29. package/src/host.d.mts +35 -0
  30. package/src/host.mjs +542 -0
  31. package/src/look.d.mts +2 -0
  32. package/src/look.mjs +358 -0
  33. package/src/media-links.mjs +68 -0
  34. package/src/of-filters.mjs +237 -0
  35. package/src/overrides.mjs +75 -0
  36. package/src/render.mjs +202 -0
  37. package/src/seekable.mjs +101 -0
  38. package/src/shared.d.mts +27 -0
  39. package/src/shared.mjs +391 -0
  40. package/src/timeline.mjs +1158 -0
  41. package/studio/client.mjs +311 -0
  42. package/studio/server/atomic.mjs +41 -0
  43. package/studio/server/bridge.js +89 -0
  44. package/studio/server/browse.mjs +104 -0
  45. package/studio/server/captions.mjs +502 -0
  46. package/studio/server/chat.mjs +49 -0
  47. package/studio/server/closure.mjs +407 -0
  48. package/studio/server/compound.mjs +133 -0
  49. package/studio/server/custom-services.mjs +299 -0
  50. package/studio/server/derived.mjs +225 -0
  51. package/studio/server/examples.mjs +89 -0
  52. package/studio/server/exports.mjs +1621 -0
  53. package/studio/server/fetch-error.mjs +14 -0
  54. package/studio/server/fetch-github.mjs +181 -0
  55. package/studio/server/fetch-repo.mjs +399 -0
  56. package/studio/server/files.mjs +628 -0
  57. package/studio/server/film-subtitle.mjs +869 -0
  58. package/studio/server/film.mjs +109 -0
  59. package/studio/server/folder-picker.mjs +199 -0
  60. package/studio/server/fonts.mjs +308 -0
  61. package/studio/server/get.mjs +700 -0
  62. package/studio/server/heard.mjs +108 -0
  63. package/studio/server/hidden.mjs +85 -0
  64. package/studio/server/history-migrate.mjs +239 -0
  65. package/studio/server/history-store.mjs +538 -0
  66. package/studio/server/history.mjs +682 -0
  67. package/studio/server/html-deck.mjs +168 -0
  68. package/studio/server/html-film.mjs +686 -0
  69. package/studio/server/html-pages.mjs +604 -0
  70. package/studio/server/http.mjs +40 -0
  71. package/studio/server/jianying.mjs +289 -0
  72. package/studio/server/keys.mjs +126 -0
  73. package/studio/server/main.mjs +120 -0
  74. package/studio/server/markers.mjs +80 -0
  75. package/studio/server/nle.mjs +537 -0
  76. package/studio/server/ops.mjs +506 -0
  77. package/studio/server/page-links.mjs +98 -0
  78. package/studio/server/pageframes.mjs +335 -0
  79. package/studio/server/pages.mjs +197 -0
  80. package/studio/server/prefs.mjs +56 -0
  81. package/studio/server/project-fonts.mjs +263 -0
  82. package/studio/server/project-settings.mjs +130 -0
  83. package/studio/server/projects.mjs +457 -0
  84. package/studio/server/providers/azure.mjs +100 -0
  85. package/studio/server/providers/brave.mjs +55 -0
  86. package/studio/server/providers/common.mjs +321 -0
  87. package/studio/server/providers/contract.mjs +98 -0
  88. package/studio/server/providers/doubao.mjs +65 -0
  89. package/studio/server/providers/edge-drm.mjs +263 -0
  90. package/studio/server/providers/edge-speech.mjs +292 -0
  91. package/studio/server/providers/edge-tts.mjs +112 -0
  92. package/studio/server/providers/elevenlabs.mjs +144 -0
  93. package/studio/server/providers/fal.mjs +212 -0
  94. package/studio/server/providers/google.mjs +201 -0
  95. package/studio/server/providers/groq.mjs +97 -0
  96. package/studio/server/providers/index.mjs +284 -0
  97. package/studio/server/providers/local-mdx.mjs +293 -0
  98. package/studio/server/providers/local-whisper.mjs +847 -0
  99. package/studio/server/providers/mdx-net.mjs +275 -0
  100. package/studio/server/providers/openai.mjs +131 -0
  101. package/studio/server/providers/pexels.mjs +40 -0
  102. package/studio/server/providers/pixabay.mjs +53 -0
  103. package/studio/server/providers/tar-bz2.mjs +356 -0
  104. package/studio/server/providers/tavily.mjs +68 -0
  105. package/studio/server/providers/web.mjs +231 -0
  106. package/studio/server/proxies.mjs +151 -0
  107. package/studio/server/recycle.mjs +222 -0
  108. package/studio/server/render-worker.mjs +29 -0
  109. package/studio/server/separate.mjs +44 -0
  110. package/studio/server/server.mjs +1021 -0
  111. package/studio/server/sfx.mjs +321 -0
  112. package/studio/server/skills.mjs +357 -0
  113. package/studio/server/slides.mjs +230 -0
  114. package/studio/server/stage.js +1553 -0
  115. package/studio/server/stock.mjs +122 -0
  116. package/studio/server/subtitle-edits.mjs +234 -0
  117. package/studio/server/subtitle-segments.mjs +602 -0
  118. package/studio/server/template-posters.mjs +68 -0
  119. package/studio/server/templates-page.mjs +93 -0
  120. package/studio/server/templates-styles.mjs +583 -0
  121. package/studio/server/templates.mjs +269 -0
  122. package/studio/server/transcripts.mjs +490 -0
  123. package/studio/server/transition-pages.mjs +43 -0
  124. package/studio/server/trash.mjs +143 -0
  125. package/studio/server/video-models.mjs +226 -0
  126. package/studio/server/watch.mjs +160 -0
  127. package/studio/server/web-agents.mjs +143 -0
  128. package/studio/server/websocket.mjs +82 -0
  129. package/studio/server/zip.mjs +159 -0
  130. package/studio/ui/dist/assets/ExportCenter-F-2vF-lQ.js +1 -0
  131. package/studio/ui/dist/assets/SettingsModal-BgVYguNu.js +2 -0
  132. package/studio/ui/dist/assets/app-icon-BpqHp2uj.png +0 -0
  133. package/studio/ui/dist/assets/es-BRUzvuoM.js +1 -0
  134. package/studio/ui/dist/assets/index-Bhg-9dA3.css +1 -0
  135. package/studio/ui/dist/assets/index-CWur1Iw9.js +1625 -0
  136. package/studio/ui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  137. package/studio/ui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  138. package/studio/ui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  139. package/studio/ui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  140. package/studio/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  141. package/studio/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  142. package/studio/ui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  143. package/studio/ui/dist/assets/ja-Cth-uDcW.js +1 -0
  144. package/studio/ui/dist/assets/ko-upvw591D.js +1 -0
  145. package/studio/ui/dist/assets/zh-CN-BCfJFazB.js +1 -0
  146. package/studio/ui/dist/index.html +23 -0
package/bin/aicut.mjs ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+
4
+ const USAGE = `aicut — a film is a function of time: given t, it draws that moment
5
+
6
+ aicut open [folder | url] start or open a project; Studio shows it to the person
7
+ aicut look [what] [t | a-b] check the film and draw it: a contact sheet with its sound and loudness
8
+ aicut render [a-b] the MP4, saved beside film.html
9
+ aicut get [what] [--options] voice-over, music, sound effects, transcripts, images, video, from a connected service
10
+
11
+ Run with no arguments for the manual; \`aicut <command> --help\` for one command's options; --version for the version.
12
+ look, render and get work on the project around the current folder.
13
+ Exit status: 0 done, 1 the film or the command failed (the output says why), 2 not a project or a wrong command.`;
14
+
15
+ /* one command's help: what it does, what it takes, where its output goes */
16
+ const COMMAND_HELP = {
17
+ open: `aicut open [folder | url]
18
+
19
+ folder a new film: a folder named after it (made if missing); or an existing project
20
+ A folder that is not a project yet gets an empty film.html, 1920 × 1080 (its viewport <meta>) with no
21
+ tracks; nothing else is created. Not a code project
22
+ (package.json, Cargo.toml, go.mod, pyproject.toml…): the film gets a folder of its own inside it.
23
+ url a film on GitHub or GitLab: a repository (https://github.com/owner/repo) or a folder in one
24
+ (…/tree/main/<folder>), copied into the projects library without its git history, then opened
25
+ without the project around the current folder, else the one opened last, else Studio's projects (where the
26
+ person can start one). The current folder is never made a project: name it (.) for that.
27
+
28
+ Starts AICUT Studio if it is not running and prints its address. A Studio page already open, in any browser,
29
+ switches to the project, and nothing more opens; otherwise open the address in your built-in browser, and if no
30
+ page connects within 15 s, Studio opens the person's browser (AICUT_NO_BROWSER=1: never).`,
31
+ look: `aicut look [what] [t | a-b]
32
+
33
+ what the project (default), a web page (.html), or a media file (video, image, sound)
34
+ t one frame at full size, e.g. 4.5
35
+ a-b a dense sheet of a stretch, e.g. 3-6
36
+ without either: a contact sheet of the whole thing, its sound and loudness, and a check that
37
+ the same t draws the same picture
38
+
39
+ Writes PNGs under .film/cache/look/ of the project and prints their paths. A project Studio has not shown yet is
40
+ opened in Studio first, so the person watches it.
41
+
42
+ --count <n> frames in a sheet (12)`,
43
+ render: `aicut render [a-b]
44
+
45
+ Render the project around the current folder to video.
46
+ The whole film goes to <project>/<folder name>.mp4, beside film.html; a range (a-b) goes to .film/cache/render/.
47
+ A project Studio has not shown yet is opened in Studio first.
48
+
49
+ --out <file> another path; .webm or .mov with --alpha
50
+ --fps <n> frame rate (30)
51
+ --4k 3840 px wide --scale <n> any device pixel ratio
52
+ --blur motion blur (8 samples) --samples <n> --shutter <0..1>
53
+ --alpha transparent background --workers <n> parallel browsers`,
54
+ };
55
+
56
+ /* `aicut` alone: the manual, the whole of what an agent needs to know. `get` takes any options its verb has,
57
+ so it goes to Studio (studio/client.mjs) before the fixed options below are parsed. */
58
+ const RAW = process.argv.slice(2);
59
+ /* the command as the agent can type it here: the one that ran it when it says so (AICUT_RUN: the development
60
+ checkout's .dev/bin/aicut), through npx always the newest (npx keeps the first one it fetched for a bare name),
61
+ else `aicut` where it is installed */
62
+ const RUN = process.env.AICUT_RUN || (process.env.npm_command === 'exec' ? 'npx -y @autoaiai/aicut@latest' : 'aicut');
63
+ /**
64
+ * A newer aicut, said once in a line before what the command prints: looked up on npm (at most daily, briefly) when
65
+ * a session starts (the manual, open), else only as learnt before, so look, render and get never wait for it.
66
+ */
67
+ const updateNotice = async (network) => {
68
+ const latest = await (await import('../studio/client.mjs')).newerAicut({ network }).catch(() => null);
69
+ if (latest) console.error(`aicut ${latest} is out (this is an older one): run commands as \`npx -y @autoaiai/aicut@latest …\` to use it; it updates Studio too.`);
70
+ };
71
+ if (RAW.length === 0) {
72
+ const { readFileSync } = await import('node:fs');
73
+ await updateNotice(true);
74
+ console.log(readFileSync(new URL('../MANUAL.md', import.meta.url), 'utf8').replaceAll('npx @autoaiai/aicut', RUN));
75
+ process.exit(0);
76
+ }
77
+ if (RAW.length === 1 && (RAW[0] === '--version' || RAW[0] === '-v')) {
78
+ const { readFileSync } = await import('node:fs');
79
+ console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
80
+ process.exit(0);
81
+ }
82
+ await updateNotice(RAW[0] === 'open');
83
+ if (RAW[0] === 'get') {
84
+ let result;
85
+ try { result = await (await import('../studio/client.mjs')).getMedia(RAW.slice(1), (line) => console.error(line)); }
86
+ catch (e) { result = { code: 1, lines: [`aicut get: ${e.message}`] }; }
87
+ for (const line of result.lines) console.log(line);
88
+ process.exit(result.code);
89
+ }
90
+
91
+ /* the help speaks the name this was run as: `aicut` or `npx @autoaiai/aicut` */
92
+ const named = (text) => text.replace(/^( {2})?aicut(?= )/gm, `$1${RUN}`).replace('`aicut <command>', `\`${RUN} <command>`);
93
+ let parsed;
94
+ try {
95
+ parsed = parseArgs({
96
+ allowPositionals: true,
97
+ options: {
98
+ count: { type: 'string' }, out: { type: 'string', short: 'o' }, '4k': { type: 'boolean' }, scale: { type: 'string' },
99
+ blur: { type: 'boolean' }, samples: { type: 'string' }, shutter: { type: 'string' }, alpha: { type: 'boolean' },
100
+ fps: { type: 'string' }, workers: { type: 'string' }, help: { type: 'boolean', short: 'h' },
101
+ },
102
+ });
103
+ } catch (e) {
104
+ /* an option no command has: say which, and what there is, rather than Node's stack */
105
+ console.error(`${RUN}: ${e.message.replace(/\. To specify a positional argument.*$/s, '')}\n\n${named(USAGE)}`);
106
+ process.exit(2);
107
+ }
108
+ const { values: o, positionals } = parsed;
109
+ const [cmd, ...args] = positionals;
110
+ const HELP = named(USAGE);
111
+ if (o.help && COMMAND_HELP[cmd]) { console.log(named(COMMAND_HELP[cmd])); process.exit(0); }
112
+ if (o.help || !cmd) { console.log(HELP); process.exit(0); }
113
+ const num = (v) => (v == null ? undefined : Number(v));
114
+ const opts = { ...o, count: num(o.count), scale: num(o.scale), samples: num(o.samples), shutter: num(o.shutter), fps: num(o.fps), workers: num(o.workers) };
115
+ /* a number option out of its range is a wrong command (2), said before anything starts */
116
+ const RANGES = { count: [1, 200, true], fps: [1, 240, true], scale: [0.1, 8, false], samples: [1, 64, true], shutter: [0, 1, false], workers: [1, 64, true] };
117
+ for (const [name, [lo, hi, whole]] of Object.entries(RANGES)) {
118
+ const v = opts[name];
119
+ if (v === undefined || (Number.isFinite(v) && v >= lo && v <= hi && (!whole || Number.isInteger(v)))) continue;
120
+ console.error(`${RUN} ${cmd ?? ''}: --${name} is ${whole ? 'a whole number' : 'a number'} from ${lo} to ${hi}, not ${o[name]}`.replace(' :', ':'));
121
+ process.exit(2);
122
+ }
123
+ const commands = {
124
+ open: () => open(args[0] ?? null),
125
+ look: () => import('../src/look.mjs').then((m) => m.look(args, opts)),
126
+ render: () => import('../src/render.mjs').then((m) => m.render(args, opts)),
127
+ };
128
+ /**
129
+ * Show a project in Studio; `.film/opened` marks a project Studio has shown. A folder named is made a project when it is
130
+ * not one; with none, the project around the current folder, else the one opened last: the current folder (a code
131
+ * repository, the home folder) is never made a project unasked.
132
+ * @param {string | null} folder
133
+ */
134
+ async function open(folder) {
135
+ /* a film on GitHub: copied into the library first, then opened as any folder */
136
+ if (folder != null && /^(https?:\/\/|(www\.)?(github|gitlab)\.com\/)/i.test(folder)) {
137
+ const fetched = await fetchFilm(folder);
138
+ if (!fetched.ok) return fetched;
139
+ folder = fetched.path;
140
+ }
141
+ const { openInStudio } = await import('../studio/client.mjs');
142
+ const { existsSync, mkdirSync, statSync, writeFileSync } = await import('node:fs');
143
+ const { dirname, join, resolve } = await import('node:path');
144
+ const fallback = process.env.AICUT_NO_BROWSER ? 0 : 15;
145
+ /* a person at a terminal has no browser of an agent's: theirs opens at once */
146
+ const now = !process.env.AICUT_NO_BROWSER && Boolean(process.stdout.isTTY && process.stdin.isTTY);
147
+ /* a checkout whose editor was never built (npm install builds it): Studio would show only that it is missing */
148
+ if (!existsSync(new URL('../studio/ui/dist/index.html', import.meta.url))) {
149
+ console.error('Studio\'s editor is not built in this checkout: run `npm run build:ui` (npm install does it).');
150
+ }
151
+ /** the project `from` is in (it, or a folder above it), if any */
152
+ const around = (from) => { for (let up = from; up !== dirname(up); up = dirname(up)) if (existsSync(join(up, 'film.html'))) return up; return null; };
153
+ let asked = folder == null ? around(process.cwd()) : resolve(folder);
154
+ if (asked && existsSync(asked) && !statSync(asked).isDirectory()) return { ok: false, usage: true, lines: [`${RUN} open: ${asked} is a file; a project is a folder`] };
155
+ /* a folder inside a project (scenes/, assets/) is the project around it, as look and render take it */
156
+ if (asked && existsSync(asked) && !existsSync(join(asked, 'film.html'))) asked = around(asked) ?? asked;
157
+ /* a code project is not a film: the film gets a folder of its own (a folder of media kept in git is fine) */
158
+ const MANIFESTS = ['package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'requirements.txt', 'Gemfile', 'pom.xml', 'build.gradle', 'composer.json', 'Package.swift'];
159
+ const code = (dir) => MANIFESTS.some((name) => existsSync(join(dir, name)));
160
+ if (asked && existsSync(asked) && !existsSync(join(asked, 'film.html')) && code(asked)) {
161
+ return { ok: false, usage: true, lines: [`${RUN} open: ${asked} is a code project, not a film; give the film a folder of its own: \`${RUN} open <name>\` makes <name>/ here`] };
162
+ }
163
+ try {
164
+ const { project, url, pages } = await openInStudio(asked, { fallback, now, say: (line) => console.error(line) });
165
+ if (project) {
166
+ mkdirSync(join(project.path, '.film'), { recursive: true });
167
+ writeFileSync(join(project.path, '.film', 'opened'), '');
168
+ }
169
+ /* an agent opens it in its own browser whatever else shows Studio: a page open elsewhere is not the agent's */
170
+ return { ok: true, lines: [project ? `project ${project.path}` : `project none yet: the person starts one in Studio, or \`${RUN} open <folder>\` makes one`, `Studio ${url}`,
171
+ /* a page in sight switched to it: nothing more opens, nor should be opened */
172
+ pages ? ' already open in a browser, now showing this project: open nothing more'
173
+ : now ? ' opened in your browser'
174
+ : fallback ? ` open this address in your built-in browser now; with none, it opens in the person's browser in ${fallback} s`
175
+ : ' open this address in your built-in browser now, or give it to the person'] };
176
+ } catch (e) { return { ok: false, lines: [`${RUN} open: ${e.message}`] }; }
177
+ }
178
+ /**
179
+ * `aicut open <url>`: the repository or folder fetched into the projects library (studio/server/fetch-repo.mjs),
180
+ * its progress on stderr.
181
+ * @param {string} url
182
+ */
183
+ async function fetchFilm(url) {
184
+ const { FetchError, fetchFilm: fetchInto, parseRepoUrl } = await import('../studio/server/fetch-repo.mjs');
185
+ const { libraryRoot } = await import('../studio/server/projects.mjs');
186
+ let source;
187
+ try { source = parseRepoUrl(url); } catch (e) { return { ok: false, usage: true, lines: [`${RUN} open: ${e.message}`] }; }
188
+ const mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
189
+ let said = '';
190
+ try {
191
+ const path = await fetchInto(source, {
192
+ library: libraryRoot(),
193
+ onProgress: ({ phase, bytes }) => {
194
+ const line = phase === 'lookup' ? `Looking up ${source.web}…` : phase === 'copy' ? `Fetched ${mb(bytes)}` : null;
195
+ if (line && line !== said) console.error((said = line));
196
+ },
197
+ });
198
+ return { ok: true, path };
199
+ } catch (e) {
200
+ return { ok: false, usage: e instanceof FetchError && ['url', 'host'].includes(e.code), lines: [`${RUN} open: ${e.message}`] };
201
+ }
202
+ }
203
+ if (!commands[cmd]) { console.error(`${RUN}: no command ${cmd}\n\n${HELP}`); process.exit(2); }
204
+
205
+ if (cmd === 'look' || cmd === 'render') {
206
+ const { existsSync } = await import('node:fs');
207
+ const { dirname, join, resolve } = await import('node:path');
208
+ /* a project is made by \`open\`: look and render work in one (the one around here, or around what they look at) */
209
+ const around = (from) => { for (let up = resolve(from); up !== dirname(up); up = dirname(up)) if (existsSync(join(up, 'film.html'))) return up; return null; };
210
+ const target = args.find((a) => !/^\d+(\.\d+)?(-\d+(\.\d+)?)?$/.test(a));
211
+ const project = around('.') ?? (target ? around(target) : null);
212
+ if (!project) { console.error(`${RUN} ${cmd}: ${resolve('.')} is not in a project; \`${RUN} open\` makes one and shows it in Studio`); process.exit(2); }
213
+ /* a project Studio has never shown (copied, cloned, forked): show it now, so the person watches from the start */
214
+ if (!existsSync(join(project, '.film', 'opened'))) {
215
+ const shown = await open(project);
216
+ for (const line of shown.lines) console.error(line);
217
+ if (!shown.ok) console.error(`${RUN} ${cmd}: carrying on without Studio`);
218
+ }
219
+ }
220
+
221
+ /** The first run on a machine has no browser yet: fetch Playwright's Chromium once, then run the command again. */
222
+ async function installChromium() {
223
+ const { createRequire } = await import('node:module');
224
+ const { spawnSync } = await import('node:child_process');
225
+ const { dirname, join } = await import('node:path');
226
+ const cli = join(dirname(createRequire(import.meta.url).resolve('playwright-core/package.json')), 'cli.js');
227
+ console.error(`${RUN}: first run on this machine, downloading the browser films are drawn in (about 100 MB, once)`);
228
+ const r = spawnSync(process.execPath, [cli, 'install', 'chromium-headless-shell'], { stdio: ['ignore', 'inherit', 'inherit'], windowsHide: true });
229
+ if (r.status !== 0) throw new Error('could not download the browser; run `npx playwright-core install chromium-headless-shell` and try again');
230
+ }
231
+
232
+ const FFMPEG_HELP = 'macOS: brew install ffmpeg · Debian/Ubuntu: sudo apt install ffmpeg · Windows: winget install ffmpeg';
233
+ const noFfmpeg = async () => {
234
+ const { spawnSync } = await import('node:child_process');
235
+ return ['ffmpeg', 'ffprobe'].some((bin) => spawnSync(bin, ['-version'], { stdio: 'ignore', windowsHide: true }).error);
236
+ };
237
+ if ((cmd === 'look' || cmd === 'render') && await noFfmpeg()) {
238
+ console.error(`${RUN} ${cmd}: needs ffmpeg and ffprobe on the PATH (${FFMPEG_HELP})`);
239
+ process.exit(2);
240
+ }
241
+ if (cmd === 'open') {
242
+ /* Studio draws pages' pictures in the headless browser: make sure one is on disk before Studio starts, not after its
243
+ first try. Asked by looking at the file rather than by launching one, because a launch here spent 1.2 s of every
244
+ open; the browser itself is only brought up when a page actually has to be drawn. */
245
+ const { browserReady } = await import('../src/host.mjs');
246
+ if (!browserReady()) await installChromium();
247
+ if (await noFfmpeg()) console.error(`${RUN} ${cmd}: Studio needs ffmpeg for video pictures, sound waves and exports; install it (${FFMPEG_HELP})`);
248
+ }
249
+
250
+ try {
251
+ let result;
252
+ try { result = await commands[cmd](); }
253
+ catch (e) {
254
+ if (!/Executable doesn't exist/.test(e.message)) throw e;
255
+ await installChromium();
256
+ result = await commands[cmd]();
257
+ }
258
+ for (const line of result.lines) console.log(line);
259
+ if (result.ok === false) process.exitCode = result.usage ? 2 : 1;
260
+ } catch (e) {
261
+ console.error(`${RUN} ${cmd}: ${e.message}`);
262
+ const { UsageError } = await import('../src/shared.mjs');
263
+ process.exit(e instanceof UsageError ? 2 : 1);
264
+ }
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
- {
2
- "name": "@autoaiai/aicut",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@autoaiai/aicut",
3
+ "version": "0.1.7",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "The open source AI video agent: films as web pages, the manual that teaches your coding agent to make them, the `aicut` command, and Studio, the editor to change anything in them.",
8
+ "license": "MIT",
9
+ "homepage": "https://aicut.dev",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/autoaiai/aicut.git",
13
+ "directory": "packages/AiCut"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/autoaiai/aicut/issues"
17
+ },
18
+ "keywords": [
19
+ "video",
20
+ "motion-graphics",
21
+ "agents",
22
+ "claude-code",
23
+ "codex",
24
+ "animation",
25
+ "render"
26
+ ],
27
+ "type": "module",
28
+ "bin": {
29
+ "aicut": "bin/aicut.mjs"
30
+ },
31
+ "exports": {
32
+ "./film-doc": "./src/film-doc.mjs",
33
+ "./host": "./src/host.mjs",
34
+ "./shared": "./src/shared.mjs",
35
+ "./look": "./src/look.mjs",
36
+ "./render": "./src/render.mjs",
37
+ "./studio": "./studio/server/server.mjs",
38
+ "./studio-ui/*": "./studio/ui/src/*",
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "README.md",
43
+ "SPEC.md",
44
+ "TRADEMARK.md",
45
+ "bin",
46
+ "skills",
47
+ "src",
48
+ "!src/*.test.mjs",
49
+ "MANUAL.md",
50
+ "studio/server/**/*.mjs",
51
+ "studio/server/**/*.js",
52
+ "!studio/server/**/*.test.mjs",
53
+ "studio/client.mjs",
54
+ "studio/ui/dist",
55
+ "THIRD_PARTY_NOTICES.md",
56
+ "product.json"
57
+ ],
58
+ "engines": {
59
+ "node": ">=22.2"
60
+ },
61
+ "dependencies": {
62
+ "onnxruntime-node": "^1.30.0",
63
+ "playwright-core": "1.61.1"
64
+ },
65
+ "scripts": {
66
+ "dev": "node scripts/dev.mjs",
67
+ "test": "node --test --test-concurrency=1 --test-timeout=300000 src/*.test.mjs studio/*.test.mjs studio/server/*.test.mjs studio/server/providers/*.test.mjs studio/ui/src/editor/*.test.ts studio/ui/src/i18n/*.test.ts studio/ui/src/lib/*.test.ts studio/ui/src/components/*.test.ts",
68
+ "typecheck": "tsc -p studio/server && tsc -p studio/ui",
69
+ "build:ui": "vite build --config studio/ui/vite.config.ts",
70
+ "prepare": "npm run build:ui",
71
+ "prepack": "node scripts/product.mjs official",
72
+ "postpack": "node scripts/product.mjs restore"
73
+ },
74
+ "devDependencies": {
75
+ "@aicut/shared": "workspace:*",
76
+ "@fontsource-variable/inter": "^5.3.0",
77
+ "@tailwindcss/vite": "^4.3.3",
78
+ "@types/node": "^22.20.5",
79
+ "@types/react": "^19.3.0",
80
+ "@types/react-dom": "^19.3.0",
81
+ "@vitejs/plugin-react": "^5.2.0",
82
+ "lucide-react": "^0.469.0",
83
+ "react": "^19.3.0",
84
+ "react-dom": "^19.3.0",
85
+ "tailwindcss": "^4.3.3",
86
+ "typescript": "^5.9.3",
87
+ "vite": "^7.3.6"
88
+ }
89
+ }
package/product.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "desktop": {
3
+ "updates": "https://aicut.dev/download/desktop"
4
+ }
5
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AiCut contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: aicut
3
+ description: Make, edit, check and render videos with AICUT, where a film is a web page that draws any moment t. Use when the user asks for a video, launch film, explainer, motion graphics, animation, title sequence, music video or subtitles; to cut footage, pictures, voice-over, music and sound effects together; or to open AICUT Studio. Also for 做视频, 剪辑, 渲染, 动效, 字幕.
4
+ license: MIT
5
+ ---
6
+
7
+ # AICUT
8
+
9
+ A film is `film.html`: it cuts animated web pages, footage, pictures and sound together on tracks. A page is a web
10
+ page that sets `window.film = { frame(t) }` and draws that moment. Nothing else — no framework, no build step: an
11
+ agent that knows the web already knows how to make one.
12
+
13
+ ## Read the manual first
14
+
15
+ `aicut` prints its own manual: the format and the commands, about a thousand words, always the current version.
16
+
17
+ ```sh
18
+ npx -y @autoaiai/aicut@latest
19
+ ```
20
+
21
+ Run it before writing a film; `npx -y @autoaiai/aicut@latest <command> --help` has one command's options. That manual is
22
+ authoritative — what follows is the workflow around it.
23
+
24
+ ## The loop
25
+
26
+ 1. **Open.** `npx -y @autoaiai/aicut@latest open <folder>` makes the folder a film (1920×1080, no tracks) and prints Studio's
27
+ address. Open that address in a browser you have: the person watches every change land there, and edits too.
28
+ 2. **Write.** `film.html` for the cut, one page per shot.
29
+ 3. **Check.** `npx -y @autoaiai/aicut@latest look` draws a contact sheet of the whole film, its sound and loudness, and checks
30
+ that the same `t` draws the same picture. `look 4.5` is one frame at full size, `look 3-6` a stretch,
31
+ `look assets/a.mp4` a media file. Look at the PNGs it writes; do not judge from the code.
32
+ 4. **Fix, repeat.** `render` only when the person asks for the file — they export from Studio themselves.
33
+
34
+ Five shots are five pages cut together, not one page that changes scene: the person can then trim, reorder and
35
+ replace each shot in Studio.
36
+
37
+ ## Hard rules
38
+
39
+ - **The picture depends only on `t`.** Same `t`, same picture: no `Math.random()`, no `Date.now()`, no state carried
40
+ between calls, no animation loop, no autoplay. A page never animates by itself; the host calls `frame(t)`.
41
+ - **Subtitles are never drawn in a page.** A sound or a video gets a WebVTT beside it with the same name
42
+ (`vo/1.wav` → `vo/1.vtt`); Studio makes the film's subtitles from those.
43
+ - **Never cut, crop or re-encode media into new files.** A long recording is trimmed with `#t=in,out` on every clip
44
+ of it; CSS crops and scales.
45
+ - **Sounds live in `film.html`, as clips.** A page has no sound. To place a hit heard at page time `t` inside a clip
46
+ that starts at `at` with in-point `in`: put it at `at + t - in` seconds of the film.
47
+ - **The person is editing too.** Read `film.html` right before changing it, change only what you mean to, keep every
48
+ `overrides` as it is, and leave whatever the tools own (`.film/`, `.git/`, any dot directory) alone. Change nothing
49
+ on a track marked `locked`.
50
+ - **Make what the person may change a DOM element.** Studio edits elements, not pixels: a headline belongs in an
51
+ `<h1>`, not painted into a canvas. A WebGL context needs `preserveDrawingBuffer: true`.
52
+ - **Only what the film can use stays in `assets/`.** Delete the pieces you made the media from.
53
+
54
+ ## What is where
55
+
56
+ - `references/film-format.md` — `film.html` and page rules on one page, when you want them in front of you.
57
+ - `references/commands.md` — the four commands, their options, where output goes, what an exit code means.
58
+ - `references/recipes.md` — snippets to copy: a title card, a canvas shot, picture-in-picture with voice-over and
59
+ subtitles, a music bed with a hit.
60
+ - `references/troubleshooting.md` — a blank frame, a picture that changes on its own, a missing encoder.
61
+
62
+ ## Before you say it is done
63
+
64
+ - `look` the whole film and read the sheet: framing, text fitting its box, nothing black where a picture should be.
65
+ - The film plays the length the person asked for, with sound where they asked for it.
66
+ - Say which files you made and what is in them; ask before rendering, unless they already asked for the MP4.
@@ -0,0 +1,72 @@
1
+ # The commands
2
+
3
+ Run them in the film's folder. `npx -y @autoaiai/aicut@latest` alone prints the manual; `--help` after a command prints that
4
+ command's options; `--version` prints the version. Exit status: **0** done, **1** the film or the command failed (the
5
+ output says why), **2** not a project or a wrong command.
6
+
7
+ Use `npx -y @autoaiai/aicut@latest` rather than a bare `aicut` unless something put one on your PATH: it is the newest, and
8
+ `npx` otherwise keeps the first version it ever fetched.
9
+
10
+ ## open
11
+
12
+ ```sh
13
+ npx -y @autoaiai/aicut@latest open # the project around this folder, else the last one, else Studio's list
14
+ npx -y @autoaiai/aicut@latest open launch-video # a new film; the folder is made if missing
15
+ npx -y @autoaiai/aicut@latest open . # this folder becomes the film
16
+ npx -y @autoaiai/aicut@latest open https://github.com/aicut/examples/tree/main/one-prompt
17
+ ```
18
+
19
+ - A folder that is not a project gets an empty `film.html`, 1920 × 1080 (its viewport `<meta>`), no tracks. A code
20
+ repository (`package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`…) gets a film folder of its own inside it.
21
+ - A GitHub or GitLab address — a repository, or a folder in one (`…/tree/main/<folder>`) — is copied into the
22
+ projects library without its git history, then opened.
23
+ - Starts Studio if it is not running and prints its address. **Open that address in your browser before writing
24
+ anything** and keep it open: the person watches every change land. A page already open switches to the project; if
25
+ no page connects within 15 s, Studio opens the person's own browser (`AICUT_NO_BROWSER=1`: never).
26
+
27
+ ## look
28
+
29
+ ```sh
30
+ npx -y @autoaiai/aicut@latest look # the whole film: contact sheet, sound, loudness, same-t check
31
+ npx -y @autoaiai/aicut@latest look 4.5 # one frame, full size
32
+ npx -y @autoaiai/aicut@latest look 3-6 # a stretch, densely
33
+ npx -y @autoaiai/aicut@latest look assets/a.mp4 # a media file; also any .html page
34
+ ```
35
+
36
+ Writes PNGs under `.film/cache/look/` and prints their paths. `--count <n>`: frames in a sheet (12). A project Studio
37
+ has not shown yet is opened in Studio first, so the person watches it.
38
+
39
+ ## render
40
+
41
+ ```sh
42
+ npx -y @autoaiai/aicut@latest render # the whole film → <project>/<folder name>.mp4, beside film.html
43
+ npx -y @autoaiai/aicut@latest render 12-20 # a range → .film/cache/render/
44
+ ```
45
+
46
+ | Option | Meaning |
47
+ | --- | --- |
48
+ | `--out <file>` | Another path; `.webm` or `.mov` with `--alpha` |
49
+ | `--fps <n>` | Frame rate (30) |
50
+ | `--4k` | 3840 px wide |
51
+ | `--scale <n>` | Any device pixel ratio |
52
+ | `--blur` | Motion blur (8 samples); `--samples <n>`, `--shutter <0..1>` |
53
+ | `--alpha` | Transparent background |
54
+ | `--workers <n>` | Parallel browsers |
55
+
56
+ Only when the person asks for the file: you check with `look`, and they export from Studio.
57
+
58
+ ## get
59
+
60
+ ```sh
61
+ npx -y @autoaiai/aicut@latest get # what works right now, and which service makes the rest
62
+ npx -y @autoaiai/aicut@latest get voice-over "A calm, low voice, 20 seconds"
63
+ npx -y @autoaiai/aicut@latest get music "warm analog synth, 90 bpm"
64
+ ```
65
+
66
+ Kinds: voice-over, music, sound effects, transcripts, images, image search, video, web search. Each works only once
67
+ the person has connected a service that makes it, with their own key, in Studio → Settings → Providers (subtitle
68
+ translation too). When a kind is not connected, tell the person which services make it and where to connect one
69
+ rather than trying again. A video clip costs a lot: the person confirms it in Studio first.
70
+
71
+ Save what you get under `assets/`. A generated sound or video gets its WebVTT beside it — that is where its words
72
+ come from, not a page.
@@ -0,0 +1,119 @@
1
+ # The format, on one page
2
+
3
+ The summary below is for having the rules in front of you. When it disagrees with `npx -y @autoaiai/aicut@latest` (the manual)
4
+ or `packages/AiCut/SPEC.md` (the protocol, version 0.1), those are right.
5
+
6
+ ## The project
7
+
8
+ | Path | What it is |
9
+ | --- | --- |
10
+ | `film.html` | The film: which clip plays when and where. The person edits it; read it before changing it. |
11
+ | `*.html` | Pages: one per shot, each loading what it needs by relative path. |
12
+ | `assets/` | Media (video, pictures, sound). Only what the film can use. |
13
+ | `.film/`, `.git/`, dot files | The tools' own. Leave them alone. |
14
+
15
+ ## film.html
16
+
17
+ ```html
18
+ <!doctype html>
19
+ <html>
20
+ <head>
21
+ <meta charset="utf-8">
22
+ <meta name="viewport" content="width=1920, height=1080">
23
+ <style>.card { object-fit: cover; border-radius: 24px; }</style>
24
+ </head>
25
+ <body>
26
+ <section>
27
+ <iframe src="title.html" style="left: 0; top: 0; width: 1920px; height: 1080px"></iframe>
28
+ </section>
29
+ <section>
30
+ <video src="assets/a.mp4#t=2,8" at="2" speed="0.5" volume="0.8" class="card"
31
+ style="left: 160px; top: 90px; width: 1600px; height: 700px"></video>
32
+ </section>
33
+ <section>
34
+ <audio src="assets/bed.mp3#t=0,14" volume="0.5"></audio>
35
+ </section>
36
+ </body>
37
+ </html>
38
+ ```
39
+
40
+ - The stage is the viewport's size in CSS pixels; without a `<meta name="viewport">`, 1920 × 1080. The head holds
41
+ styles and fonts and **no script**.
42
+ - Each `<section>` is a track. The first is on top (titles before the footage they cover, background last). A track
43
+ has no kind; any clip goes on any track. Clips on one track never overlap — overlapping pictures go on different
44
+ tracks.
45
+ - A track may be `hidden` (no picture, no sound), `muted` (no sound) or `locked` (the person's: change nothing on it).
46
+ - A clip is one element and holds nothing: `<iframe>` a page, `<video>`, `<img>` a still, `<audio>` a sound.
47
+
48
+ | Attribute | On | Meaning |
49
+ | --- | --- | --- |
50
+ | `src` | all | The file, relative to `film.html`. `#t=in,out` in the file's own seconds; `#t=in` to its end; nothing, all of it. A still, or a page without `duration`, **must** have `#t=0,length`. |
51
+ | `id` | all | Unique in the film; without one, the file's name. |
52
+ | `at` | all | The film second it starts at (default 0). |
53
+ | `speed` | video, sound | 0.25–4, pitch kept. The clip lasts `(out − in) / speed`. |
54
+ | `volume` | video, sound | Linear gain, 1 = the file's own level; `muted` = 0. |
55
+ | `size` | page | `size="1920x1080"`: the pixels a page without its own `width`/`height` is laid out at (default: the stage). An editor writes it when the frame changes shape; keep it. |
56
+ | `overrides` | all | The person's Studio changes, JSON. Keep as it is. |
57
+
58
+ - **Placement is only** `left`, `top`, `width`, `height` (stage px) and `rotate` (degrees, clockwise about the center),
59
+ in the clip's `style`. `position`, `transform`, `margin` and the like on a clip are an error — the host places the
60
+ clip itself.
61
+ - With only one of `width`/`height`, the other follows the picture's proportions. A video or still fills its box as
62
+ its CSS says (`object-fit: contain` unless the film says otherwise); without a box it is the stage. A page is
63
+ fitted whole inside its box, centered; without a box it is its own size, centered.
64
+ - Look is CSS: `class`, the rest of `style` (`border-radius`, `clip-path`, `mask`, `filter`, `opacity`,
65
+ `mix-blend-mode`), and the film's `<style>`. **The film's CSS has no time** — what moves is a page.
66
+ - `overrides` entries: `at` (a CSS selector, page clips only) with optional `n` (which match, from 1), `t` (translate
67
+ px), `s` (scale), `r` (rotate degrees), `style` (CSS, applied with priority), `text` (replaces the element's text),
68
+ `lock` (true: not selectable or movable; changes no pixels). One entry without `at`, on any clip, is the clip's own:
69
+ `{"fade": [in, out]}` in seconds — the picture's opacity and the sound's gain ramp 0→1 and 1→0.
70
+ - Times are seconds to the millisecond; a host rounds each to three decimals. Read strictly: a misspelled attribute
71
+ is reported, not ignored.
72
+ - A page clip is drawn at the page's local time `in + (t − at)`; past the page's `duration` it holds its last frame.
73
+
74
+ ## A page
75
+
76
+ ```js
77
+ window.film = {
78
+ frame(t) { /* draw the whole picture at t seconds; may return a Promise */ },
79
+ duration: 12, // optional: its own length; without it, like a still
80
+ ready: loadEverything(), // optional: Promise (or a function returning one) — frame is called after it
81
+ width: 480, height: 320, // optional: a page that is part of the picture; without them, the stage
82
+ };
83
+ ```
84
+
85
+ - Set `window.film` before `load` (a module script does that).
86
+ - **One rule: the picture depends only on `t`.** Repaint everything each call; never accumulate (`x += v`) across
87
+ calls. History (a trail, a physics run) is computed from `t` or precomputed during `ready`.
88
+ - The host holds the page's clocks at `t` while it draws: `performance.now()`, `requestAnimationFrame` timestamps,
89
+ `Date`, CSS/Web Animations the page left running, and `Math.random` (seeded from `t`) all agree with it. Timers and
90
+ history do not — do not use them.
91
+ - Libraries that run their own clock are driven by `t`: `timeline.seek(t)`, `animation.currentTime = t * 1000`,
92
+ `p5.redraw()`, `renderer.render(scene, camera)`.
93
+ - Tweens that capture their start value the first time they run (GSAP `to`/`from`) depend on draw order: use
94
+ explicit start and end values (`fromTo`).
95
+ - Lay the page out for `innerWidth × innerHeight`. A canvas should allocate
96
+ `innerWidth * devicePixelRatio` pixels.
97
+ - WebGL: create the context with `preserveDrawingBuffer: true`.
98
+ - Load web fonts per face (`document.fonts.load('700 64px …')`), not just `document.fonts.ready`.
99
+ - A page opened as a plain web page has no `window.filmHost` and should play itself:
100
+ `if (!window.filmHost) { … requestAnimationFrame loop … }`. Optional, and it makes every film a page that just works.
101
+
102
+ ## Sound
103
+
104
+ - A page has **no sound**. Every sound is a clip: an `<audio>`, or a `<video>`'s own sound.
105
+ - A sound that belongs to a shot is placed at the film second the shot plays it. A hit heard at page time `t` in a
106
+ clip that starts at `at` with in-point `in` goes at **`at + t − in`**. Moving the page clip does not move it —
107
+ move them together.
108
+ - The clip is silent when its `volume` is 0, or its track is `muted` or `hidden`.
109
+ - Beat, onset and word timings are data, not API: precompute them into files the page reads during `ready`.
110
+
111
+ ## Words
112
+
113
+ - What a sound or video says is a WebVTT beside it with the same name: `assets/vo/1.wav` → `assets/vo/1.vtt`, in the
114
+ file's own seconds. One cue per subtitle line; timestamp tags inside a cue time its words.
115
+ - Translations sit beside it as `1.<language>.vtt`, the same lines at the same times.
116
+ - A file starting with `NOTE Each cue is one subtitle, as cut by hand in AICUT Studio.` keeps its cues exactly as
117
+ they are.
118
+ - **Never draw subtitles in a page.** Studio draws the film's subtitles from the clips that can be heard, placed by
119
+ their `at`, `#t=` and `speed`.