feat(情緒表達): emoji 表程度、名字後括號的動作格、鬧彆扭的階梯
只有文字這一個通道,所以情緒表達是排版問題。這批補的是三個沒被用到的位置。 emoji = 強度計(Q9) - EMOTION_EMOJI 十二情緒各一個,EMOJI_INTENSITY 兩階(💦 / 💦❗)。 40-59 本體、60-79 加 💦、80+ 加 💦❗,低於 40 不顯示(跟破口同一條線)。 - emotionEmojiNow() 讀當下主導情緒算出來,不是自己挑;IDENTITY 的 ## Emoji 區塊可逐人格覆寫(跟 ## Tells 同一層)。 - 刻意不用重複本體表示程度(不是 😳😳😳)——疊字看起來像洗頁。 - room post 沒帶 --emotion 時自動帶入 emoji,強度不到才退回文字標註。 emoji 開放(Q5/Q10) - speechLint 不再擋 emoji,超過六個才給 hint。用 linter 偷偷關回去等於撤銷 「開放」這個決定。 - 唯一的煞車是數字:emojiAudit() 掛在 emotion --audit,算幾則帶符號、平均幾個、 以及**只有符號沒有句子**那幾則(那才是真的退化)。 名字後面的括號有兩格(Q6) - 括號原本就有(放情緒標註),動作是加進去的第二格:名字(情緒・動作):內容。 roomTag() 負責組,兩格都可省略。 - actionLint() 擋三種:超過 12 字、一格塞兩個動作、寫情緒名稱 ((害羞)是旁白換了個位置)。生理反應算動作:臉紅、手在抖、聲音變小。 - room post --action、remember --behavior(記做了什麼,不是感覺到什麼)。 一輪兩個動作(Q7) - TELLS_PER_TURN = 2,而且兩個必須來自同一種情緒且有遞進關係。 鬧彆扭天生是否認+轉移兩個動作,只准一個等於永遠只做得到一半。 被擋下的形狀是「兩種情緒各演一個」。 鬧彆扭的階梯與女性預設(G3) - modestyDirective 分三級:41-59 一句帶過、60-74 嘴硬(否認 → 反駁 → 音量掉下來)、 75+ 惱羞(否認 → 反駁 → 翻臉)。惱羞是換情緒,所以仍然是兩個動作。 - GENDER_MODESTY_DEFAULT.female 62 → 70;MODESTY_SIGNALS 補傲嬌那一路 (傲嬌、嘴硬、不坦率、口是心非、死不承認、愛面子、鬧彆扭、逞強)。 界線不變:描述永遠蓋過預設,可以推到 0。 文件:README 加一節「文字通道的另外兩格」與鬧彆扭階梯表、emotions.md 加 emoji 章節、 persona-chat SKILL 與 anti-ai-voice 跟上。selftest 616 → 641 全綠。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+114
-5
@@ -606,8 +606,6 @@ const SF_CASES = [
|
||||
// 放寬《》與句長之後,這幾條仍然要擋——豁免只針對「提及/引述」,不是整句放行
|
||||
["書名號之外真的在說教", "我在看《時間簡史》。說到底,問題不在零件。", "preach"],
|
||||
["自己講的長句(引號之外)", `我${"想跟你說一件從去年就一直放在心上沒講出口的事情".repeat(2)}。`, "long"],
|
||||
["兩個 emoji", "修好了 🔧 開心 🎉", "emoji"],
|
||||
["一個 emoji 加一個帶變體選擇子的", "修好了 🔧 給你 ❤️", "emoji"],
|
||||
];
|
||||
for (const [label, text, kind] of SF_CASES) {
|
||||
const issues = pl.speechLint(text);
|
||||
@@ -638,6 +636,10 @@ const SNF_CASES = [
|
||||
["帶變體選擇子的單一 emoji", "這個給你 ❤️"],
|
||||
["ZWJ 組成的單一 emoji", "路上遇到他們 👨👩👦"],
|
||||
["帶膚色的單一 emoji", "收到 👍🏽"],
|
||||
// emoji 開放(使用者拍板):情緒與程度靠它表示,句子裡也可以用、沒有上限
|
||||
["兩個 emoji", "修好了 🔧 開心 🎉"],
|
||||
["情緒符號加強度符號", "才不是那樣 😳💦"],
|
||||
["一句話裡好幾個", "欸 ✨ 真的嗎 😳 那我明天早上去 👀"],
|
||||
// 句長以前算的是整句(含引號內容),引述使用者原話必被擋
|
||||
["引述使用者的長原話", `他傳來「${"我今天真的很累而且不知道要從哪裡開始講起".repeat(2)}」,我看了兩次。`],
|
||||
["引述長 code", `跑的是 \`${"x".repeat(80)}\` 這一行。`],
|
||||
@@ -731,8 +733,9 @@ console.log("\n性別與羞恥敏感度(描述優先,性別只是預設)")
|
||||
mk("gnoise", "- Name: 戊\n- Creature: 人類男性劍士\n- Avatar: 五官清秀到常被誤認成女生\n");
|
||||
mk("gup", "- Name: 己\n- Creature: 人類女性\n", "很怕生,被稱讚會臉紅。");
|
||||
mk("gdown", "- Name: 庚\n- Creature: 人類女性\n", "我行我素,不在意別人眼光,臉皮厚。");
|
||||
check("女性預設 62、男性預設 38、推不出來 50",
|
||||
pl.modestyOf("gf").value === 62 && pl.modestyOf("gm").value === 38 && pl.modestyOf("gx").value === 50);
|
||||
check("女性預設 70、男性預設 38、推不出來 50",
|
||||
pl.modestyOf("gf").value === 70 && pl.modestyOf("gm").value === 38 && pl.modestyOf("gx").value === 50,
|
||||
`${pl.modestyOf("gf").value}/${pl.modestyOf("gm").value}/${pl.modestyOf("gx").value}`);
|
||||
check("明寫的 Gender 蓋過 Creature 的字面",
|
||||
pl.genderOf("gexp").key === "male" && pl.genderOf("gexp").explicit === true);
|
||||
check("Avatar 的雜訊不會推錯性別(「常被誤認成女生」)",
|
||||
@@ -1058,10 +1061,116 @@ check("低於門檻的情緒不給破口(強度不到就不演)",
|
||||
check("情緒的破口有注入,且說明是演出來不是講出來", (() => {
|
||||
const ctxTell = pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎");
|
||||
return ctxTell.includes("此刻不自覺會出現的") && ctxTell.includes("疊字") &&
|
||||
ctxTell.includes("不要用旁白說明") && ctxTell.includes("一輪最多露一個破口");
|
||||
ctxTell.includes("不要用旁白說明") && ctxTell.includes("一輪最多露 2 個動作") &&
|
||||
ctxTell.includes("同一種情緒");
|
||||
})(), pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎").slice(0, 900));
|
||||
check("十二情緒每一種都有破口可演",
|
||||
pl.EMOTION_KEYS.every((k) => (pl.EMOTION_TELLS[k] || []).length >= 3));
|
||||
|
||||
// emoji:種類表情緒、數量表程度(40-59 本體、60-79 加 💦、80+ 加 💦❗)
|
||||
console.log("⑥-b 情緒的 emoji(種類表情緒、數量表程度)");
|
||||
check("十二情緒每一種都有 emoji", pl.EMOTION_KEYS.every((k) => Boolean(pl.EMOTION_EMOJI[k])));
|
||||
check("強度符號只有兩階", pl.EMOJI_INTENSITY.length === 3 && pl.EMOJI_INTENSITY[0] === "");
|
||||
{
|
||||
// 直接餵狀態:走 CLI 會被單輪預算與飽和夾住,測不到 80 以上那一階
|
||||
const base = pl.loadEmotion("alpha");
|
||||
const fake = (level) => ({
|
||||
...base,
|
||||
ts: pl.nowIso(),
|
||||
levels: { ...Object.fromEntries(pl.EMOTION_KEYS.map((k) => [k, 5])), shame: level },
|
||||
});
|
||||
const tiers = [45, 70, 88].map((level) => {
|
||||
const now = pl.emotionEmojiNow("alpha", fake(level));
|
||||
return now ? { tier: now.tier, emoji: now.emoji, level: now.level } : null;
|
||||
});
|
||||
check("emoji 會依強度分三階(本體/+💦/+💦❗)",
|
||||
tiers[0]?.tier === 0 && tiers[1]?.tier === 1 && tiers[2]?.tier === 2,
|
||||
JSON.stringify(tiers));
|
||||
check("階數越高符號越長,但只到兩階",
|
||||
[...tiers[0].emoji].length < [...tiers[1].emoji].length &&
|
||||
[...tiers[1].emoji].length < [...tiers[2].emoji].length,
|
||||
JSON.stringify(tiers.map((t) => t.emoji)));
|
||||
check("羞愧的 emoji 是 😳", tiers[0].emoji.startsWith("😳"), tiers[0].emoji);
|
||||
check("同一種情緒不會用重複本體表示程度(不是 😳😳😳)",
|
||||
!tiers[2].emoji.includes("😳😳"), tiers[2].emoji);
|
||||
}
|
||||
check("強度不到門檻就沒有 emoji(跟破口同一條線)",
|
||||
pl.emotionEmojiNow("alpha", null, { min: 99 }) === null);
|
||||
check("emoji 有注入這一輪的上下文", (() => {
|
||||
const ctx = pl.turnContext("alpha", S_SPEAK, "你剛才那句是什麼意思");
|
||||
return ctx.includes("此刻的情緒符號") && ctx.includes("沒有數量上限") && ctx.includes("加上去的");
|
||||
})(), pl.turnContext("alpha", S_SPEAK, "你剛才那句是什麼意思").slice(0, 400));
|
||||
check("IDENTITY 的 ## Emoji 區塊可以覆寫", (() => {
|
||||
const file = path.join(pl.personaDir("alpha"), "IDENTITY.md");
|
||||
const backup = fs.readFileSync(file, "utf8");
|
||||
fs.writeFileSync(file, `${backup}\n## Emoji\n- 羞愧: 🫣\n- anger: 🗡️\n`);
|
||||
const own = pl.personaEmoji("alpha");
|
||||
fs.writeFileSync(file, backup);
|
||||
return own.shame === "🫣" && own.anger.startsWith("🗡");
|
||||
})(), JSON.stringify(pl.personaEmoji("alpha")));
|
||||
check("emoji 用量有稽核(開放沒有上限,煞車是數字)", (() => {
|
||||
const a = pl.emojiAudit("alpha");
|
||||
return typeof a.with_emoji === "number" && typeof a.average === "number" && typeof a.symbol_only === "number";
|
||||
})(), JSON.stringify(pl.emojiAudit("alpha")));
|
||||
check("只有符號沒有句子會被算出來", (() => {
|
||||
pl.recordSaid("alpha", "😳😳😳", { kind: "test" });
|
||||
return pl.emojiAudit("alpha").symbol_only >= 1;
|
||||
})(), JSON.stringify(pl.emojiAudit("alpha")));
|
||||
|
||||
// 動作格:括號的第二格,只放看得見或聽得見的動作
|
||||
console.log("⑥-c 名字後面括號的動作格");
|
||||
check("臉紅這種生理反應算動作", pl.actionLint("臉紅").length === 0);
|
||||
check("別過頭算動作", pl.actionLint("別過頭").length === 0);
|
||||
check("情緒名稱不能當動作(那是前面那一格)",
|
||||
pl.actionLint("害羞").some((i) => i.kind === "action-emotion") &&
|
||||
pl.actionLint("羞愧").some((i) => i.kind === "action-emotion"));
|
||||
check("兩個動作會被擋(一輪的額度只有兩個)",
|
||||
pl.actionLint("別過頭,把杯子推過來").some((i) => i.kind === "action-multi"));
|
||||
check("動作格超過 12 字會被擋",
|
||||
pl.actionLint("把桌上那杯已經冷掉的茶推到他面前").some((i) => i.kind === "action-long"));
|
||||
check("括號兩格:情緒在前、動作在後、用・分隔",
|
||||
pl.roomTag({ emotion: "😳💦", action: "臉紅" }) === "(😳💦・臉紅)");
|
||||
check("只有情緒時括號照舊(現行行為不變)", pl.roomTag({ emotion: "😳" }) === "(😳)");
|
||||
check("只有動作時也成立", pl.roomTag({ action: "別過頭" }) === "(別過頭)");
|
||||
check("兩格都沒有就不出現括號", pl.roomTag({}) === "");
|
||||
check("記憶可以記行為備註(做了什麼,不是感覺到什麼)", (() => {
|
||||
const out = cli(["remember", "--persona", "alpha", "--session", S_SPEAK, "--role", "persona",
|
||||
"--text", "他說我修得比上次穩", "--behavior", "把手縮回來", "--salience", "45"]);
|
||||
const rows = pl.readJsonl(pl.shortTermPath("alpha"));
|
||||
return out.status === 0 && rows[rows.length - 1].behavior === "把手縮回來";
|
||||
})());
|
||||
check("行為備註寫情緒名稱會被擋", (() => {
|
||||
const out = cli(["remember", "--persona", "alpha", "--session", S_SPEAK, "--role", "persona",
|
||||
"--text", "測試行為備註的界線", "--behavior", "覺得害羞"], { expectOk: false });
|
||||
return out.status !== 0 && String(out.stderr).includes("是情緒不是動作");
|
||||
})());
|
||||
|
||||
// 鬧彆扭的階梯:40 分一句帶過、60 分嘴硬三段式、75 分以上惱羞
|
||||
console.log("⑥-d 鬧彆扭的階梯與三段式");
|
||||
{
|
||||
const mkp = (slug, identity, soul = "") => {
|
||||
fs.mkdirSync(path.join(H, slug), { recursive: true });
|
||||
fs.writeFileSync(path.join(H, slug, "IDENTITY.md"), identity);
|
||||
fs.writeFileSync(path.join(H, slug, "SOUL.md"), soul);
|
||||
};
|
||||
// 女性預設 70 就落在嘴硬那一級(60–74),不必再加描述
|
||||
mkp("shyladder", "- Name: 辛\n- Creature: 人類女性\n", "劍術很好,說話簡短。");
|
||||
mkp("shymax", "- Name: 壬\n- Creature: 人類女性\n",
|
||||
"極度怕生,非常容易臉紅,很在意別人的眼光,靦腆、矜持,動不動就會不好意思。");
|
||||
const shy = pl.modestyDirective("shyladder", {});
|
||||
check("嘴硬那一級有三段式的形狀(否認 → 反駁 → 音量掉下來)",
|
||||
shy.includes("先否認") && shy.includes("反駁") && shy.includes("音量掉下來"),
|
||||
`${pl.modestyOf("shyladder").value}|${shy.slice(0, 160)}`);
|
||||
const max = pl.modestyDirective("shymax", {});
|
||||
check("惱羞那一級換成翻臉,而且講明還是兩個動作",
|
||||
pl.modestyOf("shymax").value >= 75 && max.includes("翻臉") && max.includes("還是兩個動作"),
|
||||
`${pl.modestyOf("shymax").value}|${max.slice(0, 160)}`);
|
||||
mkp("tsun", "- Name: 癸\n- Creature: 人類女性\n", "嘴硬、不坦率,明明在意卻死不承認。");
|
||||
mkp("plainf", "- Name: 子\n- Creature: 人類女性\n", "講話直接。");
|
||||
check("傲嬌那一路的詞會推高羞恥敏感度",
|
||||
pl.modestyOf("tsun").value > pl.modestyOf("plainf").value,
|
||||
`${pl.modestyOf("tsun").value} vs ${pl.modestyOf("plainf").value}`);
|
||||
}
|
||||
check("說話規則有注入「話短、日常字、看得見的東西、不要回頭解釋」", (() => {
|
||||
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
||||
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));
|
||||
|
||||
Reference in New Issue
Block a user