[{"data":1,"prerenderedAt":1610},["ShallowReactive",2],{"\u002F2026-06-25":3,"\u002F2026-06-25-rel":352},{"id":4,"title":5,"body":6,"column":334,"date":335,"description":12,"extension":336,"hero_image":337,"meta":338,"navigation":339,"path":340,"seo":341,"series_id":337,"severity":337,"stem":342,"summary":343,"tags":344,"__hash__":351},"posts\u002F2026-06-25-长文写作的记忆分层.md","三十万字之后，AI 该记住什么？",{"type":7,"value":8,"toc":323},"minimark",[9,13,16,20,23,26,29,32,36,39,46,56,62,67,73,78,84,89,100,103,107,110,116,121,127,132,137,142,148,153,159,164,167,170,174,177,180,183,186,189,197,235,246,257,260,263,272,281,294,307,310,317,320],[10,11,12],"p",{},"长篇小说写到三十万字后，\"记住前面写了什么\"变成最大的瓶颈。角色在第五章确立的设定，到第二十章突然对不上；伏笔埋了二十章没回收；世界观里的时间线出现逻辑洞；前面说过的势力关系后面又改了。全量注入上一章或全书到上下文里根本不可能，纯向量检索又容易因为相似度不够而漏掉关键的硬约束。",[10,14,15],{},"这个问题需要分层的记忆系统。根据信息的稳定性和用途把记忆分成三层：设定层、事实层、文本层。每层的存储方式和检索策略完全不同。",[17,18,19],"h2",{"id":19},"为什么不能统一用向量库",[10,21,22],{},"一个很自然的想法是\"把所有信息存到向量库里，需要时向量检索\"。但设定信息不行。",[10,24,25],{},"世界规则、人物档案这类信息是硬约束，一旦因为向量相似度不够高而没被召回，就会产生直接的设定冲突。模型可能生成\"主角这章掌握了某个禁忌知识\"，但系统没召回\"世界规则里明确说这个知识是自杀性的\"，结果后续剧情完全跑偏。向量检索是概率性的，概率性检索不能承载硬约束。",[10,27,28],{},"事实信息（已发生的事件、角色状态变化、伏笔）可以用向量检索，因为\"不完美的匹配\"还能通过更多文本从模型推理出来。如果系统检索到\"角色在某章失去了一条胳膊\"这个事实后又检索到\"这个事件在剧情中的含义是……\"，模型有足够的上下文修复漏掉的细节。",[10,30,31],{},"原文（正文片段）是纯量级的数据，全量注入的成本太高，用摘要替代是更经济的做法。",[17,33,35],{"id":34},"设定层结构化存储全量或定向注入","设定层：结构化存储、全量或定向注入",[10,37,38],{},"设定层装的是世界级的硬规则和静态档案，包括：",[10,40,41,45],{},[42,43,44],"strong",{},"世界规则","（约 600 字压缩）",[47,48,49,53],"ul",{},[50,51,52],"li",{},"现实是否稳定、超常能力是否公开、死亡是否可逆、信息获取是否受限",[50,54,55],{},"这个世界\"允许什么、不允许什么\"",[10,57,58,61],{},[42,59,60],{},"系统性规则","（通常 4-6 条）",[47,63,64],{},[50,65,66],{},"力量体系的上限、交易与代价的原理、禁忌知识的危害方式",[10,68,69,72],{},[42,70,71],{},"主要势力","（通常 3-5 个）",[47,74,75],{},[50,76,77],{},"每个势力的名称、目标、常用手段、与其他势力的关系",[10,79,80,83],{},[42,81,82],{},"核心人物档案","（通常 4-8 个）",[47,85,86],{},[50,87,88],{},"每个人物的身份、核心目标、掌握的信息、心理底线",[10,90,91,92,95,96,99],{},"这些东西在整部小说中是不变的（或变化极缓），写作的任何环节都需要遵循它们。设定层必须在调用 AI 生成章节时",[42,93,94],{},"全量注入","或",[42,97,98],{},"按需定向注入","。全量注入适合小说世界相对简洁的情况；如果世界复杂设定众多，可以做定向注入——比如这一章涉及势力 A，就只注入 A 的档案和相关规则。",[10,101,102],{},"关键是：设定层的记忆片段永远不能因为上下文窗口压力而被省略。这是整个系统的天花板。",[17,104,106],{"id":105},"事实层向量检索-时间过滤","事实层：向量检索 + 时间过滤",[10,108,109],{},"事实层装的是已经发生的、会影响后续剧情的信息。",[10,111,112,115],{},[42,113,114],{},"章节摘要","（每章一句话）",[47,117,118],{},[50,119,120],{},"\"主角从势力 B 得到了关键物品 X，但同时被势力 A 注意到了\"",[10,122,123,126],{},[42,124,125],{},"角色动态状态","（追加式、非覆盖）",[47,128,129],{},[50,130,131],{},"某角色掌握了什么新信息、失去了什么能力、和某人的关系发生了什么变化",[10,133,134],{},[42,135,136],{},"伏笔记录",[47,138,139],{},[50,140,141],{},"埋下的伏笔及其状态（待回收 \u002F 已回收）、涉及的章号",[10,143,144,147],{},[42,145,146],{},"时间线","（关键事件及其时间距离）",[47,149,150],{},[50,151,152],{},"\"第五章后的第三天，X 事件发生\"",[10,154,155,158],{},[42,156,157],{},"世界新设定","（剧情中确立、原设定没有的）",[47,160,161],{},[50,162,163],{},"\"这个世界原本不知道 X，但第十二章中 Y 揭露了 X\"",[10,165,166],{},"这一层用向量检索的原因是数据量大（几十万字的小说可能有几百条事实）且不需要完美精确。模型在知道\"五章前主角失去了左腿\"和\"十章前主角加入了某组织\"的基础上，能够合理推理出后续事件。即使系统漏掉了某条非关键事实，模型也不太会产生硬冲突。",[10,168,169],{},"时间过滤很重要。检索结果应该默认优先最近的事件，因为离当前章节越近的事件通常影响力越大。\"三章前角色的转折\"比\"五十章前的背景\"更应该被注入。",[17,171,173],{"id":172},"文本层只取最近段落","文本层：只取最近段落",[10,175,176],{},"文本层就是原始的正文片段，用于衔接语气和细节。一个几十万字的小说，全部原文根本放不进上下文。",[10,178,179],{},"解决方案是简单的：只注入前一章的结尾（约 800 字），这足以让模型维持住文笔连贯性和情感线的延续。对于需要回顾很久之前的情节细节的场景，用事实层的摘要替代——\"第五章中，X 因为 Y 而死亡\"这一条摘要比翻出整个第五章的原文高效得多。",[10,181,182],{},"当然也存在\"某章需要直接引用或高度呼应某个很久以前的场景细节\"的情况。这时候文本层可以临时扩大范围，但这是特例，不是常态。",[17,184,185],{"id":185},"具体的实现策略",[10,187,188],{},"设定层在每次生成章节前直接注入——要么全部、要么按这章涉及的范围选择。不需要任何检索逻辑，就是结构化的数据块。",[10,190,191,192,196],{},"事实层维护一个 ",[193,194,195],"code",{},"memory.json"," 文件，存储：",[47,198,199,205,211,217,223,229],{},[50,200,201,204],{},[193,202,203],{},"synopsis","：全书概要，每章更新后重写，封顶 1000 字",[50,206,207,210],{},[193,208,209],{},"chapterDigests","：逐章一句话摘要",[50,212,213,216],{},[193,214,215],{},"characters","：角色的动态状态（与静态档案分开）",[50,218,219,222],{},[193,220,221],{},"foreshadow","：伏笔列表，标记待回收或已回收",[50,224,225,228],{},[193,226,227],{},"timeline","：关键事件及章号和相对时间",[50,230,231,234],{},[193,232,233],{},"worldFacts","：剧情中新确立的设定事实",[10,236,237,238,241,242,245],{},"每章生成完成后，系统调用一次 AI，输入\"旧记忆 + 本章正文\"，要求输出",[42,239,240],{},"增量","——这一章新增了什么伏笔、角色状态如何变化、有没有新设定。然后用纯函数 ",[193,243,244],{},"normalizeMemory()"," 合并增量到旧记忆里：新伏笔追加、已回收伏笔置状态、角色按 name upsert（同 ID 的覆盖）、synopsis 重写、timeline 追加。",[10,247,248,249,252,253,256],{},"生成下一章时，",[193,250,251],{},"buildChapterMessages()"," 的流程变成：注入设定块 → 注入记忆块（由 ",[193,254,255],{},"buildMemoryContext()"," 生成） → 注入上一章结尾 → 生成新章。记忆块里包含：最近 6 章摘要、全部角色现状、最后 8 条未回收伏笔、最后 5 条时间线、最后 10 条世界新设定。这些都是硬上限，防止记忆块因为章数增多而无限膨胀。",[17,258,259],{"id":259},"一致性与失败处理",[10,261,262],{},"这套流水线的核心难点不是单点生成质量，而是一致性和失败恢复。",[10,264,265,268,269,271],{},[42,266,267],{},"一致性来自增量而不是合并","。每章记忆更新要求 AI 只输出增量，而不是整个记忆的重写。这样设计有两个好处：一是防止 AI 每次都改掉前面的内容（一种无意义的膨胀），二是增量小得多，解析 JSON 时出错的概率降低。合并逻辑交给纯函数 ",[193,270,244],{},"，这个函数可以单测，保证逻辑稳定。",[10,273,274,277,278,280],{},[42,275,276],{},"同一章重建幂等","。由于 ",[193,279,209],{}," 按章号存储，相同章号的摘要会覆盖而不是追加，所以即使记忆重建时对同一章调用多次，也不会出现重复记录。角色状态的 upsert 机制也是同理。",[10,282,283,286,287,289,290,293],{},[42,284,285],{},"记忆更新失败不影响正文","。每章正文先落盘、记忆更新在后。如果记忆更新失败（比如 AI 返回格式错误、JSON 解析异常），降级处理：用这章的 title 或 summary 作为摘要追加到 ",[193,288,209],{},"，",[193,291,292],{},"lastChapterNo"," 照常推进，其余记忆保持不变。这样下一章的生成可以继续进行，用户不会察觉到记忆层的故障。",[10,295,296,299,300,303,304,306],{},[42,297,298],{},"重建任务的断点续跑","。存量书如果想补全记忆，后台可以跑 ",[193,301,302],{},"runMemoryRebuild()"," 任务，按章序遍历，跳过 ",[193,305,292],{}," 以内的章（已纳入过的）。这个任务可停、可继续，防止重复扣费。",[17,308,309],{"id":309},"记忆分层的边界",[10,311,312,313,316],{},"这个设计对应的问题空间是：",[42,314,315],{},"长篇写作中保持内容一致性和连贯性","。它解决的是系统层面的记忆管理，不覆盖人工的内容审校和设定调整。",[10,318,319],{},"如果作者在某个时刻决定\"我要改前面某角色的设定\"，那是人为修改设定层、然后手动落盘的过程。系统的记忆不会自动同步这种改动，因为改动涉及主观判断。类似的，如果某章生成出来的内容和记忆产生冲突（比如模型莫名其妙加了个新势力），那也需要人工介入编辑，而不是靠记忆系统自动修正。",[10,321,322],{},"记忆系统的职责是：提供足够的上下文约束，让模型生成时减少无意义的冲突；一旦冲突出现，系统快速降级，不中断工作流。",{"title":324,"searchDepth":325,"depth":325,"links":326},"",2,[327,328,329,330,331,332,333],{"id":19,"depth":325,"text":19},{"id":34,"depth":325,"text":35},{"id":105,"depth":325,"text":106},{"id":172,"depth":325,"text":173},{"id":185,"depth":325,"text":185},{"id":259,"depth":325,"text":259},{"id":309,"depth":325,"text":309},"内容流水线","2026-06-25","md",null,{},true,"\u002F2026-06-25",{"title":5,"description":12},"2026-06-25-长文写作的记忆分层","长篇到几十万字后，用分层记忆解决剧情断裂和设定崩坏——设定层硬约束、事实层概率检索、文本层按需取用，层级的存储与检索策略完全不同。",[345,346,347,348,349,350],"长篇写作","记忆管理","内容生成","AI辅助创作","向量检索","一致性维护","IK_-YIn1bPi-9X-l1KlQn3a-D0cTe45drq3MhS7m-AA",[353,1074,1391],{"id":354,"title":355,"body":356,"column":334,"date":1061,"description":360,"extension":336,"hero_image":337,"meta":1062,"navigation":339,"path":1063,"seo":1064,"series_id":337,"severity":337,"stem":1065,"summary":1066,"tags":1067,"__hash__":1073},"posts\u002F2026-07-08-视频包装与数字人配音.md","配音时长不可控——那就让画面跟着它走",{"type":7,"value":357,"toc":1052},[358,361,365,372,375,378,413,416,430,433,436,443,625,638,641,652,655,658,665,676,719,726,729,740,748,757,760,771,774,777,783,818,824,856,862,879,882,888,925,931,950,955,984,987,990,993,996,1010,1013,1016,1019,1038,1041,1048],[10,359,360],{},"数字人口播的完整链路已能生成对口型视频原片，但从音频出发反向约束画面节奏、再分层合成包装成可发布的成片，这一环涉及三个工程难点，都不是生成问题，而是一致性与路径确定性问题。",[17,362,364],{"id":363},"tts-时长驱动的反向工作流","TTS 时长驱动的反向工作流",[10,366,367,368,371],{},"MiMo TTS 客户端同步调用返回 base64 音频与实际音长（单位秒），这个时长是后续所有操作的源头。关键约束在于",[42,369,370],{},"时长由上游决定，不能人为指定","。",[10,373,374],{},"拿一个具体例子：用户输入 60 字的口播文案，送给 MiMo preset 模式（冰糖音色、wav 格式）。MiMo 返回的不是\"这段话应该播 30 秒\"，而是\"我合成出来的音频实际是 28.5 秒\"。TTS 生成的时长由语速、音色、模型版本等因素共同决定，变量太多了。",[10,376,377],{},"在这个约束下，整个包装流程必须反向适配：",[379,380,381,391,404],"ol",{},[50,382,383,386,387,390],{},[42,384,385],{},"TTS 合成音频"," → 得到 ",[193,388,389],{},"audioDurationSec","（来自 ffprobe 或 MiMo 返回）",[50,392,393,396,397,399,400,403],{},[42,394,395],{},"飞天对口型"," → 输入 ",[193,398,389],{},"，输出\"这个长度的人物口播视频\"（飞天返回 ",[193,401,402],{},"duration","）",[50,405,406,409,410,412],{},[42,407,408],{},"HyperFrames 包装"," → 模板中的时间线也以这个 ",[193,411,402],{}," 为基准构建字幕、转场、片头片尾的时间点",[10,414,415],{},"如果反过来做——先定好画面框架是 60 秒，再想办法让音频往里塞——就陷入了\"剪音频、变速播放、或者音视不同步\"的泥潭。",[10,417,418,419,422,423,426,427,429],{},"实现上，MiMo 音频落地后先上传我方 S3，用 ffprobe 测长作为权威值（",[193,420,421],{},"probeVideoDurationSec"," 对音频也适用），这个测值再被传给飞天 ",[193,424,425],{},"create_by_audio"," 接口。飞天不是按输入时长生成固定时长的视频，而是真正根据音轨长度对口型——返回的 ",[193,428,402],{}," 几乎就等于输入的音频时长（考虑 AAC 编码器的 priming delay，实测偏差 0.64 帧即 21.33ms，可忽略）。",[17,431,432],{"id":432},"数字人作为独立图层而非重新生成",[10,434,435],{},"一个常见的工程误区：既然最终成片是\"数字人 + 包装\"，能不能让 HyperFrames 直接去生成数字人？不能。飞天数字人是外部供应商接口，响应慢（异步任务，等待 2-5 分钟常态），接口也不开放生成能力给外部工具（只提供对口型 API）。再者，用户可能想用同一份配音试多个数字人形象、或试多个包装模板，重新生成整条视频的成本太高。",[10,437,438,439,442],{},"设计决策是把飞天原片（已完成对口型的 MP4 文件）当作 HyperFrames composition 里的一个 ",[193,440,441],{},"\u003Cvideo>"," 图层。模板里声明这样的结构：",[444,445,449],"pre",{"className":446,"code":447,"language":448,"meta":324,"style":324},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv data-track-index=\"0\">\n  \u003Cvideo data-start=\"0s\" data-duration=\"$MAIN_VIDEO_DURATION\" \n          data-has-audio=\"true\" \n          src=\"$MAIN_VIDEO_URL\">\n  \u003C\u002Fvideo>\n\u003C\u002Fdiv>\n\n\u003Cdiv data-track-index=\"1\">\n  \u003C!-- 字幕、花字、角标等包装层 -->\n\u003C\u002Fdiv>\n\n\u003Cdiv data-track-index=\"2\">\n  \u003C!-- 片头片尾、转场 -->\n\u003C\u002Fdiv>\n","html",[193,450,451,478,505,518,531,541,551,557,573,580,589,594,610,616],{"__ignoreMap":324},[452,453,456,460,464,468,471,475],"span",{"class":454,"line":455},"line",1,[452,457,459],{"class":458},"sVt8B","\u003C",[452,461,463],{"class":462},"s9eBZ","div",[452,465,467],{"class":466},"sScJk"," data-track-index",[452,469,470],{"class":458},"=",[452,472,474],{"class":473},"sZZnC","\"0\"",[452,476,477],{"class":458},">\n",[452,479,480,483,486,489,491,494,497,499,502],{"class":454,"line":325},[452,481,482],{"class":458},"  \u003C",[452,484,485],{"class":462},"video",[452,487,488],{"class":466}," data-start",[452,490,470],{"class":458},[452,492,493],{"class":473},"\"0s\"",[452,495,496],{"class":466}," data-duration",[452,498,470],{"class":458},[452,500,501],{"class":473},"\"$MAIN_VIDEO_DURATION\"",[452,503,504],{"class":458}," \n",[452,506,508,511,513,516],{"class":454,"line":507},3,[452,509,510],{"class":466},"          data-has-audio",[452,512,470],{"class":458},[452,514,515],{"class":473},"\"true\"",[452,517,504],{"class":458},[452,519,521,524,526,529],{"class":454,"line":520},4,[452,522,523],{"class":466},"          src",[452,525,470],{"class":458},[452,527,528],{"class":473},"\"$MAIN_VIDEO_URL\"",[452,530,477],{"class":458},[452,532,534,537,539],{"class":454,"line":533},5,[452,535,536],{"class":458},"  \u003C\u002F",[452,538,485],{"class":462},[452,540,477],{"class":458},[452,542,544,547,549],{"class":454,"line":543},6,[452,545,546],{"class":458},"\u003C\u002F",[452,548,463],{"class":462},[452,550,477],{"class":458},[452,552,554],{"class":454,"line":553},7,[452,555,556],{"emptyLinePlaceholder":339},"\n",[452,558,560,562,564,566,568,571],{"class":454,"line":559},8,[452,561,459],{"class":458},[452,563,463],{"class":462},[452,565,467],{"class":466},[452,567,470],{"class":458},[452,569,570],{"class":473},"\"1\"",[452,572,477],{"class":458},[452,574,576],{"class":454,"line":575},9,[452,577,579],{"class":578},"sJ8bj","  \u003C!-- 字幕、花字、角标等包装层 -->\n",[452,581,583,585,587],{"class":454,"line":582},10,[452,584,546],{"class":458},[452,586,463],{"class":462},[452,588,477],{"class":458},[452,590,592],{"class":454,"line":591},11,[452,593,556],{"emptyLinePlaceholder":339},[452,595,597,599,601,603,605,608],{"class":454,"line":596},12,[452,598,459],{"class":458},[452,600,463],{"class":462},[452,602,467],{"class":466},[452,604,470],{"class":458},[452,606,607],{"class":473},"\"2\"",[452,609,477],{"class":458},[452,611,613],{"class":454,"line":612},13,[452,614,615],{"class":578},"  \u003C!-- 片头片尾、转场 -->\n",[452,617,619,621,623],{"class":454,"line":618},14,[452,620,546],{"class":458},[452,622,463],{"class":462},[452,624,477],{"class":458},[10,626,627,630,631,633,634,637],{},[193,628,629],{},"data-has-audio=\"true\""," 这一行是命门——没有它，渲染器会给 ",[193,632,441],{}," 默认加 ",[193,635,636],{},"muted","，成片就没声音。",[10,639,640],{},"这样做的好处显而易见：",[47,642,643,646,649],{},[50,644,645],{},"数字人原片一旦生成就不动，支持单独重做配音（只需重新调用 TTS 和飞天，不影响包装渲染）",[50,647,648],{},"同一个配音可套多个模板，不需要重新对口型",[50,650,651],{},"包装层的 HTML\u002FCSS 改动不会触发数字人重生成，迭代快",[10,653,654],{},"缺点是需要镜像内内置 Chromium 和 FFmpeg（官方渲染镜像实测 3.72GB），与 API 镜像分离部署。但这换来的是确定性输出和可控的环境——生产机、开发机、CI 跑同一个 composition，出片应该帧级一致（除了字体、系统库这类版本差异导致的微调，都锁版本了）。",[17,656,657],{"id":657},"成片路径的幂等性与状态机",[10,659,660,661,664],{},"用户选定模板、确认方案后，系统提交一个 ",[193,662,663],{},"VideoRenderJob","：输入模板 ID、原片 key、字幕 cues、音频 URL 等，输出成片 objectKey。同一份输入如果重试或重新提交，必须得到同样的输出（或者快速失败）。",[10,666,667,668,671,672,675],{},"状态流转是 ",[193,669,670],{},"queued → preparing → rendering → uploading → completed"," 或 ",[193,673,674],{},"failed","：",[47,677,678,688,694,708,714],{},[50,679,680,683,684,687],{},[42,681,682],{},"queued","：任务入队，等待 worker 消费。这一步是防并发上限的信号量检查——飞天有并发限制（错误码 1001），Redis 信号量 ",[193,685,686],{},"yunclaude:dub:sky:sem"," 兜底（触顶不直接失败，改为入队等待）。",[50,689,690,693],{},[42,691,692],{},"preparing","：下载原片、组装模板（注入数据、渲染占位符）。这一步的幂等性来自 objectKey 的内容寻址——同一个原片 key、同一个模板版本，组装出的 composition.html 比特级相同。",[50,695,696,699,700,703,704,707],{},[42,697,698],{},"rendering","：Chromium 逐帧捕获、FFmpeg 合成。这是确定性的关键——环境锁定（Node 24、Chromium 版本、FFmpeg 版本、字体版本都在镜像里写死），同一个 composition 渲染多次输出帧级一致。M1 POC 中用 seek 点验证：",[193,701,702],{},"t=4.5s→第 135 帧","、",[193,705,706],{},"t=30.0s→第 900 帧","（读原片内置计数器，nb_frames=1800），长时间点零漂移。",[50,709,710,713],{},[42,711,712],{},"uploading","：上传 OSS，记录 objectKey。返回给前端时用现签（每次读时重新签，不存短时 URL）。",[50,715,716,718],{},[42,717,674],{},"：任何一步异常，立即 rollback。计费侧已扣的视频点全额退款（resource operationId 幂等）。",[10,720,721,722,725],{},"失败的兜底是 reaper（BullMQ 的死信队列处理），轮询 ",[193,723,724],{},"status=running"," 超期（>10 分钟）的任务，标记为失败并补退款。",[17,727,728],{"id":728},"音画同步与字幕对齐",[10,730,731,732,735,736,739],{},"成片里字幕何时出现、何时消失，这些时间点由分析步生成的 ",[193,733,734],{},"subtitleCues"," 定义（格式 WebVTT）。一个 cue 的结构是 ",[193,737,738],{},"start → end"," + 文本，例如：",[444,741,746],{"className":742,"code":744,"language":745},[743],"language-text","00:05.000 --> 00:08.500\n这是一段口播文案\n","text",[193,747,744],{"__ignoreMap":324},[10,749,750,751,753,754,756],{},"HyperFrames 的字幕图层根据这些 cues 生成动画：start 时刻淡入，end 时刻淡出。整个成片的时间线参考都来自主视频（",[193,752,441],{}," 元素），而主视频的时长就是飞天返回的 ",[193,755,402],{},"——由音频长度决定。",[10,758,759],{},"一个细节：AAC-LC 编码器有 priming delay（约 1024 samples@48kHz = 21.33ms），所以成片里音频起点和视频起点存在一个已知的 21ms 偏移。但这是编码器层的常数，不是渲染问题，接受即可。",[10,761,762,763,766,767,770],{},"关键词高亮（比如把卖点词着色为黄色）需要在分析时做标记，例如 HTML 标签：",[193,764,765],{},"\u003Cspan class=\"highlight\">关键词\u003C\u002Fspan>","，然后 CSS 定义颜色。模板编写规约里明确禁止动画 ",[193,768,769],{},"letterSpacing"," 等布局属性（会在逐帧捕获时 snap 到整数像素产生抖动），只允许 transform（x\u002Fy\u002Fscale\u002Fopacity）。",[17,772,773],{"id":773},"成片路径的端到端烟测",[10,775,776],{},"发版前的验收分六个阶段：",[10,778,779,782],{},[42,780,781],{},"A. 发版前置","（缺一项线上就炸）",[47,784,785,796,802,812,815],{},[50,786,787,788,791,792,795],{},"后台已配渲染单价（resourceKey ",[193,789,790],{},"video_render_sec","），且 ",[193,793,794],{},"enabled"," 勾选",[50,797,798,799,403],{},"灰度名单已配置（环境变量 ",[193,800,801],{},"VIDEO_RENDER_HTML_USER_IDS",[50,803,804,805,808,809,403],{},"发版机 ",[193,806,807],{},"release.sh"," 已改（支持第 6 个镜像 ",[193,810,811],{},"yc-video-render",[50,813,814],{},"K8s 集群配额已提升（requestQuota 新增 2C\u002F2Gi、limitsQuota 新增 4C\u002F4Gi）",[50,816,817],{},"构建机磁盘充足（≥10GB 空闲）",[10,819,820,823],{},[42,821,822],{},"B. 发版后基础设施","（不通过立即 rollout undo）",[47,825,826,832,839,846,853],{},[50,827,828,829,831],{},"迁移已执行（",[193,830,663],{}," 表已建）",[50,833,834,835,838],{},"渲染 worker 已起（",[193,836,837],{},"READY 1\u002F1","，镜像 tag 与本次发版一致）",[50,840,841,842,845],{},"容器内 hyperframes CLI 可用（",[193,843,844],{},"hyperframes --version"," 返回 0.7.70）",[50,847,848,849,852],{},"容器内中文字体已装（",[193,850,851],{},"fc-list :lang=zh"," 非空）",[50,854,855],{},"worker 连上 Redis 队列（日志无 crash loop）",[10,857,858,861],{},[42,859,860],{},"C. 回归防线：未放量用户零感知","（最高优先级）",[47,863,864,867,870,873],{},[50,865,866],{},"不在灰度名单的用户进增强步看不到\"包装模板\"卡",[50,868,869],{},"旧链路（ffmpeg）完整出片，字幕\u002F转场\u002FBGM 都在",[50,871,872],{},"旧链路仍生成 AI 插片，计费时间线出现 seedance 扣费",[50,874,875,876,878],{},"未放量时 ",[193,877,663],{}," 表没有新行",[10,880,881],{},"这段最重要是因为旧链路服务着所有现存数字人用户。一个新功能开关不应该波及灰度外的用户。",[10,883,884,887],{},[42,885,886],{},"D. 灰度用户正向流程","（核心价值）",[47,889,890,893,900,907,910,913,916,919,922],{},[50,891,892],{},"模板列表可见：增强步看到\"不加包装\"+\"美食探店\"两张卡",[50,894,895,896,899],{},"选模板后出片最终 ",[193,897,898],{},"completed","，可播放",[50,901,902,903,906],{},"成片有口播声音（这是 ",[193,904,905],{},"data-has-audio"," 命门检验）",[50,908,909],{},"片头\u002F片尾\u002F角标都在：0-3s 品牌片头、右上角全程角标、最后 3s CTA",[50,911,912],{},"中文不乱码：片头标题与字幕汉字正常",[50,914,915],{},"字幕关键词高亮：关键词黄色，其余白色，标签没被打碎",[50,917,918],{},"进度实时可见：出片过程中进度条推进，不死在一个数字",[50,920,921],{},"成片链接可下载且长期有效：15 分钟后刷新页面仍能播放",[50,923,924],{},"渲染时长符合预期：60s 成片≤5 分钟（M1 POC 实测 39.5s）",[10,926,927,930],{},[42,928,929],{},"E. 资金正确性","（看后台账单，不能只看页面）",[47,932,933,938,941,944,947],{},[50,934,935,936,403],{},"扣的是视频点不是算力点（计费时间线条目 title 首段是 ",[193,937,485],{},[50,939,940],{},"不再扣 seedance 插片钱（灰度用户的时间线里没有插片扣费）",[50,942,943],{},"结算按实际秒数（units ≈ 成片时长整秒向上取整）",[50,945,946],{},"余额不足时不产生任务、不扣费",[50,948,949],{},"重复提交不重复扣费（operationId 幂等）",[10,951,952],{},[42,953,954],{},"F. 异常路径",[47,956,957,963,969,972,975,978],{},[50,958,959,960,962],{},"渲染失败会退款：任务置 ",[193,961,674],{},"，计费时间线出现退款条目",[50,964,965,966,968],{},"失败同步反映到增强任务：增强任务也变 ",[193,967,674],{},"，前端不会永远停在\"渲染中\"",[50,970,971],{},"排队中可取消并全额退：返回成功，退款到账",[50,973,974],{},"已在渲染中不可取消：返回 409，不退款（CPU 已烧）",[50,976,977],{},"worker 重启不丢任务：任务被重新捞起或置失败退款",[50,979,980,981,983],{},"超时判失败：>10 分钟置 ",[193,982,674],{}," 并退款",[10,985,986],{},"每一条都写了具体的检查命令和判据。比如验证成片有声音，就是直接播放、耳朵听；验证字幕关键词高亮，就是肉眼看颜色；验证退款，就是对比出片前后的计费时间线。",[17,988,989],{"id":989},"设计的权衡",[10,991,992],{},"这套方案的成本是什么？",[10,994,995],{},"首先是镜像大小：官方渲染镜像装了 280 多个 apt 包、node_modules、Chromium 和 FFmpeg，构建出的镜像实测 3.72GB，单独占用一个 K8s node pool，构建时间约 10 分钟，推拉镜像耗时显著上升。但这是\"要么装进 API 镜像肥到 6GB+、拖累三个部署，要么单独镜像\"的取舍——选了单独。",[10,997,998,999,1001,1002,1005,1006,1009],{},"其次是模板编写规约的学习成本。一个看起来\"普通的网页动画\"可能在逐帧 seek 渲染时炸：GSAP 退场动画必须挂在 clip 内层并补 hard kill，禁止 ",[193,1000,769],{}," 等布局属性，中文字体必须显式 ",[193,1003,1004],{},"@font-face","（容器内用 Noto Sans CJK）。",[193,1007,1008],{},"hyperframes check"," 作为模板上架的强制 gate，能一次性拦截这类问题。",[10,1011,1012],{},"获得的是什么？確定性输出。同一个模板、同一份配音，渲染 100 次得到 100 个帧级一致的成片（前提是输入稳定，不变数字人形象、不变字体库版本）。这对 CI 自动回归、成片质量审核都很有意义。还有灵活性：配音、数字人、包装模板可以独立迭代，不互相阻塞。",[17,1014,1015],{"id":1015},"线上部署的最后两步",[10,1017,1018],{},"发版后的两个关键项，缺一项都会导致灰度用户无法下单：",[379,1020,1021,1030],{},[50,1022,1023,675,1026,1029],{},[42,1024,1025],{},"后台配价",[193,1027,1028],{},"resourceKey=video_render_sec"," 的单价必须填（单位元\u002F秒），且勾选 enabled。没配的话 chargeResource 会返回 404，用户点开始出片就直接报错。",[50,1031,1032,675,1035,1037],{},[42,1033,1034],{},"灰度名单",[193,1036,801],{}," 环境变量决定了谁能看到模板卡。留空 = 全员走旧 ffmpeg 链路（可用于紧急回滚），指定用户 ID = 该用户进入新链路。发版初期应只填 1-2 个测试账号。",[10,1039,1040],{},"这两个配置是代码之外的硬依赖，容易遗漏。烟测清单里放在最前面，作为\"不做后面全走不通\"的前置项。",[10,1042,1043,1044,1047],{},"整个方案的核心原则是",[42,1045,1046],{},"分层隔离与路径确定性","：TTS 决定时长、飞天决定人像、HyperFrames 决定包装，各层独立演进，成片路径从输入到输出一条流水线，无分支、无条件、无随机。这对一个产生可发布物料的流水线来说，是底线。",[1049,1050,1051],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":324,"searchDepth":325,"depth":325,"links":1053},[1054,1055,1056,1057,1058,1059,1060],{"id":363,"depth":325,"text":364},{"id":432,"depth":325,"text":432},{"id":657,"depth":325,"text":657},{"id":728,"depth":325,"text":728},{"id":773,"depth":325,"text":773},{"id":989,"depth":325,"text":989},{"id":1015,"depth":325,"text":1015},"2026-07-08",{},"\u002F2026-07-08",{"title":355,"description":360},"2026-07-08-视频包装与数字人配音","数字人口播成片的最后一环：配音时长不可控，如何反向驱动画面？原片如何作为独立图层无损合成？成片路径如何保证确定性？",[1068,1069,1070,1071,1072],"TTS","数字人","HyperFrames","音视同步","流水线设计","ISg-n2hvjyCKUW_WMTk0Ais8ji7YhOK5rXPmNyUBWQk",{"id":1075,"title":1076,"body":1077,"column":334,"date":1377,"description":1081,"extension":336,"hero_image":337,"meta":1378,"navigation":339,"path":1379,"seo":1380,"series_id":337,"severity":337,"stem":1381,"summary":1382,"tags":1383,"__hash__":1390},"posts\u002F2026-07-02-工作流迁移与复用.md","搬一条生产线，难的不是业务逻辑",{"type":7,"value":1078,"toc":1366},[1079,1082,1085,1088,1092,1099,1102,1108,1111,1114,1121,1136,1139,1142,1159,1162,1181,1184,1187,1200,1203,1209,1235,1246,1249,1252,1255,1262,1265,1268,1271,1274,1281,1284,1313,1328,1331,1334,1360,1363],[10,1080,1081],{},"旧平台上已经跑通了漫剧和小说两条完整的创作工作流。新平台已具备图片生成、异步任务、用户鉴权和资源计费的基础设施。现在需要把这两条链路搬过来，但不是照搬代码，而是复用领域逻辑重新适配。",[10,1083,1084],{},"迁移看似是一个代码挪动的问题，实际上是一个架构解耦的问题。两个平台在三个维度上产生了紧密耦合，直接挪动代码会导致新平台继承旧平台的设计包袱。",[17,1086,1087],{"id":1087},"三处不兼容的耦合",[1089,1090,1091],"h3",{"id":1091},"任务队列与调度模型",[10,1093,1094,1095,1098],{},"旧平台的工作流采用进程内任务 Map 和命令式 API 网关。漫剧工作流从剧本拆分镜头、生成资产图、生成视频、拼接整集——这一系列操作都是通过 ",[193,1096,1097],{},"__api\u002Fcomic_*"," 这样的命令端点来驱动的，任务状态存在内存 Map 里，重启进程就丢了。",[10,1100,1101],{},"新平台设计了一套资源计费底座，任务需要经历预留（reserve）→ 执行 → 结算（settle）→ 失败时退款（refund）的流程。以小说工作流为例，每个阶段（设定、世界观、角色、分卷、大纲、章节）都是独立任务，生成前要冻结估算的算力点，生成后按实际输出字符数结算，多退少补。这套计费流程嵌入到新平台的 Billing 服务里，不能绕过。",[10,1103,1104,1105,371],{},"如果我直接把旧平台的任务调度逻辑搬过来，新平台的任务会跳过 reserve 这一步，造成\"已生成但余额不足无法扣费\"的问题。同样，旧平台的视频任务如果失败只是标记 failed，没有退款逻辑，新平台则需要原子地调用 ",[193,1106,1107],{},"RefundCharge",[10,1109,1110],{},"两边的任务模型在原语层面就不兼容。",[1089,1112,1113],{"id":1113},"存储路径与对象约定",[10,1115,1116,1117,1120],{},"旧平台用本地 JSON 文件存储小说项目数据。小说作品、设定、世界观、角色这些结构化阶段的原始输出被序列化到磁盘上，依赖一套约定好的目录结构。漫剧项目也类似，源项目中通过 ",[193,1118,1119],{},"featureDir(userId, 'comic')"," 这样的函数来确定资产文件的存储根路径。",[10,1122,1123,1124,1127,1128,1131,1132,1135],{},"新平台统一使用 S3 \u002F OOS 对象存储加 Prisma 数据库。项目的元数据（作品标题、阶段状态、版本历史）存 Prisma 表，生成的内容（图片、视频、结构化文本）存对象存储并记录 key。例如小说的 ",[193,1125,1126],{},"NovelSection"," 表存储化后的 JSON 结构和展示文本，章节正文存在 ",[193,1129,1130],{},"NovelChapterVersion"," 表的 ",[193,1133,1134],{},"content"," 字段。",[10,1137,1138],{},"如果我从旧平台直接挪过来一套\"读本地 JSON\"的逻辑，新平台就要维护两套存储系统。更重要的是，新平台的计费系统依赖持久化的结构化数据——只有把规范化后的展示文本存进表里，后台才能追溯某次扣费对应的具体内容。",[1089,1140,1141],{"id":1141},"计费模型与资源定价",[10,1143,1144,1145,703,1148,1151,1152,671,1155,1158],{},"旧平台对小说的计费可能是按生成 token 次数、按模型调用、或者干脆不计费。新平台设计了一套通用资源计费框架：每个资源有一个 key（例如 ",[193,1146,1147],{},"image_generation",[193,1149,1150],{},"novel_text_output","），配套一个定价模型（",[193,1153,1154],{},"PER_CALL",[193,1156,1157],{},"PER_UNIT","），后台可以随时调价。",[10,1160,1161],{},"对小说工作流，新平台的选择是按可见字符数计费，不按 token。这意味着：",[47,1163,1164,1167,1174],{},[50,1165,1166],{},"用户最终看到的文本去掉空白后的字符数才会被计入。",[50,1168,1169,1170,1173],{},"模型原始输出如果是 JSON 格式，那么 ",[193,1171,1172],{},"{}"," 括号、字段名、引号、逗号、缩进都不算——只计算最终展示的内容。",[50,1175,1176,1177,1180],{},"每个阶段的计费单位是 ",[193,1178,1179],{},"算力点 \u002F 千字","，后台可配置，用户无感知。",[10,1182,1183],{},"从旧平台的某种计费方式切换到这套新模型，需要重新梳理每个环节的计费触发点。如果只是把旧的生成函数搬过来调用一遍，新平台的 Billing 根本没有机会介入。",[17,1185,1186],{"id":1186},"迁移方案的抽象层次",[10,1188,1189,1190,703,1193,703,1196,1199],{},"面对这三处耦合，如果采取\"完全抽象\"的思路——把工作流定义成一个通用的步骤编排引擎，每个平台只需要实现 ",[193,1191,1192],{},"execute_step",[193,1194,1195],{},"store_result",[193,1197,1198],{},"charge_resource"," 这样的抽象接口——理论上很优雅，但代价是引入了一层不必要的复杂性。通用引擎要支持各种平台的差异，势必要留下很多可配置项，导致理解和维护难度上升。",[10,1201,1202],{},"实际采取的方案是有选择的复用：",[10,1204,1205,1208],{},[42,1206,1207],{},"复用稳定部分","：工作流的步骤编排和领域逻辑。小说工作流的七个阶段顺序（设定 → 宏观 → 世界观 → 角色 → 分卷 → 拆章 → 正文）是领域知识，与平台无关。这套提示词组装、JSON 解析、规范化、展示文本提取的逻辑，从旧平台完整迁移到新平台，TypeScript 化但不改核心算法。漫剧工作流的四阶段（剧本 → 资产 → 分镜 → 成片）也是如此。",[10,1210,1211,1214,1215,703,1218,703,1220,703,1223,1226,1227,1230,1231,1234],{},[42,1212,1213],{},"重新实现易变部分","：任务调度、存储、计费。小说模块新增 ",[193,1216,1217],{},"NovelProject",[193,1219,1126],{},[193,1221,1222],{},"NovelChapter",[193,1224,1225],{},"NovelTask"," 等 Prisma 模型，完全按新平台的设计来。生成任务在开始前调用 ",[193,1228,1229],{},"billing.reserveResource()","，成功后提取展示文本、调用 ",[193,1232,1233],{},"billing.settleResource()","，失败时退款。这套接口与 Image Generation 任务的流程完全一致，在平台已有的基础上建造。",[10,1236,1237,1238,1241,1242,1245],{},"具体的模型调用、LLM 的参数、生成的超时策略这些\"具体模型调用\"的细节，留在各自的适配层。例如小说模块的 ",[193,1239,1240],{},"novel-generation.ts"," 只负责组装提示词、调用 LLM、解析结果，不涉及数据库操作；数据库操作交给 ",[193,1243,1244],{},"novel-service.ts","。这样领域逻辑与平台逻辑的边界清晰，后续不同平台可以并行维护。",[17,1247,1248],{"id":1248},"关键的取舍决定",[10,1250,1251],{},"完全抽象的诱惑在于\"一套代码支持多平台\"的承诺。但这需要引入足够的可配置性和接口设计，而代价是灵活性反而下降——当某个平台需要特殊处理某个步骤时，通用引擎不得不打补丁。这在两个平台差异较大的情况下尤其成立。",[10,1253,1254],{},"选择有针对性的复用，意味着承认重复：数据模型要各写一份，任务调度逻辑要各写一份。但这个重复是可控的，因为它们在各自平台内部是一致的。小说模块的任务预留\u002F结算逻辑完全复用 Image Generation 已有的那套，不需要新增抽象层。",[10,1256,1257,1258,1261],{},"另一个关键的取舍是用户模型的配置。旧平台可能让用户选择小说生成用哪个模型，新平台则完全隐藏模型选择，由后台配置或环境变量决定。这简化了前端和 API 的设计——用户请求体里根本不接受 ",[193,1259,1260],{},"model"," 字段，Billing 也不需要按模型定价。代价是失去了\"用户自行选择成本与质量的平衡\"的灵活性，但换来了计费模型的清晰和后台的可控。",[10,1263,1264],{},"这类决定需要在迁移前明确：哪些能力是目标平台\"必须有\"的，哪些是\"很有但可以先不做\"的。漫剧工作流迁移时明确排除了白模（3D 白模式）视频相关的所有功能，不复制 Blender 依赖、不暴露白模提示词输入、成片阶段只处理普通视频。这个决定减少了迁移的复杂性，也避免了在新平台上重新部署 Blender 和白模渲染的基础设施。",[17,1266,1267],{"id":1267},"稳定部分与易变部分的边界",[10,1269,1270],{},"这个分法的关键在于，稳定部分要确实稳定。领域逻辑（各阶段的提示词、章节的上下文组装、长篇的记忆管理）在两个平台上是一样的，因为它反映的是小说创作或漫剧创作的规律。但是，一旦涉及\"这个阶段的输入从哪里读、输出存到哪里、失败后怎么处理\"，就已经是平台相关的。",[10,1272,1273],{},"以小说的世界观生成为例：",[10,1275,1276,1277,1280],{},"稳定部分是 ",[193,1278,1279],{},"worldPrompt(previousSections, projectSettings)"," 这个函数，它组装 LLM 提示词，描述\"根据设定和前序阶段的输出，生成世界观\"。这个函数与平台无关。",[10,1282,1283],{},"易变部分是：",[47,1285,1286,1293,1296,1306],{},[50,1287,1288,1289,1292],{},"生成前如何预留算力点（新平台调 ",[193,1290,1291],{},"billing.reserveResource","，旧平台可能不调）",[50,1294,1295],{},"生成结果是一个 JSON 对象，如何从中提取展示文本（两个平台的数据模型可能不同，但提取逻辑应该是一样的——属于稳定部分）",[50,1297,1298,1299,1302,1303,403],{},"提取后的展示文本存到哪里（新平台的 ",[193,1300,1301],{},"NovelSection.displayText"," 字段，旧平台可能是本地文件 ",[193,1304,1305],{},"sections\u002Fworld.json",[50,1307,1308,1309,1312],{},"失败时如何处理（新平台调 ",[193,1310,1311],{},"billing.refundResource","，旧平台可能是清除临时文件）",[10,1314,1315,1316,703,1319,1321,1322,703,1324,1327],{},"这样划分后，新平台只需要在适配层（",[193,1317,1318],{},"novel-routes.ts",[193,1320,1244],{},"）重新实现存储和计费部分，核心生成逻辑（",[193,1323,1240],{},[193,1325,1326],{},"novel-prompts.ts","）从旧平台迁移过来。",[17,1329,1330],{"id":1330},"实际的迁移清单",[10,1332,1333],{},"从这个分析，迁移的具体工作清单变成：",[379,1335,1336,1342,1348,1354],{},[50,1337,1338,1341],{},[42,1339,1340],{},"评估可复用的代码"," — 在旧平台找出真正与平台无关的部分。对小说工作流，这包括提示词、JSON 解析、规范化逻辑。对漫剧工作流，这包括分镜拆分的逻辑、资产提取的规则。",[50,1343,1344,1347],{},[42,1345,1346],{},"定义新平台的合同"," — 每个工作流步骤的输入输出是什么，资源消耗如何计量。小说阶段的输出是规范化 JSON 加展示文本，消耗的资源是可见字符数。漫剧镜头的输出是镜头配置和首帧图片，消耗的资源是视频秒数（按模型和分辨率估算）。",[50,1349,1350,1353],{},[42,1351,1352],{},"实现平台适配层"," — 数据模型（Prisma 表）、任务调度（与 Billing 集成）、错误处理和重试。这部分代码是新平台特有的，不能也不应该复用。",[50,1355,1356,1359],{},[42,1357,1358],{},"测试边界"," — 在适配层写测试，验证计费逻辑、任务状态机、错误恢复。在稳定部分写测试，验证生成质量、规范化正确性。",[10,1361,1362],{},"不采用这样的分层，而是直接把旧平台的 Next.js 路由、本地文件操作、命令式 API 端点搬到新平台，结果是新平台沦为旧平台代码的容器。后续需要升级某个依赖、调整计费模型、或对接新的生成模型时，都会发现改一个地方影响到另一个地方。",[10,1364,1365],{},"设置清晰的边界，允许有选择的重复，是避免这个问题的方法。",{"title":324,"searchDepth":325,"depth":325,"links":1367},[1368,1373,1374,1375,1376],{"id":1087,"depth":325,"text":1087,"children":1369},[1370,1371,1372],{"id":1091,"depth":507,"text":1091},{"id":1113,"depth":507,"text":1113},{"id":1141,"depth":507,"text":1141},{"id":1186,"depth":325,"text":1186},{"id":1248,"depth":325,"text":1248},{"id":1267,"depth":325,"text":1267},{"id":1330,"depth":325,"text":1330},"2026-07-02",{},"\u002F2026-07-02",{"title":1076,"description":1081},"2026-07-02-工作流迁移与复用","两条生产工作流从旧平台迁移到新平台，难点不在业务逻辑重写，而在解耦三处平台依赖——任务调度、存储约定、计费接口。",[1384,1385,1386,1387,1388,1389],"架构","工作流","微服务","平台化","任务队列","计费系统","zEJUEqZgsQkHJOxHWOoc4WL1iYD_NaAli4VZYCye-ZY",{"id":4,"title":5,"body":1392,"column":334,"date":335,"description":12,"extension":336,"hero_image":337,"meta":1607,"navigation":339,"path":340,"seo":1608,"series_id":337,"severity":337,"stem":342,"summary":343,"tags":1609,"__hash__":351},{"type":7,"value":1393,"toc":1598},[1394,1396,1398,1400,1402,1404,1406,1408,1410,1412,1416,1422,1426,1430,1434,1438,1442,1446,1452,1454,1456,1458,1462,1466,1470,1474,1478,1482,1486,1490,1494,1498,1500,1502,1504,1506,1508,1510,1512,1514,1518,1544,1550,1556,1558,1560,1566,1572,1580,1588,1590,1594,1596],[10,1395,12],{},[10,1397,15],{},[17,1399,19],{"id":19},[10,1401,22],{},[10,1403,25],{},[10,1405,28],{},[10,1407,31],{},[17,1409,35],{"id":34},[10,1411,38],{},[10,1413,1414,45],{},[42,1415,44],{},[47,1417,1418,1420],{},[50,1419,52],{},[50,1421,55],{},[10,1423,1424,61],{},[42,1425,60],{},[47,1427,1428],{},[50,1429,66],{},[10,1431,1432,72],{},[42,1433,71],{},[47,1435,1436],{},[50,1437,77],{},[10,1439,1440,83],{},[42,1441,82],{},[47,1443,1444],{},[50,1445,88],{},[10,1447,91,1448,95,1450,99],{},[42,1449,94],{},[42,1451,98],{},[10,1453,102],{},[17,1455,106],{"id":105},[10,1457,109],{},[10,1459,1460,115],{},[42,1461,114],{},[47,1463,1464],{},[50,1465,120],{},[10,1467,1468,126],{},[42,1469,125],{},[47,1471,1472],{},[50,1473,131],{},[10,1475,1476],{},[42,1477,136],{},[47,1479,1480],{},[50,1481,141],{},[10,1483,1484,147],{},[42,1485,146],{},[47,1487,1488],{},[50,1489,152],{},[10,1491,1492,158],{},[42,1493,157],{},[47,1495,1496],{},[50,1497,163],{},[10,1499,166],{},[10,1501,169],{},[17,1503,173],{"id":172},[10,1505,176],{},[10,1507,179],{},[10,1509,182],{},[17,1511,185],{"id":185},[10,1513,188],{},[10,1515,191,1516,196],{},[193,1517,195],{},[47,1519,1520,1524,1528,1532,1536,1540],{},[50,1521,1522,204],{},[193,1523,203],{},[50,1525,1526,210],{},[193,1527,209],{},[50,1529,1530,216],{},[193,1531,215],{},[50,1533,1534,222],{},[193,1535,221],{},[50,1537,1538,228],{},[193,1539,227],{},[50,1541,1542,234],{},[193,1543,233],{},[10,1545,237,1546,241,1548,245],{},[42,1547,240],{},[193,1549,244],{},[10,1551,248,1552,252,1554,256],{},[193,1553,251],{},[193,1555,255],{},[17,1557,259],{"id":259},[10,1559,262],{},[10,1561,1562,268,1564,271],{},[42,1563,267],{},[193,1565,244],{},[10,1567,1568,277,1570,280],{},[42,1569,276],{},[193,1571,209],{},[10,1573,1574,286,1576,289,1578,293],{},[42,1575,285],{},[193,1577,209],{},[193,1579,292],{},[10,1581,1582,299,1584,303,1586,306],{},[42,1583,298],{},[193,1585,302],{},[193,1587,292],{},[17,1589,309],{"id":309},[10,1591,312,1592,316],{},[42,1593,315],{},[10,1595,319],{},[10,1597,322],{},{"title":324,"searchDepth":325,"depth":325,"links":1599},[1600,1601,1602,1603,1604,1605,1606],{"id":19,"depth":325,"text":19},{"id":34,"depth":325,"text":35},{"id":105,"depth":325,"text":106},{"id":172,"depth":325,"text":173},{"id":185,"depth":325,"text":185},{"id":259,"depth":325,"text":259},{"id":309,"depth":325,"text":309},{},{"title":5,"description":12},[345,346,347,348,349,350],1785406912222]