@hraness/slopcamera 3.6.0 → 3.8.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 +49 -33
- package/apps/desktop/README.md +3 -10
- package/apps/desktop/application/html-overlay-renderer.ts +6 -0
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/application/spatial-assets.ts +1 -1
- package/apps/desktop/cli/{menubar-status.ts → activity-status.ts} +24 -21
- package/apps/desktop/cli/args.ts +62 -10
- package/apps/desktop/cli/command-host-resources.ts +5 -1
- package/apps/desktop/cli/commands.ts +26 -9
- package/apps/desktop/cli/desktop-status.ts +271 -0
- package/apps/desktop/cli/help.ts +36 -28
- 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/legacy-login.ts +184 -0
- package/apps/desktop/cli/main.ts +9 -0
- package/apps/desktop/cli/outputs.ts +19 -0
- package/apps/desktop/cli/portable-surface.ts +55 -1
- package/apps/desktop/cli/registry.ts +314 -0
- package/apps/desktop/cli/verbs.ts +193 -0
- package/apps/desktop/dist/cli/main.js +338 -316
- 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-k9bjw80z.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 +6 -6
- 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 +24 -11
- package/skills/slopcamera/SKILL.md +4 -3
- package/skills/slopcamera/references/install.md +3 -3
- package/skills/slopcamera/references/launch-films.md +158 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- 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/apps/desktop/cli/menubar.ts +0 -272
- 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,15 +103,16 @@ 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
|
|
95
|
-
balance
|
|
96
|
-
companion
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
SlopCamera is a CLI with no menu-bar app or background process.
|
|
107
|
+
`slopcamera status` shows what is running, how the last job ended, your last
|
|
108
|
+
known credits balance, the newest outputs and whether the retired menu-bar
|
|
109
|
+
companion still opens at login. `slopcamera tui` keeps
|
|
110
|
+
that screen open and refreshes it; `slopcamera tui --snapshot` prints it once
|
|
111
|
+
and `--json` returns the data. `slopcamera outputs list|open|reveal` lists the
|
|
112
|
+
newest outputs or opens one on this Mac, `slopcamera commands --json` lists
|
|
113
|
+
every command with what it may change, and `slopcamera legacy retire` stops
|
|
114
|
+
the retired menu-bar companion opening at login by moving its LaunchAgent aside; it
|
|
115
|
+
deletes nothing, and `slopcamera doctor` prints the command that restores it.
|
|
102
116
|
|
|
103
117
|
<details>
|
|
104
118
|
<summary>Historical Atet release evidence</summary>
|
|
@@ -106,14 +120,14 @@ when working from source.
|
|
|
106
120
|
[Atet v3.2.3](https://github.com/hraness/atet/releases/tag/v3.2.3) and its
|
|
107
121
|
[original archive](https://github.com/hraness/atet/releases/download/v3.2.3/hraness-atet-3.2.3.tgz)
|
|
108
122
|
remain historical publication evidence. Those immutable bytes install Atet,
|
|
109
|
-
not
|
|
123
|
+
not SlopCamera. Renaming the repository does not create a renamed package or
|
|
110
124
|
change an existing release.
|
|
111
125
|
|
|
112
126
|
</details>
|
|
113
127
|
|
|
114
128
|
## Make your first diagram
|
|
115
129
|
|
|
116
|
-
After installing
|
|
130
|
+
After installing SlopCamera, this local task needs no model account. In a new directory,
|
|
117
131
|
create the included diagram, check it, and render it:
|
|
118
132
|
|
|
119
133
|
```sh
|
|
@@ -136,10 +150,12 @@ For detailed native 3D, follow
|
|
|
136
150
|
[Your first native film](docs/tutorials/first-native-film.md). To generate editable
|
|
137
151
|
architecture from dimensions, [build a parametric design](https://slopcamera.com/docs/how-to/parametric-design).
|
|
138
152
|
|
|
139
|
-
## What
|
|
153
|
+
## What SlopCamera does
|
|
140
154
|
|
|
141
|
-
|
|
155
|
+
SlopCamera lets your coding agent make images, diagrams, animation, 3D scenes,
|
|
142
156
|
and video from source files it can keep revising.
|
|
157
|
+
Direct cel animation, pixel art, math explainers, and period film looks with the
|
|
158
|
+
[style direction guide](docs/how-to/direct-visual-styles.md).
|
|
143
159
|
|
|
144
160
|
### Author scenes and direct cameras
|
|
145
161
|
|
|
@@ -158,7 +174,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
|
|
|
158
174
|
capture appearance; they do not establish collision geometry or editable native
|
|
159
175
|
meshes. See [Directed scenes](docs/spatial-scenes.md).
|
|
160
176
|
|
|
161
|
-
|
|
177
|
+
SlopCamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
|
|
162
178
|
document describes beats, actions, camera coverage, and look intents, compiles
|
|
163
179
|
into proposed performance/camera/cinema/material-lighting/shot documents, and
|
|
164
180
|
fans out into bounded galleries per axis. Declared effects bind into renders,
|
|
@@ -174,12 +190,12 @@ slopcamera workflows plan cinematic-world --input pack-input.json --json
|
|
|
174
190
|
The `cinematic-world` workflow consumes one inert recipe pack — a bounded,
|
|
175
191
|
content-addressed JSON document an agent can author — and stops before
|
|
176
192
|
selection. Registration and selection stay explicit `scene project` operations.
|
|
177
|
-
See [
|
|
193
|
+
See [Plan camera moves, lighting, and effects for a 3D scene](docs/how-to/direct-cinematic-worlds.md) and the
|
|
178
194
|
[extension architecture](docs/extension-architecture.md).
|
|
179
195
|
|
|
180
196
|
### Film native worlds and educational animation
|
|
181
197
|
|
|
182
|
-
|
|
198
|
+
SlopCamera can direct Blender for detailed sets, materials,
|
|
183
199
|
lighting, skinned characters, cloth and liquid caches; CadQuery for parametric
|
|
184
200
|
solids and STEP; and Manim Community for mathematical animation. Seven editable
|
|
185
201
|
starters include a product, character, shaded street, cloth, liquid, CAD bracket,
|
|
@@ -195,7 +211,7 @@ camera clock for reuse. Supported GLB derivatives and calibrated cameras can
|
|
|
195
211
|
cross between native and portable scenes; rigs, solvers, and procedural materials
|
|
196
212
|
remain native. Blender and Python environments are installed separately, and
|
|
197
213
|
source execution requires explicit current-user trust. See [Native film studio](docs/studio.md)
|
|
198
|
-
and [Make
|
|
214
|
+
and [Make a math explainer video with Manim](docs/how-to/educational-video.md).
|
|
199
215
|
|
|
200
216
|
### Build diagrams and motion graphics
|
|
201
217
|
|
|
@@ -269,12 +285,12 @@ slopcamera workflows list --json
|
|
|
269
285
|
slopcamera workflows show social-variants --json
|
|
270
286
|
```
|
|
271
287
|
|
|
272
|
-
## How
|
|
288
|
+
## How SlopCamera works
|
|
273
289
|
|
|
274
290
|
Keep the source that owns each creative decision. A native scene owns a rig or
|
|
275
291
|
simulation; a portable scene owns supported geometry, cameras, and media surfaces;
|
|
276
292
|
a diagram owns its objects and labels; a video project owns cuts and delivery.
|
|
277
|
-
|
|
293
|
+
SlopCamera connects these through explicit assets and rendered derivatives.
|
|
278
294
|
|
|
279
295
|
1. **Prepare the sources.** Import footage and assets or author a scene, diagram,
|
|
280
296
|
or native program. Inspect available tools before choosing an engine.
|
|
@@ -318,7 +334,7 @@ declarative graphs, approvals, and resuming work.
|
|
|
318
334
|
|
|
319
335
|
## Design and trust
|
|
320
336
|
|
|
321
|
-
There is no
|
|
337
|
+
There is no SlopCamera account, hosted project database, or browser generation service.
|
|
322
338
|
Ordinary editing and rendering remain local. Gateway generation and selected
|
|
323
339
|
cloud analysis use credentials from the local process and request explicit
|
|
324
340
|
acknowledgement before uploading named media. The optional hosted API runs each
|
|
@@ -349,7 +365,7 @@ vision](https://algal.computer/docs/vision/) states the bet behind it.
|
|
|
349
365
|
## Documentation
|
|
350
366
|
|
|
351
367
|
- **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) · [
|
|
368
|
+
- **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
369
|
- **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
370
|
- **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
371
|
|
|
@@ -413,4 +429,4 @@ an acknowledgment; the unacknowledged offer expires after ten minutes and does n
|
|
|
413
429
|
consume the weekly shown cooldown. `support dismiss` opts out across the local suite;
|
|
414
430
|
`support snooze` pauses for 30 days. `HRANESS_SUPPORT_AUDIENCE=off` disables ambient
|
|
415
431
|
notices and claimed offers. Payment is an explicit human choice on the shared support
|
|
416
|
-
page;
|
|
432
|
+
page; SlopCamera has no product-updates mailing list or account-status lookup.
|
package/apps/desktop/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SlopCamera local host and CLI
|
|
2
2
|
|
|
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.
|
|
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. `slopcamera status` and `slopcamera tui` show what is running, credits and the newest agent outputs in the terminal.
|
|
4
4
|
|
|
5
5
|
## Recording bundles
|
|
6
6
|
|
|
@@ -819,13 +819,6 @@ bun run test:html-overlay
|
|
|
819
819
|
bun run benchmark:code-concurrency
|
|
820
820
|
```
|
|
821
821
|
|
|
822
|
-
The optional Rust menu-bar companion lives under `desktop/` at the repository root and builds with Cargo:
|
|
823
|
-
|
|
824
|
-
```sh
|
|
825
|
-
cargo build --manifest-path desktop/menubar/Cargo.toml
|
|
826
|
-
slopcamera menubar
|
|
827
|
-
```
|
|
828
|
-
|
|
829
822
|
`test:html-overlay` is portable and leaves the real-browser tests registered as
|
|
830
823
|
skipped. `test:html-overlay:browser:macos` runs the real Chrome frame suite,
|
|
831
824
|
including a declared PNG. `test:html-overlay:libraries:macos` additionally
|
|
@@ -847,4 +840,4 @@ The compiled smoke copies the executable away from the checkout and proves
|
|
|
847
840
|
diagram initialization/rendering plus PNG-to-SVG vectorization without
|
|
848
841
|
`node_modules`.
|
|
849
842
|
|
|
850
|
-
Screen recording, microphone, camera, Input Monitoring, and Accessibility are separate macOS permissions. A missing optional source becomes a typed diagnostic; strict-input mode fails before recording begins. Slopcamera ships as a CLI
|
|
843
|
+
Screen recording, microphone, camera, Input Monitoring, and Accessibility are separate macOS permissions. A missing optional source becomes a typed diagnostic; strict-input mode fails before recording begins. Slopcamera ships as a CLI; it does not produce, install, sign, or notarize a desktop application.
|
|
@@ -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.8.0" as const;
|
|
8
8
|
|
|
9
9
|
export const OPERATION_KINDS = [
|
|
10
10
|
"scene.audit",
|
|
@@ -459,7 +459,7 @@ export async function withPreparedSpatialAssets<Result>(
|
|
|
459
459
|
}
|
|
460
460
|
const decoded = await raster(bytes, interpretation.alpha === "opaque");
|
|
461
461
|
if (decoded.width !== interpretation.width || decoded.height !== interpretation.height) throw new RangeError("Image dimensions differ from its interpretation.");
|
|
462
|
-
profiles.add("sdr-png-jpeg-sharp-0.35.
|
|
462
|
+
profiles.add("sdr-png-jpeg-sharp-0.35.4");
|
|
463
463
|
return publishRaster(decoded);
|
|
464
464
|
};
|
|
465
465
|
const native = async (argv: readonly [string, ...string[]], maxOutputBytes = 16_384) => {
|
|
@@ -5,18 +5,20 @@ import { join } from "node:path";
|
|
|
5
5
|
import type { CliCommand } from "./args";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* The small status file
|
|
8
|
+
* The small status file `slopcamera status` and `slopcamera tui` read: what Slopcamera is doing now,
|
|
9
9
|
* how the last render ended, and the last credits balance a command saw.
|
|
10
10
|
* It holds plain labels, times and fixed error codes only — never paths,
|
|
11
11
|
* prompts, tokens or account identifiers. Writes are best effort: a status
|
|
12
12
|
* that can't be saved never fails the command.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
// The on-disk name predates the menu bar's retirement and stays frozen, so a
|
|
15
|
+
// rolled-back release and existing state keep reading the same file.
|
|
16
|
+
export const ACTIVITY_STATUS_FILE = "menubar-status.json";
|
|
15
17
|
const MAX_STATUS_BYTES = 16 * 1024;
|
|
16
18
|
|
|
17
19
|
export type ActivityState = "running" | "done" | "failed";
|
|
18
20
|
|
|
19
|
-
export interface
|
|
21
|
+
export interface RecordedActivity {
|
|
20
22
|
readonly state: ActivityState;
|
|
21
23
|
readonly label: string;
|
|
22
24
|
readonly startedAt: number;
|
|
@@ -24,16 +26,16 @@ export interface MenubarActivity {
|
|
|
24
26
|
readonly error?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export interface
|
|
29
|
+
export interface RecordedCredits {
|
|
28
30
|
readonly usd: string;
|
|
29
31
|
readonly low: boolean;
|
|
30
32
|
readonly checkedAt: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export interface
|
|
35
|
+
export interface ActivityStatus {
|
|
34
36
|
readonly schemaVersion: 1;
|
|
35
|
-
readonly activity?:
|
|
36
|
-
readonly credits?:
|
|
37
|
+
readonly activity?: RecordedActivity;
|
|
38
|
+
readonly credits?: RecordedCredits;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Plain labels for the commands that make media. Everything else leaves the status alone. */
|
|
@@ -43,6 +45,7 @@ export function activityLabel(command: CliCommand): string | undefined {
|
|
|
43
45
|
case "project-render": return command.action === "run" && !command.dryRun ? "Rendering a project" : undefined;
|
|
44
46
|
case "project-cinema": return "Rendering a project";
|
|
45
47
|
case "html-render": return "Rendering an HTML scene";
|
|
48
|
+
case "html-film": return command.action === "deliver" ? "Encoding a film" : "Rendering film stills";
|
|
46
49
|
case "diagram-render": return "Rendering a diagram";
|
|
47
50
|
case "image-vectorize": return "Vectorizing an image";
|
|
48
51
|
case "ai-image-generate": return "Generating an image";
|
|
@@ -57,13 +60,13 @@ export function activityLabel(command: CliCommand): string | undefined {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function parse(text: string):
|
|
63
|
+
function parse(text: string): ActivityStatus {
|
|
61
64
|
try {
|
|
62
65
|
const value: unknown = JSON.parse(text);
|
|
63
66
|
if (typeof value !== "object" || value === null || (value as { schemaVersion?: unknown }).schemaVersion !== 1) return { schemaVersion: 1 };
|
|
64
67
|
const record = value as Record<string, unknown>;
|
|
65
|
-
const activity = record.activity as
|
|
66
|
-
const credits = record.credits as
|
|
68
|
+
const activity = record.activity as RecordedActivity | undefined;
|
|
69
|
+
const credits = record.credits as RecordedCredits | undefined;
|
|
67
70
|
return {
|
|
68
71
|
schemaVersion: 1,
|
|
69
72
|
...(activity !== undefined && typeof activity === "object" && typeof activity.label === "string" ? { activity } : {}),
|
|
@@ -74,8 +77,8 @@ function parse(text: string): MenubarStatus {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
export function
|
|
78
|
-
const path = join(stateRoot,
|
|
80
|
+
export function readActivityStatus(stateRoot: string): ActivityStatus {
|
|
81
|
+
const path = join(stateRoot, ACTIVITY_STATUS_FILE);
|
|
79
82
|
try {
|
|
80
83
|
const info = lstatSync(path);
|
|
81
84
|
if (!info.isFile() || info.size > MAX_STATUS_BYTES) return { schemaVersion: 1 };
|
|
@@ -85,7 +88,7 @@ export function readMenubarStatus(stateRoot: string): MenubarStatus {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
const LOCK_FILE = `.${
|
|
91
|
+
const LOCK_FILE = `.${ACTIVITY_STATUS_FILE}.lock`;
|
|
89
92
|
/** A held lock blocks a write for this long before it is skipped. */
|
|
90
93
|
const LOCK_BUDGET_MS = 250;
|
|
91
94
|
/** A lock older than this is a crashed writer's and is reclaimed. */
|
|
@@ -127,14 +130,14 @@ function withStatusLock<T>(stateRoot: string, update: () => T): T | undefined {
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
/** Replaces the status file atomically with an owner-only file. Never throws. */
|
|
130
|
-
export function
|
|
133
|
+
export function writeActivityStatus(stateRoot: string, update: (current: ActivityStatus) => ActivityStatus): void {
|
|
131
134
|
withStatusLock(stateRoot, () => {
|
|
132
|
-
const path = join(stateRoot,
|
|
133
|
-
const staged = join(stateRoot, `.${
|
|
135
|
+
const path = join(stateRoot, ACTIVITY_STATUS_FILE);
|
|
136
|
+
const staged = join(stateRoot, `.${ACTIVITY_STATUS_FILE}.${randomUUID()}.tmp`);
|
|
134
137
|
try {
|
|
135
138
|
const root = lstatSync(stateRoot);
|
|
136
139
|
if (!root.isDirectory()) return;
|
|
137
|
-
const text = `${JSON.stringify(update(
|
|
140
|
+
const text = `${JSON.stringify(update(readActivityStatus(stateRoot)))}\n`;
|
|
138
141
|
if (Buffer.byteLength(text) > MAX_STATUS_BYTES) return;
|
|
139
142
|
const fd = openSync(staged, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
140
143
|
try { writeSync(fd, text); } finally { closeSync(fd); }
|
|
@@ -146,10 +149,10 @@ export function writeMenubarStatus(stateRoot: string, update: (current: MenubarS
|
|
|
146
149
|
});
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
export function recordActivity(stateRoot: string, activity:
|
|
150
|
-
|
|
152
|
+
export function recordActivity(stateRoot: string, activity: RecordedActivity): void {
|
|
153
|
+
writeActivityStatus(stateRoot, (current) => ({ ...current, activity }));
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
export function recordCredits(stateRoot: string, credits:
|
|
154
|
-
|
|
156
|
+
export function recordCredits(stateRoot: string, credits: RecordedCredits): void {
|
|
157
|
+
writeActivityStatus(stateRoot, (current) => ({ ...current, credits }));
|
|
155
158
|
}
|
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
|
|
@@ -333,7 +335,6 @@ export type CliCommand =
|
|
|
333
335
|
readonly tint: number | undefined;
|
|
334
336
|
readonly videoStreamIndex: number;
|
|
335
337
|
} & JsonOption)
|
|
336
|
-
| ({ readonly kind: "menubar"; readonly action: "run" | "install" | "uninstall" | "status"; readonly mode: "foreground" | "background" } & JsonOption)
|
|
337
338
|
| ({ readonly kind: "outputs" } & JsonOption)
|
|
338
339
|
| ({ readonly kind: "credits-status" } & JsonOption)
|
|
339
340
|
| ({
|
|
@@ -731,6 +732,63 @@ function fail(message: string): never {
|
|
|
731
732
|
throw new CliError("usage", message);
|
|
732
733
|
}
|
|
733
734
|
|
|
735
|
+
const HTML_STILL_USAGE = "Use html still --input <scene.json> --at <seconds>[,<seconds>...] --output <dir> [--json].";
|
|
736
|
+
const HTML_PREVIEW_USAGE = "Use html preview --input <scene.json> --output <dir> [--every <seconds>] [--json].";
|
|
737
|
+
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].";
|
|
738
|
+
|
|
739
|
+
function secondsList(value: string | undefined, usage: string): readonly number[] {
|
|
740
|
+
if (value === undefined) fail(usage);
|
|
741
|
+
return value.split(",").map(part => {
|
|
742
|
+
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}.`);
|
|
743
|
+
return Number(part.trim());
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function parseHtmlFilmArgs(action: "still" | "preview" | "deliver", argv: readonly string[]): HtmlFilmCommand {
|
|
748
|
+
if (action === "still") {
|
|
749
|
+
const parsed = parseOptions(argv, { "--input": "value", "--at": "value", "--output": "value", "--json": "flag" });
|
|
750
|
+
const input = optionString(parsed, "--input");
|
|
751
|
+
const output = optionString(parsed, "--output");
|
|
752
|
+
if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_STILL_USAGE);
|
|
753
|
+
return { kind: "html-film", action, input, output, at: secondsList(optionString(parsed, "--at"), HTML_STILL_USAGE), json: optionFlag(parsed, "--json") };
|
|
754
|
+
}
|
|
755
|
+
if (action === "preview") {
|
|
756
|
+
const parsed = parseOptions(argv, { "--input": "value", "--every": "value", "--output": "value", "--json": "flag" });
|
|
757
|
+
const input = optionString(parsed, "--input");
|
|
758
|
+
const output = optionString(parsed, "--output");
|
|
759
|
+
if (input === undefined || output === undefined || parsed.positionals.length !== 0) fail(HTML_PREVIEW_USAGE);
|
|
760
|
+
const every = optionString(parsed, "--every");
|
|
761
|
+
const [seconds] = every === undefined ? [2] : secondsList(every, HTML_PREVIEW_USAGE);
|
|
762
|
+
if (seconds === undefined || seconds <= 0) fail("--every must be more than 0 seconds.");
|
|
763
|
+
return { kind: "html-film", action, input, output, every: seconds, json: optionFlag(parsed, "--json") };
|
|
764
|
+
}
|
|
765
|
+
const parsed = parseOptions(argv, {
|
|
766
|
+
"--basename": "value", "--poster-at": "value", "--social-at": "value", "--cuts": "value",
|
|
767
|
+
"--per-beat-clips": "flag", "--beats": "value", "--output": "value", "--json": "flag",
|
|
768
|
+
});
|
|
769
|
+
const basename = optionString(parsed, "--basename");
|
|
770
|
+
if (parsed.positionals.length !== 1 || basename === undefined) fail(HTML_DELIVER_USAGE);
|
|
771
|
+
const single = (name: string) => {
|
|
772
|
+
const values = secondsList(optionString(parsed, name), HTML_DELIVER_USAGE);
|
|
773
|
+
if (values.length !== 1) fail(`${name} takes one time in seconds.`);
|
|
774
|
+
return values[0]!;
|
|
775
|
+
};
|
|
776
|
+
const cutsInput = optionString(parsed, "--cuts");
|
|
777
|
+
const cuts = cutsInput === undefined ? [] : cutsInput.split(",").map(cut => {
|
|
778
|
+
if (!(HTML_FILM_CUTS as readonly string[]).includes(cut)) fail(`--cuts takes ${HTML_FILM_CUTS.join(", ")}; got ${cut}.`);
|
|
779
|
+
return cut as HtmlFilmCut;
|
|
780
|
+
});
|
|
781
|
+
const beats = optionString(parsed, "--beats");
|
|
782
|
+
const output = optionString(parsed, "--output");
|
|
783
|
+
return {
|
|
784
|
+
kind: "html-film", action, exportPath: parsed.positionals[0]!, basename,
|
|
785
|
+
posterAt: single("--poster-at"), socialAt: single("--social-at"), cuts,
|
|
786
|
+
perBeatClips: optionFlag(parsed, "--per-beat-clips"),
|
|
787
|
+
...(beats === undefined ? {} : { beats }), ...(output === undefined ? {} : { output }),
|
|
788
|
+
json: optionFlag(parsed, "--json"),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
734
792
|
function parseOptions(argv: readonly string[], spec: OptionSpec): ParsedOptions {
|
|
735
793
|
const options: Record<string, OptionValue> = {};
|
|
736
794
|
const positionals: string[] = [];
|
|
@@ -3631,6 +3689,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
|
|
|
3631
3689
|
const command = argv[0]!;
|
|
3632
3690
|
switch (command) {
|
|
3633
3691
|
case "html": {
|
|
3692
|
+
if (argv[1] === "still" || argv[1] === "preview" || argv[1] === "deliver") return parseHtmlFilmArgs(argv[1], argv.slice(2));
|
|
3634
3693
|
if (argv[1] !== "render") fail("Use html render --input <scene.json> [--dry-run] [--json].");
|
|
3635
3694
|
const parsed = parseOptions(argv.slice(2), { "--input": "value", "--dry-run": "flag", "--json": "flag" });
|
|
3636
3695
|
const input = optionString(parsed, "--input");
|
|
@@ -3656,15 +3715,8 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
|
|
|
3656
3715
|
case "project": return parseProject(argv.slice(1));
|
|
3657
3716
|
case "inspect": return parseInspect(argv.slice(1));
|
|
3658
3717
|
case "events": return parseEvents(argv.slice(1));
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
const positionals = parsed.positionals;
|
|
3662
|
-
if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status", "start"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] or slopcamera menubar install|uninstall|status|start. See slopcamera help menubar.");
|
|
3663
|
-
if (positionals[0] !== undefined && (optionFlag(parsed, "--foreground") || optionFlag(parsed, "--background"))) throw new CliError("usage", "Choose a menu-bar action or a run mode, not both.");
|
|
3664
|
-
// `start` opens it now and returns, like --background; the login item's hints use it.
|
|
3665
|
-
if (positionals[0] === "start") return { kind: "menubar", action: "run", mode: "background", json: optionFlag(parsed, "--json") };
|
|
3666
|
-
return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
|
|
3667
|
-
}
|
|
3718
|
+
// Retired with the menu-bar companion; the old command name explains where it went.
|
|
3719
|
+
case "menubar": throw new CliError("usage", "The menu bar was retired. Use slopcamera status or slopcamera tui to see what Slopcamera is doing, and slopcamera legacy retire to stop an old copy opening at login.");
|
|
3668
3720
|
case "outputs": {
|
|
3669
3721
|
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
3670
3722
|
exactPositionals(parsed, 0, "slopcamera outputs [--json]");
|
|
@@ -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"]);
|
|
@@ -261,7 +266,6 @@ export function commandHostResourceClaims(
|
|
|
261
266
|
case "help":
|
|
262
267
|
case "capabilities":
|
|
263
268
|
case "inspect":
|
|
264
|
-
case "menubar":
|
|
265
269
|
case "operations-list":
|
|
266
270
|
case "operations-show":
|
|
267
271
|
case "outputs":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { activityLabel, recordActivity, recordCredits } from "./
|
|
1
|
+
import { activityLabel, recordActivity, recordCredits } from "./activity-status";
|
|
2
2
|
import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
|
|
3
3
|
import { SlopcameraCloudError } from "../../../src/cloud-errors";
|
|
4
4
|
import {
|
|
@@ -188,8 +188,10 @@ 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
|
-
import {
|
|
193
|
+
import { reportOutputsRoot } from "./outputs";
|
|
194
|
+
import { inspectLegacyLogin, retiredLegacyLogins } from "./legacy-login";
|
|
193
195
|
import {
|
|
194
196
|
codePreparationHostResourceClaims,
|
|
195
197
|
combineHostResourceClaims,
|
|
@@ -363,7 +365,7 @@ import {
|
|
|
363
365
|
workflowRunStore,
|
|
364
366
|
} from "./workflow-runs";
|
|
365
367
|
|
|
366
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
368
|
+
export const SLOPCAMERA_VERSION = "3.8.0";
|
|
367
369
|
|
|
368
370
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
369
371
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -7450,6 +7452,16 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
7450
7452
|
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
7451
7453
|
return;
|
|
7452
7454
|
}
|
|
7455
|
+
case "html-film": {
|
|
7456
|
+
const output = await executeHtmlFilmCommand(applicationContext(context), command,
|
|
7457
|
+
context.abortSignal ?? new AbortController().signal,
|
|
7458
|
+
{ progress: stage => context.io.stderr(`HTML film: ${stage}\n`) });
|
|
7459
|
+
writeValue(context.io, command.json, output, () => formatHtmlFilmResult(output));
|
|
7460
|
+
if (output.kind === "slopcamera.html-film-delivery" && !output.withinBudget) {
|
|
7461
|
+
throw new CliError("invalid-data", "One or more delivered files are over budget.");
|
|
7462
|
+
}
|
|
7463
|
+
return;
|
|
7464
|
+
}
|
|
7453
7465
|
case "studio": {
|
|
7454
7466
|
const output = await executeStudioCommand(applicationContext(context), command, context.abortSignal ?? new AbortController().signal);
|
|
7455
7467
|
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
@@ -7973,6 +7985,10 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
7973
7985
|
platform: { architecture: process.arch, macOSVersion: macVersion.stdout.trim(), name: context.io.platform },
|
|
7974
7986
|
repositoryRoot: context.paths.repositoryRoot,
|
|
7975
7987
|
tools: Object.fromEntries(capabilities.map((capability) => [capability.name, capability])),
|
|
7988
|
+
legacyLoginItem: {
|
|
7989
|
+
...inspectLegacyLogin(context.io.env.HOME),
|
|
7990
|
+
retired: retiredLegacyLogins(context.io.env.HOME),
|
|
7991
|
+
},
|
|
7976
7992
|
version: context.version,
|
|
7977
7993
|
};
|
|
7978
7994
|
writeValue(context.io, command.json, output, () => [
|
|
@@ -7981,6 +7997,12 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
7981
7997
|
`platform ${output.platform.name} ${output.platform.macOSVersion}`,
|
|
7982
7998
|
...capabilities.map((item) => `${item.name} ${item.available ? item.version ?? item.command : "unavailable"}`),
|
|
7983
7999
|
`emoji ${emoji.provenance} ${emoji.installedCount}/${emoji.catalogCount}; generate: ${emoji.generationCommand}`,
|
|
8000
|
+
output.legacyLoginItem.state === "found"
|
|
8001
|
+
? "login the old menu bar still opens at login; run: slopcamera legacy retire"
|
|
8002
|
+
: output.legacyLoginItem.state === "not-ours"
|
|
8003
|
+
? `login ${output.legacyLoginItem.label ?? "an item"} was changed outside Slopcamera and is left alone`
|
|
8004
|
+
: "login nothing starts at login",
|
|
8005
|
+
...output.legacyLoginItem.retired.map((item) => `retired ${item.path}; restore: ${item.restore}`),
|
|
7984
8006
|
].join("\n"));
|
|
7985
8007
|
return;
|
|
7986
8008
|
}
|
|
@@ -8021,11 +8043,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
8021
8043
|
case "credits-forget": await handleCreditsForget(context, command); return;
|
|
8022
8044
|
case "media-audio": await handleMediaAudio(context, command); return;
|
|
8023
8045
|
case "media-color": await handleMediaColor(context, command); return;
|
|
8024
|
-
case "menubar": {
|
|
8025
|
-
if (command.action === "run") await launchMenubar(context.io, context.paths.repositoryRoot, command.json, command.mode);
|
|
8026
|
-
else await manageMenubar(context.io, context.paths.repositoryRoot, command.action, command.json);
|
|
8027
|
-
return;
|
|
8028
|
-
}
|
|
8029
8046
|
case "outputs": {
|
|
8030
8047
|
await reportOutputsRoot(context.io, context.stateRoot, command.json);
|
|
8031
8048
|
return;
|
|
@@ -8269,6 +8286,7 @@ type MutationReference =
|
|
|
8269
8286
|
function commandMutationReference(command: CliCommand): MutationReference | undefined {
|
|
8270
8287
|
switch (command.kind) {
|
|
8271
8288
|
case "html-render": return undefined; // A fresh retained attempt owns its publication lease.
|
|
8289
|
+
case "html-film": return undefined; // Fresh private job directories; outputs are plain caller files.
|
|
8272
8290
|
case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
|
|
8273
8291
|
case "directing": return undefined; // The directing store owns its explicit lease.
|
|
8274
8292
|
case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
|
|
@@ -8353,7 +8371,6 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
8353
8371
|
case "fillers-list":
|
|
8354
8372
|
case "emoji-search":
|
|
8355
8373
|
case "emoji-resolve":
|
|
8356
|
-
case "menubar":
|
|
8357
8374
|
case "outputs":
|
|
8358
8375
|
case "complete": return undefined;
|
|
8359
8376
|
// Fresh projects write outside an existing mutable bundle and publish by
|