@homie-rocks/studio 0.51.4 → 0.51.6
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/CHANGELOG.md +49 -0
- package/README.md +10 -29
- package/apps/APPS.md +16 -1
- package/bin/homie-studio.mjs +28 -30
- package/guides/app/NEEDS.md +160 -0
- package/guides/app/SKILL.md +20 -4
- package/guides/art/SKILL.md +68 -140
- package/guides/art/scripts/art.mjs +6 -79
- package/guides/models/SKILL.md +73 -177
- package/guides/models/references/RIGHTS.md +1 -1
- package/guides/models/scripts/models.mjs +7 -288
- package/guides/music/SKILL.md +74 -183
- package/guides/music/references/COMPOSITION.md +21 -26
- package/guides/music/references/GAME-AUDIO.md +3 -3
- package/guides/music/references/RIGHTS.md +12 -15
- package/guides/music/scripts/music.mjs +11 -202
- package/guides/parts/SKILL.md +1 -1
- package/guides/shop/SKILL.md +68 -77
- package/guides/sound/SKILL.md +70 -1
- package/guides/sound/scripts/player/sound.js +233 -0
- package/guides/studio-setup/SKILL.md +11 -27
- package/guides/studio-setup/references/OFFICIAL-MEDIA.md +66 -0
- package/guides/style/SKILL.md +66 -3
- package/guides/video/SKILL.md +83 -0
- package/guides/video/references/DELIVERY.md +73 -0
- package/guides/video/references/HONESTY.md +40 -0
- package/guides/video/references/METHOD.md +144 -0
- package/guides/video/references/RECORD.md +137 -0
- package/guides/video/references/TRAILER.md +154 -0
- package/guides/video/references/examples/site-walkthrough.json +14 -0
- package/guides/video/references/examples/studio-play-phone.json +15 -0
- package/guides/video/references/examples/studio-play.json +17 -0
- package/guides/video/scripts/capture-game.mjs +175 -0
- package/guides/video/scripts/clock.js +136 -0
- package/guides/video/scripts/drawover/film.js +58 -0
- package/guides/video/scripts/drawover/index.html +9 -0
- package/guides/video/scripts/drawover/look.js +115 -0
- package/guides/video/scripts/drawover/shots.json +12 -0
- package/guides/video/scripts/lib/browser.mjs +87 -0
- package/guides/video/scripts/lib/frames.mjs +170 -0
- package/guides/video/scripts/lib/remix.mjs +159 -0
- package/guides/video/scripts/qa.mjs +127 -0
- package/guides/video/scripts/record-fixed.mjs +403 -0
- package/guides/video/scripts/record-page.mjs +477 -0
- package/guides/video/scripts/tap.js +108 -0
- package/guides/video/scripts/video.mjs +888 -0
- package/lib/app-needs.mjs +86 -18
- package/lib/app-ready.mjs +21 -0
- package/lib/build.mjs +12 -9
- package/lib/check.mjs +16 -7
- package/lib/cloudflare.mjs +6 -11
- package/lib/codex.mjs +7 -5
- package/lib/dev.mjs +1 -2
- package/lib/doctor.mjs +10 -54
- package/lib/mcp-tools.mjs +42 -21
- package/lib/mcp.mjs +1 -1
- package/lib/media-import.mjs +41 -0
- package/lib/media-workflows.mjs +13 -0
- package/lib/needs-headless.mjs +34 -0
- package/lib/official-media.mjs +15 -0
- package/lib/rules-build.mjs +13 -5
- package/lib/scaffold.mjs +2 -2
- package/lib/shop-links.mjs +81 -19
- package/lib/shop-listen.mjs +43 -0
- package/lib/shop-local.mjs +1 -1
- package/lib/shop-mcp.mjs +58 -0
- package/lib/stripe-connect.mjs +36 -15
- package/lib/template-history.json +87 -0
- package/lib/upgrade-media.mjs +13 -0
- package/lib/upgrade.mjs +10 -0
- package/package.json +1 -1
- package/parts/PARTS.md +1 -1
- package/rules/view.ts +4 -0
- package/shop/SHOP.md +110 -34
- package/worker/app-records.mjs +1 -0
- package/worker/build-id.mjs +1 -1
- package/worker/event-resources.mjs +9 -3
- package/worker/index.mjs +5 -1
- package/worker/local-test.mjs +4 -0
- package/worker/mcp-events.mjs +2 -2
- package/worker/mcp-tools.mjs +19 -1
- package/worker/mcp.mjs +1 -1
- package/worker/purchase-reconciliation.mjs +16 -1
- package/worker/shop-links.mjs +1 -1
- package/worker/shop-rules.mjs +3 -0
- package/worker/shop-subscriptions.mjs +2 -0
- package/worker/shop.mjs +58 -8
- package/worker/stripe-ownership.mjs +7 -0
- package/worker/version.mjs +1 -1
- package/guides/models/references/models.json +0 -175
- package/guides/music/scripts/lib/eleven.mjs +0 -260
- package/lib/projects-env.mjs +0 -68
- package/lib/projects.mjs +0 -311
- package/lib/provider-key.mjs +0 -21
package/guides/art/SKILL.md
CHANGED
|
@@ -1,154 +1,82 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: art
|
|
3
|
-
description: Make
|
|
4
|
-
compatibility: Node 22
|
|
3
|
+
description: "Make game and app art using the official fal MCP, studio art direction, local capture and asset checks."
|
|
4
|
+
compatibility: Node 22 and the official fal plugin in the AI client. Local media processing uses ffmpeg and Chrome where needed.
|
|
5
5
|
metadata:
|
|
6
6
|
providers: fal
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# Art for
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
The local paid script discovers an existing fal key using the lookup below. If none is available, continue
|
|
61
|
-
with local assets or captured footage and explain that this paid-script connection is not ready.
|
|
62
|
-
Do not make account configuration a prerequisite for a live studio. **fal's own MCP server** is how you find a
|
|
63
|
-
model and read its input schema and price (`search_models`, `recommend_model`, `get_model_schema`,
|
|
64
|
-
`get_pricing`): use it rather than memory, because endpoints retire and reprice. Not connected? Offer
|
|
65
|
-
it, and the person signs in on fal's own page (no key): in Claude Code `claude mcp add --transport http
|
|
66
|
-
--scope user fal https://mcp.fal.ai/mcp-relay`, then `/mcp`; in Codex `codex mcp add fal --url
|
|
67
|
-
https://mcp.fal.ai/mcp-relay`, then `codex mcp login fal`. **Paid calls go through `art.mjs gen`,
|
|
68
|
-
never the MCP's `run_model` or `submit_job`**: `gen` prices, caps, receipts and resumes; the MCP's run
|
|
69
|
-
does none of that (the Homie mod in Claude Code, and Homie's hooks in Codex, hold one as a call whose cost cannot be read first).
|
|
70
|
-
|
|
71
|
-
```sh
|
|
72
|
-
node <art.mjs> price --model fal-ai/flux/dev --input art/cover/input.json # free: fal's unit price x this input
|
|
73
|
-
node <art.mjs> budget cover --cap <dollars the person agreed to>
|
|
74
|
-
node <art.mjs> gen cover --model fal-ai/flux/dev --input art/cover/input.json --out cover-painted.jpg --dry-run
|
|
75
|
-
node <art.mjs> gen cover --model fal-ai/flux/dev --input art/cover/input.json --out cover-painted.jpg --yes
|
|
9
|
+
# Art for this studio
|
|
10
|
+
|
|
11
|
+
Read the game's or app's style, codex and existing scene before writing a prompt.
|
|
12
|
+
Use its palette, shape language, materials, lighting and camera. Keep approved references
|
|
13
|
+
and the style revision. Covers must describe what the playable experience really offers.
|
|
14
|
+
A frame from the running game is free and often the best cover.
|
|
15
|
+
|
|
16
|
+
For generated art use fal's `recommend_model` or `search_models`, `get_model_schema`
|
|
17
|
+
and `get_pricing`, then `run_model`. Continue a pending request with `check_job` and
|
|
18
|
+
`get_job_result`. Use `upload_file` only as documented by the official plugin for references.
|
|
19
|
+
Set an explicit image count. Inspect the first result before any dependent generation.
|
|
20
|
+
|
|
21
|
+
Local `scripts/art.mjs` keeps `check`, `frame`, `cover`, `tile`, `fit` and `sheet`.
|
|
22
|
+
Use `cover <game> --from <image>` for a 1600×900 cover and game.json reference;
|
|
23
|
+
`fit <image> --out <file>` for phone-sized delivery. Check tile seams with a repeated
|
|
24
|
+
2×2 preview, transparency against light and dark, readability at phone size, and
|
|
25
|
+
compression after fitting. Do not replace an engine effect with a large image by default.
|
|
26
|
+
|
|
27
|
+
## Official generation workflow
|
|
28
|
+
|
|
29
|
+
The owner's AI uses the provider's official plugin/MCP in its own client. Homie never
|
|
30
|
+
asks for, stores, reads or prints provider credentials. Read the current provider
|
|
31
|
+
skill and discover its available tools and input schemas; host prefixes vary.
|
|
32
|
+
Connection instructions and verified sources: [official integrations](../studio-setup/references/OFFICIAL-MEDIA.md).
|
|
33
|
+
If tools are missing, connect that official plugin once in the client's browser flow.
|
|
34
|
+
Do not fall back to a Homie client, key file, environment key or CLI generation wrapper.
|
|
35
|
+
|
|
36
|
+
Before spending, state the model, count/duration, estimated cost and remaining approved
|
|
37
|
+
budget. Use the owner's existing authorization; ask only if the spend is outside it.
|
|
38
|
+
Record the request ID as soon as accepted. Poll that same request; never resubmit to
|
|
39
|
+
retry a slow job. Unknown cost is unknown, never zero. Provider billing is authoritative;
|
|
40
|
+
Homie's local receipt cannot enforce spending in another client.
|
|
41
|
+
|
|
42
|
+
## Place and prove the result
|
|
43
|
+
|
|
44
|
+
Download the completed tool's output using the AI client's file/download facilities.
|
|
45
|
+
Keep temporary signed URLs out of committed records. Keep the original in the studio's
|
|
46
|
+
art/job folder; put the optimized delivered asset beside its game/app, or in music/ or
|
|
47
|
+
videos/ for a published work. Record a receipt with these fields:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"provider": "fal",
|
|
52
|
+
"tool": "run_model",
|
|
53
|
+
"model": "exact endpoint returned by the provider",
|
|
54
|
+
"requestId": "actual request ID",
|
|
55
|
+
"prompt": "the actual prompt, including style constraints",
|
|
56
|
+
"styleRevision": "revision from the studio's style",
|
|
57
|
+
"rights": {"source": "provider/model terms URL", "checkedAt": "YYYY-MM-DD", "commercial": false, "note": "record the actual plan and permitted use"},
|
|
58
|
+
"cost": {"status": "unknown"}
|
|
59
|
+
}
|
|
76
60
|
```
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
the prompt, run it once more. Spend the riskiest image first.
|
|
84
|
-
- No real people, no real brands, no copyrighted characters, no "in the style of" a living artist, in
|
|
85
|
-
prompts or references.
|
|
86
|
-
- **Every prompt starts from the game's derived style prompt** (`homie-studio style prompt <id>`: medium,
|
|
87
|
-
shape, palette hexes, light, framing; computed from the decisions, never edited by hand) and uses the golden
|
|
88
|
-
images as references on a multi-reference model. A picture made under an older palette is listed stale when
|
|
89
|
-
the palette changes (the `style` skill's blast radius).
|
|
90
|
-
- What the provider's terms say about the pictures, with read dates: `references/RIGHTS.md`. Read the live page
|
|
91
|
-
again before anything commercial is published.
|
|
92
|
-
- 3D models (props, characters) are the `models` skill's; mood images for the style board are too
|
|
93
|
-
(`models.mjs mood`).
|
|
62
|
+
For ElevenLabs use its actual discovered tool and model, and its flow/session or request
|
|
63
|
+
identifier. A measured cost uses `{"status":"actual","amount":1,"unit":"credits"}`;
|
|
64
|
+
a quote uses `estimate`. Never claim commercial rights from the fact that generation
|
|
65
|
+
succeeded. Read the provider and model terms for the account's plan; record attribution,
|
|
66
|
+
reference-image permissions and any restrictions. Mark test fixtures `"fixture": true`.
|
|
94
67
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
A procedural gradient sky is the tell that a game was generated; a painted one costs one image at build
|
|
98
|
-
time and loads like any other asset. Good candidates: a skybox or far matte, a ground or wall texture,
|
|
99
|
-
a character plate, a title card. Bad: anything the game generates procedurally for a reason.
|
|
68
|
+
Wire the delivered path into the scene or manifest, then run:
|
|
100
69
|
|
|
101
70
|
```sh
|
|
102
|
-
|
|
103
|
-
node <art.mjs> fit art/floor/floor.png --out games/<id>/public/art/floor.jpg --width 1024 --max-kb 250
|
|
104
|
-
node <art.mjs> sheet art/floor # everything in a job, labelled, to look at
|
|
71
|
+
npx --no-install homie-studio media import --file <downloaded-file> --receipt <receipt.json> --out games/<id>/assets/<name> --used-by games/<id>/view.ts
|
|
105
72
|
```
|
|
106
73
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
asset that doubled the load time made the game worse.
|
|
113
|
-
- Put art where the game's own build puts assets (`games/<id>/public/` for a bundled game, the
|
|
114
|
-
game's folder for a static one) and load it with a relative path.
|
|
115
|
-
|
|
116
|
-
## 4. Art direction: paintovers, then change the game
|
|
117
|
-
|
|
118
|
-
Images do not fix a game that looks unfinished; the best-loved browser games often ship no texture
|
|
119
|
-
files at all. What makes a frame look finished is light, contrast between the subject and the ground,
|
|
120
|
-
detail at more than one scale, and a clear horizon or depth. The method:
|
|
121
|
-
|
|
122
|
-
1. **The style bible is the game's decisions** (the `style` skill): render style, palette with its jobs,
|
|
123
|
-
light, shape language, camera, fonts, drawn in the codex's Art direction tab. An older studio's
|
|
124
|
-
`art/STYLE.md` retires into it: carry what it says into `style set` / `style steer`, then keep only the
|
|
125
|
-
decisions. Paint with the LOCKED palette and light.
|
|
126
|
-
2. **Paintovers**: real frames of the game (`frame`), painted over by an image model with the derived style
|
|
127
|
-
prompt (`npx --no-install homie-studio style prompt <id>`) and the frame as the reference, plus the golden
|
|
128
|
-
images (`style golden`) where there are any. These are the TARGET, never shipped as the game.
|
|
129
|
-
3. **Change the game's own rendering** (lights, materials, fog, the floor, the camera) until frames of
|
|
130
|
-
the running game match the paintover; compare them side by side.
|
|
131
|
-
4. **Judge blind**: frames of the old build, the new one and a reference game the person admires,
|
|
132
|
-
shuffled under random labels, scored by a fresh reviewer before the labels are revealed (the
|
|
133
|
-
`playtest` skill's review). A reference is a bar, not a template.
|
|
134
|
-
|
|
135
|
-
## Tell the person
|
|
136
|
-
|
|
137
|
-
What was made and where it is used, the file sizes, what it cost (from the receipts) against the cap,
|
|
138
|
-
what is a real frame and what is generated, and the pictures themselves.
|
|
139
|
-
|
|
140
|
-
## Never
|
|
141
|
-
|
|
142
|
-
- Never ship generated art as a screenshot of gameplay, or a cover that shows what the game does not have.
|
|
143
|
-
- Never spend without a budget, past it, or without a receipt; never print or store a key.
|
|
144
|
-
- Never replace art a person made without asking.
|
|
145
|
-
|
|
146
|
-
### Existing provider credentials
|
|
74
|
+
This copies bytes, verifies a static reference, hashes the asset and records allowlisted
|
|
75
|
+
provenance in `media/provenance.json`. Reimporting the same output is idempotent; it
|
|
76
|
+
never generates or charges. Include each generation step's receipt for derived assets.
|
|
77
|
+
Run the scene and look/listen: a file and static reference are not proof that it renders
|
|
78
|
+
or plays. Report the placed path, usage evidence, rights and actual/estimated cost.
|
|
147
79
|
|
|
148
|
-
The
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
installs. Checks name the source, never the key. Keep these files private (mode
|
|
152
|
-
0600); never copy them into a studio. ElevenLabs also accepts its CLI browser
|
|
153
|
-
sign-in and the existing Stripe Projects connection. Run the tool’s `check`
|
|
154
|
-
before declaring a provider disconnected.
|
|
80
|
+
The toolkit’s `lib/media-workflows.mjs` declares this skill’s verified tool references.
|
|
81
|
+
Validate them against the host’s current tool inventory before execution; tool discovery
|
|
82
|
+
is required for generation capabilities that have no stable documented name.
|
|
@@ -1,33 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
* anyone plays. Real frames come from the running game for free; generated images come from fal
|
|
5
|
-
* through the creator's own account, priced first, capped by a budget, receipted the moment fal
|
|
6
|
-
* accepts them.
|
|
7
|
-
*
|
|
8
|
-
* check ffmpeg, Chrome, the studio, the fal key (a free check)
|
|
9
|
-
* frame <game> --url <site> [--view tv|play] [--seconds 12] [--slug <art job>]
|
|
10
|
-
* real frames of the game playing, the busiest one picked
|
|
11
|
-
* cover <game> --from <image> [--focus x,y]
|
|
12
|
-
* the game's cover: 16:9, 1600x900 JPEG, into the game, named in game.json
|
|
13
|
-
* budget <slug> --cap <usd> what the person agreed to spend on this art job
|
|
14
|
-
* price --model <fal model> --input <input.json>
|
|
15
|
-
* gen <slug> --model <fal model> --input <input.json> --out <file> [--dry-run | --yes]
|
|
16
|
-
* one paid image (or a few), inside the cap, resumable, receipted
|
|
17
|
-
* tile <image> does a texture tile without a seam? numbers and a 2x2 preview
|
|
18
|
-
* fit <image> --out <file> [--width 1024] [--max-kb 300]
|
|
19
|
-
* a texture or backdrop at a size a phone can download
|
|
20
|
-
* sheet <folder> a labelled contact sheet of every image in a folder
|
|
21
|
-
*
|
|
22
|
-
* Every command takes --json. Run it from inside the studio (the folder with studio.json).
|
|
23
|
-
*/
|
|
2
|
+
/** art.mjs: local asset preparation only. Generation, schemas and pricing belong to the official provider MCP.
|
|
3
|
+
* Read ../SKILL.md for the current workflow. */
|
|
24
4
|
import { spawnSync } from 'node:child_process';
|
|
25
5
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
26
6
|
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
27
7
|
import { fileURLToPath } from 'node:url';
|
|
28
8
|
import { experienceDir, experienceJson, isServerRules, SLUG, checkBudget, findStudio, readBudget, readJson, setBudget, writeJson } from '../../music/scripts/lib/studio.mjs';
|
|
29
9
|
import { GPU_FLAGS, chromePath, htmlToPng, loadPuppeteer } from '../../video/scripts/lib/browser.mjs';
|
|
30
|
-
|
|
10
|
+
|
|
31
11
|
import { decode, stats } from '../../playtest/scripts/lib/pixels.mjs';
|
|
32
12
|
|
|
33
13
|
const argv = process.argv.slice(2);
|
|
@@ -61,29 +41,15 @@ function gameDir(root, game) {
|
|
|
61
41
|
}
|
|
62
42
|
|
|
63
43
|
/** A receipt line: the job's budget (running total), art/receipts.jsonl, and HOMIE_SPEND_LEDGER when set. */
|
|
64
|
-
|
|
65
|
-
const at = new Date().toISOString();
|
|
66
|
-
const b = readBudget(dir);
|
|
67
|
-
if (b) {
|
|
68
|
-
b.spent = +(b.spent + (Number(line.cost) || 0)).toFixed(6);
|
|
69
|
-
b.calls.push({ at, provider: line.provider, model: line.model, cost: line.cost, unit: line.unit, requestId: line.requestId ?? null, artifact: line.artifact ?? null });
|
|
70
|
-
writeJson(join(dir, 'budget.json'), b);
|
|
71
|
-
}
|
|
72
|
-
mkdirSync(join(root, 'art'), { recursive: true });
|
|
73
|
-
appendFileSync(join(root, 'art', 'receipts.jsonl'), `${JSON.stringify({ schema: 'homie.media-receipt/1', at, ...line })}\n`);
|
|
74
|
-
const ledger = process.env.HOMIE_SPEND_LEDGER;
|
|
75
|
-
if (ledger) {
|
|
76
|
-
try { appendFileSync(ledger, `${JSON.stringify({ schema: 'homie.brand-spend/1', at, provider: line.provider, model: line.model, jobId: line.requestId ?? null, units: line.units ?? null, unit: line.unitName ?? line.unit, usd: line.cost, credits: null, unpriced: null, brand: process.env.HOMIE_SPEND_BRAND ?? null, attribution: process.env.HOMIE_SPEND_ATTRIBUTION ?? null, artifact: line.artifact ?? null })}\n`); } catch (e) { process.stderr.write(`could not write the ledger: ${e.message}\n`); }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
44
|
+
|
|
79
45
|
|
|
80
46
|
/* ---------------------------------------------------------------- check */
|
|
81
47
|
|
|
82
48
|
async function check() {
|
|
83
49
|
const ff = spawnSync('ffmpeg', ['-version']).status === 0;
|
|
84
50
|
const root = findStudio();
|
|
85
|
-
|
|
86
|
-
|
|
51
|
+
return { ok: ff, command: 'check', ffmpeg: ff, chrome: chromePath(), puppeteer: Boolean(loadPuppeteer(root)), studio: root, generation: 'Use the official fal MCP in your AI client' };
|
|
52
|
+
|
|
87
53
|
}
|
|
88
54
|
|
|
89
55
|
/* ---------------------------------------------------------------- frame */
|
|
@@ -175,43 +141,6 @@ function budget(root) {
|
|
|
175
141
|
return { ok: true, command: 'budget', slug: pos[1], cap: b.cap, spent: b.spent };
|
|
176
142
|
}
|
|
177
143
|
|
|
178
|
-
function inputOf(file) {
|
|
179
|
-
const j = readJson(resolve(String(file ?? '')), null);
|
|
180
|
-
if (!j) throw new Error('--input <file.json>: the model\'s input (its schema is on the model\'s fal page, or the fal MCP get_model_schema)');
|
|
181
|
-
return j;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function price() {
|
|
185
|
-
const model = String(flags.get('model') ?? '');
|
|
186
|
-
if (!model) throw new Error('--model <fal model id, e.g. fal-ai/flux/dev>');
|
|
187
|
-
const p = await priceOf(model, inputOf(flags.get('input')));
|
|
188
|
-
return { ok: true, command: 'price', ...p };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function gen(root) {
|
|
192
|
-
const slug = pos[1];
|
|
193
|
-
const dir = artDir(root, slug);
|
|
194
|
-
const model = String(flags.get('model') ?? '');
|
|
195
|
-
if (!model) throw new Error('--model <fal model id>');
|
|
196
|
-
if (!flags.get('out')) throw new Error('--out <file in this art job, e.g. cover-painted.png>');
|
|
197
|
-
const input = inputOf(flags.get('input'));
|
|
198
|
-
const out = resolve(dir, String(flags.get('out')));
|
|
199
|
-
if (!out.startsWith(`${dir}/`)) throw new Error('--out must stay inside the art job\'s folder');
|
|
200
|
-
if (existsSync(out)) return { ok: true, command: 'gen', slug, already: rel(root, out) };
|
|
201
|
-
const p = await priceOf(model, input);
|
|
202
|
-
if (flags.has('dry-run')) return { ok: true, command: 'gen', dryRun: true, slug, price: p };
|
|
203
|
-
const resuming = existsSync(`${out}.request`);
|
|
204
|
-
if (!resuming) {
|
|
205
|
-
const b = checkBudget(dir, p.usd, { provider: 'fal', unit: 'usd' });
|
|
206
|
-
if (!flags.has('yes')) return { ok: false, command: 'gen', needs: 'approval', slug, price: p, budget: { cap: b.cap, spent: b.spent }, why: `This call costs about US$${p.usd.toFixed(3)} (${p.basis}); US$${b.spent.toFixed(2)} of US$${b.cap.toFixed(2)} spent so far. Run it with --yes once that is fine.` };
|
|
207
|
-
}
|
|
208
|
-
const r = await falRun(model, input, {
|
|
209
|
-
out, base: dir, uploads: join(dir, 'uploads.json'), price: p, log: (m) => { if (!JSON_OUT) process.stderr.write(`${m}\n`); },
|
|
210
|
-
onAccepted: async (q) => { receipt(root, dir, { provider: 'fal', model, requestId: q.request_id, cost: p.usd, unit: 'usd', units: p.units, unitName: p.unit, basis: p.basis, artifact: rel(root, out) }); },
|
|
211
|
-
});
|
|
212
|
-
return { ok: true, command: 'gen', slug, files: r.files.map((f) => rel(root, f)), requestId: r.requestId, usd: p.usd, spent: readJson(join(dir, 'budget.json'), {}).spent, next: 'look at it before anything else; a second call only with a changed prompt, never a loop' };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
144
|
/* ---------------------------------------------------------------- tile / fit / sheet */
|
|
216
145
|
|
|
217
146
|
function tile() {
|
|
@@ -281,14 +210,12 @@ async function main() {
|
|
|
281
210
|
return { ok: true, command: 'help', usage: head.replace(/^#!.*\n\/\*\*?/, '').replace(/^ \* ?/gm, '').trim() };
|
|
282
211
|
}
|
|
283
212
|
if (cmd === 'check') return check();
|
|
284
|
-
if (cmd === 'price') return price();
|
|
285
213
|
if (cmd === 'tile') return tile();
|
|
286
214
|
if (cmd === 'fit') return fit();
|
|
287
215
|
const root = studioRoot();
|
|
288
216
|
if (cmd === 'frame') return frame(root);
|
|
289
217
|
if (cmd === 'cover') return cover(root);
|
|
290
218
|
if (cmd === 'budget') return budget(root);
|
|
291
|
-
if (cmd === 'gen') return gen(root);
|
|
292
219
|
if (cmd === 'sheet') return sheet(root);
|
|
293
220
|
return { ok: false, command: cmd, why: `unknown command "${cmd}" (art.mjs help)` };
|
|
294
221
|
}
|