test(表達驗收): 新增 expression-check.mjs,四段情境 × 三檔羞恥度
selftest 是機械檢查,它只能守住「不崩」——括號格式對不對、emoji 門檻有沒有跳掉。 但「有沒有變成模仿腔」「是不是在用旁白演情緒」機械上驗不出來,那要人看。 所以這是另一組,而且它刻意不判定通過或失敗,只把材料排好+附一張「人工看什麼」。 四段情境照 TODO 階段 5 的驗收方式明列的那四段:誇外表、被說在害羞、被戳穿嘴硬、 第三個人在場。每段跑低/嘴硬/惱羞三檔,攤開鬧彆扭指示、句數句長、破口、標點、 情緒符號、分段額度、稱呼與語氣層、語域。 三檔用兩個人格加情緒推,不是同一個人格推三次:羞恥敏感度主要是氣質(性別預設+ 描述),同一個人格靠情緒推不到 18 那麼低,硬推出來的低檔不是真的低檔。 刻意不帶 prev:真的對話裡上一輪的餘溫是正回饋,但這裡沒有上一輪。帶進來的話 建人格時留在 felt.jsonl 的那筆會把三檔拉平——實測高檔會從 82.9 掉到 15。 用自己的暫時 PERSONA_HOME 並關掉 Gitea,不動真的人格資料。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// G2/G3 表達驗收:把同一個情境在不同羞恥度下的**注入內容**攤開,讓人用眼睛看。
|
||||||
|
//
|
||||||
|
// 為什麼不寫進 selftest:selftest 是機械檢查,它只能守住「不崩」——括號格式對不對、
|
||||||
|
// emoji 門檻有沒有跳掉。但「有沒有變成模仿腔」「是不是在用旁白演情緒」機械上驗不出來,
|
||||||
|
// 那要人看。所以這是另一組,而且它**不判定通過或失敗**,只把材料排好。
|
||||||
|
//
|
||||||
|
// 用法:`node scripts/expression-check.mjs`
|
||||||
|
// 會開一個暫時的 PERSONA_HOME,不動到你真正的人格資料,也不碰 Gitea。
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const STORE = fs.mkdtempSync(path.join(os.tmpdir(), "persona-expression-"));
|
||||||
|
process.env.PERSONA_HOME = STORE;
|
||||||
|
process.env.PERSONA_GITEA = "off";
|
||||||
|
|
||||||
|
const pl = await import("./persona-lib.mjs");
|
||||||
|
const CLI = path.join(HERE, "persona.mjs");
|
||||||
|
|
||||||
|
const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8" });
|
||||||
|
|
||||||
|
// 四段情境(TODO 階段 5 的驗收方式明列的那四段)
|
||||||
|
const SCENES = [
|
||||||
|
{ key: "誇外表", prompt: "你今天這樣看起來很好看。" },
|
||||||
|
{ key: "被說在害羞", prompt: "你是不是在害羞?" },
|
||||||
|
{ key: "被戳穿嘴硬", prompt: "你剛剛明明就很開心,不要裝。" },
|
||||||
|
{ key: "第三個人在場", prompt: "他剛剛跟我說他很在意你,你知道嗎?" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 三檔羞恥度。低檔用另一個人格:羞恥敏感度主要是**氣質**(性別預設+描述),
|
||||||
|
// 同一個人格靠情緒推不到 38 那麼低——硬推出來的低檔不是真的低檔。
|
||||||
|
const BANDS = [
|
||||||
|
{
|
||||||
|
key: "低",
|
||||||
|
slug: "low",
|
||||||
|
session: "sess-expr-low-1",
|
||||||
|
identity: "- Name: 低\n- Creature: 人類男性\n",
|
||||||
|
soul: "不在意別人眼光,該說就說,被戳穿也直接承認。",
|
||||||
|
shame: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "中(嘴硬)",
|
||||||
|
slug: "mid",
|
||||||
|
session: "sess-expr-mid-2",
|
||||||
|
identity: "- Name: 中\n- Creature: 人類女性\n",
|
||||||
|
soul: "劍術很好,說話簡短。",
|
||||||
|
shame: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "高(惱羞)",
|
||||||
|
slug: "high",
|
||||||
|
session: "sess-expr-high-3",
|
||||||
|
identity: "- Name: 高\n- Creature: 人類女性\n",
|
||||||
|
soul: "劍術很好,說話簡短。",
|
||||||
|
shame: 75,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const band of BANDS) {
|
||||||
|
run(["create", "--persona", band.slug, "--session", band.session,
|
||||||
|
"--name", band.key, "--creature", "人類", "--vibe", "簡短", "--emoji", "🗡"]);
|
||||||
|
fs.writeFileSync(path.join(STORE, band.slug, "IDENTITY.md"), band.identity);
|
||||||
|
fs.writeFileSync(path.join(STORE, band.slug, "SOUL.md"), band.soul);
|
||||||
|
// 對象:語氣層與「害羞時的稱呼」都要有對象才算得出來
|
||||||
|
run(["relation", "node", "--session", band.session, "--name", "他",
|
||||||
|
"--kind", "human", "--bond", "partner", "--closeness", "88", "--trust", "85"]);
|
||||||
|
run(["relation", "speaker", "--session", band.session, "--name", "他"]);
|
||||||
|
// 語域:G2 的核心是「把名字遮掉還認得出是誰」,沒有這幾格就看不出差別
|
||||||
|
for (const [facet, value] of [["自稱", "我"], ["句尾", "⋯吧"], ["口癖", "算了"], ["不說", "其實我覺得"]]) {
|
||||||
|
run(["voice", "add", "--session", band.session, "--kind", "idiolect", "--facet", facet, "--value", value]);
|
||||||
|
}
|
||||||
|
if (band.shame > 0) {
|
||||||
|
run(["emotion", "--session", band.session, "--apply", `shame=+${band.shame}`, "--trigger", "驗收:把羞恥推上去"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = (s = "─") => s.repeat(78);
|
||||||
|
const show = (label, text) => {
|
||||||
|
if (!text) return;
|
||||||
|
console.log(` ${label}`);
|
||||||
|
for (const row of String(text).split("\n")) console.log(` ${row.replace(/^\s+/, "")}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("G2/G3 表達驗收——同一段話,三檔羞恥度");
|
||||||
|
console.log(`暫存人格家目錄:${STORE}`);
|
||||||
|
console.log(line("═"));
|
||||||
|
|
||||||
|
for (const scene of SCENES) {
|
||||||
|
console.log(`\n【${scene.key}】使用者說:${scene.prompt}`);
|
||||||
|
console.log(line());
|
||||||
|
for (const band of BANDS) {
|
||||||
|
const { slug } = band;
|
||||||
|
const read = pl.readUserEmotion(scene.prompt);
|
||||||
|
// 刻意**不帶 `prev`**:真的對話裡上一輪的餘溫是正回饋,但這裡沒有上一輪。
|
||||||
|
// 帶進來的話 `felt.jsonl` 裡建人格時留下的那筆會把三檔全部拉平(實測高檔會掉到 15)。
|
||||||
|
const opts = { read, tone: pl.toneLayerOf(slug) };
|
||||||
|
const modesty = pl.modestyState(slug, opts);
|
||||||
|
const tells = pl.emotionTells(slug);
|
||||||
|
const punct = pl.punctuationBudget(slug);
|
||||||
|
const emoji = pl.emotionEmojiNow(slug);
|
||||||
|
const split = pl.splitBudget(slug, opts);
|
||||||
|
console.log(`\n ▸ ${band.key} 羞恥度 ${modesty.value}(氣質 ${modesty.trait}/情緒推 ${modesty.push})`);
|
||||||
|
show("鬧彆扭:", pl.modestyDirective(slug, opts));
|
||||||
|
show("句數與句長:", pl.speakDirective(slug, opts));
|
||||||
|
show("破口:", tells.length
|
||||||
|
? tells.map((t) => `${t.zh} ${t.level} → ${t.tells.join("、")}${t.own ? "(專屬)" : ""}`).join(";")
|
||||||
|
: "(強度不到,這一輪不演)");
|
||||||
|
show("標點:", punct ? `${punct.zh} ${punct.level} → ${punct.hint}` : "(沒到門檻,不調標點)");
|
||||||
|
show("情緒符號:", emoji ? `${emoji.emoji}(${emoji.zh} ${emoji.level})` : "(不到 40,不顯示)");
|
||||||
|
show("分段:", split.allowed ? `可以補第二則(${split.reason})` : `只有一則(${split.reason})`);
|
||||||
|
show("稱呼與語氣層:", pl.toneDirective(slug, opts));
|
||||||
|
show("語域:", pl.idiolectBrief(slug));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${line("═")}`);
|
||||||
|
console.log(`
|
||||||
|
人工看什麼(機械驗不出來的那些):
|
||||||
|
|
||||||
|
✗ 旁白演情緒 「我愣了一下」「她臉紅了」——動作只能在名字後面的括號裡
|
||||||
|
✗ 模仿腔 三檔講起來像同一個人套了不同強度,而不是三種不同的反應
|
||||||
|
✗ 符號代替句子 把 emoji 刪掉之後就看不出情緒了(emoji 是加上去的)
|
||||||
|
✗ 標點當強度計 驚嘆號、刪節號拿來表示「更激動」,而不是表示分布
|
||||||
|
✗ 三段式變公式 每次嘴硬都是同一組句子(那要 \`## Tells\` 的 sulk 逐人格覆寫)
|
||||||
|
✓ 中檔要看得出「先否認 → 反駁 → 音量掉下來」
|
||||||
|
✓ 高檔的第三段是翻臉,不是音量掉下來,而且動作仍然只有兩個
|
||||||
|
✓ 低檔不該演害羞:該承認就承認、該回嘴就回嘴
|
||||||
|
|
||||||
|
看完就可以把暫存目錄刪掉:rm -rf ${STORE}
|
||||||
|
`);
|
||||||
Reference in New Issue
Block a user