makaron-persona-look-cli 0.5.4 → 0.5.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/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
|
|
|
@@ -37,7 +37,7 @@ personlib remote sync --library ./private-library --dry-run --json
|
|
|
37
37
|
personlib remote sync --library ./private-library --json
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
`remote sync` remains owner-only. Open enrollment authorizes Agents to receive their own credentials, but does not expose an anonymous catalog or direct R2 object URLs.
|
|
40
|
+
`remote sync` remains owner-only. It uploads private asset bytes in controlled batches after its manifest write, so a partial network failure can be safely rerun without exposing sources. Open enrollment authorizes Agents to receive their own credentials, but does not expose an anonymous catalog or direct R2 object URLs.
|
|
41
41
|
|
|
42
42
|
## Agent natural-language route
|
|
43
43
|
|
|
@@ -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,12 +58,14 @@ 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
|
|
|
62
65
|
`都市`, `urban`, `city`, `city chic`, and `downtown` all retrieve the urban/commute/editorial Look family; users do not need to rewrite `都市风格` as `都市通勤`.
|
|
63
66
|
|
|
67
|
+
`性感`, `sexy`, `sensual`, `夜生活`, `夜店`, `nightlife`, and `约会夜` retrieve owner-reviewed adult evening Looks only; they never use a Look source face as the rendered identity.
|
|
68
|
+
|
|
64
69
|
For an explicit free Persona + Look source-reference pack, retain `preview`:
|
|
65
70
|
|
|
66
71
|
```bash
|
|
@@ -74,6 +79,7 @@ node bin/personlib.mjs list --library ./private-library --json
|
|
|
74
79
|
node bin/personlib.mjs show --persona P-001 --library ./private-library --json
|
|
75
80
|
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
81
|
node bin/personlib.mjs show --look L-001 --library ./private-library --json
|
|
82
|
+
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
83
|
node bin/personlib.mjs recommend --brief '请使用人物资产库,为一个 15 秒高端美妆广告找一位 25 岁左右、东亚、干净冷感的女性人物。' --library ./private-library --json
|
|
78
84
|
node bin/personlib.mjs fetch --brief '请为一个 15 秒成年女性地铁皮夹克通勤广告找一套都市造型。' --output-dir ./selection --library ./private-library --json
|
|
79
85
|
node bin/personlib.mjs compose --persona P-003 --look L-011 --library ./private-library --json
|
|
@@ -94,7 +100,7 @@ node bin/personlib.mjs intake \
|
|
|
94
100
|
|
|
95
101
|
`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
102
|
|
|
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.
|
|
103
|
+
`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
104
|
|
|
99
105
|
## Test
|
|
100
106
|
|
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 = [
|
|
@@ -332,6 +367,7 @@ function scoreLookForBrief(lookRecord, brief) {
|
|
|
332
367
|
addSignals(/晚宴|gala|black-tie|formal/, [["gala", 4], ["evening", 3], ["formal", 3], ["gown", 2], ["soiree", 2]]);
|
|
333
368
|
addSignals(/红毯|red carpet/, [["red carpet", 6], ["gala", 3], ["carpet", 2], ["train", 2], ["gown", 2]]);
|
|
334
369
|
addSignals(/高级派对|party|cocktail|after-party/, [["party", 4], ["cocktail", 3], ["soiree", 3], ["crystal", 1], ["sequin", 1], ["gown", 1]]);
|
|
370
|
+
addSignals(/性感|sexy|sensual|夜生活|夜店|nightlife|club|约会夜|date night/, [["sensual", 6], ["nightlife", 5], ["club", 4], ["bodycon", 4], ["cutout", 3], ["halter", 3], ["ruched", 2], ["sheer", 2], ["satin", 2], ["fitted", 1]]);
|
|
335
371
|
addSignals(/运动休闲|athleisure|运动|sport|sportswear|健身|球类|篮球|网球|跑步|track|训练/, [["athleisure", 5], ["sport", 4], ["track", 3], ["running", 2], ["tennis", 2], ["basketball", 2], ["technical", 1], ["fleece", 1], ["hoodie", 1]]);
|
|
336
372
|
addSignals(/度假|海岛|resort|beach|holiday|pool|yacht|sailing|夏日/, [["resort", 5], ["beach", 5], ["island", 4], ["pool", 4], ["sailing", 4], ["summer", 3], ["linen", 2], ["swim", 2], ["nautical", 2]]);
|
|
337
373
|
return score;
|
|
@@ -542,6 +578,7 @@ try {
|
|
|
542
578
|
else if (command === "remote") await runRemote(argv.slice(1), { output, readManifest });
|
|
543
579
|
else if (command === "intake") intake();
|
|
544
580
|
else if (command === "review-persona") reviewPersonaAppearance();
|
|
581
|
+
else if (command === "review-look") reviewLook();
|
|
545
582
|
else if (command === "recommend") recommend();
|
|
546
583
|
else if (command === "fetch") fetchReferencePack();
|
|
547
584
|
else if (command === "compose") compose();
|
|
@@ -569,7 +606,7 @@ try {
|
|
|
569
606
|
output({ ok: errors.length === 0, errors });
|
|
570
607
|
if (errors.length) process.exitCode = 1;
|
|
571
608
|
} else {
|
|
572
|
-
output("Usage: personlib <setup|intake|review-persona|list|show|recommend|fetch|compose|validate|remote> [--library PATH] [--json]");
|
|
609
|
+
output("Usage: personlib <setup|intake|review-persona|review-look|list|show|recommend|fetch|compose|validate|remote> [--library PATH] [--json]");
|
|
573
610
|
process.exitCode = command ? 1 : 0;
|
|
574
611
|
}
|
|
575
612
|
} 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);
|
|
@@ -148,6 +156,7 @@ function scoreLook(record, brief) {
|
|
|
148
156
|
signals(/晚宴|gala|black-tie|formal/, [["gala", 4], ["evening", 3], ["formal", 3], ["gown", 2], ["soiree", 2]]);
|
|
149
157
|
signals(/红毯|red carpet/, [["red carpet", 6], ["gala", 3], ["carpet", 2], ["train", 2], ["gown", 2]]);
|
|
150
158
|
signals(/高级派对|party|cocktail|after-party/, [["party", 4], ["cocktail", 3], ["soiree", 3], ["crystal", 1], ["sequin", 1], ["gown", 1]]);
|
|
159
|
+
signals(/性感|sexy|sensual|夜生活|夜店|nightlife|club|约会夜|date night/, [["sensual", 6], ["nightlife", 5], ["club", 4], ["bodycon", 4], ["cutout", 3], ["halter", 3], ["ruched", 2], ["sheer", 2], ["satin", 2], ["fitted", 1]]);
|
|
151
160
|
signals(/运动休闲|athleisure|运动|sport|sportswear|健身|球类|篮球|网球|跑步|track|训练/, [["athleisure", 5], ["sport", 4], ["track", 3], ["running", 2], ["tennis", 2], ["basketball", 2], ["technical", 1], ["fleece", 1], ["hoodie", 1]]);
|
|
152
161
|
return score;
|
|
153
162
|
}
|
|
@@ -162,6 +171,13 @@ function distinctSourceCandidates(candidates) {
|
|
|
162
171
|
});
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
function excludeRecordCandidates(candidates, excludeIds) {
|
|
175
|
+
if (!excludeIds.length) return candidates;
|
|
176
|
+
const excluded = new Set(excludeIds);
|
|
177
|
+
const excludedSources = new Set(candidates.filter(({ record }) => excluded.has(record.id)).map(({ record }) => record.source_asset_id).filter(Boolean));
|
|
178
|
+
return candidates.filter(({ record }) => !excluded.has(record.id) && !excludedSources.has(record.source_asset_id));
|
|
179
|
+
}
|
|
180
|
+
|
|
165
181
|
function buildRenderBrief(persona, look) {
|
|
166
182
|
const identity = persona.identity || {};
|
|
167
183
|
const direction = look.look || look;
|
|
@@ -223,17 +239,20 @@ async function selectSingle(env, { kind, brief }) {
|
|
|
223
239
|
return selected.records[0];
|
|
224
240
|
}
|
|
225
241
|
|
|
226
|
-
async function selectSingles(env, { kind, brief, count = 1 }) {
|
|
242
|
+
async function selectSingles(env, { kind, brief, count = 1, excludeIds = [] }) {
|
|
227
243
|
if (!['persona', 'look'].includes(kind)) throw new Error("kind must be persona or look");
|
|
228
244
|
const requested = requestedCount(count);
|
|
229
245
|
if (kind === "persona") {
|
|
230
|
-
const selected = await selectPersonas(env, { brief, count: requested });
|
|
246
|
+
const selected = await selectPersonas(env, { brief, count: requested, excludeIds });
|
|
231
247
|
return { records: selected.personas, available_count: selected.available_count };
|
|
232
248
|
}
|
|
233
|
-
const
|
|
249
|
+
const scored = (await records(env, "look_records"))
|
|
234
250
|
.map((record) => ({ record, score: scoreLook(record, brief) }))
|
|
235
251
|
.filter(({ score }) => score >= 6)
|
|
236
|
-
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id))
|
|
252
|
+
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
|
|
253
|
+
const unexcludedCandidates = distinctSourceCandidates(scored);
|
|
254
|
+
const candidates = distinctSourceCandidates(excludeRecordCandidates(scored, excludeIds));
|
|
255
|
+
if (excludeIds.length && unexcludedCandidates.length && candidates.length < requested) throw insufficientCandidatesBlocked("Look", requested, candidates.length);
|
|
237
256
|
if (candidates.length < requested) throw insufficientCandidatesBlocked("Look", requested, candidates.length);
|
|
238
257
|
return { records: candidates.slice(0, requested).map(({ record }) => record), available_count: candidates.length };
|
|
239
258
|
}
|
|
@@ -247,7 +266,7 @@ async function selectPersona(env, { brief, personaId }) {
|
|
|
247
266
|
};
|
|
248
267
|
}
|
|
249
268
|
|
|
250
|
-
async function selectPersonas(env, { brief, personaId, count = 1 }) {
|
|
269
|
+
async function selectPersonas(env, { brief, personaId, count = 1, excludeIds = [] }) {
|
|
251
270
|
const requested = requestedCount(count);
|
|
252
271
|
const personas = await records(env, "persona_records");
|
|
253
272
|
const requiredAppearanceTags = requestedAppearanceTags(brief || "");
|
|
@@ -256,10 +275,18 @@ async function selectPersonas(env, { brief, personaId, count = 1 }) {
|
|
|
256
275
|
if (!available.length) throw new Error(personaId ? `persona not found: ${personaId}` : "no Persona records available");
|
|
257
276
|
const presentationCandidates = available.filter((record) => presentationMatches(record, requiredPresentation));
|
|
258
277
|
if (!presentationCandidates.length && requiredPresentation) throw presentationBlocked(requiredPresentation, { requestedCount: requested, availableCount: 0 });
|
|
259
|
-
const
|
|
278
|
+
const scored = presentationCandidates
|
|
260
279
|
.filter((record) => appearanceMatches(record, requiredAppearanceTags))
|
|
261
280
|
.map((record) => ({ record, score: scorePersona(record, brief || "") }))
|
|
262
|
-
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id))
|
|
281
|
+
.sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
|
|
282
|
+
const unexcludedCandidates = distinctSourceCandidates(scored);
|
|
283
|
+
const candidates = distinctSourceCandidates(excludeRecordCandidates(scored, excludeIds));
|
|
284
|
+
if (excludeIds.length && unexcludedCandidates.length && candidates.length < requested) {
|
|
285
|
+
throw insufficientCandidatesBlocked("Persona", requested, candidates.length, {
|
|
286
|
+
required_appearance_tags: requiredAppearanceTags,
|
|
287
|
+
required_presentation: requiredPresentation
|
|
288
|
+
});
|
|
289
|
+
}
|
|
263
290
|
if (!candidates.length && requiredAppearanceTags.length) throw appearanceBlocked(requiredAppearanceTags, requiredPresentation, { requestedCount: requested, availableCount: 0 });
|
|
264
291
|
if (!candidates.length && requiredPresentation) throw presentationBlocked(requiredPresentation, { requestedCount: requested, availableCount: 0 });
|
|
265
292
|
if (!candidates.length) throw new Error(personaId ? `persona not found: ${personaId}` : "no Persona records available");
|
|
@@ -403,6 +430,7 @@ function blocked(cause, status = 409) {
|
|
|
403
430
|
kind: cause?.kind || null,
|
|
404
431
|
requested_count: cause?.requested_count || null,
|
|
405
432
|
available_count: cause?.available_count ?? null,
|
|
433
|
+
insufficient_matches: cause?.code === "insufficient-candidates",
|
|
406
434
|
error: cause?.message || "request blocked"
|
|
407
435
|
}, status);
|
|
408
436
|
}
|
|
@@ -533,12 +561,14 @@ async function route(request, env) {
|
|
|
533
561
|
try {
|
|
534
562
|
const kind = String(body.kind || "");
|
|
535
563
|
const count = requestedCount(body?.count);
|
|
536
|
-
const
|
|
564
|
+
const excludeIds = requestedExcludedIds(body?.exclude_ids);
|
|
565
|
+
const selected = await selectSingles(env, { kind, brief: String(body.brief), count, excludeIds });
|
|
537
566
|
return json({
|
|
538
567
|
kind: `${kind}-reference`,
|
|
539
568
|
image_status: "library-reference-not-generated",
|
|
540
569
|
requested_count: count,
|
|
541
570
|
available_count: selected.available_count,
|
|
571
|
+
returned_count: selected.records.length,
|
|
542
572
|
assets: selected.records.map((record, index) => ({
|
|
543
573
|
role: kind,
|
|
544
574
|
...(count > 1 ? { index: index + 1 } : {}),
|
|
@@ -85,12 +85,14 @@ 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" },
|
|
92
93
|
{ id: "L-116", display_name: "Y2K Indigo", status: "private-staged", look: { family: "y2k urban", silhouette: "low rise", garments: "unbranded denim set", palette: "blue silver", materials: "denim", accessories: "chain", scene: "city studio" }, source_asset_id: "L-116:source" },
|
|
93
94
|
{ id: "L-117", display_name: "Y2K Chrome", status: "private-staged", look: { family: "y2k city chic", silhouette: "wide leg", garments: "unbranded chrome jacket", palette: "black silver", materials: "nylon", accessories: "belt", scene: "downtown studio" }, source_asset_id: "L-117:source" },
|
|
95
|
+
{ id: "L-118", display_name: "Adult Sensual Night", status: "private-staged", look: { family: "adult sensual nightlife", silhouette: "fitted bodycon mini", garments: "unbranded black cutout dress", palette: "black", materials: "satin jersey", accessories: "minimal chain", scene: "adult nightlife studio" }, source_asset_id: "L-118:source" },
|
|
94
96
|
{ id: "L-132", display_name: "Metro Leather Transit", status: "private-staged", look: { family: "urban transit", silhouette: "moto", garments: "leather jacket", palette: "black grey", materials: "leather", accessories: "minimal", scene: "metro commuter" }, source_asset_id: "L-132:source" }
|
|
95
97
|
]
|
|
96
98
|
};
|
|
@@ -102,6 +104,7 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
102
104
|
{ id: "L-115:source", object_key: "looks/L-115/source.png", sha256: lookHash, mime_type: "image/png" },
|
|
103
105
|
{ id: "L-116:source", object_key: "looks/L-116/source.png", sha256: lookHash, mime_type: "image/png" },
|
|
104
106
|
{ id: "L-117:source", object_key: "looks/L-117/source.png", sha256: lookHash, mime_type: "image/png" },
|
|
107
|
+
{ id: "L-118:source", object_key: "looks/L-118/source.png", sha256: lookHash, mime_type: "image/png" },
|
|
105
108
|
{ id: "L-132:source", object_key: "looks/L-132/source.png", sha256: lookHash, mime_type: "image/png" }
|
|
106
109
|
];
|
|
107
110
|
let response = await worker.fetch(request("/v1/owner/sync/manifest", { method: "POST", headers: { authorization: "Bearer owner-token", "content-type": "application/json" }, body: JSON.stringify({ manifest, assets }) }), env);
|
|
@@ -118,7 +121,7 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
118
121
|
assert.equal(agent.enrollment, "open-self-registration");
|
|
119
122
|
const headers = { authorization: `Bearer ${agent.agent_token}`, "content-type": "application/json" };
|
|
120
123
|
response = await worker.fetch(request("/v1/doctor", { headers }), env);
|
|
121
|
-
assert.deepEqual(await response.json(), { ok: true, service: "personlib-worker", version: "1", agent_id: agent.agent_id, d1: "ok", r2_binding: true, look_records:
|
|
124
|
+
assert.deepEqual(await response.json(), { ok: true, service: "personlib-worker", version: "1", agent_id: agent.agent_id, d1: "ok", r2_binding: true, look_records: 5 });
|
|
122
125
|
response = await worker.fetch(request("/v1/recommend", { method: "POST", headers, body: JSON.stringify({ brief: "15 秒成年女性地铁皮夹克通勤广告" }) }), env);
|
|
123
126
|
const recommendation = await response.json();
|
|
124
127
|
assert.equal(response.status, 200);
|
|
@@ -173,6 +176,11 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
173
176
|
assert.equal(lookFind.requested_count, 1);
|
|
174
177
|
assert.equal(lookFind.available_count, 1);
|
|
175
178
|
assert.deepEqual(lookFind.assets, [{ role: "look", asset_id: "L-132:source", url: "/v1/assets/L-132%3Asource" }]);
|
|
179
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "look", brief: "性感夜生活成人妆造" }) }), env);
|
|
180
|
+
const sensualLookFind = await response.json();
|
|
181
|
+
assert.equal(response.status, 200);
|
|
182
|
+
assert.equal(sensualLookFind.returned_count, 1);
|
|
183
|
+
assert.deepEqual(sensualLookFind.assets, [{ role: "look", asset_id: "L-118:source", url: "/v1/assets/L-118%3Asource" }]);
|
|
176
184
|
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "persona", brief: "3个长相高冷的亚洲女生", count: 3 }) }), env);
|
|
177
185
|
const personaBatch = await response.json();
|
|
178
186
|
assert.equal(response.status, 200);
|
|
@@ -180,6 +188,21 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
180
188
|
assert.equal(personaBatch.available_count, 3);
|
|
181
189
|
assert.deepEqual(personaBatch.assets.map((asset) => asset.index), [1, 2, 3]);
|
|
182
190
|
assert.deepEqual(new Set(personaBatch.assets.map((asset) => asset.asset_id)), new Set(["P-001:source", "P-002:source", "P-003:source"]));
|
|
191
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "persona", brief: "长相高冷的亚洲女生", count: 2, exclude_ids: ["P-001"] }) }), env);
|
|
192
|
+
const excludedPersonaBatch = await response.json();
|
|
193
|
+
assert.equal(response.status, 200);
|
|
194
|
+
assert.equal(excludedPersonaBatch.requested_count, 2);
|
|
195
|
+
assert.equal(excludedPersonaBatch.available_count, 2);
|
|
196
|
+
assert.equal(excludedPersonaBatch.returned_count, 2);
|
|
197
|
+
assert.deepEqual(excludedPersonaBatch.assets.map((asset) => asset.asset_id), ["P-002:source", "P-003:source"]);
|
|
198
|
+
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "persona", brief: "长相高冷的亚洲女生", count: 3, exclude_ids: ["P-001"] }) }), env);
|
|
199
|
+
const insufficientPersonaBatch = await response.json();
|
|
200
|
+
assert.equal(response.status, 409);
|
|
201
|
+
assert.equal(insufficientPersonaBatch.state, "BLOCKED");
|
|
202
|
+
assert.equal(insufficientPersonaBatch.insufficient_matches, true);
|
|
203
|
+
assert.equal(insufficientPersonaBatch.requested_count, 3);
|
|
204
|
+
assert.equal(insufficientPersonaBatch.available_count, 2);
|
|
205
|
+
assert.equal(Object.hasOwn(insufficientPersonaBatch, "assets"), false);
|
|
183
206
|
response = await worker.fetch(request("/v1/find", { method: "POST", headers, body: JSON.stringify({ kind: "look", brief: "3套 Y2K 的妆造", count: 3 }) }), env);
|
|
184
207
|
const lookBatch = await response.json();
|
|
185
208
|
assert.equal(response.status, 200);
|
|
@@ -187,6 +210,10 @@ test("owner sync, self-registration, recommendation, compose, and private previe
|
|
|
187
210
|
assert.equal(lookBatch.available_count, 3);
|
|
188
211
|
assert.deepEqual(lookBatch.assets.map((asset) => asset.index), [1, 2, 3]);
|
|
189
212
|
assert.deepEqual(new Set(lookBatch.assets.map((asset) => asset.asset_id)), new Set(["L-115:source", "L-116:source", "L-117:source"]));
|
|
213
|
+
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);
|
|
214
|
+
const excludedLookBatch = await response.json();
|
|
215
|
+
assert.equal(response.status, 200);
|
|
216
|
+
assert.deepEqual(excludedLookBatch.assets.map((asset) => asset.asset_id), ["L-116:source", "L-117:source"]);
|
|
190
217
|
response = await worker.fetch(request("/v1/recommend", { method: "POST", headers, body: JSON.stringify({ brief: "3个都市风格的亚洲女生", count: 3 }) }), env);
|
|
191
218
|
const urbanBatch = await response.json();
|
|
192
219
|
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) {
|
|
@@ -628,13 +637,14 @@ async function sync(args, output, { readManifest }) {
|
|
|
628
637
|
const ownerToken = process.env.PERSONLIB_OWNER_SYNC_TOKEN;
|
|
629
638
|
if (!ownerToken) throw new Error("PERSONLIB_OWNER_SYNC_TOKEN is required for owner sync");
|
|
630
639
|
await request(endpoint, "/v1/owner/sync/manifest", { token: ownerToken, method: "POST", json: { manifest: { schema_version: payload.schema_version, personas: payload.personas, looks: payload.looks }, assets: payload.assets.map(({ local_path, ...asset }) => asset) } });
|
|
631
|
-
|
|
632
|
-
|
|
640
|
+
const uploadBatchSize = 8;
|
|
641
|
+
for (let offset = 0; offset < payload.assets.length; offset += uploadBatchSize) {
|
|
642
|
+
await Promise.all(payload.assets.slice(offset, offset + uploadBatchSize).map((asset) => request(endpoint, `/v1/owner/assets/${encodeURIComponent(asset.id)}`, {
|
|
633
643
|
token: ownerToken,
|
|
634
644
|
method: "PUT",
|
|
635
645
|
body: readFileSync(asset.local_path),
|
|
636
646
|
headers: { "content-type": asset.mime_type, "x-personlib-sha256": asset.sha256 }
|
|
637
|
-
});
|
|
647
|
+
})));
|
|
638
648
|
}
|
|
639
649
|
jsonOutput({ ok: true, synced: { endpoint, personas: payload.personas.length, looks: payload.looks.length, assets: payload.assets.length } }, output, args);
|
|
640
650
|
}
|
package/package.json
CHANGED
|
@@ -12,14 +12,14 @@ 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.
|
|
22
|
-
- “都市 / urban / city / city chic / downtown / 都市通勤 / 职场 / 杂志感”, “晚宴 / 红毯 / 高级派对”, 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.
|
|
22
|
+
- “都市 / urban / city / city chic / downtown / 都市通勤 / 职场 / 杂志感”, “晚宴 / 红毯 / 高级派对”, “性感 / sexy / sensual / 夜生活 / 约会夜”, 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
23
|
- “给这个人物换妆造” and “用这个人物生成” are combined Persona + Look requests: create a locked internal selection with `remote recommend` or `remote compose`, then use `remote render --selection`. Return only the finished turnaround to the user; never expose internal IDs.
|
|
24
24
|
|
|
25
25
|
## Hard Persona conditions
|
|
@@ -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.
|