「每日故事」是怎么写出来的
拾缕 的「故事」功能要在你点一下之后,把「照片里有什么、在哪儿拍的、那天天气如何」揉成一小段中文文字。这篇手记拆解它的工作方式,以及每个环节上画的隐私边界。
第一步:收集三类事实
故事的原料是结构化事实,不是照片本身。生成任何一天的故事前,引擎先收集三样东西:
- 内容 — 用系统 Vision 框架(
VNClassifyImageRequest)在本机识别照片,得到「美食 / 穿搭 / 风景 / 宠物」等标签,再映射成中文类别;云端附图需单独授权。 - 地点 — 读取照片自带的 GPS 元数据反查地名。注意这不需要定位权限:照片授权本来就带出元数据,系统
CLGeocoder完成反查,结果按约 1 公里粒度缓存,避免重复请求。 - 天气 — 调 Open-Meteo 的历史/预报接口。它免费、无需 API Key;请求只携带取整后的坐标和日期,结果按天缓存。
DayFactsCollector
├─ Vision 标签 (本机,无网络)
├─ CLGeocoder 地名 (按 ~1km 缓存)
└─ Open-Meteo 天气 (仅取整坐标 + 日期,按天缓存)
第二步:三种引擎,一次契约
事实到位后,由选定的「故事引擎」把它们写成文字。三种引擎遵守同一份输出契约,所以可以随时热切换:
- 智能(默认) — Apple 的设备端大模型(FoundationModels),在支持 Apple Intelligence 的机型上直接在手机里写作;不可用时自动回退到模板引擎。
- 模板 — 纯规则拼接,任何 iPhone 都能用,完全离线,速度最快,风格克制。
- 云端(自带 Key) — 接入用户自己的 OpenAI 兼容服务。Key 存在本机钥匙串。
云端引擎的默认边界
云端引擎是唯一「数据出设备」的路径,所以它的默认值被设计得非常保守:只发送结构化文字事实(日期、天气、地名、内容标签),不发送任何照片。附带压缩照片是一个独立的开关,藏在设置里,默认关闭,并且开启前有明确的确认说明。
一句话总结隐私模型:识别在本机,天气只送坐标和日期,云端默认只送文字,照片永远 opt-in。
缓存与离线
再次打开已保存的故事会读取本机副本。重新生成可能重新收集事实,并在选择云端时发起新的请求。保存失败会保留本次生成的文字,重试保存不会重新生成;前一次有效保存也会保留供恢复。小组件使用共享快照,互动保留另行校验 StoreKit 权益。
为什么「默认智能」是设备端
把「开箱即用的最佳体验」绑在设备端模型上,意味着没有 API Key 的用户也能拿到自然、有文采的中文叙事,而隐私默认值就是最强档。模板引擎兜底保证功能永远可用,云端引擎留给愿意自己配置的进阶用户——三层按需降级,谁都不被排除。
工程细节会随版本继续演化,行为契约以仓库内 spec/001-day-story.md 为准。下一篇设计手记:小组件设计笔记:一天五个时刻。