makaron-persona-look-cli 0.5.1 → 0.5.2
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
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What an Agent gets
|
|
6
6
|
|
|
7
|
-
`preview` downloads the selected Persona source/approved auxiliary views and the Look source, plus a safe `selection.json` render brief. It is intentionally a **source-reference pack**, not a synthesized image. `render` is the delivery route: it sends those private inputs to the caller's authenticated Makaron CLI and writes exactly one full-body front/right-side/back turnaround sheet. The Persona is the only identity reference; the Look image's face is excluded.
|
|
7
|
+
`find` is the no-generation delivery route for a single library result: `--kind persona` returns one matched Persona image, and `--kind look` returns one matched Look image. It does not expose IDs or selection metadata. `preview` downloads the selected Persona source/approved auxiliary views and the Look source, plus a safe `selection.json` render brief. It is intentionally a **source-reference pack**, not a synthesized image. `render` is the combined Persona + Look delivery route: it sends those private inputs to the caller's authenticated Makaron CLI and writes exactly one full-body front/right-side/back turnaround sheet. The Persona is the only identity reference; the Look image's face is excluded.
|
|
8
8
|
|
|
9
9
|
The raw `private-library/` is ignored by Git and is never included in the npm package. D1 holds catalog metadata, private R2 holds approved source bytes, and a Worker issues an individual credential to each enrolling Agent. The individual credential is stored only in the Agent's local mode-600 config file and is never printed by the CLI.
|
|
10
10
|
|
|
@@ -41,15 +41,22 @@ personlib remote sync --library ./private-library --json
|
|
|
41
41
|
|
|
42
42
|
## Agent natural-language route
|
|
43
43
|
|
|
44
|
-
Once setup and `remote doctor` succeed,
|
|
44
|
+
Once setup and `remote doctor` succeed, use the route that matches the request:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
47
|
+
# Persona-only: return one matching library image; no Makaron request.
|
|
48
|
+
personlib remote find --kind persona --brief '我要一个长相高冷的女生' --output-dir ./personlib-persona --json
|
|
49
|
+
|
|
50
|
+
# Look-only: return one matching library image; no Makaron request.
|
|
51
|
+
personlib remote find --kind look --brief '我要一套 Y2K 的妆造' --output-dir ./personlib-look --json
|
|
52
|
+
|
|
53
|
+
# Persona + Look: render the fused three-view image.
|
|
54
|
+
personlib remote render --brief '我要一个嘻哈风格的亚洲女生' --output-dir ./personlib-turnaround --json
|
|
48
55
|
```
|
|
49
56
|
|
|
50
|
-
|
|
57
|
+
The third route is a paid external generation action. In the owner-approved direct-delivery mode, a combined Persona + Look request authorizes exactly one Makaron submission, so the Agent generates and returns the turnaround without a second confirmation step. The result is `turnaround.png` (or `.jpg`/`.webp`), plus `turnaround-plan.json`, `prompt_used.md`, and `qc_report.md`. The CLI removes only brand identifiers: visible logos, wordmarks, brand names, protected monograms, recognizable trademark symbols, and brand/team crests. Ordinary stripes, numbers, abstract graphics, and shoe construction remain part of the Look. The CLI never automatically retries a paid job. Use `--dry-run` only to inspect the exact prompt and matching IDs without downloading inputs or submitting anything, or `--no-wait` to keep the returned Makaron run ID for later retrieval.
|
|
51
58
|
|
|
52
|
-
For
|
|
59
|
+
For an explicit free Persona + Look source-reference pack, retain `preview`:
|
|
53
60
|
|
|
54
61
|
```bash
|
|
55
62
|
personlib remote preview --brief '20 秒嘻哈说唱女 rapper 舞台视频,红黑街头风、宽松工装、强节奏' --output-dir ./personlib-selection --json
|
|
@@ -118,6 +118,19 @@ async function select(env, { brief, personaId, lookId }) {
|
|
|
118
118
|
return { persona, look, look_score: lookScore };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
async function selectSingle(env, { kind, brief }) {
|
|
122
|
+
if (!['persona', 'look'].includes(kind)) throw new Error("kind must be persona or look");
|
|
123
|
+
const table = kind === "persona" ? "persona_records" : "look_records";
|
|
124
|
+
const candidates = (await records(env, table)).map((record) => ({
|
|
125
|
+
record,
|
|
126
|
+
score: kind === "persona" ? scorePersona(record, brief) : scoreLook(record, brief)
|
|
127
|
+
})).sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
|
|
128
|
+
const selected = candidates[0];
|
|
129
|
+
if (!selected) throw new Error(`no ${kind === "persona" ? "Persona" : "Look"} records available`);
|
|
130
|
+
if (kind === "look" && selected.score < 6) throw new Error("no catalog Look matched this brief");
|
|
131
|
+
return selected.record;
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
async function route(request, env) {
|
|
122
135
|
const url = new URL(request.url);
|
|
123
136
|
if (request.method === "POST" && url.pathname === "/v1/agents/setup") {
|
|
@@ -183,6 +196,19 @@ async function route(request, env) {
|
|
|
183
196
|
return json({ composition: { persona: publicPersona(selected.persona), look: publicLook(selected.look), policies: { use_persona_as_only_identity_reference: true, look_source_face: "excluded", source_logos_and_wordmarks: "replace-with-original-generic-design" }, render_brief: buildRenderBrief(selected.persona, selected.look), render_status: "brief-only-no-generation-submitted" } });
|
|
184
197
|
} catch (cause) { return error(cause.message, 404); }
|
|
185
198
|
}
|
|
199
|
+
if (request.method === "POST" && url.pathname === "/v1/find") {
|
|
200
|
+
const body = await request.json().catch(() => undefined);
|
|
201
|
+
if (!body?.brief) return error("brief is required");
|
|
202
|
+
try {
|
|
203
|
+
const kind = String(body.kind || "");
|
|
204
|
+
const selected = await selectSingle(env, { kind, brief: String(body.brief) });
|
|
205
|
+
return json({
|
|
206
|
+
kind: `${kind}-reference`,
|
|
207
|
+
image_status: "library-reference-not-generated",
|
|
208
|
+
assets: [{ role: kind, asset_id: selected.source_asset_id, url: `/v1/assets/${encodeURIComponent(selected.source_asset_id)}` }]
|
|
209
|
+
});
|
|
210
|
+
} catch (cause) { return error(cause.message, 404); }
|
|
211
|
+
}
|
|
186
212
|
if (request.method === "POST" && url.pathname === "/v1/preview") {
|
|
187
213
|
const body = await request.json().catch(() => undefined);
|
|
188
214
|
try {
|
|
@@ -101,6 +101,14 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
101
101
|
assert.match(recommendation.render_brief, /do not use the Look reference face/i);
|
|
102
102
|
assert.match(recommendation.render_brief, /urban transit/);
|
|
103
103
|
assert.doesNotMatch(recommendation.render_brief, /undefined/);
|
|
104
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "persona", brief: "长相高冷的成年女生" }) }), env);
|
|
105
|
+
const personaFind = await response.json();
|
|
106
|
+
assert.equal(response.status, 200);
|
|
107
|
+
assert.deepEqual(personaFind, { kind: "persona-reference", image_status: "library-reference-not-generated", assets: [{ role: "persona", asset_id: "P-001:source", url: "/v1/assets/P-001%3Asource" }] });
|
|
108
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "look", brief: "地铁皮夹克通勤" }) }), env);
|
|
109
|
+
const lookFind = await response.json();
|
|
110
|
+
assert.equal(response.status, 200);
|
|
111
|
+
assert.deepEqual(lookFind, { kind: "look-reference", image_status: "library-reference-not-generated", assets: [{ role: "look", asset_id: "L-132:source", url: "/v1/assets/L-132%3Asource" }] });
|
|
104
112
|
response = await worker.fetch(request("/v1/compose", { method: "POST", headers, body: JSON.stringify({ persona_id: "P-001", look_id: "L-132" }) }), env);
|
|
105
113
|
assert.equal((await response.json()).composition.policies.look_source_face, "excluded");
|
|
106
114
|
response = await worker.fetch(request("/v1/preview", { method: "POST", headers, body: JSON.stringify({ persona_id: "P-001", look_id: "L-132" }) }), env);
|
package/lib/remote-client.mjs
CHANGED
|
@@ -189,10 +189,11 @@ async function requestedReferencePack(args) {
|
|
|
189
189
|
return { remote, payload };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function downloadReferencePack(remote, payload, destination, args) {
|
|
192
|
+
async function downloadReferencePack(remote, payload, destination, args, { roles, writeSelection = true } = {}) {
|
|
193
193
|
mkdirSync(destination, { recursive: true });
|
|
194
194
|
const downloaded = [];
|
|
195
|
-
|
|
195
|
+
const assets = (payload.assets || []).filter((asset) => !roles || roles.includes(asset.role));
|
|
196
|
+
for (const asset of assets) {
|
|
196
197
|
const response = await fetch(`${remote.endpoint}${asset.url}`, { headers: { authorization: `Bearer ${remote.token}` } });
|
|
197
198
|
if (!response.ok) throw new Error(`remote asset download failed (${response.status}): ${asset.asset_id}`);
|
|
198
199
|
const extension = { "image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp" }[response.headers.get("content-type")] || ".bin";
|
|
@@ -202,10 +203,25 @@ async function downloadReferencePack(remote, payload, destination, args) {
|
|
|
202
203
|
writeFileSync(target, Buffer.from(await response.arrayBuffer()));
|
|
203
204
|
downloaded.push({ role: asset.role, filename: name, path: target });
|
|
204
205
|
}
|
|
205
|
-
writeFileSync(join(destination, "selection.json"), `${JSON.stringify({ ...payload, assets: downloaded.map(({ path, ...asset }) => asset) }, null, 2)}\n`, "utf8");
|
|
206
|
+
if (writeSelection) writeFileSync(join(destination, "selection.json"), `${JSON.stringify({ ...payload, assets: downloaded.map(({ path, ...asset }) => asset) }, null, 2)}\n`, "utf8");
|
|
206
207
|
return downloaded;
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
async function find(args, output) {
|
|
211
|
+
const kind = getOption(args, "--kind");
|
|
212
|
+
if (!['persona', 'look'].includes(kind)) throw new Error("usage: personlib remote find --kind <persona|look> --brief TEXT --output-dir PATH");
|
|
213
|
+
const brief = getOption(args, "--brief");
|
|
214
|
+
if (!brief) throw new Error("personlib remote find requires --brief TEXT");
|
|
215
|
+
const destination = getOption(args, "--output-dir");
|
|
216
|
+
if (!destination) throw new Error("personlib remote find requires --output-dir PATH so it can return an image");
|
|
217
|
+
const remote = agentConnection(args);
|
|
218
|
+
const { payload } = await request(remote.endpoint, "/v1/find", { token: remote.token, method: "POST", json: { kind, brief } });
|
|
219
|
+
const absoluteDestination = resolve(destination);
|
|
220
|
+
const files = await downloadReferencePack(remote, payload, absoluteDestination, args, { roles: [kind], writeSelection: false });
|
|
221
|
+
if (files.length !== 1) throw new Error(`library search expected one ${kind} image, received ${files.length}`);
|
|
222
|
+
jsonOutput({ ok: true, retrieval: { kind: `${kind}-reference`, image_status: "library-reference-not-generated", output_dir: absoluteDestination, files: files.map(({ path }) => path) } }, output, args);
|
|
223
|
+
}
|
|
224
|
+
|
|
209
225
|
async function preview(args, output) {
|
|
210
226
|
const { remote, payload } = await requestedReferencePack(args);
|
|
211
227
|
const destination = getOption(args, "--output-dir");
|
|
@@ -382,7 +398,8 @@ export async function runRemote(args, { output, readManifest }) {
|
|
|
382
398
|
return authenticated(childArgs, output, "/v1/compose", { persona_id: personaId, look_id: lookId });
|
|
383
399
|
}
|
|
384
400
|
if (subcommand === "preview") return preview(args.slice(1), output);
|
|
401
|
+
if (subcommand === "find") return find(args.slice(1), output);
|
|
385
402
|
if (subcommand === "render") return renderTurnaround(args.slice(1), output);
|
|
386
403
|
if (subcommand === "sync") return sync(args.slice(1), output, { readManifest });
|
|
387
|
-
throw new Error("usage: personlib remote <setup|doctor|recommend|compose|preview|render|sync>");
|
|
404
|
+
throw new Error("usage: personlib remote <setup|doctor|recommend|compose|preview|find|render|sync>");
|
|
388
405
|
}
|
package/package.json
CHANGED
|
@@ -11,24 +11,23 @@ Persona is the stable identity reference: face structure, natural skin traits, s
|
|
|
11
11
|
|
|
12
12
|
## Natural-language routing
|
|
13
13
|
|
|
14
|
+
- Search-only rule: requests that ask only for a Persona or only for a Look do not authorize image generation. Return the matched reference image only—never expose IDs, match scores, or internal selection metadata—and do not call Makaron or `remote render`.
|
|
15
|
+
- “我要一个长相高冷的女生” is Persona-only search: use `personlib remote find --kind persona --brief TEXT --output-dir PATH --json`, then return only the Persona reference image. Do not generate a three-view image.
|
|
16
|
+
- “我要一套 Y2K 的妆造” is Look-only search: use `personlib remote find --kind look --brief TEXT --output-dir PATH --json`, then return only the Look reference image. Do not generate a three-view image.
|
|
14
17
|
- “这张图进 Persona 库” is an owner-only intake request. Require clear owner authorization, then use `personlib intake --into persona --rights authorized`.
|
|
15
18
|
- “这张图进 Look 库” is an owner-only intake request. Use `personlib intake --into look --rights authorized`; extract only clothing, hair, makeup, accessory, palette, material, and scene information. Never use the source face as a new Persona.
|
|
16
|
-
- “找一个人物”
|
|
17
|
-
-
|
|
18
|
-
- “按这段文案获取对应图” should use `personlib remote render --brief TEXT --output-dir PATH --json` on 小龙虾. The default engine is Makaron and the requested output is exactly one full-body front/right-side/back turnaround sheet of one adult Persona in one unchanged Look. Before that command, confirm a successful `personlib remote doctor`, local Makaron authentication, and the user's explicit approval to spend generation credits. Use `--dry-run` to show the chosen Persona/Look IDs and exact prompt without submitting.
|
|
19
|
+
- “找一个人物” and “为广告找一个人物” are Persona-only searches: use `personlib remote find --kind persona --brief TEXT --output-dir PATH --json`, then return only the Persona reference image. Do not expose the selected ID or create a temporary Look.
|
|
20
|
+
- A request that combines a person and a style—such as “我要一个嘻哈风格的亚洲女生”—is an image request and should use `personlib remote render --brief TEXT --output-dir PATH --json` on 小龙虾. Explicit wording such as “生成三视图”, “出图”, “生成这个人物”, or “做视频出图” follows the same route. The default engine is Makaron and the output is exactly one full-body front/right-side/back turnaround sheet of one adult Persona in one unchanged Look. In the owner-approved direct-delivery mode, that request authorizes exactly one paid submission: do not pause to display Persona/Look or request a second confirmation. Return the finished turnaround and QC result directly. Use `--dry-run` only when the user asks to preview the selection without generating.
|
|
19
21
|
- “只看看匹配的参考” should use `personlib remote preview --brief TEXT --output-dir PATH --json`. This is free retrieval of the private Persona + Look reference pack, not a synthesized image.
|
|
20
|
-
- “都市通勤 / 职场 /
|
|
21
|
-
-
|
|
22
|
-
- “运动休闲时尚” should use `personlib recommend --brief TEXT`; prefer adult athleisure, retro sport, track, court, and sport-luxe Looks without retaining team, school, or brand identity.
|
|
23
|
-
- “给这个人物换妆造” means keep the Persona and select or create a Look.
|
|
24
|
-
- “用这个人物生成” means compose Persona first, Look second, then use `personlib remote render --persona P-xxx --look L-xxx --output-dir PATH --json` through the caller's Makaron authentication.
|
|
22
|
+
- “都市通勤 / 职场 / 杂志感”, “晚宴 / 红毯 / 高级派对”, and “运动休闲时尚” are Look-only searches: use `personlib remote find --kind look --brief TEXT --output-dir PATH --json`, then return only the Look reference image. Do not generate a person.
|
|
23
|
+
- “给这个人物换妆造” and “用这个人物生成” are combined Persona + Look requests: select the matching internal records, then use `personlib remote render --brief TEXT --output-dir PATH --json`. Return only the finished turnaround to the user; never expose internal IDs.
|
|
25
24
|
|
|
26
25
|
## Access boundary
|
|
27
26
|
|
|
28
|
-
Any enrolled OpenClaw Agent may use credentialed `remote doctor`, `remote recommend`, `remote compose`, `remote preview`, and `remote render`. It may not call `remote sync`, upload source images, modify tags, delete assets, or publish the bundle. `remote render` additionally needs the Agent's own already-authenticated Makaron CLI
|
|
27
|
+
Any enrolled OpenClaw Agent may use credentialed `remote doctor`, `remote find`, `remote recommend`, `remote compose`, `remote preview`, and `remote render`. It may not call `remote sync`, upload source images, modify tags, delete assets, or publish the bundle. `remote render` additionally needs the Agent's own already-authenticated Makaron CLI. In owner-approved direct-delivery mode, a combined Persona + Look request is approval for exactly one paid submission; do not ask a second confirmation. Open enrollment is an owner-selected access rule; never request or print API keys, Agent tokens, owner tokens, or Makaron credentials. `find` returns one library image; `preview` returns source references only; `render` returns the turnaround and preserves its plan, prompt, run ID, and QC record.
|
|
29
28
|
|
|
30
29
|
## Current local-admin milestone
|
|
31
30
|
|
|
32
|
-
The current package supports local Persona and Look intake, list, show, integrity validation, `recommend --brief TEXT`, `fetch --brief TEXT --output-dir PATH`,
|
|
31
|
+
The current package supports local Persona and Look intake, list, show, integrity validation, `recommend --brief TEXT`, `fetch --brief TEXT --output-dir PATH`, `compose --persona P-xxx --look L-xxx`, and no-generation `remote find --kind persona|look --brief TEXT --output-dir PATH`; it also ships the remote Worker/client implementation. The `find` route returns only one requested reference image, with no IDs or selection metadata. The remote commands need a deployed private Worker and a successful top-level `setup` before use; do not claim that the current host is live until `remote doctor` succeeds against its Worker URL. `remote render` obtains the selected private inputs just in time, calls Makaron with the Persona as the sole identity reference, and asks for one neutral-studio front/right-side/back turnaround. It retains the Look's silhouette, palette, materials, energy, ordinary stripes, numbers, abstract graphics, and shoe construction. It removes only brand identifiers: logos, wordmarks, brand names, protected monograms, recognizable trademark symbols, and brand/team crests. It keeps internal inputs in `_internal_references/` for traceability but only the finished turnaround is customer-ready. Reject an output that is not one three-view adult turnaround, mixes identities or Looks, uses the Look source face, or includes a brand identifier. Record `PASS`, `REROLL`, or `BLOCKED`; do not submit a hidden retry.
|
|
33
32
|
|
|
34
33
|
When a source has preppy, uniform-adjacent, or youth-coded styling, store only an adult office-core/editorial interpretation. Do not use a student identity, school context, or sexualized youth styling.
|