openfilm 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +108 -4
  3. package/SPEC.md +176 -0
  4. package/TRADEMARK.md +25 -0
  5. package/bin/openfilm.mjs +153 -1
  6. package/package.json +71 -6
  7. package/skills/openfilm/SKILL.md +9 -0
  8. package/src/film-doc.d.mts +94 -0
  9. package/src/film-doc.mjs +420 -0
  10. package/src/host.d.mts +23 -0
  11. package/src/host.mjs +252 -0
  12. package/src/look.d.mts +2 -0
  13. package/src/look.mjs +321 -0
  14. package/src/render.mjs +135 -0
  15. package/src/shared.d.mts +14 -0
  16. package/src/shared.mjs +272 -0
  17. package/src/timeline.html +473 -0
  18. package/studio/client.mjs +160 -0
  19. package/studio/server/account.mjs +351 -0
  20. package/studio/server/asset-index.mjs +454 -0
  21. package/studio/server/bridge.js +77 -0
  22. package/studio/server/captions.mjs +775 -0
  23. package/studio/server/derived.mjs +231 -0
  24. package/studio/server/exports.mjs +1138 -0
  25. package/studio/server/files.mjs +176 -0
  26. package/studio/server/film-subtitle.mjs +748 -0
  27. package/studio/server/film.mjs +73 -0
  28. package/studio/server/fonts.mjs +179 -0
  29. package/studio/server/get.mjs +578 -0
  30. package/studio/server/history.mjs +228 -0
  31. package/studio/server/http.mjs +56 -0
  32. package/studio/server/keys.mjs +88 -0
  33. package/studio/server/main.mjs +36 -0
  34. package/studio/server/nle.mjs +540 -0
  35. package/studio/server/ops.mjs +352 -0
  36. package/studio/server/pageframes.mjs +134 -0
  37. package/studio/server/pages.mjs +80 -0
  38. package/studio/server/projects.mjs +250 -0
  39. package/studio/server/providers/common.mjs +241 -0
  40. package/studio/server/providers/contract.mjs +92 -0
  41. package/studio/server/providers/elevenlabs.mjs +129 -0
  42. package/studio/server/providers/fal.mjs +115 -0
  43. package/studio/server/providers/google.mjs +170 -0
  44. package/studio/server/providers/index.mjs +150 -0
  45. package/studio/server/providers/openai.mjs +108 -0
  46. package/studio/server/providers/openfilm.mjs +321 -0
  47. package/studio/server/server.mjs +455 -0
  48. package/studio/server/slides.mjs +230 -0
  49. package/studio/server/stage.js +876 -0
  50. package/studio/server/trash.mjs +62 -0
  51. package/studio/server/tsconfig.json +15 -0
  52. package/studio/server/watch.mjs +136 -0
  53. package/studio/server/zip.mjs +116 -0
  54. package/studio/ui/dist/assets/index-MXaXGM_-.css +1 -0
  55. package/studio/ui/dist/assets/index-b7xfB12d.js +17 -0
  56. package/studio/ui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  57. package/studio/ui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  58. package/studio/ui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  59. package/studio/ui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  60. package/studio/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  61. package/studio/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  62. package/studio/ui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  63. package/studio/ui/dist/index.html +22 -0
  64. package/templates/MANUAL.md +66 -0
package/LICENSE ADDED
@@ -0,0 +1,55 @@
1
+ Apache License
2
+
3
+ Version 2.0, January 2004
4
+
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
16
+
17
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
18
+
19
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
20
+
21
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
22
+
23
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
24
+
25
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
26
+
27
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
28
+
29
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
30
+
31
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
32
+
33
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
34
+
35
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
36
+
37
+ You must give any other recipients of the Work or Derivative Works a copy of this License; and
38
+
39
+ You must cause any modified files to carry prominent notices stating that You changed the files; and
40
+
41
+ You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
42
+
43
+ If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
44
+
45
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46
+
47
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48
+
49
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50
+
51
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52
+
53
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54
+
55
+ END OF TERMS AND CONDITIONS
package/README.md CHANGED
@@ -1,7 +1,111 @@
1
- # OpenFilm
1
+ <h1 align="center">OpenFilm</h1>
2
2
 
3
- Make any video. Change anything.
3
+ <p align="center"><b>The open source AI video agent.</b><br>
4
+ Turns your Codex, Claude Code or any coding agent into a video agent, and gives you an editor to change anything it makes.</p>
4
5
 
5
- A film is a web page that draws any moment of itself. OpenFilm gives coding agents one command to make films, and gives people an editor to change them.
6
+ <p align="center">
7
+ <a href="#make-a-film">Make a film</a> ·
8
+ <a href="#fork-a-film">Fork a film</a> ·
9
+ <a href="#how-it-works">How it works</a> ·
10
+ <a href="SPEC.md">Spec</a> ·
11
+ <a href="templates/MANUAL.md">Manual</a>
12
+ </p>
6
13
 
