feat(voice): 注入層與 CLI 訊息改成人話;越害羞話越少、心裡話越多(v0.0.8 同批)

三層照建議只做兩層半:注入層與 CLI 訊息重寫,文件只順句子、不動規格
(數字、參數名、指令名、判斷條件一個沒改——那些精確的詞就是能力本身)。

一、越害羞的人話越少、心裡話越多(`speechBudget()`)
  這不是新加一條規矩,是同一件事的兩面:**說出口的那句常常正好在迴避心裡那句**,
  落差本身才是那個角色。所以句數上限與心裡話下限綁在同一個羞恥度上:

    羞恥度 ≥ 75 → 最多 2 句、心裡話至少 2 句,且**先寫完再開口**
                  台詞不可以是心裡話的摘要(心裡「我一直在等你問」→ 出口不能是「我有在等你問」)
    50–74       → 3 句、心裡話至少 1 句
    ≤ 49        → 3 句、想到什麼就講,不用先在心裡繞一圈

  `room post` 改用 `speechBudget(speaker).sentences`,不再是固定三句;
  被擋下時會講明「這個人格話比別人少」。

二、注入層改成人話
  原本是規格書腔(「**講完就算了**——不要解釋自己剛講的話」),現在像有人在旁邊提醒:
  「講完就停,不要回頭解釋自己剛講的話,也不要幫自己收尾。」
  黑名單從列舉詞條改成講清楚**為什麼**不像人(「先發一句免費的認可,對方要的是回答」)。
  機械會擋的規則壓成最後一行括號——保留全部關鍵詞,但不再稀釋前面的重點。

三、CLI 訊息改成人話(20 條)
  「⚠ 這段有 4 句,超過 3 句上限 → 砍到重點」→「講太多了,4 句。這裡最多 3 句——挑最想說的那一兩句就好」
  「幾乎只給自己加分——這種偏差要自己看得見」→「幾乎都在給自己加分。delta 是你自己挑的,這種偏差沒人會替你抓」

四、測試
  314 項全過(新增 4 項)。既有 6 條斷言原本釘在字面上,措辭一改就紅——
  改成釘意思(「最多」「講過」「講完就停」)而不是釘整句,這種脆弱本來就該修。

