@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.
- package/README.md +41 -26
- package/apps/desktop/README.md +1 -1
- package/apps/desktop/application/html-overlay-renderer.ts +6 -0
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +60 -0
- package/apps/desktop/cli/command-host-resources.ts +5 -0
- package/apps/desktop/cli/commands.ts +13 -1
- package/apps/desktop/cli/help.ts +26 -4
- package/apps/desktop/cli/html-film-init.ts +158 -0
- package/apps/desktop/cli/html-film-names.ts +17 -0
- package/apps/desktop/cli/html-film.ts +471 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
- package/apps/desktop/cli/html-scene.ts +3 -3
- package/apps/desktop/cli/main.ts +1 -0
- package/apps/desktop/cli/menubar-status.ts +1 -0
- package/apps/desktop/cli/portable-surface.ts +55 -1
- package/apps/desktop/dist/cli/main.js +328 -293
- package/apps/desktop/html-overlay/film.ts +418 -0
- package/dist/cli.js +13 -2
- package/dist/generate.js +1 -1
- package/dist/{index-4z9y2wa2.js → index-842t02pp.js} +1 -1
- package/dist/{index-qxfexvj3.js → index-nv3byqa1.js} +20 -4
- package/dist/index-p74xy76d.js +3 -0
- package/dist/{index-fevf4ppk.js → index-y8spxs77.js} +10 -7
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +5 -5
- package/examples/html/launch-film/README.md +51 -0
- package/examples/html/launch-film/build.ts +158 -0
- package/examples/html/launch-film/film.css +593 -0
- package/examples/html/launch-film/film.html +85 -0
- package/examples/html/launch-film/film.js +410 -0
- package/examples/html/launch-film/film.json +63 -0
- package/examples/html/launch-film/mockups.tsx +74 -0
- package/examples/html/launch-film/timeline.ts +57 -0
- package/package.json +13 -6
- package/skills/slopcamera/SKILL.md +3 -2
- package/skills/slopcamera/references/install.md +3 -3
- package/skills/slopcamera/references/launch-films.md +154 -0
- package/skills/slopcamera/references/visual-communication.md +1 -1
- package/src/cli.ts +125 -9
- package/src/generation-pricing.ts +21 -1
- package/src/icon-set.ts +1086 -0
- package/src/icon.ts +396 -59
- package/src/index.ts +1 -0
- package/src/operations.ts +52 -0
- package/src/version.ts +1 -1
- package/dist/index-nxtmy18t.js +0 -3
package/README.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SlopCamera
|
|
2
2
|
|
|
3
|
-
[](https://slopcamera.com)
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
-
|
|
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
|
|
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
|
|
19
|
+
## Made with SlopCamera
|
|
20
20
|
|
|
21
21
|
[](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
|
|
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
|
-
##
|
|
48
|
+
## Compared with other tools
|
|
49
49
|
|
|
50
|
-
|
|
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
|
-
[
|
|
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.
|
|
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
|
-
|
|
94
|
-
what
|
|
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
|
|
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
|
|
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
|
|
152
|
+
## What SlopCamera does
|
|
140
153
|
|
|
141
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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) · [
|
|
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;
|
|
431
|
+
page; SlopCamera has no product-updates mailing list or account-status lookup.
|
package/apps/desktop/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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.
|
|
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",
|
package/apps/desktop/cli/args.ts
CHANGED
|
@@ -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.
|
|
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.
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
+
};
|