voice-mode 4.4.0__py3-none-any.whl → 4.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voice_mode/__version__.py +1 -1
- voice_mode/cli.py +79 -3
- voice_mode/cli_commands/transcribe.py +7 -6
- voice_mode/config.py +1 -1
- voice_mode/conversation_logger.py +6 -0
- voice_mode/core.py +9 -2
- voice_mode/frontend/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/app-build-manifest.json +5 -5
- voice_mode/frontend/.next/build-manifest.json +3 -3
- voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
- voice_mode/frontend/.next/next-server.js.nft.json +1 -1
- voice_mode/frontend/.next/prerender-manifest.json +1 -1
- voice_mode/frontend/.next/required-server-files.json +1 -1
- voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
- voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/server/app/_not-found.html +1 -1
- voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
- voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
- voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
- voice_mode/frontend/.next/server/app/index.html +1 -1
- voice_mode/frontend/.next/server/app/index.rsc +2 -2
- voice_mode/frontend/.next/server/app/page.js +3 -3
- voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/server/chunks/994.js +1 -1
- voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
- voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
- voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
- voice_mode/frontend/.next/server/pages/404.html +1 -1
- voice_mode/frontend/.next/server/pages/500.html +1 -1
- voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
- voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
- voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/page.js +3 -3
- voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/server.js +1 -1
- voice_mode/frontend/.next/static/chunks/app/layout-d3ec7f6f14ea7396.js +1 -0
- voice_mode/frontend/.next/static/chunks/app/{page-ae0d14863ed895ea.js → page-471796963fb1a4bd.js} +1 -1
- voice_mode/frontend/.next/static/chunks/{main-app-836e76fc70b52220.js → main-app-78da5e437b6a2a9f.js} +1 -1
- voice_mode/frontend/.next/trace +43 -43
- voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
- voice_mode/frontend/.next/types/app/layout.ts +1 -1
- voice_mode/frontend/.next/types/app/page.ts +1 -1
- voice_mode/frontend/package-lock.json +26 -15
- voice_mode/provider_discovery.py +55 -79
- voice_mode/providers.py +61 -45
- voice_mode/simple_failover.py +41 -12
- voice_mode/tools/__init__.py +138 -30
- voice_mode/tools/converse.py +148 -337
- voice_mode/tools/diagnostics.py +2 -1
- voice_mode/tools/voice_registry.py +24 -28
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/METADATA +5 -2
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/RECORD +74 -74
- voice_mode/frontend/.next/static/chunks/app/layout-917e8410913fe899.js +0 -1
- /voice_mode/frontend/.next/static/{WhZriRkBKVNPSmCnOFRav → Ni4GIqyDdn0QehvmlLBZg}/_buildManifest.js +0 -0
- /voice_mode/frontend/.next/static/{WhZriRkBKVNPSmCnOFRav → Ni4GIqyDdn0QehvmlLBZg}/_ssgManifest.js +0 -0
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/WHEEL +0 -0
- {voice_mode-4.4.0.dist-info → voice_mode-4.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
// File: /tmp/build-via-sdist-
|
1
|
+
// File: /tmp/build-via-sdist-yrh8rni2/voice_mode-4.5.0/voice_mode/frontend/app/api/connection-details/route.ts
|
2
2
|
import * as entry from '../../../../../app/api/connection-details/route.js'
|
3
3
|
import type { NextRequest } from 'next/server.js'
|
4
4
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
// File: /tmp/build-via-sdist-
|
1
|
+
// File: /tmp/build-via-sdist-yrh8rni2/voice_mode-4.5.0/voice_mode/frontend/app/layout.tsx
|
2
2
|
import * as entry from '../../../app/layout.js'
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
4
4
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
// File: /tmp/build-via-sdist-
|
1
|
+
// File: /tmp/build-via-sdist-yrh8rni2/voice_mode-4.5.0/voice_mode/frontend/app/page.tsx
|
2
2
|
import * as entry from '../../../app/page.js'
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
4
4
|
|
@@ -1328,6 +1328,16 @@
|
|
1328
1328
|
"dev": true,
|
1329
1329
|
"license": "MIT"
|
1330
1330
|
},
|
1331
|
+
"node_modules/baseline-browser-mapping": {
|
1332
|
+
"version": "2.8.5",
|
1333
|
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.5.tgz",
|
1334
|
+
"integrity": "sha512-TiU4qUT9jdCuh4aVOG7H1QozyeI2sZRqoRPdqBIaslfNt4WUSanRBueAwl2x5jt4rXBMim3lIN2x6yT8PDi24Q==",
|
1335
|
+
"dev": true,
|
1336
|
+
"license": "Apache-2.0",
|
1337
|
+
"bin": {
|
1338
|
+
"baseline-browser-mapping": "dist/cli.js"
|
1339
|
+
}
|
1340
|
+
},
|
1331
1341
|
"node_modules/binary-extensions": {
|
1332
1342
|
"version": "2.3.0",
|
1333
1343
|
"dev": true,
|
@@ -1360,9 +1370,9 @@
|
|
1360
1370
|
}
|
1361
1371
|
},
|
1362
1372
|
"node_modules/browserslist": {
|
1363
|
-
"version": "4.
|
1364
|
-
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.
|
1365
|
-
"integrity": "sha512-
|
1373
|
+
"version": "4.26.2",
|
1374
|
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
1375
|
+
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
|
1366
1376
|
"dev": true,
|
1367
1377
|
"funding": [
|
1368
1378
|
{
|
@@ -1380,9 +1390,10 @@
|
|
1380
1390
|
],
|
1381
1391
|
"license": "MIT",
|
1382
1392
|
"dependencies": {
|
1383
|
-
"
|
1384
|
-
"
|
1385
|
-
"
|
1393
|
+
"baseline-browser-mapping": "^2.8.3",
|
1394
|
+
"caniuse-lite": "^1.0.30001741",
|
1395
|
+
"electron-to-chromium": "^1.5.218",
|
1396
|
+
"node-releases": "^2.0.21",
|
1386
1397
|
"update-browserslist-db": "^1.1.3"
|
1387
1398
|
},
|
1388
1399
|
"bin": {
|
@@ -1489,9 +1500,9 @@
|
|
1489
1500
|
}
|
1490
1501
|
},
|
1491
1502
|
"node_modules/caniuse-lite": {
|
1492
|
-
"version": "1.0.
|
1493
|
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
1494
|
-
"integrity": "sha512-
|
1503
|
+
"version": "1.0.30001743",
|
1504
|
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
|
1505
|
+
"integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
|
1495
1506
|
"dev": true,
|
1496
1507
|
"funding": [
|
1497
1508
|
{
|
@@ -1774,9 +1785,9 @@
|
|
1774
1785
|
"license": "MIT"
|
1775
1786
|
},
|
1776
1787
|
"node_modules/electron-to-chromium": {
|
1777
|
-
"version": "1.5.
|
1778
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
1779
|
-
"integrity": "sha512-
|
1788
|
+
"version": "1.5.220",
|
1789
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.220.tgz",
|
1790
|
+
"integrity": "sha512-TWXijEwR1ggr4BdAKrb1nMNqYLTx1/4aD1fkeZU+FVJGTKu53/T7UyHKXlqEX3Ub02csyHePbHmkvnrjcaYzMA==",
|
1780
1791
|
"dev": true,
|
1781
1792
|
"license": "ISC"
|
1782
1793
|
},
|
@@ -3799,9 +3810,9 @@
|
|
3799
3810
|
}
|
3800
3811
|
},
|
3801
3812
|
"node_modules/node-releases": {
|
3802
|
-
"version": "2.0.
|
3803
|
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.
|
3804
|
-
"integrity": "sha512-
|
3813
|
+
"version": "2.0.21",
|
3814
|
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
3815
|
+
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
|
3805
3816
|
"dev": true,
|
3806
3817
|
"license": "MIT"
|
3807
3818
|
},
|
voice_mode/provider_discovery.py
CHANGED
@@ -26,6 +26,8 @@ logger = logging.getLogger("voice-mode")
|
|
26
26
|
|
27
27
|
def detect_provider_type(base_url: str) -> str:
|
28
28
|
"""Detect provider type from base URL."""
|
29
|
+
if not base_url:
|
30
|
+
return "unknown"
|
29
31
|
if "openai.com" in base_url:
|
30
32
|
return "openai"
|
31
33
|
elif ":8880" in base_url:
|
@@ -47,6 +49,8 @@ def detect_provider_type(base_url: str) -> str:
|
|
47
49
|
|
48
50
|
def is_local_provider(base_url: str) -> bool:
|
49
51
|
"""Check if a provider URL is for a local service."""
|
52
|
+
if not base_url:
|
53
|
+
return False
|
50
54
|
provider_type = detect_provider_type(base_url)
|
51
55
|
return provider_type in ["kokoro", "whisper", "local"] or \
|
52
56
|
"127.0.0.1" in base_url or \
|
@@ -57,13 +61,11 @@ def is_local_provider(base_url: str) -> bool:
|
|
57
61
|
class EndpointInfo:
|
58
62
|
"""Information about a discovered endpoint."""
|
59
63
|
base_url: str
|
60
|
-
healthy: bool
|
61
64
|
models: List[str]
|
62
65
|
voices: List[str] # Only for TTS
|
63
|
-
last_health_check: str # ISO format timestamp
|
64
|
-
response_time_ms: Optional[float] = None
|
65
|
-
error: Optional[str] = None
|
66
66
|
provider_type: Optional[str] = None # e.g., "openai", "kokoro", "whisper"
|
67
|
+
last_check: Optional[str] = None # ISO format timestamp of last attempt
|
68
|
+
last_error: Optional[str] = None # Last error if any
|
67
69
|
|
68
70
|
|
69
71
|
class ProviderRegistry:
|
@@ -78,44 +80,38 @@ class ProviderRegistry:
|
|
78
80
|
self._initialized = False
|
79
81
|
|
80
82
|
async def initialize(self):
|
81
|
-
"""Initialize the registry
|
83
|
+
"""Initialize the registry with configured endpoints."""
|
82
84
|
if self._initialized:
|
83
85
|
return
|
84
|
-
|
86
|
+
|
85
87
|
async with self._discovery_lock:
|
86
88
|
if self._initialized: # Double-check after acquiring lock
|
87
89
|
return
|
88
|
-
|
89
|
-
logger.info("Initializing provider registry
|
90
|
-
|
91
|
-
# Initialize TTS endpoints
|
90
|
+
|
91
|
+
logger.info("Initializing provider registry...")
|
92
|
+
|
93
|
+
# Initialize TTS endpoints
|
92
94
|
for url in TTS_BASE_URLS:
|
93
95
|
provider_type = detect_provider_type(url)
|
94
96
|
self.registry["tts"][url] = EndpointInfo(
|
95
97
|
base_url=url,
|
96
|
-
healthy=True,
|
97
98
|
models=["gpt4o-mini-tts", "tts-1", "tts-1-hd"] if provider_type == "openai" else ["tts-1"],
|
98
99
|
voices=["alloy", "echo", "fable", "nova", "onyx", "shimmer"] if provider_type == "openai" else ["af_alloy", "af_aoede", "af_bella", "af_heart", "af_jadzia", "af_jessica", "af_kore", "af_nicole", "af_nova", "af_river", "af_sarah", "af_sky", "af_v0", "af_v0bella", "af_v0irulan", "af_v0nicole", "af_v0sarah", "af_v0sky", "am_adam", "am_echo", "am_eric", "am_fenrir", "am_liam", "am_michael", "am_onyx", "am_puck", "am_santa", "am_v0adam", "am_v0gurney", "am_v0michael", "bf_alice", "bf_emma", "bf_lily", "bf_v0emma", "bf_v0isabella", "bm_daniel", "bm_fable", "bm_george", "bm_lewis", "bm_v0george", "bm_v0lewis", "ef_dora", "em_alex", "em_santa", "ff_siwis", "hf_alpha", "hf_beta", "hm_omega", "hm_psi", "if_sara", "im_nicola", "jf_alpha", "jf_gongitsune", "jf_nezumi", "jf_tebukuro", "jm_kumo", "pf_dora", "pm_alex", "pm_santa", "zf_xiaobei", "zf_xiaoni", "zf_xiaoxiao", "zf_xiaoyi", "zm_yunjian", "zm_yunxi", "zm_yunxia", "zm_yunyang"],
|
99
|
-
last_health_check=datetime.now(timezone.utc).isoformat(),
|
100
|
-
response_time_ms=None,
|
101
100
|
provider_type=provider_type
|
102
101
|
)
|
103
102
|
|
104
|
-
# Initialize STT endpoints
|
103
|
+
# Initialize STT endpoints
|
105
104
|
for url in STT_BASE_URLS:
|
106
105
|
provider_type = detect_provider_type(url)
|
107
106
|
self.registry["stt"][url] = EndpointInfo(
|
108
107
|
base_url=url,
|
109
|
-
healthy=True,
|
110
108
|
models=["whisper-1"],
|
111
|
-
voices=[],
|
112
|
-
last_health_check=datetime.now(timezone.utc).isoformat(),
|
113
|
-
response_time_ms=None,
|
109
|
+
voices=[], # STT doesn't have voices
|
114
110
|
provider_type=provider_type
|
115
111
|
)
|
116
|
-
|
112
|
+
|
117
113
|
self._initialized = True
|
118
|
-
logger.info(f"Provider registry initialized with {len(self.registry['tts'])} TTS and {len(self.registry['stt'])} STT endpoints
|
114
|
+
logger.info(f"Provider registry initialized with {len(self.registry['tts'])} TTS and {len(self.registry['stt'])} STT endpoints")
|
119
115
|
|
120
116
|
async def _discover_endpoints(self, service_type: str, base_urls: List[str]):
|
121
117
|
"""Discover all endpoints for a service type."""
|
@@ -131,12 +127,11 @@ class ProviderRegistry:
|
|
131
127
|
logger.error(f"Failed to discover {service_type} endpoint {url}: {result}")
|
132
128
|
self.registry[service_type][url] = EndpointInfo(
|
133
129
|
base_url=url,
|
134
|
-
healthy=False,
|
135
130
|
models=[],
|
136
131
|
voices=[],
|
137
|
-
|
138
|
-
|
139
|
-
|
132
|
+
provider_type=detect_provider_type(url),
|
133
|
+
last_check=datetime.now(timezone.utc).isoformat(),
|
134
|
+
last_error=str(result)
|
140
135
|
)
|
141
136
|
|
142
137
|
async def _discover_endpoint(self, service_type: str, base_url: str) -> None:
|
@@ -201,12 +196,11 @@ class ProviderRegistry:
|
|
201
196
|
# Store endpoint info
|
202
197
|
self.registry[service_type][base_url] = EndpointInfo(
|
203
198
|
base_url=base_url,
|
204
|
-
healthy=True,
|
205
199
|
models=models,
|
206
200
|
voices=voices,
|
207
|
-
|
208
|
-
|
209
|
-
|
201
|
+
provider_type=detect_provider_type(base_url),
|
202
|
+
last_check=datetime.now(timezone.utc).isoformat(),
|
203
|
+
last_error=None
|
210
204
|
)
|
211
205
|
|
212
206
|
logger.info(f"Successfully discovered {service_type} endpoint {base_url} with {len(models)} models and {len(voices)} voices")
|
@@ -215,12 +209,11 @@ class ProviderRegistry:
|
|
215
209
|
logger.warning(f"Endpoint {base_url} discovery failed: {e}")
|
216
210
|
self.registry[service_type][base_url] = EndpointInfo(
|
217
211
|
base_url=base_url,
|
218
|
-
healthy=False,
|
219
212
|
models=[],
|
220
213
|
voices=[],
|
221
|
-
|
222
|
-
|
223
|
-
|
214
|
+
provider_type=detect_provider_type(base_url),
|
215
|
+
last_check=datetime.now(timezone.utc).isoformat(),
|
216
|
+
last_error=str(e)
|
224
217
|
)
|
225
218
|
|
226
219
|
async def _discover_voices(self, base_url: str, client: AsyncOpenAI) -> List[str]:
|
@@ -247,41 +240,35 @@ class ProviderRegistry:
|
|
247
240
|
# The system will use configured defaults instead
|
248
241
|
return []
|
249
242
|
|
250
|
-
async def check_health(self, service_type: str, base_url: str) -> bool:
|
251
|
-
"""Check the health of a specific endpoint and update registry."""
|
252
|
-
logger.debug(f"Health check for {service_type} endpoint: {base_url}")
|
253
|
-
|
254
|
-
# Re-discover the endpoint
|
255
|
-
await self._discover_endpoint(service_type, base_url)
|
256
|
-
|
257
|
-
# Return health status
|
258
|
-
endpoint_info = self.registry[service_type].get(base_url)
|
259
|
-
return endpoint_info.healthy if endpoint_info else False
|
260
243
|
|
261
|
-
def
|
262
|
-
"""Get all
|
244
|
+
def get_endpoints(self, service_type: str) -> List[EndpointInfo]:
|
245
|
+
"""Get all endpoints for a service type in priority order."""
|
263
246
|
endpoints = []
|
264
|
-
|
247
|
+
|
265
248
|
# Return endpoints in the order they were configured
|
266
249
|
base_urls = TTS_BASE_URLS if service_type == "tts" else STT_BASE_URLS
|
267
|
-
|
250
|
+
|
268
251
|
for url in base_urls:
|
269
252
|
info = self.registry[service_type].get(url)
|
270
|
-
if info
|
253
|
+
if info:
|
271
254
|
endpoints.append(info)
|
272
|
-
|
255
|
+
|
273
256
|
return endpoints
|
257
|
+
|
258
|
+
def get_healthy_endpoints(self, service_type: str) -> List[EndpointInfo]:
|
259
|
+
"""Deprecated: Use get_endpoints instead. Returns all endpoints."""
|
260
|
+
return self.get_endpoints(service_type)
|
274
261
|
|
275
262
|
def find_endpoint_with_voice(self, voice: str) -> Optional[EndpointInfo]:
|
276
|
-
"""Find the first
|
277
|
-
for endpoint in self.
|
263
|
+
"""Find the first TTS endpoint that supports a specific voice."""
|
264
|
+
for endpoint in self.get_endpoints("tts"):
|
278
265
|
if voice in endpoint.voices:
|
279
266
|
return endpoint
|
280
267
|
return None
|
281
|
-
|
268
|
+
|
282
269
|
def find_endpoint_with_model(self, service_type: str, model: str) -> Optional[EndpointInfo]:
|
283
|
-
"""Find the first
|
284
|
-
for endpoint in self.
|
270
|
+
"""Find the first endpoint that supports a specific model."""
|
271
|
+
for endpoint in self.get_endpoints(service_type):
|
285
272
|
if model in endpoint.models:
|
286
273
|
return endpoint
|
287
274
|
return None
|
@@ -291,47 +278,36 @@ class ProviderRegistry:
|
|
291
278
|
return {
|
292
279
|
"tts": {
|
293
280
|
url: {
|
294
|
-
"healthy": info.healthy,
|
295
281
|
"models": info.models,
|
296
282
|
"voices": info.voices,
|
297
|
-
"
|
298
|
-
"last_check": info.
|
299
|
-
"
|
283
|
+
"provider_type": info.provider_type,
|
284
|
+
"last_check": info.last_check,
|
285
|
+
"last_error": info.last_error
|
300
286
|
}
|
301
287
|
for url, info in self.registry["tts"].items()
|
302
288
|
},
|
303
289
|
"stt": {
|
304
290
|
url: {
|
305
|
-
"healthy": info.healthy,
|
306
291
|
"models": info.models,
|
307
|
-
"
|
308
|
-
"last_check": info.
|
309
|
-
"
|
292
|
+
"provider_type": info.provider_type,
|
293
|
+
"last_check": info.last_check,
|
294
|
+
"last_error": info.last_error
|
310
295
|
}
|
311
296
|
for url, info in self.registry["stt"].items()
|
312
297
|
}
|
313
298
|
}
|
314
299
|
|
315
|
-
async def
|
316
|
-
"""
|
317
|
-
|
318
|
-
|
319
|
-
|
300
|
+
async def mark_failed(self, service_type: str, base_url: str, error: str):
|
301
|
+
"""Record that an endpoint failed.
|
302
|
+
|
303
|
+
This updates the last_error and last_check fields for diagnostics,
|
304
|
+
but doesn't prevent the endpoint from being tried again.
|
320
305
|
"""
|
321
306
|
if base_url in self.registry[service_type]:
|
322
|
-
#
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
# Update error and last check time for diagnostics, but keep healthy=True
|
327
|
-
self.registry[service_type][base_url].error = f"{error} (will retry)"
|
328
|
-
self.registry[service_type][base_url].last_health_check = datetime.now(timezone.utc).isoformat()
|
329
|
-
else:
|
330
|
-
# Normal behavior - mark as unhealthy
|
331
|
-
self.registry[service_type][base_url].healthy = False
|
332
|
-
self.registry[service_type][base_url].error = error
|
333
|
-
self.registry[service_type][base_url].last_health_check = datetime.now(timezone.utc).isoformat()
|
334
|
-
logger.warning(f"Marked {service_type} endpoint {base_url} as unhealthy: {error}")
|
307
|
+
# Update error and last check time for diagnostics
|
308
|
+
self.registry[service_type][base_url].last_error = error
|
309
|
+
self.registry[service_type][base_url].last_check = datetime.now(timezone.utc).isoformat()
|
310
|
+
logger.info(f"{service_type} endpoint {base_url} failed: {error}")
|
335
311
|
|
336
312
|
|
337
313
|
# Global registry instance
|
voice_mode/providers.py
CHANGED
@@ -10,7 +10,7 @@ from typing import Dict, Optional, List, Any, Tuple
|
|
10
10
|
from openai import AsyncOpenAI
|
11
11
|
|
12
12
|
from .config import TTS_VOICES, TTS_MODELS, TTS_BASE_URLS, OPENAI_API_KEY, get_voice_preferences
|
13
|
-
from .provider_discovery import provider_registry, EndpointInfo
|
13
|
+
from .provider_discovery import provider_registry, EndpointInfo, is_local_provider
|
14
14
|
|
15
15
|
logger = logging.getLogger("voice-mode")
|
16
16
|
|
@@ -49,21 +49,24 @@ async def get_tts_client_and_voice(
|
|
49
49
|
if base_url:
|
50
50
|
logger.info(f"TTS Provider Selection: Using specific base URL: {base_url}")
|
51
51
|
endpoint_info = provider_registry.registry["tts"].get(base_url)
|
52
|
-
if not endpoint_info
|
53
|
-
raise ValueError(f"Requested base URL {base_url} is not
|
54
|
-
|
52
|
+
if not endpoint_info:
|
53
|
+
raise ValueError(f"Requested base URL {base_url} is not configured")
|
54
|
+
|
55
55
|
selected_voice = voice or _select_voice_for_endpoint(endpoint_info)
|
56
56
|
selected_model = model or _select_model_for_endpoint(endpoint_info)
|
57
|
-
|
57
|
+
|
58
|
+
# Disable retries for local endpoints - they either work or don't
|
59
|
+
max_retries = 0 if is_local_provider(base_url) else 2
|
58
60
|
client = AsyncOpenAI(
|
59
61
|
api_key=OPENAI_API_KEY or "dummy-key-for-local",
|
60
|
-
base_url=base_url
|
62
|
+
base_url=base_url,
|
63
|
+
max_retries=max_retries
|
61
64
|
)
|
62
|
-
|
65
|
+
|
63
66
|
logger.info(f" • Selected endpoint: {base_url}")
|
64
67
|
logger.info(f" • Selected voice: {selected_voice}")
|
65
68
|
logger.info(f" • Selected model: {selected_model}")
|
66
|
-
|
69
|
+
|
67
70
|
return client, selected_voice, selected_model, endpoint_info
|
68
71
|
|
69
72
|
# Voice-first selection algorithm
|
@@ -83,69 +86,75 @@ async def get_tts_client_and_voice(
|
|
83
86
|
logger.info(f" Specific voice requested: {voice}")
|
84
87
|
for url in TTS_BASE_URLS:
|
85
88
|
endpoint_info = provider_registry.registry["tts"].get(url)
|
86
|
-
if not endpoint_info
|
89
|
+
if not endpoint_info:
|
87
90
|
continue
|
88
|
-
|
91
|
+
|
89
92
|
if voice in endpoint_info.voices:
|
90
93
|
selected_voice = voice
|
91
94
|
selected_model = _select_model_for_endpoint(endpoint_info, model)
|
92
|
-
|
95
|
+
|
93
96
|
api_key = OPENAI_API_KEY if endpoint_info.provider_type == "openai" else (OPENAI_API_KEY or "dummy-key-for-local")
|
94
|
-
|
95
|
-
|
97
|
+
# Disable retries for local endpoints - they either work or don't
|
98
|
+
max_retries = 0 if is_local_provider(url) else 2
|
99
|
+
client = AsyncOpenAI(api_key=api_key, base_url=url, max_retries=max_retries)
|
100
|
+
|
96
101
|
logger.info(f" ✓ Selected endpoint: {url} ({endpoint_info.provider_type})")
|
97
102
|
logger.info(f" ✓ Selected voice: {selected_voice}")
|
98
103
|
logger.info(f" ✓ Selected model: {selected_model}")
|
99
|
-
|
104
|
+
|
100
105
|
return client, selected_voice, selected_model, endpoint_info
|
101
106
|
|
102
107
|
# No specific voice requested - iterate through voice preferences
|
103
108
|
logger.info(" No specific voice requested, checking voice preferences...")
|
104
109
|
for preferred_voice in combined_voice_list:
|
105
110
|
logger.debug(f" Looking for voice: {preferred_voice}")
|
106
|
-
|
111
|
+
|
107
112
|
# Check each endpoint for this voice
|
108
113
|
for url in TTS_BASE_URLS:
|
109
114
|
endpoint_info = provider_registry.registry["tts"].get(url)
|
110
|
-
if not endpoint_info
|
115
|
+
if not endpoint_info:
|
111
116
|
continue
|
112
|
-
|
117
|
+
|
113
118
|
if preferred_voice in endpoint_info.voices:
|
114
119
|
logger.info(f" Found voice '{preferred_voice}' at {url} ({endpoint_info.provider_type})")
|
115
120
|
selected_voice = preferred_voice
|
116
121
|
selected_model = _select_model_for_endpoint(endpoint_info, model)
|
117
|
-
|
122
|
+
|
118
123
|
api_key = OPENAI_API_KEY if endpoint_info.provider_type == "openai" else (OPENAI_API_KEY or "dummy-key-for-local")
|
119
|
-
|
120
|
-
|
124
|
+
# Disable retries for local endpoints - they either work or don't
|
125
|
+
max_retries = 0 if is_local_provider(url) else 2
|
126
|
+
client = AsyncOpenAI(api_key=api_key, base_url=url, max_retries=max_retries)
|
127
|
+
|
121
128
|
logger.info(f" ✓ Selected endpoint: {url} ({endpoint_info.provider_type})")
|
122
129
|
logger.info(f" ✓ Selected voice: {selected_voice}")
|
123
130
|
logger.info(f" ✓ Selected model: {selected_model}")
|
124
|
-
|
131
|
+
|
125
132
|
return client, selected_voice, selected_model, endpoint_info
|
126
133
|
|
127
134
|
# No preferred voices found - fall back to any available endpoint
|
128
135
|
logger.warning(" No preferred voices available, using any available endpoint...")
|
129
136
|
for url in TTS_BASE_URLS:
|
130
137
|
endpoint_info = provider_registry.registry["tts"].get(url)
|
131
|
-
if not endpoint_info
|
138
|
+
if not endpoint_info:
|
132
139
|
continue
|
133
|
-
|
140
|
+
|
134
141
|
if endpoint_info.voices:
|
135
142
|
selected_voice = endpoint_info.voices[0]
|
136
143
|
selected_model = _select_model_for_endpoint(endpoint_info, model)
|
137
|
-
|
144
|
+
|
138
145
|
api_key = OPENAI_API_KEY if endpoint_info.provider_type == "openai" else (OPENAI_API_KEY or "dummy-key-for-local")
|
139
|
-
|
140
|
-
|
146
|
+
# Disable retries for local endpoints - they either work or don't
|
147
|
+
max_retries = 0 if is_local_provider(url) else 2
|
148
|
+
client = AsyncOpenAI(api_key=api_key, base_url=url, max_retries=max_retries)
|
149
|
+
|
141
150
|
logger.info(f" ✓ Selected endpoint: {url} ({endpoint_info.provider_type})")
|
142
151
|
logger.info(f" ✓ Selected voice: {selected_voice}")
|
143
152
|
logger.info(f" ✓ Selected model: {selected_model}")
|
144
|
-
|
153
|
+
|
145
154
|
return client, selected_voice, selected_model, endpoint_info
|
146
|
-
|
155
|
+
|
147
156
|
# No suitable endpoint found
|
148
|
-
raise ValueError("No
|
157
|
+
raise ValueError("No TTS endpoints found that support requested voice/model preferences")
|
149
158
|
|
150
159
|
|
151
160
|
async def get_stt_client(
|
@@ -171,30 +180,36 @@ async def get_stt_client(
|
|
171
180
|
# If specific base_url is requested, use it directly
|
172
181
|
if base_url:
|
173
182
|
endpoint_info = provider_registry.registry["stt"].get(base_url)
|
174
|
-
if not endpoint_info
|
175
|
-
raise ValueError(f"Requested base URL {base_url} is not
|
176
|
-
|
183
|
+
if not endpoint_info:
|
184
|
+
raise ValueError(f"Requested base URL {base_url} is not configured")
|
185
|
+
|
177
186
|
selected_model = model or "whisper-1" # Default STT model
|
178
|
-
|
187
|
+
|
188
|
+
# Disable retries for local endpoints - they either work or don't
|
189
|
+
max_retries = 0 if is_local_provider(base_url) else 2
|
179
190
|
client = AsyncOpenAI(
|
180
191
|
api_key=OPENAI_API_KEY or "dummy-key-for-local",
|
181
|
-
base_url=base_url
|
192
|
+
base_url=base_url,
|
193
|
+
max_retries=max_retries
|
182
194
|
)
|
183
|
-
|
195
|
+
|
184
196
|
return client, selected_model, endpoint_info
|
185
|
-
|
186
|
-
# Get
|
187
|
-
|
188
|
-
if not
|
189
|
-
raise ValueError("No
|
190
|
-
|
191
|
-
endpoint_info =
|
197
|
+
|
198
|
+
# Get STT endpoints in priority order
|
199
|
+
endpoints = provider_registry.get_endpoints("stt")
|
200
|
+
if not endpoints:
|
201
|
+
raise ValueError("No STT endpoints available")
|
202
|
+
|
203
|
+
endpoint_info = endpoints[0]
|
192
204
|
selected_model = model or "whisper-1"
|
193
205
|
|
194
206
|
api_key = OPENAI_API_KEY if endpoint_info.provider_type == "openai" else (OPENAI_API_KEY or "dummy-key-for-local")
|
207
|
+
# Disable retries for local endpoints - they either work or don't
|
208
|
+
max_retries = 0 if is_local_provider(endpoint_info.base_url) else 2
|
195
209
|
client = AsyncOpenAI(
|
196
210
|
api_key=api_key,
|
197
|
-
base_url=endpoint_info.base_url
|
211
|
+
base_url=endpoint_info.base_url,
|
212
|
+
max_retries=max_retries
|
198
213
|
)
|
199
214
|
|
200
215
|
return client, selected_model, endpoint_info
|
@@ -256,8 +271,9 @@ async def is_provider_available(provider_id: str, timeout: float = 2.0) -> bool:
|
|
256
271
|
# Check in appropriate registry
|
257
272
|
service_type = "tts" if provider_id in ["kokoro", "openai"] else "stt"
|
258
273
|
endpoint_info = provider_registry.registry[service_type].get(base_url)
|
259
|
-
|
260
|
-
return
|
274
|
+
|
275
|
+
# Without health checks, we just return if the endpoint is configured
|
276
|
+
return endpoint_info is not None
|
261
277
|
|
262
278
|
|
263
279
|
def get_provider_by_voice(voice: str) -> Optional[Dict[str, Any]]:
|