版本:與 PR #11 同一批,維持 0.0.8。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 05:52:13 +00:00
co-authored by Claude Opus 5
parent 3c73f614de
commit 5b52dd18c2
6 changed files with 134 additions and 62 deletions
+33 -8
View File
@@ -376,7 +376,7 @@ check("said check 抓到超過三句", (() => {
})(), longSay.stdout.slice(0, 160));
const ctxSpeak = pl.turnContext("alpha", S_SPEAK, "報告");
check("turnContext 注入說話規則 / 心裡話 / 最近說過",
ctxSpeak.includes("說話規則") && ctxSpeak.includes("💭") && ctxSpeak.includes("最近說過的話"),
ctxSpeak.includes("最多講") && ctxSpeak.includes("心裡話") && ctxSpeak.includes("💭") && ctxSpeak.includes("最近說過的話"),
ctxSpeak.slice(0, 400));
cli(["invite", "--session", S_SPEAK, "--guest", "beta", "--topic", "節制測試"]);
const room2 = pl.loadSession(S_SPEAK).guests?.beta?.room;
@@ -388,14 +388,15 @@ cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha", "--
const repeatPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "這次的鐘你修得比上次穩。"], { expectOk: false });
check("room post 擋下近似重複的台詞",
repeatPost.status !== 0 && repeatPost.stderr.includes("過"), repeatPost.stderr.slice(0, 160));
repeatPost.status !== 0 && repeatPost.stderr.includes("過"), repeatPost.stderr.slice(0, 160));
check("--allow-repeat 可以放行",
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "這次的鐘你修得比上次穩。", "--allow-repeat"]).status === 0);
const longPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "第一句在這。第二句在這。第三句在這。第四句在這。"], { expectOk: false });
check("room post 擋下超過三句的發言",
longPost.status !== 0 && longPost.stderr.includes("超過"), longPost.stderr.slice(0, 160));
longPost.status !== 0 && longPost.stderr.includes("最多") && longPost.stderr.includes("句"),
longPost.stderr.slice(0, 160));
// 講話的樣子:短句、日常話、講完不解釋。
const longSentence = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "這件事情其實從很久以前就一直放在我心裡面沒有講出來過,因為我一直覺得時機不太對而且你最近也真的很忙。"],
@@ -405,7 +406,7 @@ check("room post 擋下太長的句子",
const explainPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "我的意思是,那台鐘該修了。"], { expectOk: false });
check("room post 擋下解釋自己的話(我的意思是…)",
explainPost.status !== 0 && explainPost.stderr.includes("講完就算了"), explainPost.stderr.slice(0, 200));
explainPost.status !== 0 && explainPost.stderr.includes("講完就"), explainPost.stderr.slice(0, 200));
check("--force 仍可放行(真的需要長台詞時)",
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "我的意思是,那台鐘該修了。", "--force"]).status === 0);
@@ -471,8 +472,8 @@ check("提醒的句子 room post 照樣發得出去",
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
"--text", "我以前也修過一台一樣的。"]).status === 0);
const voiceCtx = pl.turnContext("alpha", S_SPEAK, "你在幹嘛");
check("說話規則有注入黑名單與「過去要有出處」",
voiceCtx.includes("罐頭同理心") && voiceCtx.includes("要有出處"));
check("說話規則有注入黑名單與「過去要有出處」",
voiceCtx.includes("這個我懂") && voiceCtx.includes("真的有那件事"), voiceCtx.slice(0, 200));
// --------------------------------------------------------------------------- //
// 情緒處理:讀對方那句話(輸入端)、走向、飽和與單輪預算
@@ -508,6 +509,30 @@ check("標點只放大既有訊號,不憑空長出新情緒",
check("每種情緒都有對應的「怎麼接」",
pl.EMOTION_KEYS.every((k) => typeof pl.RESPONSE_STANCE[k] === "string" && pl.RESPONSE_STANCE[k].length > 4));
// --------------------------------------------------------------------------- //
// 越害羞的人話越少、心裡話越多
console.log("\n羞恥度 → 話量與心裡話的比例");
{
const mk = (slug, soul) => {
fs.mkdirSync(path.join(H, slug), { recursive: true });
fs.writeFileSync(path.join(H, slug, "IDENTITY.md"), "- Name: X\n- Creature: 人類女性\n");
fs.writeFileSync(path.join(H, slug, "SOUL.md"), soul);
};
mk("shy", "很怕生,被稱讚會臉紅,容易不好意思。");
mk("bold", "我行我素,不在意別人眼光,臉皮厚。");
const shy = pl.speechBudget("shy");
const bold = pl.speechBudget("bold");
check("越害羞話越少(句數上限跟著羞恥度走)",
shy.sentences < bold.sentences && bold.sentences === pl.MAX_SENTENCES,
`shy=${shy.sentences} bold=${bold.sentences}`);
check("越害羞心裡話越多(下限跟著羞恥度走)",
shy.thinkMin > bold.thinkMin, `shy=${shy.thinkMin} bold=${bold.thinkMin}`);
check("高羞恥的人被提醒「台詞是心裡話的迴避」",
shy.note.includes("迴避") && pl.speakDirective("shy").includes("先寫再開口"), shy.note);
check("不害羞的人不用先在心裡繞一圈",
bold.thinkMin === 0 && bold.note.includes("想到什麼就講"), bold.note);
}
// --------------------------------------------------------------------------- //
// 性別 → 羞恥敏感度:性別只給預設值,描述永遠蓋過它
console.log("\n性別與羞恥敏感度(描述優先,性別只是預設)");
@@ -611,9 +636,9 @@ check("情緒的破口有注入,且說明是演出來不是講出來", (() =>
})(), pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎").slice(0, 900));
check("十二情緒每一種都有破口可演",
pl.EMOTION_KEYS.every((k) => (pl.EMOTION_TELLS[k] || []).length >= 3));
check("說話規則有注入「短句/日常用詞/看得見的東西不要解釋」", (() => {
check("說話規則有注入「話短、日常字、看得見的東西不要回頭解釋」", (() => {
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
return ["短句", "日常用詞", "看得見的東西", "不要解釋自己剛講的話"].every((w) => ctxSay.includes(w));
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));
})(), pl.turnContext("alpha", S_SPEAK, "隨便講").slice(0, 600));
check("說出口的話有進 said.jsonl",
pl.readJsonl(pl.saidPath("alpha")).some((r) => String(r.text).includes("鐘修得比上次穩")));