dulus 0.2.53__tar.gz → 0.2.54__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.
- {dulus-0.2.53/dulus.egg-info → dulus-0.2.54}/PKG-INFO +1 -1
- {dulus-0.2.53 → dulus-0.2.54/dulus.egg-info}/PKG-INFO +1 -1
- {dulus-0.2.53 → dulus-0.2.54}/dulus.egg-info/SOURCES.txt +3 -1
- {dulus-0.2.53 → dulus-0.2.54}/dulus.py +333 -2
- {dulus-0.2.53 → dulus-0.2.54}/input.py +73 -19
- {dulus-0.2.53 → dulus-0.2.54}/pyproject.toml +1 -1
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_voice.py +56 -0
- {dulus-0.2.53 → dulus-0.2.54}/voice/__init__.py +8 -0
- dulus-0.2.54/voice/audio_utils.py +37 -0
- {dulus-0.2.53 → dulus-0.2.54}/voice/recorder.py +11 -7
- {dulus-0.2.53 → dulus-0.2.54}/voice/stt.py +20 -0
- {dulus-0.2.53 → dulus-0.2.54}/voice/tts.py +109 -36
- dulus-0.2.54/voice/wake_word.py +371 -0
- {dulus-0.2.53 → dulus-0.2.54}/LICENSE +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/MANIFEST.in +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/README.md +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/agent.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/agents_bridge.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/compressor.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/context.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/githook.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/marketplace.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/personas.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/plugins.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/server.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/backend/tasks.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/batch_api.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/checkpoint/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/checkpoint/hooks.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/checkpoint/store.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/checkpoint/types.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/claude_code_watcher.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/clipboard_utils.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/cloudsave.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/common.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/compaction.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/config.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/context.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/active_persona.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/context.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/marketplace.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/personas.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/data/tasks.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/README.md +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/api.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/architecture.md +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/dashboard/index.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/divider.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/generate.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/hero.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/index.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/news.md +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/particle-playground.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/personas/index.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/poetry-banner.png +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/preview.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-agents.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-features.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-memory.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-models.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-perms.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/spinners.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/split-pane.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus_gui.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus_mcp/client.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus_mcp/config.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/dulus_mcp/types.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/agent_bridge.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/chat_widget.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/main_window.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/personas.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/session_utils.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/settings_dialog.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/sidebar.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/tasks_view.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/themes.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/gui/tool_panel.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/license_manager.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/audit.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/consolidator.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/context.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/offload.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/palace.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/scan.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/sessions.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/store.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/types.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/memory/vector_search.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/multi_agent/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/multi_agent/subagent.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/multi_agent/tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/offload_helper.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/plugin/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/plugin/autoadapter.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/plugin/loader.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/plugin/recommend.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/plugin/store.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/plugin/types.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/providers.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/README.md +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/components.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/index.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/wallpaper-default.jpg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/wallpapers/default.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/wallpapers/light.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/dist/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/eslint.config.js +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/index.html +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/info.md +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/package-lock.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/package.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/postcss.config.js +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/public/wallpaper-default.jpg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/public/wallpapers/default.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/public/wallpapers/light.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/public/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/public/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/App.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/AgentMonitor.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ApiTester.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/AppRouter.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ArchiveManager.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/AsciiArt.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Base64Tool.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Browser.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Calculator.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Calendar.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Chat.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Chess.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Clock.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/CodeEditor.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ColorPalette.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ColorPicker.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Contacts.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/DocumentViewer.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Drawing.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Email.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/FileManager.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/FlappyBird.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/FtpClient.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Game2048.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/GitClient.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ImageGallery.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ImageViewer.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/JsonFormatter.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/MatrixRain.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/MediaConverter.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Memory.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/MemoryManager.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Minesweeper.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/MusicPlayer.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/NetworkTools.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Notes.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/PasswordManager.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/PhotoEditor.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Pong.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/RegexTester.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Reminders.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/RssReader.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Settings.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Snake.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Solitaire.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Spreadsheet.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Sudoku.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/SystemMonitor.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/TaskManager.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Terminal.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Tetris.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/TextEditor.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/TicTacToe.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Todo.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/VideoPlayer.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Weather.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/Whiteboard.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/apps/registry.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/AppContainer.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/AppLauncher.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/BootSequence.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ContextMenu.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/Desktop.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/Dock.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/LoginScreen.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/NotImplemented.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/NotificationCenter.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/NotificationSystem.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/TopPanel.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/WindowFrame.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/WindowManager.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/accordion.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/alert.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/avatar.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/badge.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/button-group.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/button.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/calendar.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/card.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/carousel.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/chart.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/checkbox.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/collapsible.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/command.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/context-menu.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/dialog.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/drawer.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/empty.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/field.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/form.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/hover-card.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/input-group.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/input-otp.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/input.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/item.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/kbd.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/label.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/menubar.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/pagination.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/popover.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/progress.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/radio-group.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/resizable.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/scroll-area.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/select.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/separator.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/sheet.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/sidebar.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/skeleton.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/slider.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/sonner.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/spinner.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/switch.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/table.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/tabs.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/textarea.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/toggle-group.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/toggle.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/components/ui/tooltip.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/index.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/use-mobile.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusAgents.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusChat.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusEvents.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusHealth.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusMemory.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusSkills.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useDulusTasks.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useFileSystem.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useOSStore.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useSkillBridge.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useSystemBattery.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/hooks/useSystemVolume.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/index.css +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/lib/dulus-api.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/lib/utils.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/main.tsx +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/types/index.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/src/utils/assets.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/tailwind.config.js +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/tsconfig.app.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/tsconfig.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/tsconfig.node.json +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/sandbox/vite.config.ts +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/setup.cfg +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skill/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skill/builtin.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skill/clawhub.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skill/executor.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skill/loader.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skill/tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/skills.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/spinner.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/string_utils.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/subagent.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/task/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/task/store.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/task/tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/task/types.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_afk_yolo.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_approval_runtime.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_background_task_tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_background_tasks.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_clipboard_utils.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_compaction.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_diff_view.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_diff_visualization.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_display_blocks.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_export_import.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_hook_engine.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_license.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_mcp.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_memory.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_notification_manager.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_plugin.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_session_fork.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_shell_mode.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_skills.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_steer_input.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_subagent.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_task.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_think_tool.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_todo_tool.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_todo_visualization.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tests/test_wire_events.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tmux_offloader.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tmux_tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tool_registry.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/tools.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/ui/__init__.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/ui/input.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/ui/render.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/voice/keyterms.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/webchat.py +0 -0
- {dulus-0.2.53 → dulus-0.2.54}/webchat_server.py +0 -0
|
@@ -90,6 +90,12 @@ Slash commands in REPL:
|
|
|
90
90
|
/voice Record voice input, transcribe, and submit
|
|
91
91
|
/voice status Show available recording and STT backends
|
|
92
92
|
/voice lang <code> Set STT language (e.g. zh, en, ja — default: auto)
|
|
93
|
+
/wake on|off Toggle wake-word (hotword) detection — say "Hey Dulus"
|
|
94
|
+
/wake status Show wake-word listener state
|
|
95
|
+
/wake phrases List recognised wake phrases
|
|
96
|
+
/wake calibrate Measure your mic levels for 5s and suggest a threshold
|
|
97
|
+
/wake test Debug mode — shows RMS + STT output for 10 seconds
|
|
98
|
+
/wake threshold <n> Tune mic sensitivity (0.001-1.0, default 0.020)
|
|
93
99
|
/proactive [dur] Background sentinel polling (e.g. /proactive 5m)
|
|
94
100
|
/proactive off Disable proactive polling
|
|
95
101
|
/cloudsave setup <token> Configure GitHub token for cloud sync
|
|
@@ -193,7 +199,7 @@ import time
|
|
|
193
199
|
import uuid
|
|
194
200
|
from datetime import datetime
|
|
195
201
|
from pathlib import Path
|
|
196
|
-
from typing import Optional, Union
|
|
202
|
+
from typing import Optional, Union, Any, Callable
|
|
197
203
|
|
|
198
204
|
if sys.platform == "win32":
|
|
199
205
|
os.system("") # Enable ANSI escape codes on Windows CMD
|
|
@@ -736,6 +742,7 @@ def cmd_help(_args: str, _state, config) -> bool:
|
|
|
736
742
|
("lite_mode", False, "/lite", "Lite mode (smaller system prompt)"),
|
|
737
743
|
("brave_search_enabled", False, "/brave", "Brave Search API integration"),
|
|
738
744
|
("tts_enabled", False, "/tts", "Automatic Text-to-Speech"),
|
|
745
|
+
("wake_enabled", False, "/wake", "Wake-word hotword detection (Hey Dulus)"),
|
|
739
746
|
("daemon", False, "/daemon", "Allow external triggers when no REPL is open"),
|
|
740
747
|
("afk_mode", False, "/afk", "AFK mode (auto-dismiss, auto-approve tools)"),
|
|
741
748
|
("yolo_mode", False, "/yolo", "YOLO mode (auto-approve ALL actions)"),
|
|
@@ -6426,6 +6433,230 @@ def cmd_voice(args: str, state, config) -> bool:
|
|
|
6426
6433
|
return ("__voice__", text)
|
|
6427
6434
|
|
|
6428
6435
|
|
|
6436
|
+
# Global handle to the running wake-word listener (managed by repl)
|
|
6437
|
+
_wake_listener: Any = None
|
|
6438
|
+
|
|
6439
|
+
|
|
6440
|
+
def cmd_wake(args: str, state, config) -> bool:
|
|
6441
|
+
"""Wake-word (hotword) detection — hands-free voice activation.
|
|
6442
|
+
|
|
6443
|
+
/wake on — start listening for "Hey Dulus" in background
|
|
6444
|
+
/wake off — stop the background listener
|
|
6445
|
+
/wake status — show whether listener is active
|
|
6446
|
+
/wake phrases — list recognised wake phrases
|
|
6447
|
+
/wake threshold <0.01-0.20> — tune mic sensitivity (default 0.035)
|
|
6448
|
+
"""
|
|
6449
|
+
global _wake_listener
|
|
6450
|
+
|
|
6451
|
+
subcmd = args.strip().lower().split()[0] if args.strip() else ""
|
|
6452
|
+
rest = args.strip()[len(subcmd):].strip()
|
|
6453
|
+
|
|
6454
|
+
# ── /wake threshold ──
|
|
6455
|
+
if subcmd == "threshold":
|
|
6456
|
+
if not rest:
|
|
6457
|
+
current = config.get("wake_threshold", 0.020)
|
|
6458
|
+
info(f"Current wake threshold: {current} (higher = less sensitive)")
|
|
6459
|
+
return True
|
|
6460
|
+
try:
|
|
6461
|
+
val = float(rest)
|
|
6462
|
+
if not 0.001 <= val <= 1.0:
|
|
6463
|
+
raise ValueError
|
|
6464
|
+
config["wake_threshold"] = val
|
|
6465
|
+
ok(f"Wake threshold set to {val}")
|
|
6466
|
+
try:
|
|
6467
|
+
from config import save_config
|
|
6468
|
+
save_config(config)
|
|
6469
|
+
except Exception as e:
|
|
6470
|
+
warn(f"Could not save config: {e}")
|
|
6471
|
+
except ValueError:
|
|
6472
|
+
err("Threshold must be a number between 0.001 and 1.0")
|
|
6473
|
+
return True
|
|
6474
|
+
|
|
6475
|
+
# ── /wake phrases ──
|
|
6476
|
+
if subcmd == "phrases":
|
|
6477
|
+
try:
|
|
6478
|
+
from voice.wake_word import WAKE_PHRASES
|
|
6479
|
+
except ImportError:
|
|
6480
|
+
err("voice/wake_word.py not found")
|
|
6481
|
+
return True
|
|
6482
|
+
print(clr(" Recognised wake phrases:", "cyan", "bold"))
|
|
6483
|
+
for p in WAKE_PHRASES:
|
|
6484
|
+
print(f" • {p}")
|
|
6485
|
+
return True
|
|
6486
|
+
|
|
6487
|
+
# ── /wake calibrate ──
|
|
6488
|
+
if subcmd == "calibrate":
|
|
6489
|
+
try:
|
|
6490
|
+
from voice import check_voice_deps
|
|
6491
|
+
except ImportError:
|
|
6492
|
+
err("voice package not available")
|
|
6493
|
+
return True
|
|
6494
|
+
available, reason = check_voice_deps()
|
|
6495
|
+
if not available:
|
|
6496
|
+
err(f"Voice deps missing: {reason}")
|
|
6497
|
+
return True
|
|
6498
|
+
print(clr(" 🎙 Calibrating mic — speak normally for 5 seconds…", "cyan"))
|
|
6499
|
+
print(clr(" Press Ctrl+C when done.\n", "dim"))
|
|
6500
|
+
try:
|
|
6501
|
+
import sounddevice as sd
|
|
6502
|
+
import numpy as np
|
|
6503
|
+
_chunk = int(16000 * 0.3)
|
|
6504
|
+
_bars = " ▁▂▃▄▅▆▇█"
|
|
6505
|
+
_max_rms = 0.0
|
|
6506
|
+
with sd.InputStream(
|
|
6507
|
+
samplerate=16000, channels=1, dtype="int16",
|
|
6508
|
+
blocksize=_chunk,
|
|
6509
|
+
device=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
6510
|
+
) as stream:
|
|
6511
|
+
import time as _time
|
|
6512
|
+
_t0 = _time.monotonic()
|
|
6513
|
+
while _time.monotonic() - _t0 < 5.0:
|
|
6514
|
+
pcm, _ = stream.read(_chunk)
|
|
6515
|
+
arr = np.frombuffer(pcm.tobytes(), dtype=np.int16).astype(np.float32)
|
|
6516
|
+
if arr.size:
|
|
6517
|
+
rms = float(np.sqrt(np.mean(arr ** 2))) / 32768.0
|
|
6518
|
+
_max_rms = max(_max_rms, rms)
|
|
6519
|
+
lvl = min(int(rms * 8 / 0.08), 8)
|
|
6520
|
+
bar = _bars[lvl]
|
|
6521
|
+
print(f"\r RMS: {rms:.4f} {bar} (max {_max_rms:.4f})", end="", flush=True)
|
|
6522
|
+
_time.sleep(0.05)
|
|
6523
|
+
print()
|
|
6524
|
+
print(clr(f"\n Max RMS detected: {_max_rms:.4f}", "cyan", "bold"))
|
|
6525
|
+
rec = _max_rms * 0.7
|
|
6526
|
+
if rec < 0.005:
|
|
6527
|
+
rec = 0.010
|
|
6528
|
+
info(f" Recommended threshold: ~{rec:.3f}")
|
|
6529
|
+
info(f" Current threshold: {config.get('wake_threshold', 0.020)}")
|
|
6530
|
+
info(" Use '/wake threshold <n>' to adjust.")
|
|
6531
|
+
except KeyboardInterrupt:
|
|
6532
|
+
print()
|
|
6533
|
+
except Exception as e:
|
|
6534
|
+
err(f"Calibration failed: {e}")
|
|
6535
|
+
return True
|
|
6536
|
+
|
|
6537
|
+
# ── /wake test ──
|
|
6538
|
+
if subcmd == "test":
|
|
6539
|
+
try:
|
|
6540
|
+
from voice import check_voice_deps
|
|
6541
|
+
from voice.wake_word import WakeWordListener
|
|
6542
|
+
except ImportError as e:
|
|
6543
|
+
err(f"Wake-word module not available: {e}")
|
|
6544
|
+
return True
|
|
6545
|
+
available, reason = check_voice_deps()
|
|
6546
|
+
if not available:
|
|
6547
|
+
err(f"Voice input not available:\n{reason}")
|
|
6548
|
+
return True
|
|
6549
|
+
print(clr(" 🎙 Wake-word TEST mode — debug output ON for 10 seconds", "cyan", "bold"))
|
|
6550
|
+
print(clr(" Say 'Hey Dulus' now. Press Ctrl+C to stop early.\n", "dim"))
|
|
6551
|
+
_test_listener = WakeWordListener(
|
|
6552
|
+
rms_threshold=config.get("wake_threshold", 0.020),
|
|
6553
|
+
device_index=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
6554
|
+
language=_voice_language,
|
|
6555
|
+
debug=True,
|
|
6556
|
+
)
|
|
6557
|
+
_found: list[str] = []
|
|
6558
|
+
|
|
6559
|
+
def _test_wake(phrase: str) -> None:
|
|
6560
|
+
print(clr(f"\n ✅ WAKE DETECTED: '{phrase}'", "green", "bold"))
|
|
6561
|
+
|
|
6562
|
+
def _test_cmd(text: str) -> None:
|
|
6563
|
+
_found.append(text)
|
|
6564
|
+
print(clr(f'\n 🎙️ COMMAND: "{text}"', "green", "bold"))
|
|
6565
|
+
|
|
6566
|
+
_test_listener.start(on_wake=_test_wake, on_command=_test_cmd)
|
|
6567
|
+
try:
|
|
6568
|
+
import time as _time
|
|
6569
|
+
_time.sleep(10)
|
|
6570
|
+
except KeyboardInterrupt:
|
|
6571
|
+
print()
|
|
6572
|
+
finally:
|
|
6573
|
+
_test_listener.stop()
|
|
6574
|
+
if not _found:
|
|
6575
|
+
warn(" No wake word detected in 10s. Try '/wake calibrate' to check your mic levels.")
|
|
6576
|
+
return True
|
|
6577
|
+
|
|
6578
|
+
# ── /wake status ──
|
|
6579
|
+
if subcmd == "status":
|
|
6580
|
+
try:
|
|
6581
|
+
from voice import check_voice_deps
|
|
6582
|
+
except ImportError:
|
|
6583
|
+
err("voice package not available")
|
|
6584
|
+
return True
|
|
6585
|
+
available, reason = check_voice_deps()
|
|
6586
|
+
if not available:
|
|
6587
|
+
err(f"Voice deps missing: {reason}")
|
|
6588
|
+
return True
|
|
6589
|
+
active = _wake_listener is not None and getattr(_wake_listener, "is_running", lambda: False)()
|
|
6590
|
+
state_str = clr("ACTIVE", "green", "bold") if active else clr("OFF", "gray")
|
|
6591
|
+
info(f"Wake-word listener: {state_str}")
|
|
6592
|
+
info(f"Threshold: {config.get('wake_threshold', 0.020)}")
|
|
6593
|
+
if active:
|
|
6594
|
+
info("Say 'Hey Dulus' followed by your command.")
|
|
6595
|
+
else:
|
|
6596
|
+
info("Use '/wake on' to start listening.")
|
|
6597
|
+
return True
|
|
6598
|
+
|
|
6599
|
+
# ── /wake off ──
|
|
6600
|
+
if subcmd in ["off", "false", "disable", "stop"]:
|
|
6601
|
+
config["wake_enabled"] = False
|
|
6602
|
+
try:
|
|
6603
|
+
from config import save_config
|
|
6604
|
+
save_config(config)
|
|
6605
|
+
except Exception:
|
|
6606
|
+
pass
|
|
6607
|
+
if _wake_listener is not None:
|
|
6608
|
+
try:
|
|
6609
|
+
_wake_listener.stop()
|
|
6610
|
+
except Exception as e:
|
|
6611
|
+
warn(f"Error stopping wake listener: {e}")
|
|
6612
|
+
_wake_listener = None
|
|
6613
|
+
ok("Wake-word listener stopped.")
|
|
6614
|
+
else:
|
|
6615
|
+
info("Wake-word listener was not running.")
|
|
6616
|
+
return True
|
|
6617
|
+
|
|
6618
|
+
# ── /wake on ──
|
|
6619
|
+
if subcmd in ["on", "true", "enable", "start"]:
|
|
6620
|
+
if _wake_listener is not None and getattr(_wake_listener, "is_running", lambda: False)():
|
|
6621
|
+
info("Wake-word listener is already active.")
|
|
6622
|
+
return True
|
|
6623
|
+
|
|
6624
|
+
try:
|
|
6625
|
+
from voice import check_voice_deps
|
|
6626
|
+
from voice.wake_word import WakeWordListener
|
|
6627
|
+
except ImportError as e:
|
|
6628
|
+
err(f"Wake-word module not available: {e}")
|
|
6629
|
+
return True
|
|
6630
|
+
|
|
6631
|
+
available, reason = check_voice_deps()
|
|
6632
|
+
if not available:
|
|
6633
|
+
err(f"Voice input not available:\n{reason}")
|
|
6634
|
+
return True
|
|
6635
|
+
|
|
6636
|
+
# The actual on_wake / on_command callbacks are injected by repl()
|
|
6637
|
+
# via the _wake_listener global handle. Here we just create the
|
|
6638
|
+
# object; repl() wires the queue when it sees the handle change.
|
|
6639
|
+
_wake_listener = WakeWordListener(
|
|
6640
|
+
rms_threshold=config.get("wake_threshold", 0.035),
|
|
6641
|
+
device_index=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
6642
|
+
language=_voice_language,
|
|
6643
|
+
)
|
|
6644
|
+
config["wake_enabled"] = True
|
|
6645
|
+
try:
|
|
6646
|
+
from config import save_config
|
|
6647
|
+
save_config(config)
|
|
6648
|
+
except Exception:
|
|
6649
|
+
pass
|
|
6650
|
+
ok("Wake-word listener starting… Say 'Hey Dulus' to activate.")
|
|
6651
|
+
return True
|
|
6652
|
+
|
|
6653
|
+
# ── Toggle ──
|
|
6654
|
+
if _wake_listener is not None and getattr(_wake_listener, "is_running", lambda: False)():
|
|
6655
|
+
return cmd_wake("off", state, config)
|
|
6656
|
+
else:
|
|
6657
|
+
return cmd_wake("on", state, config)
|
|
6658
|
+
|
|
6659
|
+
|
|
6429
6660
|
def cmd_image(args: str, state, config) -> Union[bool, tuple]:
|
|
6430
6661
|
"""Grab image from clipboard and send to vision model with optional prompt."""
|
|
6431
6662
|
import sys as _sys
|
|
@@ -7539,6 +7770,7 @@ COMMANDS = {
|
|
|
7539
7770
|
"lite": cmd_lite,
|
|
7540
7771
|
"cloudsave": cmd_cloudsave,
|
|
7541
7772
|
"voice": cmd_voice,
|
|
7773
|
+
"wake": cmd_wake,
|
|
7542
7774
|
"git": cmd_git,
|
|
7543
7775
|
"webchat": cmd_webchat,
|
|
7544
7776
|
"sandbox": cmd_sandbox,
|
|
@@ -7650,6 +7882,7 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
|
|
|
7650
7882
|
"cloudsave": ("Cloud-sync sessions to GitHub Gist", ["setup", "auto", "list", "load", "push"]),
|
|
7651
7883
|
"tts": ("Toggle automatic TTS + lang/provider/auto", ["lang", "provider", "voice", "auto"]),
|
|
7652
7884
|
"voice": ("Voice input (record → STT)", ["lang", "status", "device"]),
|
|
7885
|
+
"wake": ("Wake-word hotword detection", ["on", "off", "status", "phrases", "calibrate", "test", "threshold"]),
|
|
7653
7886
|
"image": ("Send clipboard image to model", []),
|
|
7654
7887
|
"img": ("Send clipboard image (alias)", []),
|
|
7655
7888
|
"batch": ("Manage Kimi Batch tasks", ["status", "list", "fetch"]),
|
|
@@ -7781,6 +8014,10 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
7781
8014
|
verbose = config.get("verbose", False)
|
|
7782
8015
|
config["_tg_send_callback"] = _tg_send
|
|
7783
8016
|
|
|
8017
|
+
# ── Wake-word queue ──
|
|
8018
|
+
import queue as _queue
|
|
8019
|
+
_wake_queue: "_queue.Queue[str]" = _queue.Queue()
|
|
8020
|
+
|
|
7784
8021
|
def _render_toolbar() -> str:
|
|
7785
8022
|
"""Return ANSI toolbar string for prompt_toolkit bottom bar.
|
|
7786
8023
|
|
|
@@ -8876,6 +9113,88 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
8876
9113
|
"Please review the results and report back to the user.")
|
|
8877
9114
|
except Exception:
|
|
8878
9115
|
pass
|
|
9116
|
+
|
|
9117
|
+
# ── Wake-word listener lifecycle ──
|
|
9118
|
+
global _wake_listener
|
|
9119
|
+
|
|
9120
|
+
# [Autostart] If enabled in config but listener not created yet
|
|
9121
|
+
if _wake_listener is None and config.get("wake_enabled"):
|
|
9122
|
+
try:
|
|
9123
|
+
from voice.wake_word import WakeWordListener
|
|
9124
|
+
_wake_listener = WakeWordListener(
|
|
9125
|
+
rms_threshold=config.get("wake_threshold", 0.035),
|
|
9126
|
+
device_index=config.get("voice_device_index", config.get("_voice_device_index")),
|
|
9127
|
+
language=_voice_language,
|
|
9128
|
+
)
|
|
9129
|
+
# Pre-load Whisper in background so detection is fast
|
|
9130
|
+
def _preload():
|
|
9131
|
+
try:
|
|
9132
|
+
from voice.stt import _get_faster_whisper_model
|
|
9133
|
+
_get_faster_whisper_model()
|
|
9134
|
+
except Exception:
|
|
9135
|
+
pass
|
|
9136
|
+
threading.Thread(target=_preload, daemon=True).start()
|
|
9137
|
+
except ImportError:
|
|
9138
|
+
pass
|
|
9139
|
+
|
|
9140
|
+
if _wake_listener is not None and not _wake_listener.is_running():
|
|
9141
|
+
def _on_wake(phrase: str) -> None:
|
|
9142
|
+
# Use safe_print_notification to avoid ghosting/re-printing in sticky mode
|
|
9143
|
+
import input as _dulus_input
|
|
9144
|
+
_dulus_input.safe_print_notification(clr(f"\x1b[32m ✅ WAKE DETECTED: '{phrase}'\x1b[0m", "bold"))
|
|
9145
|
+
_dulus_input.set_toolbar_status(clr("Waking up...", "cyan"))
|
|
9146
|
+
|
|
9147
|
+
# Immediate audible feedback — universal beep
|
|
9148
|
+
try:
|
|
9149
|
+
from voice import beep
|
|
9150
|
+
beep(880, 150)
|
|
9151
|
+
except Exception:
|
|
9152
|
+
pass
|
|
9153
|
+
# TTS feedback
|
|
9154
|
+
# NOTE: say() is blocking, which correctly delays the command recording
|
|
9155
|
+
# in WakeWordListener until after the response is finished.
|
|
9156
|
+
try:
|
|
9157
|
+
from voice import say
|
|
9158
|
+
_resp = config.get("wake_response", "¿Dime, papi?")
|
|
9159
|
+
say(_resp, provider=config.get("tts_provider", "auto"))
|
|
9160
|
+
except Exception:
|
|
9161
|
+
pass
|
|
9162
|
+
|
|
9163
|
+
def _on_command(text: str) -> None:
|
|
9164
|
+
# Filter common Whisper hallucinations on silence/noise
|
|
9165
|
+
# NOTE: We allow "gracias" as it's a valid thing a user might say.
|
|
9166
|
+
_HALLUC = {
|
|
9167
|
+
"thank you.", "thank you", "thanks for watching.",
|
|
9168
|
+
"thanks for watching!", "thanks.", ".", "you",
|
|
9169
|
+
"subtitles by the amara.org community",
|
|
9170
|
+
}
|
|
9171
|
+
_norm = text.strip().lower()
|
|
9172
|
+
if not _norm or _norm in _HALLUC:
|
|
9173
|
+
# Ignore hallucinations silently
|
|
9174
|
+
import input as _dulus_input
|
|
9175
|
+
_dulus_input.set_toolbar_status("")
|
|
9176
|
+
return
|
|
9177
|
+
|
|
9178
|
+
# Always put in queue so the main loop can pick it up
|
|
9179
|
+
_wake_queue.put(text)
|
|
9180
|
+
|
|
9181
|
+
# Signal the active prompt to exit (unblocks dulus_input.read_line_split)
|
|
9182
|
+
import input as _dulus_input
|
|
9183
|
+
_dulus_input.request_exit()
|
|
9184
|
+
|
|
9185
|
+
_dulus_input.set_toolbar_status("") # Clear toolbar on success
|
|
9186
|
+
_dulus_input.safe_print_notification(clr(f"\n 🎙️ COMMAND: \"{text}\"", "cyan", "bold"))
|
|
9187
|
+
|
|
9188
|
+
_wake_listener.start(on_wake=_on_wake, on_command=_on_command)
|
|
9189
|
+
|
|
9190
|
+
# ── Check for wake-word command before blocking on keyboard ──
|
|
9191
|
+
user_input = ""
|
|
9192
|
+
_wake_cmd: str | None = None
|
|
9193
|
+
try:
|
|
9194
|
+
_wake_cmd = _wake_queue.get_nowait()
|
|
9195
|
+
except _queue.Empty:
|
|
9196
|
+
_wake_cmd = None
|
|
9197
|
+
|
|
8879
9198
|
try:
|
|
8880
9199
|
cwd_short = Path.cwd().name
|
|
8881
9200
|
# Live context-usage indicator: "[73%]" — green<60, yellow<85, red otherwise.
|
|
@@ -8900,7 +9219,12 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
8900
9219
|
prompt = _rl_safe(clr(f"\n[{cwd_short}] ", "dim") + ctx_tag + clr("» ", "cyan", "bold"))
|
|
8901
9220
|
if in_batch_mode:
|
|
8902
9221
|
prompt = _rl_safe(clr(f" batch[{len(batch_buffer)}] » ", "yellow", "bold"))
|
|
8903
|
-
|
|
9222
|
+
|
|
9223
|
+
if _wake_cmd is not None:
|
|
9224
|
+
user_input = _wake_cmd
|
|
9225
|
+
import input as _dulus_input
|
|
9226
|
+
_dulus_input.safe_print_notification(clr(f"\n 🦅 [Wake] » {user_input}\n", "green", "bold"))
|
|
9227
|
+
elif config.pop("_auto_voice_next", False) and not in_batch_mode:
|
|
8904
9228
|
print(clr(" 🎙 [auto-voice] Listening… (Ctrl+C to type instead)", "cyan"))
|
|
8905
9229
|
try:
|
|
8906
9230
|
from voice import voice_input as _av_voice_input
|
|
@@ -8935,6 +9259,13 @@ def repl(config: dict, initial_prompt: str = None):
|
|
|
8935
9259
|
user_input = _read_input(prompt)
|
|
8936
9260
|
except (EOFError, KeyboardInterrupt):
|
|
8937
9261
|
print()
|
|
9262
|
+
# ── Stop wake-word listener on exit ──
|
|
9263
|
+
try:
|
|
9264
|
+
if _wake_listener is not None:
|
|
9265
|
+
_wake_listener.stop()
|
|
9266
|
+
globals()["_wake_listener"] = None
|
|
9267
|
+
except Exception:
|
|
9268
|
+
pass
|
|
8938
9269
|
# ── Sleep Trigger: Ask to consolidate before final exit ─────────
|
|
8939
9270
|
try:
|
|
8940
9271
|
# Only ask if there's actually a session worth saving
|
|
@@ -17,7 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from typing import Any, Callable, Optional
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
|
-
from prompt_toolkit import PromptSession
|
|
20
|
+
from prompt_toolkit import PromptSession, Application
|
|
21
21
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
22
22
|
from prompt_toolkit.completion import (
|
|
23
23
|
Completer, Completion, FuzzyCompleter, WordCompleter, merge_completers,
|
|
@@ -49,6 +49,8 @@ except ImportError:
|
|
|
49
49
|
_commands_provider: Optional[Callable[[], dict]] = None
|
|
50
50
|
_meta_provider: Optional[Callable[[], dict]] = None
|
|
51
51
|
_toolbar_provider: Optional[Callable[[], str]] = None
|
|
52
|
+
_toolbar_status: str = "" # Background status (e.g. wake energy bar)
|
|
53
|
+
_active_app: Optional["Application"] = None # Track currently running prompt-toolkit app
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
_TOOLBAR_SENTINEL = object()
|
|
@@ -337,13 +339,22 @@ def _build_session(history_path: Optional[Path]):
|
|
|
337
339
|
|
|
338
340
|
def _bottom_toolbar():
|
|
339
341
|
provider = _toolbar_provider
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
342
|
+
text = ""
|
|
343
|
+
if provider is not None:
|
|
344
|
+
try:
|
|
345
|
+
text = provider()
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
# Inject status (e.g. energy bar) after the main toolbar text
|
|
350
|
+
status = _toolbar_status
|
|
351
|
+
if status:
|
|
352
|
+
if text:
|
|
353
|
+
text += " | " + status
|
|
354
|
+
else:
|
|
355
|
+
text = status
|
|
356
|
+
|
|
357
|
+
return ANSI(text) if text else ""
|
|
347
358
|
|
|
348
359
|
return PromptSession(
|
|
349
360
|
history=history,
|
|
@@ -365,7 +376,7 @@ def read_line(prompt_ansi: str, history_path: Optional[Path] = None) -> str:
|
|
|
365
376
|
two line-editors use incompatible formats. See Dulus REPL for the
|
|
366
377
|
dedicated PT_HISTORY_FILE.
|
|
367
378
|
"""
|
|
368
|
-
global _SESSION, _SESSION_HISTORY_PATH, _notification_callback
|
|
379
|
+
global _SESSION, _SESSION_HISTORY_PATH, _notification_callback, _active_app
|
|
369
380
|
|
|
370
381
|
# Drain any pending background notifications before showing prompt
|
|
371
382
|
notifications = drain_notifications()
|
|
@@ -395,7 +406,11 @@ def read_line(prompt_ansi: str, history_path: Optional[Path] = None) -> str:
|
|
|
395
406
|
_sys.stdout.flush()
|
|
396
407
|
|
|
397
408
|
with patch_stdout(raw=True):
|
|
398
|
-
|
|
409
|
+
try:
|
|
410
|
+
_active_app = _SESSION.app
|
|
411
|
+
result = _SESSION.prompt(ANSI(prompt_ansi))
|
|
412
|
+
finally:
|
|
413
|
+
_active_app = None
|
|
399
414
|
|
|
400
415
|
_sys.stdout.write("\0338\033[J") # DEC restore cursor (ESC 8) → erase to end
|
|
401
416
|
_sys.stdout.flush()
|
|
@@ -566,7 +581,7 @@ def read_line_split(prompt: str = "> ", history_path: Optional[Path] = None) ->
|
|
|
566
581
|
|
|
567
582
|
Similar to Kimi Code and Claude Code interfaces.
|
|
568
583
|
"""
|
|
569
|
-
global _split_app, _split_buffer, _output_buffer, _original_stdout, _notification_callback
|
|
584
|
+
global _split_app, _split_buffer, _output_buffer, _original_stdout, _notification_callback, _active_app
|
|
570
585
|
|
|
571
586
|
# Drain any pending background notifications before showing prompt
|
|
572
587
|
# Drain notifications but don't display yet - we'll add them after creating the app
|
|
@@ -871,7 +886,13 @@ def read_line_split(prompt: str = "> ", history_path: Optional[Path] = None) ->
|
|
|
871
886
|
# Refresh to show notifications
|
|
872
887
|
_split_app.invalidate()
|
|
873
888
|
|
|
874
|
-
|
|
889
|
+
# Track as active
|
|
890
|
+
_active_app = _split_app
|
|
891
|
+
try:
|
|
892
|
+
result = _split_app.run()
|
|
893
|
+
finally:
|
|
894
|
+
_active_app = None
|
|
895
|
+
_split_app = None
|
|
875
896
|
|
|
876
897
|
# Restore stdout
|
|
877
898
|
sys.stdout = _original_stdout
|
|
@@ -987,7 +1008,7 @@ def drain_notifications() -> list[str]:
|
|
|
987
1008
|
return notifications
|
|
988
1009
|
|
|
989
1010
|
|
|
990
|
-
def safe_print_notification(text: str) -> None:
|
|
1011
|
+
def safe_print_notification(text: str, end: str = "\n", flush: bool = False) -> None:
|
|
991
1012
|
"""Print a notification in a prompt_toolkit-safe way.
|
|
992
1013
|
|
|
993
1014
|
If split layout is active, uses append_output.
|
|
@@ -995,8 +1016,9 @@ def safe_print_notification(text: str) -> None:
|
|
|
995
1016
|
"""
|
|
996
1017
|
global _split_app, _original_stdout
|
|
997
1018
|
|
|
998
|
-
#
|
|
999
|
-
|
|
1019
|
+
# We only strip if not using specific 'end' (to maintain tail control)
|
|
1020
|
+
if end == "\n":
|
|
1021
|
+
text = text.strip('\r\n')
|
|
1000
1022
|
|
|
1001
1023
|
if _split_app and getattr(_split_app, "is_running", False):
|
|
1002
1024
|
from prompt_toolkit.application.run_in_terminal import run_in_terminal
|
|
@@ -1004,12 +1026,14 @@ def safe_print_notification(text: str) -> None:
|
|
|
1004
1026
|
|
|
1005
1027
|
def _target():
|
|
1006
1028
|
if _original_stdout:
|
|
1007
|
-
_original_stdout.write(text +
|
|
1008
|
-
|
|
1029
|
+
_original_stdout.write(text + end)
|
|
1030
|
+
if flush:
|
|
1031
|
+
_original_stdout.flush()
|
|
1009
1032
|
else:
|
|
1010
1033
|
import sys
|
|
1011
|
-
sys.stdout.write(text +
|
|
1012
|
-
|
|
1034
|
+
sys.stdout.write(text + end)
|
|
1035
|
+
if flush:
|
|
1036
|
+
sys.stdout.flush()
|
|
1013
1037
|
|
|
1014
1038
|
def _schedule():
|
|
1015
1039
|
try:
|
|
@@ -1028,3 +1052,33 @@ def safe_print_notification(text: str) -> None:
|
|
|
1028
1052
|
else:
|
|
1029
1053
|
# Fallback to regular print
|
|
1030
1054
|
print(text)
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def set_toolbar_status(text: str) -> None:
|
|
1058
|
+
"""Set a short status string to be shown in the bottom toolbar.
|
|
1059
|
+
|
|
1060
|
+
Thread-safe. Automatically invalidates the display if split layout is active.
|
|
1061
|
+
Pass "" to clear.
|
|
1062
|
+
"""
|
|
1063
|
+
global _toolbar_status, _split_app
|
|
1064
|
+
_toolbar_status = text.strip().replace("\n", " ")
|
|
1065
|
+
if _split_app:
|
|
1066
|
+
# Invalidate soon via the UI thread
|
|
1067
|
+
try:
|
|
1068
|
+
_split_app.loop.call_soon_threadsafe(_split_app.invalidate)
|
|
1069
|
+
except Exception:
|
|
1070
|
+
pass
|
|
1071
|
+
def request_exit() -> bool:
|
|
1072
|
+
"""Signal the active prompt session to exit immediately.
|
|
1073
|
+
|
|
1074
|
+
Returns True if successfully signaled, False if no prompt is active.
|
|
1075
|
+
Thread-safe.
|
|
1076
|
+
"""
|
|
1077
|
+
global _active_app
|
|
1078
|
+
if _active_app and getattr(_active_app, "is_running", False):
|
|
1079
|
+
try:
|
|
1080
|
+
_active_app.loop.call_soon_threadsafe(_active_app.exit)
|
|
1081
|
+
return True
|
|
1082
|
+
except Exception:
|
|
1083
|
+
pass
|
|
1084
|
+
return False
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dulus"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.54"
|
|
8
8
|
description = "Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -238,3 +238,59 @@ class TestReplVoiceIntegration:
|
|
|
238
238
|
# Reset
|
|
239
239
|
dulus.cmd_voice("lang auto", object(), {})
|
|
240
240
|
assert dulus._voice_language == "auto"
|
|
241
|
+
|
|
242
|
+
def test_wake_in_commands(self):
|
|
243
|
+
import dulus
|
|
244
|
+
assert "wake" in dulus.COMMANDS
|
|
245
|
+
|
|
246
|
+
def test_wake_command_callable(self):
|
|
247
|
+
import dulus
|
|
248
|
+
assert callable(dulus.COMMANDS["wake"])
|
|
249
|
+
|
|
250
|
+
def test_wake_status_no_crash(self, capsys):
|
|
251
|
+
import dulus
|
|
252
|
+
try:
|
|
253
|
+
dulus.cmd_wake("status", object(), {})
|
|
254
|
+
except SystemExit:
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
def test_wake_phrases_no_crash(self, capsys):
|
|
258
|
+
import dulus
|
|
259
|
+
try:
|
|
260
|
+
dulus.cmd_wake("phrases", object(), {})
|
|
261
|
+
except SystemExit:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ── Wake-word unit tests (no hardware required) ───────────────────────────
|
|
266
|
+
|
|
267
|
+
class TestWakeWord:
|
|
268
|
+
def test_contains_wake_exact(self):
|
|
269
|
+
from voice.wake_word import _contains_wake
|
|
270
|
+
assert _contains_wake("hey dulus busca el clima") == "hey dulus"
|
|
271
|
+
|
|
272
|
+
def test_contains_wake_case_insensitive(self):
|
|
273
|
+
from voice.wake_word import _contains_wake
|
|
274
|
+
assert _contains_wake("HEY DULUS") == "hey dulus"
|
|
275
|
+
|
|
276
|
+
def test_contains_wake_no_match(self):
|
|
277
|
+
from voice.wake_word import _contains_wake
|
|
278
|
+
assert _contains_wake("hola cómo estás") is None
|
|
279
|
+
|
|
280
|
+
def test_wake_phrases_list(self):
|
|
281
|
+
from voice.wake_word import WAKE_PHRASES
|
|
282
|
+
assert isinstance(WAKE_PHRASES, list)
|
|
283
|
+
assert len(WAKE_PHRASES) > 0
|
|
284
|
+
assert "dulus" in WAKE_PHRASES
|
|
285
|
+
|
|
286
|
+
def test_listener_init(self):
|
|
287
|
+
from voice.wake_word import WakeWordListener
|
|
288
|
+
wl = WakeWordListener()
|
|
289
|
+
assert wl.is_running() is False
|
|
290
|
+
assert "dulus" in wl.wake_phrases
|
|
291
|
+
|
|
292
|
+
def test_voice_package_exports_wake(self):
|
|
293
|
+
import voice
|
|
294
|
+
assert hasattr(voice, "WakeWordListener")
|
|
295
|
+
assert hasattr(voice, "listen_once")
|
|
296
|
+
assert hasattr(voice, "WAKE_PHRASES")
|