普通视图

发现新文章,点击刷新页面。
昨天以前首页

如何在本地跑 Core ML 模型识别呼噜声,并用 iCloud 优雅同步?

作者 Flutter笔记
2026年4月2日 15:00

大家好,我最近开发了一款App《SleepDiary(睡眠声音日记)》。

9771791b1b272012179e60c5853cedc8.jpg

作为一款睡眠监测类 App,核心业务逻辑可以用一句话概括:

录一整夜的音,把打呼噜和说梦话的片段摘出来,最后生成睡眠报告。

看似简单,但在工程实现上却困难重重:

  1. 隐私与成本问题:长达 8 小时的音频绝对不能一整段传到服务器端,这不仅会直接把你的服务器带宽跑破产,还会被用户骂死(谁敢把在卧室一整夜的录音全传到网上?)。
  2. 性能与功耗问题:放在端侧跑模型,势必要使用长时间的后台保活,如何避免手机发热和 OOM (Out Of Memory)?

经过最近这段时间的研究,我用 AVFoundation + Core ML + SwiftData 的纯血原生技术栈把这套流程跑通了。今天就和大家分享一下我的实现思路与踩坑日记。

一、端侧的 AI:硬核从零训练自己的鼾声分类模型

最初的设计方案很简单粗暴:开个录音,每秒去判断分贝,超过阈值就保存。但这完全不行,深夜翻身的声音、空调声、外面的汽车声都会被误判。

市面上现成的声音分类模型要么太大(动辄上百MB),要么对“鼾声”、“梦话”这种特定场景不够敏锐。于是我决定硬核一点——自己动手,从收集数据开始训练一个专用的轻量级神经网络(SnoreWave.mlpackage)。

1.1 数据收集与模型训练

为了让模型足够精准,我花了大量时间收集开源数据集并结合自己实录的各种“打雷级”打呼声(最终 1.2w 条数据)。 把杂乱的音频转换成模型能“看懂”的输入是第一步——将音频流转化为梅尔频谱图(Mel-spectrogram) 。这相当于将一维的声音信号,变成了二维的图像图像特征,然后再喂给我用深度学习框架搭建的 CNN(卷积神经网络)进行分类。

模型训练收敛后,我依靠 coremltools 将其转换为了 Apple 原生支持的 .mlpackage。为了控制 App 包体积并保证低功耗运转,这个模型被我极致压缩,剥离了非必要分支,达到了极高的预测效率。

1.2 AVAudioEngine 实时截流送显

有了自己的模型,下一步就是在 iOS 端跑通流式推理。 我们不使用高层的 AVAudioRecorder,而是使用 AVAudioEngine。因为它允许我们通过 installTap 在音频流经过的过程中“截胡”到 AVAudioPCMBuffer

然后在端侧把这个 Buffer 原样转化成模型需要的数组输入:

// 截胡音频流的伪代码
let inputNode = audioEngine.inputNode
let format = inputNode.inputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] (buffer, time) in
    // 捕获到音频帧后,交给我们自定义的分类器管线
    self?.audioCaptureService.processAudioBuffer(buffer)
}
audioEngine.prepare()
try audioEngine.start()

1.3 降维打击 OOM 崩溃:用 Actor 隔离模型生命周期

坑点来了!如果每次截取到一个 buffer,都在主线程或者随机的 Dispatch Queue 去实例化这个自定义模型进行预测,一晚上下来你的 App 必因为内存暴涨被系统强制 Kill 掉(Jetsam Event)。

解法:引入 Swift Actor 隔离与复用机制 

在《睡眠声音日记》中,我是用全局唯一的 Actor 来维持模型的单一生命周期,使用环形缓冲区去缓存几秒钟的声音片断,组合后一次性输出:

swift
actor EventDetectionPipeline {
    // 全局唯一持有我们自己训练好的模型实例
    private let model = try? SnoreWaveformCNN(configuration: MLModelConfiguration())
    
    func processAudioWindow(_ window: AudioWindow) async {
        // 将音频转化成梅尔频谱所需的 MLMultiArray
        guard let multiArray = window.toMLMultiArray() else { return }
        
        // 发起端侧离线推理
        if let prediction = try? model?.prediction(input: multiArray) {
            if prediction.classLabel == "snore" {
                // 命中目标:触发存储!
                await persistCapturedEvent(label: .snore)
            }
        }
    }
}

