makaron-persona-look-cli 0.5.4 → 0.5.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.
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## What an Agent gets
|
|
6
6
|
|
|
7
|
-
`find` is the no-generation delivery route: `--kind persona` returns matched Persona library images, and `--kind look` returns matched Look library images. `--count N` returns exactly N different references or `BLOCKED`;
|
|
7
|
+
`find` is the no-generation delivery route: `--kind persona` returns matched Persona library images, and `--kind look` returns matched Look library images. `--count N` (default `1`) returns exactly N different references or `BLOCKED`; `--exclude ID[,ID...]` omits prior internal selections, including records that share their source image. It does not expose IDs or selection metadata to the end user. `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 creates a locked selection for each requested result, sends those private inputs to the caller's authenticated Makaron CLI, and writes one full-body front/right-side/back turnaround sheet per selection. 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
|
|
|
@@ -47,6 +47,9 @@ Once setup and `remote doctor` succeed, use the route that matches the request:
|
|
|
47
47
|
# Persona-only: return matching library image(s); no Makaron request.
|
|
48
48
|
personlib remote find --kind persona --brief '3个长相高冷的亚洲女生' --count 3 --output-dir ./personlib-personas --json
|
|
49
49
|
|
|
50
|
+
# Follow-up internal search: exclude the previous records without exposing their IDs to an end user.
|
|
51
|
+
personlib remote find --kind persona --brief '长相高冷的亚洲女生' --count 2 --exclude P-001,P-002 --output-dir ./personlib-personas-next --json
|
|
52
|
+
|
|
50
53
|
# Look-only: return matching library image(s); no Makaron request.
|
|
51
54
|
personlib remote find --kind look --brief '3套 Y2K 的妆造' --count 3 --output-dir ./personlib-looks --json
|
|
52
55
|
|
|
@@ -55,7 +58,7 @@ personlib remote render --brief '3个都市风格的亚洲女生' --count 3 --ou
|
|
|
55
58
|
personlib remote render --brief '3个都市风格的亚洲女生' --count 3 --output-dir ./personlib-turnarounds --json
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
The third route is a paid external generation action. `render --brief` first creates Agent-bound, 24-hour selection tokens containing the exact Persona plus catalog Look or a complete temporary Look specification, then resolves those exact tokens; it never reranks or silently swaps a result after selection. A pre-confirmed single selection can also be rendered with `--selection`. In the owner-approved direct-delivery mode, an Agent keeps tokens internal and makes exactly one Makaron submission per selection without a second user confirmation. A batch writes `turnaround-01/`, `turnaround-02/`, and so on; each directory contains one turnaround image (or a pending run ID with `--no-wait`), `turnaround-plan.json`, `prompt_used.md`, and `qc_report.md`. If fewer than `--count` distinct eligible
|
|
61
|
+
The third route is a paid external generation action. `render --brief` first creates Agent-bound, 24-hour selection tokens containing the exact Persona plus catalog Look or a complete temporary Look specification, then resolves those exact tokens; it never reranks or silently swaps a result after selection. A pre-confirmed single selection can also be rendered with `--selection`. In the owner-approved direct-delivery mode, an Agent keeps tokens internal and makes exactly one Makaron submission per selection without a second user confirmation. A batch writes `turnaround-01/`, `turnaround-02/`, and so on; each directory contains one turnaround image (or a pending run ID with `--no-wait`), `turnaround-plan.json`, `prompt_used.md`, and `qc_report.md`. If a `find` request has fewer than `--count` distinct eligible records after exclusions, it returns `BLOCKED`, `insufficient_matches: true`, and the available count without downloading partial or duplicate images. 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` to inspect every locked request and complete prompt without downloading inputs or submitting anything, or `--no-wait` to keep returned Makaron run IDs for later retrieval.
|
|
59
62
|
|
|
60
63
|
`亚洲女生` is a hard retrieval and render condition, not a visual guess. The Worker requires both an owner-reviewed `east_asian` appearance tag and the Persona metadata `presentation: "feminine adult"`; no matching record returns `BLOCKED` before Makaron is called. `韩国女生` additionally requires `korean_style_compatible`. Appearance tags are never inferred by the running model. The current library needs owner review before any existing Persona is made eligible for these requests.
|
|
61
64
|
|
|
@@ -74,6 +77,7 @@ node bin/personlib.mjs list --library ./private-library --json
|
|
|
74
77
|
node bin/personlib.mjs show --persona P-001 --library ./private-library --json
|
|
75
78
|
node bin/personlib.mjs review-persona --persona P-001 --appearance-tags east_asian,korean_style_compatible --reviewed-by owner-name --owner-confirmed --library ./private-library --json
|
|
76
79
|
node bin/personlib.mjs show --look L-001 --library ./private-library --json
|
|
80
|
+
node bin/personlib.mjs review-look --look L-001 --family 'adult hip-hop streetwear' --silhouette 'relaxed low-rise layered proportion' --garments 'unbranded color-block jacket, cropped top, baggy denim and retro sneakers' --palette 'red, blue, ivory and indigo' --materials 'nylon, cotton jersey and washed denim' --accessories 'minimal metal chain' --scene 'neutral adult music-fashion studio' --reviewed-by owner-name --owner-confirmed --library ./private-library --json
|
|
77
81
|
node bin/personlib.mjs recommend --brief '请使用人物资产库,为一个 15 秒高端美妆广告找一位 25 岁左右、东亚、干净冷感的女性人物。' --library ./private-library --json
|
|
78
82
|
node bin/personlib.mjs fetch --brief '请为一个 15 秒成年女性地铁皮夹克通勤广告找一套都市造型。' --output-dir ./selection --library ./private-library --json
|
|
79
83
|
node bin/personlib.mjs compose --persona P-003 --look L-011 --library ./private-library --json
|
|
@@ -94,7 +98,7 @@ node bin/personlib.mjs intake \
|
|
|
94
98
|
|
|
95
99
|
`intake` records neither facial nor styling assertions automatically. A human/vision-review step adds Persona or Look fields. A Look always carries a source-face exclusion and brand-sanitization rule.
|
|
96
100
|
|
|
97
|
-
`review-persona` is an owner-only manual-audit command. It writes the reviewed tags plus reviewer/time provenance; do not run it from an image model, an automatic classifier, or an Agent guess.
|
|
101
|
+
`review-persona` is an owner-only manual-audit command. It writes the reviewed tags plus reviewer/time provenance; do not run it from an image model, an automatic classifier, or an Agent guess. `review-look` is the equivalent owner-only gate for a new Look: all seven structured styling fields, source-face exclusion, source-brand replacement, and reviewer/time provenance are required before it can become `private-staged`. Follow either review with owner `remote sync` so the Worker receives only approved metadata and private source bytes.
|
|
98
102
|
|
|
99
103
|
## Test
|
|
100
104
|
|
package/bin/personlib.mjs
CHANGED
|
@@ -132,6 +132,11 @@ function validate(manifest, root) {
|
|
|
132
132
|
} else if (lookRecord.look?.review_state !== "needs-structured-review") {
|
|
133
133
|
errors.push(`${lookRecord.id}: intake-review records must declare look.review_state`);
|
|
134
134
|
}
|
|
135
|
+
if (lookRecord.look_review !== undefined) {
|
|
136
|
+
if (lookRecord.status !== "private-staged" || lookRecord.look_review?.state !== "owner-reviewed" || !lookRecord.look_review?.reviewed_by || !lookRecord.look_review?.reviewed_at) {
|
|
137
|
+
errors.push(`${lookRecord.id}: look_review requires private-staged owner-reviewed provenance`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
135
140
|
}
|
|
136
141
|
return errors;
|
|
137
142
|
}
|
|
@@ -264,6 +269,36 @@ function reviewPersonaAppearance() {
|
|
|
264
269
|
output({ ok: true, persona: { id: persona.id, appearance_tags: tags, appearance_tags_review: persona.appearance_tags_review } });
|
|
265
270
|
}
|
|
266
271
|
|
|
272
|
+
function reviewLook() {
|
|
273
|
+
const root = rootPath();
|
|
274
|
+
const lookId = option("--look");
|
|
275
|
+
const reviewedBy = option("--reviewed-by");
|
|
276
|
+
const fields = ["family", "silhouette", "garments", "palette", "materials", "accessories", "scene"];
|
|
277
|
+
const look = Object.fromEntries(fields.map((field) => [field, option(`--${field}`)?.trim()]));
|
|
278
|
+
if (!lookId || !reviewedBy || !has("--owner-confirmed") || fields.some((field) => !look[field])) {
|
|
279
|
+
throw new Error("usage: personlib review-look --look L-001 --family TEXT --silhouette TEXT --garments TEXT --palette TEXT --materials TEXT --accessories TEXT --scene TEXT --reviewed-by NAME --owner-confirmed --library PATH");
|
|
280
|
+
}
|
|
281
|
+
const manifest = readManifest(root);
|
|
282
|
+
const record = (manifest.looks || []).find((entry) => entry.id === lookId);
|
|
283
|
+
if (!record) throw new Error(`look not found: ${lookId}`);
|
|
284
|
+
if (!["intake-review", "private-staged"].includes(record.status)) throw new Error(`look is not ready for owner review: ${lookId}`);
|
|
285
|
+
record.status = "private-staged";
|
|
286
|
+
record.look = look;
|
|
287
|
+
record.identity_policy = "do-not-use-source-face";
|
|
288
|
+
record.brand_policy = "replace-source-logos-and-wordmarks";
|
|
289
|
+
record.look_review = {
|
|
290
|
+
state: "owner-reviewed",
|
|
291
|
+
reviewed_by: reviewedBy,
|
|
292
|
+
reviewed_at: new Date().toISOString(),
|
|
293
|
+
method: "manual-owner-review-source-face-excluded"
|
|
294
|
+
};
|
|
295
|
+
record.cloud_sync = "not-started";
|
|
296
|
+
const errors = validate(manifest, root);
|
|
297
|
+
if (errors.length) throw new Error(`library validation failed: ${errors.join("; ")}`);
|
|
298
|
+
saveManifest(root, manifest);
|
|
299
|
+
output({ ok: true, look: { id: record.id, status: record.status, look: record.look, brand_policy: record.brand_policy, look_review: record.look_review } });
|
|
300
|
+
}
|
|
301
|
+
|
|
267
302
|
function buildRenderBrief(persona, look) {
|
|
268
303
|
const identity = persona.identity;
|
|
269
304
|
const identityLock = [
|
|
@@ -542,6 +577,7 @@ try {
|
|
|
542
577
|
else if (command === "remote") await runRemote(argv.slice(1), { output, readManifest });
|
|
543
578
|
else if (command === "intake") intake();
|
|
544
579
|
else if (command === "review-persona") reviewPersonaAppearance();
|
|
580
|
+
else if (command === "review-look") reviewLook();
|
|
545
581
|
else if (command === "recommend") recommend();
|
|
546
582
|
else if (command === "fetch") fetchReferencePack();
|
|
547
583
|
else if (command === "compose") compose();
|
|
@@ -569,7 +605,7 @@ try {
|
|
|
569
605
|
output({ ok: errors.length === 0, errors });
|
|
570
606
|
if (errors.length) process.exitCode = 1;
|
|
571
607
|
} else {
|
|
572
|
-
output("Usage: personlib <setup|intake|review-persona|list|show|recommend|fetch|compose|validate|remote> [--library PATH] [--json]");
|
|
608
|
+
output("Usage: personlib <setup|intake|review-persona|review-look|list|show|recommend|fetch|compose|validate|remote> [--library PATH] [--json]");
|
|
573
609
|
process.exitCode = command ? 1 : 0;
|
|
574
610
|
}
|
|
575
611
|
} catch (error) {
|
|
@@ -18,6 +18,14 @@ function requestedCount(value) {
|
|
|
18
18
|
return count;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function requestedExcludedIds(value) {
|
|
22
|
+
if (value === undefined) return [];
|
|
23
|
+
if (!Array.isArray(value)) throw new Error("exclude_ids must be an array of record IDs");
|
|
24
|
+
const ids = [...new Set(value.map((id) => String(id || "").trim()).filter(Boolean))];
|
|
25
|
+
if (!ids.length) throw new Error("exclude_ids must contain at least one record ID when supplied");
|
|
26
|
+
return ids;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
async function sha256(value) {
|
|
22
30
|
const input = typeof value === "string" ? encoder.encode(value) : value;
|
|
23
31
|
const hash = await crypto.subtle.digest("SHA-256", input);
|
|
@@ -162,6 +170,13 @@ function distinctSourceCandidates(candidates) {
|
|
|
162
170
|
});
|
|
163
171
|
}
|
|
164
172
|
|
|
173
|
+
function excludeRecordCandidates(candidates, excludeIds) {
|
|
174
|
+
if (!excludeIds.length) return candidates;
|
|
175
|
+
const excluded = new Set(excludeIds);
|
|
176
|
+
const excludedSources = new Set(candidates.filter(({ record }) => excluded.has(record.id)).map(({ record }) => record.source_asset_id).filter(Boolean));
|
|
177
|
+
return candidates.filter(({ record }) => !excluded.has(record.id) && !excludedSources.has(record.source_asset_id));
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
function buildRenderBrief(persona, look) {
|
|
166
181
|
const identity = persona.identity || {};
|
|
167
182
|
const direction = look.look || look;
|
|
@@ -223,17 +238,20 @@ async function selectSingle(env, { kind, brief }) {
|
|
|
223
238
|
return selected.records[0];
|
|
224
239
|
}
|
|
225
240
|
|
|
226
|
-
async function selectSingles(env, { kind, brief, count = 1 }) {
|
|
241
|
+
async function selectSingles(env, { kind, brief, count = 1, excludeIds = [] }) {
|
|
227
242
|
if (!['persona', 'look'].includes(kind)) throw new Error("kind must be persona or look");
|
|
228
243
|
const requested = requestedCount(count);
|
|
229
244
|
if (kind === "persona") {
|
|
230
|
-
const selected = await selectPersonas(env, { brief, count: requested });
|
|
245
|
+
const selected = await selectPersonas(env, { brief, count: requested, excludeIds });
|
|
231
246
|
return { records: selected.personas, available_count: selected.available_count };
|
|
232
247
|
}
|
|
233
|
-
const
|
|
248
|
+
const scored = (await records(env, "look_records"))
|
|
234
249
|
.map((record) => ({ record, score: scoreLook(record, brief) }))
|
|
235
250
|
.filter(({ score }) => score >= 6)
|
|
236
|
-
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id))
|
|
251
|
+
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
|
|
252
|
+
const unexcludedCandidates = distinctSourceCandidates(scored);
|
|
253
|
+
const candidates = distinctSourceCandidates(excludeRecordCandidates(scored, excludeIds));
|
|
254
|
+
if (excludeIds.length && unexcludedCandidates.length && candidates.length < requested) throw insufficientCandidatesBlocked("Look", requested, candidates.length);
|
|
237
255
|
if (candidates.length < requested) throw insufficientCandidatesBlocked("Look", requested, candidates.length);
|
|
238
256
|
return { records: candidates.slice(0, requested).map(({ record }) => record), available_count: candidates.length };
|
|
239
257
|
}
|
|
@@ -247,7 +265,7 @@ async function selectPersona(env, { brief, personaId }) {
|
|
|
247
265
|
};
|
|
248
266
|
}
|
|
249
267
|
|
|
250
|
-
async function selectPersonas(env, { brief, personaId, count = 1 }) {
|
|
268
|
+
async function selectPersonas(env, { brief, personaId, count = 1, excludeIds = [] }) {
|
|
251
269
|
const requested = requestedCount(count);
|
|
252
270
|
const personas = await records(env, "persona_records");
|
|
253
271
|
const requiredAppearanceTags = requestedAppearanceTags(brief || "");
|
|
@@ -256,10 +274,18 @@ async function selectPersonas(env, { brief, personaId, count = 1 }) {
|
|
|
256
274
|
if (!available.length) throw new Error(personaId ? `persona not found: ${personaId}` : "no Persona records available");
|
|
257
275
|
const presentationCandidates = available.filter((record) => presentationMatches(record, requiredPresentation));
|
|
258
276
|
if (!presentationCandidates.length && requiredPresentation) throw presentationBlocked(requiredPresentation, { requestedCount: requested, availableCount: 0 });
|
|
259
|
-
const
|
|
277
|
+
const scored = presentationCandidates
|
|
260
278
|
.filter((record) => appearanceMatches(record, requiredAppearanceTags))
|
|
261
279
|
.map((record) => ({ record, score: scorePersona(record, brief || "") }))
|
|
262
|
-
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id))
|
|
280
|
+
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
|
|
281
|
+
const unexcludedCandidates = distinctSourceCandidates(scored);
|
|
282
|
+
const candidates = distinctSourceCandidates(excludeRecordCandidates(scored, excludeIds));
|
|
283
|
+
if (excludeIds.length && unexcludedCandidates.length && candidates.length < requested) {
|
|
284
|
+
throw insufficientCandidatesBlocked("Persona", requested, candidates.length, {
|
|
285
|
+
required_appearance_tags: requiredAppearanceTags,
|
|
286
|
+
required_presentation: requiredPresentation
|
|
287
|
+
});
|
|
288
|
+
}
|
|
263
289
|
if (!candidates.length && requiredAppearanceTags.length) throw appearanceBlocked(requiredAppearanceTags, requiredPresentation, { requestedCount: requested, availableCount: 0 });
|
|
264
290
|
if (!candidates.length && requiredPresentation) throw presentationBlocked(requiredPresentation, { requestedCount: requested, availableCount: 0 });
|
|
265
291
|
if (!candidates.length) throw new Error(personaId ? `persona not found: ${personaId}` : "no Persona records available");
|
|
@@ -403,6 +429,7 @@ function blocked(cause, status = 409) {
|
|
|
403
429
|
kind: cause?.kind || null,
|
|
404
430
|
requested_count: cause?.requested_count || null,
|
|
405
431
|
available_count: cause?.available_count ?? null,
|
|
432
|
+
insufficient_matches: cause?.code === "insufficient-candidates",
|
|
406
433
|
error: cause?.message || "request blocked"
|
|
407
434
|
}, status);
|
|
408
435
|
}
|
|
@@ -533,12 +560,14 @@ async function route(request, env) {
|
|
|
533
560
|
try {
|
|
534
561
|
const kind = String(body.kind || "");
|
|
535
562
|
const count = requestedCount(body?.count);
|
|
536
|
-
const
|
|
563
|
+
const excludeIds = requestedExcludedIds(body?.exclude_ids);
|
|
564
|
+
const selected = await selectSingles(env, { kind, brief: String(body.brief), count, excludeIds });
|
|
537
565
|
return json({
|
|
538
566
|
kind: `${kind}-reference`,
|
|
539
567
|
image_status: "library-reference-not-generated",
|
|
540
568
|
requested_count: count,
|
|
541
569
|
available_count: selected.available_count,
|
|
570
|
+
returned_count: selected.records.length,
|
|
542
571
|
assets: selected.records.map((record, index) => ({
|
|
543
572
|
role: kind,
|
|
544
573
|
...(count > 1 ? { index: index + 1 } : {}),
|
|
@@ -85,7 +85,8 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
85
85
|
{ id: "P-000", display_name: "Adult Man", status: "private-staged", identity: { presentation: "masculine adult", face_shape: "square", brows: "straight", eyes: "round", nose: "straight", mouth: "thin", skin: "natural", stable_marks: "none" }, appearance_tags: ["east_asian"], appearance_tags_review: { state: "owner-reviewed", reviewed_by: "test-owner", reviewed_at: "2026-09-04T00:00:00.000Z" }, source_asset_id: "P-000:source", identity_references: [] },
|
|
86
86
|
{ id: "P-001", display_name: "Adult One", status: "private-staged", identity: { presentation: "feminine adult", face_shape: "oval", brows: "straight", eyes: "almond", nose: "straight", mouth: "soft", skin: "natural", stable_marks: "none" }, appearance_tags: ["east_asian"], appearance_tags_review: { state: "owner-reviewed", reviewed_by: "test-owner", reviewed_at: "2026-09-04T00:00:00.000Z" }, source_asset_id: "P-001:source", identity_references: [] },
|
|
87
87
|
{ id: "P-002", display_name: "Adult Two", status: "private-staged", identity: { presentation: "feminine adult", face_shape: "long oval", brows: "arched", eyes: "hooded", nose: "compact", mouth: "full", skin: "neutral", stable_marks: "freckle" }, appearance_tags: ["east_asian"], appearance_tags_review: { state: "owner-reviewed", reviewed_by: "test-owner", reviewed_at: "2026-09-04T00:00:00.000Z" }, source_asset_id: "P-002:source", identity_references: [] },
|
|
88
|
-
{ id: "P-003", display_name: "Adult Three", status: "private-staged", identity: { presentation: "feminine adult", face_shape: "heart", brows: "soft", eyes: "narrow", nose: "straight", mouth: "centered", skin: "even", stable_marks: "beauty mark" }, appearance_tags: ["east_asian"], appearance_tags_review: { state: "owner-reviewed", reviewed_by: "test-owner", reviewed_at: "2026-09-04T00:00:00.000Z" }, source_asset_id: "P-003:source", identity_references: [] }
|
|
88
|
+
{ id: "P-003", display_name: "Adult Three", status: "private-staged", identity: { presentation: "feminine adult", face_shape: "heart", brows: "soft", eyes: "narrow", nose: "straight", mouth: "centered", skin: "even", stable_marks: "beauty mark" }, appearance_tags: ["east_asian"], appearance_tags_review: { state: "owner-reviewed", reviewed_by: "test-owner", reviewed_at: "2026-09-04T00:00:00.000Z" }, source_asset_id: "P-003:source", identity_references: [] },
|
|
89
|
+
{ id: "P-004", display_name: "Adult One Duplicate Record", status: "private-staged", identity: { presentation: "feminine adult", face_shape: "oval", brows: "straight", eyes: "almond", nose: "straight", mouth: "soft", skin: "natural", stable_marks: "none" }, appearance_tags: ["east_asian"], appearance_tags_review: { state: "owner-reviewed", reviewed_by: "test-owner", reviewed_at: "2026-09-04T00:00:00.000Z" }, source_asset_id: "P-001:source", identity_references: [] }
|
|
89
90
|
],
|
|
90
91
|
looks: [
|
|
91
92
|
{ id: "L-115", display_name: "Hip-Hop Rap", status: "private-staged", look: { family: "hip-hop rap y2k", silhouette: "baggy", garments: "unbranded jersey and cargo trousers", palette: "red black", materials: "denim", accessories: "chain", scene: "neutral studio" }, source_asset_id: "L-115:source" },
|
|
@@ -180,6 +181,21 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
180
181
|
assert.equal(personaBatch.available_count, 3);
|
|
181
182
|
assert.deepEqual(personaBatch.assets.map((asset) => asset.index), [1, 2, 3]);
|
|
182
183
|
assert.deepEqual(new Set(personaBatch.assets.map((asset) => asset.asset_id)), new Set(["P-001:source", "P-002:source", "P-003:source"]));
|
|
184
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "persona", brief: "长相高冷的亚洲女生", count: 2, exclude_ids: ["P-001"] }) }), env);
|
|
185
|
+
const excludedPersonaBatch = await response.json();
|
|
186
|
+
assert.equal(response.status, 200);
|
|
187
|
+
assert.equal(excludedPersonaBatch.requested_count, 2);
|
|
188
|
+
assert.equal(excludedPersonaBatch.available_count, 2);
|
|
189
|
+
assert.equal(excludedPersonaBatch.returned_count, 2);
|
|
190
|
+
assert.deepEqual(excludedPersonaBatch.assets.map((asset) => asset.asset_id), ["P-002:source", "P-003:source"]);
|
|
191
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "persona", brief: "长相高冷的亚洲女生", count: 3, exclude_ids: ["P-001"] }) }), env);
|
|
192
|
+
const insufficientPersonaBatch = await response.json();
|
|
193
|
+
assert.equal(response.status, 409);
|
|
194
|
+
assert.equal(insufficientPersonaBatch.state, "BLOCKED");
|
|
195
|
+
assert.equal(insufficientPersonaBatch.insufficient_matches, true);
|
|
196
|
+
assert.equal(insufficientPersonaBatch.requested_count, 3);
|
|
197
|
+
assert.equal(insufficientPersonaBatch.available_count, 2);
|
|
198
|
+
assert.equal(Object.hasOwn(insufficientPersonaBatch, "assets"), false);
|
|
183
199
|
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "look", brief: "3套 Y2K 的妆造", count: 3 }) }), env);
|
|
184
200
|
const lookBatch = await response.json();
|
|
185
201
|
assert.equal(response.status, 200);
|
|
@@ -187,6 +203,10 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
187
203
|
assert.equal(lookBatch.available_count, 3);
|
|
188
204
|
assert.deepEqual(lookBatch.assets.map((asset) => asset.index), [1, 2, 3]);
|
|
189
205
|
assert.deepEqual(new Set(lookBatch.assets.map((asset) => asset.asset_id)), new Set(["L-115:source", "L-116:source", "L-117:source"]));
|
|
206
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "look", brief: "3套 Y2K 的妆造", count: 2, exclude_ids: ["L-115"] }) }), env);
|
|
207
|
+
const excludedLookBatch = await response.json();
|
|
208
|
+
assert.equal(response.status, 200);
|
|
209
|
+
assert.deepEqual(excludedLookBatch.assets.map((asset) => asset.asset_id), ["L-116:source", "L-117:source"]);
|
|
190
210
|
response = await worker.fetch(request("/v1/recommend", { method: "POST", headers, body: JSON.stringify({ brief: "3个都市风格的亚洲女生", count: 3 }) }), env);
|
|
191
211
|
const urbanBatch = await response.json();
|
|
192
212
|
assert.equal(response.status, 200);
|
package/lib/remote-client.mjs
CHANGED
|
@@ -28,6 +28,14 @@ function countOption(args) {
|
|
|
28
28
|
return count;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function excludeIdsOption(args) {
|
|
32
|
+
const value = getOption(args, "--exclude");
|
|
33
|
+
if (value === undefined) return [];
|
|
34
|
+
const ids = [...new Set(value.split(",").map((id) => id.trim()).filter(Boolean))];
|
|
35
|
+
if (!ids.length) throw new Error("--exclude must be a comma-separated list of record IDs");
|
|
36
|
+
return ids;
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
function jsonOption(args, name) {
|
|
32
40
|
const value = getOption(args, name);
|
|
33
41
|
if (!value) return undefined;
|
|
@@ -252,14 +260,15 @@ async function downloadReferencePack(remote, payload, destination, args, { roles
|
|
|
252
260
|
|
|
253
261
|
async function find(args, output) {
|
|
254
262
|
const kind = getOption(args, "--kind");
|
|
255
|
-
if (!['persona', 'look'].includes(kind)) throw new Error("usage: personlib remote find --kind <persona|look> --brief TEXT --output-dir PATH");
|
|
263
|
+
if (!['persona', 'look'].includes(kind)) throw new Error("usage: personlib remote find --kind <persona|look> --brief TEXT [--count N] [--exclude ID[,ID...]] --output-dir PATH");
|
|
256
264
|
const brief = getOption(args, "--brief");
|
|
257
265
|
if (!brief) throw new Error("personlib remote find requires --brief TEXT");
|
|
258
266
|
const destination = getOption(args, "--output-dir");
|
|
259
267
|
if (!destination) throw new Error("personlib remote find requires --output-dir PATH so it can return an image");
|
|
260
268
|
const count = countOption(args);
|
|
269
|
+
const excludeIds = excludeIdsOption(args);
|
|
261
270
|
const remote = agentConnection(args);
|
|
262
|
-
const { response, payload } = await request(remote.endpoint, "/v1/find", { token: remote.token, method: "POST", json: { kind, brief, count }, allowError: true });
|
|
271
|
+
const { response, payload } = await request(remote.endpoint, "/v1/find", { token: remote.token, method: "POST", json: { kind, brief, count, ...(excludeIds.length ? { exclude_ids: excludeIds } : {}) }, allowError: true });
|
|
263
272
|
if (!response.ok) {
|
|
264
273
|
if (payload?.state === "BLOCKED") return jsonOutput(payload, output, args);
|
|
265
274
|
throw new Error(`remote request failed (${response.status}): ${payload?.error || "find failed"}`);
|
|
@@ -267,7 +276,7 @@ async function find(args, output) {
|
|
|
267
276
|
const absoluteDestination = resolve(destination);
|
|
268
277
|
const files = await downloadReferencePack(remote, payload, absoluteDestination, args, { roles: [kind], writeSelection: false });
|
|
269
278
|
if (files.length !== count) throw new Error(`library search expected ${count} ${kind} images, received ${files.length}`);
|
|
270
|
-
jsonOutput({ ok: true, retrieval: { kind: `${kind}-reference`, image_status: "library-reference-not-generated", requested_count: count, available_count: payload.available_count, output_dir: absoluteDestination, files: files.map(({ path }) => path) } }, output, args);
|
|
279
|
+
jsonOutput({ ok: true, retrieval: { kind: `${kind}-reference`, image_status: "library-reference-not-generated", requested_count: count, available_count: payload.available_count, returned_count: files.length, output_dir: absoluteDestination, files: files.map(({ path }) => path) } }, output, args);
|
|
271
280
|
}
|
|
272
281
|
|
|
273
282
|
async function preview(args, output) {
|
package/package.json
CHANGED
|
@@ -12,10 +12,10 @@ Persona is the stable identity reference: face structure, natural skin traits, s
|
|
|
12
12
|
## Natural-language routing
|
|
13
13
|
|
|
14
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. If the request says `3个` (or another quantity), pass `--count N`; return exactly N different references or report `BLOCKED`, never fewer.
|
|
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. If the request says `3套` (or another quantity), pass `--count N`; return exactly N different references or report `BLOCKED`, never fewer.
|
|
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. If the request says `3个` (or another quantity), pass `--count N`; return exactly N different references or report `BLOCKED`, never fewer. For a follow-up search, the Agent may pass internal prior record IDs as `--exclude ID[,ID...]`; never expose those IDs to the end user.
|
|
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. If the request says `3套` (or another quantity), pass `--count N`; return exactly N different references or report `BLOCKED`, never fewer. For a follow-up search, the Agent may pass internal prior record IDs as `--exclude ID[,ID...]`; never expose those IDs to the end user.
|
|
17
17
|
- “这张图进 Persona 库” is an owner-only intake request. Require clear owner authorization, then use `personlib intake --into persona --rights authorized`.
|
|
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.
|
|
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. Before an intake Look can be searched remotely, the owner must use `personlib review-look` with all structured Look fields and `--owner-confirmed`; it preserves source-face exclusion and removes only source brand identifiers in future output.
|
|
19
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
20
|
- A request that combines a person and a style—such as “我要一个嘻哈风格的亚洲女生”—is an image request. Run `personlib remote render --brief TEXT --output-dir PATH --json`; it creates and resolves a locked internal selection before submitting, so it never reranks after selection. Never expose the internal selection ID to the user. For `3个都市风格的亚洲女生`, pass `--count 3`: dry-run must return three independent plans, and a real run submits exactly three Makaron jobs—one front/right-side/back sheet per distinct Persona. If the library has fewer than the requested number, return `BLOCKED`; do not silently deliver fewer images or submit any job. A pre-confirmed single selection may use `remote render --selection`. Explicit wording such as “生成三视图”, “出图”, “生成这个人物”, or “做视频出图” follows the same route. The default engine is Makaron and each 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 per selected result: 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 locked selection(s) without generating.
|
|
21
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.
|
|
@@ -30,10 +30,10 @@ Persona is the stable identity reference: face structure, natural skin traits, s
|
|
|
30
30
|
|
|
31
31
|
## Access boundary
|
|
32
32
|
|
|
33
|
-
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, manually apply appearance 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 per locked selection; 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 --count N` returns N library images without IDs; `preview` returns source references only; `render --count N` returns N turnarounds plus one plan, prompt, run ID, and QC record per result.
|
|
33
|
+
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, manually apply appearance 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 per locked selection; 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 --count N` returns N library images without IDs; its internal-only `--exclude ID[,ID...]` prevents a previously selected record or its matching source image from returning again. On shortfall it returns `BLOCKED` with `insufficient_matches: true` and a count, never a duplicate. `preview` returns source references only; `render --count N` returns N turnarounds plus one plan, prompt, run ID, and QC record per result.
|
|
34
34
|
|
|
35
35
|
## Current local-admin milestone
|
|
36
36
|
|
|
37
|
-
The current package supports local Persona and Look intake, owner-reviewed appearance tags, 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 --count N --output-dir PATH`; it also ships the remote Worker/client implementation. The `find` route returns exactly the requested number of distinct reference images, with no IDs or selection metadata, or `BLOCKED` with the available count. 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 --brief TEXT --count N` creates and resolves N locked selections just in time, calls Makaron exactly N times after a passing dry-run, and asks for one neutral-studio front/right-side/back turnaround per selection. 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, fails an owner-reviewed requested appearance tag or presentation, or includes a brand identifier. Record `PASS`, `REROLL`, or `BLOCKED`; do not submit a hidden retry.
|
|
37
|
+
The current package supports local Persona and Look intake, owner-reviewed Persona appearance tags, owner-reviewed structured Looks (`review-look`), 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 --count N --output-dir PATH`; it also ships the remote Worker/client implementation. The `find` route returns exactly the requested number of distinct reference images, with no IDs or selection metadata, or `BLOCKED` with the available count. 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 --brief TEXT --count N` creates and resolves N locked selections just in time, calls Makaron exactly N times after a passing dry-run, and asks for one neutral-studio front/right-side/back turnaround per selection. 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, fails an owner-reviewed requested appearance tag or presentation, or includes a brand identifier. Record `PASS`, `REROLL`, or `BLOCKED`; do not submit a hidden retry.
|
|
38
38
|
|
|
39
39
|
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.
|