gemcode 0.3.100__tar.gz → 0.3.102__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 (158) hide show
  1. {gemcode-0.3.100/src/gemcode.egg-info → gemcode-0.3.102}/PKG-INFO +4 -1
  2. {gemcode-0.3.100 → gemcode-0.3.102}/pyproject.toml +2 -1
  3. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/cli.py +141 -8
  4. gemcode-0.3.102/src/gemcode/live_audio_engine.py +275 -0
  5. {gemcode-0.3.100 → gemcode-0.3.102/src/gemcode.egg-info}/PKG-INFO +4 -1
  6. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode.egg-info/requires.txt +4 -0
  7. gemcode-0.3.100/src/gemcode/live_audio_engine.py +0 -124
  8. {gemcode-0.3.100 → gemcode-0.3.102}/LICENSE +0 -0
  9. {gemcode-0.3.100 → gemcode-0.3.102}/MANIFEST.in +0 -0
  10. {gemcode-0.3.100 → gemcode-0.3.102}/README.md +0 -0
  11. {gemcode-0.3.100 → gemcode-0.3.102}/setup.cfg +0 -0
  12. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/__init__.py +0 -0
  13. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/__main__.py +0 -0
  14. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/agent.py +0 -0
  15. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/audit.py +0 -0
  16. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/autocompact.py +0 -0
  17. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/autotune.py +0 -0
  18. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/callbacks.py +0 -0
  19. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/capability_routing.py +0 -0
  20. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/checkpoints.py +0 -0
  21. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/curated_memory.py +0 -0
  29. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/dynamic_policy.py +0 -0
  30. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/evals/harness.py +0 -0
  31. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/hitl_session.py +0 -0
  32. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/hooks.py +0 -0
  33. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/ide_protocol.py +0 -0
  34. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/ide_stdio.py +0 -0
  35. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/intent_classifier.py +0 -0
  36. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/interactions.py +0 -0
  37. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/invoke.py +0 -0
  38. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/kaira_daemon.py +0 -0
  39. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/learning.py +0 -0
  40. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/limits.py +0 -0
  41. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/logging_config.py +0 -0
  42. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/mcp_loader.py +0 -0
  43. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/memory/__init__.py +0 -0
  44. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/memory/embedding_memory_service.py +0 -0
  45. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/memory/file_memory_service.py +0 -0
  46. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/modality_tools.py +0 -0
  47. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/model_errors.py +0 -0
  48. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/model_routing.py +0 -0
  49. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/multimodal_input.py +0 -0
  50. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/openapi_loader.py +0 -0
  51. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/output_styles.py +0 -0
  52. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/paths.py +0 -0
  53. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/permissions.py +0 -0
  54. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/plugins/__init__.py +0 -0
  55. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  56. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  57. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/policy_profile.py +0 -0
  58. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/pricing.py +0 -0
  59. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/prompt_suggestions.py +0 -0
  60. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/__init__.py +0 -0
  61. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/config.py +0 -0
  62. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/deps.py +0 -0
  63. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/engine.py +0 -0
  64. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/stop_hooks.py +0 -0
  65. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/token_budget.py +0 -0
  66. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query/transitions.py +0 -0
  67. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/query_sanitizer.py +0 -0
  68. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/refine.py +0 -0
  69. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/repl_commands.py +0 -0
  70. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/repl_slash.py +0 -0
  71. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/review_agent.py +0 -0
  72. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/rules.py +0 -0
  73. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/session_runtime.py +0 -0
  74. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/session_store.py +0 -0
  75. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/session_summariser.py +0 -0
  76. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/skills.py +0 -0
  77. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/slash_commands.py +0 -0
  78. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/thinking.py +0 -0
  79. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tool_prompt_manifest.py +0 -0
  80. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tool_registry.py +0 -0
  81. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tool_result_store.py +0 -0
  82. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/__init__.py +0 -0
  83. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/bash.py +0 -0
  84. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/browser.py +0 -0
  85. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/compress_memory.py +0 -0
  86. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/curated_memory.py +0 -0
  87. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/edit.py +0 -0
  88. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/filesystem.py +0 -0
  89. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/notebook.py +0 -0
  90. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/notes.py +0 -0
  91. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/repo_map.py +0 -0
  92. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/search.py +0 -0
  93. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/shell.py +0 -0
  94. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/shell_gate.py +0 -0
  95. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/skills.py +0 -0
  96. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/subtask.py +0 -0
  97. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/tasks.py +0 -0
  98. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/think.py +0 -0
  99. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/todo.py +0 -0
  100. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/veomem_tools.py +0 -0
  101. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/web.py +0 -0
  102. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools/web_search.py +0 -0
  103. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tools_inspector.py +0 -0
  104. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/trust.py +0 -0
  105. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tui/input_handler.py +0 -0
  106. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tui/scrollback.py +0 -0
  107. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tui/spinner.py +0 -0
  108. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tui/welcome_banner.py +0 -0
  109. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/tui/welcome_rich.py +0 -0
  110. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/veomem_bridge.py +0 -0
  111. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/version.py +0 -0
  112. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/vertex.py +0 -0
  113. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/wal.py +0 -0
  114. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/web/__init__.py +0 -0
  115. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/web/sse_adapter.py +0 -0
  116. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/web/terminal_repl.py +0 -0
  117. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/web/web_sse_compat.py +0 -0
  118. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode/workspace_hints.py +0 -0
  119. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode.egg-info/SOURCES.txt +0 -0
  120. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode.egg-info/dependency_links.txt +0 -0
  121. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode.egg-info/entry_points.txt +0 -0
  122. {gemcode-0.3.100 → gemcode-0.3.102}/src/gemcode.egg-info/top_level.txt +0 -0
  123. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_add_dir.py +0 -0
  124. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_agent_instruction.py +0 -0
  125. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_autocompact.py +0 -0
  126. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_capability_routing.py +0 -0
  127. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_checkpoint_diff_command.py +0 -0
  128. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_cli_init.py +0 -0
  129. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_compress_memory_tool.py +0 -0
  130. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_computer_use_permissions.py +0 -0
  131. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_context_budget.py +0 -0
  132. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_context_warning.py +0 -0
  133. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_credentials.py +0 -0
  134. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_eval_harness_layout.py +0 -0
  135. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_ide_stdio_attachments.py +0 -0
  136. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_interactive_permission_ask.py +0 -0
  137. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_kaira_scheduler.py +0 -0
  138. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_modality_tools.py +0 -0
  139. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_model_error_retry.py +0 -0
  140. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_model_errors.py +0 -0
  141. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_model_routing.py +0 -0
  142. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_multimodal_input.py +0 -0
  143. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_output_styles_and_rules.py +0 -0
  144. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_paths.py +0 -0
  145. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_permissions.py +0 -0
  146. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_prompt_suggestions.py +0 -0
  147. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_repl_commands.py +0 -0
  148. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_repl_slash.py +0 -0
  149. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_skills.py +0 -0
  150. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_slash_commands.py +0 -0
  151. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_slash_completion_registry.py +0 -0
  152. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_thinking_config.py +0 -0
  153. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_token_budget.py +0 -0
  154. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_tool_context_circulation.py +0 -0
  155. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_tools.py +0 -0
  156. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_tools_inspector.py +0 -0
  157. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_web_sse_adapter.py +0 -0
  158. {gemcode-0.3.100 → gemcode-0.3.102}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.100
