@hraness/slopcamera 3.3.3 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +60 -26
  2. package/apps/desktop/application/directing-contract.ts +15 -3
  3. package/apps/desktop/application/directing-plan.ts +2 -1
  4. package/apps/desktop/application/operation.ts +1 -1
  5. package/apps/desktop/cli/args.ts +62 -1
  6. package/apps/desktop/cli/capability-manifest.ts +4 -1
  7. package/apps/desktop/cli/cinema-renderer.ts +9 -6
  8. package/apps/desktop/cli/command-host-resources.ts +7 -0
  9. package/apps/desktop/cli/commands.ts +555 -2
  10. package/apps/desktop/cli/credits.ts +818 -0
  11. package/apps/desktop/cli/directing-blob.ts +17 -12
  12. package/apps/desktop/cli/directing-media.ts +14 -4
  13. package/apps/desktop/cli/directing-quote.ts +29 -11
  14. package/apps/desktop/cli/directing-service.ts +6 -2
  15. package/apps/desktop/cli/gateway-media-catalog.ts +57 -0
  16. package/apps/desktop/cli/gateway-media-service.ts +12 -2
  17. package/apps/desktop/cli/help.ts +51 -8
  18. package/apps/desktop/cli/main.ts +1 -1
  19. package/apps/desktop/cli/project-renderer.ts +23 -9
  20. package/apps/desktop/cli/spatial-scene-service.ts +1 -1
  21. package/apps/desktop/dist/cli/main.js +337 -257
  22. package/apps/desktop/html-overlay/spatial-splat-runtime.ts +6 -0
  23. package/dist/cli.js +14 -4
  24. package/dist/index-11cb717y.js +28 -0
  25. package/dist/{index-296qhpf2.js → index-qtn3s421.js} +1 -1
  26. package/dist/{index-0bbesn4m.js → index-sy1n4zfy.js} +2 -2
  27. package/dist/index.js +1 -1
  28. package/dist/operations.js +1 -1
  29. package/dist/workflow.js +1 -1
  30. package/docs/README.md +7 -3
  31. package/docs/studio.md +63 -3
  32. package/package.json +13 -7
  33. package/schema/drawing.schema.json +27 -0
  34. package/skills/slopcamera/SKILL.md +1 -0
  35. package/skills/slopcamera/references/directing-video.md +3 -3
  36. package/skills/slopcamera/references/educational-video.md +1 -1
  37. package/skills/slopcamera/references/install.md +1 -1
  38. package/skills/slopcamera/references/patent-drawings.md +53 -0
  39. package/skills/slopcamera/references/workflows-sdk.md +1 -1
  40. package/src/cli.ts +90 -2
  41. package/src/drawing-pdf.ts +210 -0
  42. package/src/drawing.ts +293 -0
  43. package/src/index.ts +19 -0
  44. package/src/render.ts +6 -3
  45. package/src/version.ts +1 -1
  46. package/dist/index-3e1pcxb5.js +0 -7
package/README.md CHANGED
@@ -9,10 +9,21 @@ from retained sources.**
9
9
  Describe a finished result to Codex, Claude, or another coding agent. Slopcamera gives
10
10
  it a Bun CLI, TypeScript SDK, and version-matched Agent Skill to inspect sources,
11
11
  direct cameras, edit a composition, and render the result. A separate MCP server
12
- exposes a fixed diagram and image toolset. There is no Slopcamera account or hosted
12
+ exposes fixed tools for diagrams, images, and scene inspection and planning. There is no Slopcamera account or hosted
13
13
  project database.
14
14
 
