feat: 圖示配色取自人格「最新一次登場」的官方視覺
新增 skill persona-icon:上網查出該人格最新的官方視覺 → 下載 →
**用 Read 親眼看過** → 取髮色/瞳色/服裝色 → 用那組配色繪製圖示,
來源網址與造型描述一併寫進 state/config.json 備查。
CLI:
icon generate --palette "hair=#..,eye=#..,accent=#..,secondary=#..,light=#.."
--source-url <網址> [--source-note <說明> --source-date <日期>]
* --palette 必須配 --source-url(配色要有出處,CLI 強制)。
* 配色與來源存進 config.icon,之後不帶 --palette 重畫會沿用,不會變回雜湊色。
* icon show 會列出調色盤與來源網址。
繪製:漸層=髮色→服裝主色、外框=瞳色、點陣紋=亮色、中央仍是編號前兩字。
一深一淺的極端配色(藍黑髮+淡粉洋裝)會把較亮端往較暗端壓到對比 ≥ 3.2,
確保字讀得到——顏色仍是照片來的,只是收斂色階。
為什麼不是把照片本身當圖示:
1) 環境裡沒有任何影像解碼器(rsvg/imagemagick/Pillow/ffmpeg 都沒有),
JPEG/WebP 讀不進來,無法轉成 PNG;
2) 把他人的美術作品原樣放進存取庫是散布,不是引用。
取配色是有依據又不搬運原圖的做法,這個取捨寫進了 README 與 skill。
已套用到兩個真實人格:
ASUNA-01 取自《Unanswered//butterfly》(2026) 官方主視覺(金栗髮/紅褐瞳/紅衣粉裙)
YUI-01 取自 AniList 官方角色圖(藍黑髮/暖棕瞳)+ Unital Ring 導航妖精造型
selftest 136 項全綠(新增第 ⑮ 節:調色盤解析、缺來源會被擋、格式錯誤會被擋、
來源寫入 config、重畫沿用配色、極端配色的對比保證)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -519,6 +519,55 @@ check("guest 只能看不能重畫圖示",
|
||||
guard({ session_id: S_HOST, agent_id: "guest-9", agent_type: "jsc-persona:persona-guest", tool_name: "Bash",
|
||||
tool_input: { command: `node persona.mjs icon generate --persona beta --session ${S_HOST} --as-guest` } }) === "deny");
|
||||
|
||||
console.log("⑮ 依參考照片配色的圖示");
|
||||
const PAL = "hair=#d9a45b,eye=#9e5b3e,accent=#c0392b,secondary=#e77a8e,light=#f2ebe3";
|
||||
const SRC = "https://example.invalid/key-visual.png";
|
||||
check("調色盤解析:hair 與 accent 必填、支援 #abc 縮寫", (() => {
|
||||
const ok = ic.parsePalette(PAL);
|
||||
const short = ic.parsePalette("hair=#abc,accent=#123456");
|
||||
return ok?.hair?.join() === "217,164,91" && ok.eye.join() === "158,91,62" &&
|
||||
short?.hair?.join() === "170,187,204" &&
|
||||
ic.parsePalette("eye=#ffffff") === null && ic.parsePalette("garbage") === null;
|
||||
})());
|
||||
check("`--palette` 沒帶 `--source-url` 會被擋(配色要有出處)",
|
||||
cli(["icon", "generate", "--session", S_CODE, "--force", "--palette", PAL], { expectOk: false }).status !== 0);
|
||||
check("格式錯誤的 `--palette` 會被擋",
|
||||
cli(["icon", "generate", "--session", S_CODE, "--force", "--palette", "hair=紅色",
|
||||
"--source-url", SRC], { expectOk: false }).status !== 0);
|
||||
cli(["icon", "generate", "--session", S_CODE, "--force", "--size", "64",
|
||||
"--palette", PAL, "--source-url", SRC, "--source-note", "測試用主視覺", "--source-date", "2026-07-30"]);
|
||||
const palSpec = ic.iconSpec("GAMMA-01");
|
||||
check("圖示改用照片配色(瞳色當外框、亮色當紋路)",
|
||||
palSpec.palette !== null && palSpec.ring.join() === "158,91,62" && palSpec.dot.join() === "242,235,227");
|
||||
check("來源網址與說明寫進 config(可查證)", (() => {
|
||||
const icon = pl.loadConfig("GAMMA-01").icon || {};
|
||||
return icon.palette === PAL && icon.source?.url === SRC &&
|
||||
icon.source?.note === "測試用主視覺" && icon.source?.date === "2026-07-30";
|
||||
})(), JSON.stringify(pl.loadConfig("GAMMA-01").icon));
|
||||
check("照片配色與雜湊配色畫出來不一樣", (() => {
|
||||
const withPal = ic.renderPng(palSpec, 32);
|
||||
const hashOnly = ic.renderPng({ ...palSpec, palette: null, c1: [40, 120, 184], c2: [200, 102, 214],
|
||||
ring: palSpec.ink, dot: palSpec.ink, ringAlpha: 0.18, dotAlpha: 0.14 }, 32);
|
||||
return Buffer.compare(withPal, hashOnly) !== 0;
|
||||
})());
|
||||
check("不帶 --palette 重畫會沿用已存的配色(不會變回雜湊色)", (() => {
|
||||
cli(["icon", "generate", "--session", S_CODE, "--force", "--size", "64"]);
|
||||
const again = ic.iconSpec("GAMMA-01");
|
||||
return again.palette !== null && ic.paletteToString(again.palette) === PAL;
|
||||
})());
|
||||
check("一深一淺的極端配色仍保證字讀得到(會收斂色階)", (() => {
|
||||
// 藍黑髮 + 淡粉洋裝:不收斂的話不論黑字白字都會有一端糊掉
|
||||
const spec = ic.iconSpec("GAMMA-01", {
|
||||
palette: ic.parsePalette("hair=#1b1b22,eye=#6b4a2f,accent=#f2b6cb,secondary=#4a7bc8,light=#fbeff3"),
|
||||
});
|
||||
const lum = (c) => {
|
||||
const f = c.map((v) => (v / 255 <= 0.03928 ? v / 255 / 12.92 : ((v / 255 + 0.055) / 1.055) ** 2.4));
|
||||
return 0.2126 * f[0] + 0.7152 * f[1] + 0.0722 * f[2];
|
||||
};
|
||||
const ratio = (a, b) => (Math.max(lum(a), lum(b)) + 0.05) / (Math.min(lum(a), lum(b)) + 0.05);
|
||||
return Math.min(ratio(spec.ink, spec.c1), ratio(spec.ink, spec.c2)) >= 3;
|
||||
})());
|
||||
|
||||
console.log(`\n${"=".repeat(60)}\n通過 ${passed} 項,失敗 ${failed} 項 → ${failed === 0 ? "全部通過 ✅" : "有測試失敗 ❌"}`);
|
||||
console.log(`(暫存倉庫留在 ${STORE},可自行刪除)`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user