如何在 CopilotChat 中启用语音输入并接入自定义 TranscriptionService 如何在 CopilotChat 中启用语音输入并接入自定义 TranscriptionService【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit假设你已经有一个跑起来的 CopilotKit 聊天页面现在想让对话输入框长出一个麦克风按钮用户说话runtime 把录音转成文字转写结果像普通消息一样自动发给 agent。文档 docs/src/content/docs/voice.mdx 描述的就是这条链路。整件事分三块runtime 侧注册一个TranscriptionService、API 路由按 V2 的 URL 结构挂载、前端CopilotChat /基本不用动。适用前提是 Next.js 应用 V2 runtimecopilotkit/runtime/v2。前端CopilotChat 会自动渲染麦克风按钮copilotkit/react-core/v2的CopilotChat /在 runtime 的/info端点返回audioFileTranscriptionEnabled: true时自动渲染麦克风按钮聊天页面本身不需要额外接线。用户点击麦克风后聊天组件会录音、向 runtime 路由的/transcribe端点 POST 音频把转写文本填进输入框并提交。前端唯一要注意的是 runtime 地址。文档示例里runtimeUrl/api/copilotkit-voice指向你的 Next.js API 路由所以 API 路由的目录路径必须和它一致。参考实现见 voice-chat.tsximport { CopilotChat } from copilotkit/react-core/v2; const AGENT_ID voice-demo; const SAMPLE_TEXT What is the weather in Tokyo?; // 通过 DOM 把文本注入 composer 的回调 const handleTranscribed (text: string) { const textarea document.querySelectorHTMLTextAreaElement( [data-testidcopilot-chat-textarea], ); if (!textarea) return; // React 管理受控输入调用原生 value setter 才能触发受管状态更新 const nativeSetter Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, value, )?.set; if (nativeSetter) { nativeSetter.call(textarea, text); } else { textarea.value text; } textarea.dispatchEvent(new Event(input, { bubbles: true })); textarea.focus(); }; export function VoiceChat() { return ( div {/* 测试用按钮同步注入一段固定文本不经过麦克风与 /transcribe */} SampleAudioButton onTranscribed{handleTranscribed} sampleText{SAMPLE_TEXT} / CopilotChat agentId{AGENT_ID} / /div ); }上面的SampleAudioButton是一个可选的测试/演示辅助它同步地把一段固定文案注入输入框完全绕过麦克风权限和/transcribe端点适合 Playwright 跑用例、截图这类不方便弹麦克风权限的场景。仓库参考实现见 sample-audio-button.tsx按钮内部就是一个onClick{() onTranscribed(sampleText)}。麦克风路径才是真正走转写的路径如果只需要文件上传音频、图片、视频、文档而不是实时转写文档建议改用 Multimodal Attachments 功能。后端用 [[...slug]] 路由挂载带 TranscriptionService 的 V2 runtime在app/api/copilotkit-voice/[[...slug]]/route.ts创建一个 API 路由。[[...slug]]catch-all 写法是必须的V2 runtime 会在同一个 base path 下按/info、/transcribe等路径做内部路由。依赖安装来自 packages/voice/README.mdpnpm add copilotkit/voice openai路由的核心代码取自仓库参考实现 route.tsimport type { NextRequest } from next/server; import { CopilotRuntime, TranscriptionService, createCopilotRuntimeHandler, InMemoryAgentRunner, } from copilotkit/runtime/v2; import type { TranscribeFileOptions } from copilotkit/runtime/v2; import { TranscriptionServiceOpenAI } from copilotkit/voice; import OpenAI from openai; // 自定义转写服务未配置 OPENAI_API_KEY 时抛出带 api key 的明确错误 class GuardedOpenAITranscriptionService extends TranscriptionService { private delegate: TranscriptionServiceOpenAI | null; constructor() { super(); const apiKey process.env.OPENAI_API_KEY; this.delegate apiKey ? new TranscriptionServiceOpenAI({ openai: new OpenAI({ apiKey }) }) : null; } async transcribeFile(options: TranscribeFileOptions): Promisestring { if (!this.delegate) { // api key 子串 → handleTranscribe 映射为 AUTH_FAILED → HTTP 401 throw new Error( OPENAI_API_KEY not configured for this deployment (api key missing). Set OPENAI_API_KEY to enable voice transcription., ); } return this.delegate.transcribeFile(options); } } let cachedHandler: ((req: Request) PromiseResponse) | null null; function getHandler(): (req: Request) PromiseResponse { if (cachedHandler) return cachedHandler; const runtime new CopilotRuntime({ agents: { voice-demo: createBuiltInAgent() }, // 换成你自己的 agent runner: new InMemoryAgentRunner(), transcriptionService: new GuardedOpenAITranscriptionService(), }); cachedHandler createCopilotRuntimeHandler({ runtime, basePath: /api/copilotkit-voice, }); return cachedHandler; } export const POST (req: NextRequest) getHandler()(req); export const GET (req: NextRequest) getHandler()(req);两个必须对齐的点createCopilotRuntimeHandler里的basePath: /api/copilotkit-voice必须与 API 路由目录路径一致否则内部 URL 路由会错位。配置了transcriptionService后runtime 才会在/info上声明audioFileTranscriptionEnabled: true——这正是前端渲染麦克风按钮的依据并把POST /transcribe转发给你的服务。文档特别提示V1 wrapper 会丢掉transcriptionService这个选项所以这里直接用copilotkit/runtime/v2的createCopilotRuntimeHandler不要走 V1 包装。上面的 guard 写法是文档推荐的模式把第三方转写服务包一层凭据没配置时返回干净的 4xxhandleTranscribe会把错误消息里的 api key / unauthorized 映射为 AUTH_FAILED → HTTP 401而不是让底层 SDK 抛出不透明的 5xx。如果你的部署一定会配置OPENAI_API_KEY也可以直接new TranscriptionServiceOpenAI({ openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }), model: whisper-1, // 默认值 language: en, // 可选ISO-639-1 语言代码 prompt: Technical discussion context, // 可选帮助领域术语识别 temperature: 0, // 可选0 确定性输出 });接入自定义 TranscriptionServiceTranscriptionService是copilotkit/runtime导出的抽象类继承它就能接入任意转写提供方——Whisper、AssemblyAI、Deepgram 或自研模型不必依赖 OpenAI。packages/voice/README.md 给出的最小接入方式import { TranscriptionService, TranscribeFileOptions, } from copilotkit/runtime; class MyTranscriptionService extends TranscriptionService { async transcribeFile(options: TranscribeFileOptions): Promisestring { // options.audioFile、options.mimeType、options.size return transcribed text; } }实现要点只有两个接收TranscribeFileOptions含audioFile、mimeType、size返回转写后的文本字符串。然后把new MyTranscriptionService()传给CopilotRuntime的transcriptionService选项即可其余链路/info声明、POST /transcribe转发、前端麦克风按钮与 OpenAI 版完全相同。如何验证接入成功仓库的 e2e 测试 voice.spec.ts 展示了对应的人工验证方式可以照着在浏览器里核对打开语音演示页后输入框区域出现麦克风按钮。测试里的判断逻辑是[data-testidcopilot-start-transcribe-button]可见就证明 runtime 在/info上声明了audioFileTranscriptionEnabled: true、transcriptionService已挂到/api/copilotkit-voice上。冷启动的开发服务器上/info往返可能较慢测试给了 15 秒超时手动验证时也请多等几秒再下结论。点麦克风按钮说话、再点一次结束转写文本应出现在输入框data-testidcopilot-chat-textarea并提交给 agent。麦克风不方便时可以走样本按钮路径验证前端链路点击样本按钮后固定文案文档示例中的是What is the weather in Tokyo?会同步出现在输入框没有 Transcribing… 中间状态、也没有/transcribe往返随后点发送data-testidcopilot-send-button应出现 agent 回复消息。这条路径不经过真实转写只验证文本进输入框 → 发送 → agent 响应这一段。后端凭据问题可以直接从 HTTP 状态码判断未配置OPENAI_API_KEY时guard 服务让/transcribe返回 401AUTH_FAILED配置后该请求应正常返回文本。限制与注意事项语音转写只针对把实时语音转成聊天输入。如果只是上传音频/图片/视频/文档附件文档指向的是 Multimodal Attachments 功能不属于本链路。麦克风按钮是否出现完全由 runtime 的/info声明决定。前端看不到按钮时按链路排查点依次是basePath与路由目录是否一致、transcriptionService是否真的传给了CopilotRuntime、/info响应里是否有audioFileTranscriptionEnabled: true。文档说明 v2 的CopilotChat目前没有外部受控输入 API向 composer 注入文本需要走data-testidcopilot-chat-textarea加原生 value setter 的 DOM 方式这也是上面样本按钮的实现原因。参考路由中的withForwardedHeaders包装、createBuiltInAgent等是 showcase 工程自身的头部转发与 agent 工厂你的项目替换为自己的 agent 与常规请求导出即可不属于语音功能必需的接线。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考