legionforge-convobox 0.4.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.
- legionforge_convobox-0.4.0/.gitattributes +15 -0
- legionforge_convobox-0.4.0/.githooks/pre-push +29 -0
- legionforge_convobox-0.4.0/.github/CODEOWNERS +6 -0
- legionforge_convobox-0.4.0/.github/dependabot.yml +15 -0
- legionforge_convobox-0.4.0/.github/pull_request_template.md +35 -0
- legionforge_convobox-0.4.0/.github/workflows/ai-attribution.yml +89 -0
- legionforge_convobox-0.4.0/.github/workflows/ci.yml +81 -0
- legionforge_convobox-0.4.0/.github/workflows/publish.yml +81 -0
- legionforge_convobox-0.4.0/.gitignore +72 -0
- legionforge_convobox-0.4.0/.gitmessage.txt +14 -0
- legionforge_convobox-0.4.0/.tours/01-architecture-and-data-flow.tour +139 -0
- legionforge_convobox-0.4.0/.tours/02-review-findings-security-performance.tour +69 -0
- legionforge_convobox-0.4.0/.tours/03-extension-points-modularity.tour +75 -0
- legionforge_convobox-0.4.0/.vscode/extensions.json +5 -0
- legionforge_convobox-0.4.0/AGENTS.md +140 -0
- legionforge_convobox-0.4.0/CHANGELOG.md +761 -0
- legionforge_convobox-0.4.0/CLAUDE.md +17 -0
- legionforge_convobox-0.4.0/CONTRIBUTING.md +66 -0
- legionforge_convobox-0.4.0/CREDITS.md +96 -0
- legionforge_convobox-0.4.0/DEPENDENCY_LICENSE_AUDIT.md +171 -0
- legionforge_convobox-0.4.0/LICENSE +21 -0
- legionforge_convobox-0.4.0/OPENCODE_API_NOTES.md +347 -0
- legionforge_convobox-0.4.0/PKG-INFO +360 -0
- legionforge_convobox-0.4.0/README.md +304 -0
- legionforge_convobox-0.4.0/TESTING.md +538 -0
- legionforge_convobox-0.4.0/UAT-WORKSPACE-NOTES.md +24 -0
- legionforge_convobox-0.4.0/convobox.example.yaml +233 -0
- legionforge_convobox-0.4.0/docs/ADVANCED-SETTINGS-SCOPE.md +182 -0
- legionforge_convobox-0.4.0/docs/AI-ATTRIBUTION.md +133 -0
- legionforge_convobox-0.4.0/docs/ARCHITECTURE.md +176 -0
- legionforge_convobox-0.4.0/docs/ARTIFACT-PANE-SCOPE.md +607 -0
- legionforge_convobox-0.4.0/docs/CONVERSATION-DESIGN-REFERENCES.md +698 -0
- legionforge_convobox-0.4.0/docs/DESIGN-0.3.0-interaction-and-safety.md +394 -0
- legionforge_convobox-0.4.0/docs/DESIGN-backend-questions.md +85 -0
- legionforge_convobox-0.4.0/docs/DESIGN-backend-sandboxing.md +177 -0
- legionforge_convobox-0.4.0/docs/DESIGN-barge-in.md +347 -0
- legionforge_convobox-0.4.0/docs/DESIGN-echo-and-barge-in.md +360 -0
- legionforge_convobox-0.4.0/docs/DESIGN-guided-setup-for-non-technical-users.md +152 -0
- legionforge_convobox-0.4.0/docs/DESIGN-mic-mute-vs-pause-listening.md +166 -0
- legionforge_convobox-0.4.0/docs/DESIGN-opencode-artifact-pane-wiring.md +224 -0
- legionforge_convobox-0.4.0/docs/KNOWN-ISSUES.md +2224 -0
- legionforge_convobox-0.4.0/docs/LESSONS-FROM-VOICE-OPENCODE.md +42 -0
- legionforge_convobox-0.4.0/docs/PERMISSION-MODEL.md +157 -0
- legionforge_convobox-0.4.0/docs/PRIOR-ART.md +125 -0
- legionforge_convobox-0.4.0/docs/QUICKSTART.md +152 -0
- legionforge_convobox-0.4.0/docs/ROADMAP.md +469 -0
- legionforge_convobox-0.4.0/docs/SECURITY.md +281 -0
- legionforge_convobox-0.4.0/docs/SETTINGS-TUI-SCOPE.md +169 -0
- legionforge_convobox-0.4.0/docs/STATUS.md +702 -0
- legionforge_convobox-0.4.0/docs/TROUBLESHOOTING.md +143 -0
- legionforge_convobox-0.4.0/docs/UAT-L3-contrast.md +97 -0
- legionforge_convobox-0.4.0/docs/UAT-checklist.md +1448 -0
- legionforge_convobox-0.4.0/docs/UAT-claude-code-smoke.md +83 -0
- legionforge_convobox-0.4.0/docs/UAT-settings-tui.md +136 -0
- legionforge_convobox-0.4.0/docs/WEB-UI-ARCHITECTURE.md +364 -0
- legionforge_convobox-0.4.0/docs/WEB-UI-DEV.md +211 -0
- legionforge_convobox-0.4.0/docs/WEB-UI-USAGE.md +173 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-18-interactive-prompt-deadlock.md +87 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-18-safeword-vad-coupling.md +78 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-19-corrections-glossary-domain-improbable-sources.md +87 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-20-aec-delay-hint-was-a-red-herring.md +140 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-20-self-barge-in-was-backchannel-not-echo.md +116 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-21-codex-approval-decision-schema-bug.md +97 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-21-sherpa-onnx-dedicated-keyword-spotting-candidate.md +127 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-cublas-dll-loading-needs-path-not-just-add-dll-directory.md +117 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-mcp-tool-calls-have-a-separate-permission-gate.md +115 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-native-allocator-leak-also-surfaces-as-numpy-memoryerror.md +124 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-shared-venv-editable-install-cross-contamination.md +110 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-23-approval-explain-clarify-help-outcome.md +104 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-25-player-is-playing-races-ahead-of-first-audio.md +129 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-25-timing-coincidence-is-not-echo-correlation.md +224 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-26-reference-capture-is-time-compressed-not-wall-clock.md +202 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-26-under-cancelled-echo-is-sometimes-transcribable.md +157 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-27-headphone-choice-does-not-eliminate-under-cancelled-echo.md +198 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-28-other-claude-code-web-uis-dont-transfer-much.md +142 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-29-openlive-comparison-and-acp-direction.md +139 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-07-31-acp-scoping-only-opencode-native.md +145 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-03-stt-config-swap-session-self-barge-in-drift.md +243 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-stt-hotwords-athena-resume-inconclusive.md +202 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-vad-segmenter-silent-unbounded-lockup.md +207 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-web-resume-desyncs-tui-display.md +200 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-web-transcript-forwarding-parity.md +118 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-06-resume-word-hallucination-and-runaway-repetition.md +396 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-06-settings-tui-cannot-open-invalid-config.md +129 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-07-codex-artifact-pane-wiring.md +117 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-07-opencode-artifact-pane-file-edited-is-a-global-event.md +148 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-07-stt-engine-continued-investment-research.md +379 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-09-aec-spectral-suppression-vs-vad-rejection-diverge-on-laptop-internal-array.md +203 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-09-hard-stop-does-not-cancel-an-in-flight-tool-call.md +209 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-10-macos-kokoro-and-tts-stt-roundtrip.md +103 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-10-macos-live-hardware-first-pass.md +173 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-10-macos-live-mic-loop-and-safeword-first-confirmation.md +126 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-artifact-editor-uri-stale-fetch-race-live-reproduced-and-fixed.md +147 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-full-volume-sweep-raw-data-and-room-rt60.md +380 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-macos-live-human-demo-safeword-bargein-and-self-echo-loop.md +155 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-permission-model-validation-claude-codex-opencode.md +336 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-self-barge-in-combined-mitigation-and-hardware-notes.md +150 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-self-barge-in-mitigation-at-demo-volume.md +126 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-safeword-and-pause-phrase-are-mutually-exclusive-within-one-utterance.md +109 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-stt-hallucination-bypasses-the-language-probability-gate-near-miss-on-approval-phrase.md +114 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-vad-freeze-exhaustive-batch-after-fixing-windows-enhancements-confound.md +153 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-vad-freeze-harness-catches-short-stalls-and-a-12-minute-unrecoverable-one.md +220 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-vad-freeze-live-reproduced-three-times-pr269-did-not-fix-it.md +165 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-14-force-kill-reliability-across-all-three-backends.md +177 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-14-mic-pipeline-silence-freeze-isolated-from-backend-via-typed-text-then-a-41-minute-compound-freeze.md +221 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-14-vad-freeze-harness-live-catches-two-more-readline-stalls-with-real-telemetry.md +166 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-claude-code-vad-freeze-re-confirmed-clean-at-good-volume.md +75 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-force-kill-does-not-reach-real-tool-call-children-on-macos.md +161 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-force-kill-macos-fix-attempts-killpg-and-processid-both-fail.md +189 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-force-kill-macos-pgrep-fallback-implemented-and-validated-15-of-15.md +200 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-kill-phrase-live-verified-during-a-genuine-freeze-resume-word-stt-unreliable.md +171 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-deterministic-audio-free-repro-orchestrator-only.md +167 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-fix-validated-retry-cancel-turns-indefinite-hang-into-3s-bound.md +143 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-genuine-event-loop-hang-not-a-volume-confound-live-repro.md +158 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-minimal-repro-attempt-inconclusive-not-a-single-hard-stop.md +111 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-repeated-cancel-mitigates-mechanism-and-workaround-candidate.md +152 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-reproduced-again-narrows-to-overlapping-hard-stops-not-a-single-one.md +133 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-root-cause-pinpointed-cancel-does-not-unstick-aiter-sse.md +162 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-retry-cancel-fix-holds-under-automated-hardstop-storm-on-windows.md +234 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-pause-phrase-fix-live-verified-hard-stop-and-pause-both-register.md +120 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-safety-phrase-reliability-battery-halt-and-bare-athena-unreliable.md +201 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-both-repro-conditions-clean-at-confirmed-good-volume.md +119 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-claude-code-backend-more-resilient-than-codex-on-macos.md +96 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-harness-confound-check-pure-text-still-stalls-severe-variant-audio-coupled.md +112 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-confirmed-no-active-stress-needed.md +97 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-does-not-reproduce-on-claude-code.md +91 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-does-not-reproduce-on-opencode.md +88 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-re-run-with-busy-diagnostic-readline-was-harmless-mic-layer-freeze-real.md +127 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-mic-layer-repro-was-a-test-harness-confound-system-output-volume-at-25pct.md +137 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-new-variant-mic-layer-only-6-minutes-self-resolved.md +115 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-re-run-with-busy-diagnostic-zero-genuine-hangs-in-fresh-10-cycle-batch.md +105 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-readline-stalls-often-just-idle-time-not-a-hang-busy-state-fix.md +145 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-root-cause-codex-blocked-on-own-stdin-not-convobox-event-loop.md +151 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-second-severe-instance-plus-a-self-resolving-66s-stall.md +110 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-mic-freeze-live-reproduced-on-macos.md +217 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-artifact-active-race-fix-live-reverified-post-restart.md +115 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-bare-athena-stt-unreliable-real-voice-windows.md +94 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-brake-safeword-stt-hallucination-false-positive-hard-stop.md +170 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-kill-phrase-live-voice-test-finds-two-real-gaps.md +148 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-macos-force-kill-pgrep-fallback-reconciled-and-shipped.md +107 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-19-kill-phrase-windows-orphaned-descendant-survives-force-kill.md +673 -0
- legionforge_convobox-0.4.0/docs/field-notes/2026-08-20-windows-amplitude-sweep-audio-enhancements-mixed-effect.md +223 -0
- legionforge_convobox-0.4.0/docs/field-notes/LICENSE +15 -0
- legionforge_convobox-0.4.0/docs/field-notes/README.md +50 -0
- legionforge_convobox-0.4.0/docs/field-notes/TEMPLATE.md +37 -0
- legionforge_convobox-0.4.0/docs/media/architecture.svg +116 -0
- legionforge_convobox-0.4.0/docs/media/backends.svg +56 -0
- legionforge_convobox-0.4.0/docs/media/use-cases.svg +78 -0
- legionforge_convobox-0.4.0/docs/media/webui-chat.png +0 -0
- legionforge_convobox-0.4.0/docs/media/webui-settings.png +0 -0
- legionforge_convobox-0.4.0/llms.txt +109 -0
- legionforge_convobox-0.4.0/pyproject.toml +262 -0
- legionforge_convobox-0.4.0/scripts/__init__.py +0 -0
- legionforge_convobox-0.4.0/scripts/_console.py +26 -0
- legionforge_convobox-0.4.0/scripts/acoustic_calibration.py +809 -0
- legionforge_convobox-0.4.0/scripts/analyze_incident.py +333 -0
- legionforge_convobox-0.4.0/scripts/audio_devices.py +843 -0
- legionforge_convobox-0.4.0/scripts/bootstrap_windows.ps1 +106 -0
- legionforge_convobox-0.4.0/scripts/check_venv_identity.py +58 -0
- legionforge_convobox-0.4.0/scripts/ci_local.py +39 -0
- legionforge_convobox-0.4.0/scripts/playback_smoketest.py +69 -0
- legionforge_convobox-0.4.0/scripts/roundtrip_smoketest.py +133 -0
- legionforge_convobox-0.4.0/scripts/run_convobox.py +3298 -0
- legionforge_convobox-0.4.0/scripts/settings_tui.py +2752 -0
- legionforge_convobox-0.4.0/scripts/spike.py +95 -0
- legionforge_convobox-0.4.0/scripts/spike_smoketest.py +126 -0
- legionforge_convobox-0.4.0/scripts/uat-l3-contrast.ps1 +86 -0
- legionforge_convobox-0.4.0/scripts/voice_picker.py +467 -0
- legionforge_convobox-0.4.0/scripts/voice_picker_tui.py +470 -0
- legionforge_convobox-0.4.0/scripts/voice_tui.py +425 -0
- legionforge_convobox-0.4.0/src/convobox/__init__.py +0 -0
- legionforge_convobox-0.4.0/src/convobox/adapters/__init__.py +61 -0
- legionforge_convobox-0.4.0/src/convobox/adapters/base.py +382 -0
- legionforge_convobox-0.4.0/src/convobox/adapters/claude_code.py +1034 -0
- legionforge_convobox-0.4.0/src/convobox/adapters/codex.py +940 -0
- legionforge_convobox-0.4.0/src/convobox/adapters/opencode.py +387 -0
- legionforge_convobox-0.4.0/src/convobox/approval/__init__.py +7 -0
- legionforge_convobox-0.4.0/src/convobox/approval/detector.py +166 -0
- legionforge_convobox-0.4.0/src/convobox/approval/hook_script.py +159 -0
- legionforge_convobox-0.4.0/src/convobox/audio/__init__.py +4 -0
- legionforge_convobox-0.4.0/src/convobox/audio/_sounddevice.py +21 -0
- legionforge_convobox-0.4.0/src/convobox/audio/ack_tones.py +61 -0
- legionforge_convobox-0.4.0/src/convobox/audio/aec.py +300 -0
- legionforge_convobox-0.4.0/src/convobox/audio/capture.py +229 -0
- legionforge_convobox-0.4.0/src/convobox/audio/correlation.py +85 -0
- legionforge_convobox-0.4.0/src/convobox/audio/incident_capture.py +151 -0
- legionforge_convobox-0.4.0/src/convobox/audio/playback.py +298 -0
- legionforge_convobox-0.4.0/src/convobox/config.py +901 -0
- legionforge_convobox-0.4.0/src/convobox/confirmword/__init__.py +3 -0
- legionforge_convobox-0.4.0/src/convobox/confirmword/detector.py +137 -0
- legionforge_convobox-0.4.0/src/convobox/interrupt_presets.py +104 -0
- legionforge_convobox-0.4.0/src/convobox/listening_pause/__init__.py +6 -0
- legionforge_convobox-0.4.0/src/convobox/listening_pause/detector.py +65 -0
- legionforge_convobox-0.4.0/src/convobox/orchestrator/__init__.py +5 -0
- legionforge_convobox-0.4.0/src/convobox/orchestrator/orchestrator.py +724 -0
- legionforge_convobox-0.4.0/src/convobox/response_tiering/__init__.py +14 -0
- legionforge_convobox-0.4.0/src/convobox/response_tiering/detector.py +123 -0
- legionforge_convobox-0.4.0/src/convobox/response_tiering/tiering.py +63 -0
- legionforge_convobox-0.4.0/src/convobox/resumeword/__init__.py +7 -0
- legionforge_convobox-0.4.0/src/convobox/resumeword/detector.py +145 -0
- legionforge_convobox-0.4.0/src/convobox/safeword/__init__.py +3 -0
- legionforge_convobox-0.4.0/src/convobox/safeword/detector.py +45 -0
- legionforge_convobox-0.4.0/src/convobox/stt/__init__.py +14 -0
- legionforge_convobox-0.4.0/src/convobox/stt/base.py +62 -0
- legionforge_convobox-0.4.0/src/convobox/stt/corrections.py +77 -0
- legionforge_convobox-0.4.0/src/convobox/stt/factory.py +26 -0
- legionforge_convobox-0.4.0/src/convobox/stt/language_tracker.py +44 -0
- legionforge_convobox-0.4.0/src/convobox/stt/transcriber.py +519 -0
- legionforge_convobox-0.4.0/src/convobox/tts/__init__.py +14 -0
- legionforge_convobox-0.4.0/src/convobox/tts/base.py +54 -0
- legionforge_convobox-0.4.0/src/convobox/tts/factory.py +216 -0
- legionforge_convobox-0.4.0/src/convobox/tts/kokoro.py +106 -0
- legionforge_convobox-0.4.0/src/convobox/tts/piper.py +193 -0
- legionforge_convobox-0.4.0/src/convobox/tui/__init__.py +4 -0
- legionforge_convobox-0.4.0/src/convobox/tui/render.py +352 -0
- legionforge_convobox-0.4.0/src/convobox/tui/state.py +154 -0
- legionforge_convobox-0.4.0/src/convobox/vad/__init__.py +3 -0
- legionforge_convobox-0.4.0/src/convobox/vad/segmenter.py +418 -0
- legionforge_convobox-0.4.0/src/convobox/web/__init__.py +3 -0
- legionforge_convobox-0.4.0/src/convobox/web/app.py +415 -0
- legionforge_convobox-0.4.0/src/convobox/web/artifacts.py +270 -0
- legionforge_convobox-0.4.0/src/convobox/web/bridge.py +607 -0
- legionforge_convobox-0.4.0/src/convobox/web/history.py +204 -0
- legionforge_convobox-0.4.0/src/convobox/web/mcp_server.py +198 -0
- legionforge_convobox-0.4.0/src/convobox/web/settings_api.py +211 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/img/legionforge-logo.svg +74 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/index.html +2381 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/manifest.json +18 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/sw.js +56 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/LICENSE +29 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/NOTICE.md +22 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/highlight.min.js +1244 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/bash.min.js +21 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/c.min.js +41 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/cpp.min.js +47 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/csharp.min.js +49 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/css.min.js +31 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/dart.min.js +25 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/go.min.js +20 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/graphql.min.js +12 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/java.min.js +38 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/javascript.min.js +81 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/json.min.js +8 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/kotlin.min.js +46 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/lua.min.js +15 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/markdown.min.js +32 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/php.min.js +59 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/powershell.min.js +39 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/python.min.js +42 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/ruby.min.js +54 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/rust.min.js +27 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/scala.min.js +28 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/sql.min.js +21 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/swift.min.js +68 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/typescript.min.js +99 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/xml.min.js +29 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/yaml.min.js +28 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/styles/github-dark.min.css +10 -0
- legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/styles/github.min.css +10 -0
- legionforge_convobox-0.4.0/src/convobox/web/stream.py +136 -0
- legionforge_convobox-0.4.0/src/convobox/web/uploads.py +109 -0
- legionforge_convobox-0.4.0/tests/__init__.py +0 -0
- legionforge_convobox-0.4.0/tests/fake_claude_cli.py +136 -0
- legionforge_convobox-0.4.0/tests/fake_codex_appserver.py +316 -0
- legionforge_convobox-0.4.0/tests/test_ack_tones.py +78 -0
- legionforge_convobox-0.4.0/tests/test_adapters_base.py +109 -0
- legionforge_convobox-0.4.0/tests/test_aec.py +335 -0
- legionforge_convobox-0.4.0/tests/test_approval.py +147 -0
- legionforge_convobox-0.4.0/tests/test_approval_explanation_rendering.py +100 -0
- legionforge_convobox-0.4.0/tests/test_approval_prompt_gate.py +324 -0
- legionforge_convobox-0.4.0/tests/test_audio.py +883 -0
- legionforge_convobox-0.4.0/tests/test_audio_correlation.py +117 -0
- legionforge_convobox-0.4.0/tests/test_audio_devices.py +356 -0
- legionforge_convobox-0.4.0/tests/test_backend_working_dir.py +61 -0
- legionforge_convobox-0.4.0/tests/test_barge_in.py +446 -0
- legionforge_convobox-0.4.0/tests/test_claude_approval_hook_script.py +256 -0
- legionforge_convobox-0.4.0/tests/test_claude_code_adapter.py +666 -0
- legionforge_convobox-0.4.0/tests/test_claude_code_approval.py +646 -0
- legionforge_convobox-0.4.0/tests/test_codex_adapter.py +1063 -0
- legionforge_convobox-0.4.0/tests/test_config.py +404 -0
- legionforge_convobox-0.4.0/tests/test_confirmword.py +138 -0
- legionforge_convobox-0.4.0/tests/test_continue_prompt_gate.py +82 -0
- legionforge_convobox-0.4.0/tests/test_conversation_tui.py +489 -0
- legionforge_convobox-0.4.0/tests/test_conversation_tui_keys.py +158 -0
- legionforge_convobox-0.4.0/tests/test_example_config.py +48 -0
- legionforge_convobox-0.4.0/tests/test_factories.py +35 -0
- legionforge_convobox-0.4.0/tests/test_incident_capture.py +181 -0
- legionforge_convobox-0.4.0/tests/test_interrupt_presets.py +77 -0
- legionforge_convobox-0.4.0/tests/test_language_tracker.py +62 -0
- legionforge_convobox-0.4.0/tests/test_listening_pause.py +72 -0
- legionforge_convobox-0.4.0/tests/test_opencode_adapter.py +815 -0
- legionforge_convobox-0.4.0/tests/test_orchestrator.py +1569 -0
- legionforge_convobox-0.4.0/tests/test_permission_mode.py +214 -0
- legionforge_convobox-0.4.0/tests/test_recognition_error_ladder.py +58 -0
- legionforge_convobox-0.4.0/tests/test_response_tiering.py +100 -0
- legionforge_convobox-0.4.0/tests/test_response_tiering_state.py +90 -0
- legionforge_convobox-0.4.0/tests/test_resumeword.py +88 -0
- legionforge_convobox-0.4.0/tests/test_roundtrip_smoketest_cli.py +32 -0
- legionforge_convobox-0.4.0/tests/test_run_convobox_echo.py +601 -0
- legionforge_convobox-0.4.0/tests/test_run_convobox_quit.py +192 -0
- legionforge_convobox-0.4.0/tests/test_safeword.py +49 -0
- legionforge_convobox-0.4.0/tests/test_settings_tui.py +2569 -0
- legionforge_convobox-0.4.0/tests/test_spike_cli.py +27 -0
- legionforge_convobox-0.4.0/tests/test_startup_announcement.py +23 -0
- legionforge_convobox-0.4.0/tests/test_stt_factory.py +65 -0
- legionforge_convobox-0.4.0/tests/test_transcribe_with_timeout.py +120 -0
- legionforge_convobox-0.4.0/tests/test_transcriber.py +848 -0
- legionforge_convobox-0.4.0/tests/test_transcript_corrections.py +59 -0
- legionforge_convobox-0.4.0/tests/test_tts.py +341 -0
- legionforge_convobox-0.4.0/tests/test_tts_factory.py +450 -0
- legionforge_convobox-0.4.0/tests/test_tts_kokoro.py +227 -0
- legionforge_convobox-0.4.0/tests/test_vad_segmenter.py +777 -0
- legionforge_convobox-0.4.0/tests/test_voice_picker_cli.py +201 -0
- legionforge_convobox-0.4.0/tests/test_voice_picker_tui.py +153 -0
- legionforge_convobox-0.4.0/tests/test_web_app.py +916 -0
- legionforge_convobox-0.4.0/tests/test_web_artifacts.py +491 -0
- legionforge_convobox-0.4.0/tests/test_web_bridge.py +1115 -0
- legionforge_convobox-0.4.0/tests/test_web_history.py +292 -0
- legionforge_convobox-0.4.0/tests/test_web_mcp_server.py +121 -0
- legionforge_convobox-0.4.0/tests/test_web_settings_api.py +324 -0
- legionforge_convobox-0.4.0/tests/test_web_uploads.py +129 -0
- legionforge_convobox-0.4.0/tests/test_working_watchdog.py +499 -0
- legionforge_convobox-0.4.0/uv.lock +2695 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
2
|
+
|
|
3
|
+
*.py text eol=lf
|
|
4
|
+
*.md text eol=lf
|
|
5
|
+
*.toml text eol=lf
|
|
6
|
+
*.yaml text eol=lf
|
|
7
|
+
*.yml text eol=lf
|
|
8
|
+
*.tour text eol=lf
|
|
9
|
+
*.json text eol=lf
|
|
10
|
+
*.lock text eol=lf
|
|
11
|
+
*.ps1 text eol=crlf
|
|
12
|
+
*.sh text eol=lf
|
|
13
|
+
|
|
14
|
+
*.wav binary
|
|
15
|
+
*.onnx binary
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Pre-push privacy scrub -- scans every commit about to be uploaded, not
|
|
3
|
+
# just the working tree (a clean tree is not a clean push: git push
|
|
4
|
+
# uploads all ancestor commits). Reference: LegionForge/jeli's hook and
|
|
5
|
+
# the 2026-07-02 stale-branch leak it was written after.
|
|
6
|
+
# Enable once per clone: git config core.hooksPath .githooks
|
|
7
|
+
PATTERN='192\.168\.|10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|ssh[^ ]*@[a-zA-Z0-9]|@[a-zA-Z0-9-]+\.local|id_ed25519|id_rsa|BEGIN [A-Z ]*PRIVATE KEY'
|
|
8
|
+
fail=0
|
|
9
|
+
while read -r _local_ref local_sha _remote_ref remote_sha; do
|
|
10
|
+
[ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
|
|
11
|
+
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
12
|
+
range=$(git rev-list "$local_sha" --not --remotes=origin 2>/dev/null)
|
|
13
|
+
else
|
|
14
|
+
range=$(git rev-list "$remote_sha..$local_sha" 2>/dev/null)
|
|
15
|
+
fi
|
|
16
|
+
[ -z "$range" ] && continue
|
|
17
|
+
# uv.lock excluded: package version strings match the IP regex
|
|
18
|
+
# (nvidia_curand 10.4.0.35 -- verified false positive 2026-07-19).
|
|
19
|
+
# This hook excludes itself: it necessarily contains the patterns it
|
|
20
|
+
# scans for (it blocked its own first push -- working as designed).
|
|
21
|
+
hits=$(git grep -l -E "$PATTERN" $range -- ':!uv.lock' ':!.githooks/pre-push' 2>/dev/null)
|
|
22
|
+
if [ -n "$hits" ]; then
|
|
23
|
+
echo "pre-push BLOCKED: possible private data in outgoing commits:" >&2
|
|
24
|
+
echo "$hits" >&2
|
|
25
|
+
echo "Inspect with: git grep -n -E '<pattern>' <sha> -- <file>" >&2
|
|
26
|
+
fail=1
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
exit $fail
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Every change requires JP's review -- the mechanical enforcement of
|
|
2
|
+
# "agents propose, JP approves" (AGENTS.md rule 10 era). GitHub
|
|
3
|
+
# auto-requests review from owners on every PR touching matched paths.
|
|
4
|
+
* @jp-cruz
|
|
5
|
+
# Workflow changes doubly so: they run with repo permissions.
|
|
6
|
+
.github/workflows/ @jp-cruz
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# The SHA-pinned dev-rig reusables and actions never update themselves
|
|
4
|
+
# -- that is the trade-off of pinning; this is the standard answer.
|
|
5
|
+
- package-ecosystem: github-actions
|
|
6
|
+
directory: /
|
|
7
|
+
schedule:
|
|
8
|
+
interval: weekly
|
|
9
|
+
groups:
|
|
10
|
+
actions:
|
|
11
|
+
patterns: ["*"]
|
|
12
|
+
- package-ecosystem: pip
|
|
13
|
+
directory: /
|
|
14
|
+
schedule:
|
|
15
|
+
interval: weekly
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
Describe the change briefly.
|
|
4
|
+
|
|
5
|
+
## Attribution
|
|
6
|
+
|
|
7
|
+
- [ ] Human-only change
|
|
8
|
+
- [ ] AI-assisted change
|
|
9
|
+
|
|
10
|
+
If you check `AI-assisted change`, the CI check will require the attribution
|
|
11
|
+
block below or an `AI-Attribution:` commit trailer.
|
|
12
|
+
|
|
13
|
+
If AI-assisted, fill in:
|
|
14
|
+
|
|
15
|
+
- Product:
|
|
16
|
+
- Provider:
|
|
17
|
+
- Model:
|
|
18
|
+
- Scope:
|
|
19
|
+
- Date:
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
Attribution: OpenAI Codex
|
|
25
|
+
Provider: OpenAI
|
|
26
|
+
Model: GPT-5
|
|
27
|
+
Scope: src/convobox/adapters/codex.py
|
|
28
|
+
Date: 2026-07-15
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Validation
|
|
32
|
+
|
|
33
|
+
- [ ] Tests run
|
|
34
|
+
- [ ] Docs updated
|
|
35
|
+
- [ ] Changelog updated, if user-visible
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
name: AI Attribution Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, edited, synchronize, reopened, ready_for_review]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pull-requests: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
check:
|
|
13
|
+
name: Require attribution when AI-assisted
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Validate PR attribution
|
|
17
|
+
uses: actions/github-script@v9
|
|
18
|
+
with:
|
|
19
|
+
script: |
|
|
20
|
+
const pull = context.payload.pull_request;
|
|
21
|
+
const body = pull.body || "";
|
|
22
|
+
|
|
23
|
+
const aiAssistedMarked = /-\s*\[[xX]\]\s*AI-assisted change/.test(body);
|
|
24
|
+
if (!aiAssistedMarked) {
|
|
25
|
+
core.info("PR is not marked as AI-assisted; skipping attribution enforcement.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check the PR body FIRST -- this needs no API call, so a
|
|
30
|
+
// correctly-attributed PR can never be blocked by a GitHub API
|
|
31
|
+
// outage (which is exactly what once spuriously failed a
|
|
32
|
+
// correct PR: the old code called listCommits unconditionally
|
|
33
|
+
// at the top and threw on a transient 503).
|
|
34
|
+
const bodyHasAttribution =
|
|
35
|
+
/Attribution:\s*/i.test(body) &&
|
|
36
|
+
/Provider:\s*/i.test(body) &&
|
|
37
|
+
/Model:\s*/i.test(body) &&
|
|
38
|
+
/Scope:\s*/i.test(body) &&
|
|
39
|
+
/Date:\s*/i.test(body);
|
|
40
|
+
if (bodyHasAttribution) {
|
|
41
|
+
core.info("AI attribution present in PR body.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The body lacks the block -> fall back to commit trailers. This
|
|
46
|
+
// is the ONLY path that needs the API. A transient API error here
|
|
47
|
+
// must NOT hard-fail the PR on infrastructure grounds: attribution
|
|
48
|
+
// is a governance requirement, and blocking correct work during a
|
|
49
|
+
// GitHub outage is worse than the rare case of one un-attributed
|
|
50
|
+
// PR slipping through while the API is down (easily caught later).
|
|
51
|
+
let commitMessages = "";
|
|
52
|
+
try {
|
|
53
|
+
const commits = await github.paginate(
|
|
54
|
+
github.rest.pulls.listCommits,
|
|
55
|
+
{
|
|
56
|
+
owner: context.repo.owner,
|
|
57
|
+
repo: context.repo.repo,
|
|
58
|
+
pull_number: pull.number,
|
|
59
|
+
per_page: 100,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
commitMessages = commits.map((commit) => commit.commit.message).join("\n\n");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
core.warning(
|
|
65
|
+
"Could not fetch commits to check for an AI-Attribution trailer (" +
|
|
66
|
+
(error && error.message ? error.message : String(error)) +
|
|
67
|
+
"). Skipping the commit-trailer check rather than failing the PR on " +
|
|
68
|
+
"an infrastructure error -- ensure attribution is present.",
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (/AI-Attribution:/i.test(commitMessages)) {
|
|
74
|
+
core.info("AI attribution present in a commit trailer.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
core.setFailed(
|
|
79
|
+
[
|
|
80
|
+
"This PR is marked AI-assisted but is missing the required attribution.",
|
|
81
|
+
"",
|
|
82
|
+
"Add either:",
|
|
83
|
+
"- the standard attribution block to the PR body, or",
|
|
84
|
+
"- an `AI-Attribution:` commit trailer on one or more commits.",
|
|
85
|
+
"",
|
|
86
|
+
"Required fields for the PR body block:",
|
|
87
|
+
"Attribution, Provider, Model, Scope, Date.",
|
|
88
|
+
].join("\n"),
|
|
89
|
+
);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# LegionForge dev-rig CI: test/lint/SAST/dependency-audit/SBOM/secrets.
|
|
2
|
+
#
|
|
3
|
+
# Consumes LegionForge/dev-rig's shared reusable workflows (shared Python CI
|
|
4
|
+
# + security rig used across LegionForge projects) rather than duplicating
|
|
5
|
+
# tool config here. See TESTING.md -> "CI / LegionForge dev-rig" for what
|
|
6
|
+
# each job checks, why it's scoped the way it is, and how to run the same
|
|
7
|
+
# checks locally before pushing.
|
|
8
|
+
#
|
|
9
|
+
# Pinned to a commit SHA, not a tag: LegionForge/dev-rig has cut no tagged
|
|
10
|
+
# release as of this writing (confirmed via `gh api repos/LegionForge/dev-rig/tags`
|
|
11
|
+
# returning empty) -- dev-rig's own README examples currently show `@main`,
|
|
12
|
+
# a mutable ref this project deliberately does not consume (see
|
|
13
|
+
# LegionForge/dev-rig's pipeline-hardening handoff doc: never consume
|
|
14
|
+
# `@main`, use a pinned tag once one exists). SHA-pin is the correct
|
|
15
|
+
# available-today choice; re-pin to a released tag once dev-rig cuts one.
|
|
16
|
+
#
|
|
17
|
+
# DEV_RIG_REF: 468d5a7c30f6540a472a32d8b3bea1ac381f2017 (main, 2026-07-10)
|
|
18
|
+
|
|
19
|
+
name: CI
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
pull_request:
|
|
23
|
+
push:
|
|
24
|
+
branches: [main]
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
test:
|
|
31
|
+
name: Tests & Coverage
|
|
32
|
+
uses: LegionForge/dev-rig/.github/workflows/test.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
|
|
33
|
+
with:
|
|
34
|
+
coverage-source: "convobox"
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
|
|
37
|
+
lint:
|
|
38
|
+
name: Lint & Type Check
|
|
39
|
+
uses: LegionForge/dev-rig/.github/workflows/lint.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
|
|
40
|
+
with:
|
|
41
|
+
source-dirs: "src/convobox"
|
|
42
|
+
python-version: "3.12"
|
|
43
|
+
# types-PyYAML is the default already; explicit for clarity. mypy's
|
|
44
|
+
# remaining untyped third-party deps (faster-whisper, sounddevice,
|
|
45
|
+
# silero-vad -- no stub packages exist for these) are scoped out via
|
|
46
|
+
# [[tool.mypy.overrides]] in pyproject.toml, not a blanket CLI flag,
|
|
47
|
+
# so this stays real type-checking rather than a broad opt-out.
|
|
48
|
+
extra-mypy-deps: "types-PyYAML"
|
|
49
|
+
|
|
50
|
+
sast:
|
|
51
|
+
name: SAST
|
|
52
|
+
uses: LegionForge/dev-rig/.github/workflows/sast.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
|
|
53
|
+
permissions:
|
|
54
|
+
contents: read
|
|
55
|
+
security-events: write # required for the CodeQL upload-sarif step inside sast.yml
|
|
56
|
+
with:
|
|
57
|
+
source-dirs: "src/convobox"
|
|
58
|
+
# Restored p/fastapi 2026-08-17: ConvoBox now has a real FastAPI web
|
|
59
|
+
# UI (src/convobox/web/) plus a mounted MCP HTTP server with a CSRF
|
|
60
|
+
# exemption for its own routes -- exactly the trust-boundary code
|
|
61
|
+
# p/fastapi's ruleset targets. dev-rig's default is "p/python
|
|
62
|
+
# p/fastapi"; kept p/security-audit alongside it rather than
|
|
63
|
+
# dropping back to the narrower default.
|
|
64
|
+
semgrep-configs: "p/python p/fastapi p/security-audit"
|
|
65
|
+
python-version: "3.12"
|
|
66
|
+
|
|
67
|
+
audit:
|
|
68
|
+
name: Dependency Audit
|
|
69
|
+
uses: LegionForge/dev-rig/.github/workflows/audit.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
|
|
70
|
+
with:
|
|
71
|
+
python-version: "3.12"
|
|
72
|
+
|
|
73
|
+
sbom:
|
|
74
|
+
name: SBOM
|
|
75
|
+
uses: LegionForge/dev-rig/.github/workflows/sbom.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
|
|
76
|
+
with:
|
|
77
|
+
python-version: "3.12"
|
|
78
|
+
|
|
79
|
+
secrets:
|
|
80
|
+
name: Secret Scanning
|
|
81
|
+
uses: LegionForge/dev-rig/.github/workflows/secrets.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Triggered by pushing a version tag (e.g. git tag v0.4.0 && git push --tags).
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token stored in secrets.
|
|
5
|
+
# Configure the trusted publisher at https://pypi.org/manage/project/legionforge-convobox/settings/
|
|
6
|
+
# before pushing the first tag:
|
|
7
|
+
# Publisher: GitHub Actions
|
|
8
|
+
# Repository: LegionForge/convobox
|
|
9
|
+
# Workflow: publish.yml
|
|
10
|
+
# Environment: pypi
|
|
11
|
+
#
|
|
12
|
+
# PyPI distribution name is legionforge-convobox (org convention: PyPI is a
|
|
13
|
+
# flat global namespace, so every LegionForge package is prefixed to avoid
|
|
14
|
+
# squatting/ambiguity -- see LegionForge/convobox's own DEPENDENCY_LICENSE_AUDIT.md
|
|
15
|
+
# and pyproject.toml `[project] name`). The repo name, import path, and CLI
|
|
16
|
+
# commands (convobox/convobox-settings/convobox-spike) stay unprefixed.
|
|
17
|
+
#
|
|
18
|
+
# Template: LegionForge/llm-valet/.github/workflows/publish.yml, adapted to
|
|
19
|
+
# this repo's actual Python floor (3.12, matching pyproject.toml's
|
|
20
|
+
# requires-python and ci.yml's python-version -- not copied blindly from
|
|
21
|
+
# llm-valet's 3.11).
|
|
22
|
+
#
|
|
23
|
+
# Every action below is pinned to a commit SHA, not a floating tag --
|
|
24
|
+
# same discipline ci.yml already applies to dev-rig (see that file's own
|
|
25
|
+
# comment on why a moved tag is a real supply-chain vector), just not
|
|
26
|
+
# consistently carried over here when this file was first written.
|
|
27
|
+
# pypa/gh-action-pypi-publish's `release/v1` ref is a BRANCH, not a tag --
|
|
28
|
+
# more mutable than a normal version tag, not less, so pinning matters
|
|
29
|
+
# even more for it. Resolved 2026-08-06:
|
|
30
|
+
# actions/checkout@v7 -> 3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
31
|
+
# actions/setup-python@v6 -> ece7cb06caefa5fff74198d8649806c4678c61a1
|
|
32
|
+
# actions/upload-artifact@v7 -> 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
|
33
|
+
# actions/download-artifact@v8 -> 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
34
|
+
# pypa/gh-action-pypi-publish@release/v1 -> dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
|
35
|
+
|
|
36
|
+
on:
|
|
37
|
+
push:
|
|
38
|
+
tags:
|
|
39
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
build:
|
|
43
|
+
name: Build distribution
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
48
|
+
|
|
49
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.12"
|
|
52
|
+
|
|
53
|
+
- name: Install build tools
|
|
54
|
+
run: pip install hatchling build
|
|
55
|
+
|
|
56
|
+
- name: Build wheel and sdist
|
|
57
|
+
run: python -m build
|
|
58
|
+
|
|
59
|
+
- name: Upload dist artifacts
|
|
60
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
61
|
+
with:
|
|
62
|
+
name: dist
|
|
63
|
+
path: dist/
|
|
64
|
+
|
|
65
|
+
publish:
|
|
66
|
+
name: Publish to PyPI
|
|
67
|
+
needs: build
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
environment: pypi
|
|
70
|
+
permissions:
|
|
71
|
+
id-token: write # required for OIDC trusted publishing
|
|
72
|
+
|
|
73
|
+
steps:
|
|
74
|
+
- name: Download dist artifacts
|
|
75
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
|
76
|
+
with:
|
|
77
|
+
name: dist
|
|
78
|
+
path: dist/
|
|
79
|
+
|
|
80
|
+
- name: Publish to PyPI
|
|
81
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
.models/
|
|
3
|
+
# scripts/run_convobox.py --aec-dump output (reference/mic WAV captures
|
|
4
|
+
# for offline AEC analysis) -- real audio, never committed. *.wav below
|
|
5
|
+
# already covers the files; this also catches the directory itself so an
|
|
6
|
+
# empty/future-dated run doesn't show up as an untracked-but-visible dir.
|
|
7
|
+
.aec-dumps/
|
|
8
|
+
.incident-captures/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.pyc
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
coverage.xml
|
|
14
|
+
*.egg-info/
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
.env
|
|
18
|
+
*.wav
|
|
19
|
+
*.log
|
|
20
|
+
# Manually rotated/renamed logs kept around for a diff (e.g.
|
|
21
|
+
# convobox-tui.log.old.20260726) -- *.log above only matches the
|
|
22
|
+
# unrotated name.
|
|
23
|
+
*.log.old*
|
|
24
|
+
uat-acoustic-calibration/
|
|
25
|
+
.DS_Store
|
|
26
|
+
|
|
27
|
+
# Per-machine local config and its variants (copy convobox.example.yaml to
|
|
28
|
+
# create one) -- these carry real device names and other per-machine
|
|
29
|
+
# specifics, same rationale as convobox.yaml itself. convobox.example.yaml
|
|
30
|
+
# is the one deliberately-tracked exception.
|
|
31
|
+
convobox*.yaml
|
|
32
|
+
!convobox.example.yaml
|
|
33
|
+
# Config backups now live in a subdirectory (settings_tui.py's
|
|
34
|
+
# backup_config/list_config_backups, GitHub issue #235 finding D4) rather
|
|
35
|
+
# than scattered as convobox.yaml.backup-* directly in the repo root --
|
|
36
|
+
# the old flat pattern is kept too so a pre-existing checkout with old-style
|
|
37
|
+
# backups still has them ignored until cleaned up.
|
|
38
|
+
.convobox-backups/
|
|
39
|
+
convobox.yaml.backup-*
|
|
40
|
+
|
|
41
|
+
# run_convobox.py's AEC delay auto-tune diagnostic sidecar (config.py's
|
|
42
|
+
# write_aec_estimate/read_aec_estimate) -- runtime-measured, not config.
|
|
43
|
+
*.aec-estimate.json
|
|
44
|
+
|
|
45
|
+
# Personal working notes kept out of the public repo -- philosophy.md is
|
|
46
|
+
# JP's running voice-dialogue log (archived separately in the vault);
|
|
47
|
+
# _diag.py/_tone.py were throwaway audio probes superseded by
|
|
48
|
+
# scripts/audio_devices.py. Structural protection against an accidental
|
|
49
|
+
# `git add -A` sweeping them in.
|
|
50
|
+
philosophy.md
|
|
51
|
+
scripts/_diag.py
|
|
52
|
+
scripts/_tone.py
|
|
53
|
+
|
|
54
|
+
# Leading-underscore convention for repo-root scratch/throwaway files
|
|
55
|
+
# (message-log dumps, one-off test artifacts) -- generalizes the same
|
|
56
|
+
# pattern _diag.py/_tone.py already used above.
|
|
57
|
+
/_*
|
|
58
|
+
|
|
59
|
+
# Web UI and persistent history -- contains user transcripts, approvals, commands.
|
|
60
|
+
# KEEP THIS unless you explicitly want to track history in a PRIVATE repo.
|
|
61
|
+
# See docs/SECURITY.md for opt-in flow.
|
|
62
|
+
.convobox-history/
|
|
63
|
+
.convobox.db
|
|
64
|
+
|
|
65
|
+
# Web server logs (may contain event data, tool inputs, responses)
|
|
66
|
+
.convobox-web.log
|
|
67
|
+
convobox-web-*.log
|
|
68
|
+
.convobox-cache/
|
|
69
|
+
|
|
70
|
+
# Private/scratch directories
|
|
71
|
+
.scratch/
|
|
72
|
+
.private/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# AI-assisted commit template
|
|
2
|
+
#
|
|
3
|
+
# Use this template for commits that include AI-assisted work.
|
|
4
|
+
# Keep the attribution block intact and replace the placeholders.
|
|
5
|
+
|
|
6
|
+
Summary:
|
|
7
|
+
|
|
8
|
+
Attribution: <product>
|
|
9
|
+
Provider: <provider>
|
|
10
|
+
Model: <model>
|
|
11
|
+
Scope: <files, feature, or commit>
|
|
12
|
+
Date: <YYYY-MM-DD>
|
|
13
|
+
|
|
14
|
+
AI-Attribution: <product>; provider=<provider>; model=<model>; scope=<files, feature, or commit>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://aka.ms/codetour-schema",
|
|
3
|
+
"title": "1. Architecture & Data Flow",
|
|
4
|
+
"description": "Full pipeline walkthrough, in call order: mic -> VAD -> STT -> safeword -> orchestrator -> backend adapter -> TTS -> playback. Each step calls out the data handoff (shape/dtype crossing the boundary) and any performance-relevant design decision at that point.",
|
|
5
|
+
"steps": [
|
|
6
|
+
{
|
|
7
|
+
"title": "Start here",
|
|
8
|
+
"description": "**ConvoBox pipeline, top to bottom.**\n\n```\n mic -> VAD -> local STT -> safeword check -> orchestrator -> backend adapter\n | |\n (deterministic, OpenCode /\n no LLM) Claude Code / ...\n |\n local streaming TTS\n```\n\nThis tour follows one utterance through every stage in the order it actually happens at runtime, not file/directory order. Watch the **data handoff** called out at each step — the shape/dtype changes as audio becomes numbers becomes text becomes routing decisions becomes speech again.\n\nSee `README.md` → Architecture for the original design diagram this pipeline implements.\n\nEach step below is anchored by both a line number and a text pattern — if the code has moved since this tour was written, CodeTour re-locates the step by searching for the pattern rather than trusting a stale line number."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"file": "scripts/spike.py",
|
|
12
|
+
"line": 38,
|
|
13
|
+
"pattern": "with MicrophoneStream\\(sample_rate=config\\.audio\\.sample_rate",
|
|
14
|
+
"description": "**The whole chain in one line.** This is the actual measurement spike — mic → VAD → STT → logged transcript — with no backend/TTS wiring, useful as an orientation map before diving into each stage:\n\n```python\nwith MicrophoneStream(...) as mic:\n async for utterance in segmenter.segment(mic.stream()):\n result = transcriber.transcribe(utterance)\n```\n\n`mic.stream()` is an async generator of raw chunks → `segmenter.segment()` re-yields only complete utterances → `transcriber.transcribe()` turns one utterance into text. Every step below zooms into one of these three calls."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"file": "src/convobox/audio/capture.py",
|
|
18
|
+
"line": 21,
|
|
19
|
+
"pattern": "class MicrophoneStream:",
|
|
20
|
+
"description": "**Stage 1: audio capture.** `MicrophoneStream` wraps a single `sounddevice.InputStream` — continuous streaming, not record-then-stop, because segmentation into utterances is VAD's job downstream, not a fixed capture window.\n\nDeliberately **not** shelling out to platform tools (sox/arecord/powershell) — see docs/LESSONS-FROM-VOICE-OPENCODE.md for why that was a real command-injection bug in an earlier related project."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"file": "src/convobox/audio/capture.py",
|
|
24
|
+
"line": 42,
|
|
25
|
+
"pattern": "def _callback\\(",
|
|
26
|
+
"description": "**The real-time boundary.** `_callback` runs on PortAudio's own audio thread, not the asyncio event loop — it must be fast and non-blocking. It just copies the incoming buffer and pushes it onto a plain `queue.Queue`.\n\n**Data handoff:** raw device audio → `float32` numpy array, shape `(blocksize,)`, values in `[-1, 1]`. float32 (not int16) because both Silero VAD and faster-whisper consume float32 directly — int16 would force a conversion on every single chunk (see the module-level comment at the top of this file)."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"file": "src/convobox/audio/capture.py",
|
|
30
|
+
"line": 70,
|
|
31
|
+
"pattern": "async def stream\\(self\\) -> AsyncIterator",
|
|
32
|
+
"description": "**Bridging the sync callback thread to async consumers.** `stream()` is an async generator that pulls off the queue via `asyncio.to_thread(self._queue.get)` — blocking the queue-get in a worker thread so it never stalls the event loop.\n\nThe `_CLOSE_SENTINEL` pattern (defined near the top of this file) is what lets `close()` wake up a consumer parked in a blocking `get()` — this exact bridge pattern gets reused later for TTS streaming too (see step on `piper.py`)."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"file": "src/convobox/vad/segmenter.py",
|
|
36
|
+
"line": 23,
|
|
37
|
+
"pattern": "class UtteranceSegmenter:",
|
|
38
|
+
"description": "**Stage 2: voice activity detection.** `UtteranceSegmenter` turns a stream of float32 chunks into discrete utterances using Silero VAD.\n\nTwo deliberate design choices worth noting up front (both in the docstring): a speech run only ends after `min_silence_ms` of *confirmed* silence (tolerant of pauses/disfluencies), and emitted utterances **include** that trailing silence on purpose — it helps STT avoid clipping the last phoneme."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"file": "src/convobox/vad/segmenter.py",
|
|
42
|
+
"line": 51,
|
|
43
|
+
"pattern": "def feed\\(self, chunk: np\\.ndarray\\)",
|
|
44
|
+
"description": "**Data handoff: arbitrary-length chunks → fixed 512-sample windows.** Silero's ONNX model only accepts exactly 512 samples (32ms) at 16kHz. Incoming capture chunks can be any length (and in `feed()`'s own tests, deliberately unaligned to 512 to prove no samples are dropped/duplicated), so they're buffered in `self._carry` and consumed in exact windows here.\n\nReturns a **list** of utterances (not zero-or-one) because a single large chunk can span the end of one utterance and the start of another."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"file": "src/convobox/vad/segmenter.py",
|
|
48
|
+
"line": 87,
|
|
49
|
+
"pattern": "def _process_window\\(self, window: np\\.ndarray\\)",
|
|
50
|
+
"description": "**The core state machine — and where a real hang bug lived.** Every window gets a `prob` from the model (this call happens unconditionally, every ~32ms, even during silence — see the Performance tour for why that's not actually wasted work).\n\nA few lines down, `_EXIT_HYSTERESIS` creates three zones: confidently speech, confidently silent, and an **ambiguous band** in between. The `elif is_speech:` branch just below (not a plain `else:`) is the fix for a bug where ambiguous-band frames were treated as speech and reset the silence timer on every frame — a speaker trailing off gradually could keep an utterance open forever. See the code comment right below it for the full story, and the Review Findings tour for the verification trail."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"file": "src/convobox/vad/segmenter.py",
|
|
54
|
+
"line": 131,
|
|
55
|
+
"pattern": "async def segment\\(",
|
|
56
|
+
"description": "**Data handoff: windows → one concatenated utterance array.** `segment()` is the async-generator wrapper `spike.py` actually calls — consumes chunks, yields complete utterances via `feed()`, and on stream close calls `flush()` to emit anything still in progress.\n\nOutput: a single float32 numpy array (all the 512-sample windows for that utterance concatenated together) — this is exactly what `LocalTranscriber.transcribe()` expects next."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"file": "src/convobox/stt/transcriber.py",
|
|
60
|
+
"line": 24,
|
|
61
|
+
"pattern": "class LocalTranscriber:",
|
|
62
|
+
"description": "**Stage 3: local speech-to-text.** `LocalTranscriber` loads the faster-whisper `WhisperModel` **once**, at construction — model loading is expensive, so this class is meant to be built once and reused across every utterance for the life of the process."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"file": "src/convobox/stt/transcriber.py",
|
|
66
|
+
"line": 33,
|
|
67
|
+
"pattern": "def transcribe\\(self, audio: np\\.ndarray\\)",
|
|
68
|
+
"description": "**Data handoff: utterance array → `TranscriptResult`.** Input is the float32 mono 16kHz array from the segmenter (defensively re-contiguous'd a couple lines down, since faster-whisper needs a contiguous buffer).\n\nOutput bundles the transcript text **with timing as a first-class field** (`latency_ms`, `duration_s`) — not just logged internally — because this project's whole first milestone was proving real accuracy/latency numbers before committing to backend/adapter design. Real measured numbers on this hardware: **STT ~140–200ms, RTF 0.05–0.13** (8–20x faster than real-time)."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"file": "src/convobox/safeword/detector.py",
|
|
72
|
+
"line": 5,
|
|
73
|
+
"pattern": "deliberately a plain normalized-substring match",
|
|
74
|
+
"description": "**Stage 4: the safeword check — deliberately dumb.** This comment says it outright: a plain normalized-substring match, **no ML, no fuzzy matching, no LLM**, on purpose. A hard stop is a safety-critical abort; a human reading this file must be able to predict exactly when it fires. That auditability guarantee is worth more here than the recall a fuzzy matcher would buy.\n\nThis runs on every transcript, in the orchestrator, **before** any busy/idle routing decision — see the next step."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"file": "src/convobox/orchestrator/orchestrator.py",
|
|
78
|
+
"line": 40,
|
|
79
|
+
"pattern": "async def handle_transcript\\(self, transcript: str\\)",
|
|
80
|
+
"description": "**Stage 5: the routing decision.** `handle_transcript` is where a finalized transcript becomes one of three concrete actions:\n\n1. **Hard stop** — checked first, unconditionally, and wins over everything else. It also stops any in-progress TTS/playback — if the user says stop, ConvoBox shouldn't keep talking.\n2. **Interject** — backend is busy, this is a side-comment, not a fresh task.\n3. **Fresh command** — backend is idle.\n\nThe `start_event_loop()` call near the top of this method is itself a fix: see the Review Findings tour for why `is_busy()` needs that loop running to stay accurate."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"file": "src/convobox/adapters/base.py",
|
|
84
|
+
"line": 32,
|
|
85
|
+
"pattern": "class BackendAdapter\\(ABC\\):",
|
|
86
|
+
"description": "**The backend adapter contract.** Three implementations exist and are each verified against a live instance: `OpenCodeAdapter` (HTTP+SSE), `ClaudeCodeAdapter` (bidirectional stream-json subprocess, `src/convobox/adapters/claude_code.py`), `CodexAdapter` (app-server JSON-RPC over stdio, `src/convobox/adapters/codex.py`). The docstring draws a hard line: `send_interject` must never derail a running task, `send_hard_stop` must abort it — these are never allowed to collapse into one \"interrupt\" action.\n\nA paragraph further down is an honesty clause added after review: a hard stop's real-world guarantee is bounded by what the backend actually exposes (see the next step for the concrete case)."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"file": "src/convobox/adapters/opencode.py",
|
|
90
|
+
"line": 82,
|
|
91
|
+
"pattern": "async def events\\(self\\) -> AsyncGenerator",
|
|
92
|
+
"description": "**The OpenCode backend adapter: HTTP + SSE.** `events()` opens a streaming GET and yields typed `BackendEvent`s parsed from each SSE frame in real time — not buffered until the connection closes.\n\n**Data handoff:** raw SSE `data:` frames (JSON text) → typed `BackendEvent` objects (`text | tool_call | tool_result | error | done`). The `finally` block a few lines down clears `self._busy = False` on **any** exit from this generator, not just an observed DONE — a dropped connection or exception used to leave `is_busy()` stuck `True` forever. See Review Findings for the full story."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"file": "src/convobox/orchestrator/orchestrator.py",
|
|
96
|
+
"line": 86,
|
|
97
|
+
"pattern": "def _on_event\\(self, event: BackendEvent\\)",
|
|
98
|
+
"description": "**Backend response → speech.** `_on_event` is where a `TEXT` event from the backend gets turned into spoken audio: strip code blocks (`strip_code_for_speech`, defined at the top of this file) so raw diffs aren't read aloud, then fire synthesis as a **background task**, not awaited inline.\n\nWhy fire-and-forget: synthesis can take noticeably longer than draining the *next* backend event (e.g. a DONE right behind this TEXT), and keeping `is_busy()` fresh matters more than serializing speech with event consumption. `AudioPlayer.play()` (next stage) is itself non-blocking, so this task's only real work is the `synthesize()` await."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"file": "src/convobox/tts/base.py",
|
|
102
|
+
"line": 14,
|
|
103
|
+
"pattern": "def sanitize_text\\(text: str\\)",
|
|
104
|
+
"description": "**The security boundary before any synthesis happens.** `sanitize_text` strips control characters and caps length on text that originates from **untrusted LLM output**. Every concrete `TTSEngine` must call this before synthesizing — this exists directly because of a real command-injection bug in an earlier related project (`voice-opencode`), where response text was interpolated straight into a shell command. See docs/LESSONS-FROM-VOICE-OPENCODE.md."
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"file": "src/convobox/tts/base.py",
|
|
108
|
+
"line": 42,
|
|
109
|
+
"pattern": "def synthesize_stream\\(self, text: str\\) -> AsyncIterator",
|
|
110
|
+
"description": "**Streaming is the primary interface; `synthesize()` is the convenience wrapper.** `synthesize_stream` is the abstract method concrete engines implement. `synthesize()` just below it concatenates the stream — prefer `synthesize_stream` directly when time-to-first-audio matters, since it lets a caller start playback on the first chunk instead of waiting for the whole utterance."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"file": "src/convobox/tts/piper.py",
|
|
114
|
+
"line": 51,
|
|
115
|
+
"pattern": "async def synthesize_stream\\(self, text: str\\)",
|
|
116
|
+
"description": "**Stage 6: local TTS, genuinely streaming.** Piper's own `synthesize()` is a **blocking sync generator**. This bridges it to async the same way `MicrophoneStream.stream()` does — a background thread pushes converted float32 chunks onto a queue, and `synthesize_stream` yields each one via `asyncio.to_thread(chunk_queue.get)` as soon as it's ready.\n\n**Data handoff:** sanitized text → a stream of float32 PCM chunks. This used to buffer the *entire* response into one list before returning anything — measured improvement from switching to real streaming: **first chunk at ~143ms vs. ~1574ms total** on a 20-sentence passage (~11x faster time-to-first-audio)."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"file": "src/convobox/orchestrator/orchestrator.py",
|
|
120
|
+
"line": 225,
|
|
121
|
+
"pattern": "async def _speak\\(self, text: str\\)",
|
|
122
|
+
"description": "**Final handoff: synthesized audio → speakers, genuinely streamed.** `_speak` calls `self._player.play_stream(self._tts.synthesize_stream(text), ...)` — chunks flow from Piper to the speakers as they're produced, not buffered into one array first. (This step used to describe a whole-utterance `synthesize()`/`play()` call as the current state with streaming as a \"known next step\" — that step has since landed; see the '2. Review Findings' tour for the measured before/after.)\n\nAlso worth knowing from here: `Orchestrator.__init__` takes an optional `on_event` callback (any caller can observe the raw backend event stream, e.g. the `--tui` conversation view) and an optional `tier_responses` flag (speaks only the first paragraph of a multi-paragraph response, with `has_more_to_reveal()`/`speak_more()` exposing the rest for a \"tell me more\" follow-up) — both off by default, zero behavior change unless a caller opts in."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"file": "src/convobox/audio/playback.py",
|
|
126
|
+
"line": 9,
|
|
127
|
+
"pattern": "class AudioPlayer:",
|
|
128
|
+
"description": "**Stage 7: playback, with barge-in.** `AudioPlayer.stop()` is what makes barge-in possible — a response can be cut off the instant the user starts speaking again, rather than waiting for playback to finish. `Orchestrator.handle_transcript`'s hard-stop path (step 12 in this tour) calls exactly this.\n\n`play()` opens a fresh `sd.OutputStream` per call rather than reusing one — a real but modest overhead (tens of ms of device-open latency per response), deliberately left as-is for now since fixing it would mean reworking a test suite that deliberately asserts today's open/close-per-call behavior. See README → Status for the full reasoning."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"file": "src/convobox/web/app.py",
|
|
132
|
+
"description": "**A parallel subsystem, now fully wired, live, and viewable: the web UI.** `convobox/web/` (HistoryDB in `history.py`, the FastAPI app in `app.py`, the SSE fan-out in `stream.py`, `WebEventForwarder` in `bridge.py`, and a plain HTML/JS frontend in `static/index.html`) is a SQLite-history + live-event-stream server with a real page to view it, built per `docs/WEB-UI-ARCHITECTURE.md`. `scripts/run_convobox.py`'s `run()` chains a `WebEventForwarder` into `Orchestrator`'s existing `on_event` hook (no change to `Orchestrator` itself needed -- it already had this extension point) and starts a real `uvicorn.Server` as a background asyncio task whenever `web.enabled`/`--web` is set, lazily importing fastapi/uvicorn (the optional \"web\" extra) so CLI/TUI-only users never pay for it. `create_app()` mounts `static/index.html` as a Starlette `StaticFiles` catch-all at `\"/\"` -- registered LAST, after every `/api/*` route, so registration order (not special-casing) is what keeps the frontend from shadowing the API; a dedicated test asserts exactly that. `EventBroadcaster` carries plain dicts, not `BackendEvent` objects -- found while building a demo of this feature (2026-07-26) that the original wiring only ever forwarded backend responses/tool calls, never the user's OWN recognized speech that prompted them (no `BackendEvent` exists for that), so a captured session showed replies with no visible prompt. `WebEventForwarder.forward_transcript()` closes that gap -- wired into all three of run_convobox.py's `orchestrator.handle_transcript()` call sites (the mic loop, --text mode, and queued-interjection delivery). Live-verified end to end each step of the way, most recently: a real multi-turn session (transcript -> response -> tool_call -> tool_result -> response) through the actual ClaudeCodeAdapter + Orchestrator + HistoryDB, served back out over a real running uvicorn server's real HTTP API. `web.enabled` defaults off (zero behavior change for everyone else). The control plane has since grown well past view-only: `app.py`'s `/approve` endpoint (`action: Literal[\"approve\", \"deny\", \"explain\"]`) resolves a pending tool-call approval from the browser through the same `approval_bridge` a spoken approval phrase uses, plus a live `convobox.yaml` editor, stop/resume listening, and quit-the-session -- each added and individually live-verified, not built in one pass. See `docs/WEB-UI-ARCHITECTURE.md` for the full account and `docs/WEB-UI-USAGE.md` for what a user actually sees."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"title": "End of tour",
|
|
136
|
+
"description": "That's the full `src/convobox/` loop: **mic → VAD → STT → safeword → orchestrator → adapter → TTS → playback**, with a hard-stop escape hatch that can short-circuit at the orchestrator regardless of where the rest of the pipeline is.\n\n**Not covered by this tour, since it lives in `scripts/run_convobox.py`'s main loop rather than `src/convobox/`**: several more pure state-machine gates sit between the safeword check and the orchestrator call in the REAL running loop -- `ListeningGate` (pause/resume: \"stop listening\" hard-stops and pauses, only the wake word resumes), `BargeInMonitor` + `QueuedInterjection` (the two-axis interrupt-preset grid: what happens to the current turn vs. the user's new words), and `ContinuePromptGate` (the response-tiering silence-timeout gate: \"continue\"/\"go on\" reveals the rest of a tiered response, silence implies done). All follow the exact same pure-state-machine shape as the classes this tour walked through, unit-tested independent of real audio/clocks -- see `docs/DESIGN-barge-in.md` and `docs/DESIGN-0.3.0-interaction-and-safety.md` for the design reasoning, not re-derived here.\n\nTwo follow-up tours: **\"2. Review Findings: Security & Performance\"** for the concrete bugs a review pass found and fixed in this exact code, and **\"3. Extension Points: Modularity & Pluggability\"** for exactly where a collaborator can plug in a new backend adapter or TTS engine (and where they deliberately can't). Command Palette → CodeTour: Start Tour."
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|