7
- Coming soon at [openfilm.dev](https://openfilm.dev).
14
+ Videos made with agents end up as MP4s you can watch but not change, or in repos that each invent their own
15
+ architecture. Either way, reusing one means rebuilding it. OpenFilm is one format every agent can read and change:
16
+ a film is a web page that can draw any moment of itself, plus a `film.json` that cuts pages, footage and sound
17
+ together.
18
+
19
+ ## Make a film
20
+
21
+ Give your agent the skill:
22
+
23
+ ```sh
24
+ npx skills add openfilm/openfilm
25
+ ```
26
+
27
+ Then ask for a video:
28
+
29
+ > Make a 20-second launch video for my CLI, with music and sound effects. Check your work and render the MP4.
30
+
31
+ The agent runs `openfilm open`, which makes the project and opens it in OpenFilm Studio so you watch the film take
32
+ shape and can change anything in it. It writes the pages, runs `openfilm look` after every change to see what it made,
33
+ and renders the MP4 beside `film.json`.
34
+
35
+ Needs Node 22 or later and ffmpeg. The first run downloads a headless Chromium (about 100 MB). Studio, the editor,
36
+ comes with the package and runs on your machine.
37
+
38
+ ## Fork a film
39
+
40
+ A film is a folder. Copy one, open it in your agent and say what to change:
41
+
42
+ ```sh
43
+ cp -r examples/edit my-launch && cd my-launch
44
+ ```
45
+
46
+ > Turn this into a launch video for Tally, a CLI that shows what each pull request adds to your cloud bill. Keep the
47
+ > structure, pacing and sound.
48
+
49
+ | Example | What it shows |
50
+ | --- | --- |
51
+ | [`examples/orbit`](examples/orbit) | One page: a WebGL scene (three.js) and a title, with its own sound |
52
+ | [`examples/edit`](examples/edit) | Footage with a slow-motion stretch, a title page used twice with a person's edit, and music |
53
+ | [`examples/compose`](examples/compose) | Two films by different agents cut into one: a picture-in-picture, a crossfade, and a person's edit to the other film's title |
54
+
55
+ ## How it works
56
+
57
+ ```js
58
+ window.film = {
59
+ duration: 20, width: 1920, height: 1080,
60
+ ready: loadFonts(), // a Promise: fonts, images, models, data
61
+ frame(t) { /* draw the complete picture at t seconds, with anything */ },
62
+ audio: [{ src: 'music.wav' }, { src: 'sfx/hit.wav', at: 7.5, volume: 0.8 }],
63
+ };
64
+ ```
65
+
66
+ **The one rule:** the same `t` always draws the same picture, whatever was drawn before. Frames are requested out of
67
+ order, in parallel and between frames (for motion blur), so any page that keeps the rule renders exactly, at any
68
+ size, on any machine. There is nothing else to learn: no timing attributes, no adapters, no components. Use three.js,
69
+ p5, GSAP, SVG, shaders or plain DOM.
70
+
71
+ ```sh
72
+ openfilm open # start or open the project; Studio shows it, live as the agent saves
73
+ openfilm look [t | a-b] # the agent's eyes: a contact sheet with the sound mix, one frame, or a range
74
+ openfilm render [a-b] # the MP4; --4k, --blur (motion blur), --alpha (.webm / .mov)
75
+ openfilm get <what> # voice-over, music, sound effects, images and video: an OpenFilm account or your keys
76
+ ```
77
+
78
+ `look` is what lets an agent finish without you. It draws the frames of its sheet twice, in two shuffled orders, and
79
+ names any frame whose picture depended on a clock, unseeded randomness or what came before. It also reports page
80
+ errors, failed requests, fonts that did not load, missing sounds and clipping.
81
+
82
+ `film.json` is the edit: tracks of clips with `at`, `time`, `speed`, `fade`, `volume` and `transform`. A clip is
83
+ footage, a still, a sound, or another film, so any film can become a shot in someone else's. `overrides` hold a
84
+ person's changes to elements of a page (a CSS selector with position, scale, rotation, style or text), and agents
85
+ keep them when they edit.
86
+
87
+ `openfilm` with no arguments prints [the manual](templates/MANUAL.md), the whole of what an agent needs to know.
88
+ [SPEC.md](SPEC.md) is for anyone building a player, renderer or editor.
89
+
90
+ ## Why no framework
91
+
92
+ Frontier models have read more HTML, CSS and WebGL than any video framework can teach them. Given plain web code and
93
+ one rule, they already make films as good as a framework's. A framework's rules then stop helping and start costing:
94
+ tokens to read, conventions to keep, a runtime every copy depends on. OpenFilm keeps only the rule that makes
95
+ rendering exact, and spends its effort where models really fail: checking the film and rendering it.
96
+
97
+ ## Media
98
+
99
+ Media can come from anywhere: a service you already use (your agent reads its API docs and uses your key), your own
100
+ code, or `openfilm get`, which makes voice-over, music, sound effects, images and video with an OpenFilm account or
101
+ your own key for a service it knows. Creating never needs an account.
102
+
103
+ ## Contributing
104
+
105
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
106
+
107
+ ## License
108
+
109
+ The format, the CLI and Studio are [Apache-2.0](LICENSE). The OpenFilm name and logo are trademarks: see
110
+ [TRADEMARK.md](TRADEMARK.md). The hosted service behind `openfilm get` and the OpenFilm desktop app are provided by the
111
+ OpenFilm project and are not part of this repository.
package/SPEC.md ADDED
@@ -0,0 +1,176 @@
1
+ # OpenFilm Protocol 0.1 (draft)
2
+
3
+ A film is a web page that can draw any moment of itself.
4
+
5
+ That is the whole idea. A page that exposes one object, `window.film`, can be previewed, scrubbed, checked,
6
+ rendered to video, cut into a timeline and edited by any host that speaks this protocol. The page may be written
7
+ with any library or none: three.js, p5, Canvas 2D, WebGL, GSAP, SVG, plain DOM. The protocol says nothing about
8
+ how a page draws, only how a host asks it to.
9
+
10
+ The key words MUST, SHOULD and MAY are used as in RFC 2119.
11
+
12
+ ## 1. The page
13
+
14
+ ```js
15
+ window.film = {
16
+ duration: 12, // seconds, finite, > 0
17
+ width: 1920, height: 1080, // the frame, in CSS pixels
18
+ fps: 30, // optional: the rate the film was designed for
19
+ ready: loadEverything(), // optional: a Promise, or a function returning one
20
+ frame(t, info) { draw(t) }, // draw the picture at t seconds; may return a Promise
21
+ audio: [{ src: 'song.mp3' }], // optional: sound the host mixes under the picture
22
+ };
23
+ ```
24
+
25
+ | Field | Type | Meaning |
26
+ | --- | --- | --- |
27
+ | `duration` | number | Length in seconds. |
28
+ | `width`, `height` | number | Frame size in CSS pixels. The host sizes the viewport to it; higher resolutions use device pixel ratio. |
29
+ | `fps` | number? | The frame rate the film was designed for. Hosts MAY render at any rate. |
30
+ | `ready` | Promise \| () => Promise | Resolves when fonts, images, models and data are loaded. The host calls `frame` only after it settles. For web fonts, load each face used (`document.fonts.load('700 64px Fraunces')`), not only `document.fonts.ready`. |
31
+ | `frame(t, info)` | function | Draw the complete picture for time `t`, `0 ≤ t < duration`. If it returns a Promise, the picture is complete when it resolves. |
32
+ | `audio` | AudioClip[]? | Sound for the film (§4). |
33
+ | `stateful` | boolean? | Set when `frame` cannot jump to an arbitrary `t` (§3). |
34
+ | `reset()` | function? | With `stateful`: return to `t = 0`. |
35
+
36
+ `info` is `{ final: boolean }`: `false` while a person is watching or scrubbing, `true` when the frame is going
37
+ into an export. A page MAY draw cheaper while `final` is false. Nothing else may change between the two.
38
+
39
+ A page SHOULD set `window.film` before its `load` event. Hosts wait for it to appear (for at least 10 seconds
40
+ after `load`) before reporting the page as not a film.
41
+
42
+ ## 2. The one rule: a frame is a function of t
43
+
44
+ For the same `t`, `frame` MUST produce the same picture, whatever was drawn before it and in whatever order frames
45
+ are requested. Hosts rely on this to scrub, to render frames in parallel, to render fractions of a frame for motion
46
+ blur, and to re-render one shot without the rest.
47
+
48
+ In practice:
49
+
50
+ - Read time from `t`. A host holds the page's clocks at `t` while it draws (§5), so `performance.now()`,
51
+ `requestAnimationFrame` timestamps, `Date`, CSS animations and `Math.random` agree with it; timers and history
52
+ do not.
53
+ - Repaint everything each call. Do not accumulate (`x += v`) across calls; if something has history (a trail, a
54
+ physics run), compute it from `t`, or precompute it during `ready` and look it up.
55
+ - Tweens that record their start values the first time they run (GSAP `to`, `from`) remember whatever was on screen
56
+ then, which depends on the order frames were drawn. Give every tween explicit start and end values (`fromTo`).
57
+ - Any library that runs its own clock is driven by `t` instead: `timeline.seek(t)`, `animation.currentTime = t * 1000`,
58
+ `p5.redraw()`, `renderer.render(scene, camera)`, `video.currentTime = …` (awaiting `seeked`).
59
+
60
+ Notes for authors:
61
+
62
+ - `window.film` may be assigned from a module script (`<script type="module">`): modules run before `load`.
63
+ - A WebGL canvas is captured after `frame` returns. Draw inside `frame` and create the context with
64
+ `preserveDrawingBuffer: true`, or the host may capture a cleared buffer.
65
+ - Size everything from `width`/`height` in CSS pixels. Hosts raise resolution with device pixel ratio, so a canvas
66
+ should allocate `width × devicePixelRatio` pixels.
67
+ - Repeating a frame is not always bit-identical in a browser (layers are re-rasterized as they change). Hosts compare
68
+ repeated frames with a tolerance, not byte for byte.
69
+
70
+ ## 3. Stateful films
71
+
72
+ Some films cannot jump: an agent simulation, a feedback shader. Such a page sets `stateful: true` and MAY provide
73
+ `reset()`. A host then only calls `frame` with increasing `t` after a `reset()` (or a reload) and reaches a later
74
+ `t` by stepping through the frames before it. Stateful films cannot be rendered in parallel or with motion blur
75
+ from fractions of a frame. Prefer computing state from `t`.
76
+
77
+ ## 4. Sound
78
+
79
+ Pages do not play sound when a host is present (§5). They declare it:
80
+
81
+ ```js
82
+ audio: [
83
+ { src: 'music/song.mp3' }, // whole file from t = 0
84
+ { src: 'vo/line-3.m4a', at: 4.2, volume: 0.9 }, // starts at 4.2 s
85
+ { src: 'sfx/hit.wav', at: 7.5, from: 0.1, to: 0.6 }, // a slice of the source
86
+ ]
87
+ ```
88
+
89
+ `src` is resolved against the page URL. `at` is the start in film seconds (default 0); `from`/`to` trim the source
90
+ in source seconds (default: whole file); `volume` is linear gain (default 1); `speed` is the playback rate (default
91
+ 1, pitch kept). The host mixes and encodes the sound;
92
+ it never records it from the page.
93
+
94
+ Timing that follows sound (beats, onsets, word timings) is data, not API: precompute it into files the page reads
95
+ during `ready`. Hosts MAY provide tools that write such files.
96
+
97
+ ## 5. Hosts
98
+
99
+ Before any page script runs, a host sets `window.filmHost = { version: '0.1', name }`. When it is present the page
100
+ MUST NOT advance on its own (no self-running loop, no autoplay of media or sound). When it is absent the page is
101
+ being opened as an ordinary web page and SHOULD play itself, so every film is also a page you can just open:
102
+
103
+ ```js
104
+ if (!window.filmHost) {
105
+ const t0 = performance.now();
106
+ const loop = () => { film.frame(((performance.now() - t0) / 1000) % film.duration, { final: false }); requestAnimationFrame(loop); };
107
+ Promise.resolve(typeof film.ready === 'function' ? film.ready() : film.ready).then(loop);
108
+ }
109
+ ```
110
+
111
+ A host:
112
+
113
+ - serves the page over HTTP (not `file://`), with the page's folder as the root;
114
+ - sizes the viewport to `width × height` (times a device pixel ratio for higher resolutions);
115
+ - awaits `ready`, then calls `frame(t, info)` and awaits its result before capturing the viewport;
116
+ - MAY call `frame` concurrently in separate page instances, and at fractional frame times;
117
+ - passes `final: true` only for frames that go into an export;
118
+ - reports uncaught errors and failed requests from the page.
119
+
120
+ From the first frame it draws, a host holds the page's clocks at the frame's `t`: `performance.now()` and
121
+ `requestAnimationFrame` timestamps read `t` in milliseconds, `Date` reads `t` after the epoch 2026-01-01T00:00:00Z,
122
+ CSS and Web Animations the page left running stand at `t` (a CSS transition, whose progress is history, is
123
+ finished), and `Math.random` restarts from a seed of `t` (while loading it runs from a fixed seed). Before the first
124
+ frame (`ready`, loading) the page runs on real time. The reference host's prelude is `FILM_CLOCK` in
125
+ `src/host.mjs`: `filmHost.time(t)` before `frame(t)`, `filmHost.settle(t)` after it.
126
+
127
+ A page may play footage with native `<video>` elements and set `currentTime` from `t`. The host makes that exact:
128
+ a media seek lands inside the frame it names, and the host waits for pending seeks before it captures.
129
+
130
+ ## 6. The edit: film.json
131
+
132
+ A film page draws a shot. An edit, the part a person adjusts by hand in an editor, is data: `film.json`.
133
+
134
+ ```json
135
+ {
136
+ "stage": { "w": 1920, "h": 1080 },
137
+ "tracks": [
138
+ { "clips": [{ "src": "scenes/title.html", "id": "title", "at": 1, "transform": { "t": [0, 0], "r": 0, "s": [1, 1] },
139
+ "overrides": [{ "at": "#title", "text": "Aura Pods", "style": { "color": "#7fd4ff" }, "t": [0, -40] }] }] },
140
+ { "clips": [{ "src": "footage/a.mp4", "id": "open", "at": 0, "time": [12.2, 15], "speed": 0.5, "volume": 0.8 }] },
141
+ { "clips": [{ "src": "music/bed.mp3", "id": "bed", "at": 0, "time": [0, 10], "volume": 0.5 }] }
142
+ ]
143
+ }
144
+ ```
145
+
146
+ - `stage`: the frame in CSS pixels.
147
+ - `tracks`: track 0 is on top. A track has no kind: any clip goes on any track, and what a clip is comes from its
148
+ file — a film page (`.html`), a video, a still picture or a sound (by extension). Clips on one track do not
149
+ overlap; overlapping pictures (a crossfade, a picture-in-picture) sit on different tracks. A track may be `locked`,
150
+ `hidden` (no picture and no sound) or `muted`. A `kind` written by an earlier edition is read and dropped.
151
+ - A clip's source (a video, a still, a page) is first fitted whole inside the stage and centered (contain); then its
152
+ `transform` applies, as in Figma: `t` [x, y] moves its top-left corner in px, `s` [x, y] scales its width and height
153
+ (the top-left corner stays), `r` rotates it clockwise in degrees about its own center.
154
+ - A clip: `src` (relative to film.json), `id` (unique across the film; without it, the file's name), `at` (start on
155
+ the film, seconds; default 0), `time` ([in, out] in the source's own seconds; default: all of it; a still's `time`
156
+ is its length), `fade` (`[in, out]` seconds, or one number for both: opacity for picture, gain for sound),
157
+ `transform` on pictures (pages, videos, stills), `volume` on what has sound (pages, videos, sounds; linear gain, 0
158
+ mutes), and `speed` on videos and sounds (playback rate from 0.25 to 4, sound keeps its pitch): the clip lasts
159
+ `(out − in) / speed` on the film. A field the clip's kind does not take is an error.
160
+ - A page clip is a film page: the host calls its `frame` with the clip's local time `in + (t − at)`, holding the
161
+ page's last frame when the clip runs past the page's end, and its `audio` is placed and trimmed with the clip.
162
+ - Any other field is an error: a host reads film.json strictly, so a misspelled field is reported, not ignored. The
163
+ reference reading is `src/film-doc.mjs`.
164
+ - `overrides` on a page clip are a person's tweaks to elements of the page, applied by the host after each frame:
165
+ `at` (a CSS selector), optional `n` (which match, from 1), `t` (translate px), `s` (scale), `r` (rotate degrees) as
166
+ the independent CSS transform properties so they compose with the page's own animation, `style` (CSS properties,
167
+ applied with priority), `text` (replaces the text of an element without children).
168
+ - The sound of a `video` clip joins the mix unless its `volume` is 0.
169
+
170
+ A host plays a film.json as a film (it is itself a protocol page), so a film.json can be previewed, checked and
171
+ rendered like any page. Editors change film.json; the pages stay as their authors wrote them.
172
+
173
+ ## 7. Versioning
174
+
175
+ This is version 0.1. Fields may be added; their absence must keep meaning what it means today. A page MAY declare
176
+ `film.version`.
package/TRADEMARK.md ADDED
@@ -0,0 +1,25 @@
1
+ # OpenFilm trademarks
2
+
3
+ The code in this repository is licensed under [Apache-2.0](LICENSE): use it, change it, ship it. The license does not
4
+ cover the OpenFilm name and logo (Apache-2.0, section 6). This page says how you can use them, so that people always
5
+ know what comes from the OpenFilm project and what does not.
6
+
7
+ ## You can
8
+
9
+ - Say what your work does with OpenFilm: "made with OpenFilm", "an OpenFilm film", "works with OpenFilm", "for the
10
+ OpenFilm format".
11
+ - Refer to OpenFilm in articles, talks, tutorials, courses and videos.
12
+ - Fork this repository and keep the name while you work on changes you mean to send back.
13
+ - Redistribute unmodified releases under their own name.
14
+
15
+ ## You can't, without written permission
16
+
17
+ - Name a modified version, a product, a hosted service or a company "OpenFilm", or anything easily confused with it.
18
+ A fork you publish as your own needs its own name.
19
+ - Use the OpenFilm logo for your own product or service, or a logo that looks like it.
20
+ - Suggest that the OpenFilm project made, endorses or supports your work when it does not.
21
+ - Use "openfilm" in a domain name, package name or account name in a way that looks official.
22
+
23
+ ## Questions
24
+
25
+ Write to legal@openfilm.dev. If you are not sure whether a use is fine, ask: most are.
package/bin/openfilm.mjs CHANGED
@@ -1,2 +1,154 @@
1
1
  #!/usr/bin/env node