3
+ Version: 0.3.102
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -185,6 +185,9 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
185
185
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
186
186
  Provides-Extra: mcp
187
187
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
188
+ Provides-Extra: live
189
+ Requires-Dist: numpy>=1.26.0; extra == "live"
190
+ Requires-Dist: sounddevice>=0.5.0; extra == "live"
188
191
  Dynamic: license-file
189
192
 
190
193
  # GemCode User Manual
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.100"
7
+ version = "0.3.102"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -42,6 +42,7 @@ Issues = "https://github.com/spiderdev27/GemCode/issues"
42
42
  [project.optional-dependencies]
43
43
  dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0"]
44
44
  mcp = ["mcp>=1.0.0"]
45
+ live = ["numpy>=1.26.0", "sounddevice>=0.5.0"]
45
46
 
46
47
  [project.scripts]
47
48
  gemcode = "gemcode.cli:main"
@@ -705,6 +705,16 @@ def main() -> None:
705
705
  action="store_true",
706
706
  help="Enable embeddings-based semantic retrieval",
707
707
  )
708
+ audio_parser.add_argument(
709
+ "--no-playback",
710
+ action="store_true",
711
+ help="Do not play model audio to speakers (still prints text if any)",
712
+ )
713
+ audio_parser.add_argument(
714
+ "--list-devices",
715
+ action="store_true",
716
+ help="List available audio devices and exit (for mic troubleshooting)",
717
+ )
708
718
 
