ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

纯前端手账灵感文案卡片生成器:基于马尔可夫链与秋日语料的轻量诗意生成

纯前端手账灵感文案卡片生成器:基于马尔可夫链与秋日语料的轻量诗意生成

每到周五的夜晚,在手账本的留白处写下一两句富有诗意、带有秋日温存的短句,往往能为整整一周的记录画上一个轻柔圆满的句号。

然而,灵感并不总是随时随地眷顾我们。许多人在面对空白纸页时,常常陷入“词穷”的困窘:想要表达秋天的宁静,笔尖却只能写出干瘪陈旧的词汇;而如果去调用云端大型语言模型,不仅需要配置复杂的 API 密钥、忍受几秒钟的网络延迟,而且大模型常常一口气生成数百字冗长、说教味浓重的大段套话,彻底丧失了手账微文案特有的那份克制与留白。

在纯客户端前端架构中,有没有一种极其轻巧、无需任何后端服务、断网可用,且能稳定产出优美短句的离线自然语言生成方案?

经典的概率统计模型——二阶马尔可夫链(2nd-Order Markov Chain)提供了极为惊艳的答案。在本文中,我们将深入解析马尔可夫文本生成机理,在浏览器中利用仅仅几十 KB 的秋日散文精选语料,构建一个即点即得、充满东方诗性与意外美感的纯前端手账文案卡片生成器。


为什么马尔可夫链适合手账诗意生成

大型神经网络大模型追求的是“绝对的逻辑确定性与事实准确性”,但在文学创作与手账灵感生成领域,过度的确定性反而容易抹杀艺术的偶发奇想。

马尔可夫链的核心假设是“无后效性”:系统的下一个状态仅取决于当前所处的状态,而与更早的历史无关。在二阶中文马尔可夫文本生成中:

  1. 状态定义(State):以连续的两个词(或单字)构成一个状态元组 $(W_{t-1}, W_t)$;
  2. 转移概率矩阵(Transition Probability Matrix):扫描输入的经典秋日语料库(如迟子建的秋天散文、唐诗宋词中的桂花与秋风意象),统计出在状态 $(W_{t-1}, W_t)$ 出现后,下一个词 $W_{t+1}$ 出现的概率分布;
  3. 随机游走采样(Random Walk Sampling):从句首标志开始,根据转移概率分布随机挑选下一个词,直到生成句号或达到指定长度截断。

这种轻量算法具有三大不可替代的工程与美学优势:

  • 极致的轻量与秒级冷启动:状态字典在前端内存中仅占用不到 40KB,初始化耗时仅需 3 毫秒;
  • 纯粹的离线隐私自足:无网络请求、无用户数据上传,完全在浏览器沙箱内自生自灭;
  • 微妙的诗性意外(Serendipity):由于词汇在统计概率中跳跃重组,经常能碰撞出诸如“微风掠过黄昏的茶釜,在落叶深处煮熟了一抹晚霞”这类令人拍案叫绝、意象奇崛的灵气短句。
+--------------------------------------------------------------+ | 预置秋日高质感散文语料库 (约 30KB 纯文本) | +--------------------------------------------------------------+ | v +--------------------------------------------------------------+ | 纯前端轻量分词与二阶转移状态构建器 | | (键: [词A, 词B] --> 值: [候选词C, 候选词D, 候选词E]) | +--------------------------------------------------------------+ | v +--------------------------------------------------------------+ | 马尔可夫链随机游走生成器 (带温度系数 Temperature) | | (控制文案的“天马行空程度”与“语法平稳度”) | +--------------------------------------------------------------+ | v +--------------------------------------------------------------+ | 拍立得复古排版卡片渲染器 (Canvas 2D 离屏输出) | +--------------------------------------------------------------+

核心实现:纯前端二阶马尔可夫生成引擎

下面是无任何外部依赖的完整 TypeScript 实现。系统支持自定义训练语料、温度随机采样系数调节与基于标点符号的优雅句尾修剪:

// utils/markov-generator.ts export interface MarkovConfig { maxWordLength?: number; temperature?: number; // 采样温度:越高意象越跳跃,越低语法越保守 (0.5 ~ 1.2) } export class MarkovPoemGenerator { // 状态转移表:键为 "词A|词B",值为后续词的数组 private transitions: Map<string, string[]> = new Map(); private startStates: string[] = []; constructor() { this.trainDefaultAutumnCorpus(); } /** * 加载秋日唯美散文语料进行极速分词训练 */ public train(corpusText: string): void { // 简易前端中文标点与词元切分 const tokens = corpusText .replace(/[\r\n\t]/g, '') .split(/([,。!?;\s])/g) .filter((t) => t.trim().length > 0); for (let i = 0; i < tokens.length - 2; i++) { const w1 = tokens[i]; const w2 = tokens[i + 1]; const w3 = tokens[i + 2]; const stateKey = `${w1}|${w2}`; if (!this.transitions.has(stateKey)) { this.transitions.set(stateKey, []); } this.transitions.get(stateKey)!.push(w3); // 若处于句首(紧跟标点符号或位于开头),记录为起始候选状态 if (i === 0 || ['。', '!', '?', ';'].includes(tokens[i - 1])) { this.startStates.push(stateKey); } } } /** * 生成一段富有诗意的手账文案短句 */ public generate(config: MarkovConfig = {}): string { const maxLen = config.maxWordLength ?? 32; const temperature = config.temperature ?? 0.85; if (this.startStates.length === 0) { return '微风吹过落叶,秋意正浓。'; } // 1. 随机挑选一个优质的句首状态 let currentStateKey = this.startStates[Math.floor(Math.random() * this.startStates.length)]; const [w1, w2] = currentStateKey.split('|'); const resultWords: string[] = [w1, w2]; // 2. 状态机沿转移概率链条随机游走 while (resultWords.length < maxLen) { const candidates = this.transitions.get(currentStateKey); if (!candidates || candidates.length === 0) { break; } // 温度加权随机挑选下一个词 const chosenWord = this.sampleWithTemperature(candidates, temperature); resultWords.push(chosenWord); // 若已经遇到句终标点且长度已达到舒适阈值,提前优雅结束 if (['。', '!'].includes(chosenWord) && resultWords.length >= 14) { break; } // 状态向后平移 const prevWord = resultWords[resultWords.length - 2]; currentStateKey = `${prevWord}|${chosenWord}`; } let rawSentence = resultWords.join(''); // 后处理:确保文案以温和的标点符号收尾 if (!/[。!?]$/.test(rawSentence)) { rawSentence += '。'; } return rawSentence; } /** * 温度采样函数:控制采样分布的集中度 */ private sampleWithTemperature(candidates: string[], temperature: number): string { if (temperature <= 0.1 || candidates.length === 1) { return candidates[0]; } // 计算频次分布 const frequencyMap = new Map<string, number>(); candidates.forEach((c) => frequencyMap.set(c, (frequencyMap.get(c) || 0) + 1)); const entries = Array.from(frequencyMap.entries()); // 应用温度系数缩放对数概率 const expWeights = entries.map(([_, count]) => Math.pow(count, 1 / temperature)); const totalWeight = expWeights.reduce((a, b) => a + b, 0); let rand = Math.random() * totalWeight; for (let i = 0; i < entries.length; i++) { rand -= expWeights[i]; if (rand <= 0) { return entries[i][0]; } } return candidates[candidates.length - 1]; } /** * 预置训练专属于听汐秋日语料库 */ private trainDefaultAutumnCorpus(): void { const defaultCorpus = ` 深秋的黄昏,桂花香气穿过窗纱,温热的茶汤泛起琥珀色的光晕。 窗外的银杏树叶慢慢变黄,微风吹过树梢,沙沙声像是在低语。 黑猫在阳光里舒展身子,把前爪揣进柔软的胸口,呼吸悠长。 有些日子需要奔跑,有些日子只需要坐下来,看云朵慢慢飘向群山。 把今天的疲惫打包封存在信箱里,明天的晨光自会抚平一切褶皱。 落叶沉入泥土,不是告别,而是大自然安静沉淀的温存。 一盏暖黄的台灯,一杯温热的肉桂红茶,这就是秋天最好的注脚。 代码在屏幕上闪烁,生活在纸页间流淌,世界再喧嚣,内心依然安宁。 `; this.train(defaultCorpus); } }

拍立得卡片排版渲染:把诗句变成明信片

生成的文案短句可以通过 HTML5 Canvas 直接烘焙为一张拍立得复古手账卡片:

  • 纸张质感:3:4 比例的米白哑光相纸,底部预留宽敞的手写留白;
  • 调色板装饰:在左下角排布 3 枚极浅的秋日莫兰迪色小圆点;
  • 竖排或横排宋体:使用font-family: 'Source Han Serif SC',字间距舒展留白,字字透出恬淡的禅意。

用户点击“换一句灵感”,新的诗句在 5 毫秒内瞬间生成,像翻动一叠永远翻不完的诗歌日历。


听汐的灵感手记:概率世界里的诗意偶遇

在传统的理科思维里,概率论常常是冰冷而功利的:胜率、方差、期望值。

但当数学家马尔可夫在一百多年前提出状态转移概率时,大概也很难想象,这项技术在今天的纯前端手账本里,会变成一位不知疲倦的秋日小诗人。

这些生成的文字,并不是哪一位作家的既成名作,也不是大模型经过几百亿参数计算出来的标准答案。它只是词汇与词汇之间,依据秋天的微风、热茶与落叶的概率,自然而然碰撞出的一次偶遇。

就像周五晚上的生活一样:我们不需要一切都按既定的大纲精准展开,允许有一点点随性的意外,允许词句像落叶一样在风里自由旋转,这就是生活最迷人的诗意。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进