2
- console.log('OpenFilm is coming soon: https://openfilm.dev');
2
+ import { parseArgs } from 'node:util';
3
+
4
+ const USAGE = `openfilm — a film is a function of time: given t, it draws that moment
5
+
6
+ openfilm open [folder] start or open the project; Studio shows it to the person
7
+ openfilm look [what] [t | a-b] check the film and draw it: a contact sheet with its sound and loudness
8
+ openfilm render [a-b] the MP4, saved beside film.json
9
+ openfilm get [what] [--options] voice-over, music, sound effects, transcripts, images, video, made by a provider
10
+
11
+ Run with no arguments for the manual; \`openfilm <command> --help\` for one command's options.
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: `openfilm open [folder]
18
+
19
+ Start or open a project (default: the current folder). A folder that is not a project yet gets an empty
20
+ film.json, { "stage": { "w": 1920, "h": 1080 }, "tracks": [] }; nothing else is created.
21
+ Starts OpenFilm Studio if it is not running and prints its address. A Studio page already open switches to the
22
+ project; otherwise open the address in your built-in browser, and if no page connects within 15 s, Studio opens
23
+ the person's browser (OPENFILM_NO_BROWSER=1: never).`,
24
+ look: `openfilm look [what] [t | a-b]
25
+
26
+ what the project (default), a film page (.html), or a media file (video, image, sound)
27
+ t one frame at full size, e.g. 4.5
28
+ a-b a dense sheet of a stretch, e.g. 3-6
29
+ without either: a contact sheet of the whole thing, its sound and loudness, and a check that
30
+ the same t draws the same picture
31
+
32
+ Writes PNGs under .film/look/ of the project and prints their paths.
33
+
34
+ --count <n> frames in a sheet (12)`,
35
+ render: `openfilm render [a-b]
36
+
37
+ Render the project around the current folder to video.
38
+ The whole film goes to <project>/<folder name>.mp4, beside film.json; a range (a-b) goes to .film/render/.
39
+
40
+ --out <file> another path; .webm or .mov with --alpha
41
+ --fps <n> frame rate (30)
42
+ --4k 3840 px wide --scale <n> any device pixel ratio
43
+ --blur motion blur (8 samples) --samples <n> --shutter <0..1>
44
+ --alpha transparent background --workers <n> parallel browsers`,
45
+ };
46
+
47
+ /* `openfilm` alone: the manual, the whole of what an agent needs to know. `get` takes any options its verb has,
48
+ so it goes to Studio (studio/client.mjs) before the fixed options below are parsed. */
49
+ const RAW = process.argv.slice(2);
50
+ /* the command as the agent can type it here: `npx openfilm` once published, `openfilm` where it is installed */
51
+ const RUN = process.env.npm_command === 'exec' ? 'npx openfilm' : 'openfilm';
52
+ if (RAW.length === 0) {
53
+ const { readFileSync } = await import('node:fs');
54
+ console.log(readFileSync(new URL('../templates/MANUAL.md', import.meta.url), 'utf8').replaceAll('npx openfilm', RUN));
55
+ process.exit(0);
56
+ }
57
+ if (RAW[0] === 'get') {
58
+ let result;
59
+ try { result = await (await import('../studio/client.mjs')).getMedia(RAW.slice(1), (line) => console.error(line)); }
60
+ catch (e) { result = { code: 1, lines: [`openfilm get: ${e.message}`] }; }
61
+ for (const line of result.lines) console.log(line);
62
+ process.exit(result.code);
63
+ }
64
+
65
+ const { values: o, positionals } = parseArgs({
66
+ allowPositionals: true,
67
+ options: {
68
+ count: { type: 'string' }, out: { type: 'string', short: 'o' }, '4k': { type: 'boolean' }, scale: { type: 'string' },
69
+ blur: { type: 'boolean' }, samples: { type: 'string' }, shutter: { type: 'string' }, alpha: { type: 'boolean' },
70
+ fps: { type: 'string' }, workers: { type: 'string' }, help: { type: 'boolean', short: 'h' },
71
+ },
72
+ });
73
+ const [cmd, ...args] = positionals;
74
+ /* the help speaks the name this was run as: `openfilm` or `npx openfilm` */
75
+ const NAME = RUN;
76
+ const named = (text) => text.replace(/^( {2})?openfilm(?= )/gm, `$1${NAME}`).replace('`openfilm <command>', `\`${NAME} <command>`);
77
+ const HELP = named(USAGE);
78
+ if (o.help && COMMAND_HELP[cmd]) { console.log(named(COMMAND_HELP[cmd])); process.exit(0); }
79
+ if (o.help || !cmd) { console.log(HELP); process.exit(0); }
80
+ const num = (v) => (v == null ? undefined : Number(v));
81
+ 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) };
82
+ const commands = {
83
+ open: () => open(args[0] ?? '.'),
84
+ look: () => import('../src/look.mjs').then((m) => m.look(args, opts)),
85
+ render: () => import('../src/render.mjs').then((m) => m.render(args, opts)),
86
+ };
87
+ /** Make (or find) the project and show it in Studio; `.film/opened` marks a project Studio has shown. @param {string} folder */
88
+ async function open(folder) {
89
+ const { openInStudio } = await import('../studio/client.mjs');
90
+ const { mkdirSync, writeFileSync } = await import('node:fs');
91
+ const { join } = await import('node:path');
92
+ const fallback = process.env.OPENFILM_NO_BROWSER ? 0 : 15;
93
+ try {
94
+ const { project, url, pages } = await openInStudio(folder, { fallback, say: (line) => console.error(line) });
95
+ mkdirSync(join(project.path, '.film'), { recursive: true });
96
+ writeFileSync(join(project.path, '.film', 'opened'), '');
97
+ return { ok: true, lines: [`project ${project.path}`, `Studio ${url}`, pages
98
+ ? ' the Studio page already open switched to this project'
99
+ : fallback ? ` open it in your built-in browser if you have one; otherwise it opens in the person's browser in ${fallback} s`
100
+ : ' open it in your built-in browser, or give it to the person'] };
101
+ } catch (e) { return { ok: false, lines: [`${NAME} open: ${e.message}`] }; }
102
+ }
103
+ if (!commands[cmd]) { console.error(`${NAME}: no command ${cmd}\n\n${HELP}`); process.exit(2); }
104
+
105
+ if (cmd === 'look' || cmd === 'render') {
106
+ const { existsSync } = await import('node:fs');
107
+ const { dirname, join, resolve } = await import('node:path');
108
+ /* a project is made by \`open\`: look and render work in one (the one around here, or around what they look at) */
109
+ const around = (from) => { for (let up = resolve(from); up !== dirname(up); up = dirname(up)) if (existsSync(join(up, 'film.json'))) return up; return null; };
110
+ const target = args.find((a) => !/^\d+(\.\d+)?(-\d+(\.\d+)?)?$/.test(a));
111
+ const project = around('.') ?? (target ? around(existsSync(target) && !/\.html?$/i.test(target) ? dirname(resolve(target)) : target) : null);
112
+ if (!project) { console.error(`${NAME} ${cmd}: ${resolve('.')} is not in a project; \`${NAME} open\` makes one and shows it in Studio`); process.exit(2); }
113
+ /* a project Studio has never shown (copied, cloned, forked): show it now, so the person watches from the start */
114
+ if (!existsSync(join(project, '.film', 'opened'))) {
115
+ const shown = await open(project);
116
+ for (const line of shown.lines) console.error(line);
117
+ if (!shown.ok) console.error(`${NAME} ${cmd}: carrying on without Studio`);
118
+ }
119
+ }
120
+
121
+ /** The first run on a machine has no browser yet: fetch Playwright's Chromium once, then run the command again. */
122
+ async function installChromium() {
123
+ const { createRequire } = await import('node:module');
124
+ const { spawnSync } = await import('node:child_process');
125
+ const { dirname, join } = await import('node:path');
126
+ const cli = join(dirname(createRequire(import.meta.url).resolve('playwright-core/package.json')), 'cli.js');
127
+ console.error(`${NAME}: first run on this machine, downloading the browser films are drawn in (about 100 MB, once)`);
128
+ const r = spawnSync(process.execPath, [cli, 'install', 'chromium-headless-shell'], { stdio: ['ignore', 'inherit', 'inherit'] });
129
+ if (r.status !== 0) throw new Error('could not download the browser; run `npx playwright-core install chromium-headless-shell` and try again');
130
+ }
131
+
132
+ if (cmd === 'look' || cmd === 'render') {
133
+ const { spawnSync } = await import('node:child_process');
134
+ const missing = () => ['ffmpeg', 'ffprobe'].some((bin) => spawnSync(bin, ['-version'], { stdio: 'ignore' }).error);
135
+ if (missing()) {
136
+ console.error(`${NAME} ${cmd}: needs ffmpeg and ffprobe on the PATH (macOS: brew install ffmpeg · Debian/Ubuntu: sudo apt install ffmpeg · Windows: winget install ffmpeg)`);
137
+ process.exit(2);
138
+ }
139
+ }
140
+
141
+ try {
142
+ let result;
143
+ try { result = await commands[cmd](); }
144
+ catch (e) {
145
+ if (!/Executable doesn't exist/.test(e.message)) throw e;
146
+ await installChromium();
147
+ result = await commands[cmd]();
148
+ }
149
+ for (const line of result.lines) console.log(line);
150
+ if (result.ok === false) process.exitCode = 1;
151
+ } catch (e) {
152
+ console.error(`${NAME} ${cmd}: ${e.message}`);
153
+ process.exit(1);
154
+ }
package/package.json CHANGED
@@ -1,10 +1,75 @@
1
1
  {
2
2
  "name": "openfilm",
3
- "version": "0.0.1",
4
- "description": "OpenFilm: make any video, change anything. A film is a web page that draws any moment of itself. Coming soon.",
5
- "homepage": "https://openfilm.dev",
3
+ "version": "0.1.0",
4
+ "description": "OpenFilm: a film is a web page that can draw any moment of itself. The spec, the agent manual and the `openfilm` command.",
6
5
  "license": "Apache-2.0",
7
- "bin": { "openfilm": "bin/openfilm.mjs" },
8
- "files": ["bin", "README.md"],
9
- "engines": { "node": ">=22" }
6
+ "homepage": "https://openfilm.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/openfilm/openfilm.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/openfilm/openfilm/issues"
13
+ },
14
+ "keywords": [
15
+ "video",
16
+ "motion-graphics",
17
+ "agents",
18
+ "claude-code",
19
+ "codex",
20
+ "animation",
21
+ "render"
22
+ ],
23
+ "type": "module",
24
+ "bin": {
25
+ "openfilm": "bin/openfilm.mjs"
26
+ },
27
+ "exports": {
28
+ "./film-doc": "./src/film-doc.mjs",
29
+ "./host": "./src/host.mjs",
30
+ "./shared": "./src/shared.mjs",
31
+ "./look": "./src/look.mjs",
32
+ "./render": "./src/render.mjs",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "README.md",
37
+ "SPEC.md",
38
+ "TRADEMARK.md",
39
+ "bin",
40
+ "skills",
41
+ "src",
42
+ "!src/*.test.mjs",
43
+ "templates",
44
+ "studio/server",
45
+ "studio/client.mjs",
46
+ "studio/ui/dist",
47
+ "!studio/**/*.test.mjs"
48
+ ],
49
+ "engines": {
50
+ "node": ">=22"
51
+ },
52
+ "dependencies": {
53
+ "playwright-core": "1.61.1"
54
+ },
55
+ "scripts": {
56
+ "test": "node --test --test-concurrency=1 src/*.test.mjs studio/*.test.mjs studio/server/*.test.mjs studio/server/providers/*.test.mjs studio/ui/src/editor/*.test.ts",
57
+ "typecheck": "tsc -p studio/server && tsc -p studio/ui",
58
+ "build:ui": "vite build --config studio/ui/vite.config.ts",
59
+ "prepack": "npm run build:ui"
60
+ },
61
+ "devDependencies": {
62
+ "@fontsource-variable/inter": "^5.3.0",
63
+ "@tailwindcss/vite": "^4.3.3",
64
+ "@types/node": "^22.20.5",
65
+ "@types/react": "^19.3.0",
66
+ "@types/react-dom": "^19.3.0",
67
+ "@vitejs/plugin-react": "^5.2.0",
68
+ "lucide-react": "^0.469.0",
69
+ "react": "^19.3.0",
70
+ "react-dom": "^19.3.0",
71
+ "tailwindcss": "^4.3.3",
72
+ "typescript": "^5.9.3",
73
+ "vite": "^7.3.6"
74
+ }
10
75
  }
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: openfilm
3
+ description: Make videos as web pages with OpenFilm. Use when the user asks for a video, motion graphics, an animation, a launch film, an explainer, a music video, or to edit, cut, check or render a film (film.json and pages with window.film).
4
+ ---
5
+
6
+ # OpenFilm
7
+
8
+ Run `npx -y openfilm` and follow the manual it prints: it is the whole of OpenFilm, a few hundred words. Run it again
9
+ whenever you need it.