feat: /voice use best grop whisper provider free and fast
Browse files- README.md +4 -1
- src/commands/voice/voice.ts +3 -3
- src/hooks/useAutoTTS.ts +3 -5
- src/hooks/useVoice.ts +21 -6
- src/utils/settings/types.ts +3 -2
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
|
| 92 |
const result = updateSettingsForSource('userSettings', {
|
| 93 |
voiceEnabled: true,
|
| 94 |
-
voiceProvider: '
|
| 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 (
|
| 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 {
|
|
|
|
| 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
|
| 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
|
| 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
|
| 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)
|
|
|
|
| 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
|