- 语音
- 桌面应用
- 本地部署
- 人工智能
- 音频
【免费下载链接】hex
Local voice dictation for mac, windows and linux
@hex-ai/service-darwin-arm64是 HEX 为 Apple Silicon macOS 规划的本地转写原生依赖包,但当前仓库里它是一个私有的占位包:没有bin/hex-service,也没有被客户端声明为依赖。本文以 sdk/service-darwin-arm64/README.md 的状态声明为主线,逐条对照仓库中的 SDK 解析逻辑、打包脚本、签名与冒烟测试代码,讲清楚"为什么必须显式传command""发布工作流为什么产不出二进制""签名与公证到底校验了什么",以及作为宿主应用(host)现在应当如何正确集成。读完你可以判断该包的可用边界,并据此写出可运行、可通过身份校验的嵌入方案。
文档定位:这是一份"状态声明",不是使用手册
原文开头就给出了两条关键限定:
Status:Private, unpublished platform artifact for Apple silicon macOS. This checkout does not contain
bin/hex-service.
这句话包含三个可验证事实:
- 包是private(未发布到公共 registry);
- 目标是Apple silicon macOS(
darwin+arm64); - 当前 checkout 中没有
bin/hex-service——即包内没有真正的可执行文件,只有元数据与说明。
原文进一步说明它是@kitlangton/hex(TypeScript 客户端)"计划中的原生依赖"的占位,并且"公共客户端并未把该包声明为依赖";平台解析器认识这个名字,但消费者目前必须向create()显式提供 helper 命令。这三点分别对应仓库中的三处证据:包的package.json、客户端的helper.ts解析器、以及客户端的package.json依赖清单。
因此,本文不会把这个包当作"已可用的 npm 二进制分发包",而是当作一个尚未闭环的分发链路来解剖。
包的元数据:private、os、cpu与空的bin
先看占位包本身,sdk/service-darwin-arm64/package.json 全文只有十几行,但每个字段都在表达"不可发布":
{ "name": "@hex-ai/service-darwin-arm64", "version": "0.0.0", "private": true, "description": "Signed native HEX transcription helper for Apple Silicon macOS", "license": "MIT", "os": ["darwin"], "cpu": ["arm64"], "files": [ "bin/hex-service", "README.md" ], "exports": { "./package.json": "./package.json" } }各字段的实际含义如下:
| 字段 | 取值 | 作用与影响 |
|---|---|---|
private | true | npm 拒绝发布该包;这也解释了原文"Private, unpublished"的表述 |
version | 0.0.0 | 占位版本号,不代表任何可用的 helper 版本 |
os/cpu | darwin/arm64 | 声明平台约束,避免被错误平台安装;但仅在包真正被解析安装时才会生效 |
files | bin/hex-service、README.md | 声明发布内容包含bin/hex-service,而当前检出中没有该文件 |
exports | 仅./package.json | 故意不导出 JS 入口,这个包只用来"被解析到路径",不提供代码 |
最后一条很容易被忽略:exports只暴露./package.json,说明它的设计用途是让客户端通过require.resolve("<package>/package.json")拿到包根目录,再相对定位bin/hex-service。这是一种"纯路径载体"式的原生包布局。
客户端如何解析 helper:helper.ts的三段式逻辑
客户端的解析器在 sdk/typescript/src/helper.ts,核心只有三个函数。它解释了原文中"平台解析器认识这个名字"和"消费者必须显式提供命令"这两句的机制。
1. 平台白名单只有一种组合
const platformPackage = (): string => { if (process.platform === "darwin" && process.arch === "arm64") { return "@hex-ai/service-darwin-arm64" } throw new HexError( "startup-failed", `HEX does not include a helper for ${process.platform}-${process.arch}`, ) }只有在darwin+arm64时才会返回包名,其他平台(Linux、Windows、Intel Mac,或在 Rosetta 下以 x64 运行)会直接抛出startup-failed,错误消息里带上实际平台与架构。这是排障时最常见的报错之一。
2. 用require.resolve定位,用accessSync校验可执行位
const packagedExecutable = (): string => { const packageName = platformPackage() let manifest: string try { manifest = require.resolve(`${packageName}/package.json`) } catch (cause) { throw new HexError("startup-failed", `The included HEX helper package ${packageName} is unavailable`, { cause, }) } const executable = resolve(dirname(manifest), "bin", "hex-service") try { accessSync(executable, constants.X_OK) } catch (cause) { throw new HexError("startup-failed", `The included HEX helper is not executable: ${executable}`, { cause, }) } return executable }这段代码做了两件事,正好对应占位包的两个缺失点:
- 包不存在的错误:
The included HEX helper package @hex-ai/service-darwin-arm64 is unavailable; - 包存在但文件缺失/不可执行:
The included HEX helper is not executable: .../bin/hex-service。
由于当前 checkout 不含bin/hex-service,即使把该包放进node_modules,第二步的X_OK检查也会失败。
3. 显式command优先,否则回退到内置路径
export const resolveCommand = ( options: Pick<CreateOptions, "command">, ): readonly [executable: string, ...arguments: readonly string[]] => options.command ?? [packagedExecutable(), "service", "--embedded"]这一行是整份 README 的技术核心:
- 只要调用方传了
command,解析器完全不触碰平台包,因此开发期、CI、或自建分发场景可以直接指向任意可执行文件; - 只有省略
command时才会走内置包路径,并附带固定参数["service", "--embedded"]——即嵌入模式。
这也解释了为什么原文说"consumers currently must supply an explicit helper command tocreate()":在当前状态下,省略command基本必然落到packagedExecutable()的失败分支。
类型定义也在表达"暂不可省"
sdk/typescript/src/types.ts 中的注释与类型同样在提示这一点:
export interface CreateOptions { /** Omit the model for the low-level host. */ readonly model?: never /** Required until bundled native helper packages are published. */ readonly command?: readonly [executable: string, ...arguments: readonly string[]] readonly cwd?: string readonly env?: Readonly<Record<string, string | undefined>> readonly startupTimeoutMs?: number readonly shutdownTimeoutMs?: number readonly signal?: AbortSignal /** Platform transport override, primarily for tests and Electron adapters. */ readonly fetch?: typeof globalThis.fetch }command仍是可选字段(为了保持与已发布 API 的兼容),但注释明确写着"Required until bundled native helper packages are published"。
客户端确实没有把它声明为依赖
查看 sdk/typescript/package.json 可以看到,@kitlangton/hex的依赖区只有peerDependencies(effect,且为 optional)与devDependencies,没有dependencies,也没有optionalDependencies指向@hex-ai/service-darwin-arm64。这与原文"公共客户端不声明此包作为依赖"完全一致。
需要注意的是,sdk/package.json 是一个私有 workspace,workspaces包含typescript与service-*,所以仓库内部会把该占位包链接进 workspace,但这是本地开发结构,不等于发布物包含原生二进制。
三条启动路径:create({ model })、低层create()、桌面connect()
理解该包的位置,最好把它放进客户端的三条路径中对比。实现分别在 sdk/typescript/src/host.ts 与 sdk/typescript/src/client.ts。
| 路径 | 入口 | 是否需要command | 是否使用原生 helper 包 | 用途 |
|---|---|---|---|---|
| 就绪转写器 | create({ model, language, onProgress }) | 当前需要 | 省略时才会尝试 | 宿主自己录音,只把 WAV 交给 helper 做推理 |
| 低层宿主 | create({ ... })(无model) | 当前需要 | 同上 | 浏览模型目录、显式models.prepare()、自行调用transcribe() |
| 桌面复用 | connect() | 不需要 | 不使用 | 复用已运行 HEX 桌面应用的麦克风与引擎 |
create()的分派逻辑很直白(sdk/typescript/src/host.ts 中create的重载实现):存在model时先startHost()再prepareTranscriber(),否则返回低层HexHost。
connect()则走完全不同的发现机制:读取~/Library/Application Support/hex/local-api.json(可用HEX_APPLICATION_SUPPORT_DIR覆盖),拼出http://127.0.0.1:<port>并调用/health验证。源码结构上可以看出,嵌入模式不写发现文件、桌面模式写发现文件,这一点在 Rust 侧同样成立。
嵌入模式的握手:为什么 token 只走管道
原文提到"embedded and packaged service smoke checks expect local API 2",这对应嵌入模式的启动协议。Rust 侧实现见 src/main.rs:Command::Service { embedded }分支在embedded == true时调用LocalApi::start_embedded,把 JSON 握手写到 stdout,并启动embedded-host-lease线程读取 stdin;stdin 读到 EOF 就置SHUTDOWN,等待 5 秒后强制std::process::exit(0)。
start_embedded的关键差异在 src/local_api.rs:它把discovery_path传成None,因此不写local-api.json、不抢全局实例锁,允许多个宿主各自运行独立 helper。embedded_endpoint()(src/local_api.rs)生成的握手结构为:
{ "type": "ready", "url": "http://127.0.0.1:<port>", "token": "<bearer token>", "apiVersion": "2", "pid": 12345 }客户端对这一行的处理相当严格(sdk/typescript/src/host.ts):
- 握手最大 16 KiB(
MAX_HANDSHAKE_BYTES),超限报invalid-handshake; - stderr 只保留最后 16 KiB(
MAX_STDERR_BYTES),用于失败时的诊断附带; - 默认启动超时 10 秒(
DEFAULT_STARTUP_TIMEOUT_MS),可被startupTimeoutMs覆盖; - 默认关闭超时 5.5 秒(
DEFAULT_SHUTDOWN_TIMEOUT_MS),可被shutdownTimeoutMs覆盖; - 必须校验
endpoint.pid === child.pid,否则视为握手异常——这是一道防止连到错误进程的防线。
关闭过程是"先礼后兵":先child.stdin.end()触发 EOF,等待优雅退出;超时发SIGTERM;再等 1 秒仍未退出则SIGKILL,仍不退出就抛shutdown-failed。
从源码到.app:两条完全不同的打包链路
这是原文最关键、也最容易被误读的一段:
The separate service-app scripts package the full desktop executable as
hex-service; they do not produce a transcription-only npm payload.
也就是说,仓库里确实有能产出hex-service的脚本,但它装的是完整的桌面可执行文件,不是"只做转写的 npm 载荷"。链条如下。
链路 A:构建、签名.app并公证
sdk/service-darwin-arm64/README.md 指出 service-app packager 要求显式的HEX_TEAM_ID与匹配的 Developer ID Application 签名身份,且prepare-service-app.sh还要求显式HEX_NOTARY_PROFILE。
对应代码 scripts/build-service-app.sh:
team_id=${HEX_TEAM_ID:?Set HEX_TEAM_ID to the Apple Developer signing team} . "$root/scripts/macos-signing.sh" identity=$(hex_codesign_identity "$team_id") bundle="$root/target/HEX Service.app" executable="$bundle/Contents/MacOS/hex-service" ... cargo build --release --manifest-path "$root/Cargo.toml" cp "$root/target/release/hex" "$executable" cp "$root/app/ServiceInfo.plist" "$bundle/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $version" ... codesign --force --timestamp --options runtime --sign "$identity" "$bundle" codesign --verify --deep --strict --verbose=2 "$bundle"注意第 16 行:target/release/hex被直接复制为hex-service。这就是"full desktop executable 改名为 hex-service"的源码证据。
签名身份校验在 scripts/macos-signing.sh 的hex_codesign_identity:先取HEX_CODESIGN_IDENTITY,否则用security find-identity -v -p codesigning过滤Developer ID Application: ... (<team_id>);随后用case再做一次字符串匹配,确保身份确实是该 team 的 Developer ID Application。签名采用--options runtime(Hardened Runtime)与--timestamp。
公证在 scripts/prepare-service-app.sh:
: "${HEX_NOTARY_PROFILE:?Set HEX_NOTARY_PROFILE to the matching notarization profile}" bundle=$("$root/scripts/build-service-app.sh") /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$bundle" "$submission" hex_notarize "$submission" xcrun stapler staple "$bundle" xcrun stapler validate "$bundle" /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$bundle" "$artifact" shasum -a 256 "$artifact" > "$artifact.sha256"HEX_NOTARY_PROFILE的强制要求与原文一致。hex_notarize支持两条路径:本地 keychain profile(--keychain-profile)或 CI 的 App Store Connect API Key(HEX_NOTARY_KEY/HEX_NOTARY_KEY_ID/HEX_NOTARY_ISSUER),都带--wait同步等待结果。产出是dist/service/HEX-Service-<version>-<arch>.zip及同名.sha256。
链路 B:把可执行文件塞进占位包
scripts/prepare-typescript-sdk.sh 是"链路 A → 占位包"的桥接:
package="$root/sdk/service-darwin-arm64" destination="$package/bin/hex-service" if [ "$(uname -s)" != "Darwin" ] || [ "$(uname -m)" != "arm64" ]; then echo "The current TypeScript SDK helper package targets arm64 macOS." >&2 exit 1 fi "$root/scripts/prepare-service-app.sh" >/dev/null source="$root/target/HEX Service.app/Contents/MacOS/hex-service" cp "$source" "$destination" chmod 755 "$destination" codesign --verify --strict --verbose=2 "$destination"三点值得注意:
- 脚本自带平台守卫,非 Darwin/arm64 直接失败;
- 它复用
prepare-service-app.sh,因此继承了HEX_TEAM_ID与HEX_NOTARY_PROFILE的强制要求; - 它把
.app内的hex-service解包后重新放到包内,并用codesign --verify --strict单独复核。
原文用一句"Extracting an executable from a stapled service app does not itself prove..."(出自 docs/plans/typescript-sdk.md)提醒:从已 staple 的.app中抽取二进制,并不等于该被抽取的二进制或最终宿主应用可分发——重签名会改变 digest 与身份,必须分别在抽取前、宿主重签后各自验证。
冒烟测试究竟验证了什么
原文最后一句话很有分量:
The embedded and packaged service smoke checks expect local API 2; passing identity guards alone does not validate a signed helper artifact.
两个脚本分别覆盖"嵌入握手"与"打包分发"两个层面。
smoke-embedded-service.sh:验证管道租赁与 API 2
scripts/smoke-embedded-service.sh 用 FIFO 模拟宿主 stdin:
mkfifo "$lease" HEX_APPLICATION_SUPPORT_DIR="$runtime/support" \ "$root/target/debug/hex" service --embedded \ <"$lease" >"$endpoint" 2>"$stderr" & pid=$! exec 3>"$lease"随后逐项断言:
- 握手字段
type == "ready"、apiVersion == "2"、pid与实际子进程一致; - 不得出现
runtime/support/local-api.json(嵌入模式不发布发现文件); /health返回apiVersion == "2"且version != null;/models非空且首项id == "parakeet_unified_en";- 关闭 fd 3(EOF)后进程必须退出,否则判定"未在宿主租约关闭时停止"。
这里可以看到 API 2 的具体约定,与 docs/specs/local-transcription-service.md 中的 wire protocol 一致。
smoke-service-app.sh:验证签名身份、公证与分发形态
scripts/smoke-service-app.sh 做的是"消费端视角"的验收:
(cd "$(dirname -- "$artifact")" && shasum -a 256 -c "$(basename -- "$digest")") /usr/bin/ditto -x -k "$artifact" "$staging" bundle="$staging/HEX Service.app" codesign --verify --deep --strict --verbose=2 "$bundle" xcrun stapler validate "$bundle" spctl --assess --type execute --verbose=2 "$bundle" team=$(codesign -dvv "$bundle" 2>&1 | sed -n 's/^TeamIdentifier=//p') identifier=$(codesign -dvv "$bundle" 2>&1 | sed -n 's/^Identifier=//p') if [ "$team" != "$expected_team" ] || [ "$identifier" != "$expected_identifier" ]; then echo "Unexpected service identity: $team / $identifier" >&2 exit 1 fi其中expected_team=${HEX_TEAM_ID:?...}、expected_identifier=anomaly.hex.service。脚本还要求通过/health与/models,最后kill -INT "$pid"并等待发现文件消失,形成完整的启动—查询—关闭闭环。
这解释了原文的措辞:通过身份守卫(team/identifier 校验)只是必要条件,如果 helper 本身不是被验证过的签名产物、或者不是 API 2 行为,冒烟测试依然会失败;反之,冒烟测试通过也只覆盖该脚本描述的路径,不等于"干净机器上的完整分发就绪"。
发布工作流为什么不产出原生二进制
原文明确写道:
The TypeScript release workflow does not build or insert
bin/hex-service.
证据在 .github/workflows/release-typescript.yml:version与publish两个 job都运行在ubuntu-latest,步骤只有 checkout、setup-bun、bun install --frozen-lockfile、bun run release(即changeset publish)以及打 tag / 建 release。整个流程中没有任何cargo build、没有任何 macOS runner、也没有把产物复制进sdk/service-darwin-arm64/bin/。
结合前文的private: true与0.0.0版本可推断:即使工作流被触发,它也只会发布@kitlangton/hex客户端包,不会产生可安装的 helper。这与"发布链路尚未闭环"的判断一致,也是为什么 docs/plans/typescript-sdk.md 把"原生包发布"列为待完成事项,并强调需要在 macOS 上做验证到打包的 fail-closed 交接。
现在应当怎样集成:显式command的最小可用写法
基于上述事实,当前唯一稳定可用的集成方式是:宿主自行准备一个能响应 API 2 的hex-service,并在create()中显式传入。下面这段代码与 sdk/typescript/README.md 的公开契约一致,并补充了本文涉及的超时参数:
import { create } from "@kitlangton/hex" const transcriber = await create({ // 当前必须显式提供;省略会走内置包解析路径并因缺少 bin/hex-service 失败 command: ["/path/to/hex-service", "service", "--embedded"], model: "parakeet_unified_en", language: "en", onProgress: (progress) => { // progress.type: "downloading" | "verifying" | "loading" console.log(progress.type) }, startupTimeoutMs: 10_000, // 默认值,按需放宽 shutdownTimeoutMs: 5_500, // 默认值,按需放宽 }) try { // 宿主自己完成录音与 PCM WAV 编码;helper 不接触麦克风 const result = await transcriber.transcribe(wav, { signal: abortController.signal }) insertIntoHostInput(result.transcript) } finally { await transcriber.close() }几条与实现强绑定的行为约束,建议在集成设计时就纳入考虑:
transcribe()永不下载模型:创建阶段负责prepare(下载 → 校验 → 加载 → 预热),转写阶段只接受已就绪模型;未就绪会得到model-not-ready一类错误。- 取消在飞请求会关闭 helper:这来自
prepareTranscriber中的显式处理(sdk/typescript/src/host.ts),因为仅中断 HTTP 无法停止原生推理;下一次录音需要新建转写器。已经处于 aborted 状态的请求不会关闭健康的转写器。 - 一个 helper 服务一段功能生命周期:不要每次录音启停;
close()幂等,且不删除模型文件。 - 音频有明确预算:仅
audio/wav,8–192 kHz、1–8 声道,64 MiB 上传上限,源帧与归一化样本各 16,777,216 上限。按 48 kHz 折算约 5.8 分钟,按 16 kHz 约 17.5 分钟。 - 凭据边界:token 只经 stdin/stdout 管道传递,服务端不返回 CORS 头;不要把它暴露给渲染进程或网页内容。
错误对照表:从报错反查缺口
以下错误码与消息直接来自 sdk/typescript/src/helper.ts、sdk/typescript/src/host.ts 与 sdk/typescript/src/errors.ts 的实现语义,可作为排查清单。
| 现象 / 错误消息 | 直接原因 | 对应本文章节 |
|---|---|---|
HEX does not include a helper for <platform>-<arch> | 非 darwin-arm64(含 Rosetta 下的 x64) | 平台白名单 |
The included HEX helper package ... is unavailable | 未安装/未解析到@hex-ai/service-darwin-arm64 | require.resolve失败 |
The included HEX helper is not executable: .../bin/hex-service | 包在但bin/hex-service缺失或无可执行位 | 占位包现状 |
HEX did not become ready within <n>ms | 握手超时(默认 10s) | 握手与超时 |
HEX exited before becoming ready (<code>) | 子进程提前退出,消息附带 stderr 尾部 | 握手失败诊断 |
HEX reported a different process identifier | 握手pid与实际子进程不符 | PID 校验 |
Invalid embedded service handshake(脚本侧) | type/apiVersion/pid不符 | 嵌入冒烟测试 |
Unexpected service identity: <team> / <identifier> | team 或 bundle id 与期望不符 | 打包冒烟测试 |
何时可以省略command:判断标准而非时间表
原文只说"planned",没有给出可用日期。要判断某次发布是否已经补齐链路,可以按仓库中已被验证的验收思路逐项核对(出自 docs/plans/typescript-sdk.md 的第 4 个 slice):
- 原生包不再是
private: true/0.0.0,且客户端以精确版本 pin 住它; - 发布工作流中存在macOS 构建与验证 → 打包的 fail-closed 交接(当前 workflow 全在 Ubuntu,明显不满足);
- 打包产物字节与已验证产物一致,并经过 digest、可执行位、签名检查;
- 缺包或不支持平台时给出可操作错误,而不是隐式安装桌面版;
- 干净机器上无需仓库、开发者工具链与手工路径配置即可完成首次模型准备与转写。
在这些条件满足之前,"省略command让 SDK 自动找 helper"仍是设计目标,而不是当前行为。
小结
sdk/service-darwin-arm64/README.md表面上只有二十余行状态说明,但每句话都能在仓库中找到对应实现:private: true与空的bin对应包元数据,平台解析器与显式命令的张力对应helper.ts的三段式回退,HEX_TEAM_ID/HEX_NOTARY_PROFILE的强制要求对应macos-signing.sh与两个打包脚本,API 2 与身份校验对应两个冒烟脚本,而"发布工作流不构建二进制"则能在全 Ubuntu 的 release workflow 中直接看到。
对宿主应用而言,当前正确姿势是:自带签名好的hex-service、显式传command、在功能生命周期内复用一个转写器、自己管理麦克风与 WAV 编码。把这条边界理解清楚,就不会在缺失bin/hex-service时误以为是自己环境问题,也不会在尚未闭环的分发链路上做出"开箱即用"的承诺。
- 语音
- 桌面应用
- 本地部署
- 人工智能
- 音频
【免费下载链接】hex
Local voice dictation for mac, windows and linux
相关推荐
Mochi Diffusion深度解析:如何在Apple Silicon上实现原生AI绘图加速
Mochi Diffusion是一款专为Apple Silicon架构优化的原生Stable Diffusion应用,让Mac用户无需复杂配置即可享受高效的AI
桌面应用媒体生成人工智能AI 应用本地部署深度学习sqlc 命名参数(Named Parameters)包深度解析:从 `sqlc.arg()` 到原生占位符的编译管线
sqlc 命名参数(Named Parameters)包深度解析:从 sqlc.arg 到原生占位符的编译管线 导读 在 sqlc 中,查询参数可以通过 sql
开发工具代码生成数据库仓颉 stdx hex 包详解:使用 fromHexString 与 toHexString 完成 Hex 编解码
仓颉 stdx hex 包详解:使用 fromHexString 与 toHexString 完成 Hex 编解码 本文是仓颉编程语言扩展库 stdx 中 st
标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考