Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

144 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sonora

本地优先的个人 AI 电台。一个会说话的 DJ:读你的网易云听歌数据,按当下时间与心情排一段节目,写主持词、合成人声,把「主持词 + 歌」串成一条不断的流。

Sonora console

一次播放的完整链路:

NCM 听歌数据 ──► taste/context ──► LLM 决策(队列 + 主持词)
                                        │
                              intro 选择/兜底 ──► TTS 合成
                                        │
                     Swift 客户端 ──► 主持词音频 ─► 歌曲流 ─► 下一首

仓库结构

  • apps/sidecar/ — Node 无状态 sidecar。REST/SSE,封装 LLM / TTS / NCM / taste / intro 逻辑,不持有播放状态。
  • apps/macos/ — SwiftUI macOS 客户端(SPM 工程)。队列状态机、AVFoundation 播放、单栏 radio-console UI,并 supervise sidecar 子进程。
  • docs/superpowers/specs/ — 设计文档;docs/superpowers/plans/ — 实施计划;docs/m*-smoke.md — 各里程碑烟测清单。
  • public/assets/ — 应用图标源。icon.svg 是唯一源文件,npm run icons 从它生成各尺寸 PNG、兜底专辑图和 macOS 的 AppIcon.icns

当前状态

已完成 M0–M4b,正在 M4b 分支上做播放体验打磨;M5(打包 .app / 图标接入 / bundled sidecar)尚未开始

里程碑 内容
M0 sidecar 化:原 Node + PWA 单体拆成无状态 sidecar,PWA/Electron 删除
M1 macOS 骨架:fork 并 supervise sidecar 子进程,REST + SSE 客户端
M2 状态机 + 持久化:AppState / StateStore(JSON、debounce、损坏自愈)/ RadioOrchestrator
M3 AVFoundation 播放:AVQueuePlayer 串主持词 + 歌曲、Now Playing / 媒体键、流失效重取
M4a 核心播放 UI:单栏 radio-console(点阵 SONORA logo + LED 时钟、分段进度条、HOST 面板、queue、chat),主题三态
M4b 设置 / 账户 / 登录:CONTROL ROOM 设置抽屉(Account / Brain / Voice)、NCM 扫码登录、保存设置后 sidecar 自重启并自动重连

M4b 之后(截至 2026-07-24)的增量:

  • 内嵌 NCM API — 未配置 NCM_BASE_URL 时 sidecar 自动在本地随机端口起一个 NeteaseCloudMusicApi 实例,扫码登录与同步不再依赖自托管服务。
  • Mimo TTS voicedesign — 音色描述本身即 voice,resolveProviderTTS_URL 识别 mimo / step / fish。
  • LLM 内容风控 — 触发提供方过滤的艺人可通过 OPENAI_BLOCKED_ARTISTS 屏蔽:候选池过滤 + 出口脱敏 + 失败重试;taste 生成被拒时回落统计画像。
  • HOST 面板统一 timeline — intro 与歌词合并成一条时间线,跟随播放进度滚动高亮。
  • 队列准入契约 — LLM 正常时只接受带真实 intro 的曲目,且 TTS 合成完成才进队列;起播前保证 intro ready,歌曲流刷新不再打断播报。

快速开始

npm install          # workspace 装依赖
npm run sidecar      # 单独跑 sidecar(127.0.0.1:随机端口)
npm test             # sidecar 测试

启动后 sidecar 把端口与 token 写入:

~/Library/Application Support/Sonora/sidecar.port
~/Library/Application Support/Sonora/sidecar.token

