Files
mindfulness/设计说明文档/文档换行算法.md
2026-02-10 11:39:33 +08:00

878 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 情绪文案换行算法规范与实现映射说明书 v1.2(裁决补充版,可直接实现)— **实现口径补充 v1.2.1**
> 本文档用于指导 **情绪文案自动换行算法** 的完整实现(可直接据此写代码)。
> 目标不是做语义理解,而是通过 **规则 + 参数 + 确定性搜索与决策**
> 在多语言、多行数、多尺寸App/Widget场景下输出稳定、可控、可解释的换行结果。
>
> **本版本为「裁决补充版 + 实现口径补充」**:已将实现时最容易产生歧义的口径点写入原文合适位置,确保跨端一致。
---
## 0. 快速摘要(给实现者)
你需要实现一个纯函数模块:
* **输入**text、lang、availableWidth、maxLines、fontSpecApp 强烈建议、contextAPP/WIDGET、可选 constraints、可选 overflowMode
* **输出**lines[]<=maxLines、wrappedText、可选 meta
核心流程(必须确定性):
1. Tokenize按语言
2. 生成候选断点 breakpoints控规模
3. 搜索断点组合DP+TopK 或 Beam生成候选 layout
4. 对 layout 做硬约束过滤 + 打分
5. tie-break 固定顺序选最优
6. 溢出/兜底处理 + 打点
---
## 1. 设计目标Why
### 1.1 核心目标
* 控制阅读节奏:让用户“自然停顿”
* 放大情绪关键词:情绪词/短语尽量靠近行尾或独立成行
* 多端稳定App 精确测量、Widget 常量估算也能稳定
* **同输入 → 同输出(确定性)**
* 可缓存:同 key 重复调用不重复计算
* 可治理:兜底/溢出可打点
### 1.2 非目标
* 不做情绪识别 / 语义理解
* 不做通用排版引擎
* 不依赖机器学习
---
## 2. 数据结构与接口What
### 2.1 公开接口(建议)
```ts
wrapText({
text: string,
lang: 'TC' | 'EN',
availableWidth: number,
maxLines: number,
context: 'APP' | 'WIDGET',
fontSpec?: { fontSize: number; fontWeight?: string; fontFamily?: string },
overflowMode?: 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT',
lineMode?: 'AUTO' | 'FIXED',
constraints?: {
protectedPhrases?: string[]; // 尽量不跨行
forbiddenBreakRanges?: Array<{start: number; end: number}>; // token index 区间内禁止断点
},
configVersion?: string,
debug?: boolean
}) => {
lines: string[];
wrappedText: string;
meta?: DebugMeta;
}
```
### 2.2 内部结构(建议)
**Token**
* ENword token保留原始字符串与长度
* TCgrapheme cluster token字符簇避免拆 emoji/ZWJ
```ts
Token {
text: string
type: 'WORD' | 'SPACE' | 'PUNCT' | 'GRAPHEME'
isPunct?: boolean
isEmojiCluster?: boolean
}
```
**Breakpoint**(断点位置是 token 边界)
```ts
Breakpoint {
pos: number; // 断点在 token 边界:切分为 [0..pos) + [pos..]
kind: 'PUNCT' | 'SPACE' | 'BALANCE' | 'SHIFT' | 'ACCUM' | 'SELF' | 'OTHER';
priority: number; // 候选断点生成阶段使用(先截断)
}
```
**LineSegment**(一行)
```ts
LineSegment {
start: number;
end: number;
text: string;
width: number;
tokenCount: number;
charCount: number;
endsWithEmotion?: boolean;
}
```
**Layout候选方案**
```ts
Layout {
breaks: number[]; // 断点序列,例如 [pos1, pos2, ...]
lines: LineSegment[];
score: number;
tieKey?: (number | string)[];
flags: {
emotionSplit?: boolean;
overflowed?: boolean;
fallback?: boolean;
}
scoreBreakdown?: ScoreBreakdown;
}
```
---
## 2.3(新增)索引体系与文本重组(必须定义,确保跨端一致)
> 最常见的实现偏差来自token 是否包含空格、pos 到底切在哪里、以及如何拼回 text。以下规则必须统一。
### 2.3.1 EN 索引约定
* EN token 序列 **只包含 WORD**(标点默认贴附在词内),不生成 SPACE token。
* 断点 `pos` 表示:在第 `pos` 个词 **之前** 断开。
* 行区间 `[start..end)` 表示 `tokens[start] ... tokens[end-1]`
* 文本重组:默认用单空格 `" "` join或使用原始空白映射见 2.3.3)。
**例**`"I am so tired"` → tokens = `[I, am, so, tired]`
* breaks=[2] → lines: `"I am"` / `"so tired"`
### 2.3.1A裁决补充EN 标点处理:极简派(必须)
为保证跨端实现一致EN 标点采用**极简派**约束:
* 所有标点均视为 **词内字符**token.text 的一部分),不额外拆分为独立 token。
例如:
* `"tired."` 是一个 token
* `"Wait..."` 是一个 token
* `"hello—world"` 作为一个 token
* `"don't"` 作为一个 token
* 因此EN 断点仅存在于**词与词之间**,不存在“标点后断点”概念。
* Shift / Accum / Self / EmotionWords 的匹配在 token.text 上执行(可大小写归一),允许被词内标点包裹(如 `"but,"` 仍可视为命中 `"but"`)。
#### 2.3.1A-1实现口径补充EN 关键词命中规则:**全词等值匹配,不做 substring**(必须)
为避免 `"rebuttal"` 被 substring 误命中 `"but"` 等问题EN 命中判断固定为:
*`token.text` 执行:`lowercase → strip 两端常见标点 → 全词等值比较`
* **不允许** substring / contains 式命中。
* “常见标点”建议配置为(可按需扩展,但全端一致):
`, . ! ? : ; " ' … — ( ) [ ] { }`
> 说明:此规则只影响“词库命中判定”,不改变 tokenization仍为极简派
### 2.3.2 TC 索引约定
* TC tokens 为 grapheme clusters。
* 断点 `pos` 表示:在第 `pos` 个 grapheme **之前** 断开。
**例**`"我好累😮‍💨"`
* tokens = `[我, 好, 累, 😮‍💨]`(😮‍💨 为一个 cluster
* breaks=[3] → `"我好累"` / `"😮‍💨"`
### 2.3.3(可选)原始空白保留策略
若产品需要保留输入中的多空格/换行(一般不建议),必须把空白映射作为单独结构:
* `rawSeparators[i]` 表示 `tokens[i]``tokens[i+1]` 之间的原始分隔符。
* 重组时按 `rawSeparators` 拼接。
否则(推荐默认):对输入先做 `normalizeWhitespace`(折叠连续空白为 1 个空格,去首尾空白),并在 meta 里打点 `hadMultiWhitespace`
---
## 3. 宽度测量(必须可实现)
### 3.1 App建议精确测量
实现者需提供一个可插拔的测量函数:
```ts
measureWidth(text: string, fontSpec): number
```
* RN iOS 推荐用原生测量(或 TextLayout 结果)
* 重要:测量时应使用与实际渲染一致的 fontSpec
### 3.2 Widget建议估算/常量)
Widget 场景不强制精确测量,推荐:
* per widgetSize 固定 availableWidth扣 padding
* EN按词数/字符数限制候选搜索范围
* TC候选断点上限更小
> 注意Widget 常量需在 iOS 大版本或版式变更时校准。
---
## 3.3(新增)测量缓存与宽度不可用降级(强烈建议)
### 3.3.1 缓存 Key 规范
为保证性能与确定性,建议至少两级缓存:
1. **字符串测量缓存**`(text, fontSpecKey, contextProfile) -> width`
2. **段落切片缓存**`(start, end) -> width`(对 DP 重复切片测量非常关键)
`fontSpecKey` 必须规范化:例如 `fontFamily|fontWeight|fontSize`
### 3.3.2 宽度不可用(或测量失败)降级
若出现以下任一情况:
* `measureWidth` 不可用
* `measureWidth` 抛错/返回 NaN
* Widget 选择不测量
则进入 `approx mode`
* 把 width 近似替换为EN 用 `wordCount`TC 用 `charCount`
* 强制使用 Widget Beam 搜索策略与更小候选规模
* 打点:`WIDTH_UNKNOWN`
---
## 4. 规则体系Rule System
规则按优先级分四层:
1. **硬约束Hard Constraints**:不满足直接淘汰
2. **情绪规则Emotion Rules**:强惩罚/强奖励
3. **节奏与语义Rhythm/Semantic Heuristics**:中等权重
4. **视觉均衡Visual Balance**:用于多行整体观感
---
## 5. 硬约束Hard Constraints
> 任何候选 layout / line 违反即淘汰
### H1 不超宽
* 对每一行:`line.width <= availableWidth`
### H2 不非法拆分
* EN断点只能在词间
* TC断点只能在 grapheme cluster 边界(不得拆 ZWJ emoji 组合)
### H3 不空行
* `trim(line.text) != ''`
### H4 行内容下限(可降级)
* TC去空格 charCount >= `minCharsPerLineTC`(默认 2
* ENtokenCount >= 1
### H5 最大行数
* layout.lines.length <= maxLines
---
## 5.1(新增)必须禁止的断点(推荐硬约束,提高稳定性)
### H6 禁止“行首标点”TC 强烈建议)
* 若一个断点导致 **下一行第一个 token 为标点**(属于 `tcPunctuations`),则该断点不可用。
### H7 受保护短语内断点可直接剔除(可选优化)
*`constraints.protectedPhrases`:可先计算其 token spanstart/end并在候选断点生成阶段直接过滤 span 内断点。
* 若不做剔除,也必须在评分中施加强惩罚(见 10.2A)。
---
## 6. Tokenize 与断点候选生成
### 6.1 英文 EN
**Tokenize**
* 以空格为分隔
* 标点贴附在词尾(实现可简化)
* 建议先 normalizeWhitespace见 2.3.3
**Breakpoints**
* 在每个“词边界”(词与词之间)产生 breakpoint
* kind=SPACEpriority=基础值
**Widow 辅助信息**
* 标记短词:`len(word) <= widowMaxLen`(默认 3
### 6.2 中文 TC
**Tokenize**
* 按 grapheme cluster 切分(实现可用现成库或平台 API无库时至少保证不拆 surrogate pair + 常见 ZWJ
**Breakpoints 优先级(高→低)**
1. 标点后kind=PUNCTpriority 高)
2. 空格后kind=SPACEpriority 中)
3. 均衡补齐断点kind=BALANCEpriority 低)
**均衡补齐BALANCE**
* 目标:即便无标点,也能在“接近理想位置”的附近有断点
* 计算理想切分点:
* `targetLines = min(maxLines, estimateNaturalLines())`
* `idealCharsPerLine ≈ totalChars / targetLines`
* 对每个理想切分点 i`[idealPos - range, idealPos + range]` 生成少量断点
* range 建议36 个 grapheme
**候选规模上限(必须)**
* TC 候选断点上限:`tcMaxCandidateBreaks`(建议 4080Widget 可更低)
* 截断策略:按 priority标点>空格>补齐) + 距离理想位置近优先
### 6.2A裁决补充BALANCE 断点与情绪短语冲突处理
* BALANCE 断点**允许生成**在 emotionPhrase / protectedPhrases 的 span 内。
* 但这类断点若导致短语被拆分,会在评分阶段触发 **10.2A 的极大惩罚**,从而被自然淘汰。
* 目的:保持候选生成简单、可控,把“是否可用”统一交给评分与搜索层决定(仍保持确定性)。
---
## 6.3新增TC Grapheme Cluster 实现要求(跨端一致性)
为避免 iOS/Android/JS 行为不一致,必须明确实现级别:
### 推荐实现(优先)
* 使用平台级 grapheme segmentation如 ICU / 系统分词器 / JS 端可用 `Intl.Segmenter` 时优先)。
### 最低可用实现(无库兜底)
至少保证以下组合不被拆开:
* surrogate pair代理对
* ZWJ sequence如家庭 emoji
* variation selectorVS16 等)
* skin tone modifier肤色修饰符
* regional indicator flags国旗
### 必须的回归样例(至少覆盖)
* `👨‍👩‍👧‍👦``🇸🇬``👍🏽``😮‍💨``é`(组合字符)
---
## 6.4(新增)候选断点的去重、排序与过滤(必须确定性)
* 去重:同一 `pos` 出现多个 breakpoint 时,保留 priority 更高者(或合并为最高 kind
* 排序:最终 breakpoints 必须按 `pos` 升序排列。
* 过滤:应用 `constraints.forbiddenBreakRanges`可选protected phrase span 过滤。
---
## 7. 情绪规范 → 系统规则映射(参数化实现)
> 这一节将“内容规范”落到系统可执行的参数与规则。
### 7.1 关键词库(配置项)
**情绪短语(强保护)**
* `emotionPhrasesTC[]`
* `emotionPhrasesEN[]`
**情绪转折词Shift**
* TC但 / 可是 / 然而 / 却 / 只是 / 偏偏
* ENbut / yet / soand 为轻量)
**情绪累积词Accum**
* TC已经 / 一直 / 曾经 / 终于 / 还是 / 到现在
* ENalready / still / even / just / really
**自我指向词Self**
* TC你 / 我 / 自己 / 我们 / 别人
* ENyou / yourself / me / we
**情绪词EmotionWords可选**
* TC累 / 痛 / 怕 / 孤单 / 委屈 / 撑 / 崩溃 / 放弃
* ENtired / afraid / lonely / hurt / overwhelmed / give up
> 备注词库不需要完美v1.2 以“少而准”为原则,通过打点迭代。
### 7.2 系统行为映射(实现点)
1. **情绪短语保护**
* 若任何情绪短语被断点拆到不同的行 → layout 加强惩罚(或直接淘汰)
2. **Shift/Accum/Self 断点奖励**
* 若断点在这些词附近(词前或词后,按语言定义) → 给该行/该断点奖励
3. **情绪落点强化**
* 若一行以情绪词结尾(或靠近行尾) → 奖励
* 若情绪词被埋在长行中间 → 惩罚
#### 7.2B(实现口径补充)“靠近行尾”的确定性定义(必须)
为避免实现者自行发挥,“靠近行尾”固定为:
* EN情绪词位于该行 **最后 1 个词**(即行尾词)时视为命中“靠近行尾”
* TC情绪词位于该行 **最后 2 个 grapheme** 范围内时视为命中“靠近行尾”
> 注:此定义只用于 EmotionWord 强化(奖励/惩罚),不影响 tokenization 与断点生成。
### 7.2A裁决补充EN 断点奖励的“行首/行尾感知”定义(必须)
对 EN 的 Shift / Accum / Self 奖励采用 **行首/行尾感知**
* 若某断点 `pos` 使得:
* **新行的第一个词**命中对应词库Shift/Accum/Self则对该断点给予奖励推荐作为主奖励路径
* 或 **上一行的最后一个词**命中对应词库,则也可给予奖励(可与前者相同或略低,但必须固定实现;若未单独配置,默认与前者相同以简化)。
* 命中判断在 token.text 上做确定性匹配(可先做小写化与两端去常见标点)。
---
## 8. 行长度与节奏参数(建议默认值)
### 8.1 中文TC
* `minCharsPerLineTC`: 4弱规则硬约束下限仍为 2
* `idealCharsPerLineTC`: 812用于评分
* `maxCharsPerLineTC`: 16强惩罚如超过可视为必须换行的压力项
### 8.2 英文EN
* `minWordsPerLineEN`: 2弱规则硬约束下限仍为 1
* `idealWordsPerLineEN`: 36
* `maxWordsPerLineEN`: 8强惩罚
> 注:这些不是“硬塞阈值”,而是用于评分与候选裁剪的偏好。
### 8.3(裁决补充)理想长度采用“宽度派”(必须)
为提升视觉一致性idealLen 采用 **宽度派**推导:
* 在 width 可用时:
*`idealWidth = availableWidth * idealWidthRatio`(建议 0.850.95,写入配置)
* 对 EN`idealWordsPerLineEN` 不作为固定常量,而作为弱上界/弱先验;评分中的 ideal 以 **width-based** 的 line.width 与 idealWidth 的距离为主。
* 对 TC`idealCharsPerLineTC` 同理,主要以 **width-based** idealWidth 做评分chars 仅作辅助项(例如 Widget 或 width unknown 时)。
* 在 width unknown / approx mode 时:
* 回退到 chars/words 的 ideal 区间812 / 36
#### 8.3A实现口径补充idealWidthRatio 默认值(必须写死到配置)
为保证首版跨端一致,默认值裁决为:
* `idealWidthRatio.APP = 0.90`
* `idealWidthRatio.WIDGET = 0.95`
> 后续若做 A/B 或灰度调整,请通过 configVersion 管理并打点回溯。
---
## 9. 搜索与组合(可直接实现)
多行换行的核心是“断点组合搜索”。要求:
* 输出最优 layout
* 复杂度可控
* 完全确定性
### 9.1 推荐DP + TopKApp
**状态定义**
* `dp[pos][linesUsed] = TopK layouts ending at token boundary pos`
* pos 是 token 边界索引0..N
**转移**
* 从 (pos, linesUsed) 选择下一个断点 `nextPos` 形成一行 [pos..nextPos)
* 计算 lineText 与 lineWidth
* 若违反硬约束,跳过
* 计算增量评分(见第 10 节)
* 插入 dp[nextPos][linesUsed+1] 的 TopK
**TopK 维护(确定性)**
* K 建议 10
* 排序score desctie-break 使用固定 keys见第 11 节)
* 插入时去重(同 breaks 序列只保留最高分)
**结束条件**
*`pos=N`(结束边界)处,从 `dp[N][<=maxLines]` 选最优
* lineMode=FIXED 时优先选 `dp[N][==maxLines]`,否则降级
### 9.2 推荐Beam SearchWidget
* 每一行扩展时只保留 TopK partial layoutsK 建议 5
* 候选断点也更少(更强裁剪)
**Beam 过程(概念)**
* 初始 beams = [{pos=0, breaks=[], score=0}]
* repeat for lineIndex in 1..maxLines:
* 对每个 beam从 pos 扩展到若干 nextPos生成新 beams
* 过滤硬约束
* 评分
* 全局保留 TopK beams
* 结束时从 beams 中挑 pos==N 最优,否则进入溢出/兜底
---
## 9.3(新增)复杂度与性能预算(建议写进实现约束)
为保证线上性能稳定,建议默认上限:
* App`tcMaxCandidateBreaks <= 80``TopK=10``maxLines<=3`(或 4
* WidgetBeam `K<=5`,每步扩展断点数 `M<=12`
当输入超长时(例如 TC>60 grapheme 或 EN>30 words建议触发候选裁剪或直接走溢出策略并打点 `TOO_LONG`
---
## 10. 评分模型Scoring Model建议实现顺序
> 评分用于在“多个合法断点组合”中选最优。权重固定以保证稳定性。
### 10.1 强烈建议实现顺序
1. 情绪短语拆分惩罚(最高优先)
2. 行超长惩罚 + 理想长度奖励
3. widow 惩罚EN/ 单字行惩罚TC
4. 标点断点奖励TC
5. Shift/Accum/Self 断点奖励
6. 多行视觉均衡(尾行过短惩罚)
#### 10.1A(实现口径补充)评分实现顺序不可重排(必须)
* 所有评分项必须按 10.1 的顺序计算并记录到 breakdown。
* 后续项不得“覆盖”前序裁决结果(例如:已触发 EmotionPhrase 拆分强惩罚后,不允许因为其他奖励而在实现层面跳过/抵消该惩罚的记录)。
* 允许在数学意义上出现“总分被奖励抬高”,但 **必须保留每一项的确定性记录**,以便 debug 与治理。
### 10.2 评分项清单(可直接落地)
**A. 情绪短语保护EmotionPhrase**
* 若拆分任意 emotionPhrase`score -= P_EMOTION_SPLIT`(非常大)
* 若被 constraints.protectedPhrases 拆分:`score -= P_PROTECTED_SPLIT`(非常大)
#### 10.2A实现口径补充Phrase 匹配必须为“连续 token 完全匹配”(必须)
* emotionPhrase / protectedPhrases 的匹配方式固定为:
**连续 token 的完全匹配**EN=连续词序列TC=连续 grapheme 序列),不允许跳 token、不允许跨断点、不允许模糊匹配。
* 若采用预计算 spanspan 的 start/end 必须与 token 索引体系一致(见 2.3)。
### 10.2A裁决补充emotionPhrases 与 protectedPhrases 的冲突优先级
当 emotionPhrase 与 protectedPhrase 发生竞争(无法同时满足)时:
* 两者视为**同级强保护**,在评分上同量级惩罚;
* **tie-break/决策时以“更长者优先”**
* 若某 layout 保住了更长短语(例如 `"真的好累"`)而拆了更短短语(例如 `"好累"`),在同等可行性下应更倾向前者。
* 实现建议在拆分惩罚触发时额外记录被拆分短语的长度token span 长度),用于 tieKey 或额外惩罚的细分(必须确定性)。
**B. Shift/Accum/Self词附近断点奖励**
* 断点在 shiftWord 行首/行尾命中:`score += R_SHIFT_BREAK`
* 断点在 accumWord 行首/行尾命中:`score += R_ACCUM_BREAK`
* 断点在 selfWord 行首/行尾命中:`score += R_SELF_BREAK`
**C. 行长度Length**
* `score += f_ideal(lineLen, idealLen)`(越接近越好)
* 若 lineLen > maxLen`score -= P_OVER_MAXLEN * (lineLen - maxLen)`
* 若 lineLen < minPreferred`score -= P_TOO_SHORT * (minPreferred - lineLen)`
> f_ideal 可用简单的:`-abs(lineLen - idealLen)`
**D. 标点断点TC**
* 若断点位于 PUNCT 后:`score += R_PUNCT_BREAK`
* 若行首为标点:淘汰(建议见 H6或极大惩罚
**E. WidowEN与尾行过短All**
* EN最后一行只有 1 个词:`score -= P_WIDOW_LINE`
* EN最后一行只有 1 个短词:`score -= P_WIDOW_WORD`
* All最后一行宽度 < 某阈值(例如 idealWidth*0.5`score -= P_SHORT_LASTLINE`
**F. TC 助词孤立TC**
* 行首/行尾为助词:`score -= P_PARTICLE_ISO`
* 注:语尾语助词(啊/喔/呢/啦)可通过例外白名单降惩罚
### 10.2F裁决补充TC 语尾语助词白名单:惩罚减半
* 对白名单中的语尾语助词(如:啊/喔/呢/啦 等):
* 若触发 PARTICLE_ISO行首/行尾助词),其惩罚使用:
**`P_PARTICLE_ISO / 2`**
* 非白名单助词仍使用完整 `P_PARTICLE_ISO`
* 白名单列表必须写入配置并全端一致。
### 10.2G裁决补充EmotionWord 与 Accum 奖励关系(必须)
为避免奖励叠加导致评分失真,奖励关系固定为:
* **EmotionWord > Accum**
* 若同一行(或同一断点)同时满足 EmotionWord 强化与 Accum 断点奖励:
* EmotionWord 相关奖励保持原值
* Accum 相关奖励按 **0.5 倍**计算(即“后者减半”)
* 该规则必须确定性实现(例如先计算 EmotionWord 命中,再对 Accum 奖励做折减)。
### 10.3 建议的默认权重(仅供实现起步)
> 你可以先用相对大小,不必精确数值。
* P_EMOTION_SPLIT10000
* P_PROTECTED_SPLIT10000
* P_WIDOW_WORD800
* P_WIDOW_LINE500
* P_SHORT_LASTLINE300
* P_PARTICLE_ISO200
* R_PUNCT_BREAK80
* R_SHIFT_BREAK60
* R_ACCUM_BREAK40
* R_SELF_BREAK20
* P_OVER_MAXLEN30
* P_TOO_SHORT10
---
## 10.4新增评分可解释结构Debug/治理必备)
为便于线上治理与调参,建议统一输出 score breakdown
```ts
ScoreBreakdown = {
total: number,
terms: Array<{ key: string; delta: number; detail?: any }>
}
```
* `key` 建议枚举:`EMOTION_SPLIT | PROTECTED_SPLIT | OVER_MAXLEN | IDEAL_LEN | TOO_SHORT | PUNCT_BREAK | SHIFT_BREAK | ACCUM_BREAK | SELF_BREAK | WIDOW_LINE | WIDOW_WORD | SHORT_LASTLINE | PARTICLE_ISO`
* debug=true 时可只输出贡献最大的前 3 项。
### 10.4A裁决补充Debug Top-3 输出排序:按规则优先级
debug=true 时输出的“关键项”排序采用**规则优先级**而非 |delta|
* 按 10.1 的实现顺序(优先级)输出
* 若同优先级内有多项,可再按 |delta| 或出现顺序稳定排序(必须确定性)
---
## 11. Tie-break必须固定确保确定性
当 score 相同或非常接近时,按顺序比较:
1. emotionSplit=false 优先
2. overflowed=false 优先
3. lastLineWidth 更大优先(避免短尾行)
4. 行宽分布更均匀优先(可用 maxWidth-minWidth 更小)
5. 断点更接近理想切分点优先(距离之和更小)
6. 断点序列字典序更靠前优先(例如 firstBreak 更小)
> 实现建议:为 Layout 计算一个 `tieKey` 数组,逐项比较。
### 11.0A裁决补充breaks[] 字典序比较定义(必须)
断点序列字典序比较规则固定为:
*`index=0` 起逐项比较 `breaks[i]`
* 首个不同元素更小者视为更小
* 若公共前缀完全相同,则:
* **更短的数组**视为更小(短数组在公共前缀相等时优先)
---
## 11.1(新增)分数精度与浮点处理(跨端一致性强烈建议)
* 建议所有评分项都用 **整数**,避免浮点误差。
* 若必须使用浮点,必须定义 `EPS`:例如 `EPS=1e-6`
* “非常接近”的定义:`abs(a-b) <= EPS`
---
## 12. 溢出与兜底(必须定义清楚)
### 12.1 overflowMode
* `ELLIPSIS`最后一行加省略号Widget 推荐默认)
* `CLIP`:截断到 maxLines
* `SYSTEM_DEFAULT`:不插入换行,交给系统
### 12.2 何时算 overflow
* 搜索无法在 <=maxLines 覆盖到文本结束边界 N
* 或 lineMode=FIXED 需要刚好 maxLines但无法到 N
### 12.3 ELLIPSIS 的实现约束
* 仅在最后一行处理
* 必须再次检查 H1加省略号后是否超宽
* 若超宽:优先从最后一行尾部移除 token 再加省略号
* 尽量避免把 emotionPhrase 截断在中间:
* 若会截断,可选择整段前移或整段省略(以评分决定)
### 12.4 无解兜底
降级链:
1. 保证 H1/H2/H3
2. 将 H4/助词/widow 从强规则降为弱惩罚
3. 若仍无解SYSTEM_DEFAULT
### 12.4A裁决补充SYSTEM_DEFAULT 的返回语义(必须)
当选择 `SYSTEM_DEFAULT` 时:
* wrapText **仍返回 lines / wrappedText可为未换行的单行或原始文本**
* 并且 **仅在 meta 中标记**`fallback_type = SYSTEM_DEFAULT`
* 外部组件UI 层)依据该 meta 决定是否完全交给系统排版(例如不插入 `\n`,或忽略 lines 渲染)。
> 关键点:算法层不擅自改变 UI 行为,只提供明确标记,保证可治理与跨端一致。
---
## 12.5新增ELLIPSIS 细则(必须明确字符与清理规则)
* 省略号字符统一:`ellipsisToken = "…"`(推荐单字符)或 `"..."`(三字符),必须写入配置并全端一致。
* EN 截断回退单位:**整词**TC 回退单位:**grapheme**。
* 清理规则:
* 不允许输出 `" …"`(空格+省略号)
* 不允许输出 `",…"``"。…"`(标点+省略号)时可按配置决定是否移除末尾标点再加省略号
* 加省略号必须重新测量宽度,确保不超宽。
---
## 13. 受控人工约束(可选,但建议实现)
### 13.1 protectedPhrases
* 行为:拆分该短语 → 视同 emotionPhrase 拆分(强惩罚)
### 13.2 forbiddenBreakRanges
* 行为:断点 pos 落在 [start,end] 内 → 候选断点直接剔除
> 注意start/end 的单位应与 token 索引一致EN=词边界TC=字符簇边界)。
---
## 14. Debug Meta 与打点(强烈建议)
### 14.1 debug meta可选输出
* chosen breaks
* score breakdown可只输出前 3 个最关键项;排序见 10.4A
* fallback/overflow 原因
### 14.2 线上打点建议
* fallback_typeNONE / RELAX_RULES / SYSTEM_DEFAULT
* overflow_typeNONE / ELLIPSIS / CLIP
* reasonNO_CANDIDATE / WIDTH_UNKNOWN / WIDOW / PARTICLE / TOO_LONG
* lang/context/maxLines/widgetSize
---
## 15. 实现步骤(建议落地路线)
1. EN tokenize + SPACE breakpoints + 2 行先跑通EN 标点按极简派;关键词命中按 2.3.1A-1
2. TC tokenizegrapheme+ PUNCT breakpoints + 2 行跑通
3. 引入 emotionPhrases 拆分惩罚phrase 匹配按 10.2A
4. 引入 DP + TopK多行
5. 引入 overflowModeWidget 先用 ELLIPSIS
6. 引入 constraints + 打点
7. 调权重与扩词库(基于数据)
---
## 16. 附录:最小配置清单(实现时至少要有)
* emotionPhrasesTC / emotionPhrasesEN
* tcPunctuations
* tcParticles含白名单子集或单独 tcParticleWhitelist
* widowMaxLen
* tcMaxCandidateBreaks
* minCharsPerLineTC
* widgetProfilesWidgetavailableWidth/maxLines/overflowMode/beamK
* ellipsisToken新增
* EPS若使用浮点新增
* idealWidthRatio新增宽度派必需默认值见 8.3A
---
## 16.1(新增)金标测试用例与回归策略(强烈建议)
为保证“确定性 + 可治理”,建议维护一份 Golden Cases
* 每种语言至少 20 个样例(短、长、含标点、无标点、含 emoji、含 protectedPhrases、含 shift/accum/self、极窄宽度
* 每个样例包含:
* input text
* lang/context
* availableWidth/maxLines/fontSpec或 widget profile
* expected lines[]
再加 3 类性质测试property tests
1. **同输入多次调用输出一致**
2. **availableWidth 变小不会让任一行变得更宽**(近似单调性检查)
3. **maxLines 增加时不应更差**(至少不从可解变 overflow
---
## 16.2(新增)配置版本治理(建议)
* `configVersion` 应代表“可灰度的规则包版本”。
* meta 与线上打点中必须带上 `configVersion`,以便回溯与对比实验。
---
## 17. 结语
这套算法的目标不是理解情绪,而是:
> 在规模化系统中,持续做出「像人一样停顿」的选择。
只要:确定性 + 可配置 + 可治理,你就能持续把“情绪表达”变成产品护城河。