15
- [Install](#install-slopcamera) · [Make a first diagram](#make-your-first-diagram) · [Capabilities](#what-slopcamera-does) · [Documentation](docs/README.md) · [slopcamera.com](https://slopcamera.com)
15
+ [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
+
17
+ ## Made with Slopcamera
18
+
19
+ [![An original brass optical instrument rendered in Blender](apps/web/media/native-product-poster-51b1658c46d3.webp)](https://slopcamera.com/docs/tutorials/first-native-film#inspect-the-finished-example)
20
+
21
+ Watch the [six-second product film](https://slopcamera.com/docs/tutorials/first-native-film#inspect-the-finished-example),
22
+ then change its [geometry, materials and camera](examples/showcase/native/product).
23
+ For 2D motion, try the [editable editorial animation](https://slopcamera.com/docs/tutorials/first-animation).
24
+ The [portrait geometry lesson](https://slopcamera.com/docs/how-to/educational-video#inspect-the-finished-example)
25
+ adds mathematical typesetting, a presenter and authored captions. Each guide pairs
26
+ the rendered output with source, runtime requirements and reproduction steps.
16
27
 
17
28
  ## Why Slopcamera
18
29
 
@@ -33,14 +44,14 @@ project database.
33
44
 
34
45
  ## Install Slopcamera
35
46
 
36
- Slopcamera v3.3.2 fixes prop reattachment, canonical scene bindings, and saved behavior-bake auditing.
47
+ Slopcamera v3.3.4 admits image and video references in directing shots, adds a hosted tool API for agent platforms, and shows reproducible workflows across the site and docs.
37
48
 
38
49
  Install [Bun 1.3.14 or newer](https://bun.sh), then install the verified
39
- [Slopcamera v3.3.2 release](https://github.com/hraness/slopcamera/releases/tag/v3.3.2)
50
+ [Slopcamera v3.3.4 release](https://github.com/hraness/slopcamera/releases/tag/v3.3.4)
40
51
  from its canonical archive:
41
52
 
42
53
  ```sh
43
- bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.2/hraness-slopcamera-3.3.2.tgz
54
+ bun add --global https://github.com/hraness/slopcamera/releases/download/v3.3.4/hraness-slopcamera-3.3.4.tgz
44
55
  slopcamera skill install --target agents
45
56
  ```
46
57
 
@@ -98,7 +109,7 @@ change an existing release.
98
109
 
99
110
  ## Make your first diagram
100
111
 
101
- After the source build above, this local task needs no model account. In a new directory,
112
+ After installing Slopcamera, this local task needs no model account. In a new directory,
102
113
  create the included diagram, check it, and render it:
103
114
 
104
115
  ```sh
@@ -115,10 +126,11 @@ You now have `example-flow.tldr`, `example-flow.light.svg`,
115
126
  editable, and the `.tldr` file is editable tldraw interchange. Rendering again
116
127
  replaces those five derived files.
117
128
 
118
- Follow [Your first diagram](docs/tutorials/first-diagram.md) to change a label and
119
- see the result. For a moving 3D subject, use [Directed scenes](docs/spatial-scenes.md).
120
- For detailed native 3D from the current source build, follow
121
- [Your first native film](docs/tutorials/first-native-film.md).
129
+ Follow [Your first diagram](https://slopcamera.com/docs/tutorials/first-diagram) to change a label and
130
+ see the result. To animate type and SVG, [make your first animation](https://slopcamera.com/docs/tutorials/first-animation). For a moving 3D subject, use [Directed scenes](docs/spatial-scenes.md).
131
+ For detailed native 3D, follow
132
+ [Your first native film](docs/tutorials/first-native-film.md). To generate editable
133
+ architecture from dimensions, [build a parametric design](https://slopcamera.com/docs/how-to/parametric-design).
122
134
 
123
135
  ## What Slopcamera does
124
136
 
@@ -134,12 +146,12 @@ GPU path. Spark admits saved splat worlds for local camera direction.
134
146
  > mount the product diagram on a screen in the scene, and show me contact frames
135
147
  > before rendering the video.
136
148
 
137
- The portable GLB profile has a defined geometry and material subset; the
138
- additive rigged profile admits bounded skins and morph targets. Saved splats
149
+ The portable GLB profiles have defined geometry and material subsets. The
150
+ rigged/morph profile in v3.3.1 admits bounded skins and morph targets. Saved splats
139
151
  capture appearance; they do not establish collision geometry or editable native
140
152
  meshes. See [Directed scenes](docs/spatial-scenes.md).
141
153
 
142
- Current source adds the cinematic planning loop: a `slopcamera.spatial-direction`
154
+ Slopcamera v3.3.1 includes the cinematic planning loop: a `slopcamera.spatial-direction`
143
155
  document describes beats, actions, camera coverage, and look intents, compiles
144
156
  into proposed performance/camera/cinema/material-lighting/shot documents, and
145
157
  fans out into bounded galleries per axis. Declared effects bind into renders,
@@ -160,7 +172,7 @@ See [Direct a cinematic world](docs/how-to/direct-cinematic-worlds.md) and the
160
172
 
161
173
  ### Film native worlds and educational animation
162
174
 
163
- The current source build can direct Blender for detailed sets, materials,
175
+ Slopcamera can direct Blender for detailed sets, materials,
164
176
  lighting, skinned characters, cloth and liquid caches; CadQuery for parametric
165
177
  solids and STEP; and Manim Community for mathematical animation. Seven editable
166
178
  starters include a product, character, shaded street, cloth, liquid, CAD bracket,
@@ -181,11 +193,19 @@ and [Make an educational video](docs/how-to/educational-video.md).
181
193
  ### Build diagrams and motion graphics
182
194
 
183
195
  Create editable diagrams with tldraw, SVG, and PNG outputs, or turn raster artwork
184
- into SVG locally with VTracer. Animate graphic layers with HTML, SVG, Motion,
196
+ into SVG locally with VTracer. [Compare a color trace and duotone](https://slopcamera.com/docs/how-to/vectorize-images)
197
+ from the same original raster. Animate graphic layers with HTML, SVG, Motion,
185
198
  p5, Two, Paper Shaders, or Three.js. Outputs can stand alone or join a video
186
199
  project. The optional vgpu example renders programmable WebGPU passes into
187
200
  retained raster frames for use on a world-space screen.
188
201
 
202
+ [Render motion graphics from HTML](https://slopcamera.com/docs/how-to/render-motion-graphics)
203
+ connects original examples across all seven profiles to editable source and exact
204
+ render requests. Try the [editorial title](examples/showcase/html/editorial.json),
205
+ [kinetic typography](examples/showcase/html/kinetic-title.json), or
206
+ [Three.js island with original music](examples/showcase/html/island-pulse.json).
207
+ These source examples live in the current repository; `html render` ships in v3.2.8.
208
+
189
209
  Use `slopcamera html catalog` to inspect the admitted local creative tools. The
190
210
  [creative toolkit reference](docs/html-overlay-creative-toolkit.md) distinguishes
191
211
  available profiles from upstream possibilities. vgpu does not enable shared GPU
@@ -194,11 +214,19 @@ textures or a Three WebGPU renderer inside the current WebGL2/Spark profile.
194
214
  ### Generate and direct media
195
215
 
196
216
  Discover image, video, speech, and transcription models through your own Vercel
197
- AI Gateway access. Generate images from text and references, add a voiceover,
217
+ AI Gateway access. Set `AI_GATEWAY_API_KEY` in the local process environment;
218
+ the Vercel CLI is optional. Generate images from text and references, add a voiceover,
198
219
  transcribe sound, or create video shots using the selected model's supported
199
220
  inputs. Availability and pricing come from the live catalog.
200
221
 
201
- The current source build also provides `direct` shot recipes: retain a film
222
+ Without a Gateway credential, prepaid Hraness Credits run prompt-only image
223
+ generation through the hosted API: `slopcamera credits topup` prints a hosted
224
+ checkout, `slopcamera credits wait` installs the returned device token, and
225
+ `slopcamera ai image generate --hosted` bills that wallet instead of your own
226
+ Gateway key. A stored token also routes `ai image generate` to the hosted API
227
+ automatically when no Gateway credential is configured.
228
+
229
+ The released CLI also provides `direct` shot recipes: retain a film
202
230
  budget across attempts, review each take before accepting it, and use an accepted
203
231
  clip's last decoded frame as the next shot's reference. Changed predecessors
204
232
  invalidate affected continuations while earlier paid results remain retained.
@@ -220,8 +248,11 @@ from the same edit.
220
248
  > Edit my product demo: cut the pauses, zoom into each important click, keep the
221
249
  > speaker framed, add captions and `logo.svg`, and show a preview before export.
222
250
 
223
- Recording bundles can carry clicks, cursor movement, key activity, and
224
- focused-input information. Read [`PRIVACY.md`](PRIVACY.md) before editing
251
+ The released and current CLIs edit existing recording bundles; they do not
252
+ capture new recordings. Bundles can carry clicks, cursor movement, key activity,
253
+ and focused-input information. Automatic screen-action zooms need this metadata.
254
+ Standalone media imports need an existing project; `html render`, `studio assemble`,
255
+ or `direct assemble` can create one. Read [`PRIVACY.md`](PRIVACY.md) before editing
225
256
  sensitive material.
226
257
 
227
258
  Start with [Edit a video](docs/how-to/edit-video.md) or inspect a reusable recipe:
@@ -236,7 +267,7 @@ slopcamera workflows show social-variants --json
236
267
  Keep the source that owns each creative decision. A native scene owns a rig or
237
268
  simulation; a portable scene owns supported geometry, cameras, and media surfaces;
238
269
  a diagram owns its objects and labels; a video project owns cuts and delivery.
239
- SLOPCAMERA connects these through explicit assets and rendered derivatives.
270
+ Slopcamera connects these through explicit assets and rendered derivatives.
240
271
 
241
272
  1. **Prepare the sources.** Import footage and assets or author a scene, diagram,
242
273
  or native program. Inspect available tools before choosing an engine.
@@ -274,16 +305,19 @@ declarative graphs, approvals, and resuming work.
274
305
  - **Trusted code is not sandboxed.** Native Python and caller-authored Bun
275
306
  workflows run with the current user's access. Hashes and receipts identify
276
307
  observed inputs and outputs; they do not make arbitrary code hermetic.
277
- - **MCP is a subset.** Its fixed tools check and render diagrams, plan and audit
278
- scenes, and run the bounded portable operations. It does not expose every
279
- local CLI operation and never mutates project state.
308
+ - **MCP is a subset.** Its 17 fixed tools check and render diagrams, plan and audit
309
+ scenes, and run six bounded portable operation codes in v3.3.1. It does not
310
+ expose every local CLI operation and never mutates project state.
280
311
 
281
312
  ## Design and trust
282
313
 
283
314
  There is no Slopcamera account, hosted project database, or browser generation service.
284
315
  Ordinary editing and rendering remain local. Gateway generation and selected
285
316
  cloud analysis use credentials from the local process and request explicit
286
- acknowledgement before uploading named media. This website never accepts a
317
+ acknowledgement before uploading named media. The optional hosted API runs each
318
+ paid image call in an ephemeral workspace against a Hraness Credits device token
319
+ and keeps no credentials or projects; the device token lives in owner-only files
320
+ under the CLI state root and never reaches argv or receipts. This website never accepts a
287
321
  Gateway credential. Native Python requires separate authorization. Custom Bun
288
322
  workflow modules execute when loaded, including during check and plan; review
289
323
  their source first. Both have the current user's access, including potential
@@ -295,7 +329,7 @@ Native tools, providers, codecs, and GPU drivers can affect results, so retained
295
329
  source identity alone does not promise identical pixels on another machine.
296
330
 
297
331
  See [Architecture](docs/architecture.md), [`SECURITY.md`](SECURITY.md),
298
- [`PRIVACY.md`](PRIVACY.md), and [`NOTICE`](NOTICE) for the detailed boundaries.
332
+ [`PRIVACY.md`](PRIVACY.md), and [`NOTICE.md`](NOTICE.md) for the detailed boundaries.
299
333
 
300
334
  ## Documentation
301
335
 
@@ -347,7 +381,7 @@ changing a package or runtime boundary. Report vulnerabilities through
347
381
 
348
382
  ## License
349
383
 
350
- [MIT](LICENSE), with third-party notices in [`NOTICE`](NOTICE).
384
+ [MIT](LICENSE), with third-party notices in [`NOTICE.md`](NOTICE.md).
351
385
 
352
386
  ## Optional support
353
387
 
@@ -20,6 +20,10 @@ const MoneySchema = z.number().int().safe().positive();
20
20
  const TextSchema = z.string().min(1).max(100_000).refine(value => value.trim().length > 0 && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(value), "Text must be nonempty and contain no disallowed control characters.");
21
21
  const ImageSchema = GatewayMediaSourceReferenceSchema.refine(source => source.mediaType.startsWith("image/") && source.bytes <= 50 * 1024 * 1024, "A directing frame must be an image of at most 50 MiB.");
22
22
  const VideoSourceSchema = GatewayMediaSourceReferenceSchema.refine(source => source.mediaType.startsWith("video/") && source.facts?.durationSeconds !== undefined, "Endpoint video sources require a measured duration.");
23
+ const ReferenceSchema = GatewayMediaSourceReferenceSchema.refine(source =>
24
+ (source.mediaType.startsWith("image/") && source.bytes <= 50 * 1024 * 1024)
25
+ || (source.mediaType.startsWith("video/") && source.bytes <= 256 * 1024 * 1024), "A directing reference must be an image of at most 50 MiB or a video of at most 256 MiB.");
26
+ const DIRECTING_REFERENCE_LIMITS = Object.freeze({ count: 8, totalBytes: 512 * 1024 * 1024 });
23
27
 
24
28
  function capture(input: unknown, name: string, maximumBytes: number): unknown {
25
29
  return createBoundedJsonValueSnapshot(input, maximumBytes, name, { maximumDepth: 32, maximumValues: 1_000_000 }).value;
@@ -39,8 +43,13 @@ export const DirectingShotSchema = z.strictObject({
39
43
  z.strictObject({ kind: z.literal("shot-end"), shotId: SlugSchema }),
40
44
  ]).optional(),
41
45
  lastFrame: ImageSchema.optional(),
46
+ references: z.array(ReferenceSchema).min(1).max(DIRECTING_REFERENCE_LIMITS.count).optional(),
42
47
  }).superRefine((shot, context) => {
43
48
  if (shot.lastFrame !== undefined && shot.firstFrame === undefined) context.addIssue({ code: "custom", message: "A last frame requires an authored first-frame reference." });
49
+ const references = shot.references ?? [];
50
+ if (references.length > 0 && (shot.firstFrame !== undefined || shot.lastFrame !== undefined)) context.addIssue({ code: "custom", message: "Directing references are mutually exclusive with frame conditioning." });
51
+ if (references.reduce((total, reference) => total + reference.bytes, 0) > DIRECTING_REFERENCE_LIMITS.totalBytes) context.addIssue({ code: "custom", message: "Directing references exceed the 512 MiB aggregate bound." });
52
+ if (new Set(references.map(reference => `${reference.path}${reference.sha256}`)).size !== references.length) context.addIssue({ code: "custom", message: "Directing references must be distinct retained media." });
44
53
  });
45
54
 
46
55
  export const DirectingRecipeSchema = z.preprocess(input => capture(input, "directing recipe", 8 * 1024 * 1024), z.strictObject({
@@ -101,7 +110,7 @@ const ReviewSchema = z.strictObject({ decision: z.enum(["accepted", "rejected"])
101
110
  const FailureSchema = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u);
102
111
  const VideoRequestSchema = z.strictObject({ operation: z.literal("video"), request: GatewayVideoOperationInputSchema }).superRefine((input, context) => {
103
112
  const request = input.request;
104
- if (request.providerOptions !== undefined || request.references !== undefined || request.generateAudio !== undefined || (request.n ?? 1) !== 1 || (request.maxVideosPerCall ?? 1) !== 1) context.addIssue({ code: "custom", message: "Directing uses exactly one video with authored frame conditioning and no provider options." });
113
+ if (request.providerOptions !== undefined || request.generateAudio !== undefined || (request.n ?? 1) !== 1 || (request.maxVideosPerCall ?? 1) !== 1) context.addIssue({ code: "custom", message: "Directing uses exactly one video with authored conditioning and no provider options." });
105
114
  });
106
115
  const AttemptBase = {
107
116
  id: AttemptIdSchema,
@@ -149,7 +158,7 @@ export function directingRequestId(input: {
149
158
  return `gateway_${canonicalJsonSha256({ domain: "slopcamera.directing-request/v1", id, attemptId, recipeSha256, shotSha256, request })}`;
150
159
  }
151
160
 
152
- /** Verify the submitted request against the authored recipe and retained frames. */
161
+ /** Verify the submitted request against the authored recipe and retained conditioning. */
153
162
  export function directingRequestMatchesShot(attempt: DirectingAttempt, shot: DirectingShot, firstFrame: GatewayMediaSourceReference | undefined): boolean {
154
163
  const request = attempt.request.request;
155
164
  for (const key of ["prompt", "model", "durationSeconds", "resolution", "aspectRatio", "fps", "seed"] as const) if (request[key] !== shot[key]) return false;
@@ -157,7 +166,10 @@ export function directingRequestMatchesShot(attempt: DirectingAttempt, shot: Dir
157
166
  const actualFirst = request.promptImage ?? frames.find(frame => frame.frameType === "first_frame")?.source;
158
167
  const actualLast = frames.find(frame => frame.frameType === "last_frame")?.source;
159
168
  const equal = (left: GatewayMediaSourceReference | undefined, right: GatewayMediaSourceReference | undefined) => left === undefined || right === undefined ? left === right : sameDirectingSource(left, right);
160
- return equal(firstFrame, actualFirst) && equal(shot.lastFrame, actualLast);
169
+ const actualReferences = request.references ?? [];
170
+ return equal(firstFrame, actualFirst) && equal(shot.lastFrame, actualLast)
171
+ && actualReferences.length === (shot.references ?? []).length
172
+ && (shot.references ?? []).every((reference, index) => sameDirectingSource(reference, actualReferences[index]!));
161
173
  }
162
174
 
163
175
  export const DirectingStateSchema = z.preprocess(input => capture(input, "directing state", DIRECTING_LIMITS.stateBytes), z.strictObject({
@@ -28,6 +28,7 @@ export interface ResolvedDirectingShot {
28
28
  readonly dependencies: DirectingDependency[];
29
29
  readonly firstFrame?: GatewayMediaSourceReference;
30
30
  readonly lastFrame?: GatewayMediaSourceReference;
31
+ readonly references?: readonly GatewayMediaSourceReference[];
31
32
  }
32
33
 
33
34
  function resolveShot(shot: DirectingShot, accepted: ReadonlyMap<string, CompletedDirectingAttempt>): ResolvedDirectingShot | undefined {
@@ -43,7 +44,7 @@ function resolveShot(shot: DirectingShot, accepted: ReadonlyMap<string, Complete
43
44
  boundDependencies.push({ ...dependency, shotSha256: predecessor.shotSha256 });
44
45
  firstFrame = predecessor.endpoint.image;
45
46
  }
46
- return { shot, shotSha256: directingBoundShotSha256(shot, boundDependencies), dependencies, ...(firstFrame === undefined ? {} : { firstFrame }), ...(shot.lastFrame === undefined ? {} : { lastFrame: shot.lastFrame }) };
47
+ return { shot, shotSha256: directingBoundShotSha256(shot, boundDependencies), dependencies, ...(firstFrame === undefined ? {} : { firstFrame }), ...(shot.lastFrame === undefined ? {} : { lastFrame: shot.lastFrame }), ...(shot.references === undefined ? {} : { references: shot.references }) };
47
48
  }
48
49
 
49
50
  function acceptedSelections(state: DirectingState): Map<string, CompletedDirectingAttempt> {
@@ -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.3.3" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.3.5" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -217,6 +217,7 @@ export type CliCommand =
217
217
  readonly allowCloudUpload: boolean;
218
218
  readonly aspectRatio: string | undefined;
219
219
  readonly count: number;
220
+ readonly hosted: boolean;
220
221
  readonly images: readonly string[];
221
222
  readonly kind: "ai-image-generate";
222
223
  readonly mask: string | undefined;
@@ -334,6 +335,14 @@ export type CliCommand =
334
335
  } & JsonOption)
335
336
  | ({ readonly kind: "menubar"; readonly action: "run" | "install" | "uninstall" | "status"; readonly mode: "foreground" | "background" } & JsonOption)
336
337
  | ({ readonly kind: "outputs" } & JsonOption)
338
+ | ({ readonly kind: "credits-status" } & JsonOption)
339
+ | ({
340
+ readonly email: string | undefined;
341
+ readonly kind: "credits-topup";
342
+ readonly pack: string | undefined;
343
+ } & JsonOption)
344
+ | ({ readonly kind: "credits-wait"; readonly timeout: string } & JsonOption)
345
+ | ({ readonly kind: "credits-forget" } & JsonOption)
337
346
  | ({ readonly kind: "recordings-list"; readonly limit: number } & JsonOption)
338
347
  | ({ readonly kind: "projects-list"; readonly limit: number } & JsonOption)
339
348
  | ({
@@ -997,6 +1006,7 @@ function parseAiImage(argv: readonly string[]): CliCommand {
997
1006
  "--allow-cloud-upload": "flag",
998
1007
  "--aspect-ratio": "value",
999
1008
  "--count": "value",
1009
+ "--hosted": "flag",
1000
1010
  "--image": "repeat",
1001
1011
  "--mask": "value",
1002
1012
  "--max-per-call": "value",
@@ -1031,6 +1041,7 @@ function parseAiImage(argv: readonly string[]): CliCommand {
1031
1041
  allowCloudUpload: optionFlag(parsed, "--allow-cloud-upload"),
1032
1042
  aspectRatio: optionString(parsed, "--aspect-ratio"),
1033
1043
  count,
1044
+ hosted: optionFlag(parsed, "--hosted"),
1034
1045
  images,
1035
1046
  json: optionFlag(parsed, "--json"),
1036
1047
  kind: "ai-image-generate",
@@ -1345,6 +1356,55 @@ function parseAiTranscribe(argv: readonly string[]): CliCommand {
1345
1356
  };
1346
1357
  }
1347
1358
 
1359
+ function parseCredits(argv: readonly string[]): CliCommand {
1360
+ const action = argv[0];
1361
+ if (action === "status") {
1362
+ const parsed = parseOptions(argv.slice(1), JSON_SPEC);
1363
+ exactPositionals(parsed, 0, "slopcamera credits status [--json]");
1364
+ return { kind: "credits-status", json: optionFlag(parsed, "--json") };
1365
+ }
1366
+ if (action === "topup") {
1367
+ const parsed = parseOptions(argv.slice(1), {
1368
+ ...JSON_SPEC,
1369
+ "--email": "value",
1370
+ "--pack": "value",
1371
+ });
1372
+ exactPositionals(parsed, 0, "slopcamera credits topup [--pack <id>] [--email <address>] [--json]");
1373
+ const email = optionString(parsed, "--email");
1374
+ if (email !== undefined && (email.length > 320 || /[\s\u0000-\u001f\u007f]/u.test(email))) {
1375
+ fail("--email must be a nonempty email address without whitespace or control characters.");
1376
+ }
1377
+ const pack = optionString(parsed, "--pack");
1378
+ if (pack !== undefined && !/^[a-z0-9][a-z0-9_-]{0,63}$/u.test(pack)) {
1379
+ fail("--pack must be a pack id such as p10, p25, p50, or p100.");
1380
+ }
1381
+ return {
1382
+ email,
1383
+ json: optionFlag(parsed, "--json"),
1384
+ kind: "credits-topup",
1385
+ pack,
1386
+ };
1387
+ }
1388
+ if (action === "wait") {
1389
+ const parsed = parseOptions(argv.slice(1), {
1390
+ ...JSON_SPEC,
1391
+ "--timeout": "value",
1392
+ });
1393
+ exactPositionals(parsed, 0, "slopcamera credits wait [--timeout <time>] [--json]");
1394
+ return {
1395
+ kind: "credits-wait",
1396
+ json: optionFlag(parsed, "--json"),
1397
+ timeout: optionString(parsed, "--timeout") ?? "30m",
1398
+ };
1399
+ }
1400
+ if (action === "forget") {
1401
+ const parsed = parseOptions(argv.slice(1), JSON_SPEC);
1402
+ exactPositionals(parsed, 0, "slopcamera credits forget [--json]");
1403
+ return { kind: "credits-forget", json: optionFlag(parsed, "--json") };
1404
+ }
1405
+ fail("Usage: slopcamera credits <status|topup|wait|forget> [options]");
1406
+ }
1407
+
1348
1408
  function parseAi(argv: readonly string[]): CliCommand {
1349
1409
  switch (argv[0]) {
1350
1410
  case "models": return parseAiModels(argv.slice(1));
@@ -3231,7 +3291,7 @@ function parseDirectingArgs(argv: readonly string[]): DirectingCommand {
3231
3291
  const required = (name: string): string => optionString(parsed, name) ?? fail(`direct ${action} requires ${name}.`);
3232
3292
  const common = { kind: "directing" as const, json: optionFlag(parsed, "--json") };
3233
3293
  if (action === "anchor") {
3234
- exactPositionals(parsed, 0, "slopcamera direct anchor --input <image>");
3294
+ exactPositionals(parsed, 0, "slopcamera direct anchor --input <media>");
3235
3295
  return { ...common, action, input: required("--input") };
3236
3296
  }
3237
3297
  const [value] = exactPositionals(parsed, 1, `slopcamera direct ${action} <${["init", "plan", "start"].includes(action) ? "recipe.json" : "direct-id"}>`);
@@ -3589,6 +3649,7 @@ export function parseCliArgs(argv: readonly string[]): CliCommand {
3589
3649
  case "runs": return parseRuns(argv.slice(1));
3590
3650
  case "doctor": return parseDoctor(argv.slice(1));
3591
3651
  case "ai": return parseAi(argv.slice(1));
3652
+ case "credits": return parseCredits(argv.slice(1));
3592
3653
  case "media": return parseMedia(argv.slice(1));
3593
3654
  case "recordings": return parseRecordings(argv.slice(1));
3594
3655
  case "projects": return parseProjects(argv.slice(1));
@@ -110,9 +110,12 @@ const capabilityModules = Object.freeze([
110
110
  { effect: "pure", name: "capabilities" },
111
111
  { effect: "local-read", name: "diagram check" },
112
112
  { effect: "local-derived-write", name: "diagram render" },
113
+ { effect: "local-derived-write", name: "diagram sheets init" },
114
+ { effect: "local-read", name: "diagram sheets check" },
115
+ { effect: "local-derived-write", name: "diagram sheets render" },
113
116
  { effect: "local-derived-write", name: "image vectorize" },
114
117
  ],
115
- description: "Portable diagrams, local vectorization, and the bounded semantic operation projection.",
118
+ description: "Portable diagrams, CLI/SDK drawing sheets, local vectorization, and the bounded semantic operation projection.",
116
119
  moduleId: "slopcamera.capability.diagram-image",
117
120
  operationKeys: diagramImageOperations,
118
121
  profiles: [{
@@ -142,11 +142,11 @@ function sliceChain(slice: CinemaMediaSlice, inputIndex: ReadonlyMap<string, num
142
142
  const inputDurationUs = slice.fileRange.endUs - slice.fileRange.startUs;
143
143
  const outputDurationUs = slice.outputRange.endUs - slice.outputRange.startUs;
144
144
  return [
145
- `[${inputSpecifier(input, slice.streamIndex)}]`,
146
- `trim=start=${seconds(slice.fileRange.startUs)}:end=${seconds(slice.fileRange.endUs)}`,
145
+ `[${inputSpecifier(input, slice.streamIndex)}]trim=start=${seconds(slice.fileRange.startUs)}:end=${seconds(slice.fileRange.endUs)}`,
147
146
  "settb=AVTB",
148
- // Source clocks may be coarse; cinema offsets stay in integer microseconds.
149
- `setpts=(PTS-STARTPTS)*${decimal(outputDurationUs / inputDurationUs)}+${seconds(slice.outputRange.startUs)}/TB`,
147
+ // Concat requires each member to start at zero. Position the complete
148
+ // window once after joining, so internal slice boundaries stay contiguous.
149
+ `setpts=(PTS-STARTPTS)*${decimal(outputDurationUs / inputDurationUs)}`,
150
150
  "format=rgba",
151
151
  ].join(",");
152
152
  }
@@ -173,7 +173,7 @@ function windowChain(
173
173
  : `scale=w=${outputWidth}:h=${outputHeight}:force_original_aspect_ratio=decrease,pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:color=black@0`;
174
174
  const joined = label(context, prefix);
175
175
  parts.push(
176
- `${members.map(member => `[${member}]`).join("")}concat=n=${members.length}:v=1:a=0,${geometry},format=rgba${gradeFilter === undefined ? "" : `,${gradeFilter}`}[${joined}]`,
176
+ `${members.map(member => `[${member}]`).join("")}concat=n=${members.length}:v=1:a=0,setpts=PTS+${seconds(slices[0]!.outputRange.startUs)}/TB,${geometry},format=rgba${gradeFilter === undefined ? "" : `,${gradeFilter}`}[${joined}]`,
177
177
  );
178
178
  context.filters.push(...parts);
179
179
  return joined;
@@ -255,8 +255,11 @@ function transitionWindowChain(
255
255
  stream = windowChain(context, slices, shot.fit, grade, outputWidth, outputHeight, prefix);
256
256
  }
257
257
  const zeroed = label(context, `${prefix}_zero`);
258
+ // A dip/flash reveals only the tail of the incoming handle. Its hidden
259
+ // prefix still consumes source time, so the tail must meet the shot body
260
+ // without skipping the last part of the handle at the transition boundary.
258
261
  context.filters.push(
259
- `[${stream}]setpts=PTS-STARTPTS,fps=${context.rate}${limitUs === undefined ? "" : `,trim=end=${seconds(limitUs)},setpts=PTS-STARTPTS`}[${zeroed}]`,
262
+ `[${stream}]setpts=PTS-STARTPTS${limitUs === undefined ? "" : `,trim=start=${seconds(durationUs - limitUs)}:end=${seconds(durationUs)},setpts=PTS-STARTPTS`},fps=${context.rate}[${zeroed}]`,
260
263
  );
261
264
  return zeroed;
262
265
  };
@@ -239,6 +239,13 @@ export function commandHostResourceClaims(
239
239
  case "ai-models-list":
240
240
  case "ai-models-show":
241
241
  return claims(coordinator, ["network"]);
242
+ case "credits-status":
243
+ case "credits-topup":
244
+ case "credits-wait":
245
+ // Bounded hosted-service calls; `wait` polls one claim until payment.
246
+ return claims(coordinator, ["network"]);
247
+ case "credits-forget":
248
+ return [];
242
249
  case "ai-provider-options-inspect":
243
250
  case "align-apply":
244
251
  case "code-init":