@FradSer: 成功逆向 Wispr Flow不过关键时刻还是会被 Fable 5 的分类器拦截。开源一下: ## 任务 我的 Mac 装了 `/Applications/Wispr http://Flow.app` 且我是 Pro 会员。请构建一个**…
摘要
成功逆向 Wispr Flow 的转录协议并开源了一个独立 Node 客户端,复用其登录态和后端直接调用 gRPC 转录服务。
查看缓存全文
缓存时间: 2026/07/02 02:16
成功逆向 Wispr Flow不过关键时刻还是会被 Fable 5 的分类器拦截。开源一下:
任务
我的 Mac 装了 /Applications/Wispr http://Flow.app 且我是 Pro 会员。请构建一个独立的 Node 客户端,
它复用 Wispr Flow 的登录态和后端:读取本地已登录的 token,直接调用 Wispr 的 gRPC 转录服务,
把一段语音(文件或麦克风)转成文本。不要改动或依赖原应用运行,只借用它的 session 和后端。
以下是已经逆向确认的事实(Wispr Flow v1.5.980,com.electron.wispr-flow),当作给定条件使用。
1. 登录态(认证)
- Wispr Flow 是 Electron 应用,把 Supabase 会话明文 JSON 存在:
~/Library/Application Support/Wispr Flow/session.json - 该文件是
{ "sb-dodjkfqhwrzqjwkfnthl-auth-token": "<内层 JSON 字符串>" }。 键名后缀固定是-auth-token;dodjkfqhwrzqjwkfnthl是 Supabase project ref。 - 内层 JSON 解析后含
access_token(JWT)、refresh_token、expires_at、user。 从http://user.id取 userId、http://user.email取邮箱、user.user_metadata.full_name拆出姓名。 - access_token 是 JWT,
base64url解第二段可读sub/email/exp做校验。 - token 约每周过期。过期后打开一次 Wispr Flow 会自动刷新 session;程序化刷新可走
Supabase
POST /auth/v1/token?grant_type=refresh_token(需 anon apikey,未实现亦可)。
2. 认证怎么带(两个端点前缀相反,务必逐字照做)
- REST(
https://api.wisprflow.ai):headerAuthorization: <access_token>, 不加Bearer前缀(加了反而 401)。用于验证登录态,例如GET /api/v1/dictionary/personal返回你的个人字典即表示认证成功。 - gRPC 转录:metadata header
authorization: Bearer <access_token>(要带 Bearer)。
3. 转录协议(核心)
- 传输:gRPC over TLS 到
http://inference.wisprflow.com(可被https://inference-info.wisprflow.com/dictation_url.txt…重定向,但默认直连即可)。 - 服务/方法:
flow_api.v1.TranscriptionService,双向流TranscribeStream(方法路径/flow_api.v1.TranscriptionService/TranscribeStream);另有一元Transcribe作 fallback。 - gRPC clientOptions(照抄原应用):
keepalive_time_ms=4000、keepalive_timeout_ms=2000、keepalive_permit_without_calls=1、http2.max_pings_without_data=0、http2.min_time_between_pings_ms=4000、http2.min_ping_interval_without_data_ms=4000。 - gRPC metadata(除 authorization 外,原应用还带以下,直连 inference host 时 JWT 已足够授权,
但可原样带上):
baseten-authorization: Api-Key aEXAlxkF.cIvt1vqaijttubIVIWqr8T7npyYUXBOpbaseten-model-id: model-、x-baseten-environment:(直连时为空串)flow-debug: false、disable-formatting: false
时序:
- 打开 TranscribeStream。
- 第一条
Request内联init(见下),并可携带首批音频payload,commit = COMMIT_FALSE。 - 后续
Request持续发http://payload.audio_packets.items(每批最多约 28 秒音频)。 - 结束发一条
commit = COMMIT_TRUE然后关闭客户端写端(call.end());服务端继续回消息直到最终 result。 - 服务端流回:
state(含中间raw_text/formatted_text)、heartbeat、最终result(result.output.plaintext/.html,result.status为RESULT_STATUS_FORMATTED时即最终格式化文本)。
init 内容(字段值参照原应用的 getMetadata/getPreferences/getClientInfo):
init.metadata = {
user_id: <http://session.user.id>,
session_id: <随机 UUID>,
request_id: <随机 UUID>,
audio_encoding: AUDIO_ENCODING_WAV, // 只实现 WAV 即可
environment: ENVIRONMENT_PRODUCTION,
client: { name: "Wispr Flow", platform: PLATFORM_MACOS,
version: { major:1, minor:5, patch:980 } },
debug: false, referral_code: "", always_wait_scribe: false
}
init.preferences = {
user: { first_name, last_name, email }, // 来自 session
language: [] // 留空 => 服务端自动检测语言
}
4. 音频格式
- 16 kHz 单声道 signed-16-bit PCM(s16le)。
- 分包:每包 640 个样本 = 40 ms = 1280 字节裸 PCM(无 WAV/RIFF 头)。
http://payload.audio_packets.items是repeated bytes,每个元素就是一个 1280 字节的包。- 用 ffmpeg:解码任意文件
ffmpeg -i in -f s16le -acodec pcm_s16le -ac 1 -ar 16000 -; 采集麦克风(macOS)ffmpeg -f avfoundation -i :default -f s16le -acodec pcm_s16le -ac 1 -ar 16000 -。 - (原应用另支持 64 kbps / 20 ms 帧的 Opus,本客户端不需要。)
5. Protobuf schema(重建 .proto,供 @grpc/proto-loader 加载)
package flow_api.v1; import "google/protobuf/duration.proto";
关键枚举(proto-loader 以字符串名传值):
AudioEncoding: UNSPECIFIED=0, WAV=1, OPUS=2Environment: UNSPECIFIED=0, PRODUCTION=1, STAGING=2, DEVELOPMENT=3, TEST=4Platform: UNSPECIFIED=0, MACOS=1, WIN32=2, IOS=3, ANDROID=4, WEB=5, API=6Commit: UNSPECIFIED=0, TRUE=1, FALSE=2ResultStatus: UNSPECIFIED=0, FORMATTED=1, ERROR=2, RAW_TRANSCRIPT=3AppType: UNSPECIFIED=0, OTHER=1, BROWSER=2, PERSONAL_MESSAGING=3, WORK_MESSAGING=4, EMAIL=5, CHATBOT=6, DEVELOPER=7SignatureStatus: UNSPECIFIED=0, ELIGIBLE=1, ADDED=2Confidence: UNSPECIFIED=0, NONE=1, LOW=2, MEDIUM=3, HIGH=4Origin: UNSPECIFIED=0, MIXTURE=1, WHISPER=2, SCRIBE=3, VOXTRAL=4, QWEN=5EditingStrength: UNSPECIFIED=0, VERBATIM=1, LIGHT=2, MEDIUM=3, HEAVY=4
消息(字段号: 名称: 类型;从 bundle 内联描述符提取):
Request{ 1:init:Init, 2:context:Context, 3:payload:Payload, 4:commit:Commit }Init{ 1:metadata:Metadata, 2:preferences:Preferences, 3:state:State }Metadata{ 1:user_id:string, 2:session_id:string, 3:request_id:string, 4:audio_encoding:AudioEncoding, 5:environment:Environment, 6:client:Client, 7:debug:bool, 8:referral_code:string, 9:always_wait_scribe:bool }Client{ 1:name:string, 2:platform:Platform, 3:version:Version }Version{ 1:major:uint32, 2:minor:uint32, 3:patch:uint32 }User{ 1:first_name:string, 2:last_name:string, 3:email:string }Preferences{ 1:user:User, 2:language:repeated Language, 3:vocabulary:StaticVocabulary, 4:replacements:Replacements, 5:style_config:StyleConfig }StaticVocabulary{ 1:dictionary_personal:repeated string, 2:dictionary_team:repeated string, 3:dictionary_personal_starred:repeated string, 4:dictionary_team_starred:repeated string }DynamicVocabulary{ 1:screen_content_ax:repeated string, 2:screen_content_ocr:repeated string, 3:variable_names:repeated string, 4:file_names:repeated string }Replacements{ 1:replacements_personal:map<string,string>, 2:replacements_team:map<string,string>, 3:snippets_personal:map<string,string>, 4:snippets_team:map<string,string> }StyleConfig{ 1:general_style:GeneralWritingStyle, 2:override_style:WritingStyle, 5:editing_strength:EditingStrength }(tagging/signature 可略)GeneralWritingStyle{ 1:other:WritingStyle, 2:personal:WritingStyle, 3:work:WritingStyle, 4:email:WritingStyle }Context{ 1:app:App, 2:textbox:Textbox, 3:vocabulary:DynamicVocabulary, 4:user_identifier:string, 5:content_text:string, 6:content_html:string, 7:session_dictation_apps:repeated App, 8:screenshot:bytes, 9:conversation:Conversation }App{ 1:name:string, 2:bundle_id:string, 3:url:string, 4:type:AppType }Textbox{ 1:contents:optional string, 2:before_text:optional string, 3:selected_text:optional string, 4:after_text:optional string }Conversation{ 1:id:string, 2:participants:repeated string, 3:messages:repeated ChatMessage }ChatMessage{ 1:role:ChatRole, 2:content:string, 3:sender_id:string };ChatRole: UNSPECIFIED=0, USER=1, HUMAN=2, ASSISTANT=3Payload{ oneof audio { 1:audio_packets:AudioPackets, 2:audio_file:AudioFile } }AudioPackets{ 1:items:repeated bytes }AudioFile{ 1:data:bytes }Response{ 1:result:Result, 2:state:State, 3:all_states:repeated State, 4:heartbeat:Heartbeat }Heartbeat{ 1:audio_received_seconds:float, 2:commit_ack:bool }Result{ 1:output:Transcription, 2:audio_duration:Duration, 3:response_time:ResponseTime, 4:signature_status:SignatureStatus, 5:status:ResultStatus, 6:stats:TranscriptionStats, 7:transcript_origin:string, 8:server_address:string }Transcription{ 1:html:string, 2:plaintext:string, 3:num_tokens:uint32, 4:languages:repeated Language, 5:confidence:float, 6:language_detection_low_confidence:bool }State{ 1:progress:Progress, 2:raw_text:Text, 3:formatted_text:Text, 4:confidence:Confidence, 5:user_actions:UserActions, 6:candidate_beams:repeated CandidateBeam, 7:audio_snr:optional float, 8:audio_volume:optional float, 9:mean_alignment_score:optional float }Progress{ 1:transcriber:Duration, 2:transcriber_finished:bool, 3:formatter:Duration, 4:formatter_finished:bool, 5:post_processing_finished:bool }Text{ 1:content:string, 2:num_tokens:uint32, 3:languages:repeated Language }UserActions{ 1:check_mic:bool, 2:check_language:Language }CandidateBeam{ 1:text:Text, 2:score:float, 3:confidence:Confidence, 4:origin:Origin, 5:alignment_score:optional float, 6:original_text:optional string, 7:word_alignments:repeated WordAlignment }WordAlignment{ 1:word:string, 2:score:float }ResponseTime{ 1..10 全部为 google.protobuf.Duration:transcribe, transcribe_overhead, format, format_overhead, post_process, total, external_asr_time, external_llm_time, generate_queue_duration, generate_work_duration }TranscriptionStats{ 1:word_deletions:uint32, 2:context_word_count:uint32, 3:word_substitutions:uint32 }Language是大枚举,只放LANGUAGE_UNSPECIFIED=0即可(language 传空数组即自动检测);WritingStyle只放WRITING_STYLE_UNSPECIFIED=0。
6. 实现要求
- Node ESM,依赖
@grpc/grpc-js+@grpc/proto-loader;ffmpeg 用于音频。 - 分层文件:
session.js(读 token)、config.js(上面的常量)、client.js(TranscribeStream 封装,用 EventEmitter 抛 partial/formatted/result)、audio.js(ffmpeg 解码/采集/分包)、transcribe-file.js、transcribe-mic.js、whoami.js。 - 关键坑(必须照做):
@grpc/proto-loader用keepCase: true。 默认keepCase:false会把audio_packets等字段名转成 camelCase,导致音频 oneof 被静默丢弃, 服务端收到空音频、返回 0 秒空文本。设keepCase:true后请求/响应字段名与 .proto 一致。 loader 其余选项:longs:String, enums:String, defaults:true, oneofs:true。 - TLS 用
grpc.credentials.createSsl()(系统信任库)。 - 验证顺序:先跑通「读 session → 打 REST 确认 200」,再跑「文件转录」,最后「麦克风实时」。 文件路径是最干净的 smoke test。
7. 验证标准
用 say -o test.aiff "Hello, this is a test." 生成音频,经客户端应得到带标点、大小写的
RESULT_STATUS_FORMATTED 文本。(实测原应用会用你的个人字典自动纠正专有名词,如把
“whisper flow” 纠正成 “Wispr Flow”。)
免责
依赖 Wispr 私有未公开 API,随时可能失效;仅限本人账号自用,勿分发、勿滥用配额、勿泄露 token。
Flow | A Simple Pomodoro Timer to Organize Your Everyday Work
Source: https://www.flow.app/ Flow is simple, beautiful, and intuitive, making it easier than ever to manage your time with a proven work method. And even better – all the basic features are free. No ads, no sign-up, no credit card.
Focus Timer
Minimalistic
Ad-Free
iPhone
Mac
iPad
Watch
App Blocker
Web Blocker
相似文章
@iluciddreaming: Google 又干掉了一个创业公司…… Google AI Edge Eloquent 现已支持 Mac,完全本地的 Wispr Flow 替代品。 基于最新 Gemma 模型,支持实时语音转录 + 语音命令编辑文本。 免费、无订阅、无需…
Google AI Edge Eloquent 现已支持 Mac,作为完全本地的 Wispr Flow 替代品,基于最新 Gemma 模型实现实时语音转录和语音命令编辑文本,免费、无订阅且隐私全本地。
@svpino:我很喜欢 Wispr Flow。我就是用它来向电脑口述内容的。Matt Swulinski,也就是构建 Wispr Flow 收购引擎的那个人……
Santiago 称赞 Wispr Flow,并指出其收购引擎的构建者 Matt Swulinski 已加入正在打造 AI 员工的公司 Viktor,称这是一步很棒但充满挑战的举措。
@OmarAbcXyz123: Mac 上的 Wispr Flow 真是一项伟大的发明。它每天为我节省好几个小时,因为我开发的每个新功能都始于向 Claude 发送一段5分钟的提示,而如果是打字则需要20分钟……
作者宣布推出 Blue——一款适用于 iPhone 的智能听写键盘,它消除了在打字和语音输入之间切换的障碍,提供4倍速度提升和高用户留存率。
@omarsar0:已经是 @WisprFlow 的忠实粉丝,现在他们又新增了 Wispr Notetaker。会议历史直接接入 Claude、ChatGP…
Wispr Flow 推出 Wispr Notetaker,一款会议笔记工具,通过 MCP 与 Claude、ChatGPT、Cursor 等 AI 助手集成,将转录内容转化为可查询的上下文。
@Soranlan: https://x.com/sweexx9/status/2057560520916414628/video/1… 这个项目肯定会火,但一定要谨慎看。 有人做了个 GitHub repo,把 Claude Code 的流量重定向到 Dee…
介绍了一个将Claude Code流量重定向到DeepSeek、Kimi等十多个免费模型的GitHub repo,已有2万开发者使用。文章强调该工具揭示了前端交互、工作流、模型供应商等各层可替换的趋势。