@hraness/slopcamera 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -26
- package/apps/desktop/README.md +1 -1
- package/apps/desktop/application/html-overlay-renderer.ts +6 -0
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +60 -0
- package/apps/desktop/cli/command-host-resources.ts +5 -0
- package/apps/desktop/cli/commands.ts +13 -1
- package/apps/desktop/cli/help.ts +26 -4
- package/apps/desktop/cli/html-film-init.ts +158 -0
- package/apps/desktop/cli/html-film-names.ts +17 -0
- package/apps/desktop/cli/html-film.ts +471 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +29 -2
- package/apps/desktop/cli/html-scene.ts +3 -3
- package/apps/desktop/cli/main.ts +1 -0
- package/apps/desktop/cli/menubar-status.ts +1 -0
- package/apps/desktop/cli/portable-surface.ts +55 -1
- package/apps/desktop/dist/cli/main.js +328 -293
- package/apps/desktop/html-overlay/film.ts +418 -0
- package/dist/cli.js +13 -2
- package/dist/generate.js +1 -1
- package/dist/{index-4z9y2wa2.js → index-842t02pp.js} +1 -1
- package/dist/{index-qxfexvj3.js → index-nv3byqa1.js} +20 -4
- package/dist/index-p74xy76d.js +3 -0
- package/dist/{index-fevf4ppk.js → index-y8spxs77.js} +10 -7
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +5 -5
- package/examples/html/launch-film/README.md +51 -0
- package/examples/html/launch-film/build.ts +158 -0
- package/examples/html/launch-film/film.css +593 -0
- package/examples/html/launch-film/film.html +85 -0
- package/examples/html/launch-film/film.js +410 -0
- package/examples/html/launch-film/film.json +63 -0
- package/examples/html/launch-film/mockups.tsx +74 -0
- package/examples/html/launch-film/timeline.ts +57 -0
- package/package.json +13 -6
- package/skills/slopcamera/SKILL.md +3 -2
- package/skills/slopcamera/references/install.md +3 -3
- package/skills/slopcamera/references/launch-films.md +154 -0
- package/skills/slopcamera/references/visual-communication.md +1 -1
- package/src/cli.ts +125 -9
- package/src/generation-pricing.ts +21 -1
- package/src/icon-set.ts +1086 -0
- package/src/icon.ts +396 -59
- package/src/index.ts +1 -0
- package/src/operations.ts +52 -0
- package/src/version.ts +1 -1
- package/dist/index-nxtmy18t.js +0 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Install and diagnose
|
|
1
|
+
# Install and diagnose SlopCamera
|
|
2
2
|
|
|
3
3
|
Use this reference when the CLI is unavailable, a command differs from the installed release, or `slopcamera doctor` reports a missing dependency for the selected workflow.
|
|
4
4
|
|
|
@@ -14,14 +14,14 @@ command -v slopcamera
|
|
|
14
14
|
|
|
15
15
|
A restricted shell can omit package-manager paths. Check known host installation paths before declaring a tool unavailable. If Bun is genuinely absent, follow its official [installation guide](https://bun.sh/docs/installation) within the user’s authorized setup scope. Do not switch package managers or pipe an unreviewed installer into a shell.
|
|
16
16
|
|
|
17
|
-
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.
|
|
17
|
+
Slopcamera installs from its verified release archive: `bun add --global https://github.com/hraness/slopcamera/releases/download/v3.6.0/hraness-slopcamera-3.6.0.tgz`, then `slopcamera doctor --json`. Building from source is the contributor path. Historical Atet archives do not install the renamed CLI; never substitute `slopcamera` into an old archive URL.
|
|
18
18
|
|
|
19
19
|
If an update from a previous local archive reports `DependencyLoop`, use Bun's
|
|
20
20
|
[named tarball syntax](https://bun.com/docs/pm/cli/add#tarball-dependencies) with
|
|
21
21
|
the same verified archive:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
bun add --global @hraness/slopcamera@https://github.com/hraness/slopcamera/releases/download/v3.
|
|
24
|
+
bun add --global @hraness/slopcamera@https://github.com/hraness/slopcamera/releases/download/v3.6.0/hraness-slopcamera-3.6.0.tgz
|
|
25
25
|
slopcamera --version
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Make a product launch film
|
|
2
|
+
|
|
3
|
+
Use this workflow for a short film that walks through a product: kinetic type,
|
|
4
|
+
the product's own UI in a browser frame with a moving camera, a cursor and a
|
|
5
|
+
drawn highlight, a proof act with real numbers, a limits card and an end card.
|
|
6
|
+
The film is plain HTML, CSS and JavaScript rendered by `slopcamera html render`.
|
|
7
|
+
React is used only to turn the site's mockup components into static markup.
|
|
8
|
+
|
|
9
|
+
The same source gives a web MP4 and WebM, a poster, a social still, square and
|
|
10
|
+
vertical cuts, one short clip per act, and WebVTT captions. Each act's caption
|
|
11
|
+
doubles as the source text for a social thread post.
|
|
12
|
+
|
|
13
|
+
## Check the command
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
slopcamera help html
|
|
17
|
+
slopcamera doctor --json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The installed CLI must list `html init`, `html still` and `html deliver`.
|
|
21
|
+
Rendering needs the admitted local Chrome runtime and FFmpeg/FFprobe. Follow
|
|
22
|
+
[installation and readiness](install.md) if the command is missing.
|
|
23
|
+
|
|
24
|
+
## Start a film
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
slopcamera html init video --template launch-film --aspect 16:9
|
|
28
|
+
cd video
|
|
29
|
+
bun install
|
|
30
|
+
bun run build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`--aspect` takes `16:9`, `1:1` or `9:16` and only sets the default in
|
|
34
|
+
`film.json`; `bun build.ts --aspect 9:16` re-lays the same film at another size.
|
|
35
|
+
The project pins the SlopCamera version that wrote it in `package.json`, so the
|
|
36
|
+
CLI version is recorded next to the source.
|
|
37
|
+
|
|
38
|
+
| File | Edit it to |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `film.json` | Change copy, steps, proof items, colors, fonts and product CSS. |
|
|
41
|
+
| `mockups.tsx` | Replace the placeholder product with the site's real mockup components. |
|
|
42
|
+
| `timeline.ts` | Change act order and length. Captions and clips follow it. |
|
|
43
|
+
| `film.css` | Restyle the stage, masks, grain and browser frame. |
|
|
44
|
+
| `film.js` | Change the choreography. Keep every value a function of `t`. |
|
|
45
|
+
|
|
46
|
+
`bun run build` writes `out/film.html`, `out/scene.json`, `out/captions.vtt`
|
|
47
|
+
and `out/beats.json`. It inlines `@hraness/design-kit` `mockups.css` when the
|
|
48
|
+
project can resolve it, then any `productCss` files and fonts, and validates the
|
|
49
|
+
scene before writing it. The document limit is 1 MiB; the build prints its size.
|
|
50
|
+
|
|
51
|
+
## Keep every frame a function of time
|
|
52
|
+
|
|
53
|
+
A renderer can ask for any frame in any order, once or twice. Build each frame
|
|
54
|
+
from `t` alone:
|
|
55
|
+
|
|
56
|
+
- Read time from `SlopcameraOverlay.onFrame(({ timeMs }) => ...)`. Never use
|
|
57
|
+
`Date.now`, `requestAnimationFrame`, CSS transitions, CSS animations or a
|
|
58
|
+
counter carried between frames.
|
|
59
|
+
- Hide finished scenes with `show(element, on)`. It sets both `visibility` and
|
|
60
|
+
`display`, so a hidden scene cannot leak a stale layer into a later frame.
|
|
61
|
+
- Measure layout once, before the first frame, and store the rectangles. Do not
|
|
62
|
+
measure inside a scaled or transformed scene.
|
|
63
|
+
- Load fonts and images through `SlopcameraOverlay.ready(...)` before frames.
|
|
64
|
+
|
|
65
|
+
The helpers in `@hraness/slopcamera/local/html-film` follow these rules.
|
|
66
|
+
`build.ts` bundles them into `out/film.html`.
|
|
67
|
+
|
|
68
|
+
| Need | Helpers |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| Numbers and easing | `clamp`, `lerp`, `prog(t, start, end)`, `easings`, `spring` |
|
|
71
|
+
| Kinetic type | `split(element, "word" \| "char")`, then `kin(element, t, inAt, outAt)` |
|
|
72
|
+
| Acts and state | `defineTimeline`, `crossfade`, `stepValue`, `show` |
|
|
73
|
+
| Product walk | `camera`, `cameraBetween`, `cameraTransform`, `cursor`, `placeCursor`, `drawMark` |
|
|
74
|
+
| Captions | `captionsFromTimeline(timeline)` writes WebVTT from act captions |
|
|
75
|
+
|
|
76
|
+
## Direct each act
|
|
77
|
+
|
|
78
|
+
- **Cold open.** Two short lines over a collage of the problem. Stagger words
|
|
79
|
+
by 60 to 90 ms and hold each line long enough to read twice.
|
|
80
|
+
- **Title.** The product name and the same one-line promise the product's site
|
|
81
|
+
uses. One idea, one hold.
|
|
82
|
+
- **Product walk.** One step per entry in `film.json`. Each step names a
|
|
83
|
+
`data-film` surface for the camera `focus`, a `target` for the cursor click,
|
|
84
|
+
an optional `highlight` outline and an optional `after` state set on the target
|
|
85
|
+
once clicked. Let the camera settle before the cursor moves, click, then draw
|
|
86
|
+
the highlight. Keep steps near 4 seconds.
|
|
87
|
+
- **Proof.** Count numbers up from a facts file or release record. Never type a
|
|
88
|
+
number into `film.json` by hand.
|
|
89
|
+
- **Limits.** Say plainly what the product does not do.
|
|
90
|
+
- **End card.** Name, address and one line.
|
|
91
|
+
|
|
92
|
+
Keep public copy in plain words and sentence case, with no exclamation marks and
|
|
93
|
+
no hype. Keep the placeholder illustration note until the surfaces show the real
|
|
94
|
+
product.
|
|
95
|
+
|
|
96
|
+
## Review stills before rendering
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
slopcamera html still --input out/scene.json --at 3,12.5,20 --output out/stills
|
|
100
|
+
slopcamera html preview --input out/scene.json --every 2 --output out/preview
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Both render with the same injected runtime as `html render`, so a still matches
|
|
104
|
+
the rendered frame at that time. `preview` also writes `contact-sheet.png`. Look
|
|
105
|
+
at each act's hold, each click and each highlight before a full render. For a
|
|
106
|
+
quick draft render, `bun build.ts --scale 0.5 --fps 15 --until 3` keeps the
|
|
107
|
+
layout and shrinks the canvas, frame rate and length.
|
|
108
|
+
|
|
109
|
+
## Render and deliver
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
slopcamera html render --input out/scene.json --json > out/export.json
|
|
113
|
+
slopcamera html deliver out/export.json --basename launch --poster-at 9 --social-at 9 \
|
|
114
|
+
--cuts 1:1,9:16 --per-beat-clips
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`deliver` reads `output.path` from the export and checks its hash, then writes
|
|
118
|
+
into `out/deliver/`:
|
|
119
|
+
|
|
120
|
+
- `launch.mp4`, H.264 with faststart for the web, and `launch.webm`, VP9
|
|
121
|
+
- `launch-poster.jpg` and a 1200x630 `launch-social.jpg`
|
|
122
|
+
- `launch-1x1.mp4` and `launch-9x16.mp4`, the whole frame over a blurred fill
|
|
123
|
+
- one 6 to 10 second clip per act from `beats.json` beside the export, or the file `--beats` names
|
|
124
|
+
- `launch-receipt.json` with each file's size, hash and budget
|
|
125
|
+
|
|
126
|
+
Budgets are 12 MB per MP4, 10 MB for the WebM and 250 KB per JPEG. `deliver`
|
|
127
|
+
exits non-zero when a file is over budget; shorten the film, simplify the grain
|
|
128
|
+
or lower the frame rate rather than raising the budget.
|
|
129
|
+
|
|
130
|
+
## Turn beats into a thread
|
|
131
|
+
|
|
132
|
+
`out/beats.json` lists each act's id, start, end and caption. Use one post per
|
|
133
|
+
act, in order, with that act's clip or still attached. The captions are already
|
|
134
|
+
short and in plain words; edit them in `film.json` so the film and the posts stay
|
|
135
|
+
the same. The WebVTT file carries the same lines for the web player.
|
|
136
|
+
|
|
137
|
+
## Coming from HyperFrames or Remotion
|
|
138
|
+
|
|
139
|
+
There is no importer. The ideas map directly:
|
|
140
|
+
|
|
141
|
+
| HyperFrames or Remotion | Launch film |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| A composition's HTML, CSS and JS | `film.html`, `film.css` and `film.js` in a `plain` scene |
|
|
144
|
+
| `useCurrentFrame()` | `timeMs` from `SlopcameraOverlay.onFrame`, as `t` in seconds |
|
|
145
|
+
| `interpolate(frame, [a, b], [x, y])` | `lerp(x, y, prog(t, a, b))` |
|
|
146
|
+
| `spring({ frame, fps })` | `spring(seconds)` |
|
|
147
|
+
| `<Sequence from durationInFrames>` | an act in `defineTimeline`, read with `timeline.local(t, id)` |
|
|
148
|
+
| `<Series>` with overlaps | acts with `overlap`, blended by `crossfade` |
|
|
149
|
+
| `staticFile()` | a declared resource read with `SlopcameraOverlay.asset(name)` |
|
|
150
|
+
| `delayRender()` | `SlopcameraOverlay.ready(promise)` |
|
|
151
|
+
| Rendering with the CLI | `slopcamera html render`, then `html deliver` |
|
|
152
|
+
|
|
153
|
+
Keep React components as mockups rendered to static markup. Do not run React
|
|
154
|
+
inside the film: every frame must come from `t`, not from component state.
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises"
|
|
4
|
-
import { resolve } from "node:path"
|
|
4
|
+
import { dirname, resolve } from "node:path"
|
|
5
5
|
import {
|
|
6
6
|
artifactSummary,
|
|
7
7
|
checkDiagramFile,
|
|
@@ -17,8 +17,16 @@ import {
|
|
|
17
17
|
} from "./generate.js"
|
|
18
18
|
import {
|
|
19
19
|
generateSlopcameraIcon,
|
|
20
|
+
slopcameraIconContexts,
|
|
20
21
|
slopcameraIconMaximumRounds,
|
|
22
|
+
type SlopcameraIconContext,
|
|
23
|
+
type SlopcameraIconPurpose,
|
|
21
24
|
} from "./icon.js"
|
|
25
|
+
import {
|
|
26
|
+
generateSlopcameraIconSet,
|
|
27
|
+
parseSlopcameraIconSetSpec,
|
|
28
|
+
slopcameraIconSetMaximumSetRounds,
|
|
29
|
+
} from "./icon-set.js"
|
|
22
30
|
import {
|
|
23
31
|
generateSlopcameraImageGallery,
|
|
24
32
|
parseSlopcameraGalleryVary,
|
|
@@ -59,7 +67,11 @@ Usage:
|
|
|
59
67
|
slopcamera image generate <prompt> --output <file.png|jpg|webp> [--model <provider/model>] [--json]
|
|
60
68
|
slopcamera image icon <subject> --output <file.svg> [--purpose <mark|illustration>]
|
|
61
69
|
[--model <provider/model>] [--ink <#rgb|#rrggbb>] [--rounds <1-${slopcameraIconMaximumRounds}>]
|
|
70
|
+
[--context <${slopcameraIconContexts.join("|")}>] [--candidates <1-${slopcameraIconMaximumRounds}>]
|
|
62
71
|
[--critique-model <provider/model>] [--keep-raster] [--json]
|
|
72
|
+
slopcamera image icon --set <file.json> --output-dir <directory>
|
|
73
|
+
[--context <${slopcameraIconContexts.join("|")}>] [--candidates <1-${slopcameraIconMaximumRounds}>]
|
|
74
|
+
[--set-rounds <1-${slopcameraIconSetMaximumSetRounds}>] [--keep-raster] [--json]
|
|
63
75
|
slopcamera image gallery <subject> --output-dir <directory> [--kind <${slopcameraGalleryKinds.join("|")}>]
|
|
64
76
|
[--count <1-${slopcameraGalleryLimits.candidates}>] [--vary <axis[=v1,v2][;axis...]>] [--candidates <file.json>]
|
|
65
77
|
[--model <provider/model>] [--cell <${slopcameraGalleryLimits.cellEdgeMin}-${slopcameraGalleryLimits.cellEdgeMax}>] [--tile|--no-tile] [--json]
|
|
@@ -103,6 +115,13 @@ vectorizer, and (when --rounds exceeds 1) critiqued by a vision model whose
|
|
|
103
115
|
feedback revises the prompt for the next attempt. Only Slopcamera's own
|
|
104
116
|
generated output is uploaded for critique — never user media.
|
|
105
117
|
|
|
118
|
+
Icon --set reads a bounded JSON manifest ({name?, ink?, context?, members:
|
|
119
|
+
[{slug, subject, purpose?, context?}]}) and generates the whole family at
|
|
120
|
+
once: each member keeps a small candidate pool, joint selection minimizes the
|
|
121
|
+
measured coverage and stroke-weight spread across the set, and a
|
|
122
|
+
contact-sheet critique reviews the family as a unit before SVGs publish.
|
|
123
|
+
Manifests and receipts stay local.
|
|
124
|
+
|
|
106
125
|
Gallery generates several bounded candidates in parallel and composes them
|
|
107
126
|
into one labelled contact sheet plus a receipt. Use it to review texture,
|
|
108
127
|
skybox, backdrop, sprite, or design alternatives, then promote a chosen
|
|
@@ -250,6 +269,7 @@ export interface SlopcameraCliDependencies {
|
|
|
250
269
|
readonly generate?: typeof generateSlopcameraImageFile
|
|
251
270
|
readonly hostResourceCoordinator?: HostResourceCoordinator
|
|
252
271
|
readonly icon?: typeof generateSlopcameraIcon
|
|
272
|
+
readonly iconSet?: typeof generateSlopcameraIconSet
|
|
253
273
|
readonly log?: (value: string) => void
|
|
254
274
|
readonly vectorize?: typeof vectorizeImage
|
|
255
275
|
}
|
|
@@ -565,7 +585,19 @@ export async function main(
|
|
|
565
585
|
if (command === "icon") {
|
|
566
586
|
const parsed = parseArguments(
|
|
567
587
|
rest,
|
|
568
|
-
new Set([
|
|
588
|
+
new Set([
|
|
589
|
+
"model",
|
|
590
|
+
"output",
|
|
591
|
+
"output-dir",
|
|
592
|
+
"ink",
|
|
593
|
+
"purpose",
|
|
594
|
+
"rounds",
|
|
595
|
+
"critique-model",
|
|
596
|
+
"context",
|
|
597
|
+
"candidates",
|
|
598
|
+
"set",
|
|
599
|
+
"set-rounds",
|
|
600
|
+
]),
|
|
569
601
|
)
|
|
570
602
|
const unknownFlags = [...parsed.flags].filter(
|
|
571
603
|
(flag) => flag !== "json" && flag !== "keep-raster",
|
|
@@ -573,13 +605,6 @@ export async function main(
|
|
|
573
605
|
if (unknownFlags.length > 0) {
|
|
574
606
|
throw new Error(`Unknown icon option: --${unknownFlags[0]}`)
|
|
575
607
|
}
|
|
576
|
-
if (parsed.positionals.length !== 1) {
|
|
577
|
-
throw new Error("slopcamera image icon accepts exactly one subject")
|
|
578
|
-
}
|
|
579
|
-
const output = requiredOption(parsed, "output")
|
|
580
|
-
if (!output.toLowerCase().endsWith(".svg")) {
|
|
581
|
-
throw new Error("--output must end in .svg")
|
|
582
|
-
}
|
|
583
608
|
const model = parsed.options.model ?? slopcameraImageModels[1]
|
|
584
609
|
if (
|
|
585
610
|
model.length > 256 ||
|
|
@@ -607,10 +632,97 @@ export async function main(
|
|
|
607
632
|
if (purpose !== undefined && purpose !== "mark" && purpose !== "illustration") {
|
|
608
633
|
throw new Error("--purpose must be mark or illustration")
|
|
609
634
|
}
|
|
635
|
+
const context = parsed.options.context
|
|
636
|
+
if (
|
|
637
|
+
context !== undefined &&
|
|
638
|
+
!slopcameraIconContexts.includes(context as never)
|
|
639
|
+
) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`--context must be one of: ${slopcameraIconContexts.join(", ")}`,
|
|
642
|
+
)
|
|
643
|
+
}
|
|
610
644
|
const rounds = parsePositiveInteger(parsed.options.rounds, "rounds")
|
|
611
645
|
if (rounds !== undefined && rounds > slopcameraIconMaximumRounds) {
|
|
612
646
|
throw new Error(`--rounds must be at most ${slopcameraIconMaximumRounds}`)
|
|
613
647
|
}
|
|
648
|
+
const candidates = parsePositiveInteger(parsed.options.candidates, "candidates")
|
|
649
|
+
if (candidates !== undefined && candidates > slopcameraIconMaximumRounds) {
|
|
650
|
+
throw new Error(`--candidates must be at most ${slopcameraIconMaximumRounds}`)
|
|
651
|
+
}
|
|
652
|
+
const setRounds = parsePositiveInteger(parsed.options["set-rounds"], "set-rounds")
|
|
653
|
+
if (setRounds !== undefined && setRounds > slopcameraIconSetMaximumSetRounds) {
|
|
654
|
+
throw new Error(`--set-rounds must be at most ${slopcameraIconSetMaximumSetRounds}`)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (parsed.options.set !== undefined) {
|
|
658
|
+
if (parsed.positionals.length !== 0) {
|
|
659
|
+
throw new Error("slopcamera image icon --set takes no subject positional")
|
|
660
|
+
}
|
|
661
|
+
if (parsed.options.output !== undefined) {
|
|
662
|
+
throw new Error("--output cannot be combined with --set; use --output-dir")
|
|
663
|
+
}
|
|
664
|
+
const outputDir = requiredOption(parsed, "output-dir")
|
|
665
|
+
const manifestPath = parsed.options.set
|
|
666
|
+
let manifestText: string
|
|
667
|
+
try {
|
|
668
|
+
manifestText = await readFile(manifestPath, "utf8")
|
|
669
|
+
} catch {
|
|
670
|
+
throw new Error(`--set manifest could not be read: ${manifestPath}`)
|
|
671
|
+
}
|
|
672
|
+
if (Buffer.byteLength(manifestText, "utf8") > 256 * 1024) {
|
|
673
|
+
throw new Error("--set manifest exceeds the 256 KiB bound")
|
|
674
|
+
}
|
|
675
|
+
let manifestJson: unknown
|
|
676
|
+
try {
|
|
677
|
+
manifestJson = JSON.parse(manifestText)
|
|
678
|
+
} catch {
|
|
679
|
+
throw new Error("--set manifest must be valid JSON")
|
|
680
|
+
}
|
|
681
|
+
const spec = parseSlopcameraIconSetSpec(manifestJson)
|
|
682
|
+
const result = await withSlopcameraOperationHostAdmission(
|
|
683
|
+
"slopcamera.image.icon",
|
|
684
|
+
async (lease) =>
|
|
685
|
+
await (dependencies.iconSet ?? generateSlopcameraIconSet)({
|
|
686
|
+
keepRaster: parsed.flags.has("keep-raster"),
|
|
687
|
+
manifestDir: dirname(resolve(manifestPath)),
|
|
688
|
+
model,
|
|
689
|
+
outputDir,
|
|
690
|
+
spec,
|
|
691
|
+
inheritedFileDescriptors: [lease.inheritedFileDescriptor],
|
|
692
|
+
...(critiqueModel === undefined ? {} : { critiqueModel }),
|
|
693
|
+
...(ink === undefined ? {} : { ink }),
|
|
694
|
+
...(context === undefined
|
|
695
|
+
? {}
|
|
696
|
+
: { context: context as SlopcameraIconContext }),
|
|
697
|
+
...(purpose === undefined
|
|
698
|
+
? {}
|
|
699
|
+
: { purpose: purpose as SlopcameraIconPurpose }),
|
|
700
|
+
...(rounds === undefined ? {} : { rounds }),
|
|
701
|
+
...(candidates === undefined ? {} : { candidatesPerMember: candidates }),
|
|
702
|
+
...(setRounds === undefined ? {} : { setRounds }),
|
|
703
|
+
}),
|
|
704
|
+
hostAdmissionOptions(dependencies),
|
|
705
|
+
)
|
|
706
|
+
if (parsed.flags.has("json")) {
|
|
707
|
+
;(dependencies.log ?? console.log)(JSON.stringify(result, null, 2))
|
|
708
|
+
} else {
|
|
709
|
+
;(dependencies.log ?? console.log)(
|
|
710
|
+
`Icon set ${result.name ?? "icon-set"}: ${result.members.length} members to ${result.outputDir} ` +
|
|
711
|
+
`(target coverage ${result.target.coverageRatio.toFixed(3)}, ` +
|
|
712
|
+
`stroke ${result.target.strokePx.toFixed(1)}px, ${result.setRoundsUsed} set rounds)`,
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
reportUsefulResult(dependencies.onUsefulResult)
|
|
716
|
+
return
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (parsed.positionals.length !== 1) {
|
|
720
|
+
throw new Error("slopcamera image icon accepts exactly one subject")
|
|
721
|
+
}
|
|
722
|
+
const output = requiredOption(parsed, "output")
|
|
723
|
+
if (!output.toLowerCase().endsWith(".svg")) {
|
|
724
|
+
throw new Error("--output must end in .svg")
|
|
725
|
+
}
|
|
614
726
|
const result = await withSlopcameraOperationHostAdmission(
|
|
615
727
|
"slopcamera.image.icon",
|
|
616
728
|
async (lease) => await (dependencies.icon ?? generateSlopcameraIcon)({
|
|
@@ -623,6 +735,10 @@ export async function main(
|
|
|
623
735
|
...(ink === undefined ? {} : { ink }),
|
|
624
736
|
...(purpose === undefined ? {} : { purpose }),
|
|
625
737
|
...(rounds === undefined ? {} : { rounds }),
|
|
738
|
+
...(context === undefined
|
|
739
|
+
? {}
|
|
740
|
+
: { context: context as SlopcameraIconContext }),
|
|
741
|
+
...(candidates === undefined ? {} : { candidatePool: candidates }),
|
|
626
742
|
}),
|
|
627
743
|
hostAdmissionOptions(dependencies),
|
|
628
744
|
)
|
|
@@ -53,7 +53,7 @@ interface ImagePricedModel {
|
|
|
53
53
|
|
|
54
54
|
export type SlopcameraImageModelPrice = TokenPricedImageModel | ImagePricedModel
|
|
55
55
|
|
|
56
|
-
export const slopcameraProviderPricesCheckedAt = "2026-09-
|
|
56
|
+
export const slopcameraProviderPricesCheckedAt = "2026-09-27"
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Cited list prices. A model without an entry here cannot be billed: the
|
|
@@ -88,6 +88,26 @@ export const slopcameraImageModelPrices: Readonly<
|
|
|
88
88
|
perImage: 35_000,
|
|
89
89
|
source: "https://vercel.com/ai-gateway/models/recraft-v4.1-utility",
|
|
90
90
|
}),
|
|
91
|
+
"bfl/flux-pro-1.1": Object.freeze({
|
|
92
|
+
kind: "image",
|
|
93
|
+
perImage: 40_000,
|
|
94
|
+
source: "https://vercel.com/ai-gateway/models/flux-pro-1.1",
|
|
95
|
+
}),
|
|
96
|
+
"bfl/flux-pro-1.1-ultra": Object.freeze({
|
|
97
|
+
kind: "image",
|
|
98
|
+
perImage: 60_000,
|
|
99
|
+
source: "https://vercel.com/ai-gateway/models/flux-pro-1.1-ultra",
|
|
100
|
+
}),
|
|
101
|
+
"bfl/flux-kontext-pro": Object.freeze({
|
|
102
|
+
kind: "image",
|
|
103
|
+
perImage: 40_000,
|
|
104
|
+
source: "https://vercel.com/ai-gateway/models/flux-kontext-pro",
|
|
105
|
+
}),
|
|
106
|
+
"bfl/flux-kontext-max": Object.freeze({
|
|
107
|
+
kind: "image",
|
|
108
|
+
perImage: 80_000,
|
|
109
|
+
source: "https://vercel.com/ai-gateway/models/flux-kontext-max",
|
|
110
|
+
}),
|
|
91
111
|
})
|
|
92
112
|
|
|
93
113
|
/**
|