feat(標點預算): 情緒決定標點分布,上限夾死免得變成強度計
只有文字的話標點就是語調:同一句「我知道了」,`我知道了。` 與 `我、我知道了⋯`
是兩種情緒,差別全在標點不在用詞。
PUNCTUATION_BUDGET 只寫拍板指名的四種(焦慮/憤怒/害羞/喜悅)。
其餘八種不配標點——硬給每種情緒配一套會讓標點變成裝飾,沒把握的就不編。
punctuationBudget() 每輪只回一種,門檻跟破口同一條線(40)。兩種情緒各配一套
會互相打架:憤怒幾乎不用問號、焦慮要多斷句,混起來不是複雜的情緒,是標點亂撒。
上限:MAX_EXCLAIM = 1、MAX_ELLIPSIS = 3,另擋 […⋯]{3,} 的連發
(中文刪節號本來就常寫兩個,所以三個起才算)。不夾死的話它會變成 emoji 的
替代品——情緒不夠就多打幾個驚嘆號。界線也寫進注入文字:標點管分布、強度歸 emoji。
selftest 711 → 721。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,10 +100,16 @@ Q5–Q10 六題已於 2026/08/03 由使用者拍板,設計不再需要確認
|
|||||||
—— `emojiAudit()` 掛在 `emotion --audit`,另外算「只有符號沒有句子」那幾則(那才是真的退化)
|
—— `emojiAudit()` 掛在 `emotion --audit`,另外算「只有符號沒有句子」那幾則(那才是真的退化)
|
||||||
- 驗收:羞恥度 45/70/90 三種輸出的 emoji 不同;把所有 emoji 刪掉之後,句子的形狀仍然分得出是哪一級
|
- 驗收:羞恥度 45/70/90 三種輸出的 emoji 不同;把所有 emoji 刪掉之後,句子的形狀仍然分得出是哪一級
|
||||||
|
|
||||||
- [ ] **5.2 標點預算(情緒 → 標點與句長的分布)**
|
- [x] **5.2 標點預算(情緒 → 標點與句長的分布)**(2026/08/03 16:21:13 完成)
|
||||||
- [ ] 把標點納入 `speechLint()` 的可調參數:焦慮 → 刪節號與逗號斷句變多;憤怒 → 句號密度上升、幾乎不用問號;
|
- [x] 把標點納入 `speechLint()` 的可調參數:焦慮 → 刪節號與逗號斷句變多;憤怒 → 句號密度上升、幾乎不用問號;
|
||||||
害羞 → 疊字與破折;喜悅 → 允許一個驚嘆號
|
害羞 → 疊字與破折;喜悅 → 允許一個驚嘆號
|
||||||
- [ ] 上限夾死(一則最多一個驚嘆號、刪節號不連發),否則會變成顏文字的替代品
|
—— `PUNCTUATION_BUDGET`(**只寫拍板指名的這四種**,其餘不硬配,
|
||||||
|
免得標點變成裝飾)+ `punctuationBudget()` 每輪注入,門檻與破口同一條線(40)。
|
||||||
|
**只回一種**:兩種情緒各配一套標點會互相打架(憤怒不用問號、焦慮要斷句)
|
||||||
|
- [x] 上限夾死(一則最多一個驚嘆號、刪節號不連發),否則會變成顏文字的替代品
|
||||||
|
—— `MAX_EXCLAIM = 1`、`MAX_ELLIPSIS = 3`,另擋 `[…⋯]{3,}` 的連發
|
||||||
|
(中文刪節號常寫兩個,所以三個起才算連發)。
|
||||||
|
界線寫進注入文字:**標點管分布、強度歸 emoji**
|
||||||
- 驗收:同一句話在四種情緒下輸出,標點分布看得出差別,且都沒有過量
|
- 驗收:同一句話在四種情緒下輸出,標點分布看得出差別,且都沒有過量
|
||||||
|
|
||||||
- [ ] **5.1 訊息的節奏:一輪可以拆成兩則(純文字唯一的「時間」訊號)**
|
- [ ] **5.1 訊息的節奏:一輪可以拆成兩則(純文字唯一的「時間」訊號)**
|
||||||
|
|||||||
@@ -905,6 +905,44 @@ export function emotionTells(slug, state = null, { min = 40, top = 2 } = {}) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 標點預算:情緒 → 標點與句長的分布。
|
||||||
|
*
|
||||||
|
* 只有文字的話,標點就是語調。同一句「我知道了」,`我知道了。` 與 `我、我知道了⋯`
|
||||||
|
* 是兩種情緒——差別全在標點,不在用詞。
|
||||||
|
*
|
||||||
|
* 這裡只寫四種(拍板時指名的那四種)。其餘情緒不給標點指示:沒有把握的就不編,
|
||||||
|
* 硬給每一種情緒配一套標點會讓它變成裝飾。
|
||||||
|
*/
|
||||||
|
export const PUNCTUATION_BUDGET = {
|
||||||
|
anxiety: "刪節號與逗號斷句變多,句子常常沒說完(「我、我知道⋯」)",
|
||||||
|
anger: "句號密度上升、句子切短;幾乎不用問號——問句是留餘地,生氣的人不留",
|
||||||
|
shame: "疊字與破折(「才、才不是」「你——不要看這邊」),話講一半就收",
|
||||||
|
joy: "可以用一個驚嘆號,句子輕、節奏快",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 硬上限:一則最多一個驚嘆號、刪節號不連發。 */
|
||||||
|
export const MAX_EXCLAIM = 1;
|
||||||
|
export const MAX_ELLIPSIS = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 這一輪的標點傾向:取主導情緒裡強度夠、而且**有寫標點指示**的那一種。
|
||||||
|
*
|
||||||
|
* 只回一種。兩種情緒各配一套標點會互相打架(憤怒不用問號、焦慮要斷句),
|
||||||
|
* 混起來的結果不是「複雜的情緒」,是標點亂撒。
|
||||||
|
*/
|
||||||
|
export function punctuationBudget(slug, state = null, { min = 40 } = {}) {
|
||||||
|
const st = decayEmotion(structuredClone(state ?? loadEmotion(slug)));
|
||||||
|
const hit = dominant(st, 4).find(({ key, level }) => level >= min && PUNCTUATION_BUDGET[key]);
|
||||||
|
if (!hit) return null;
|
||||||
|
return {
|
||||||
|
key: hit.key,
|
||||||
|
zh: EMOTIONS[hit.key].zh,
|
||||||
|
level: Math.round(hit.level),
|
||||||
|
hint: PUNCTUATION_BUDGET[hit.key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// 情緒 → emoji(種類表情緒、數量表程度)
|
// 情緒 → emoji(種類表情緒、數量表程度)
|
||||||
//
|
//
|
||||||
@@ -3163,6 +3201,18 @@ export function speechLint(text) {
|
|||||||
if (emoji.length > EMOJI_HINT_AT) issues.push({ kind: "emoji", level: "hint", count: emoji.length });
|
if (emoji.length > EMOJI_HINT_AT) issues.push({ kind: "emoji", level: "hint", count: emoji.length });
|
||||||
const dashes = (raw.match(/——/g) || []).length;
|
const dashes = (raw.match(/——/g) || []).length;
|
||||||
if (dashes > 1) issues.push({ kind: "dash", level: "block", count: dashes });
|
if (dashes > 1) issues.push({ kind: "dash", level: "block", count: dashes });
|
||||||
|
// 標點預算的上限要夾死,否則它會變成 emoji 的替代品:情緒不夠就多打幾個驚嘆號。
|
||||||
|
// 標點是**分布**,不是強度計——強度歸 emoji(那邊才是開放的)。
|
||||||
|
const exclaims = (body.match(/!/g) || []).length;
|
||||||
|
if (exclaims > MAX_EXCLAIM) {
|
||||||
|
issues.push({ kind: "exclaim", level: "block", count: exclaims, max: MAX_EXCLAIM });
|
||||||
|
}
|
||||||
|
// 連發:`⋯⋯⋯` 以上。中文的刪節號本來就常寫成兩個,所以三個起才算。
|
||||||
|
if (/[…⋯]{3,}/.test(raw)) issues.push({ kind: "ellipsis-run", level: "block" });
|
||||||
|
const ellipses = (body.match(/[…⋯]/g) || []).length;
|
||||||
|
if (ellipses > MAX_ELLIPSIS) {
|
||||||
|
issues.push({ kind: "ellipsis", level: "block", count: ellipses, max: MAX_ELLIPSIS });
|
||||||
|
}
|
||||||
if (/\*\*|(^|\n)\s*[-*+]\s|(^|\n)#{1,6}\s/.test(raw)) issues.push({ kind: "markdown", level: "block" });
|
if (/\*\*|(^|\n)\s*[-*+]\s|(^|\n)#{1,6}\s/.test(raw)) issues.push({ kind: "markdown", level: "block" });
|
||||||
const negParallel = (body.match(/不是[^,。!?]{1,20}而是|不只是[^,。!?]{1,20}更是|不僅[^,。!?]{1,20}更/g) || []).length;
|
const negParallel = (body.match(/不是[^,。!?]{1,20}而是|不只是[^,。!?]{1,20}更是|不僅[^,。!?]{1,20}更/g) || []).length;
|
||||||
if (negParallel > 1) issues.push({ kind: "parallel", level: "block", count: negParallel });
|
if (negParallel > 1) issues.push({ kind: "parallel", level: "block", count: negParallel });
|
||||||
@@ -3192,6 +3242,12 @@ export function speechLintMessage(issue) {
|
|||||||
return `這一則有 ${issue.count} 個 emoji——沒有上限,但看一下是不是在拿符號代替句子`;
|
return `這一則有 ${issue.count} 個 emoji——沒有上限,但看一下是不是在拿符號代替句子`;
|
||||||
case "dash":
|
case "dash":
|
||||||
return `${issue.count} 個破折號——一輪留一個,其他改成逗號或句號`;
|
return `${issue.count} 個破折號——一輪留一個,其他改成逗號或句號`;
|
||||||
|
case "exclaim":
|
||||||
|
return `${issue.count} 個驚嘆號,上限 ${issue.max}——情緒的強度歸 emoji,標點只管分布`;
|
||||||
|
case "ellipsis-run":
|
||||||
|
return "刪節號連發了(`⋯⋯⋯`)——最多兩個,話講一半的效果靠句子斷掉,不靠符號拉長";
|
||||||
|
case "ellipsis":
|
||||||
|
return `${issue.count} 個刪節號,上限 ${issue.max}——再多就變成語氣的裝飾了`;
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return "有粗體、清單或標題——講話的人不會排版,收掉";
|
return "有粗體、清單或標題——講話的人不會排版,收掉";
|
||||||
case "parallel":
|
case "parallel":
|
||||||
@@ -5712,6 +5768,15 @@ export function turnContext(slug, sessionId, prompt = "") {
|
|||||||
"強度不到就不演。",
|
"強度不到就不演。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 標點預算:只有文字的話,標點就是語調。跟破口同一條門檻(強度不到就不調)。
|
||||||
|
const punct = punctuationBudget(slug, state);
|
||||||
|
if (punct) {
|
||||||
|
lines.push(
|
||||||
|
`此刻的標點:${punct.zh} ${punct.level} → ${punct.hint}。` +
|
||||||
|
`上限夾死——一則最多 ${MAX_EXCLAIM} 個驚嘆號、刪節號不連發(最多兩個)。` +
|
||||||
|
"標點管的是**分布**,強度歸 emoji;用標點加強度會變成用符號代替情緒。",
|
||||||
|
);
|
||||||
|
}
|
||||||
// 情緒的 emoji:種類表情緒、數量表程度。算出來的,不要自己挑
|
// 情緒的 emoji:種類表情緒、數量表程度。算出來的,不要自己挑
|
||||||
const nowEmoji = emotionEmojiNow(slug, state);
|
const nowEmoji = emotionEmojiNow(slug, state);
|
||||||
if (nowEmoji) {
|
if (nowEmoji) {
|
||||||
|
|||||||
@@ -1203,6 +1203,34 @@ console.log("⑥-d 鬧彆扭的階梯與三段式");
|
|||||||
`${pl.modestyOf("ownsulk").value}|${ownShape.slice(0, 220)}`);
|
`${pl.modestyOf("ownsulk").value}|${ownShape.slice(0, 220)}`);
|
||||||
check("沒寫覆寫的人格照舊用預設模板", pl.personaSulk("shyladder").length === 0);
|
check("沒寫覆寫的人格照舊用預設模板", pl.personaSulk("shyladder").length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 標點預算:情緒 → 標點與句長的分布。上限夾死,否則變成 emoji 的替代品
|
||||||
|
console.log("⑥-e 標點預算");
|
||||||
|
check("拍板的四種情緒都有標點指示,而且四種不一樣", (() => {
|
||||||
|
const keys = ["anxiety", "anger", "shame", "joy"];
|
||||||
|
const hints = keys.map((k) => pl.PUNCTUATION_BUDGET[k]);
|
||||||
|
return hints.every(Boolean) && new Set(hints).size === 4;
|
||||||
|
})(), JSON.stringify(pl.PUNCTUATION_BUDGET));
|
||||||
|
check("沒把握的情緒就不配標點(不硬給十二種)",
|
||||||
|
Object.keys(pl.PUNCTUATION_BUDGET).length === 4, JSON.stringify(Object.keys(pl.PUNCTUATION_BUDGET)));
|
||||||
|
check("一個驚嘆號可以", pl.speechBlockers("太好了!").length === 0);
|
||||||
|
check("兩個驚嘆號會被擋(強度歸 emoji,不歸標點)",
|
||||||
|
pl.speechBlockers("太好了!真的!").some((i) => i.kind === "exclaim"));
|
||||||
|
check("刪節號連發會被擋", pl.speechBlockers("我知道⋯⋯⋯").some((i) => i.kind === "ellipsis-run"));
|
||||||
|
check("兩個刪節號是正常的中文寫法,不擋",
|
||||||
|
pl.speechBlockers("我知道⋯⋯").every((i) => !String(i.kind).startsWith("ellipsis")));
|
||||||
|
check("刪節號整則超過上限會被擋",
|
||||||
|
pl.speechBlockers("我⋯不知道⋯你說⋯這樣⋯").some((i) => i.kind === "ellipsis"));
|
||||||
|
check("標點預算取主導情緒,而且只回一種(兩種會互相打架)", (() => {
|
||||||
|
const p = pl.punctuationBudget("alpha");
|
||||||
|
return p !== null && typeof p.hint === "string" && typeof p.key === "string" && p.level >= 40;
|
||||||
|
})(), JSON.stringify(pl.punctuationBudget("alpha")));
|
||||||
|
check("強度不到就不調標點(跟破口同一條線)",
|
||||||
|
pl.punctuationBudget("alpha", null, { min: 99 }) === null);
|
||||||
|
check("標點指示有注入,而且講明上限與「強度歸 emoji」", (() => {
|
||||||
|
const ctxP = pl.turnContext("alpha", S_SPEAK, "那件事後來怎麼了");
|
||||||
|
return ctxP.includes("此刻的標點") && ctxP.includes("刪節號不連發") && ctxP.includes("強度歸 emoji");
|
||||||
|
})(), pl.turnContext("alpha", S_SPEAK, "那件事後來怎麼了").slice(0, 500));
|
||||||
check("說話規則有注入「話短、日常字、看得見的東西、不要回頭解釋」", (() => {
|
check("說話規則有注入「話短、日常字、看得見的東西、不要回頭解釋」", (() => {
|
||||||
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
||||||
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));
|
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));
|
||||||
|
|||||||
Reference in New Issue
Block a user