前面十五章讲的是「为什么这么做」。这一篇不讲道理,只列东西——把 740 次提交里长出来的每一件事,按模块摆出来。它更像一张零件清单:你可以从任何一行进去,回头翻到对应的章节看它为什么长成这样。
看法:粗体是这个模块的骨头,缩进的小字是它身上真实存在的细节。数字都是实数,不是约数。
一、录音与上传
- 一键录音:打开即录,红键是全屏接管界面,点一下开始,不做「按住说话」。
- 暂存名 → 正式名:录制中文件叫
recording-<ts>.m4a,录完 finalize 成功才改名成VoiceDrop-<时间>-<时长>-<星期>-<时段>[-<城市>-<区>].m4a。改名这一步就是「这条录音完整」的凭证——半截文件永远拿不到正式名。 - 文件名即元数据:时间、时长、星期、时段、城市区县全写进文件名,服务端不用查库就知道这条录音是什么。
- 为语音调过的编码:单声道 AAC,标准档 16 kHz / 32 kbps、高质档 24 kHz / 64 kbps。原来是 44.1 kHz——但这段音频只喂给播放器和火山 ASR(原生就是 16 kHz),高采样率纯属浪费。改完 5 分钟录音从 2.4 MB 降到约 1.2 MB。
- Documents 目录就是待传队列:盘上还躺着
VoiceDrop-*.m4a= 还没传完。不另建队列表,因为任何独立的队列都可能和真实文件对不上。 - 后台传输:走 background
URLSession,录完立刻锁屏、切走、进程被杀,传输都由系统守护进程接着做完。完成事件在进程复活后回放,收尾逻辑幂等——「进程一直活着」和「隔世重启」两条路收敛到同一结果。 - 重灌触发点:App 启动、网络恢复(
NWPathMonitor)、新录音入队、前台刷新,任一发生就重扫盘上残留。 - 录音时拍照:停止键右边一个极淡的相机图标,点开是不打断录音的取景器(只开视频通道)。方形取景、九宫格、连拍成胶片条、可逐张删、支持相册导入(≤9 张)、前后置翻转。旋转角度读手机物理朝向,不写死角度。
二、挖矿:一段声音怎么变成文章
- 火山 ASR:中文转写走豆包,文件式接口
volc.bigasr.auc。 - Claude 挖文章:一段录音可以挖出多篇,不是一篇。结构化输出直接落成文章数组。
- 四种「没东西可写」:
corrupt(文件坏)、silent(没声音)、no-speech(有声音但没人说话)、too-short(太短),外加no-article(说了但挖不出文章)和asr-error:<码>。任一命中就写.empty标记——App 统一显示「无语音」,但服务端知道具体是哪一种。 - 「已处理」的定义:
.json或.empty存在即已处理。音频永不自动删除,只有用户在 App 里删。 - ASR 断点:转写结果单独存 checkpoint,重挖不重复付 ASR 的钱。
- 请求级热词:单一真源
agent/src/asr-hotwords.js,批量挖矿直接带corpus.context,流式路径由 Worker 拦帧注入。加个词只改数组。 - 触发路径三条:上传落盘即派发、每 6 小时定时、手动
POST /agent/mine/trigger。 - 逐条日志:每条录音的处理过程写
minelogs/<日期>/,每次 Claude 调用的原始请求响应写llmlogs/<日期>/(30 天生命周期),都有管理页可看。
三、文风
- 文风是带版本的资产:
CLAUDE.json,schema-3 版本信封,和文章用同一套版本机制,走专用/files/api/style路由读写。 - 版本号跟着文章走:
articles[i].style = N是 per-article 字段。曾经是正文里的隐形注释<!-- style: 风格 vN -->,因为 iOS 剥注释、服务端不剥,导致「改第 3 行」错行 +1,一次性迁移掉了:扫 382 篇、迁 50 篇、线上残留 0。 - 蒸馏文风:喂样本进风格数据集(
style/<id>.json),从样本里提炼文风,而不是让你从预设 tone 里挑一个。 - 换风格复用旧版本:换回用过的风格时匹配已有版本,不重新生成。
四、改稿
- 用嘴改稿:长按红键对某篇文章说「把第三行改得口语一点」,一条 WebSocket 连到
ArticleEditor这个 Durable Object。 - 行号由服务端算:模型不会数数,所以行号是服务端编好递进去的(
linenum.js),编号前把一切 HTML 注释剥掉。 - 严格串行:多条指令在客户端排队,上一条返回
updated才发下一条——每一刀都改在上一刀的结果上。 - 听写走火山流式:不用 Apple 本地识别。服务端是哑中继,注入密钥、剥掉客户端 Authorization,App 永远碰不到火山凭证。
- 长按菜单:长按配图 → 图片风格(宫崎骏 / 广告 / 水彩 / 素描 / 油画 / 胶片);长按段落 → 改写这段(更简洁 / 更口语 / 更书面 / 扩写)、插入公众号题图、拷贝。菜单内容是服务端配置(
GET /agent/ui-config),改 R2 就能改文案,零部署。 - 版本与撤销:每次改动铸一个版本,可回看历史、可切回任一版。
- 编辑用 Haiku:挖矿仍用 Opus。模型 id 写在 R2
config/model.json,每轮动态读,换模型零部署。
五、照片
- 标记即键:正文里的
[[photo:<相对 key>]],token 本身就是照片的 R2 键。早期用的是[[photo:N]]下标,改一张就把后面全改错位,现在自包含、零耦合。 - 键的形状:
photos/<录音时间戳>/<偏移秒>-<3位随机>.jpg。偏移秒就是「第几秒加进来的」;三位随机尾是为了同一秒内多张照片不互相覆盖——九张相册导入的回调几乎同时触发,旧方案会静默丢照片。 - 一个照片端点:
GET /files/api/photo/<完整 key>,公开无鉴权。社区、公开分享页、导出、甚至主人自己的详情页,全走这一个。 - 一张都不能丢,三层保证:① 写盘前重新列一次照片目录(兜住照片在 ASR 期间才传完的竞态),正文没引用的按序补到末尾;② 晚到的照片由服务端补写——照片落盘 poke 一下 Miner,60 秒合并窗口后回头把它塞进已成文的文章,并铸一个
photo-backfill版本;③ iOS 有落盘队列,成功才删。 - 删掉的不复活:补写时算的是「所有版本里出现过的键」的并集——出现过又被你删掉的,是你的意志,永不复活。
- AI 改图:口述「把这张变成广告」→ 蒸馏 prompt → 调 paint 服务出图 → 回调写回。失败写原图副本、不扣费。
六、出口:文章往哪儿去
- 公开分享链接:
voicedrop.cn/<短码>,短码是HMAC(key)的前 10 位。 - 分享卡片元标签:
og:title、og:description、og:image全部随?s=<第几段>变——你分享的是哪一节,卡片就显示哪一节的标题、摘要和那一节自己的第一张照片。微信的抓取器只读<meta name=description>不读og:description,所以两个都发。没照片的段落就不发og:image,是一张干净的文字卡——不硬塞一张通用banner,因为每次分享都同一张图看起来像垃圾。 - 公众号草稿:同步发布,等真实结果(
created/updated或真实 errcode),不是发完就不管。Markdown → 微信 HTML 的转换在东京那台 VPS 的 relay 上。十张共享题图按哈希分配。 - 小红书图文包:
xhs_pack,打成可直接发的图文。 - 微信小程序:文章、社区、书三类分享路径,iOS 和安卓分享给好友时发的是小程序卡片。
- 导出:带照片的 HTML 导出,标记在不能显示照片的地方一律剥掉。
七、VD社区
- 帖子是活指针:
community/<shareId>.json只存{shareId, owner, articleKey, author, firstSharedAt},不拷贝正文。改原文,社区里立刻跟着变;删原录音,指针自动回收。 - 合规四件套(Apple 审核指南 1.2):举报、拉黑、内容过滤、24 小时内处理。被举报的帖子连公开分享页一起 404。
- 投币:读者可以给作者投算力。点赞绝不碰钱——点赞是表态,投币是转账,混在一起两个都不成立。
- 推荐排序是可拔掉的旁车:
voicedrop-reco单独一个服务,拔掉它社区照常工作,只是排序退回时间序。 - 举报当负权重:举报信号进排序,不只是删帖开关。
八、算力
- 锚死的换算:23 算力 = 1 元。单位无现金价值、不可提现、不可退款。
- 微元整数:钱一律以 1e-6 元的整数存,一律
Math.ceil——只多收不少收,绝不亏。未知模型成本记 0。 - 手感:一条录音挖一篇约 2 算力,Haiku 改一刀约 1.4,Opus 约 4,写一本书一口价 320。新用户送 200。
- 两张表:
account(余额 / 已赠 / 已花)+ledger(只追加的流水,带余额快照)。用db.batch()原子化。 - 闸门在花钱之前:挖矿在 ASR 之前判、编辑在调 Claude 之前判,不够就拒绝,不空花。
- fail-open:扣费失败绝不中断挖文章——宁可少收一次钱,不能让用户的录音卡住。
- 非终态与终态:余额不足写
.blocked标记,是非终态(下次够了删标记重挖);录音超 3 小时是终态。 - 硬闸:单条录音 ≤3 小时、单篇编辑 ≤100 次(数的是真实编辑次数,不是 Claude 调用次数)。
- 邀请奖励:三层归因(链接 / IP / 剪贴板),按币记价的实时汇率入账。
九、写书与书架
- 四个角色:建筑师切大纲、写手每章一个并行写、评审独立复核(绝不是本章写手、不喂写手思路)、发布器增量上线。
- 过审即发:每章写完过审就立刻发布,绝不攒到最后。会话断了、任务崩了,已发的章一章不丢。
- 三种书:科普(费曼式)、绘本(图为主,创建即隐藏,主人验收后上架)、小说。三种共用一套骨架,只换写作模块。
- 修书是一条永久对话线:这本书的历史里,第一条是开书种子,之后每条是修改指令加回执。
- 有声书:章节页「听本章」,首播火山流式边合成边播并写回 R2,二播直接放 mp3 且支持 Range 请求。
- 整本下载 PDF:Browser Rendering 渲染一个专门的整本打印视图,结果缓存到 R2;修书后自动失效重渲。
- 封面:竖版 1024×1536,文字由模型直接画进图里,不用任何本地叠字工具。
十、MCP:44 个工具
第十五章讲过它是什么,这里只把清单列全:文章读写与版本、文风读写与蒸馏、挖矿与重写、社区读写与投币、算力余额与账单、分享链接与公众号与小红书、书架读书与写书修书、提示词分享与导入。登录是 6 位码加 4 位码的两步握手,握手只活 2 分钟。
十一、账号
- 匿名优先:一装 App 就有一个匿名能力令牌(存 iCloud 钥匙串),不登录也能全功能用。
- Apple 登录只是绑定:把 Apple ID 绑到已有的匿名盒子上,绑完 scope 还是
users/anon-<哈希>/——匿名和登录解析到同一个人。 - 设备配对:新设备用 6+4 握手登录老账号。
- 安卓微信登录:换 token 在 Cloudflare Pages Function 上。
- 账号删除:一次调用删干净——用户目录全部对象、本人社区帖及其举报标记、指向本人的短链、Apple 绑定。算力流水保留(成孤儿,无害)。
十二、地基
- 按商店区域定线路:入口主机由 App Store 商店区域运行时决定,竞速探测已经拆掉——确定性比自适应值钱。
- 国内边缘:
voicedrop.cn走腾讯 EdgeOne 边缘函数反代,照片在国内边缘缓存。 - 存储收口到 D1:小 JSON 从 R2 迁进
voicedrop-core(10 张表),双写期结束后 R2 副本停写停读、D1 成唯一真源;大文件(音频、照片、书页)仍在 R2。 - 每晚备份:R2 数据由 Worker 加定时任务增量镜像,删除和覆盖的旧版本保 14 天。
- 实时状态:
StatusHub每个用户一个实例,挖矿每个阶段推一次,列表的徽标从「待处理 → 听录音 → 挖文章 → 已成文」自己走,不轮询。 - 意见反馈:写 R2 存档 + APNs 直推管理员,同一用户 60 秒只推一条,存档不节流。
- 报警:实时通道断了看
close的source字段区分是墙还是配额,APNs 节流报警。 - 测试即契约:服务端
agent/test/覆盖文章仓储、API 路由、工具、循环;iOS 侧 125 条纯逻辑单测,不打网络。向后兼容是靠测试钉住的。
十三、砍掉的
清单不列砍掉的东西就不诚实。第十二章讲过为什么,这里只记账:SDUI 自定义首页(Phase 1 写完了,从未进主干,存档在一个 tag 里)、多风格并排对比、挖矿实时预览、线路竞速探测、文章后处理轮询器(整套拆除:定时器、状态、环境变量、skill 全删)、加急处理按钮(路由还在,按钮没了)、转发沿用原码(等有真实传播量再说)。
这份清单会过时——写下它的那一天之后还会有新的提交。但它记录的不是功能有多少,而是一件事:每一行都曾经是一个具体的坑,或者一个具体的人在具体一天说「这里不对」。