chenbhao commited on
Commit
52c09f9
·
1 Parent(s): da5882b

feat: /voice use best grop whisper provider free and fast

Browse files
README.md CHANGED
@@ -95,7 +95,8 @@ cp VersperClaw ~/.local/bin
95
  "ENABLE_TOOL_SEARCH": "true",
96
  "TAVILY_API_KEY": "",
97
  # "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "0", default 1
98
- "teammateMode": "tmux" # or in-process
 
99
  }
100
 
101
  # set auto-dream config
@@ -154,6 +155,8 @@ mitmproxy -p 8888
154
 
155
  ```bash
156
  # /voice
 
 
157
  uv venv
158
  uv pip install faster-whisper edge-tts voxcpm
159
 
 
95
  "ENABLE_TOOL_SEARCH": "true",
96
  "TAVILY_API_KEY": "",
97
  # "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "0", default 1
98
+ "teammateMode": "tmux", # or in-process
99
+ "groqApiKey": "gsk_" # https://console.groq.com
100
  }
101
 
102
  # set auto-dream config
 
155
 
156
  ```bash
157
  # /voice
158
+ # node-edge-tts
159
+ # local whisper-stt
160
  uv venv
161
  uv pip install faster-whisper edge-tts voxcpm
162
 
src/commands/voice/voice.ts CHANGED
@@ -88,10 +88,10 @@ export const call: LocalCommandCall = async () => {
88
  }
89
  }
90
 
91
- // Enable voice with local provider
92
  const result = updateSettingsForSource('userSettings', {
93
  voiceEnabled: true,
94
- voiceProvider: 'local' as const,
95
  voiceAutoTTS: true,
96
  })
97
  if (result.error) {
@@ -124,6 +124,6 @@ export const call: LocalCommandCall = async () => {
124
  }
125
  return {
126
  type: 'text' as const,
127
- value: `Voice mode enabled (local whisper). Hold ${key} to record.${langNote}`,
128
  }
129
  }
 
88
  }
89
  }
90
 
91
+ // Enable voice with Groq provider (cloud Whisper API, no Python needed)
92
  const result = updateSettingsForSource('userSettings', {
93
  voiceEnabled: true,
94
+ voiceProvider: 'groq' as const,
95
  voiceAutoTTS: true,
96
  })
97
  if (result.error) {
 
124
  }
125
  return {
126
  type: 'text' as const,
127
+ value: `Voice mode enabled (Groq Whisper). Hold ${key} to record.${langNote}`,
128
  }
129
  }
src/hooks/useAutoTTS.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { useEffect, useRef } from 'react'
2
- import { speakWithEdgeTTS, playAudioFile } from '../services/voice/edgeTTS.js'
 
3
  import { getInitialSettings } from '../utils/settings/settings.js'
4
  import type { RenderableMessage } from '../types/message.js'
5
 
@@ -67,10 +68,7 @@ export function useAutoTTS(messages: RenderableMessage[], isLoading?: boolean):
67
  const text = content.text
68
 
