voice-mode 2.22.3__tar.gz → 2.23.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 (108) hide show
  1. {voice_mode-2.22.3 → voice_mode-2.23.0}/CHANGELOG.md +20 -0
  2. {voice_mode-2.22.3 → voice_mode-2.23.0}/PKG-INFO +1 -1
  3. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/__version__.py +1 -1
  4. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/config.py +3 -0
  5. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/converse.py +64 -19
  6. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/service.py +11 -5
  7. voice_mode-2.23.0/voice_mode/utils/services/common.py +80 -0
  8. voice_mode-2.22.3/voice_mode/utils/services/common.py +0 -22
  9. {voice_mode-2.22.3 → voice_mode-2.23.0}/.gitignore +0 -0
  10. {voice_mode-2.22.3 → voice_mode-2.23.0}/README.md +0 -0
  11. {voice_mode-2.22.3 → voice_mode-2.23.0}/build_hooks.py +0 -0
  12. {voice_mode-2.22.3 → voice_mode-2.23.0}/pyproject.toml +0 -0
  13. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/__init__.py +0 -0
  14. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/__main__.py +0 -0
  15. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/cli.py +0 -0
  16. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/cli_commands/__init__.py +0 -0
  17. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/cli_commands/exchanges.py +0 -0
  18. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/conversation_logger.py +0 -0
  19. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/core.py +0 -0
  20. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/data/versions.json +0 -0
  21. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/__init__.py +0 -0
  22. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/conversations.py +0 -0
  23. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/filters.py +0 -0
  24. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/formatters.py +0 -0
  25. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/models.py +0 -0
  26. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/reader.py +0 -0
  27. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/exchanges/stats.py +0 -0
  28. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/README.md +0 -0
  29. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/app/api/connection-details/route.ts +0 -0
  30. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/app/favicon.ico +0 -0
  31. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/app/globals.css +0 -0
  32. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/app/layout.tsx +0 -0
  33. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/app/page.tsx +0 -0
  34. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/components/CloseIcon.tsx +0 -0
  35. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/components/NoAgentNotification.tsx +0 -0
  36. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/components/TranscriptionView.tsx +0 -0
  37. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/hooks/useCombinedTranscriptions.ts +0 -0
  38. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/hooks/useLocalMicTrack.ts +0 -0
  39. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/next-env.d.ts +0 -0
  40. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/next.config.mjs +0 -0
  41. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/package-lock.json +0 -0
  42. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/package.json +0 -0
  43. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/pnpm-lock.yaml +0 -0
  44. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/postcss.config.mjs +0 -0
  45. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/tailwind.config.ts +0 -0
  46. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/frontend/tsconfig.json +0 -0
  47. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/prompts/README.md +0 -0
  48. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/prompts/__init__.py +0 -0
  49. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/prompts/converse.py +0 -0
  50. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/prompts/release_notes.py +0 -0
  51. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/prompts/services.py +0 -0
  52. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/provider_discovery.py +0 -0
  53. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/providers.py +0 -0
  54. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/__init__.py +0 -0
  55. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/audio_files.py +0 -0
  56. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/changelog.py +0 -0
  57. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/configuration.py +0 -0
  58. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/statistics.py +0 -0
  59. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/version.py +0 -0
  60. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/resources/whisper_models.py +0 -0
  61. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/server.py +0 -0
  62. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/shared.py +0 -0
  63. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/simple_failover.py +0 -0
  64. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/statistics.py +0 -0
  65. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/streaming.py +0 -0
  66. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/launchd/com.voicemode.frontend.plist +0 -0
  67. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/launchd/com.voicemode.kokoro.plist +0 -0
  68. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/launchd/com.voicemode.livekit.plist +0 -0
  69. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/launchd/com.voicemode.whisper.plist +0 -0
  70. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/launchd/start-kokoro-with-health-check.sh +0 -0
  71. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/launchd/start-whisper-with-health-check.sh +0 -0
  72. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/systemd/voicemode-frontend.service +0 -0
  73. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/systemd/voicemode-kokoro.service +0 -0
  74. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/systemd/voicemode-livekit.service +0 -0
  75. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/templates/systemd/voicemode-whisper.service +0 -0
  76. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/__init__.py +0 -0
  77. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/configuration_management.py +0 -0
  78. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/dependencies.py +0 -0
  79. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/devices.py +0 -0
  80. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/diagnostics.py +0 -0
  81. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/providers.py +0 -0
  82. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/kokoro/install.py +0 -0
  83. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/kokoro/uninstall.py +0 -0
  84. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/list_versions.py +0 -0
  85. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/livekit/__init__.py +0 -0
  86. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/livekit/frontend.py +0 -0
  87. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/livekit/install.py +0 -0
  88. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/livekit/production_server.py +0 -0
  89. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/livekit/uninstall.py +0 -0
  90. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/version_info.py +0 -0
  91. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/whisper/download_model.py +0 -0
  92. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/whisper/install.py +0 -0
  93. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/services/whisper/uninstall.py +0 -0
  94. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/statistics.py +0 -0
  95. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/tools/voice_registry.py +0 -0
  96. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/__init__.py +0 -0
  97. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/audio_diagnostics.py +0 -0
  98. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/event_logger.py +0 -0
  99. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/ffmpeg_check.py +0 -0
  100. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/format_migration.py +0 -0
  101. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/gpu_detection.py +0 -0
  102. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/migration_helpers.py +0 -0
  103. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/services/kokoro_helpers.py +0 -0
  104. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/services/livekit_helpers.py +0 -0
  105. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/services/whisper_helpers.py +0 -0
  106. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/utils/version_helpers.py +0 -0
  107. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/version.py +0 -0
  108. {voice_mode-2.22.3 → voice_mode-2.23.0}/voice_mode/voice_preferences.py +0 -0
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.23.0] - 2025-08-16
11
+
12
+ ### Added
13
+ - **`skip_tts` parameter** - Dynamic control over text-to-speech in converse tool
14
+ - Add optional `skip_tts` parameter to override global `VOICEMODE_SKIP_TTS` setting
15
+ - When `True`: Skip TTS for faster text-only responses
16
+ - When `False`: Always use TTS regardless of environment setting
17
+ - When `None` (default): Follow `VOICEMODE_SKIP_TTS` environment variable
18
+ - Enables LLM to intelligently choose between voice and text-only responses
19
+ - **`VOICEMODE_SKIP_TTS` environment variable** - Global TTS skip configuration
20
+ - Set to `true` for permanent text-only mode (faster responses)
21
+ - Can be overridden per-call with `skip_tts` parameter
22
+ - Useful for rapid development iterations or when voice isn't needed
23
+
24
+ ### Fixed
25
+ - **Service status detection** - Correctly identify SSH-forwarded vs locally running services
26
+ - SSH processes listening on service ports are now recognized as port forwards
27
+ - Status command now shows 🔄 for forwarded services vs ✅ for local services
28
+ - Prevents confusion about where services are actually running
29
+
10
30
  ## [2.22.3] - 2025-08-16