所有 /api/*/shutdown 需带 X-Sonora-Token: <token> 头;/health/tts/<sha>.mp3 不需要。

macOS 客户端

开发模式(App 启动时自动 fork 仓库内的 sidecar):

cd apps/macos
SONORA_DEV_SIDECAR="$(cd .. && pwd)/sidecar" swift run

或在 Xcode 打开:open apps/macos/Package.swift,然后 Cmd+R(需在 scheme 里加同样的环境变量)。

环境变量:

  • SONORA_DEV_SIDECAR=<repo>/apps/sidecar — fork 仓库源码而非打包产物。开发模式必填:缺失时走 .bundled 模式(M5 才实装),app 直接进 failed
  • SONORA_EXTERNAL_SIDECAR_PORT=<port> — 不 fork,直连已在跑的 sidecar(配 SONORA_EXTERNAL_SIDECAR_TOKEN)。
  • SONORA_DATA_DIR=<dir> — 覆盖数据目录(测试隔离用)。

测试

npm test                      # sidecar:node --test,91 项
cd apps/macos && swift test   # 客户端:单元 + 集成,169 项

集成测试会真起一个 node sidecar,缺 node 时自动跳过。

swift test 需要完整 Xcode.app —— Command Line Tools 不含 xctest runner,只能编译不能执行。

REST / SSE 接口

POST /api/show/generate         LLM 决策 + NCM hydrate + intro 选择
POST /api/track/hydrate         单曲水合
POST /api/track/refresh-stream  重新取 NCM stream URL(短时效,会过期)
POST /api/tts/synthesize        合成 + 缓存,返回 /tts/<sha>.mp3
POST /api/chat                  SSE:流式回复 + 结构化 action
POST /api/search                NCM 搜索
POST /api/ncm/login/qr/create   扫码登录:创建二维码
GET  /api/ncm/login/qr/check    扫码登录:轮询 800/801/802/803
POST /api/ncm/logout            登出
POST /api/ncm/sync              同步歌单/喜欢/画像
GET  /api/ncm/sync/status       同步进度
GET  /api/ncm/status            登录态
GET  /api/taste                 读画像
POST /api/taste/import          导入画像
GET  /api/settings              读配置(密钥不回填)
POST /api/settings              写 .env,随后 sidecar 自重启
GET  /health                    不需 token
GET  /tts/<sha>.mp3             静态文件,不需 token,AVPlayer 直接拉流
POST /shutdown                  干净退出

数据目录

默认 ~/Library/Application Support/Sonora/,可用 SONORA_DATA_DIR 覆盖。

~/Library/Application Support/Sonora/
├── sidecar.port / sidecar.token      # sidecar 启动时写,退出时删
├── .env                               # 配置(可选;POST /api/settings 写入)
├── state.json                         # 客户端队列/历史/当前曲(Swift 端 StateStore 写)
├── cache/
│   ├── tts/<sha256>.mp3               # TTS 结果,按内容 + 音色哈希,跨重启复用
│   └── cover/                         # 封面缓存
└── user/
    ├── ncm-session.json               # NCM 登录 cookie + 最近 profile
    ├── active-user.json               # 当前激活的 NCM userId
    └── users/<uid>/                   # taste.md / playlists.json / likelist.json /
                                       # taste_stats.json / profile.json / sync-status.json

sidecar 自身不持有播放状态 —— 队列/历史/当前曲的状态机在 Swift 客户端。

配置

环境变量或数据目录下的 .env(也可以在 app 的 SET 面板里填):

OPENAI_BASE_URL=...
OPENAI_API_KEY=...
OPENAI_MODEL=...
OPENAI_BLOCKED_ARTISTS=...     # 逗号分隔;只影响发给 LLM 的内容

TTS_URL=...                    # Mimo / StepFun / Fish / 兼容 OpenAI audio.speech
TTS_API_KEY=...
TTS_MODEL_ID=...
TTS_VOICE_ID=...               # mimo voicedesign 下这里填音色描述
TTS_EN_MALE_VOICE_ID=...       # 英文/粤语分轨音色,缺省回落 TTS_VOICE_ID
TTS_YUE_FEMALE_VOICE_ID=...

NCM_BASE_URL=...               # 留空则用内嵌 NeteaseCloudMusicApi
SONORA_EMBEDDED_NCM=0          # 显式关掉内嵌实例

旧变量(STEP_API_KEY / STEPFUN_API_KEY / FISH_API_KEY / STEP_TTS_* / FISH_*)仍作为兜底被识别。

降级

外部服务都可缺省,缺哪个降哪一层,npm run sidecar 在全新 checkout 上直接能跑:

缺失 行为
NCM_BASE_URL 起内嵌 NeteaseCloudMusicApi;连它也起不来则回落内置样例曲目(local-*),搜索返回空
OPENAI_* AgentBrain.fallback() 按时段规则从候选池选曲;intro 走事实拼接兜底
TTS_* synthesize() 返回空 url,调用方按静音处理

文档

  • 总体设计:docs/superpowers/specs/2026-05-28-swift-rewrite-design.md
  • M4a 播放 UI:docs/superpowers/specs/2026-06-21-m4a-playback-ui-design.md
  • M4b 设置/账户:docs/superpowers/specs/2026-06-22-m4b-settings-account-design.md
  • Mimo TTS voicedesign:docs/superpowers/specs/2026-07-10-mimo-tts-voicedesign-design.md
  • 实施计划:docs/superpowers/plans/
  • 烟测清单:docs/m0-smoke.mddocs/m4b-smoke.md

About

一个懂你歌单和此刻心情的私人 AI 电台,在夜色、天气和听歌记忆里为你选下一首歌|A private AI radio host that reads your taste, mood, and moment to cue the next song with cinematic TTS intros.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages