@hraness/slopcamera 3.3.6 → 3.5.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 +30 -16
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +1 -1
- package/apps/desktop/cli/capability-manifest.ts +13 -0
- package/apps/desktop/cli/commands.ts +8 -1
- package/apps/desktop/cli/help.ts +45 -10
- package/apps/desktop/cli/html-overlay-renderer.ts +16 -0
- package/apps/desktop/cli/menubar.ts +91 -15
- package/apps/desktop/cli/portable-surface.ts +2 -0
- package/apps/desktop/code/public.ts +1 -0
- package/apps/desktop/core/visual-style-look.ts +34 -0
- package/apps/desktop/dist/cli/main.js +338 -282
- package/apps/desktop/html-overlay/catalog.ts +1 -0
- package/apps/desktop/html-overlay/runtime.ts +15 -0
- package/apps/desktop/html-overlay/spatial.ts +10 -6
- package/apps/desktop/workflows/index.ts +1 -0
- package/dist/cli.js +6 -2
- package/dist/{index-sy1n4zfy.js → index-24w75s6a.js} +1 -1
- package/dist/{index-qtn3s421.js → index-8bhp7cj2.js} +1 -1
- package/dist/{index-10bz03js.js → index-jab2mg0s.js} +4 -3
- package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/vectorize/worker.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +4 -3
- package/examples/studio/vgpu/README.md +1 -1
- package/examples/style-portfolio/ANIMATION-STUDIES.md +34 -0
- package/examples/style-portfolio/FILM-PLATE.md +19 -0
- package/examples/style-portfolio/README.md +137 -0
- package/examples/style-portfolio/animation-studies.html +597 -0
- package/examples/style-portfolio/assets/instrument-serif-OFL.txt +93 -0
- package/examples/style-portfolio/assets/instrument-serif-latin-400.woff2 +0 -0
- package/examples/style-portfolio/build-gallery.ts +403 -0
- package/examples/style-portfolio/film-plate.html +76 -0
- package/examples/style-portfolio/finish-film.ts +152 -0
- package/examples/style-portfolio/market-street/README.md +43 -0
- package/examples/style-portfolio/market-street/mesh.py +145 -0
- package/examples/style-portfolio/market-street/scene.py +760 -0
- package/examples/style-portfolio/market-street/source.json +1 -0
- package/examples/style-portfolio/market-street/street_data.py +72 -0
- package/examples/style-portfolio/render-film-plate.ts +128 -0
- package/examples/style-portfolio/render.ts +89 -0
- package/examples/style-portfolio/verify-animation-source.mjs +94 -0
- package/package.json +14 -5
- package/skills/slopcamera/SKILL.md +13 -3
- package/skills/slopcamera/references/brand-illustrations.md +237 -0
- package/skills/slopcamera/references/gateway-media.md +6 -1
- package/skills/slopcamera/references/install.md +10 -1
- package/skills/slopcamera/references/music-video.md +5 -0
- package/skills/slopcamera/references/native-studio.md +4 -0
- package/skills/slopcamera/references/patent-drawings.md +4 -4
- package/skills/slopcamera/references/support.md +1 -1
- package/skills/slopcamera/references/vectorization.md +3 -1
- package/skills/slopcamera/references/visual-style-direction.md +143 -0
- package/src/cli.ts +28 -0
- package/src/code/public-operations.ts +9 -0
- package/src/index.ts +1 -0
- package/src/mcp/tools.ts +4 -0
- package/src/operations.ts +4 -0
- package/src/support.ts +1 -1
- package/src/vectorize/command.ts +6 -1
- package/src/vectorize/supervisor.ts +23 -3
- package/src/vectorize/tool.ts +8 -4
- package/src/version.ts +1 -1
- package/src/visual-style.ts +378 -0
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Slopcamera
|
|
2
2
|
|
|
3
|
-
[](https://slopcamera.com)
|
|
4
4
|
|
|
5
|
-
**Slopcamera is a
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
**Slopcamera is a media studio for coding agents. Codex, Claude Code, and other
|
|
6
|
+
agents make images, diagrams, animation, 3D scenes, and edited video from source
|
|
7
|
+
files they can keep revising.**
|
|
8
8
|
|
|
9
|
-
Describe
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
11
|
+
to the CLI version, so it can inspect sources, set up cameras, edit a composition,
|
|
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
|
|
14
|
+
account. Generation uses your own Vercel AI Gateway account or, for prompt-only
|
|
15
|
+
images, prepaid Hraness Credits.
|
|
14
16
|
|
|
15
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)
|
|
16
18
|
|
|
@@ -38,20 +40,21 @@ the rendered output with source, runtime requirements and reproduction steps.
|
|
|
38
40
|
timing, framing, captions, and effects as project decisions. Preview and final
|
|
39
41
|
renders use the same timeline and composition.
|
|
40
42
|
- **Choose where computation happens.** Local rendering and editing use your
|
|
41
|
-
machine. Optional model-backed work uses your Vercel AI Gateway access
|
|
42
|
-
|
|
43
|
+
machine. Optional model-backed work uses your Vercel AI Gateway access and
|
|
44
|
+
uploads named local media only after you acknowledge it. Prompt-only images can
|
|
45
|
+
instead run on the hosted API with prepaid Hraness Credits. Native Python
|
|
43
46
|
authoring requires separate trust because it runs as your current user.
|
|
44
47
|
|
|
45
48
|
## Install Slopcamera
|
|
46
49
|
|
|
47
|
-
Slopcamera v3.
|
|
50
|
+
Slopcamera v3.4.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.
|
|
48
51
|
|
|
49
52
|
Install [Bun 1.3.14 or newer](https://bun.sh), then install the verified
|
|
50
|
-
[Slopcamera v3.
|
|
53
|
+
[Slopcamera v3.4.0 release](https://github.com/hraness/slopcamera/releases/tag/v3.4.0)
|
|
51
54
|
from its canonical archive:
|
|
52
55
|
|
|
53
56
|
```sh
|
|
54
|
-
bun add --global https://github.com/hraness/slopcamera/releases/download/v3.
|
|
57
|
+
bun add --global https://github.com/hraness/slopcamera/releases/download/v3.4.0/hraness-slopcamera-3.4.0.tgz
|
|
55
58
|
slopcamera skill install --target agents
|
|
56
59
|
```
|
|
57
60
|
|
|
@@ -134,6 +137,9 @@ architecture from dimensions, [build a parametric design](https://slopcamera.com
|
|
|
134
137
|
|
|
135
138
|
## What Slopcamera does
|
|
136
139
|
|
|
140
|
+
Slopcamera lets your coding agent make images, diagrams, animation, 3D scenes,
|
|
141
|
+
and video from source files it can keep revising.
|
|
142
|
+
|
|
137
143
|
### Author scenes and direct cameras
|
|
138
144
|
|
|
139
145
|
Place geometry, images, video, diagrams, and text in a portable scene. Inspect
|
|
@@ -151,7 +157,7 @@ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved spl
|
|
|
151
157
|
capture appearance; they do not establish collision geometry or editable native
|
|
152
158
|
meshes. See [Directed scenes](docs/spatial-scenes.md).
|
|
153
159
|
|
|
154
|
-
Slopcamera v3.3.1
|
|
160
|
+
Slopcamera v3.3.1 introduced the cinematic planning loop: a `slopcamera.spatial-direction`
|
|
155
161
|
document describes beats, actions, camera coverage, and look intents, compiles
|
|
156
162
|
into proposed performance/camera/cinema/material-lighting/shot documents, and
|
|
157
163
|
fans out into bounded galleries per axis. Declared effects bind into renders,
|
|
@@ -306,7 +312,7 @@ declarative graphs, approvals, and resuming work.
|
|
|
306
312
|
workflows run with the current user's access. Hashes and receipts identify
|
|
307
313
|
observed inputs and outputs; they do not make arbitrary code hermetic.
|
|
308
314
|
- **MCP is a subset.** Its 17 fixed tools check and render diagrams, plan and audit
|
|
309
|
-
scenes, and run six bounded
|
|
315
|
+
scenes, and run six bounded operation codes. It does not
|
|
310
316
|
expose every local CLI operation and never mutates project state.
|
|
311
317
|
|
|
312
318
|
## Design and trust
|
|
@@ -331,6 +337,14 @@ source identity alone does not promise identical pixels on another machine.
|
|
|
331
337
|
See [Architecture](docs/architecture.md), [`SECURITY.md`](SECURITY.md),
|
|
332
338
|
[`PRIVACY.md`](PRIVACY.md), and [`NOTICE.md`](NOTICE.md) for the detailed boundaries.
|
|
333
339
|
|
|
340
|
+
Your agent renders images, diagrams, scenes, animation, and video from source
|
|
341
|
+
files you keep, and important operations record their inputs and outputs in
|
|
342
|
+
receipts, so a rendered result arrives with the record of how it was made: the
|
|
343
|
+
design every Hraness project shares. [The thread through
|
|
344
|
+
hraness](https://hraness.com/writing/the-thread-through-hraness) follows that
|
|
345
|
+
design across the projects, and the [ALGAL
|
|
346
|
+
vision](https://algal.computer/docs/vision/) states the bet behind it.
|
|
347
|
+
|
|
334
348
|
## Documentation
|
|
335
349
|
|
|
336
350
|
- **Learn:** [Your first diagram](docs/tutorials/first-diagram.md) · [Your first native film](docs/tutorials/first-native-film.md).
|
|
@@ -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.5.0" as const;
|
|
8
8
|
|
|
9
9
|
export const OPERATION_KINDS = [
|
|
10
10
|
"scene.audit",
|
package/apps/desktop/cli/args.ts
CHANGED
|
@@ -3659,7 +3659,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
|
|
|
3659
3659
|
case "menubar": {
|
|
3660
3660
|
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--foreground": "flag", "--background": "flag" });
|
|
3661
3661
|
const positionals = parsed.positionals;
|
|
3662
|
-
if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background]
|
|
3662
|
+
if (positionals.length > 1 || (positionals[0] !== undefined && !["install", "uninstall", "status"].includes(positionals[0]))) throw new CliError("usage", "Use slopcamera menubar [--foreground|--background] or slopcamera menubar install|uninstall|status. See slopcamera help menubar.");
|
|
3663
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
3664
|
return { kind: "menubar", action: (positionals[0] as "install" | "uninstall" | "status" | undefined) ?? "run", mode: optionFlag(parsed, "--background") ? "background" : "foreground", json: optionFlag(parsed, "--json") };
|
|
3665
3665
|
}
|
|
@@ -104,6 +104,19 @@ const gatewayOperations = [
|
|
|
104
104
|
] as const;
|
|
105
105
|
|
|
106
106
|
const capabilityModules = Object.freeze([
|
|
107
|
+
defineSlopcameraCapabilityModule({
|
|
108
|
+
commands: [{ effect: "pure", name: "style list" }, { effect: "pure", name: "style show" }],
|
|
109
|
+
description: "Renderer-independent visual direction, delivery recommendations, and deterministic exposure timing for film and animation.",
|
|
110
|
+
moduleId: "slopcamera.capability.visual-style",
|
|
111
|
+
operationKeys: [],
|
|
112
|
+
profiles: [{
|
|
113
|
+
id: "visual-style-direction-v1", kind: "authoring",
|
|
114
|
+
qualification: { evidence: ["src/visual-style.test.ts", "src/visual-style.property.test.ts"], level: "unit", status: "qualified" },
|
|
115
|
+
requirements: [],
|
|
116
|
+
}],
|
|
117
|
+
stability: "stable", title: "Visual style direction", toolNames: [],
|
|
118
|
+
trust: "portable", version: 1, workflowKeys: [],
|
|
119
|
+
}),
|
|
107
120
|
defineSlopcameraCapabilityModule({
|
|
108
121
|
commands: [
|
|
109
122
|
{ effect: "pure", name: "operations list" },
|
|
@@ -362,7 +362,7 @@ import {
|
|
|
362
362
|
workflowRunStore,
|
|
363
363
|
} from "./workflow-runs";
|
|
364
364
|
|
|
365
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
365
|
+
export const SLOPCAMERA_VERSION = "3.5.0";
|
|
366
366
|
|
|
367
367
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
368
368
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -739,6 +739,13 @@ function humanTime(microseconds: number): string {
|
|
|
739
739
|
|
|
740
740
|
function requireCapability(capabilities: readonly Capability[], name: Capability["name"]): string {
|
|
741
741
|
const capability = capabilityByName(capabilities, name);
|
|
742
|
+
if ((!capability.available || capability.command === undefined) && name === "face-analyzer") {
|
|
743
|
+
throw new CliError(
|
|
744
|
+
"unavailable",
|
|
745
|
+
"Face analysis needs a small helper that isn't built on this Mac yet. In a Slopcamera checkout, run bun run build:desktop:analysis:macos; it uses Apple's command line tools and asks before installing them.",
|
|
746
|
+
{ capability: name, ...(capability.reason === undefined ? {} : { reason: capability.reason }) },
|
|
747
|
+
);
|
|
748
|
+
}
|
|
742
749
|
if (!capability.available || capability.command === undefined) {
|
|
743
750
|
throw new CliError(
|
|
744
751
|
"unavailable",
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { HTML_OVERLAY_SCAFFOLD_KINDS } from "../html-overlay";
|
|
|
3
3
|
const HTML_OVERLAY_SCAFFOLD_KIND_HELP =
|
|
4
4
|
HTML_OVERLAY_SCAFFOLD_KINDS.join("|");
|
|
5
5
|
|
|
6
|
-
const GLOBAL_HELP = `slopcamera
|
|
6
|
+
const GLOBAL_HELP = `slopcamera: a media studio for coding agents
|
|
7
7
|
|
|
8
8
|
Usage: slopcamera <command> [options]
|
|
9
9
|
|
|
10
10
|
Commands:
|
|
11
|
-
capabilities
|
|
12
|
-
operations list|show
|
|
11
|
+
capabilities List what this build can do and how well each part is tested
|
|
12
|
+
operations list|show List the operations agents can run and their limits
|
|
13
13
|
diagram init|check|render Create, validate, or render portable diagram sources
|
|
14
14
|
diagram sheets init|check|render
|
|
15
15
|
Create and render black-and-white drawing sheets
|
|
@@ -24,6 +24,7 @@ Commands:
|
|
|
24
24
|
image vectorize|generate|gallery|icon
|
|
25
25
|
Create local SVGs, generated images, or review galleries
|
|
26
26
|
html catalog|scaffold|render Author HTML scenes and export video with local audio
|
|
27
|
+
style list|show Discover reusable art direction and review criteria
|
|
27
28
|
workflows list|show|plan|run Plan or run a reviewed reusable workflow
|
|
28
29
|
code init|check|plan|run Author, preflight, and run trusted TypeScript workflows
|
|
29
30
|
runs list|show|resume|approve|cancel
|
|
@@ -34,8 +35,9 @@ Commands:
|
|
|
34
35
|
credits status|topup|wait|forget
|
|
35
36
|
Buy and inspect hosted generation credits
|
|
36
37
|
media audio|color Apply local non-destructive audio and video effects
|
|
37
|
-
menubar [--background]
|
|
38
|
-
|
|
38
|
+
menubar [--foreground|--background]
|
|
39
|
+
Show your outputs in the menu bar
|
|
40
|
+
menubar install|uninstall|status Open it at login, stop that, or check it
|
|
39
41
|
support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
|
|
40
42
|
Optional support; no feature requires payment
|
|
41
43
|
outputs Print the agent outputs directory
|
|
@@ -54,13 +56,36 @@ Commands:
|
|
|
54
56
|
render plan|run <recording> Resolve or execute a render plan
|
|
55
57
|
assets emoji search|resolve Find checked local emoji overlays
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
Examples:
|
|
60
|
+
slopcamera doctor Check what this Mac can render
|
|
61
|
+
slopcamera html scaffold plain --output intro.html
|
|
62
|
+
Start an HTML scene
|
|
63
|
+
slopcamera render run REC --output out.mp4 Render a recording
|
|
60
64
|
|
|
61
|
-
Run slopcamera help <command> for command-specific help
|
|
65
|
+
Run slopcamera help <command> for command-specific help.
|
|
66
|
+
Optional support: slopcamera support · Turn off: HRANESS_SUPPORT_AUDIENCE=off`;
|
|
62
67
|
|
|
63
68
|
const HELP: Readonly<Record<string, string>> = {
|
|
69
|
+
menubar: `Usage:
|
|
70
|
+
slopcamera menubar [--foreground|--background] [--json]
|
|
71
|
+
slopcamera menubar install|uninstall|status [--json]
|
|
72
|
+
|
|
73
|
+
Shows 📷 in the menu bar with your recent outputs. It only lists and opens
|
|
74
|
+
files in the outputs folder (slopcamera outputs).
|
|
75
|
+
|
|
76
|
+
menubar Open it now; it closes when this command ends
|
|
77
|
+
menubar --background Open it now and return
|
|
78
|
+
menubar install Open it now and every time you log in
|
|
79
|
+
menubar uninstall Stop opening it at login and remove the installed copy
|
|
80
|
+
menubar status Check whether it opens at login and is running
|
|
81
|
+
|
|
82
|
+
If it's already in your menu bar, starting it again says so and succeeds.
|
|
83
|
+
macOS shows a notice that slopcamera-menubar can open at login. Turn it off any
|
|
84
|
+
time in System Settings › General › Login Items & Extensions.
|
|
85
|
+
|
|
86
|
+
The menu bar isn't in released packages yet. In a Slopcamera checkout, build it
|
|
87
|
+
with cargo build --release --manifest-path desktop/Cargo.toml and slopcamera
|
|
88
|
+
finds it; elsewhere, set SLOPCAMERA_MENUBAR to the built file.`,
|
|
64
89
|
capabilities: `Usage: slopcamera capabilities [--json]
|
|
65
90
|
|
|
66
91
|
Print the exact statically assembled capability manifest for this CLI build. Each module owns named operation, workflow, MCP-tool, command, and runtime-profile identities. The manifest includes trust classes, resource/effect policies, runtime requirements, qualification status, and a canonical SHA-256. It reads no workspace source, loads no plugin, probes no runtime, and grants no authority. Use doctor separately to inspect this machine's currently available executables.`,
|
|
@@ -293,6 +318,15 @@ composes a labelled contact sheet plus receipt for agent review. Texture cells r
|
|
|
293
318
|
candidate 2×2 so seams are inspectable (--tile/--no-tile overrides). It never replaces existing
|
|
294
319
|
outputs and never promotes a candidate into authored source; review the sheet and select
|
|
295
320
|
explicitly.`,
|
|
321
|
+
style: `Usage:
|
|
322
|
+
slopcamera style list [--json]
|
|
323
|
+
slopcamera style show <id> [--json]
|
|
324
|
+
|
|
325
|
+
Styles describe palette, shape, materials, camera, exposure cadence, finishing targets,
|
|
326
|
+
recommended delivery, and visual review criteria. They work across authored scenes and
|
|
327
|
+
generation prompts, independently of the seven HTML library profiles. Discovery is local
|
|
328
|
+
and read-only: it does not render, select a model, or apply an effect. A profile is art
|
|
329
|
+
direction, not proof of visual quality or historically authentic footage.`,
|
|
296
330
|
html: `Usage:
|
|
297
331
|
slopcamera html catalog [--json]
|
|
298
332
|
slopcamera html scaffold <${HTML_OVERLAY_SCAFFOLD_KIND_HELP}> --output <file.html>
|
|
@@ -673,12 +707,13 @@ export function commandHelp(topic: readonly string[]): string {
|
|
|
673
707
|
|
|
674
708
|
export function completions(words: readonly string[]): readonly string[] {
|
|
675
709
|
const topLevel = [
|
|
676
|
-
"capabilities", "operations", "diagram", "direct", "studio", "image", "html", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
|
|
710
|
+
"capabilities", "operations", "diagram", "direct", "studio", "image", "html", "style", "workflows", "code", "runs", "doctor", "ai", "credits", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "scene", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
|
|
677
711
|
];
|
|
678
712
|
if (words.length <= 1) return topLevel;
|
|
679
713
|
const command = words[0];
|
|
680
714
|
if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
|
|
681
715
|
if (command === "html") return ["catalog", "scaffold", "render"];
|
|
716
|
+
if (command === "style") return ["list", "show"];
|
|
682
717
|
if (command === "direct") return ["init", "anchor", "plan", "start", "inspect", "revise", "generate", "resume", "review", "assemble", "cleanup"];
|
|
683
718
|
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"];
|
|
684
719
|
if (command === "operations") return ["list", "show"];
|
|
@@ -135,6 +135,7 @@ type BrowserRuntimeSnapshotLease = Readonly<
|
|
|
135
135
|
interface HtmlOverlayHostController {
|
|
136
136
|
renderFrame(frame: HtmlOverlayRuntimeFrame): Promise<void>;
|
|
137
137
|
securityViolationCount(): number;
|
|
138
|
+
settlePresentation(): Promise<void>;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
interface PreparedRoute {
|
|
@@ -2332,6 +2333,21 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
|
|
|
2332
2333
|
);
|
|
2333
2334
|
}
|
|
2334
2335
|
await inspectGpu();
|
|
2336
|
+
// Wait for the compositor to present the frame the callbacks
|
|
2337
|
+
// just produced. renderFrame resolves when authored work returns,
|
|
2338
|
+
// not when the new surface reaches the screen; under heavy GPU
|
|
2339
|
+
// load a screenshot taken immediately can capture the previous
|
|
2340
|
+
// presented frame. Two animation frames bound the wait: the draw
|
|
2341
|
+
// is committed before the first callback fires and presented
|
|
2342
|
+
// before the second.
|
|
2343
|
+
await boundedBrowserStep(
|
|
2344
|
+
async () => await host.evaluate(
|
|
2345
|
+
async (controller) => await controller.settlePresentation(),
|
|
2346
|
+
),
|
|
2347
|
+
signal,
|
|
2348
|
+
this.#browserStepTimeoutMs,
|
|
2349
|
+
`frame ${String(frameIndex)} presentation settle`,
|
|
2350
|
+
);
|
|
2335
2351
|
await boundedBrowserStep(
|
|
2336
2352
|
async () => await page.screenshot({
|
|
2337
2353
|
omitBackground: HTML_OVERLAY_RENDERER_CONTRACT.screenshot.omitBackground,
|
|
@@ -208,33 +208,109 @@ export function uninstallLaunchAgent(_binary: string, environment: Environment =
|
|
|
208
208
|
if (infoAt(stable) !== null) rmSync(stable);
|
|
209
209
|
return "absent";
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
/** `slopcamera-menubar` exits with this code when another copy already holds the menu bar. */
|
|
212
|
+
export const ALREADY_RUNNING_EXIT = 3;
|
|
213
|
+
const NOT_BUILT = "The Slopcamera menu bar isn't in this release yet. Build it with cargo build --release --manifest-path desktop/Cargo.toml in a Slopcamera checkout; slopcamera finds it there, or set SLOPCAMERA_MENUBAR to the built file.";
|
|
214
|
+
const LOGIN_ITEMS = "System Settings › General › Login Items & Extensions";
|
|
215
|
+
|
|
216
|
+
export type Launched = "running" | "already-running" | "exited";
|
|
217
|
+
export type BinaryRunner = (binary: string, foreground: boolean) => Promise<number | null>;
|
|
218
|
+
|
|
219
|
+
/** Resolves the exit code, or null when a background copy is still running after startup. */
|
|
220
|
+
async function spawnBinary(binary: string, foreground: boolean): Promise<number | null> {
|
|
212
221
|
let child: Bun.Subprocess;
|
|
213
|
-
try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar
|
|
214
|
-
if (!foreground) { child.unref();
|
|
222
|
+
try { child = Bun.spawn([binary], foreground ? { stdin: "inherit", stdout: "inherit", stderr: "inherit" } : { stdin: "ignore", stdout: "ignore", stderr: "ignore" }); } catch { throw new CliError("unavailable", "The Slopcamera menu bar couldn't start."); }
|
|
223
|
+
if (!foreground) { child.unref(); return await Promise.race([child.exited.then((code) => code as number | null), Bun.sleep(SETTLE_MS).then(() => null)]); }
|
|
215
224
|
return await child.exited;
|
|
216
225
|
}
|
|
217
|
-
|
|
226
|
+
|
|
227
|
+
/** Interprets the companion's exit: another running copy is success, not a failure. */
|
|
228
|
+
export function launchOutcome(code: number | null, foreground: boolean): Launched {
|
|
229
|
+
if (code === ALREADY_RUNNING_EXIT) return "already-running";
|
|
230
|
+
if (code === null && !foreground) return "running";
|
|
231
|
+
if (code === 0 && foreground) return "exited";
|
|
232
|
+
throw new CliError("unavailable", "The Slopcamera menu bar stopped while starting. Run it in the foreground to see why: slopcamera menubar --foreground");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface Marks { readonly ok: string; readonly warn: string; readonly note: string; readonly next: string }
|
|
236
|
+
/** ✓ ⚠ 🔐 → with ASCII fallbacks for dumb terminals and non-UTF-8 locales. */
|
|
237
|
+
export function marks(env: Readonly<Record<string, string | undefined>>): Marks {
|
|
238
|
+
const utf8 = [env.LC_ALL, env.LC_CTYPE, env.LANG].some((value) => value !== undefined && /utf-?8/i.test(value));
|
|
239
|
+
const ascii = env.TERM === "dumb" || !utf8 || env.HRANESS_ASCII === "1";
|
|
240
|
+
return ascii ? { ok: "OK", warn: "WARN", note: "NOTE", next: "->" } : { ok: "✓", warn: "⚠", note: "🔐", next: "→" };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The LOGIN_ITEM notice (notifies, so there is no confirm line). The requester is the bare companion until Slopcamera ships as an app. */
|
|
244
|
+
export function loginItemNotice(env: Readonly<Record<string, string | undefined>>): string {
|
|
245
|
+
return `${marks(env).note} macOS will show a notice that slopcamera-menubar can open at login. That's Slopcamera's menu bar.\n It lists your outputs and opens when you log in. Nothing else runs in the background. Turn it off any time in ${LOGIN_ITEMS}.\n`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function launchMessage(outcome: Launched, env: Readonly<Record<string, string | undefined>>): string {
|
|
249
|
+
const mark = marks(env);
|
|
250
|
+
if (outcome === "already-running") return `${mark.ok} Slopcamera is already in your menu bar. Look for 📷.`;
|
|
251
|
+
if (outcome === "running") return `${mark.ok} Slopcamera is in your menu bar. Look for 📷.`;
|
|
252
|
+
return "The Slopcamera menu bar closed.";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function stateMessage(action: "install" | "uninstall" | "status", state: LaunchAgentState, running: boolean, env: Readonly<Record<string, string | undefined>>): string {
|
|
256
|
+
const mark = marks(env);
|
|
257
|
+
if (state === "conflict") return `${mark.warn} Another menu-bar setup that this command didn't create is in the way. It was left untouched.\n Remove ~/Library/LaunchAgents/${LABEL}.plist yourself if you don't need it.`;
|
|
258
|
+
if (state === "absent") return action === "uninstall" ? `${mark.ok} Slopcamera no longer opens in your menu bar at login.` : `Slopcamera doesn't open at login.\n${mark.next} slopcamera menubar install`;
|
|
259
|
+
if (action === "install") return `${mark.ok} Slopcamera opens in your menu bar now and at every login. Look for 📷.\n Remove it any time: slopcamera menubar uninstall`;
|
|
260
|
+
return running
|
|
261
|
+
? `${mark.ok} Slopcamera opens at login and is in your menu bar now.`
|
|
262
|
+
: `${mark.warn} Slopcamera is set to open at login but isn't running. It may be turned off in ${LOGIN_ITEMS}.\n${mark.next} slopcamera menubar install`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Whether launchd reports the loaded companion as running. Reads state only. */
|
|
266
|
+
export function serviceRunning(runLaunchctl: LaunchctlRunner): boolean {
|
|
267
|
+
const output = runLaunchctl(["print", `gui/{uid}/${LABEL}`], true);
|
|
268
|
+
return output !== null && output.split("\n").some((line) => line.trim() === "state = running");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type ProcessProbe = () => boolean;
|
|
272
|
+
/** Whether any copy of the companion runs for this user, including one started by hand. */
|
|
273
|
+
function companionProcessRunning(): boolean {
|
|
274
|
+
const uid = process.getuid?.();
|
|
275
|
+
if (uid === undefined) return false;
|
|
276
|
+
try {
|
|
277
|
+
return Bun.spawnSync(["/usr/bin/pgrep", "-x", "-U", String(uid), "slopcamera-menubar"], { stdin: "ignore", stdout: "ignore", stderr: "ignore", timeout: 5_000 }).exitCode === 0;
|
|
278
|
+
} catch { return false; }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The menu bar is showing when launchd runs the login item or another copy holds it. */
|
|
282
|
+
async function menubarShowing(runLaunchctl: LaunchctlRunner, probe: ProcessProbe, waitMs: number): Promise<boolean> {
|
|
283
|
+
const deadline = Date.now() + waitMs;
|
|
284
|
+
for (;;) {
|
|
285
|
+
if (serviceRunning(runLaunchctl) || probe()) return true;
|
|
286
|
+
if (Date.now() >= deadline) return false;
|
|
287
|
+
await Bun.sleep(200);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function launchMenubar(io: CliIo, repositoryRoot: string, asJson: boolean, mode: "foreground" | "background" = "foreground", runBinary: BinaryRunner = spawnBinary): Promise<void> {
|
|
218
292
|
const binary = resolveMenubarBinary(repositoryRoot, io.env);
|
|
219
|
-
if (binary === null) throw new CliError("unavailable",
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
else writeLine(io, mode === "foreground" ? "Slopcamera menu bar exited." : "Slopcamera menu bar is running.");
|
|
293
|
+
if (binary === null) throw new CliError("unavailable", NOT_BUILT);
|
|
294
|
+
const outcome = launchOutcome(await runBinary(binary, mode === "foreground"), mode === "foreground");
|
|
295
|
+
if (asJson) writeJson(io, { running: outcome !== "exited", foreground: mode === "foreground", alreadyRunning: outcome === "already-running" });
|
|
296
|
+
else writeLine(io, launchMessage(outcome, io.env));
|
|
224
297
|
}
|
|
225
|
-
export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean): Promise<void> {
|
|
298
|
+
export async function manageMenubar(io: CliIo, repositoryRoot: string, action: "install" | "uninstall" | "status", asJson: boolean, runLaunchctl: LaunchctlRunner = launchctl, probe: ProcessProbe = companionProcessRunning): Promise<void> {
|
|
226
299
|
assertMac(io.platform);
|
|
227
300
|
const stable = installedBinaryPath(io.env);
|
|
228
301
|
let state: LaunchAgentState;
|
|
229
302
|
if (action === "install") {
|
|
230
303
|
const binary = resolveMenubarBinary(repositoryRoot, io.env);
|
|
231
|
-
if (binary === null) throw new CliError("unavailable",
|
|
232
|
-
|
|
304
|
+
if (binary === null) throw new CliError("unavailable", NOT_BUILT);
|
|
305
|
+
if (!asJson) io.stderr(loginItemNotice(io.env));
|
|
306
|
+
state = installLaunchAgent(binary, io.env, io.platform, runLaunchctl);
|
|
233
307
|
} else {
|
|
234
|
-
state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform) : launchAgentState(stable, io.env);
|
|
308
|
+
state = action === "uninstall" ? uninstallLaunchAgent(stable, io.env, io.platform, runLaunchctl) : launchAgentState(stable, io.env);
|
|
235
309
|
}
|
|
236
|
-
|
|
237
|
-
|
|
310
|
+
// A fresh login item may take a moment to start, so install waits briefly.
|
|
311
|
+
const running = state === "installed" && await menubarShowing(runLaunchctl, probe, action === "install" ? 1_000 : 0);
|
|
312
|
+
if (asJson) writeJson(io, { launchAgent: state, running });
|
|
313
|
+
else writeLine(io, stateMessage(action, state, running, io.env));
|
|
238
314
|
}
|
|
239
315
|
export async function reportOutputsRoot(io: CliIo, stateRoot: string, asJson: boolean): Promise<void> {
|
|
240
316
|
const directory = outputsRoot(stateRoot);
|
|
@@ -220,12 +220,14 @@ export async function runPortableSurface(
|
|
|
220
220
|
dependencies: PortableSurfaceDependencies = {},
|
|
221
221
|
): Promise<number | undefined> {
|
|
222
222
|
const argv = canonicalizeUnifiedCliArgs(argvInput);
|
|
223
|
+
if (argv[0] === "style" && (argv.includes("--help") || argv.includes("-h"))) return undefined;
|
|
223
224
|
if (argv[0] === "html") {
|
|
224
225
|
if (argv[1] === "render" || argv.includes("--help") || argv.includes("-h")) return undefined;
|
|
225
226
|
if (argv[1] === "catalog") return runHtmlCatalog(argv, dependencies);
|
|
226
227
|
return await runHtmlScaffold(argv, dependencies);
|
|
227
228
|
}
|
|
228
229
|
const delegatesToHeadless = argv[0] === "diagram"
|
|
230
|
+
|| argv[0] === "style"
|
|
229
231
|
|| argv[0] === "mcp"
|
|
230
232
|
|| argv[0] === "canvas"
|
|
231
233
|
|| argv[0] === "skill"
|
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
} from "./define-workflow";
|
|
11
11
|
export * from "../../../src/spatial-scene/index";
|
|
12
12
|
export * from "../../../src/studio/index";
|
|
13
|
+
export { createVisualStyleVideoLook, VisualStyleVideoLookOptionsSchema } from "../core/visual-style-look";
|
|
13
14
|
export { StudioRunInputSchema, BoundStudioRunInputSchema, StudioRunOutputSchema, type StudioRunInput, type BoundStudioRunInput, type StudioRunOutput } from "../application/studio-port";
|
|
14
15
|
export type { SpatialRenderInput, BoundSpatialRenderInput } from "../application/operations/spatial-render";
|
|
15
16
|
export type { SpatialRenderRequest, SpatialRenderResult } from "../application/spatial-render";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getVisualStyleProfile } from "../../../src/visual-style";
|
|
3
|
+
import type { VideoEffectInput, VideoLookV1 } from "../contracts/video-effects";
|
|
4
|
+
import { createVideoLook } from "./video-effects";
|
|
5
|
+
|
|
6
|
+
export const VisualStyleVideoLookOptionsSchema = z.strictObject({
|
|
7
|
+
height: z.number().int().min(64).max(4320).default(1080),
|
|
8
|
+
seed: z.number().int().safe().min(0).max(2_147_483_647).default(20260923),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Translate only supported finishing controls into the existing ordered look
|
|
13
|
+
* graph. Scene construction, motion, gate weave, grain size, and true halation
|
|
14
|
+
* remain authoring decisions; screen diffusion is not a film-stock simulation.
|
|
15
|
+
*/
|
|
16
|
+
export function createVisualStyleVideoLook(style: unknown, options: unknown = {}): VideoLookV1 {
|
|
17
|
+
const profile = getVisualStyleProfile(style);
|
|
18
|
+
const settings = VisualStyleVideoLookOptionsSchema.parse(options);
|
|
19
|
+
const finish = profile.finishing;
|
|
20
|
+
const effects: VideoEffectInput[] = [{
|
|
21
|
+
kind: "color-grade", amount: 1,
|
|
22
|
+
grade: { kind: "custom", controls: { saturation: finish.saturation, contrast: finish.contrast } },
|
|
23
|
+
}];
|
|
24
|
+
if (finish.halation > 0) effects.push({
|
|
25
|
+
kind: "diffusion", amount: finish.halation,
|
|
26
|
+
radiusPx: Math.max(0.25, 1.8 * settings.height / 1080), blendMode: "screen",
|
|
27
|
+
});
|
|
28
|
+
if (finish.grain > 0) effects.push({
|
|
29
|
+
kind: "film-grain", amount: finish.grain, seed: settings.seed,
|
|
30
|
+
cadence: "frame-varying", chroma: finish.saturation === 0 ? 0 : 0.12,
|
|
31
|
+
});
|
|
32
|
+
if (finish.vignette > 0) effects.push({ kind: "vignette", amount: finish.vignette });
|
|
33
|
+
return createVideoLook(effects);
|
|
34
|
+
}
|