11
31
 
12
32
  ## [2.23.0] - 2025-08-16
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-mode
3
- Version: 2.22.3
3
+ Version: 2.23.0
4
4
  Summary: VoiceMode - Voice interaction capabilities for AI assistants (formerly voice-mcp)
5
5
  Project-URL: Homepage, https://github.com/mbailey/voicemode
6
6
  Project-URL: Repository, https://github.com/mbailey/voicemode
@@ -1,3 +1,3 @@
1
1
  # This file is automatically updated by 'make release'
2
2
  # Do not edit manually
3
- __version__ = "2.22.3"
3
+ __version__ = "2.23.0"
@@ -175,6 +175,9 @@ SAVE_TRANSCRIPTIONS = SAVE_ALL or DEBUG or os.getenv("VOICEMODE_SAVE_TRANSCRIPTI
175
175
  # Audio feedback configuration
176
176
  AUDIO_FEEDBACK_ENABLED = os.getenv("VOICEMODE_AUDIO_FEEDBACK", "true").lower() in ("true", "1", "yes", "on")
177
177
 
178
+ # Skip TTS configuration (skip text-to-speech for faster responses)
179
+ SKIP_TTS = os.getenv("VOICEMODE_SKIP_TTS", "false").lower() in ("true", "1", "yes", "on")
180
+
178
181
  # Local provider preference configuration
179
182
  PREFER_LOCAL = os.getenv("VOICEMODE_PREFER_LOCAL", "true").lower() in ("true", "1", "yes", "on")
180
183
 
@@ -48,6 +48,7 @@ from voice_mode.config import (
48
48
  VAD_AGGRESSIVENESS,
49
49
  SILENCE_THRESHOLD_MS,
50
50
  MIN_RECORDING_DURATION,
51
+ SKIP_TTS,
51
52
  VAD_CHUNK_DURATION_MS,
52
53
  INITIAL_SILENCE_GRACE_PERIOD,
53
54
  DEFAULT_LISTEN_DURATION,
@@ -1248,7 +1249,8 @@ async def converse(
1248
1249
  audio_format: Optional[str] = None,
1249
1250
  disable_silence_detection: Union[bool, str] = False,
1250
1251
  speed: Optional[float] = None,
1251
- vad_aggressiveness: Optional[int] = None
1252
+ vad_aggressiveness: Optional[int] = None,
1253
+ skip_tts: Optional[Union[bool, str]] = None
1252
1254
  ) -> str:
1253
1255
  """Have a voice conversation - speak a message and optionally listen for response.
1254
1256
 
@@ -1320,6 +1322,11 @@ async def converse(
1320
1322
 
1321
1323
  Use lower values (0-1) in quiet environments to catch all speech
1322
1324
  Use higher values (2-3) in noisy environments to reduce false triggers
1325
+ skip_tts: Skip text-to-speech and only show text (default: None uses VOICEMODE_SKIP_TTS env var)
1326
+ When True: Skip TTS for faster response, text-only output
1327
+ When False: Always use TTS regardless of environment setting
1328
+ When None: Follow VOICEMODE_SKIP_TTS environment variable
1329
+ Useful for rapid development iterations or when voice isn't needed
1323
1330
  If wait_for_response is False: Confirmation that message was spoken
1324
1331
  If wait_for_response is True: The voice response received (or error/timeout message)
1325
1332
 
@@ -1360,6 +1367,12 @@ async def converse(
1360
1367
 
1361
1368
  Remember: Lower values (0-1) = more permissive, may detect non-speech as speech
1362
1369
  Higher values (2-3) = more strict, may miss soft speech or whispers
1370
+
1371
+ Skip TTS Examples:
1372
+ - Fast iteration mode: converse("Processing your request", skip_tts=True) # Text only, no voice
1373
+ - Important announcement: converse("Warning: System will restart", skip_tts=False) # Always use voice
1374
+ - Quick confirmation: converse("Done!", skip_tts=True, wait_for_response=False) # Fast text-only
1375
+ - Follow user preference: converse("Hello") # Uses VOICEMODE_SKIP_TTS setting
1363
1376
  """
1364
1377
  # Convert string booleans to actual booleans
1365
1378
  if isinstance(wait_for_response, str):
@@ -1368,6 +1381,16 @@ async def converse(
1368
1381
  disable_silence_detection = disable_silence_detection.lower() in ('true', '1', 'yes', 'on')
1369
1382
  if isinstance(audio_feedback, str):
1370
1383
  audio_feedback = audio_feedback.lower() in ('true', '1', 'yes', 'on')
1384
+ if skip_tts is not None and isinstance(skip_tts, str):
1385
+ skip_tts = skip_tts.lower() in ('true', '1', 'yes', 'on')
1386
+
1387
+ # Determine whether to skip TTS
1388
+ if skip_tts is not None:
1389
+ # Parameter explicitly set, use it
1390
+ should_skip_tts = skip_tts
1391
+ else:
1392
+ # Use global setting
1393
+ should_skip_tts = SKIP_TTS
1371
1394
 
1372
1395
  # Convert string speed to float
1373
1396
  if speed is not None and isinstance(speed, str):
@@ -1457,15 +1480,26 @@ async def converse(
1457
1480
  if not wait_for_response:
1458
1481
  try:
1459
1482
  async with audio_operation_lock:
1460
- success, tts_metrics, tts_config = await text_to_speech_with_failover(
1461
- message=message,
1462
- voice=voice,
1463
- model=tts_model,
1464
- instructions=tts_instructions,
1465
- audio_format=audio_format,
1466
- initial_provider=tts_provider,
1467
- speed=speed
1468
- )
1483
+ if should_skip_tts:
1484
+ # Skip TTS entirely
1485
+ success = True
1486
+ tts_metrics = {
1487
+ 'ttfa': 0,
1488
+ 'generation': 0,
1489
+ 'playback': 0,
1490
+ 'total': 0
1491
+ }
1492
+ tts_config = {'provider': 'no-op', 'voice': 'none'}
1493
+ else:
1494
+ success, tts_metrics, tts_config = await text_to_speech_with_failover(
1495
+ message=message,
1496
+ voice=voice,
1497
+ model=tts_model,
1498
+ instructions=tts_instructions,
1499
+ audio_format=audio_format,
1500
+ initial_provider=tts_provider,
1501
+ speed=speed
1502
+ )
1469
1503
 
1470
1504
  # Include timing info if available
1471
1505
  timing_info = ""
@@ -1589,15 +1623,26 @@ async def converse(
1589
1623
  async with audio_operation_lock:
1590
1624
  # Speak the message
1591
1625
  tts_start = time.perf_counter()
1592
- tts_success, tts_metrics, tts_config = await text_to_speech_with_failover(
1593
- message=message,
1594
- voice=voice,
1595
- model=tts_model,
1596
- instructions=tts_instructions,
1597
- audio_format=audio_format,
1598
- initial_provider=tts_provider,
1599
- speed=speed
1600
- )
1626
+ if should_skip_tts:
1627
+ # Skip TTS entirely for faster response
1628
+ tts_success = True
1629
+ tts_metrics = {
1630
+ 'ttfa': 0,
1631
+ 'generation': 0,
1632
+ 'playback': 0,
1633
+ 'total': 0
1634
+ }
1635
+ tts_config = {'provider': 'no-op', 'voice': 'none'}
1636
+ else:
1637
+ tts_success, tts_metrics, tts_config = await text_to_speech_with_failover(
1638
+ message=message,
1639
+ voice=voice,
1640
+ model=tts_model,
1641
+ instructions=tts_instructions,
1642
+ audio_format=audio_format,
1643
+ initial_provider=tts_provider,
1644
+ speed=speed
1645
+ )
1601
1646
 
1602
1647
  # Add TTS sub-metrics
1603
1648
  if tts_metrics:
@@ -14,7 +14,7 @@ import psutil
14
14
 
15
15
  from voice_mode.server import mcp
16
16
  from voice_mode.config import WHISPER_PORT, KOKORO_PORT, LIVEKIT_PORT, SERVICE_AUTO_ENABLE
17
- from voice_mode.utils.services.common import find_process_by_port
17
+ from voice_mode.utils.services.common import find_process_by_port, check_service_status
18
18
  from voice_mode.utils.services.whisper_helpers import find_whisper_server, find_whisper_model
19
19
  from voice_mode.utils.services.kokoro_helpers import find_kokoro_fastapi, has_gpu_support
20
20
 
@@ -195,10 +195,16 @@ async def status_service(service_name: str) -> str:
195
195
  port = LIVEKIT_PORT
196
196
  else: # frontend
197
197
  port = 3000
198
- proc = find_process_by_port(port)
199
198
 
200
- if not proc:
201
- return f"{service_name.capitalize()} is not running on port {port}"
199
+ status, proc = check_service_status(port)
200
+
201
+ if status == "not_available":
202
+ return f"❌ {service_name.capitalize()} is not available"
203
+ elif status == "forwarded":
204
+ return f"""🔄 {service_name.capitalize()} is available via port forwarding
205
+ Port: {port} (forwarded)
206
+ Local process: Not running
207
+ Remote: Accessible"""
202
208
 
203
209
  try:
204
210
  with proc.oneshot():
@@ -269,7 +275,7 @@ async def status_service(service_name: str) -> str:
269
275
  if extra_info_parts:
270
276
  extra_info = "\n " + "\n ".join(extra_info_parts)
271
277
 
272
- return f"""✅ {service_name.capitalize()} is running
278
+ return f"""✅ {service_name.capitalize()} is running locally
273
279
  PID: {proc.pid}
274
280
  Port: {port}
275
281
  CPU: {cpu_percent:.1f}%
@@ -0,0 +1,80 @@
1
+ """Common utilities for service management tools."""
2
+
3
+ import psutil
4
+ import socket
5
+ from typing import Optional, Tuple
6
+ import logging
7
+
8
+ logger = logging.getLogger("voice-mode")
9
+
10
+
11
+ def find_process_by_port(port: int) -> Optional[psutil.Process]:
12
+ """Find a process listening on the specified port.
13
+
14
+ Returns None if port is only accessible via SSH forwarding or other non-local means.
15
+ """
16
+ try:
17
+ for proc in psutil.process_iter(['pid', 'name']):
18
+ try:
19
+ # Skip if we can't access process info (might be another user's process)
20
+ if not proc.is_running():
21
+ continue
22
+
23
+ # Skip SSH processes - these are port forwards, not actual services
24
+ proc_name = proc.name().lower()
25
+ if proc_name in ['ssh', 'sshd']:
26
+ continue
27
+
28
+ for conn in proc.connections():
29
+ if conn.laddr.port == port and conn.status == 'LISTEN':
30
+ # Verify this is a real local process
31
+ try:
32
+ # Try to access basic process info to ensure it's real
33
+ _ = proc.pid
34
+ _ = proc.create_time()
35
+ return proc
36
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
37
+ # Process doesn't actually exist or we can't access it
38
+ continue
39
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
40
+ continue
41
+ except Exception as e:
42
+ logger.error(f"Error finding process by port: {e}")
43
+ return None
44
+
45
+
46
+ def is_port_accessible(port: int, host: str = "127.0.0.1", timeout: float = 1.0) -> bool:
47
+ """Check if a port is accessible (can connect to it).
48
+
49
+ This will return True for both locally running services and SSH-forwarded ports.
50
+ """
51
+ try:
52
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
53
+ sock.settimeout(timeout)
54
+ result = sock.connect_ex((host, port))
55
+ return result == 0
56
+ except Exception as e:
57
+ logger.error(f"Error checking port accessibility: {e}")
58
+ return False
59
+
60
+
61
+ def check_service_status(port: int) -> Tuple[str, Optional[psutil.Process]]:
62
+ """Check the status of a service on a given port.
63
+
64
+ Returns:
65
+ Tuple of (status, process):
66
+ - ("local", process) if running locally
67
+ - ("forwarded", None) if accessible but not local
68
+ - ("not_available", None) if not accessible at all
69
+ """
70
+ # First check if there's a local process
71
+ proc = find_process_by_port(port)
72
+ if proc:
73
+ return ("local", proc)
74
+
75
+ # No local process, check if port is accessible (might be forwarded)
76
+ if is_port_accessible(port):
77
+ return ("forwarded", None)
78
+
79
+ # Not accessible at all
80
+ return ("not_available", None)
@@ -1,22 +0,0 @@
1
- """Common utilities for service management tools."""
2
-
3
- import psutil
4
- from typing import Optional
5
- import logging
6
-
7
- logger = logging.getLogger("voice-mode")
8
-
9
-
10
- def find_process_by_port(port: int) -> Optional[psutil.Process]:
11
- """Find a process listening on the specified port."""
12
- try:
13
- for proc in psutil.process_iter(['pid', 'name']):
14
- try:
15
- for conn in proc.connections():
16
- if conn.laddr.port == port and conn.status == 'LISTEN':
17
- return proc
18
- except (psutil.NoSuchProcess, psutil.AccessDenied):
19
- continue
20
- except Exception as e:
21
- logger.error(f"Error finding process by port: {e}")
22
- return None
File without changes
File without changes
File without changes
File without changes