@hraness/slopcamera 3.6.0 → 3.7.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 (49) hide show
  1. package/README.md +41 -26
  2. package/apps/desktop/README.md +1 -1
  3. package/apps/desktop/application/html-overlay-renderer.ts +6 -0
  4. package/apps/desktop/application/operation.ts +1 -1
  5. package/apps/desktop/cli/args.ts +60 -0
  6. package/apps/desktop/cli/command-host-resources.ts +5 -0
  7. package/apps/desktop/cli/commands.ts +13 -1
  8. package/apps/desktop/cli/help.ts +26 -4
  9. package/apps/desktop/cli/html-film-init.ts +158 -0
  10. package/apps/desktop/cli/html-film-names.ts +17 -0
  11. package/apps/desktop/cli/html-film.ts +471 -0
  12. package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
  13. package/apps/desktop/cli/html-scene.ts +3 -3
  14. package/apps/desktop/cli/main.ts +1 -0
  15. package/apps/desktop/cli/menubar-status.ts +1 -0
  16. package/apps/desktop/cli/portable-surface.ts +55 -1
  17. package/apps/desktop/dist/cli/main.js +328 -293
  18. package/apps/desktop/html-overlay/film.ts +418 -0
  19. package/dist/cli.js +13 -2
  20. package/dist/generate.js +1 -1
  21. package/dist/{index-4z9y2wa2.js → index-842t02pp.js} +1 -1
  22. package/dist/{index-qxfexvj3.js → index-nv3byqa1.js} +20 -4
  23. package/dist/index-p74xy76d.js +3 -0
  24. package/dist/{index-fevf4ppk.js → index-y8spxs77.js} +10 -7
  25. package/dist/index.js +1 -1
  26. package/dist/operations.js +1 -1
  27. package/dist/workflow.js +1 -1
  28. package/docs/README.md +5 -5
  29. package/examples/html/launch-film/README.md +51 -0
  30. package/examples/html/launch-film/build.ts +158 -0
  31. package/examples/html/launch-film/film.css +593 -0
  32. package/examples/html/launch-film/film.html +85 -0
  33. package/examples/html/launch-film/film.js +410 -0
  34. package/examples/html/launch-film/film.json +63 -0
  35. package/examples/html/launch-film/mockups.tsx +74 -0
  36. package/examples/html/launch-film/timeline.ts +57 -0
  37. package/package.json +13 -6
  38. package/skills/slopcamera/SKILL.md +3 -2
  39. package/skills/slopcamera/references/install.md +3 -3
  40. package/skills/slopcamera/references/launch-films.md +154 -0
  41. package/skills/slopcamera/references/visual-communication.md +1 -1
  42. package/src/cli.ts +125 -9
  43. package/src/generation-pricing.ts +21 -1
  44. package/src/icon-set.ts +1086 -0
  45. package/src/icon.ts +396 -59
  46. package/src/index.ts +1 -0
  47. package/src/operations.ts +52 -0
  48. package/src/version.ts +1 -1
  49. package/dist/index-nxtmy18t.js +0 -3
package/README.md CHANGED
@@ -1,22 +1,22 @@
1
- # Slopcamera
1
+ # SlopCamera
2
2
 