709
719
  args = audio_parser.parse_args(sys.argv[2:])
710
720
  load_cli_environment()
@@ -725,15 +735,138 @@ def main() -> None:
725
735
  session_id = args.session or str(uuid.uuid4())
726
736
  from gemcode.live_audio_engine import run_live_audio
727
737
 
728
- asyncio.run(
729
- run_live_audio(
730
- cfg,
731
- session_id=session_id,
732
- seconds=args.seconds,
733
- input_rate=args.rate,
734
- language_code=args.language,
738
+ if args.list_devices:
739
+ try:
740
+ import sounddevice as sd # type: ignore
741
+ except Exception:
742
+ print(
743
+ "\n[gemcode live-audio] Audio deps missing. Install:\n"
744
+ " python3 -m pip install -U \"gemcode[live]\"\n",
745
+ file=sys.stderr,
746
+ )
747
+ raise SystemExit(2)
748
+ try:
749
+ devs = sd.query_devices()
750
+ default_in, default_out = sd.default.device
751
+ print("Audio devices:")
752
+ for i, d in enumerate(devs):
753
+ name = str(d.get("name") or "")
754
+ mi = int(d.get("max_input_channels") or 0)
755
+ mo = int(d.get("max_output_channels") or 0)
756
+ mark = ""
757
+ if i == default_in:
758
+ mark += " [default-in]"
759
+ if i == default_out:
760
+ mark += " [default-out]"
761
+ print(f" {i:>2}: in={mi} out={mo} {name}{mark}")
762
+ print("\nTip: set GEMCODE_LIVE_AUDIO_INPUT_DEVICE to a device index or name.")
763
+ except Exception as e:
764
+ print(f"[gemcode live-audio] Could not list devices: {e}", file=sys.stderr)
765
+ raise SystemExit(2)
766
+ raise SystemExit(0)
767
+
768
+ # One-time explicit permission prompt (HITL) for mic/speaker use.
769
+ if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
770
+ try:
771
+ ask = os.environ.get("GEMCODE_LIVE_AUDIO_ASK", "1").lower() not in ("0", "false", "no", "off")
772
+ if ask and not args.yes:
773
+ print(
774
+ "\n[gemcode live-audio] Permissions\n"
775
+ "GemCode will access your microphone"
776
+ + (" and play audio to your speakers" if not args.no_playback else "")
777
+ + ".\n"
778
+ "Allow this now? [y/N] ",
779
+ file=sys.stderr,
780
+ end="",
781
+ )
782
+ ans = input().strip().lower()
783
+ if ans not in ("y", "yes"):
784
+ raise SystemExit("live-audio cancelled by user.")
785
+ except EOFError:
786
+ raise SystemExit("live-audio cancelled (no TTY input).")
787
+
788
+ try:
789
+ # Suppress non-actionable serialization warning seen in some SDK versions.
790
+ try:
791
+ import warnings as _warnings
792
+ _warnings.filterwarnings(
793
+ "ignore",
794
+ message=r".*Pydantic serializer warnings.*",
795
+ category=UserWarning,
796
+ )
797
+ except Exception:
798
+ pass
799
+
800
+ # Some SDK builds print a close-1000 traceback directly to stderr even when it's benign.
801
+ # Capture stderr during the run and suppress that specific known noise.
802
+ _hide = os.environ.get("GEMCODE_LIVE_AUDIO_HIDE_SDK_TRACE", "1").lower() not in (
803
+ "0",
804
+ "false",
805
+ "no",
806
+ "off",
735
807
  )
736
- )
808
+ if _hide:
809
+ import io
810
+ from contextlib import redirect_stderr
811
+
812
+ buf = io.StringIO()
813
+ with redirect_stderr(buf):
814
+ asyncio.run(
815
+ run_live_audio(
816
+ cfg,
817
+ session_id=session_id,
818
+ seconds=args.seconds,
819
+ input_rate=args.rate,
820
+ language_code=args.language,
821
+ playback=(not args.no_playback),
822
+ )
823
+ )
824
+ captured = buf.getvalue()
825
+ if captured and "An unexpected error occurred in live flow: 1000" not in captured:
826
+ # Re-emit unexpected stderr.
827
+ print(captured, file=sys.stderr, end="")
828
+ else:
829
+ asyncio.run(
830
+ run_live_audio(
831
+ cfg,
832
+ session_id=session_id,
833
+ seconds=args.seconds,
834
+ input_rate=args.rate,
835
+ language_code=args.language,
836
+ playback=(not args.no_playback),
837
+ )
838
+ )
839
+ except Exception as e:
840
+ # Some SDK/ADK versions surface a normal websocket close (1000 OK) as an exception.
841
+ try:
842
+ from google.genai.errors import APIError # type: ignore
843
+ if isinstance(e, APIError) and (getattr(e, "status_code", None) == 1000 or "1000" in str(e)):
844
+ print("\n[gemcode live-audio] Session ended.", file=sys.stderr)
845
+ raise SystemExit(0)
846
+ except Exception:
847
+ pass
848
+ # websockets can also surface a close directly.
849
+ if "ConnectionClosedOK" in repr(e) or "sent 1000 (OK)" in str(e):
850
+ print("\n[gemcode live-audio] Session ended.", file=sys.stderr)
851
+ raise SystemExit(0)
852
+ raise
853
+ except RuntimeError as e:
854
+ msg = str(e or "")
855
+ if "Mic capture requires `sounddevice` and `numpy`" in msg:
856
+ print(
857
+ "\n[gemcode live-audio] Microphone capture dependencies are missing.\n\n"
858
+ "Install:\n"
859
+ " python3 -m pip install -U \"gemcode[live]\"\n\n"
860
+ "If that fails on your system, try:\n"
861
+ " python3 -m pip install -U numpy sounddevice\n\n"
862
+ "Then re-run:\n"
863
+ f" gemcode live-audio -C {cfg.project_root}\n\n"
864
+ "If the mic is still blocked, enable Microphone access for your terminal app in:\n"
865
+ " System Settings → Privacy & Security → Microphone\n",
866
+ file=sys.stderr,
867
+ )
868
+ raise SystemExit(2)
869
+ raise
737
870
  print(f"\n[gemcode live-audio] session_id={session_id}", file=sys.stderr)
738
871
  return
739
872
 
@@ -0,0 +1,275 @@
1
+ """
2
+ Live audio engine (Gemini Live API via ADK).
3
+
4
+ This wires GemCode's existing outer session + callbacks into ADK's
5
+ `Runner.run_live()` path for real-time audio input/output.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+ import time
13
+ import sys
14
+ from dataclasses import dataclass
15
+ from typing import Optional
16
+
17
+ from google.adk.agents.live_request_queue import LiveRequestQueue
18
+ from google.adk.agents.run_config import RunConfig
19
+ from google.genai import types
20
+
21
+ from gemcode.config import GemCodeConfig
22
+ from gemcode.session_runtime import create_runner
23
+
24
+
25
+ def _mime_type_for_rate(rate: int) -> str:
26
+ # ADK/examples commonly use this mime type.
27
+ return f"audio/pcm;rate={rate}"
28
+
29
+
30
+ def _require_audio_deps():
31
+ """
32
+ Import audio deps (sounddevice + numpy). Raised error is caught by CLI to show friendly instructions.
33
+ """
34
+ try:
35
+ import sounddevice as sd # type: ignore
36
+ import numpy as np # type: ignore
37
+ except ImportError as e:
38
+ raise RuntimeError(
39
+ "Mic capture requires `sounddevice` and `numpy`. Install them to use `gemcode live-audio`."
40
+ ) from e
41
+ return sd, np
42
+
43
+
44
+ def _parse_pcm_rate(mime_type: str | None) -> int | None:
45
+ mt = (mime_type or "").lower()
46
+ if "audio/pcm" not in mt:
47
+ return None
48
+ # e.g. audio/pcm;rate=24000
49
+ for part in mt.split(";"):
50
+ p = part.strip()
51
+ if p.startswith("rate="):
52
+ try:
53
+ return int(p.split("=", 1)[1])
54
+ except Exception:
55
+ return None
56
+ return None
57
+
58
+
59
+ @dataclass
60
+ class _AudioIO:
61
+ sd: object
62
+ np: object
63
+ input_rate: int
64
+ output_rate: int
65
+ playback: bool
66
+ _out_stream: object | None = None
67
+
68
+ def ensure_output(self) -> None:
69
+ if not self.playback:
70
+ return
71
+ if self._out_stream is not None:
72
+ return
73
+ # RawOutputStream writes bytes directly.
74
+ self._out_stream = self.sd.RawOutputStream( # type: ignore[attr-defined]
75
+ samplerate=int(self.output_rate),
76
+ channels=1,
77
+ dtype="int16",
78
+ )
79
+ self._out_stream.start()
80
+
81
+ def write_audio(self, pcm_bytes: bytes) -> None:
82
+ if not self.playback:
83
+ return
84
+ self.ensure_output()
85
+ try:
86
+ self._out_stream.write(pcm_bytes) # type: ignore[union-attr]
87
+ except Exception:
88
+ pass
89
+
90
+ def close(self) -> None:
91
+ try:
92
+ if self._out_stream is not None:
93
+ self._out_stream.stop()
94
+ self._out_stream.close()
95
+ except Exception:
96
+ pass
97
+ self._out_stream = None
98
+
99
+
100
+ async def run_live_audio(
101
+ cfg: GemCodeConfig,
102
+ *,
103
+ session_id: str,
104
+ user_id: str = "local",
105
+ seconds: int = 10,
106
+ input_rate: int = 24_000,
107
+ language_code: Optional[str] = None,
108
+ playback: bool = True,
109
+ ) -> None:
110
+ """
111
+ Realtime microphone streaming to Gemini Live + realtime model audio playback.
112
+
113
+ Behavior:
114
+ - streams mic audio in small PCM chunks for up to `seconds`
115
+ - prints model-authored text parts (if any)
116
+ - plays model audio parts live when `playback=True`
117
+ """
118
+
119
+ sd, np = _require_audio_deps()
120
+
121
+ runner = create_runner(cfg)
122
+ live_queue = LiveRequestQueue()
123
+
124
+ speech_config = None
125
+ if language_code:
126
+ speech_config = types.SpeechConfig(language_code=language_code)
127
+
128
+ run_config = RunConfig(
129
+ # Prefer the enum value (avoids pydantic serializer warnings in some SDK versions).
130
+ # Request TEXT too so users see transcripts even when the model returns only audio.
131
+ response_modalities=[types.Modality.AUDIO, types.Modality.TEXT],
132
+ speech_config=speech_config,
133
+ # Keep SDK defaults for STT/TTS transcription configs.
134
+ )
135
+
136
+ agen = runner.run_live(
137
+ user_id=user_id,
138
+ session_id=session_id,
139
+ live_request_queue=live_queue,
140
+ run_config=run_config,
141
+ )
142
+
143
+ printed_any = False
144
+ audio_io = _AudioIO(sd=sd, np=np, input_rate=input_rate, output_rate=input_rate, playback=playback)
145
+
146
+ async def _consume_events() -> None:
147
+ nonlocal printed_any
148
+ try:
149
+ async for event in agen:
150
+ if not event.content or not event.content.parts:
151
+ continue
152
+ for part in event.content.parts:
153
+ part_text = getattr(part, "text", None)
154
+ # We only print model-authored text to avoid echoing user input.
155
+ if part_text and getattr(event, "author", None) != "user":
156
+ sys.stdout.write(part_text)
157
+ sys.stdout.flush()
158
+ printed_any = True
159
+ # Play audio responses when present.
160
+ inline = getattr(part, "inline_data", None)
161
+ if inline is not None and getattr(event, "author", None) != "user":
162
+ try:
163
+ mime = getattr(inline, "mime_type", None)
164
+ data = getattr(inline, "data", None)
165
+ if isinstance(data, (bytes, bytearray)) and _parse_pcm_rate(mime) is not None:
166
+ r = _parse_pcm_rate(mime) or input_rate
167
+ audio_io.output_rate = int(r)
168
+ audio_io.write_audio(bytes(data))
169
+ except Exception:
170
+ pass
171
+ except Exception as e:
172
+ # Some SDK/ADK versions surface a normal websocket close (1000 OK) as an exception.
173
+ # Treat it as a clean end-of-session (no error).
174
+ try:
175
+ from google.genai.errors import APIError # type: ignore
176
+ if isinstance(e, APIError) and (
177
+ getattr(e, "status_code", None) == 1000 or "1000" in str(e)
178
+ ):
179
+ return
180
+ except Exception:
181
+ pass
182
+ if "sent 1000 (OK)" in str(e) or "ConnectionClosedOK" in repr(e) or "1000 None" in str(e):
183
+ return
184
+ # Runner/live failures are expected to be surfaced as terminal errors
185
+ # in session state + audit logs; don't crash the CLI.
186
+ raise
187
+
188
+ consumer_task = asyncio.create_task(_consume_events())
189
+
190
+ # Mic capture → async queue (threaded producer).
191
+ pcm_q: asyncio.Queue[bytes] = asyncio.Queue(maxsize=50)
192
+ stop_at = time.time() + max(1, int(seconds))
193
+ chunks_sent = 0
194
+ non_silent_chunks = 0
195
+
196
+ def _mic_thread() -> None:
197
+ # ~20ms frames is a good latency/overhead balance.
198
+ blocksize = max(120, int(input_rate // 50))
199
+ device = os.environ.get("GEMCODE_LIVE_AUDIO_INPUT_DEVICE")
200
+ try:
201
+ stream = sd.RawInputStream( # type: ignore[attr-defined]
202
+ samplerate=int(input_rate),
203
+ channels=1,
204
+ dtype="int16",
205
+ blocksize=int(blocksize),
206
+ device=device if device else None,
207
+ )
208
+ except Exception:
209
+ # Let the consumer surface this as empty audio.
210
+ return
211
+ with stream:
212
+ while time.time() < stop_at:
213
+ try:
214
+ data, _overflow = stream.read(blocksize)
215
+ if not data:
216
+ continue
217
+ # Push into asyncio queue safely.
218
+ try:
219
+ asyncio.get_running_loop().call_soon_threadsafe(pcm_q.put_nowait, bytes(data))
220
+ except Exception:
221
+ # If the loop isn't available, just drop.
222
+ pass
223
+ except Exception:
224
+ break
225
+
226
+ # Send "user started speaking" signal and start streaming.
227
+ live_queue.send_activity_start()
228
+ mic_task = asyncio.create_task(asyncio.to_thread(_mic_thread))
229
+
230
+ try:
231
+ while time.time() < stop_at:
232
+ try:
233
+ chunk = await asyncio.wait_for(pcm_q.get(), timeout=0.25)
234
+ except asyncio.TimeoutError:
235
+ continue
236
+ chunks_sent += 1
237
+ try:
238
+ arr = np.frombuffer(chunk, dtype="int16") # type: ignore[attr-defined]
239
+ # Consider it non-silent if mean abs amplitude crosses a tiny threshold.
240
+ if arr.size and float(np.mean(np.abs(arr))) > 25.0: # type: ignore[attr-defined]
241
+ non_silent_chunks += 1
242
+ except Exception:
243
+ pass
244
+ live_queue.send_realtime(
245
+ types.Blob(data=chunk, mime_type=_mime_type_for_rate(input_rate))
246
+ )
247
+ finally:
248
+ # End speech activity and close the queue regardless of failures.
249
+ live_queue.send_activity_end()
250
+ live_queue.close()
251
+ try:
252
+ await mic_task
253
+ except Exception:
254
+ pass
255
+
256
+ # Wait for event stream to drain.
257
+ try:
258
+ await consumer_task
259
+ finally:
260
+ audio_io.close()
261
+
262
+ if not printed_any:
263
+ print("\n[gemcode live-audio] No model text received (audio may have been silent).")
264
+ if chunks_sent <= 2 or non_silent_chunks == 0:
265
+ print(
266
+ "\n[gemcode live-audio] Mic input looks silent or unavailable.\n"
267
+ "Check:\n"
268
+ "- System Settings → Privacy & Security → Microphone (allow your terminal)\n"
269
+ "- Your input device selection\n"
270
+ "Tip: set GEMCODE_LIVE_AUDIO_INPUT_DEVICE to a device index/name from sounddevice.query_devices().\n",
271
+ file=sys.stderr,
272
+ )
273
+
274
+ await runner.close()
275
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.100
3
+ Version: 0.3.102
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -185,6 +185,9 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
185
185
  Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
186
186
  Provides-Extra: mcp
187
187
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
188
+ Provides-Extra: live
189
+ Requires-Dist: numpy>=1.26.0; extra == "live"
190
+ Requires-Dist: sounddevice>=0.5.0; extra == "live"
188
191
  Dynamic: license-file
189
192
 
190
193
  # GemCode User Manual
@@ -8,5 +8,9 @@ prompt_toolkit>=3.0.0
8
8
  pytest>=8.0.0
9
9
  pytest-asyncio>=0.24.0
10
10
 
11
+ [live]
12
+ numpy>=1.26.0
13
+ sounddevice>=0.5.0
14
+
11
15
  [mcp]
12
16
  mcp>=1.0.0
@@ -1,124 +0,0 @@
1
- """
2
- Live audio engine (Gemini Live API via ADK).
3
-
4
- This wires GemCode's existing outer session + callbacks into ADK's
5
- `Runner.run_live()` path for real-time audio input/output.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import asyncio
11
- import sys
12
- from typing import Optional
13
-
14
- from google.adk.agents.live_request_queue import LiveRequestQueue
15
- from google.adk.agents.run_config import RunConfig
16
- from google.genai import types
17
-
18
- from gemcode.config import GemCodeConfig
19
- from gemcode.session_runtime import create_runner
20
-
21
-
22
- def _mime_type_for_rate(rate: int) -> str:
23
- # ADK/examples commonly use this mime type.
24
- return f"audio/pcm;rate={rate}"
25
-
26
-
27
- def _record_mic_pcm_blocking(*, rate: int, seconds: int) -> bytes:
28
- try:
29
- import sounddevice as sd
30
- import numpy as np
31
- except ImportError as e:
32
- raise RuntimeError(
33
- "Mic capture requires `sounddevice` and `numpy`. Install them to use `gemcode live-audio`."
34
- ) from e
35
-
36
- frames = int(rate * seconds)
37
- # mono int16
38
- audio = sd.rec(frames, samplerate=rate, channels=1, dtype="int16")
39
- sd.wait()
40
- pcm = np.asarray(audio).astype("int16", copy=False)
41
- return pcm.tobytes()
42
-
43
-
44
- async def run_live_audio(
45
- cfg: GemCodeConfig,
46
- *,
47
- session_id: str,
48
- user_id: str = "local",
49
- seconds: int = 10,
50
- input_rate: int = 24_000,
51
- language_code: Optional[str] = None,
52
- ) -> None:
53
- """
54
- Record microphone audio for `seconds` and send it to Gemini Live.
55
-
56
- MVP behavior:
57
- - sends the entire recorded buffer as a single audio blob
58
- - prints any model text parts it returns (typically transcriptions)
59
- """
60
-
61
- runner = create_runner(cfg)
62
- live_queue = LiveRequestQueue()
63
-
64
- speech_config = None
65
- if language_code:
66
- speech_config = types.SpeechConfig(language_code=language_code)
67
-
68
- run_config = RunConfig(
69
- response_modalities=["AUDIO"],
70
- speech_config=speech_config,
71
- # Keep SDK defaults for STT/TTS transcription configs.
72
- )
73
-
74
- agen = runner.run_live(
75
- user_id=user_id,
76
- session_id=session_id,
77
- live_request_queue=live_queue,
78
- run_config=run_config,
79
- )
80
-
81
- printed_any = False
82
-
83
- async def _consume_events() -> None:
84
- nonlocal printed_any
85
- try:
86
- async for event in agen:
87
- if not event.content or not event.content.parts:
88
- continue
89
- for part in event.content.parts:
90
- part_text = getattr(part, "text", None)
91
- # We only print model-authored text to avoid echoing user input.
92
- if part_text and getattr(event, "author", None) != "user":
93
- sys.stdout.write(part_text)
94
- sys.stdout.flush()
95
- printed_any = True
96
- except Exception:
97
- # Runner/live failures are expected to be surfaced as terminal errors
98
- # in session state + audit logs; don't crash the CLI.
99
- raise
100
-
101
- consumer_task = asyncio.create_task(_consume_events())
102
-
103
- # Send "user started speaking" signal.
104
- live_queue.send_activity_start()
105
-
106
- pcm_bytes = await asyncio.to_thread(
107
- _record_mic_pcm_blocking, rate=input_rate, seconds=seconds
108
- )
109
- live_queue.send_realtime(
110
- types.Blob(data=pcm_bytes, mime_type=_mime_type_for_rate(input_rate))
111
- )
112
-
113
- # Send "user finished speaking" signal and close the queue.
114
- live_queue.send_activity_end()
115
- live_queue.close()
116
-
117
- # Wait for event stream to drain.
118
- await consumer_task
119
-
120
- if not printed_any:
121
- print("\n[gemcode live-audio] No model text received (audio may have been silent).")
122
-
123
- await runner.close()
124
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes