voice-vibecoder 2.12.2__tar.gz → 2.13.0__tar.gz

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.
Files changed (44) hide show
  1. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/CHANGELOG.md +21 -0
  2. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/cli_serve.py +17 -5
  6. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/config.py +149 -38
  7. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/server.py +3 -6
  8. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/session.py +17 -7
  9. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/screen.py +3 -7
  10. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/setup.py +195 -56
  11. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/wizard.py +44 -14
  12. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/voice_providers/__init__.py +26 -2
  13. voice_vibecoder-2.13.0/src/voice_vibecoder/voice_providers/gemini.py +395 -0
  14. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/.github/workflows/ci.yml +0 -0
  15. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/.github/workflows/release.yml +0 -0
  16. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/.gitignore +0 -0
  17. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/LICENSE +0 -0
  18. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/README.md +0 -0
  19. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/app.py +0 -0
  20. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/index.html +0 -0
  21. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/app_config.py +0 -0
  22. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/audio.py +0 -0
  23. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  24. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  25. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  26. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  27. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  28. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/instances.py +0 -0
  29. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  30. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  31. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/tools/definitions.py +0 -0
  32. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
  33. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  34. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/tools/handlers.py +0 -0
  35. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  36. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  37. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  38. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  39. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/panels.py +0 -0
  40. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/state.py +0 -0
  41. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/styles.py +0 -0
  42. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  43. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {voice_vibecoder-2.12.2 → voice_vibecoder-2.13.0}/src/voice_vibecoder/worktrees.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.13.0 (2026-02-21)
6
+
7
+ ### Features
8
+
9
+ - Add silence duration and prefix padding options
10
+ ([`0d7d89c`](https://github.com/snokam/voice-vibecoder/commit/0d7d89cc238cbe396b0fe30a0fbf666118e8faf5))
11
+
12
+ ### Refactoring
13
+
14
+ - **config**: Store voice and VAD settings per provider
15
+ ([`fca2e8b`](https://github.com/snokam/voice-vibecoder/commit/fca2e8bf20ee16bc66a29ffb0b0c1544d3c39c35))
16
+
17
+
18
+ ## v2.12.3 (2026-02-21)
19
+
20
+ ### Bug Fixes
21
+
22
+ - Remove duplicate transcript send on text input
23
+ ([`21bd69b`](https://github.com/snokam/voice-vibecoder/commit/21bd69b61d1b5ceb788110fcb7a847d3e7b74d80))
24
+
25
+
5
26
  ## v2.12.2 (2026-02-21)
6
27
 
7
28
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.12.2
3
+ Version: 2.13.0
4
4
  Summary: Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code
5
5
  Project-URL: Homepage, https://github.com/snokam/voice-vibecoder
6
6
  Project-URL: Repository, https://github.com/snokam/voice-vibecoder
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "voice-vibecoder"
7
- version = "2.12.2"
7
+ version = "2.13.0"
8
8
  description = "Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  A voice-controlled coding assistant using OpenAI Realtime API + Claude Code.
4
4
  """
5
5
 
6
- __version__ = "2.12.2"
6
+ __version__ = "2.13.0"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -35,22 +35,34 @@ def _detect_git_root() -> Path:
35
35
 
36
36
  def _apply_env_overrides(settings: RealtimeSettings) -> RealtimeSettings:
37
37
  """Override settings with VOICE_* environment variables when present."""
38
- env_map = {
38
+ applied = []
39
+
40
+ # Top-level overrides
41
+ simple_map = {
39
42
  "VOICE_PROVIDER": "provider",
40
43
  "VOICE_API_KEY": "api_key",
41
- "VOICE_AZURE_ENDPOINT": "azure_endpoint",
42
- "VOICE_AZURE_DEPLOYMENT": "azure_deployment",
43
44
  "VOICE_LANGUAGE": "language",
44
45
  "VOICE_PERMISSION_MODE": "permission_mode",
45
46
  }
46
- applied = []
47
- for env_var, attr in env_map.items():
47
+ for env_var, attr in simple_map.items():
48
48
  value = os.environ.get(env_var)
49
49
  if value:
50
50
  setattr(settings, attr, value)
51
51
  safe = env_var if "KEY" not in env_var else f"{env_var}=***"
52
52
  applied.append(safe)
53
53
 
54
+ # Nested provider overrides
55
+ nested_map = {
56
+ "VOICE_AZURE_ENDPOINT": (settings.azure, "endpoint"),
57
+ "VOICE_AZURE_DEPLOYMENT": (settings.azure, "deployment"),
58
+ "VOICE_GEMINI_MODEL": (settings.gemini, "model"),
59
+ }
60
+ for env_var, (obj, attr) in nested_map.items():
61
+ value = os.environ.get(env_var)
62
+ if value:
63
+ setattr(obj, attr, value)
64
+ applied.append(env_var)
65
+
54
66
  if applied:
55
67
  logger.info("Env overrides applied: %s", ", ".join(applied))
56
68
  return settings
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ from dataclasses import dataclass, asdict, field
6
7
  from pathlib import Path
7
8
  from typing import Literal
8
9
 
@@ -228,7 +229,7 @@ def get_system_instructions(
228
229
  return base
229
230
 
230
231
 
231
- Provider = Literal["openai", "azure"]
232
+ Provider = Literal["openai", "azure", "gemini"]
232
233
 
233
234
  VOICES = ["ash", "ballad", "coral", "sage", "shimmer", "alloy", "echo", "verse"]
234
235
 
@@ -239,6 +240,10 @@ AZURE_DEFAULT_ENDPOINT = ""
239
240
  AZURE_DEFAULT_DEPLOYMENT = ""
240
241
  AZURE_API_VERSION = "2024-10-01-preview"
241
242
 
243
+ GEMINI_WS_URL = "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent"
244
+ GEMINI_DEFAULT_MODEL = "models/gemini-2.5-flash-native-audio-latest"
245
+ GEMINI_VOICES = ["Kore", "Aoede", "Charon", "Fenrir", "Puck", "Leda", "Orus", "Zephyr"]
246
+
242
247
  # Module-level configurable path — set via configure_paths()
243
248
  from voice_vibecoder.app_config import _default_config_dir
244
249
 
@@ -259,6 +264,38 @@ def configure_paths(config_dir: Path) -> None:
259
264
  SETTINGS_PATH = config_dir / "realtime.json"
260
265
 
261
266
 
267
+ @dataclass
268
+ class OpenAIConfig:
269
+ """Per-provider settings for OpenAI."""
270
+ model: str = OPENAI_DEFAULT_MODEL
271
+ voice: str = "ash"
272
+ vad_threshold: float = 0.8
273
+ silence_duration_ms: int = 700
274
+ prefix_padding_ms: int = 300
275
+
276
+
277
+ @dataclass
278
+ class AzureConfig:
279
+ """Per-provider settings for Azure OpenAI."""
280
+ endpoint: str = AZURE_DEFAULT_ENDPOINT
281
+ deployment: str = AZURE_DEFAULT_DEPLOYMENT
282
+ voice: str = "ash"
283
+ vad_threshold: float = 0.8
284
+ silence_duration_ms: int = 700
285
+ prefix_padding_ms: int = 300
286
+
287
+
288
+ @dataclass
289
+ class GeminiConfig:
290
+ """Per-provider settings for Gemini."""
291
+ model: str = GEMINI_DEFAULT_MODEL
292
+ voice: str = "Puck"
293
+ start_sensitivity: str = "HIGH" # "HIGH" or "LOW"
294
+ end_sensitivity: str = "LOW" # "HIGH" or "LOW"
295
+ silence_duration_ms: int = 700
296
+ prefix_padding_ms: int = 100
297
+
298
+
262
299
  class RealtimeSettings:
263
300
  """Persisted settings for the Realtime voice coding feature."""
264
301
 
@@ -266,17 +303,10 @@ class RealtimeSettings:
266
303
  self,
267
304
  provider: Provider = "openai",
268
305
  api_key: str = "",
269
- # OpenAI
270
- openai_model: str = OPENAI_DEFAULT_MODEL,
271
- # Azure
272
- azure_endpoint: str = AZURE_DEFAULT_ENDPOINT,
273
- azure_deployment: str = AZURE_DEFAULT_DEPLOYMENT,
274
- # Voice & session
275
- voice: str = "ash",
276
- # VAD — higher threshold = less sensitive to background noise
277
- vad_threshold: float = 0.8,
278
- silence_duration_ms: int = 700,
279
- prefix_padding_ms: int = 300,
306
+ # Per-provider configs
307
+ openai: OpenAIConfig | None = None,
308
+ azure: AzureConfig | None = None,
309
+ gemini: GeminiConfig | None = None,
280
310
  # Agent
281
311
  agent_timeout: int = 300,
282
312
  # Language
@@ -290,33 +320,51 @@ class RealtimeSettings:
290
320
  ) -> None:
291
321
  self.provider = provider
292
322
  self.api_key = api_key
293
- self.openai_model = openai_model
294
- self.azure_endpoint = azure_endpoint
295
- self.azure_deployment = azure_deployment
296
- self.voice = voice
297
- self.vad_threshold = vad_threshold
298
- self.silence_duration_ms = silence_duration_ms
299
- self.prefix_padding_ms = prefix_padding_ms
323
+ self.openai = openai or OpenAIConfig()
324
+ self.azure = azure or AzureConfig()
325
+ self.gemini = gemini or GeminiConfig()
300
326
  self.agent_timeout = agent_timeout
301
327
  self.language = language
302
328
  self.permission_mode = permission_mode
303
329
  self.input_mode = input_mode
304
330
  self.enabled_agents: list[str] = enabled_agents or ["claude"]
305
331
 
332
+ @property
333
+ def active_voice(self) -> str:
334
+ """Return the voice setting for the active provider."""
335
+ if self.provider == "azure":
336
+ return self.azure.voice
337
+ if self.provider == "gemini":
338
+ return self.gemini.voice
339
+ return self.openai.voice
340
+
341
+ @property
342
+ def active_provider_config(self) -> OpenAIConfig | AzureConfig | GeminiConfig:
343
+ """Return the config object for the active provider."""
344
+ if self.provider == "azure":
345
+ return self.azure
346
+ if self.provider == "gemini":
347
+ return self.gemini
348
+ return self.openai
349
+
306
350
  @property
307
351
  def ws_url(self) -> str:
308
352
  if self.provider == "azure":
309
353
  return (
310
- f"wss://{self.azure_endpoint}/openai/realtime"
354
+ f"wss://{self.azure.endpoint}/openai/realtime"
311
355
  f"?api-version={AZURE_API_VERSION}"
312
- f"&deployment={self.azure_deployment}"
356
+ f"&deployment={self.azure.deployment}"
313
357
  )
314
- return f"{OPENAI_WS_URL}?model={self.openai_model}"
358
+ if self.provider == "gemini":
359
+ return f"{GEMINI_WS_URL}?key={self.api_key}"
360
+ return f"{OPENAI_WS_URL}?model={self.openai.model}"
315
361
 
316
362
  @property
317
363
  def ws_headers(self) -> dict[str, str]:
318
364
  if self.provider == "azure":
319
365
  return {"api-key": self.api_key}
366
+ if self.provider == "gemini":
367
+ return {}
320
368
  return {
321
369
  "Authorization": f"Bearer {self.api_key}",
322
370
  "OpenAI-Beta": "realtime=v1",
@@ -329,8 +377,10 @@ class RealtimeSettings:
329
377
  @property
330
378
  def provider_label(self) -> str:
331
379
  if self.provider == "azure":
332
- return f"Azure ({self.azure_deployment})"
333
- return f"OpenAI ({self.openai_model})"
380
+ return f"Azure ({self.azure.deployment})"
381
+ if self.provider == "gemini":
382
+ return f"Gemini ({self.gemini.model})"
383
+ return f"OpenAI ({self.openai.model})"
334
384
 
335
385
  def save(self) -> None:
336
386
  SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -340,13 +390,9 @@ class RealtimeSettings:
340
390
  return {
341
391
  "provider": self.provider,
342
392
  "api_key": self.api_key,
343
- "openai_model": self.openai_model,
344
- "azure_endpoint": self.azure_endpoint,
345
- "azure_deployment": self.azure_deployment,
346
- "voice": self.voice,
347
- "vad_threshold": self.vad_threshold,
348
- "silence_duration_ms": self.silence_duration_ms,
349
- "prefix_padding_ms": self.prefix_padding_ms,
393
+ "openai": asdict(self.openai),
394
+ "azure": asdict(self.azure),
395
+ "gemini": asdict(self.gemini),
350
396
  "agent_timeout": self.agent_timeout,
351
397
  "language": self.language,
352
398
  "permission_mode": self.permission_mode,
@@ -360,16 +406,15 @@ class RealtimeSettings:
360
406
  return cls()
361
407
  try:
362
408
  data = json.loads(SETTINGS_PATH.read_text())
409
+ # Migrate flat format → nested format
410
+ if "openai_model" in data or "vad_threshold" in data:
411
+ data = cls._migrate_flat(data)
363
412
  return cls(
364
413
  provider=data.get("provider", "openai"),
365
414
  api_key=data.get("api_key", ""),
366
- openai_model=data.get("openai_model", OPENAI_DEFAULT_MODEL),
367
- azure_endpoint=data.get("azure_endpoint", AZURE_DEFAULT_ENDPOINT),
368
- azure_deployment=data.get("azure_deployment", AZURE_DEFAULT_DEPLOYMENT),
369
- voice=data.get("voice", "ash"),
370
- vad_threshold=data.get("vad_threshold", 0.8),
371
- silence_duration_ms=data.get("silence_duration_ms", 700),
372
- prefix_padding_ms=data.get("prefix_padding_ms", 300),
415
+ openai=cls._load_openai(data.get("openai", {})),
416
+ azure=cls._load_azure(data.get("azure", {})),
417
+ gemini=cls._load_gemini(data.get("gemini", {})),
373
418
  agent_timeout=data.get("agent_timeout", 300),
374
419
  language=data.get("language", "en"),
375
420
  permission_mode=data.get("permission_mode", "bypass"),
@@ -379,6 +424,72 @@ class RealtimeSettings:
379
424
  except (json.JSONDecodeError, OSError):
380
425
  return cls()
381
426
 
427
+ @staticmethod
428
+ def _load_openai(d: dict) -> OpenAIConfig:
429
+ return OpenAIConfig(
430
+ model=d.get("model", OPENAI_DEFAULT_MODEL),
431
+ voice=d.get("voice", "ash"),
432
+ vad_threshold=d.get("vad_threshold", 0.8),
433
+ silence_duration_ms=d.get("silence_duration_ms", 700),
434
+ prefix_padding_ms=d.get("prefix_padding_ms", 300),
435
+ )
436
+
437
+ @staticmethod
438
+ def _load_azure(d: dict) -> AzureConfig:
439
+ return AzureConfig(
440
+ endpoint=d.get("endpoint", AZURE_DEFAULT_ENDPOINT),
441
+ deployment=d.get("deployment", AZURE_DEFAULT_DEPLOYMENT),
442
+ voice=d.get("voice", "ash"),
443
+ vad_threshold=d.get("vad_threshold", 0.8),
444
+ silence_duration_ms=d.get("silence_duration_ms", 700),
445
+ prefix_padding_ms=d.get("prefix_padding_ms", 300),
446
+ )
447
+
448
+ @staticmethod
449
+ def _load_gemini(d: dict) -> GeminiConfig:
450
+ return GeminiConfig(
451
+ model=d.get("model", GEMINI_DEFAULT_MODEL),
452
+ voice=d.get("voice", "Puck"),
453
+ start_sensitivity=d.get("start_sensitivity", "HIGH"),
454
+ end_sensitivity=d.get("end_sensitivity", "LOW"),
455
+ silence_duration_ms=d.get("silence_duration_ms", 700),
456
+ prefix_padding_ms=d.get("prefix_padding_ms", 100),
457
+ )
458
+
459
+ @staticmethod
460
+ def _migrate_flat(data: dict) -> dict:
461
+ """Migrate old flat-key format into nested provider dicts."""
462
+ voice = data.get("voice", "ash")
463
+ vad_t = data.get("vad_threshold", 0.8)
464
+ silence = data.get("silence_duration_ms", 700)
465
+ prefix = data.get("prefix_padding_ms", 300)
466
+
467
+ data["openai"] = {
468
+ "model": data.pop("openai_model", OPENAI_DEFAULT_MODEL),
469
+ "voice": voice,
470
+ "vad_threshold": vad_t,
471
+ "silence_duration_ms": silence,
472
+ "prefix_padding_ms": prefix,
473
+ }
474
+ data["azure"] = {
475
+ "endpoint": data.pop("azure_endpoint", AZURE_DEFAULT_ENDPOINT),
476
+ "deployment": data.pop("azure_deployment", AZURE_DEFAULT_DEPLOYMENT),
477
+ "voice": voice,
478
+ "vad_threshold": vad_t,
479
+ "silence_duration_ms": silence,
480
+ "prefix_padding_ms": prefix,
481
+ }
482
+ data["gemini"] = {
483
+ "model": data.pop("gemini_model", GEMINI_DEFAULT_MODEL),
484
+ "voice": voice if voice in GEMINI_VOICES else "Puck",
485
+ "start_sensitivity": "HIGH",
486
+ "end_sensitivity": "HIGH",
487
+ }
488
+ # Remove migrated flat keys
489
+ for k in ("voice", "vad_threshold", "silence_duration_ms", "prefix_padding_ms"):
490
+ data.pop(k, None)
491
+ return data
492
+
382
493
  @staticmethod
383
494
  def clear() -> None:
384
495
  try:
@@ -40,7 +40,7 @@ from voice_vibecoder.app_config import VoiceConfig
40
40
  from voice_vibecoder.config import RealtimeSettings
41
41
  from voice_vibecoder.instances import InstanceStatus
42
42
  from voice_vibecoder.session import RealtimeSession
43
- from voice_vibecoder.voice_providers.openai import OpenAIVoiceProvider
43
+ from voice_vibecoder.voice_providers import create_provider
44
44
 
45
45
  logger = logging.getLogger(__name__)
46
46
 
@@ -70,10 +70,7 @@ class WebSocketBridge:
70
70
  self._user_jwt = user_jwt
71
71
 
72
72
  async def run(self) -> None:
73
- provider = OpenAIVoiceProvider(
74
- ws_url=self._settings.ws_url,
75
- ws_headers=self._settings.ws_headers,
76
- )
73
+ provider = create_provider(self._settings)
77
74
 
78
75
  self._session = RealtimeSession(
79
76
  provider=provider,
@@ -140,7 +137,7 @@ class WebSocketBridge:
140
137
  # Text-only mode: inject user message directly
141
138
  text = msg.get("text", "") or msg.get("message", "")
142
139
  if text and self._session:
143
- self._send({"type": "transcript", "role": "user", "text": text})
140
+ # Transcript sent by session.inject_user_message
144
141
  await self._session.inject_user_message(text)
145
142
  elif msg_type == "sync_state":
146
143
  # Frontend requests full state sync from backend
@@ -30,6 +30,7 @@ from voice_vibecoder.voice_providers import (
30
30
  AudioCommitted,
31
31
  AudioDelta,
32
32
  FunctionCall,
33
+ GeminiVADSettings,
33
34
  ResponseCreated,
34
35
  ResponseDone,
35
36
  SpeechStarted,
@@ -142,17 +143,26 @@ class RealtimeSession:
142
143
  active_instances=get_active_instances_text(),
143
144
  )
144
145
 
145
- vad: VADSettings | None = None
146
+ vad: VADSettings | GeminiVADSettings | None = None
146
147
  if s.input_mode != "ptt":
147
- vad = VADSettings(
148
- threshold=s.vad_threshold,
149
- silence_duration_ms=s.silence_duration_ms,
150
- prefix_padding_ms=s.prefix_padding_ms,
151
- )
148
+ if s.provider == "gemini":
149
+ vad = GeminiVADSettings(
150
+ start_sensitivity=s.gemini.start_sensitivity,
151
+ end_sensitivity=s.gemini.end_sensitivity,
152
+ silence_duration_ms=s.gemini.silence_duration_ms,
153
+ prefix_padding_ms=s.gemini.prefix_padding_ms,
154
+ )
155
+ else:
156
+ cfg = s.openai if s.provider == "openai" else s.azure
157
+ vad = VADSettings(
158
+ threshold=cfg.vad_threshold,
159
+ silence_duration_ms=cfg.silence_duration_ms,
160
+ prefix_padding_ms=cfg.prefix_padding_ms,
161
+ )
152
162
 
153
163
  await self._provider.configure(
154
164
  instructions=instructions,
155
- voice=s.voice,
165
+ voice=s.active_voice,
156
166
  tools=get_tool_definitions(),
157
167
  vad=vad,
158
168
  )
@@ -174,7 +174,7 @@ class VoiceCodingScreen(PanelMixin, CallbackMixin, Screen):
174
174
  self._input_mode = settings.input_mode
175
175
 
176
176
  self.app.call_from_thread(
177
- self._log, f"[dim]{settings.provider_label} | voice: {settings.voice}[/dim]"
177
+ self._log, f"[dim]{settings.provider_label} | voice: {settings.active_voice}[/dim]"
178
178
  )
179
179
  self.app.call_from_thread(self._log, "[dim]Connecting...[/dim]")
180
180
  self.app.call_from_thread(self._start_session, settings)
@@ -315,14 +315,10 @@ class VoiceCodingScreen(PanelMixin, CallbackMixin, Screen):
315
315
  # -- Session lifecycle --
316
316
 
317
317
  def _start_session(self, settings) -> None:
318
- from voice_vibecoder.voice_providers.openai import OpenAIVoiceProvider
318
+ from voice_vibecoder.voice_providers import create_provider
319
319
  from voice_vibecoder.session import RealtimeSession
320
320
 
321
- provider = OpenAIVoiceProvider(
322
- ws_url=settings.ws_url,
323
- ws_headers=settings.ws_headers,
324
- transcription_model="gpt-4o-mini-transcribe",
325
- )
321
+ provider = create_provider(settings)
326
322
 
327
323
  self._session = RealtimeSession(
328
324
  provider=provider,