878 lines
28 KiB
Markdown
878 lines
28 KiB
Markdown
# 情绪文案换行算法规范与实现映射说明书 v1.2(裁决补充版,可直接实现)— **实现口径补充 v1.2.1**
|
||
|
||
> 本文档用于指导 **情绪文案自动换行算法** 的完整实现(可直接据此写代码)。
|
||
> 目标不是做语义理解,而是通过 **规则 + 参数 + 确定性搜索与决策**,
|
||
> 在多语言、多行数、多尺寸(App/Widget)场景下,输出稳定、可控、可解释的换行结果。
|
||
>
|
||
> **本版本为「裁决补充版 + 实现口径补充」**:已将实现时最容易产生歧义的口径点写入原文合适位置,确保跨端一致。
|
||
|
||
---
|
||
|
||
## 0. 快速摘要(给实现者)
|
||
|
||
你需要实现一个纯函数模块:
|
||
|
||
* **输入**:text、lang、availableWidth、maxLines、fontSpec(App 强烈建议)、context(APP/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**
|
||
|
||
* EN:word token(保留原始字符串与长度)
|
||
* TC:grapheme 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)
|
||
* EN:tokenCount >= 1(词)
|
||
|
||
### H5 最大行数
|
||
|
||
* layout.lines.length <= maxLines
|
||
|
||
---
|
||
|
||
## 5.1(新增)必须禁止的断点(推荐硬约束,提高稳定性)
|
||
|
||
### H6 禁止“行首标点”(TC 强烈建议)
|
||
|
||
* 若一个断点导致 **下一行第一个 token 为标点**(属于 `tcPunctuations`),则该断点不可用。
|
||
|
||
### H7 受保护短语内断点可直接剔除(可选优化)
|
||
|
||
* 对 `constraints.protectedPhrases`:可先计算其 token span(start/end),并在候选断点生成阶段直接过滤 span 内断点。
|
||
* 若不做剔除,也必须在评分中施加强惩罚(见 10.2A)。
|
||
|
||
---
|
||
|
||
## 6. Tokenize 与断点候选生成
|
||
|
||
### 6.1 英文 EN
|
||
|
||
**Tokenize**
|
||
|
||
* 以空格为分隔
|
||
* 标点贴附在词尾(实现可简化)
|
||
* 建议先 normalizeWhitespace(见 2.3.3)
|
||
|
||
**Breakpoints**
|
||
|
||
* 在每个“词边界”(词与词之间)产生 breakpoint
|
||
* kind=SPACE,priority=基础值
|
||
|
||
**Widow 辅助信息**
|
||
|
||
* 标记短词:`len(word) <= widowMaxLen`(默认 3)
|
||
|
||
### 6.2 中文 TC
|
||
|
||
**Tokenize**
|
||
|
||
* 按 grapheme cluster 切分(实现可用现成库或平台 API;无库时至少保证不拆 surrogate pair + 常见 ZWJ)
|
||
|
||
**Breakpoints 优先级(高→低)**
|
||
|
||
1. 标点后(kind=PUNCT,priority 高)
|
||
2. 空格后(kind=SPACE,priority 中)
|
||
3. 均衡补齐断点(kind=BALANCE,priority 低)
|
||
|
||
**均衡补齐(BALANCE)**
|
||
|
||
* 目标:即便无标点,也能在“接近理想位置”的附近有断点
|
||
|
||
* 计算理想切分点:
|
||
|
||
* `targetLines = min(maxLines, estimateNaturalLines())`
|
||
* `idealCharsPerLine ≈ totalChars / targetLines`
|
||
|
||
* 对每个理想切分点 i:在 `[idealPos - range, idealPos + range]` 生成少量断点
|
||
|
||
* range 建议:3~6 个 grapheme
|
||
|
||
**候选规模上限(必须)**
|
||
|
||
* TC 候选断点上限:`tcMaxCandidateBreaks`(建议 40~80,Widget 可更低)
|
||
* 截断策略:按 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 selector(VS16 等)
|
||
* 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:但 / 可是 / 然而 / 却 / 只是 / 偏偏
|
||
* EN:but / yet / so(and 为轻量)
|
||
|
||
**情绪累积词(Accum)**
|
||
|
||
* TC:已经 / 一直 / 曾经 / 终于 / 还是 / 到现在
|
||
* EN:already / still / even / just / really
|
||
|
||
**自我指向词(Self)**
|
||
|
||
* TC:你 / 我 / 自己 / 我们 / 别人
|
||
* EN:you / yourself / me / we
|
||
|
||
**情绪词(EmotionWords,可选)**
|
||
|
||
* TC:累 / 痛 / 怕 / 孤单 / 委屈 / 撑 / 崩溃 / 放弃
|
||
* EN:tired / 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`: 8~12(用于评分)
|
||
* `maxCharsPerLineTC`: 16(强惩罚;如超过可视为必须换行的压力项)
|
||
|
||
### 8.2 英文(EN)
|
||
|
||
* `minWordsPerLineEN`: 2(弱规则;硬约束下限仍为 1)
|
||
* `idealWordsPerLineEN`: 3~6
|
||
* `maxWordsPerLineEN`: 8(强惩罚)
|
||
|
||
> 注:这些不是“硬塞阈值”,而是用于评分与候选裁剪的偏好。
|
||
|
||
### 8.3(裁决补充)理想长度采用“宽度派”(必须)
|
||
|
||
为提升视觉一致性,idealLen 采用 **宽度派**推导:
|
||
|
||
* 在 width 可用时:
|
||
|
||
* 以 `idealWidth = availableWidth * idealWidthRatio`(建议 0.85~0.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 区间(8~12 / 3~6)。
|
||
|
||
#### 8.3A(实现口径补充)idealWidthRatio 默认值(必须写死到配置)
|
||
|
||
为保证首版跨端一致,默认值裁决为:
|
||
|
||
* `idealWidthRatio.APP = 0.90`
|
||
* `idealWidthRatio.WIDGET = 0.95`
|
||
|
||
> 后续若做 A/B 或灰度调整,请通过 configVersion 管理并打点回溯。
|
||
|
||
---
|
||
|
||
## 9. 搜索与组合(可直接实现)
|
||
|
||
多行换行的核心是“断点组合搜索”。要求:
|
||
|
||
* 输出最优 layout
|
||
* 复杂度可控
|
||
* 完全确定性
|
||
|
||
### 9.1 推荐:DP + TopK(App)
|
||
|
||
**状态定义**
|
||
|
||
* `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 desc;tie-break 使用固定 keys(见第 11 节)
|
||
* 插入时去重(同 breaks 序列只保留最高分)
|
||
|
||
**结束条件**
|
||
|
||
* 在 `pos=N`(结束边界)处,从 `dp[N][<=maxLines]` 选最优
|
||
* lineMode=FIXED 时优先选 `dp[N][==maxLines]`,否则降级
|
||
|
||
### 9.2 推荐:Beam Search(Widget)
|
||
|
||
* 每一行扩展时只保留 TopK partial layouts(K 建议 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)
|
||
* Widget:Beam `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、不允许跨断点、不允许模糊匹配。
|
||
* 若采用预计算 span:span 的 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. Widow(EN)与尾行过短(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_SPLIT:10000
|
||
* P_PROTECTED_SPLIT:10000
|
||
* P_WIDOW_WORD:800
|
||
* P_WIDOW_LINE:500
|
||
* P_SHORT_LASTLINE:300
|
||
* P_PARTICLE_ISO:200
|
||
* R_PUNCT_BREAK:80
|
||
* R_SHIFT_BREAK:60
|
||
* R_ACCUM_BREAK:40
|
||
* R_SELF_BREAK:20
|
||
* P_OVER_MAXLEN:30
|
||
* P_TOO_SHORT:10
|
||
|
||
---
|
||
|
||
## 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_type:NONE / RELAX_RULES / SYSTEM_DEFAULT
|
||
* overflow_type:NONE / ELLIPSIS / CLIP
|
||
* reason:NO_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 tokenize(grapheme)+ PUNCT breakpoints + 2 行跑通
|
||
3. 引入 emotionPhrases 拆分惩罚(phrase 匹配按 10.2A)
|
||
4. 引入 DP + TopK(多行)
|
||
5. 引入 overflowMode(Widget 先用 ELLIPSIS)
|
||
6. 引入 constraints + 打点
|
||
7. 调权重与扩词库(基于数据)
|
||
|
||
---
|
||
|
||
## 16. 附录:最小配置清单(实现时至少要有)
|
||
|
||
* emotionPhrasesTC / emotionPhrasesEN
|
||
* tcPunctuations
|
||
* tcParticles(含白名单子集或单独 tcParticleWhitelist)
|
||
* widowMaxLen
|
||
* tcMaxCandidateBreaks
|
||
* minCharsPerLineTC
|
||
* widgetProfiles(Widget:availableWidth/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. 结语
|
||
|
||
这套算法的目标不是理解情绪,而是:
|
||
|
||
> 在规模化系统中,持续做出「像人一样停顿」的选择。
|
||
|
||
只要:确定性 + 可配置 + 可治理,你就能持续把“情绪表达”变成产品护城河。
|