3
- [![Slopcamera: Visual work your agent can keep revising.](https://slopcamera.com/og.png)](https://slopcamera.com)
3
+ [![SlopCamera: Images, diagrams, animation, 3D and video your coding agent can keep revising.](https://slopcamera.com/og.png)](https://slopcamera.com)
4
4
 
5
- **Slopcamera is a media studio for coding agents. Codex, Claude Code, and other
5
+ **SlopCamera is a media studio for coding agents. Codex, Claude Code, and other
6
6
  agents make images, diagrams, animation, 3D scenes, and edited video from source
7
7
  files they can keep revising.**
8
8
 
9
9
  Describe the result you want to Codex, Claude Code, or another coding agent.
10
- Slopcamera gives the agent a Bun CLI, a TypeScript SDK, and an Agent Skill matched
10
+ SlopCamera gives the agent a Bun CLI, a TypeScript SDK, and an Agent Skill matched
11
11
  to the CLI version, so it can inspect sources, set up cameras, edit a composition,
12
12
  and render. `slopcamera mcp` runs an MCP server with a smaller, fixed set of diagram,
13
- image, and scene tools. Projects stay in local files, and there is no Slopcamera
13
+ image, and scene tools. Projects stay in local files, and there is no SlopCamera
14
14
  account. Generation uses your own Vercel AI Gateway account or, for prompt-only
15
15
  images, prepaid Hraness Credits.
16
16
 
17
17
  [Install](#install-slopcamera) · [Make a first diagram](#make-your-first-diagram) · [Capabilities](#what-slopcamera-does) · [Documentation](https://slopcamera.com/docs) · [slopcamera.com](https://slopcamera.com)
18
18
 
19
- ## Made with Slopcamera
19
+ ## Made with SlopCamera
20
20
 
21
21
  [![An original brass optical instrument rendered in Blender](apps/web/media/native-product-poster-51b1658c46d3.webp)](https://slopcamera.com/docs/tutorials/first-native-film#inspect-the-finished-example)
22
22
 
@@ -27,7 +27,7 @@ The [portrait geometry lesson](https://slopcamera.com/docs/how-to/educational-vi
27
27
  adds mathematical typesetting, a presenter and authored captions. Each guide pairs
28
28
  the rendered output with source, runtime requirements and reproduction steps.
29
29
 
30
- ## Why Slopcamera
30
+ ## Why SlopCamera
31
31
 
32
32
  - **Keep creative work editable.** Native scenes, portable scenes, diagrams,
33
33
  and video projects retain their own sources and settings. Rendered frames
@@ -45,16 +45,29 @@ the rendered output with source, runtime requirements and reproduction steps.
45
45
  instead run on the hosted API with prepaid Hraness Credits. Native Python
46
46
  authoring requires separate trust because it runs as your current user.
47
47
 
48
- ## Install Slopcamera
48
+ ## Compared with other tools
49
49
 
50
- Slopcamera v3.5.0 includes 17 reusable film and animation direction profiles, deterministic exposure helpers, and a sample renderer that defaults to native 4K. Use the [style direction guide](docs/how-to/direct-visual-styles.md) to author cel animation, pixel art, mathematical explanations, and historical film treatments from retained sources. On macOS, `slopcamera menubar` puts Slopcamera in the menu bar and explains each macOS notice before it appears.
50
+ - [Remotion](https://www.remotion.dev/) renders React components to video and can
51
+ render on AWS Lambda. It is source-available and needs a paid license for
52
+ companies of four or more people.
53
+ - [HyperFrames](https://github.com/heygen-com/hyperframes) renders HTML to MP4
54
+ under Apache 2.0, with optional HeyGen-hosted rendering.
55
+ - [MCP for Blender](https://github.com/ahujasid/mcp-for-blender), formerly Blender
56
+ MCP, lets an agent drive a running Blender interactively.
57
+
58
+ SlopCamera renders HTML motion too, and adds diagrams, Three.js scenes, headless
59
+ Blender, CadQuery, and Manim renders, and edits of your own footage in one local
60
+ project. It has no cloud rendering and a much smaller community. See
61
+ [the full comparison](https://slopcamera.com/docs/explanation/why-slopcamera#compared-with-other-tools).
62
+
63
+ ## Install SlopCamera
51
64
 
52
65
  Install [Bun 1.3.14 or newer](https://bun.sh), then install the verified
53
- [Slopcamera v3.5.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.5.0)
66
+ [SlopCamera v3.6.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.6.0)
54
67
  from its canonical archive:
55
68
 
56
69
  ```sh
57
- bun add --global https://github.com/hraness/slopcamera/releases/download/v3.5.0/hraness-slopcamera-3.5.0.tgz
70
+ bun add --global https://github.com/hraness/slopcamera/releases/download/v3.6.0/hraness-slopcamera-3.6.0.tgz
58
71
  slopcamera skill install --target agents
59
72
  ```
60
73
 
@@ -90,8 +103,8 @@ Native engines install separately. The [source-install guide](docs/how-to/use-cu
90
103
  explains workspace placement and durable-run identity; the
91
104
  [capability reference](docs/reference/capabilities.md) names runtime requirements.
92
105
 
93
- Slopcamera is a CLI. On macOS, an optional unbundled menu-bar companion shows
94
- what Slopcamera is rendering, how the last job ended, your last known credits
106
+ SlopCamera is a CLI. On macOS, an optional unbundled menu-bar companion shows
107
+ what SlopCamera is rendering, how the last job ended, your last known credits
95
108
  balance and the newest files in the agent outputs directory; `slopcamera menubar` runs a prebuilt
96
109
  companion directly and does not install or launch an application bundle. The
97
110
  companion is a thin product binary over the shared
@@ -106,14 +119,14 @@ when working from source.
106
119
  [Atet v3.2.3](https://github.com/hraness/atet/releases/tag/v3.2.3) and its
107
120
  [original archive](https://github.com/hraness/atet/releases/download/v3.2.3/hraness-atet-3.2.3.tgz)
108
121
  remain historical publication evidence. Those immutable bytes install Atet,
109
- not Slopcamera. Renaming the repository does not create a renamed package or
122
+ not SlopCamera. Renaming the repository does not create a renamed package or
110
123
  change an existing release.
111
124
 
112
125
  </details>
113
126
 
114
127
  ## Make your first diagram
115
128
 
116
- After installing Slopcamera, this local task needs no model account. In a new directory,
129
+ After installing SlopCamera, this local task needs no model account. In a new directory,
117
130
  create the included diagram, check it, and render it:
118
131
 
119
132
  ```sh
@@ -136,10 +149,12 @@ For detailed native 3D, follow
136
149
  [Your first native film](docs/tutorials/first-native-film.md). To generate editable
137
150
  architecture from dimensions, [build a parametric design](https://slopcamera.com/docs/how-to/parametric-design).
138
151
 
139
- ## What Slopcamera does
152
+ ## What SlopCamera does
140
153
 
141
- Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes,
154
+ SlopCamera lets your coding agent make images, diagrams, animation, 3D scenes,
142
155
  and video from source files it can keep revising.
156
+ Direct cel animation, pixel art, math explainers, and period film looks with the
157
+ [style direction guide](docs/how-to/direct-visual-styles.md).
143
158
 
144
159
  ### Author scenes and direct cameras
145
160
 
@@ -158,7 +173,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
158
173
  capture appearance; they do not establish collision geometry or editable native
159
174
  meshes. See [Directed scenes](docs/spatial-scenes.md).
160
175
 
161
- Slopcamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
176
+ SlopCamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
162
177
  document describes beats, actions, camera coverage, and look intents, compiles
163
178
  into proposed performance/camera/cinema/material-lighting/shot documents, and
164
179
  fans out into bounded galleries per axis. Declared effects bind into renders,
@@ -174,12 +189,12 @@ slopcamera workflows plan cinematic-world --input pack-input.json --json
174
189
  The `cinematic-world` workflow consumes one inert recipe pack — a bounded,
175
190
  content-addressed JSON document an agent can author — and stops before
176
191
  selection. Registration and selection stay explicit `scene project` operations.
177
- See [Direct a cinematic world](docs/how-to/direct-cinematic-worlds.md) and the
192
+ See [Plan camera moves, lighting, and effects for a 3D scene](docs/how-to/direct-cinematic-worlds.md) and the
178
193
  [extension architecture](docs/extension-architecture.md).
179
194
 
180
195
  ### Film native worlds and educational animation
181
196
 
182
- Slopcamera can direct Blender for detailed sets, materials,
197
+ SlopCamera can direct Blender for detailed sets, materials,
183
198
  lighting, skinned characters, cloth and liquid caches; CadQuery for parametric
184
199
  solids and STEP; and Manim Community for mathematical animation. Seven editable
185
200
  starters include a product, character, shaded street, cloth, liquid, CAD bracket,
@@ -195,7 +210,7 @@ camera clock for reuse. Supported GLB derivatives and calibrated cameras can
195
210
  cross between native and portable scenes; rigs, solvers, and procedural materials
196
211
  remain native. Blender and Python environments are installed separately, and
197
212
  source execution requires explicit current-user trust. See [Native film studio](docs/studio.md)
198
- and [Make an educational video](docs/how-to/educational-video.md).
213
+ and [Make a math explainer video with Manim](docs/how-to/educational-video.md).
199
214
 
200
215
  ### Build diagrams and motion graphics
201
216
 
@@ -269,12 +284,12 @@ slopcamera workflows list --json
269
284
  slopcamera workflows show social-variants --json
270
285
  ```
271
286
 
272
- ## How Slopcamera works
287
+ ## How SlopCamera works
273
288
 
274
289
  Keep the source that owns each creative decision. A native scene owns a rig or
275
290
  simulation; a portable scene owns supported geometry, cameras, and media surfaces;
276
291
  a diagram owns its objects and labels; a video project owns cuts and delivery.
277
- Slopcamera connects these through explicit assets and rendered derivatives.
292
+ SlopCamera connects these through explicit assets and rendered derivatives.
278
293
 
279
294
  1. **Prepare the sources.** Import footage and assets or author a scene, diagram,
280
295
  or native program. Inspect available tools before choosing an engine.
@@ -318,7 +333,7 @@ declarative graphs, approvals, and resuming work.
318
333
 
319
334
  ## Design and trust
320
335
 
321
- There is no Slopcamera account, hosted project database, or browser generation service.
336
+ There is no SlopCamera account, hosted project database, or browser generation service.
322
337
  Ordinary editing and rendering remain local. Gateway generation and selected
323
338
  cloud analysis use credentials from the local process and request explicit
324
339
  acknowledgement before uploading named media. The optional hosted API runs each
@@ -349,7 +364,7 @@ vision](https://algal.computer/docs/vision/) states the bet behind it.
349
364
  ## Documentation
350
365
 
351
366
  - **Learn:** [Your first diagram](docs/tutorials/first-diagram.md) · [Your first native film](docs/tutorials/first-native-film.md).
352
- - **Make a result:** [Edit video](docs/how-to/edit-video.md) · [Generate media](docs/how-to/generate-media.md) · [Run workflows](docs/how-to/run-workflows.md) · [Educational video](docs/how-to/educational-video.md) · [Direct a cinematic world](docs/how-to/direct-cinematic-worlds.md).
367
+ - **Make a result:** [Edit video](docs/how-to/edit-video.md) · [Generate media](docs/how-to/generate-media.md) · [Run workflows](docs/how-to/run-workflows.md) · [Educational video](docs/how-to/educational-video.md) · [Plan camera moves, lighting, and effects for a 3D scene](docs/how-to/direct-cinematic-worlds.md).
353
368
  - **Look up support:** [Capabilities and release availability](docs/reference/capabilities.md) · [SDK entrypoints](docs/reference/sdk.md) · [Creative tools](docs/html-overlay-creative-toolkit.md).
354
369
  - **Understand the system:** [Architecture](docs/architecture.md) · [Native studio](docs/studio.md) · [Directed scenes](docs/spatial-scenes.md) · [Extension architecture](docs/extension-architecture.md).
355
370
 
@@ -413,4 +428,4 @@ an acknowledgment; the unacknowledged offer expires after ten minutes and does n
413
428
  consume the weekly shown cooldown. `support dismiss` opts out across the local suite;
414
429
  `support snooze` pauses for 30 days. `HRANESS_SUPPORT_AUDIENCE=off` disables ambient
415
430
  notices and claimed offers. Payment is an explicit human choice on the shared support
416
- page; Slopcamera has no product-updates mailing list or account-status lookup.
431
+ page; SlopCamera has no product-updates mailing list or account-status lookup.
@@ -1,4 +1,4 @@
1
- # Slopcamera local host and CLI
1
+ # SlopCamera local host and CLI
2
2
 
3
3
  Slopcamera reads finished recording bundles from repository-local directories, combines independent camera and audio takes on one typed project clock, then applies non-destructive edits through the `slopcamera` CLI. The CLI is the product surface for agents. An optional unbundled `slopcamera menubar` companion surfaces the agent outputs directory in the macOS menu bar; it carries no capture or editing authority.
4
4
 
@@ -19,6 +19,12 @@ export interface HtmlOverlayFrameRenderRequest {
19
19
  readonly browserRuntime: HtmlOverlayBrowserRuntimeBinding;
20
20
  readonly outputDirectory: string;
21
21
  readonly resources: readonly BoundHtmlOverlayResource[];
22
+ /**
23
+ * Optional ascending subset of frame indexes to capture, for stills and
24
+ * previews. Omitted means every frame. Captured files keep their absolute
25
+ * frame index in the name, so `frame-%08d.png` stays addressable.
26
+ */
27
+ readonly frames?: readonly number[];
22
28
  }
23
29
 
24
30
  export interface HtmlOverlayFrameRenderResult {
@@ -4,7 +4,7 @@ import type { z } from "zod";
4
4
 
5
5
  import type { ApplicationContext } from "./context";
6
6
 
7
- export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.6.0" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.7.0" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "./errors";
2
2
  import { MAX_EVENT_QUERY_LIMIT } from "./query-limits";
3
+ import { HTML_FILM_CUTS, type HtmlFilmCommand, type HtmlFilmCut } from "./html-film-names";
3
4
  import { STUDIO_TEMPLATES, type StudioTemplate } from "./studio-template-names";
4
5
  import {
5
6
  slopcameraGalleryKinds,
@@ -121,6 +122,7 @@ export type SpatialProjectCommand = JsonOption & {
121
122
 
122
123
  export type CliCommand =
123
124
  | { readonly kind: "html-render"; readonly input: string; readonly dryRun: boolean; readonly json: boolean }
125
+ | HtmlFilmCommand
124
126
  | StudioCommand
125
127
  | DirectingCommand
126
128
  | SpatialWorldCommand
@@ -731,6 +733,63 @@ function fail(message: string): never {
731
733
  throw new CliError("usage", message);
732
734
  }
733
735
 
736
+ const HTML_STILL_USAGE = "Use html still --input <scene.json> --at <seconds>[,<seconds>...] --output <dir> [--json].";
737
+ const HTML_PREVIEW_USAGE = "Use html preview --input <scene.json> --output <dir> [--every <seconds>] [--json].";
738
+ const HTML_DELIVER_USAGE = "Use html deliver <export.json> --basename <name> --poster-at <seconds> --social-at <seconds> [--cuts 1:1,9:16] [--per-beat-clips] [--beats <beats.json>] [--output <dir>] [--json].";
739
+
740
+ function secondsList(value: string | undefined, usage: string): readonly number[] {
741
+ if (value === undefined) fail(usage);
742
+ return value.split(",").map(part => {
743
+ if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/u.test(part.trim())) fail(`Times must be seconds such as 3 or 12.5; got ${part}.`);
744
+ return Number(part.trim());
745
+ });
746
+ }
747
+
748
+ function parseHtmlFilmArgs(action: "still" | "preview" | "deliver", argv: readonly string[]): HtmlFilmCommand {
749
+ if (action === "still") {
750
+ const parsed = parseOptions(argv, { "--input": "value", "--at": "value", "--output": "value", "--json": "flag" });
751
+ const input = optionString(parsed, "--input");
752
+ const output = optionString(parsed, "--output");
753
+ if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_STILL_USAGE);
754
+ return { kind: "html-film", action, input, output, at: secondsList(optionString(parsed, "--at"), HTML_STILL_USAGE), json: optionFlag(parsed, "--json") };
755
+ }
756
+ if (action === "preview") {
757
+ const parsed = parseOptions(argv, { "--input": "value", "--every": "value", "--output": "value", "--json": "flag" });
758
+ const input = optionString(parsed, "--input");
759
+ const output = optionString(parsed, "--output");
760
+ if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_PREVIEW_USAGE);
761
+ const every = optionString(parsed, "--every");
762
+ const [seconds] = every === undefined ? [2] : secondsList(every, HTML_PREVIEW_USAGE);
763
+ if (seconds === undefined || seconds <= 0) fail("--every must be more than 0 seconds.");
764
+ return { kind: "html-film", action, input, output, every: seconds, json: optionFlag(parsed, "--json") };
765
+ }
766
+ const parsed = parseOptions(argv, {
767
+ "--basename": "value", "--poster-at": "value", "--social-at": "value", "--cuts": "value",
768
+ "--per-beat-clips": "flag", "--beats": "value", "--output": "value", "--json": "flag",
769
+ });
770
+ const basename = optionString(parsed, "--basename");
771
+ if (parsed.positionals.length !== 1 || basename === undefined) fail(HTML_DELIVER_USAGE);
772
+ const single = (name: string) => {
773
+ const values = secondsList(optionString(parsed, name), HTML_DELIVER_USAGE);
774
+ if (values.length !== 1) fail(`${name} takes one time in seconds.`);
775
+ return values[0]!;
776
+ };
777
+ const cutsInput = optionString(parsed, "--cuts");
778
+ const cuts = cutsInput === undefined ? [] : cutsInput.split(",").map(cut => {
779
+ if (!(HTML_FILM_CUTS as readonly string[]).includes(cut)) fail(`--cuts takes ${HTML_FILM_CUTS.join(", ")}; got ${cut}.`);
780
+ return cut as HtmlFilmCut;
781
+ });
782
+ const beats = optionString(parsed, "--beats");
783
+ const output = optionString(parsed, "--output");
784
+ return {
785
+ kind: "html-film", action, exportPath: parsed.positionals[0]!, basename,
786
+ posterAt: single("--poster-at"), socialAt: single("--social-at"), cuts,
787
+ perBeatClips: optionFlag(parsed, "--per-beat-clips"),
788
+ ...(beats === undefined ? {} : { beats }), ...(output === undefined ? {} : { output }),
789
+ json: optionFlag(parsed, "--json"),
790
+ };
791
+ }
792
+
734
793
  function parseOptions(argv: readonly string[], spec: OptionSpec): ParsedOptions {
735
794
  const options: Record<string, OptionValue> = {};
736
795
  const positionals: string[] = [];
@@ -3631,6 +3690,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3631
3690
  const command = argv[0]!;
3632
3691
  switch (command) {
3633
3692
  case "html": {
3693
+ if (argv[1] === "still" || argv[1] === "preview" || argv[1] === "deliver") return parseHtmlFilmArgs(argv[1], argv.slice(2));
3634
3694
  if (argv[1] !== "render") fail("Use html render --input <scene.json> [--dry-run] [--json].");
3635
3695
  const parsed = parseOptions(argv.slice(2), { "--input": "value", "--dry-run": "flag", "--json": "flag" });
3636
3696
  const input = optionString(parsed, "--input");
@@ -125,6 +125,11 @@ export function commandHostResourceClaims(
125
125
  coordinator: HostResourceCoordinator,
126
126
  ): readonly HostResourceClaim[] {
127
127
  switch (command.kind) {
128
+ case "html-film": return command.action === "deliver"
129
+ ? claims(coordinator, ["cpu", "ffmpeg", "local-io"])
130
+ : claims(coordinator, command.action === "preview"
131
+ ? ["cpu", "local-io", "browser", "ffmpeg"]
132
+ : ["cpu", "local-io", "browser"]);
128
133
  case "html-render": return command.dryRun
129
134
  ? claims(coordinator, ["cpu", "local-io"])
130
135
  : claims(coordinator, ["cpu", "local-io", "browser", "ffmpeg", "output-publication", "project-render"]);
@@ -188,6 +188,7 @@ import { commandHelp, completions } from "./help";
188
188
  import { createLocalSlopcameraCapabilityManifest } from "./capability-manifest";
189
189
  import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
190
190
  import { executeHtmlSceneCommand } from "./html-scene";
191
+ import { executeHtmlFilmCommand, formatHtmlFilmResult } from "./html-film";
191
192
  import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
192
193
  import { launchMenubar, manageMenubar, reportOutputsRoot } from "./menubar";
193
194
  import {
@@ -363,7 +364,7 @@ import {
363
364
  workflowRunStore,
364
365
  } from "./workflow-runs";
365
366
 
366
- export const SLOPCAMERA_VERSION = "3.6.0";
367
+ export const SLOPCAMERA_VERSION = "3.7.0";
367
368
 
368
369
  // Legacy direct renders predate per-target output contracts. Keep them
369
370
  // bounded generously enough for long-form production while preventing one
@@ -7450,6 +7451,16 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
7450
7451
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
7451
7452
  return;
7452
7453
  }
7454
+ case "html-film": {
7455
+ const output = await executeHtmlFilmCommand(applicationContext(context), command,
7456
+ context.abortSignal ?? new AbortController().signal,
7457
+ { progress: stage => context.io.stderr(`HTML film: ${stage}\n`) });
7458
+ writeValue(context.io, command.json, output, () => formatHtmlFilmResult(output));
7459
+ if (output.kind === "slopcamera.html-film-delivery" && !output.withinBudget) {
7460
+ throw new CliError("invalid-data", "One or more delivered files are over budget.");
7461
+ }
7462
+ return;
7463
+ }
7453
7464
  case "studio": {
7454
7465
  const output = await executeStudioCommand(applicationContext(context), command, context.abortSignal ?? new AbortController().signal);
7455
7466
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
@@ -8269,6 +8280,7 @@ type MutationReference =
8269
8280
  function commandMutationReference(command: CliCommand): MutationReference | undefined {
8270
8281
  switch (command.kind) {
8271
8282
  case "html-render": return undefined; // A fresh retained attempt owns its publication lease.
8283
+ case "html-film": return undefined; // Fresh private job directories; outputs are plain caller files.
8272
8284
  case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
8273
8285
  case "directing": return undefined; // The directing store owns its explicit lease.
8274
8286
  case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
@@ -25,7 +25,8 @@ Commands:
25
25
  Author and render retained Blender, CAD and Manim productions
26
26
  image vectorize|generate|gallery|icon
27
27
  Create local SVGs, generated images, or review galleries
28
- html catalog|scaffold|render Author HTML scenes and export video with local audio
28
+ html catalog|scaffold|init|render|still|preview|deliver
29
+ Author HTML scenes and launch films, render, and deliver web video
29
30
  style list|show Discover reusable art direction and review criteria
30
31
  workflows list|show|plan|run Plan or run a reviewed reusable workflow
31
32
  code init|check|plan|run Author, preflight, and run trusted TypeScript workflows
@@ -304,7 +305,11 @@ compliance. Drawing sheets are exposed through this CLI and the root SDK, not co
304
305
  [--cell <n>] [--tile|--no-tile] [--json]
305
306
  slopcamera image icon <subject> --output <file.svg>
306
307
  [--purpose <mark|illustration>] [--model <model>] [--ink <#rgb>]
308
+ [--context <card|hero|inline>] [--candidates <1-4>]
307
309
  [--rounds <1-4>] [--critique-model <model>] [--keep-raster] [--json]
310
+ slopcamera image icon --set <file.json> --output-dir <directory>
311
+ [--context <card|hero|inline>] [--candidates <1-4>]
312
+ [--set-rounds <1-4>] [--keep-raster] [--json]
308
313
 
309
314
  Explicit --output file commands delegate to @hraness/slopcamera. Vectorization is local,
310
315
  bounded, checksum-pinned, and emits inert SVG. File generation uses Vercel AI Gateway with the
@@ -312,7 +317,10 @@ caller's environment credential. The --prompt spelling without --output is an al
312
317
  content-addressed \`ai image generate\` lane and returns project-composable content hash references.
313
318
  Icon combines a style-locked Gateway raster, local ink extraction and VTracer tracing into a
314
319
  canonical isometric line-art SVG; --rounds above 1 adds a vision-model critique that revises the
315
- prompt between attempts. Gallery fans one subject out to bounded parallel candidates — driven by
320
+ prompt between attempts. --context tunes the prompt and gates to where the art lands, and --set
321
+ generates a manifest-declared family at once: pooled candidates, joint selection toward the
322
+ family's measured coverage and stroke median, and a contact-sheet critique of the whole set before
323
+ anything publishes. Gallery fans one subject out to bounded parallel candidates — driven by
316
324
  variation axes or an explicit candidate list — keeps every candidate with its provenance, and
317
325
  composes a labelled contact sheet plus receipt for agent review. Texture cells repeat the
318
326
  candidate 2×2 so seams are inspectable (--tile/--no-tile overrides). It never replaces existing
@@ -330,7 +338,12 @@ direction, not proof of visual quality or historically authentic footage.`,
330
338
  html: `Usage:
331
339
  slopcamera html catalog [--json]
332
340
  slopcamera html scaffold <${HTML_OVERLAY_SCAFFOLD_KIND_HELP}> --output <file.html>
341
+ slopcamera html init <dir> --template launch-film [--aspect 16:9|1:1|9:16] [--json]
333
342
  slopcamera html render --input <scene.json> [--dry-run] [--json]
343
+ slopcamera html still --input <scene.json> --at <seconds>[,<seconds>...] --output <dir> [--json]
344
+ slopcamera html preview --input <scene.json> --output <dir> [--every <seconds>] [--json]
345
+ slopcamera html deliver <export.json> --basename <name> --poster-at <seconds> --social-at <seconds>
346
+ [--cuts 1:1,9:16,4:5] [--per-beat-clips] [--beats <beats.json>] [--output <dir>] [--json]
334
347
 
335
348
  Catalog lists the closed scaffold profiles in stable order with their primary jobs, render
336
349
  substrates, and current exact browser-library versions. Scaffold creates a complete transparent
@@ -344,7 +357,16 @@ with separate scene-video and original-audio tracks. Device dimensions must be e
344
357
  Duration rounds up to whole frames; audio starts at zero and is trimmed or padded to that
345
358
  duration. Soundtracks encode as 48 kHz stereo AAC at 320 kb/s. Dry run checks source and
346
359
  workload bounds without launching a browser, importing audio, or writing project state.
347
- Progress uses stderr; --json keeps stdout machine-readable.`,
360
+ Progress uses stderr; --json keeps stdout machine-readable.
361
+
362
+ Init creates a launch-film project (film.html, film.css, film.js, build.ts and a sample
363
+ product mockup) in a new directory. Its build writes out/scene.json, out/beats.json and
364
+ out/captions.vtt. Still and preview draw frames with the same injected runtime that render
365
+ uses, so a still matches the rendered frame at that time; preview adds a contact sheet.
366
+ Deliver reads output.path from html render --json output and writes a faststart H.264 MP4,
367
+ a VP9 WebM, a poster, a 1200x630 social still, optional aspect cuts over a blurred fill, and
368
+ optional 6 to 10 second clips per beat. It prints a receipt per file and exits non-zero when
369
+ a file is over its budget: 12 MB per MP4, 10 MB for the WebM, 250 KB per JPEG.`,
348
370
  operations: `Usage:
349
371
  slopcamera operations list [--json]
350
372
  slopcamera operations show <kind>[@<version>] [--json]
@@ -712,7 +734,7 @@ export function completions(words: readonly string[]): readonly string[] {
712
734
  if (words.length <= 1) return topLevel;
713
735
  const command = words[0];
714
736
  if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
715
- if (command === "html") return ["catalog", "scaffold", "render"];
737
+ if (command === "html") return ["catalog", "scaffold", "init", "render", "still", "preview", "deliver"];
716
738
  if (command === "style") return ["list", "show"];
717
739
  if (command === "direct") return ["init", "anchor", "plan", "start", "inspect", "revise", "generate", "resume", "review", "assemble", "cleanup"];
718
740
  if (command === "studio") return words[2] === "assets" || words[1] === "assets" ? ["search", "describe", "plan", "import"] : ["init", "bundle", "plan", "probe", "run", "encode", "asset", "assemble", "inspect", "reconcile", "assets"];
@@ -0,0 +1,158 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, open, readFile, realpath } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+
5
+ import { CliError } from "./errors";
6
+ import { discoverRepositoryRoot } from "./paths";
7
+
8
+ import { HTML_FILM_TEMPLATES, type HtmlFilmTemplate } from "./html-film-names";
9
+
10
+ export { HTML_FILM_TEMPLATES, type HtmlFilmTemplate };
11
+ export const HTML_FILM_ASPECTS = ["16:9", "1:1", "9:16"] as const;
12
+ export type HtmlFilmAspect = (typeof HTML_FILM_ASPECTS)[number];
13
+
14
+ /** Files copied from examples/html/<template>/, in write order. */
15
+ export const HTML_FILM_TEMPLATE_FILES = [
16
+ "README.md", "build.ts", "film.css", "film.html", "film.js", "film.json", "mockups.tsx", "timeline.ts",
17
+ ] as const;
18
+
19
+ const REACT_VERSION = "19.2.3";
20
+
21
+ export interface HtmlFilmInitResult {
22
+ readonly kind: "slopcamera.html-film-init";
23
+ readonly schemaVersion: 1;
24
+ readonly directory: string;
25
+ readonly template: HtmlFilmTemplate;
26
+ readonly aspect: HtmlFilmAspect;
27
+ readonly slopcameraVersion: string;
28
+ readonly files: readonly string[];
29
+ readonly executed: false;
30
+ readonly next: readonly string[];
31
+ }
32
+
33
+ export interface HtmlFilmTemplateSource {
34
+ readonly version: string;
35
+ readonly files: Readonly<Record<string, string>>;
36
+ }
37
+
38
+ /** Reads the template from the installed package or checkout that runs this CLI. */
39
+ export async function readHtmlFilmTemplate(template: HtmlFilmTemplate, from = import.meta.dir): Promise<HtmlFilmTemplateSource> {
40
+ if (from.startsWith("/$bunfs/")) {
41
+ throw new CliError("unavailable", "html init needs the @hraness/slopcamera package on disk. Run it through bunx @hraness/slopcamera or a checkout.");
42
+ }
43
+ const root = await discoverRepositoryRoot(from);
44
+ const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8")) as { readonly version?: unknown };
45
+ if (typeof manifest.version !== "string") throw new CliError("invalid-data", "The Slopcamera package has no version.");
46
+ const files: Record<string, string> = {};
47
+ for (const name of HTML_FILM_TEMPLATE_FILES) {
48
+ files[name] = await readFile(join(root, "examples", "html", template, name), "utf8");
49
+ }
50
+ return { version: manifest.version, files };
51
+ }
52
+
53
+ function packageName(directory: string): string {
54
+ const name = basename(directory).toLowerCase().replace(/[^a-z0-9._-]+/gu, "-").replace(/^[._-]+|[-]+$/gu, "");
55
+ return name === "" ? "launch-film" : name;
56
+ }
57
+
58
+ /**
59
+ * The files a new film project holds: the template, with its aspect set, plus
60
+ * a package.json that pins this Slopcamera version next to the source.
61
+ */
62
+ export function htmlFilmProjectFiles(
63
+ source: HtmlFilmTemplateSource,
64
+ options: { readonly directory: string; readonly aspect: HtmlFilmAspect },
65
+ ): Readonly<Record<string, string>> {
66
+ const film = JSON.parse(source.files["film.json"]!) as Record<string, unknown>;
67
+ const files: Record<string, string> = { ...source.files, "film.json": `${JSON.stringify({ ...film, aspect: options.aspect }, null, 2)}\n` };
68
+ files["package.json"] = `${JSON.stringify({
69
+ name: packageName(options.directory),
70
+ private: true,
71
+ type: "module",
72
+ scripts: {
73
+ build: "bun build.ts",
74
+ still: "slopcamera html still --input out/scene.json --at 3,12.5 --output out/stills",
75
+ draft: "bun build.ts --scale 0.5 --fps 15",
76
+ render: "slopcamera html render --input out/scene.json --json > out/export.json",
77
+ deliver: "slopcamera html deliver out/export.json --basename launch --poster-at 9 --social-at 9 --cuts 1:1,9:16 --per-beat-clips",
78
+ },
79
+ dependencies: {
80
+ "@hraness/slopcamera": source.version,
81
+ react: REACT_VERSION,
82
+ "react-dom": REACT_VERSION,
83
+ },
84
+ devDependencies: {
85
+ "@types/bun": "latest",
86
+ "@types/react": "^19.2.0",
87
+ "@types/react-dom": "^19.2.0",
88
+ },
89
+ }, null, 2)}\n`;
90
+ files["tsconfig.json"] = `${JSON.stringify({
91
+ compilerOptions: {
92
+ target: "ES2023",
93
+ module: "Preserve",
94
+ moduleResolution: "bundler",
95
+ jsx: "react-jsx",
96
+ strict: true,
97
+ noEmit: true,
98
+ allowImportingTsExtensions: true,
99
+ skipLibCheck: true,
100
+ types: ["bun"],
101
+ },
102
+ include: ["*.ts", "*.tsx"],
103
+ }, null, 2)}\n`;
104
+ files[".gitignore"] = "node_modules/\nout/\n";
105
+ return files;
106
+ }
107
+
108
+ async function writeNew(path: string, text: string): Promise<void> {
109
+ const handle = await open(path, constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_WRONLY, 0o644);
110
+ try {
111
+ await handle.writeFile(text, "utf8");
112
+ await handle.sync();
113
+ } finally {
114
+ await handle.close();
115
+ }
116
+ }
117
+
118
+ /** Creates a new film project. It never merges into an existing directory. */
119
+ export async function createHtmlFilmProject(
120
+ directoryInput: string,
121
+ options: { readonly template: HtmlFilmTemplate; readonly aspect: HtmlFilmAspect; readonly source?: HtmlFilmTemplateSource },
122
+ ): Promise<HtmlFilmInitResult> {
123
+ const requested = resolve(directoryInput);
124
+ const parent = await realpath(dirname(requested)).catch(() => {
125
+ throw new CliError("not-found", `The parent directory does not exist: ${dirname(requested)}`);
126
+ });
127
+ const directory = join(parent, basename(requested));
128
+ const source = options.source ?? await readHtmlFilmTemplate(options.template);
129
+ const files = htmlFilmProjectFiles(source, { directory, aspect: options.aspect });
130
+ try {
131
+ await mkdir(directory, { mode: 0o755 });
132
+ } catch (error) {
133
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
134
+ throw new CliError("conflict", `Refusing to write into an existing path: ${directory}`);
135
+ }
136
+ throw error;
137
+ }
138
+ if (!(await lstat(directory)).isDirectory()) throw new CliError("conflict", "The film directory changed while it was created.");
139
+ const names = Object.keys(files).sort();
140
+ for (const name of names) await writeNew(join(directory, name), files[name]!);
141
+ return {
142
+ kind: "slopcamera.html-film-init",
143
+ schemaVersion: 1,
144
+ directory,
145
+ template: options.template,
146
+ aspect: options.aspect,
147
+ slopcameraVersion: source.version,
148
+ files: names,
149
+ executed: false,
150
+ next: [
151
+ `cd ${JSON.stringify(directory)} && bun install`,
152
+ "Edit film.json and mockups.tsx, then run bun run build.",
153
+ "slopcamera html still --input out/scene.json --at 3,12.5 --output out/stills",
154
+ "slopcamera html render --input out/scene.json --json > out/export.json",
155
+ "slopcamera html deliver out/export.json --basename launch --poster-at 9 --social-at 9",
156
+ ],
157
+ };
158
+ }
@@ -0,0 +1,17 @@
1
+ /** Launch-film command shapes, kept free of runtime imports for the argument parser. */
2
+
3
+ /** Film templates that `html init --template` writes; public copy cites these ids. */
4
+ export const HTML_FILM_TEMPLATES = ["launch-film"] as const;
5
+ export type HtmlFilmTemplate = (typeof HTML_FILM_TEMPLATES)[number];
6
+
7
+ export const HTML_FILM_CUTS = ["1:1", "9:16", "4:5"] as const;
8
+ export type HtmlFilmCut = (typeof HTML_FILM_CUTS)[number];
9
+
10
+ export type HtmlFilmCommand =
11
+ | { readonly kind: "html-film"; readonly action: "still"; readonly input: string; readonly at: readonly number[]; readonly output: string; readonly json: boolean }
12
+ | { readonly kind: "html-film"; readonly action: "preview"; readonly input: string; readonly every: number; readonly output: string; readonly json: boolean }
13
+ | {
14
+ readonly kind: "html-film"; readonly action: "deliver"; readonly exportPath: string; readonly basename: string;
15
+ readonly posterAt: number; readonly socialAt: number; readonly cuts: readonly HtmlFilmCut[];
16
+ readonly perBeatClips: boolean; readonly beats?: string; readonly output?: string; readonly json: boolean;
17
+ };