@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.
- package/LICENSE +21 -0
- package/MANUAL.md +84 -0
- package/README.md +36 -2
- package/SPEC.md +221 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/TRADEMARK.md +25 -0
- package/bin/aicut.mjs +264 -0
- package/package.json +89 -6
- package/product.json +5 -0
- package/skills/aicut/LICENSE +21 -0
- package/skills/aicut/SKILL.md +66 -0
- package/skills/aicut/references/commands.md +72 -0
- package/skills/aicut/references/film-format.md +119 -0
- package/skills/aicut/references/recipes.md +184 -0
- package/skills/aicut/references/troubleshooting.md +57 -0
- package/skills/aicut-motion/LICENSE +21 -0
- package/skills/aicut-motion/SKILL.md +255 -0
- package/skills/aicut-motion/references/design-systems/README.md +25 -0
- package/skills/aicut-motion/references/design-systems/bold-poster.md +62 -0
- package/skills/aicut-motion/references/design-systems/data-instrument.md +71 -0
- package/skills/aicut-motion/references/design-systems/drafting-table.md +76 -0
- package/skills/aicut-motion/references/design-systems/editorial-ink.md +83 -0
- package/skills/aicut-motion/references/design-systems/riso-halftone.md +70 -0
- package/skills/aicut-motion/references/design-systems/storybook-scene.md +75 -0
- package/skills/aicut-motion/references/design-systems/swiss-kinetic.md +67 -0
- package/skills/aicut-motion/references/pages.md +258 -0
- package/src/film-doc.d.mts +204 -0
- package/src/film-doc.mjs +1225 -0
- package/src/host.d.mts +35 -0
- package/src/host.mjs +542 -0
- package/src/look.d.mts +2 -0
- package/src/look.mjs +358 -0
- package/src/media-links.mjs +68 -0
- package/src/of-filters.mjs +237 -0
- package/src/overrides.mjs +75 -0
- package/src/render.mjs +202 -0
- package/src/seekable.mjs +101 -0
- package/src/shared.d.mts +27 -0
- package/src/shared.mjs +391 -0
- package/src/timeline.mjs +1158 -0
- package/studio/client.mjs +311 -0
- package/studio/server/atomic.mjs +41 -0
- package/studio/server/bridge.js +89 -0
- package/studio/server/browse.mjs +104 -0
- package/studio/server/captions.mjs +502 -0
- package/studio/server/chat.mjs +49 -0
- package/studio/server/closure.mjs +407 -0
- package/studio/server/compound.mjs +133 -0
- package/studio/server/custom-services.mjs +299 -0
- package/studio/server/derived.mjs +225 -0
- package/studio/server/examples.mjs +89 -0
- package/studio/server/exports.mjs +1621 -0
- package/studio/server/fetch-error.mjs +14 -0
- package/studio/server/fetch-github.mjs +181 -0
- package/studio/server/fetch-repo.mjs +399 -0
- package/studio/server/files.mjs +628 -0
- package/studio/server/film-subtitle.mjs +869 -0
- package/studio/server/film.mjs +109 -0
- package/studio/server/folder-picker.mjs +199 -0
- package/studio/server/fonts.mjs +308 -0
- package/studio/server/get.mjs +700 -0
- package/studio/server/heard.mjs +108 -0
- package/studio/server/hidden.mjs +85 -0
- package/studio/server/history-migrate.mjs +239 -0
- package/studio/server/history-store.mjs +538 -0
- package/studio/server/history.mjs +682 -0
- package/studio/server/html-deck.mjs +168 -0
- package/studio/server/html-film.mjs +686 -0
- package/studio/server/html-pages.mjs +604 -0
- package/studio/server/http.mjs +40 -0
- package/studio/server/jianying.mjs +289 -0
- package/studio/server/keys.mjs +126 -0
- package/studio/server/main.mjs +120 -0
- package/studio/server/markers.mjs +80 -0
- package/studio/server/nle.mjs +537 -0
- package/studio/server/ops.mjs +506 -0
- package/studio/server/page-links.mjs +98 -0
- package/studio/server/pageframes.mjs +335 -0
- package/studio/server/pages.mjs +197 -0
- package/studio/server/prefs.mjs +56 -0
- package/studio/server/project-fonts.mjs +263 -0
- package/studio/server/project-settings.mjs +130 -0
- package/studio/server/projects.mjs +457 -0
- package/studio/server/providers/azure.mjs +100 -0
- package/studio/server/providers/brave.mjs +55 -0
- package/studio/server/providers/common.mjs +321 -0
- package/studio/server/providers/contract.mjs +98 -0
- package/studio/server/providers/doubao.mjs +65 -0
- package/studio/server/providers/edge-drm.mjs +263 -0
- package/studio/server/providers/edge-speech.mjs +292 -0
- package/studio/server/providers/edge-tts.mjs +112 -0
- package/studio/server/providers/elevenlabs.mjs +144 -0
- package/studio/server/providers/fal.mjs +212 -0
- package/studio/server/providers/google.mjs +201 -0
- package/studio/server/providers/groq.mjs +97 -0
- package/studio/server/providers/index.mjs +284 -0
- package/studio/server/providers/local-mdx.mjs +293 -0
- package/studio/server/providers/local-whisper.mjs +847 -0
- package/studio/server/providers/mdx-net.mjs +275 -0
- package/studio/server/providers/openai.mjs +131 -0
- package/studio/server/providers/pexels.mjs +40 -0
- package/studio/server/providers/pixabay.mjs +53 -0
- package/studio/server/providers/tar-bz2.mjs +356 -0
- package/studio/server/providers/tavily.mjs +68 -0
- package/studio/server/providers/web.mjs +231 -0
- package/studio/server/proxies.mjs +151 -0
- package/studio/server/recycle.mjs +222 -0
- package/studio/server/render-worker.mjs +29 -0
- package/studio/server/separate.mjs +44 -0
- package/studio/server/server.mjs +1021 -0
- package/studio/server/sfx.mjs +321 -0
- package/studio/server/skills.mjs +357 -0
- package/studio/server/slides.mjs +230 -0
- package/studio/server/stage.js +1553 -0
- package/studio/server/stock.mjs +122 -0
- package/studio/server/subtitle-edits.mjs +234 -0
- package/studio/server/subtitle-segments.mjs +602 -0
- package/studio/server/template-posters.mjs +68 -0
- package/studio/server/templates-page.mjs +93 -0
- package/studio/server/templates-styles.mjs +583 -0
- package/studio/server/templates.mjs +269 -0
- package/studio/server/transcripts.mjs +490 -0
- package/studio/server/transition-pages.mjs +43 -0
- package/studio/server/trash.mjs +143 -0
- package/studio/server/video-models.mjs +226 -0
- package/studio/server/watch.mjs +160 -0
- package/studio/server/web-agents.mjs +143 -0
- package/studio/server/websocket.mjs +82 -0
- package/studio/server/zip.mjs +159 -0
- package/studio/ui/dist/assets/ExportCenter-F-2vF-lQ.js +1 -0
- package/studio/ui/dist/assets/SettingsModal-BgVYguNu.js +2 -0
- package/studio/ui/dist/assets/app-icon-BpqHp2uj.png +0 -0
- package/studio/ui/dist/assets/es-BRUzvuoM.js +1 -0
- package/studio/ui/dist/assets/index-Bhg-9dA3.css +1 -0
- package/studio/ui/dist/assets/index-CWur1Iw9.js +1625 -0
- package/studio/ui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio/ui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio/ui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio/ui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio/ui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio/ui/dist/assets/ja-Cth-uDcW.js +1 -0
- package/studio/ui/dist/assets/ko-upvw591D.js +1 -0
- package/studio/ui/dist/assets/zh-CN-BCfJFazB.js +1 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
|
|
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,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`.
|