通过自己训练轻量级模型 + Actor 的串行数据处理,保证了模型资源的极致释放。即便后台连续疯狂推理 8 个小时,CPU 的平均占用率也能被压在极低的水平,用户即使整晚充着电,手机也完全不发烫。

二、存储的艺术:音频文件与 SwiftData 模型分离

识别完事件后,怎么持久化? 这引发了第二个大问题——千万别把音频这种大块二进制流全都写进 SwiftData 或者 Core Data!

2.1 相对路径是王道

我的存储策略是:结构化数据走 SwiftData(打点时间、标签量化数据),音频文件走沙盒原生写入。 在《睡眠声音日记》的 SleepEventRecord 模型中,我只存了一个相对路径(filePath)。

@Model
final class SleepEventRecord {
    var timestamp: Date
    var duration: TimeInterval
    var eventLabel: EventLabel // .snore, .speech, .cough
    var filePath: String? // 只存相对路径: "20240315/snore_0234.m4a"
    
    init(timestamp: Date, eventLabel: EventLabel) {
        self.timestamp = timestamp
        self.eventLabel = eventLabel
    }
}

为什么要相对路径?  因为沙盒路径(UUID)在每次应用重签或重新安装时是会变的。如果存绝对路径,第二天文件全找不到了!读取时,永远使用 FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(filePath) 动态拼装。

2.2 防治 iCloud 把服务器挤爆

录了一晚上的高音质 M4A 文件,如果不加限制,系统的 iCloud 备份会自动把它们全传上去。用户那可怜的 5GB iCloud 很快就会爆满。因此,我在写入音频文件后,立马用原生 API 给文件打上“拒绝备份”的 Tag:

var url = documentDirectoryURL.appendingPathComponent(fileName)
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true // 保护用户的 iCloud 空间!
try url.setResourceValues(resourceValues)

三、私有 CloudKit 的优雅同步体验

音频不用同步了,但我们的 SleepSessionRecord(当晚评分,鼾声次数统计,分析数据)需要跨设备(尤其是和 Apple Watch 联动时)和防止在用户删掉 App 重装后丢失。

以往做 Core Data + CloudKit 繁琐得让人想死。但在 iOS 17 的 SwiftData 下,它变成了真正的“优雅”:我们甚至可以动态控制它的开启闭合。

我把开关值存到了 NSUbiquitousKeyValueStore(KVS),根据这个从远程同步过来的用户偏好,动态初始化 ModelContainer

// 在 SleepDiaryApp.swift 入口处动态配置容器
var sharedModelContainer: ModelContainer = {
    let schema = Schema([SleepSessionRecord.self, SleepEventRecord.self])
    
    // 读取 UserDefaults/KVS 的 iCloud 开关
    let isCloudSyncEnabled = UserDefaults.standard.bool(forKey: "iCloudSyncEnabled")
    
    let configuration = ModelConfiguration(
        schema: schema,
        isStoredInMemoryOnly: false,
        // 如果开启,赋予 private 数据库标识;如果不开启,设为 .automatic 或本地优先
        cloudKitDatabase: isCloudSyncEnabled ? .private("iCloud.xxxxx") : .none
    )
    
    do {
        return try ModelContainer(for: schema, configurations: [configuration])
    } catch {
        fatalError("Could not create ModelContainer: (error)")
    }
}()

依靠云端大容量配额下的 .private 标识,只要用户打开 iCloud 同步,他们在换了新手机后重新下载 App,所有的睡眠数据历史记录就会像魔法一样哗哗哗回到列表中。


四、写在最后

开发《睡眠声音日记》的这段时间里,我最大的感触是:苹果的原生护城河真的很香。  只依靠一套 Swift 兵器库:从 SwiftUI 的丝滑动画绘制、到 HealthKit 获取深度睡眠的联动、再到 Core ML 的底层加速,这是以往杂糅其它中间件完全得不到的性能优势和开发爽感。

感兴趣的同行们,可以在 App Store 搜  “睡眠声音日记-SleepDiary”  下载把玩一下,有任何架构或技术点上的建议,大家评论区见,或者私下找我交流。也欢迎吐槽!

我做了一个鼾声记录App,聊聊背后的功能设计

作者 Flutter笔记
2026年3月27日 10:47

最近做了一款叫「睡眠声音日记」的App,主要用来记录睡眠时的鼾声和梦话。

今天主要聊聊这个App的功能设计思路。

为什么做这个App?

