Hex 本地转录服务与 SDK¶
状态: 内部服务和直接子进程模式已经实现。
@kitlangton/hex@0.3.0 通过 Promise 和 Effect 入口发布就绪的转写器。
本地 Electron 集成已经过演练,但原生辅助包仍是私有项,并未作为客户端依赖包含在内。
仍需要显式的辅助命令;签名消费方分发尚未验证。
权威说明: 本文档描述嵌入式服务协议,并将拟议的分发方案单独标注。SDK README 是当前的使用者指南。交付范围、证据和验收门控位于 ../plans/typescript-sdk.md。
产品边界¶
HEX Service 拥有本地转录模型和推理能力。宿主应用负责录音。
Host application HEX Service
---------------- -----------
microphone permission service discovery and authentication
device selection model download and verification
capture start/stop strict-Metal load and prewarm
levels and waveform PCM WAV resampling and inference
capture UX ----------> final raw transcript
settings and model choice
宿主的签名应用持有麦克风授权。正确配置的打包宿主会以自身身份弹出授权提示,而不是以 Hex 的身份;开发期 Electron 运行时可能会显示为 Electron。嵌入式辅助程序绝不会打开麦克风。
决策¶
| 决策 | 理由 |
|---|---|
| 宿主拥有麦克风采集 | 正确的 TCC 身份、直观的权限提示、无需共享的权限代理。 |
| 服务拥有模型和推理 | 各宿主共享经过验证的模型工件,同时每个辅助程序拥有自己的预热运行时。 |
| 完整音频通过 localhost 传输 | 简单的仅结果契约;没有部分转写或双向会话协议。 |
| 模型准备是显式的 | 转写调用永远不会隐藏大型网络下载。 |
| 已安装但未预热的模型可自动重新加载 | 同一辅助程序中请求不同模型会替换其预热运行时。 |
| 无产品时长限制 | 热键采集和服务转写会明确结束;运行资源耗尽仍然是一种类型化失败。 |
| 仅提供原始转写 | 宿主负责含义解读、重写和插入。 |
| 可信的 Node/Electron 主进程 | 普通浏览器代码和不受信任的渲染器不得获得原生进程权限或服务凭据。 |
| 宿主启动的辅助程序 | 宿主拥有进程、端点和生命周期,而不是去发现独立选举出的服务。 |
| 共享模型工件,私有预热运行时 | 宿主复用经过验证的下载内容,而不耦合其模型选择或进程生命周期。 |
拟议的嵌入式分发(尚未发布)¶
目标 SDK 会将已签名、已公证的辅助程序声明为可选的架构特定包,并自动选择当前平台的包。应用只需调用 create(),无需定位可执行文件。在宿主显式使用时,SDK 会直接将内置辅助程序作为子进程启动,无需 npm 安装脚本、管理员权限、登录项批准、Dock 图标或前台窗口。宿主不得通过 Launch Services 启动辅助程序。
由于嵌入式服务不采集音频,因此它不需要麦克风 TCC 身份。完整的 Hex.app 已经拥有自己的全局热键采集和转录管线;桌面端 connect() 也可以通过其独立的采集 API 使用应用的麦克风。
经过验证的模型工件仍然按用户共享,存放在 Application Support 下。每个嵌入式辅助程序都有自己的预热模型和推理队列。多个宿主可以复用同一个经过验证的文件,但它们不共享 Metal 分配或运行时选择。在 Hex.app 内部,对于听写已加载的 GGUF 模型,时长不超过 30 秒的片段会在听写间隙使用听写模型运行,因此应用只保留一份;其他模型或更长的片段则使用服务自身的模型,并且当听写持有该模型时,更长片段的那份副本会被释放。当前缓存基于文件名;为独立版本化的辅助程序提供不可变的、修订安全的存储仍在计划中。
嵌入式启动与身份验证¶
宿主通过管道 stdin 和 stdout 启动 hex-service service --embedded。辅助程序绑定 127.0.0.1:0,并在需要身份验证的 API 就绪后向 stdout 写入恰好一行 JSON:
{
"type": "ready",
"url": "http://127.0.0.1:49731",
"token": "hex_a1b2c3...",
"apiVersion": "2",
"pid": 12345
}
宿主保持子进程的 stdin 开启,作为其生命周期租约。EOF 请求干净关闭;如果原生工作未返回,辅助程序在五秒宽限期后强制退出。嵌入式模式不写入共享发现文件,也不获取全局服务锁,因此两个宿主应用可以运行相互独立的辅助程序。
当前可执行文件在本地 API 初始化之后才开始租约监控,而不是在进程入口处。启动失败和后代进程清理仍是交付计划中原生分发的验收门控。
每个请求都需要 Authorization: Bearer <token>。令牌仅通过子进程的 stdout 管道传递。服务器有意不发送 CORS 头。缺少或携带错误 bearer 凭据的已解析请求会返回空 401 响应;格式错误的 HTTP 可以在身份验证之前被拒绝。独立的 Hex 应用仍可为其自身的现有本地 API 发布仅属主可见的发现信息。嵌入式 create() 使用子进程握手;桌面端 connect() 仍然走独立的基于发现的路径。
线路协议(API 2)¶
GET /health
GET /capabilities
GET /models
POST /models/{id}/prepare SSE preparation progress, then ok or error
POST /transcriptions audio/wav body, final transcript response
POST /transcriptions 需要一个 model 查询参数。准备和转录接受可选的 language 查询参数,默认为 en。GET /models?language=... 将该语言用于就绪元数据,而不是用于过滤返回的目录。转录需要 Content-Length;不支持分块传输编码。
POST /transcriptions 接受完整的 PCM WAV 音频。服务将请求体增量读入有界缓冲区,然后在分配解码样本并运行推理之前验证 WAV 元数据。它在内部进行下混和重采样。API 2 接受 8 kHz 至 192 kHz、1 至 8 声道的整数 PCM 和 32 位浮点 WAV。上传上限为 64 MiB;源帧和归一化的 16 kHz 单声道 Float32 样本各上限为 16,777,216(64 MiB 的 Float32 样本)。帧上限允许在 48 kHz 下约 5.8 分钟,或在 16 kHz 下约 17.5 分钟,然后才受其他限制。标头有两秒截止时间,上传有三十秒截止时间。这些限制并非进程内存总量的上限。
HTTP 转录路径不会保存上传的 WAV 文件。当前可执行文件仍会初始化共享进程日志记录,并继承原生诊断标志(如 TRANSCRIBE_DUMP_DIR),这些标志可能会写入音频派生张量。不要将此视为经审计的无持久化边界;嵌入式诊断隔离仍是分发计划的一部分。
没有单独的持续时间上限。一个上传占用音频内存准入槽位,直到其排队中或进行中的推理释放该片段。绝对的标头和上传截止时间、字节与归一化样本预算,以及类型化资源拒绝,限制了来自有缺陷的本地客户端的准入。检测到客户端取消或服务关闭会抑制结果,但原生推理可能会在内部完成。就绪的 SDK 转录器则会在进行中的取消时关闭其拥有的辅助进程,并在拒绝前等待清理;仅凭低级请求中止并不能证明原生工作已停止。
健康¶
export interface Health {
version: string;
apiVersion: "2";
}
export interface Capabilities {
audioFormats: readonly ["audio/wav"];
partialTranscripts: false;
serviceCapture: false;
developerControl: false;
}
这些能力值描述了嵌入式辅助程序。桌面本地 API 可以声明捕获和仅调试用的开发者控制。SDK 将 serviceCapture 暴露为布尔值,并省略仅原生的 developerControl 字段。
模型¶
export type ModelId =
| "parakeet_unified_en"
| "parakeet_v2"
| "parakeet_v3"
| "whisper_large_v3_turbo"
| "qwen3_asr06_b"
| "qwencleo_asr_1_7b"
| "ivrit_whisper_large_v3_turbo"
| "sense_voice_small"
| "cohere_transcribe"
| "underdog_bark_3_pro"
| "underdog_bark_3_clean"
| "parakeet_ultra"
| "nemotron_3_5_asr"
| "gigaam_v3"
| "nb_whisper_medium"
| "granite_speech_5"
| "apple_speech"
// An imported or found model, listed while it is ready.
| `custom-${string}`;
export interface ModelInfo {
id: ModelId;
name: string;
installed: boolean;
verified: boolean;
managed: boolean;
downloadBytes: number | null;
languages: readonly string[];
supportsLanguageDetection: boolean;
// download | hex | imported | found: owner/name@abcdef1 (another app's
// Hugging Face download, used where it is). Older hosts omit it.
source?: string;
}
export type ModelProgress =
| { type: "downloading"; downloadedBytes: number; totalBytes: number }
| { type: "verifying" }
| { type: "loading" };
ModelId 是 SDK 的封闭标识符集合,而非可用性保证。原生目录当前排除 apple_speech,准备和转录会拒绝它。保留的 Apple Speech 实现和系统管理的元数据并不使其成为受支持的服务模型。
对于可用的 GGUF 模型,准备意味着按需下载、校验和验证(或复用匹配的验证收据)、严格 Metal 加载以及预热。managed 表示系统管理,而非由 Hex 下载,因此这些模型报告 managed: false。准备会激活辅助程序的预热模型,但不会更改完整的 Hex.app 设置。服务会串行化准备过程,只发布经过验证的替换文件,并在候选加载失败时保留之前的预热运行时。经过验证的下载可以在加载失败后保持已安装状态;工件发布和运行时激活是两个独立的步骤。
准备一个已加载的模型(包括桌面应用的听写模型)会重新运行预热:对 1.5 秒静音(最小片段长度)执行一次推理,在应用内它像会议块一样为听写让路。ggml 在运行后会将 Metal 权重驻留三分钟;之后 macOS 可能会压缩或调换它们,下一次转录就需要付出恢复代价。自行录制音频的主机可以在录制开始时进行准备,这样恢复发生在录制期间而不是之后。如果模型在最后一分钟内运行过,则跳过预热。当请求的正是应用的听写模型时,准备会使用该模型,且绝不会加载第二份副本;如果听写正在录制或转录,模型已经处于预热状态,准备会立即成功。
就绪转录器¶
SDK 0.3.0 为两个入口点都添加了模型绑定的操作:
import { create } from "@kitlangton/hex"
const transcriber = await create({
command: ["/path/to/hex-service", "service", "--embedded"],
model: "parakeet_unified_en",
language: "en",
onProgress,
})
try {
const result = await transcriber.transcribe(wav, { signal })
input.insert(result.transcript)
} finally {
await transcriber.close()
}
创建会在准备完成后解析。可跨录制复用转录器;当主机不再需要时关闭它。取消进行中的请求会关闭辅助程序,因此后续工作需要新的转录器。已经中止的请求不会关闭健康的转录器。清理失败会被报告,而不是隐藏。Effect 提供相同的就绪形态,使用作用域拥有的清理而不是 close()。
进度回调描述的是模型准备过程,而非推理百分比。转录仅返回最终文本;部分转录结果和数值形式的转录进度不通过此协议暴露。
底层 TypeScript 接口¶
SDK README 记录了宿主的创建和客户端的使用方式。公共类型 定义了完整的客户端、音频、进度和结果契约;本规范负责线路协议和生命周期保证,而非重复这些接口。
常见的宿主流程明确地说明了准备阶段:
await client.models.prepare("parakeet_unified_en", { onProgress });
// The host records, meters, and encodes the clip.
const result = await client.transcribe({
audio: { data: wav, contentType: "audio/wav" },
model: "parakeet_unified_en",
signal,
});
input.insert(result.transcript);
@kitlangton/hex/effect 入口点提供作用域化的辅助获取、基于 schema 的类型化错误、Effect 操作、用于模型准备进度的 Effect Stream,以及用于依赖注入的 Layer。关闭作用域会关闭宿主租约,并限定精确子项终止。
transcribe() 从不下载模型。当构件尚未准备就绪时,它返回 model-not-ready。由于模型驻留是瞬态的,它可能会透明地重新加载已安装但处于冷状态的模型。
首个嵌入式版本的非目标¶
- 不提供服务自有的麦克风采集、电平指示、热键、粘贴或剪贴板功能。
- 不提供部分转录结果。
- 不提供压缩音频编解码器。
- 不提供 OpenCode 重写配置文件。
- 不提供非 localhost 访问。
- 不提供普通的浏览器引导。
- 不提供跨宿主的热运行时共享或服务选举。
- 在真实消费者需要之前,不提供长文分段、说话人轮次、可恢复性或搜索功能。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex