From 7541f85ece4310de47b966fbdcd0682248f73527 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 3 Aug 2026 09:11:58 +0000 Subject: [PATCH] =?UTF-8?q?test(=E8=A1=A8=E9=81=94=E9=A9=97=E6=94=B6):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20expression-check.mjs=EF=BC=8C=E5=9B=9B?= =?UTF-8?q?=E6=AE=B5=E6=83=85=E5=A2=83=20=C3=97=20=E4=B8=89=E6=AA=94?= =?UTF-8?q?=E7=BE=9E=E6=81=A5=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit selftest 是機械檢查,它只能守住「不崩」——括號格式對不對、emoji 門檻有沒有跳掉。 但「有沒有變成模仿腔」「是不是在用旁白演情緒」機械上驗不出來,那要人看。 所以這是另一組,而且它刻意不判定通過或失敗,只把材料排好+附一張「人工看什麼」。 四段情境照 TODO 階段 5 的驗收方式明列的那四段:誇外表、被說在害羞、被戳穿嘴硬、 第三個人在場。每段跑低/嘴硬/惱羞三檔,攤開鬧彆扭指示、句數句長、破口、標點、 情緒符號、分段額度、稱呼與語氣層、語域。 三檔用兩個人格加情緒推,不是同一個人格推三次:羞恥敏感度主要是氣質(性別預設+ 描述),同一個人格靠情緒推不到 18 那麼低,硬推出來的低檔不是真的低檔。 刻意不帶 prev:真的對話裡上一輪的餘溫是正回饋,但這裡沒有上一輪。帶進來的話 建人格時留在 felt.jsonl 的那筆會把三檔拉平——實測高檔會從 82.9 掉到 15。 用自己的暫時 PERSONA_HOME 並關掉 Gitea,不動真的人格資料。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/expression-check.mjs | 135 +++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 scripts/expression-check.mjs diff --git a/scripts/expression-check.mjs b/scripts/expression-check.mjs new file mode 100644 index 0000000..5105362 --- /dev/null +++ b/scripts/expression-check.mjs @@ -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} +`);