起因很简单:我自己打鼾,但完全不知道每晚打多少、什么时候最严重。 市面上的睡眠App大多侧重睡眠阶段分析,对鼾声的处理比较粗糙。我想做一个真正能听清楚每一段鼾声的工具。

核心:ML鼾声识别

App用CoreML跑了一个本地训练的声音分类模型,实时区分鼾声和人声(梦话)。每检测到一段就自动裁剪保存音频片段,第二天可以逐段回听。

不依赖网络,所有识别都在本地完成,隐私上比较放心。

灵敏度做了三档可调,适配不同噪音环境。

睡眠评分:5个维度

单纯告诉用户"你昨晚打了12次鼾"其实没什么指导意义,所以我做了一套100分制的评分系统,拆成5个维度:睡眠时长、鼾声/呼吸、深睡质量、睡眠连续性、身体恢复。每个维度单独打分,用户一眼就能看出问题出在哪。

AI个性化分析

接入了大模型做每日分析。不是泛泛的建议,而是把用户昨晚的实际数据(鼾声次数、时段分布、评分、HealthKit数据)传进去,生成针对性的建议。

历史页面还有基于多晚数据的趋势分析,能发现长期规律。如果鼾声连续多晚偏重,会主动建议用户去做专业评估。

趋势可视化

做了7晚和30晚两个维度的趋势图表:鼾声趋势、评分趋势、心率趋势、血氧趋势、睡眠时长柱状图。

还有一个昼夜节律分析,记录满5晚后自动解锁,分析用户的时型(早起型/夜猫子)。

这些图表对于观察干预效果很有用——比如换了枕头之后鼾声是不是真的少了。

Apple Watch用户体验拉满

如果你有Apple Watch,体验会更完整:

  • 手表上能看录音状态,直接停止记录
  • 昨晚的评分、鼾声、时长一目了然
  • 详情页有睡眠阶段时间线(核心/深睡/REM),鼾声事件直接叠在上面,一眼看出"你在深睡的时候鼾声最重"
  • 心率、血氧趋势图也有

小组件 + 灵动岛

桌面小组件做了3个尺寸,核心交互是一键开始/停止记录。大号组件额外展示鼾声时间分布图。录音期间支持灵动岛实时活动,锁屏上也能看到计时和事件计数。

其他细节

  • iCloud多设备同步
  • 数据备份恢复,支持导出
  • 音频自动清理(3/7/14/30天),重要片段可钉住跳过清理
  • 睡眠目标 + 睡前提醒
  • 成就系统,增加使用粘性
  • 一键生成分享图片,方便发给医生或朋友
  • iPad侧边栏适配

订阅模式

月订阅6元,年订阅38元,终身买断68元。

欢迎试用,有反馈随时评论区交流~

独立开发了一个睡眠记录 App:SleepDiary / 睡眠声音日记

作者 Flutter笔记
2026年3月18日 14:50

最近一直在弄一个睡眠记录 App,名字叫

SleepDiary / 睡眠声音日记

已经上架 App Store 了,直接搜名字可以搜到。

这东西我其实做了挺久了,中间一直在反复改。
最近才慢慢觉得,差不多到了一个能发出来聊聊的阶段。

最开始想做,原因也不复杂。
就是我自己之前看过一些睡眠类产品,总觉得不太顺手。
要么广告很多,要么功能特别杂。
本来只是想看看晚上有没有鼾声、睡得怎么样,结果经常还得被一些很重的流程打断。

所以我做这个的时候,想法一直挺简单的:
打开就能录,醒来能看,别整太复杂。

我自己这段时间已经连续用了很多晚,边用边改。
首页、历史、单晚详情这些地方都来回调了很多次。
尤其是单晚详情,我自己会比较在意。
因为如果只是告诉你“昨晚有鼾声”,其实没什么意思。
但如果能大概看到什么时间段更明显,和整晚记录能对起来,那这个东西至少会更像个能参考的工具。

还有就是,这类 App 本来就比较私人。
毕竟会涉及睡眠、声音这些东西。
所以我自己做的时候,鼾声识别都是用的端侧模型,不会上传。
不一定一步到位,但至少会一直往这个方向抠。

发出来主要也是想看看,这里会不会有人对这种东西有兴趣。
不是想听“看起来不错”这种话,主要还是想知道更实际一点的:

你会不会真的用睡眠记录类 App?
你到底会看什么?
是看单晚,还是看一段时间的变化?
如果是鼾声、梦话这种信息,你会希望它怎么给你看,才算有用?

有想法都可以聊聊。

❌
❌