实时转写流(/streams)¶
/streams 允许本机上的另一个应用使用 Hex 已加载的听写模型实时转写自己的音频。该应用录制麦克风(或其他任何音源),并推送 16 kHz 单声道样本;Hex 在聆听时发送部分文本,每个窗口一个最终片段,并在应用结束时发送最终文本。
这是计划中的共享引擎服务(ROADMAP.md,“Shared engine service”)的 API。目前由运行中的 Hex 应用提供该服务;将来,一个 launchd 服务(anomaly.hex.engine)将提供相同的路由,Hex UI 将成为另一个客户端。根据本文档编写的客户端无需更改。
代码位于 src/local_api/streams.rs(路由)和 src/transcription/stream.rs(窗口、优先级、租约)。
可用性¶
- 在 macOS、Linux 和 Windows 上运行中的 Hex 应用,且已由听写加载 GGUF 模型(在 Linux 和 Windows 上,需监听器已开启)。
- 当路由存在时,
GET /capabilities会报告"streams": true。 - 流从不加载、下载或切换模型。它们使用听写所持有的任何模型和语言;如果没有,创建流会返回
503 model-not-loaded。
发现与认证¶
读取 Hex 数据文件夹中仅所有者可读的发现文件 local-api.json(在 macOS 上为 ~/Library/Application Support/hex,在 Linux 上为 $XDG_DATA_HOME/hex 或 ~/.local/share/hex,在 Windows 上为 %APPDATA%\hex)(包含 port、token、apiVersion),然后携带以下内容调用 http://127.0.0.1:<port>:
| 请求头 | 用于 | 值 |
|---|---|---|
Authorization |
每个请求 | Bearer <token>(来自发现文件) |
X-Hex-Stream-Token |
每个 /streams/{id}/… 请求 |
该流的 ownerToken |
Content-Type |
创建 / 音频 | application/json / application/octet-stream |
Content-Length |
每个带请求体的请求 | 必需;拒绝分块请求体 |
错误或缺失的流令牌会返回 404 stream-not-found,与未知 id 相同,因此无法探测 id。
音频格式¶
16 000 Hz,单声道,小端序,在创建流时声明:
f32le:[-1, 1] 范围内的 32 位浮点数(超出范围的值会被钳制;拒绝 NaN 或无穷大)。s16le:有符号 16 位整数。
每个音频请求只携带完整的样本。录制时按小块推送;每个请求通常为 100 ms 到 1 s。
端点¶
POST /streams¶
model 和 language 是可选的;如果提供,则必须与听写所持有的模型和语言匹配,否则返回 409 model-mismatch 并附带所持有的值:
201:
{
"id": 3,
"ownerToken": "hex_9f…",
"model": "parakeet_v3",
"language": "auto",
"sampleRate": 16000,
"sampleFormat": "f32le",
"partials": true,
"leaseMs": 10000
}
仅当加载的模型能在其设备上进行流式处理时,partials 才为 true;无法流式处理的模型只发送片段和最终结果。
POST /streams/{id}/audio¶
请求体:以声明格式提供的原始样本,最多 256 KiB。入队时返回 204。同时续订租约。
GET /streams/{id}/events¶
服务器发送事件,每行 data: 一个 JSON 对象,每 15 秒发送一条 : keep-alive 注释。每个订阅者都会从开始收到每个片段,然后收到实时事件,最后收到结束事件,随后响应关闭。新订阅者会替换旧订阅者。流结束后,事件仍可读取,直到创建下一个流。
data: {"type":"ready","id":3,"model":"parakeet_v3","language":"auto","sampleFormat":"f32le","partials":true}
data: {"type":"partial","segment":0,"stable":"so the plan for","tentative":" today"}
data: {"type":"segment","segment":0,"startMs":0,"endMs":7420,"text":"So the plan for today is the release."}
data: {"type":"final","text":"So the plan for today is the release. …","segments":[…],"durationMs":31250}
type |
含义 |
|---|---|
ready |
首先发送:流的模型和格式。 |
partial |
正在转写的窗口。stable 是模型不会修改的已提交文本;tentative 仍可能变化。替换同一 segment 的上一个 partial。仅当 partials 为 true 时。 |
segment |
已完成窗口的最终文本(空窗口不发送)。 |
final |
所有片段连接在一起;流已结束。 |
error |
{"code","message"};流已结束(见下面的错误码)。 |
cancelled |
流已被取消。 |
不发送单词时间信息。
POST /streams/{id}/finish¶
转写剩余内容,并以 final 事件的形式返回最终结果:
{ "text": "…", "segments": [{ "segment": 0, "startMs": 0, "endMs": 7420, "text": "…" }], "durationMs": 31250 }
一旦开始结束,就不再接受更多音频。它最多等待 120 秒(例如在一段较长的听写之后),然后返回 504 stream-finish-timeout;流会继续运行,因此请再次调用 finish 或读取事件。已经失败的流会返回 409 及其 {"code","message"};已取消的流返回 409 stream-cancelled。
POST /streams/{id}/heartbeat¶
返回 204;续订租约。流结束后返回 409 stream-ended。
POST /streams/{id}/cancel¶
返回 204。丢弃所有内容;流以 cancelled 结束。
租约¶
当流在 10 秒(leaseMs)内既未收到音频也未收到心跳时,会以 error lease-expired 结束。暂停的录音器应每隔几秒发送一次心跳。结束过程中租约不计时。
限制¶
| 限制 | 值 |
|---|---|
| 活动流 | 1(第二次创建返回 409 stream-busy;已结束的流不计数) |
| 创建请求体 | 1 KiB |
| 音频请求 | 256 KiB,5 秒内读取完 |
| 积压(已接收但尚未转写) | 120 秒音频,然后返回 429 stream-backlog-full |
| 窗口 | 在停顿处切割,最多 25 秒 |
| 租约 | 10 秒 |
| 结束等待 | 120 秒 |
调度:听写始终优先¶
Hex 的听写功能与流共享引擎,热键绝不能等待流。因此:
- 音频在停顿处被切分为窗口(最长 25 秒)。每个窗口作为 yielding 实时流送入引擎,最多比引擎已解码的内容提前 0.4 秒,因此听写请求几乎不需要排队。
- 任何听写请求(快捷键的 capture 流、整段剪辑的转录)会先关闭 yielding 流;在听写流打开期间,yielding 流会被拒绝。
- 从按下快捷键到听写文本完成,流不会向引擎送入任何内容;其音频在积压队列中等待,之后再追赶。
- 一个其实时流被听写关闭的窗口,会在听写空闲后从窗口开头重新开始。若一个窗口完成时无法做到这一点,或任何窗口位于不支持流式的模型上,则会作为对已加载模型的一次前台请求进行离线转录:排在已发送的听写之后、会议分块之前(与 capture-word 请求同类)。
听写的 capture 从不等待引擎:其音频被独立、无损地录制。只有听写的转录会等待,而且只等待已经在运行的工作,因为在 Metal 上无法中止解码:正在传输中的输入(最多 0.4 秒音频,几十毫秒,在 300 毫秒保持阈值内),或是极少数情况下一个在听写空闲期间启动的离线窗口(最多 25 秒音频)。流排在会议分块之前,因为流的应用正在等待文本,而会议可以稍后追赶。
错误代码¶
错误是 JSON {"code": "…"}(有些会附带 message),但 401 没有响应体。
| 状态 | 代码 | 触发时机 |
|---|---|---|
| 400 | invalid-request |
创建请求体不是上述 JSON。 |
| 400 | unsupported-sample-rate |
sampleRate 不是 16000。 |
| 400 | unsupported-sample-format |
不是 f32le 或 s16le。 |
| 400 | unknown-model |
model 不是 Hex 模型 ID。 |
| 400 | invalid-audio |
样本不完整或浮点数非有限。 |
| 401 | (无响应体) | 缺少或错误的 bearer token。 |
| 404 | stream-not-found |
未知 ID、已被替换的流或错误的流令牌。 |
| 409 | model-mismatch |
请求的模型/语言与当前持有的不一致。 |
| 409 | stream-busy |
另一个流处于活动状态。 |
| 409 | stream-ended |
结束之后收到音频或心跳。 |
| 409 | stream-finishing |
finish 之后收到音频。 |
| 409 | stream-cancelled |
对已取消的流调用 finish。 |
| 409 | lease-expired、model-changed、transcription-failed、hex-quitting |
对以该方式失败的流调用 finish(与 error 事件相同的代码)。 |
| 411 | length-required |
缺少 Content-Length。 |
| 413 | request-too-large、audio-too-large |
超出限制。 |
| 415 | unsupported-content-type |
Content-Type 错误。 |
| 429 | stream-backlog-full |
120 秒尚未转录;请放慢速度或等待。 |
| 503 | engine-unavailable |
Hex 的听写引擎未在运行。 |
| 503 | model-not-loaded |
听写未持有 GGUF 模型(例如 Apple Speech)。 |
| 503 | shutting-down |
Hex 正在退出。 |
| 504 | stream-finish-timeout |
finish 等待了 120 秒;流继续运行。 |
model-changed 表示用户在流进行中切换了听写模型;请开启一个新的流。hex-quitting 表示 Hex 已退出。
会议结束事件(GET /meetings/events,Labs)¶
不属于 /streams,但位于同一个本地 API,使用相同的发现机制和 bearer token。仅当“Labs › 会议结束事件”(labs.meeting_finished_event,hex labs on meeting-finished-event)开启时可用;关闭时,它返回 404 labs-off。代码:src/local_api/meeting_events.rs(路由)和 src/meeting/finished_events.rs(录制器向其发布事件的中枢)。
服务器发送事件每 15 秒包含一条 : keep-alive 注释。每个会议一次,在其最终转录文本和 Markdown 写入完成、且其清单显示 complete 之后(收到 Stop,或 Hex 在录制期间退出后的最终处理):
只发送 ID:不包含标题、路径、转录文本或音频。使用 hex meetings show <id>(或 hex mcp)读取其余内容。同一时间只允许一个订阅者;第二个订阅者会收到 409 subscriber-busy。没有人在监听时不会排队,因此重新连接的客户端应列出会议以追赶进度。最终转录失败的会议不会发送任何内容。流会在 Hex 退出或开关关闭时结束。hex service 提供该路由,但不录制任何会议,因此永远不会发送会议结束事件。
离线后追赶进度¶
事件是尽力而为的信号,并不是队列。若要不遗漏任何内容,请将 meetings 文件夹视为记录,并把事件当作“现在去看”的提示:
- 维护一个游标:你已处理的最新会议的结束时间(
timestamp_ms + duration_ms,Unix 毫秒),以及处理过的附近 ID。 - 启动或重新连接时,先订阅
GET /meetings/events,然后运行hex meetings list --since <cursor> --limit 200 --json(或 MCP 的list_meetings并带since)。先订阅意味着在你列列表期间结束的会议仍会作为事件到达。 - 当会议的
status为complete且transcript_source为final时,即为已结束;使用hex meetings show <id> --json读取。将游标留在任何仍处于recording、transcribing或interrupted状态的会议之前,以便下次列表能再次看到它。 - 投递至少一次:同一个 ID 可能同时来自列表和事件,也可能来自重叠的列表。按会议
id(在会议生命周期内稳定)去重,并自行记录你已经处理的内容。
限制:
- 一次列表最多返回 200 个会议,最新的在前;如果还有更多,会设置
truncated;使用--until <oldest timestamp_ms>向前翻页。 --since/--until会包含所有录制时间与该范围重叠的会议,因此游标处的会议会再次出现;由去重逻辑处理。- 已被保留策略移除的会议不复存在,无法再追赶。
- Hex 在录制期间退出的会议,在下次启动并开启 Meetings 后完成最终处理之前,其状态为
interrupted;之后它会变为complete,并发送事件。 - 没有单独的转录修订 ID;会议 ID 是关键。最终转录是原子发布的,并且每个会议只发送一次事件。
最终转录就绪状态与笔记是分开的。事件和 complete 表示转录文件和 Markdown 已写入;笔记(Apple Intelligence,或带有 Summarize with OpenCode 的 OpenCode)是在那之后写入的,可能会失败,并且可以稍后重新生成。从 hex meetings show <id> --json 中的 notes 读取它们(在写入之前为 null;重新写入时 generated_at_ms 会改变)。订阅、列出和显示是只读操作:它们不运行任何命令,不上传任何内容,也不写入任何内容。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex