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

28 KiB
Raw Blame History

情绪文案换行算法规范与实现映射说明书 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 公开接口(建议)

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
Token {
  text: string
  type: 'WORD' | 'SPACE' | 'PUNCT' | 'GRAPHEME'
  isPunct?: boolean
  isEmojiCluster?: boolean
}

Breakpoint(断点位置是 token 边界)

Breakpoint {
  pos: number;                // 断点在 token 边界:切分为 [0..pos) + [pos..]
  kind: 'PUNCT' | 'SPACE' | 'BALANCE' | 'SHIFT' | 'ACCUM' | 'SELF' | 'OTHER';
  priority: number;           // 候选断点生成阶段使用(先截断)
}

LineSegment(一行)

LineSegment {
  start: number;
  end: number;
  text: string;
  width: number;
  tokenCount: number;
  charCount: number;
  endsWithEmotion?: boolean;
}

Layout候选方案

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建议精确测量

实现者需提供一个可插拔的测量函数:

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 用 wordCountTC 用 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,写入配置)
    • 对 ENidealWordsPerLineEN 不作为固定常量,而作为弱上界/弱先验;评分中的 ideal 以 width-based 的 line.width 与 idealWidth 的距离为主。
    • 对 TCidealCharsPerLineTC 同理,主要以 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(新增)复杂度与性能预算(建议写进实现约束)

为保证线上性能稳定,建议默认上限:

  • ApptcMaxCandidateBreaks <= 80TopK=10maxLines<=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

  • 若拆分任意 emotionPhrasescore -= 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 > maxLenscore -= P_OVER_MAXLEN * (lineLen - maxLen)
  • 若 lineLen < minPreferredscore -= 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.5score -= 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

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. 结语

这套算法的目标不是理解情绪,而是:

在规模化系统中,持续做出「像人一样停顿」的选择。

只要:确定性 + 可配置 + 可治理,你就能持续把“情绪表达”变成产品护城河。