feat(記憶與目標): 專案目標三條 + 固化候選不再重算(0.2.1) #17
@@ -100,10 +100,16 @@ Q5–Q10 六題已於 2026/08/03 由使用者拍板,設計不再需要確認
|
||||
—— `emojiAudit()` 掛在 `emotion --audit`,另外算「只有符號沒有句子」那幾則(那才是真的退化)
|
||||
- 驗收:羞恥度 45/70/90 三種輸出的 emoji 不同;把所有 emoji 刪掉之後,句子的形狀仍然分得出是哪一級
|
||||
|
||||
- [ ] **5.2 標點預算(情緒 → 標點與句長的分布)**
|
||||
- [ ] 把標點納入 `speechLint()` 的可調參數:焦慮 → 刪節號與逗號斷句變多;憤怒 → 句號密度上升、幾乎不用問號;
|
||||
- [x] **5.2 標點預算(情緒 → 標點與句長的分布)**(2026/08/03 16:21:13 完成)
|
||||
- [x] 把標點納入 `speechLint()` 的可調參數:焦慮 → 刪節號與逗號斷句變多;憤怒 → 句號密度上升、幾乎不用問號;
|
||||
害羞 → 疊字與破折;喜悅 → 允許一個驚嘆號
|
||||
- [ ] 上限夾死(一則最多一個驚嘆號、刪節號不連發),否則會變成顏文字的替代品
|
||||
—— `PUNCTUATION_BUDGET`(**只寫拍板指名的這四種**,其餘不硬配,
|
||||
免得標點變成裝飾)+ `punctuationBudget()` 每輪注入,門檻與破口同一條線(40)。
|
||||
**只回一種**:兩種情緒各配一套標點會互相打架(憤怒不用問號、焦慮要斷句)
|
||||
- [x] 上限夾死(一則最多一個驚嘆號、刪節號不連發),否則會變成顏文字的替代品
|
||||
—— `MAX_EXCLAIM = 1`、`MAX_ELLIPSIS = 3`,另擋 `[…⋯]{3,}` 的連發
|
||||
(中文刪節號常寫兩個,所以三個起才算連發)。
|
||||
界線寫進注入文字:**標點管分布、強度歸 emoji**
|
||||
- 驗收:同一句話在四種情緒下輸出,標點分布看得出差別,且都沒有過量
|
||||
|
||||
- [ ] **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(種類表情緒、數量表程度)
|
||||
//
|
||||
@@ -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) {
|
||||
|
||||
@@ -1203,6 +1203,34 @@ console.log("⑥-d 鬧彆扭的階梯與三段式");
|
||||
`${pl.modestyOf("ownsulk").value}|${ownShape.slice(0, 220)}`);
|
||||
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("說話規則有注入「話短、日常字、看得見的東西、不要回頭解釋」", (() => {
|
||||
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
||||
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));
|
||||
|
||||
Reference in New Issue
Block a user