69
  const run = async () => {
70
- const result = await speakWithEdgeTTS(text, {
71
- voice,
72
- pythonPath: settings.voiceTTSCommand || undefined,
73
- })
74
  if (result.success && result.audioPath) {
75
  await playAudioFile(result.audioPath)
76
  }
 
1
  import { useEffect, useRef } from 'react'
2
+ import { playAudioFile } from '../services/voice/edgeTTS.js'
3
+ import { edgeTts } from '../friend/tts.js'
4
  import { getInitialSettings } from '../utils/settings/settings.js'
5
  import type { RenderableMessage } from '../types/message.js'
6
 
 
68
  const text = content.text
69
 
70
  const run = async () => {
71
+ const result = await edgeTts({ text, voice })
 
 
 
72
  if (result.success && result.audioPath) {
73
  await playAudioFile(result.audioPath)
74
  }
src/hooks/useVoice.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
  type VoiceStreamConnection,
23
  } from '../services/voiceStreamSTT.js'
24
  import { connectDoubaoStream } from '../services/doubaoSTT.js'
 
25
  import { connectLocalWhisperStream, preloadWhisperModel } from '../services/voice/whisperSTT.js'
26
  import { logForDebugging } from '../utils/debug.js'
27
  import { toError } from '../utils/errors.js'
@@ -148,6 +149,10 @@ function isLocalProvider(): boolean {
148
  return getInitialSettings().voiceProvider === 'local'
149
  }
150
 
 
 
 
 
151
  // Lazy-loaded voice module. We defer importing voice.ts (and its native
152
  // audio-capture-napi dependency) until voice input is actually activated.
153
  // On macOS, loading the native audio module can trigger a TCC microphone
@@ -401,8 +406,8 @@ export function useVoice({
401
  !silentDropRetriedRef.current &&
402
  fullAudioRef.current.length > 0
403
  ) {
404
- // Local whisper doesn't support silent-drop replay (different backend)
405
- if (isLocalProvider()) {
406
  callbacks.onClose()
407
  return
408
  }
@@ -595,7 +600,7 @@ export function useVoice({
595
  // stop when it loses focus. This enables a "multi-clauding army"
596
  // workflow where voice input follows window focus.
597
  useEffect(() => {
598
- if (!enabled || !focusMode || isDoubaoProvider() || isLocalProvider()) {
599
  // Focus mode was disabled while a focus-driven recording was active —
600
  // stop the recording so it doesn't linger until the silence timer fires.
601
  if (focusTriggeredRef.current && stateRef.current === 'recording') {
@@ -810,6 +815,9 @@ export function useVoice({
810
  connectLocalWhisperStream(cbs, { language: stt.code })
811
  } else if (isDoubaoProvider()) {
812
  connectFn = (cbs, opts) => connectDoubaoStream(cbs, opts)
 
 
 
813
  } else {
814
  connectFn = (cbs, opts) => connectVoiceStream(cbs, opts)
815
  }
@@ -1026,6 +1034,13 @@ export function useVoice({
1026
  onErrorRef.current?.(
1027
  'Local voice mode failed. Ensure Python with faster-whisper is installed.',
1028
  )
 
 
 
 
 
 
 
1029
  } else {
1030
  logForDebugging(
1031
  '[voice] Failed to connect to voice_stream (no OAuth token?)',
@@ -1051,8 +1066,8 @@ export function useVoice({
1051
  })
1052
  }
1053
 
1054
- // Doubao and local backends don't use keyterms — skip the async fetch
1055
- if (isDoubaoProvider() || isLocalProvider()) {
1056
  attemptConnect([])
1057
  } else {
1058
  void getVoiceKeyterms().then(attemptConnect)
@@ -1070,7 +1085,7 @@ export function useVoice({
1070
  // delay of ~500ms on macOS).
1071
  const handleKeyEvent = useCallback(
1072
  (fallbackMs = REPEAT_FALLBACK_MS): void => {
1073
- const sttAvailable = isLocalProvider()
1074
  ? true
1075
  : isDoubaoProvider()
1076
  ? isDoubaoAvailableSync()
 
22
  type VoiceStreamConnection,
23
  } from '../services/voiceStreamSTT.js'
24
  import { connectDoubaoStream } from '../services/doubaoSTT.js'
25
+ import { connectGroqStream } from '../services/voice/groqSTT.js'
26
  import { connectLocalWhisperStream, preloadWhisperModel } from '../services/voice/whisperSTT.js'
27
  import { logForDebugging } from '../utils/debug.js'
28
  import { toError } from '../utils/errors.js'
 
149
  return getInitialSettings().voiceProvider === 'local'
150
  }
151
 
152
+ function isGroqProvider(): boolean {
153
+ return getInitialSettings().voiceProvider === 'groq'
154
+ }
155
+
156
  // Lazy-loaded voice module. We defer importing voice.ts (and its native
157
  // audio-capture-napi dependency) until voice input is actually activated.
158
  // On macOS, loading the native audio module can trigger a TCC microphone
 
406
  !silentDropRetriedRef.current &&
407
  fullAudioRef.current.length > 0
408
  ) {
409
+ // Local whisper & groq don't support silent-drop replay (batch backends)
410
+ if (isLocalProvider() || isGroqProvider()) {
411
  callbacks.onClose()
412
  return
413
  }
 
600
  // stop when it loses focus. This enables a "multi-clauding army"
601
  // workflow where voice input follows window focus.
602
  useEffect(() => {
603
+ if (!enabled || !focusMode || isDoubaoProvider() || isLocalProvider() || isGroqProvider()) {
604
  // Focus mode was disabled while a focus-driven recording was active —
605
  // stop the recording so it doesn't linger until the silence timer fires.
606
  if (focusTriggeredRef.current && stateRef.current === 'recording') {
 
815
  connectLocalWhisperStream(cbs, { language: stt.code })
816
  } else if (isDoubaoProvider()) {
817
  connectFn = (cbs, opts) => connectDoubaoStream(cbs, opts)
818
+ } else if (isGroqProvider()) {
819
+ connectFn = (cbs, opts) =>
820
+ connectGroqStream(cbs, { language: opts.language })
821
  } else {
822
  connectFn = (cbs, opts) => connectVoiceStream(cbs, opts)
823
  }
 
1034
  onErrorRef.current?.(
1035
  'Local voice mode failed. Ensure Python with faster-whisper is installed.',
1036
  )
1037
+ } else if (isGroqProvider()) {
1038
+ logForDebugging(
1039
+ '[voice] Groq STT failed to initialize',
1040
+ )
1041
+ onErrorRef.current?.(
1042
+ 'Groq STT failed. Check your GROQ_API_KEY.',
1043
+ )
1044
  } else {
1045
  logForDebugging(
1046
  '[voice] Failed to connect to voice_stream (no OAuth token?)',
 
1066
  })
1067
  }
1068
 
1069
+ // Doubao, local, and groq backends don't use keyterms — skip the async fetch
1070
+ if (isDoubaoProvider() || isLocalProvider() || isGroqProvider()) {
1071
  attemptConnect([])
1072
  } else {
1073
  void getVoiceKeyterms().then(attemptConnect)
 
1085
  // delay of ~500ms on macOS).
1086
  const handleKeyEvent = useCallback(
1087
  (fallbackMs = REPEAT_FALLBACK_MS): void => {
1088
+ const sttAvailable = isLocalProvider() || isGroqProvider()
1089
  ? true
1090
  : isDoubaoProvider()
1091
  ? isDoubaoAvailableSync()
src/utils/settings/types.ts CHANGED
@@ -868,10 +868,11 @@ export const SettingsSchema = lazySchema(() =>
868
  .optional()
869
  .describe('Enable voice mode (hold-to-talk dictation)'),
870
  voiceProvider: z
871
- .enum(['local', 'doubao', 'anthropic'])
872
  .optional()
873
  .describe(
874
- 'Voice STT backend: "local" (whisper/whisper.cpp / faster-whisper) or "doubao" (Doubao ASR).'
 
875
  + ' "anthropic" is a client-side compatibility alias.',
876
  ),
877
  voiceAutoTTS: z
 
868
  .optional()
869
  .describe('Enable voice mode (hold-to-talk dictation)'),
870
  voiceProvider: z
871
+ .enum(['local', 'doubao', 'anthropic', 'groq'])
872
  .optional()
873
  .describe(
874
+ 'Voice STT backend: "local" (whisper/whisper.cpp / faster-whisper),'
875
+ + ' "doubao" (Doubao ASR), or "groq" (Groq Whisper API).'
876
  + ' "anthropic" is a client-side compatibility alias.',
877
  ),
878
  voiceAutoTTS: z