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:
@@ -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(種類表情緒、數量表程度)
|
||||
//
|
||||
@@ -3163,6 +3201,18 @@ export function speechLint(text) {
|
||||
if (emoji.length > EMOJI_HINT_AT) issues.push({ kind: "emoji", level: "hint", count: emoji.length });
|
||||
const dashes = (raw.match(/——/g) || []).length;
|
||||
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" });
|
||||
const negParallel = (body.match(/不是[^,。!?]{1,20}而是|不只是[^,。!?]{1,20}更是|不僅[^,。!?]{1,20}更/g) || []).length;
|
||||
if (negParallel > 1) issues.push({ kind: "parallel", level: "block", count: negParallel });
|
||||
@@ -3192,6 +3242,12 @@ export function speechLintMessage(issue) {
|
||||
return `這一則有 ${issue.count} 個 emoji——沒有上限,但看一下是不是在拿符號代替句子`;
|
||||
case "dash":
|
||||
return `${issue.count} 個破折號——一輪留一個,其他改成逗號或句號`;
|
||||
case "exclaim":
|
||||
return `${issue.count} 個驚嘆號,上限 ${issue.max}——情緒的強度歸 emoji,標點只管分布`;
|
||||
case "ellipsis-run":
|
||||
return "刪節號連發了(`⋯⋯⋯`)——最多兩個,話講一半的效果靠句子斷掉,不靠符號拉長";
|
||||
case "ellipsis":
|
||||
return `${issue.count} 個刪節號,上限 ${issue.max}——再多就變成語氣的裝飾了`;
|
||||
case "markdown":
|
||||
return "有粗體、清單或標題——講話的人不會排版,收掉";
|
||||
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:種類表情緒、數量表程度。算出來的,不要自己挑
|
||||
const nowEmoji = emotionEmojiNow(slug, state);
|
||||
if (nowEmoji) {
|
||||
|
||||
Reference in New Issue
Block a user