dulus 0.2.53__tar.gz → 0.2.55__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. {dulus-0.2.53/dulus.egg-info → dulus-0.2.55}/PKG-INFO +1 -1
  2. {dulus-0.2.53 → dulus-0.2.55}/data/context.json +27 -25
  3. {dulus-0.2.53 → dulus-0.2.55/dulus.egg-info}/PKG-INFO +1 -1
  4. {dulus-0.2.53 → dulus-0.2.55}/dulus.egg-info/SOURCES.txt +3 -1
  5. {dulus-0.2.53 → dulus-0.2.55}/dulus.py +333 -2
  6. {dulus-0.2.53 → dulus-0.2.55}/input.py +90 -26
  7. {dulus-0.2.53 → dulus-0.2.55}/pyproject.toml +1 -1
  8. {dulus-0.2.53 → dulus-0.2.55}/tests/test_voice.py +56 -0
  9. {dulus-0.2.53 → dulus-0.2.55}/voice/__init__.py +8 -0
  10. dulus-0.2.55/voice/audio_utils.py +40 -0
  11. {dulus-0.2.53 → dulus-0.2.55}/voice/recorder.py +11 -7
  12. {dulus-0.2.53 → dulus-0.2.55}/voice/stt.py +20 -0
  13. {dulus-0.2.53 → dulus-0.2.55}/voice/tts.py +113 -41
  14. dulus-0.2.55/voice/wake_word.py +374 -0
  15. {dulus-0.2.53 → dulus-0.2.55}/LICENSE +0 -0
  16. {dulus-0.2.53 → dulus-0.2.55}/MANIFEST.in +0 -0
  17. {dulus-0.2.53 → dulus-0.2.55}/README.md +0 -0
  18. {dulus-0.2.53 → dulus-0.2.55}/agent.py +0 -0
  19. {dulus-0.2.53 → dulus-0.2.55}/backend/__init__.py +0 -0
  20. {dulus-0.2.53 → dulus-0.2.55}/backend/agents_bridge.py +0 -0
  21. {dulus-0.2.53 → dulus-0.2.55}/backend/compressor.py +0 -0
  22. {dulus-0.2.53 → dulus-0.2.55}/backend/context.py +0 -0
  23. {dulus-0.2.53 → dulus-0.2.55}/backend/githook.py +0 -0
  24. {dulus-0.2.53 → dulus-0.2.55}/backend/marketplace.py +0 -0
  25. {dulus-0.2.53 → dulus-0.2.55}/backend/mempalace_bridge.py +0 -0
  26. {dulus-0.2.53 → dulus-0.2.55}/backend/personas.py +0 -0
  27. {dulus-0.2.53 → dulus-0.2.55}/backend/plugins.py +0 -0
  28. {dulus-0.2.53 → dulus-0.2.55}/backend/server.py +0 -0
  29. {dulus-0.2.53 → dulus-0.2.55}/backend/tasks.py +0 -0
  30. {dulus-0.2.53 → dulus-0.2.55}/batch_api.py +0 -0
  31. {dulus-0.2.53 → dulus-0.2.55}/checkpoint/__init__.py +0 -0
  32. {dulus-0.2.53 → dulus-0.2.55}/checkpoint/hooks.py +0 -0
  33. {dulus-0.2.53 → dulus-0.2.55}/checkpoint/store.py +0 -0
  34. {dulus-0.2.53 → dulus-0.2.55}/checkpoint/types.py +0 -0
  35. {dulus-0.2.53 → dulus-0.2.55}/claude_code_watcher.py +0 -0
  36. {dulus-0.2.53 → dulus-0.2.55}/clipboard_utils.py +0 -0
  37. {dulus-0.2.53 → dulus-0.2.55}/cloudsave.py +0 -0
  38. {dulus-0.2.53 → dulus-0.2.55}/common.py +0 -0
  39. {dulus-0.2.53 → dulus-0.2.55}/compaction.py +0 -0
  40. {dulus-0.2.53 → dulus-0.2.55}/config.py +0 -0
  41. {dulus-0.2.53 → dulus-0.2.55}/context.py +0 -0
  42. {dulus-0.2.53 → dulus-0.2.55}/data/__init__.py +0 -0
  43. {dulus-0.2.53 → dulus-0.2.55}/data/active_persona.json +0 -0
  44. {dulus-0.2.53 → dulus-0.2.55}/data/marketplace.json +0 -0
  45. {dulus-0.2.53 → dulus-0.2.55}/data/personas.json +0 -0
  46. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/__init__.py +0 -0
  47. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/composio/__init__.py +0 -0
  48. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  49. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  50. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  51. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/composio/plugin.json +0 -0
  52. {dulus-0.2.53 → dulus-0.2.55}/data/plugins/composio/plugin_tool.py +0 -0
  53. {dulus-0.2.53 → dulus-0.2.55}/data/tasks.json +0 -0
  54. {dulus-0.2.53 → dulus-0.2.55}/docs/README.md +0 -0
  55. {dulus-0.2.53 → dulus-0.2.55}/docs/__init__.py +0 -0
  56. {dulus-0.2.53 → dulus-0.2.55}/docs/api.html +0 -0
  57. {dulus-0.2.53 → dulus-0.2.55}/docs/architecture.md +0 -0
  58. {dulus-0.2.53 → dulus-0.2.55}/docs/azure-speech-template.json +0 -0
  59. {dulus-0.2.53 → dulus-0.2.55}/docs/dashboard/index.html +0 -0
  60. {dulus-0.2.53 → dulus-0.2.55}/docs/divider.svg +0 -0
  61. {dulus-0.2.53 → dulus-0.2.55}/docs/generate.py +0 -0
  62. {dulus-0.2.53 → dulus-0.2.55}/docs/hero.svg +0 -0
  63. {dulus-0.2.53 → dulus-0.2.55}/docs/index.html +0 -0
  64. {dulus-0.2.53 → dulus-0.2.55}/docs/news.md +0 -0
  65. {dulus-0.2.53 → dulus-0.2.55}/docs/nvidia-models.svg +0 -0
  66. {dulus-0.2.53 → dulus-0.2.55}/docs/particle-playground.html +0 -0
  67. {dulus-0.2.53 → dulus-0.2.55}/docs/personas/index.html +0 -0
  68. {dulus-0.2.53 → dulus-0.2.55}/docs/poetry-banner.png +0 -0
  69. {dulus-0.2.53 → dulus-0.2.55}/docs/preview.html +0 -0
  70. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-agents.svg +0 -0
  71. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-brainstorm.svg +0 -0
  72. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-bridges.svg +0 -0
  73. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-features.svg +0 -0
  74. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-freetier.svg +0 -0
  75. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-memory.svg +0 -0
  76. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-models.svg +0 -0
  77. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-perms.svg +0 -0
  78. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-plugins.svg +0 -0
  79. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-quickstart.svg +0 -0
  80. {dulus-0.2.53 → dulus-0.2.55}/docs/sec-ssj.svg +0 -0
  81. {dulus-0.2.53 → dulus-0.2.55}/docs/spinners.svg +0 -0
  82. {dulus-0.2.53 → dulus-0.2.55}/docs/split-pane.svg +0 -0
  83. {dulus-0.2.53 → dulus-0.2.55}/docs/terminal-boot.svg +0 -0
  84. {dulus-0.2.53 → dulus-0.2.55}/docs/uploads/particle-playground.html +0 -0
  85. {dulus-0.2.53 → dulus-0.2.55}/dulus.egg-info/dependency_links.txt +0 -0
  86. {dulus-0.2.53 → dulus-0.2.55}/dulus.egg-info/entry_points.txt +0 -0
  87. {dulus-0.2.53 → dulus-0.2.55}/dulus.egg-info/requires.txt +0 -0
  88. {dulus-0.2.53 → dulus-0.2.55}/dulus.egg-info/top_level.txt +0 -0
  89. {dulus-0.2.53 → dulus-0.2.55}/dulus_gui.py +0 -0
  90. {dulus-0.2.53 → dulus-0.2.55}/dulus_mcp/__init__.py +0 -0
  91. {dulus-0.2.53 → dulus-0.2.55}/dulus_mcp/client.py +0 -0
  92. {dulus-0.2.53 → dulus-0.2.55}/dulus_mcp/config.py +0 -0
  93. {dulus-0.2.53 → dulus-0.2.55}/dulus_mcp/tools.py +0 -0
  94. {dulus-0.2.53 → dulus-0.2.55}/dulus_mcp/types.py +0 -0
  95. {dulus-0.2.53 → dulus-0.2.55}/gui/__init__.py +0 -0
  96. {dulus-0.2.53 → dulus-0.2.55}/gui/agent_bridge.py +0 -0
  97. {dulus-0.2.53 → dulus-0.2.55}/gui/chat_widget.py +0 -0
  98. {dulus-0.2.53 → dulus-0.2.55}/gui/main_window.py +0 -0
  99. {dulus-0.2.53 → dulus-0.2.55}/gui/personas.py +0 -0
  100. {dulus-0.2.53 → dulus-0.2.55}/gui/session_utils.py +0 -0
  101. {dulus-0.2.53 → dulus-0.2.55}/gui/settings_dialog.py +0 -0
  102. {dulus-0.2.53 → dulus-0.2.55}/gui/sidebar.py +0 -0
  103. {dulus-0.2.53 → dulus-0.2.55}/gui/tasks_view.py +0 -0
  104. {dulus-0.2.53 → dulus-0.2.55}/gui/themes.py +0 -0
  105. {dulus-0.2.53 → dulus-0.2.55}/gui/tool_panel.py +0 -0
  106. {dulus-0.2.53 → dulus-0.2.55}/license_manager.py +0 -0
  107. {dulus-0.2.53 → dulus-0.2.55}/memory/__init__.py +0 -0
  108. {dulus-0.2.53 → dulus-0.2.55}/memory/audit.py +0 -0
  109. {dulus-0.2.53 → dulus-0.2.55}/memory/consolidator.py +0 -0
  110. {dulus-0.2.53 → dulus-0.2.55}/memory/context.py +0 -0
  111. {dulus-0.2.53 → dulus-0.2.55}/memory/offload.py +0 -0
  112. {dulus-0.2.53 → dulus-0.2.55}/memory/palace.py +0 -0
  113. {dulus-0.2.53 → dulus-0.2.55}/memory/scan.py +0 -0
  114. {dulus-0.2.53 → dulus-0.2.55}/memory/sessions.py +0 -0
  115. {dulus-0.2.53 → dulus-0.2.55}/memory/store.py +0 -0
  116. {dulus-0.2.53 → dulus-0.2.55}/memory/tools.py +0 -0
  117. {dulus-0.2.53 → dulus-0.2.55}/memory/types.py +0 -0
  118. {dulus-0.2.53 → dulus-0.2.55}/memory/vector_search.py +0 -0
  119. {dulus-0.2.53 → dulus-0.2.55}/multi_agent/__init__.py +0 -0
  120. {dulus-0.2.53 → dulus-0.2.55}/multi_agent/subagent.py +0 -0
  121. {dulus-0.2.53 → dulus-0.2.55}/multi_agent/tools.py +0 -0
  122. {dulus-0.2.53 → dulus-0.2.55}/offload_helper.py +0 -0
  123. {dulus-0.2.53 → dulus-0.2.55}/plugin/__init__.py +0 -0
  124. {dulus-0.2.53 → dulus-0.2.55}/plugin/autoadapter.py +0 -0
  125. {dulus-0.2.53 → dulus-0.2.55}/plugin/loader.py +0 -0
  126. {dulus-0.2.53 → dulus-0.2.55}/plugin/recommend.py +0 -0
  127. {dulus-0.2.53 → dulus-0.2.55}/plugin/store.py +0 -0
  128. {dulus-0.2.53 → dulus-0.2.55}/plugin/types.py +0 -0
  129. {dulus-0.2.53 → dulus-0.2.55}/providers.py +0 -0
  130. {dulus-0.2.53 → dulus-0.2.55}/sandbox/README.md +0 -0
  131. {dulus-0.2.53 → dulus-0.2.55}/sandbox/__init__.py +0 -0
  132. {dulus-0.2.53 → dulus-0.2.55}/sandbox/components.json +0 -0
  133. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
  134. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
  135. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/index.html +0 -0
  136. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/wallpaper-default.jpg +0 -0
  137. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/wallpapers/default.jpeg +0 -0
  138. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/wallpapers/light.jpeg +0 -0
  139. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/wallpapers/nature.jpeg +0 -0
  140. {dulus-0.2.53 → dulus-0.2.55}/sandbox/dist/wallpapers/tech.jpeg +0 -0
  141. {dulus-0.2.53 → dulus-0.2.55}/sandbox/eslint.config.js +0 -0
  142. {dulus-0.2.53 → dulus-0.2.55}/sandbox/index.html +0 -0
  143. {dulus-0.2.53 → dulus-0.2.55}/sandbox/info.md +0 -0
  144. {dulus-0.2.53 → dulus-0.2.55}/sandbox/package-lock.json +0 -0
  145. {dulus-0.2.53 → dulus-0.2.55}/sandbox/package.json +0 -0
  146. {dulus-0.2.53 → dulus-0.2.55}/sandbox/postcss.config.js +0 -0
  147. {dulus-0.2.53 → dulus-0.2.55}/sandbox/public/wallpaper-default.jpg +0 -0
  148. {dulus-0.2.53 → dulus-0.2.55}/sandbox/public/wallpapers/default.jpeg +0 -0
  149. {dulus-0.2.53 → dulus-0.2.55}/sandbox/public/wallpapers/light.jpeg +0 -0
  150. {dulus-0.2.53 → dulus-0.2.55}/sandbox/public/wallpapers/nature.jpeg +0 -0
  151. {dulus-0.2.53 → dulus-0.2.55}/sandbox/public/wallpapers/tech.jpeg +0 -0
  152. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/App.tsx +0 -0
  153. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/AgentMonitor.tsx +0 -0
  154. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ApiTester.tsx +0 -0
  155. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/AppRouter.tsx +0 -0
  156. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ArchiveManager.tsx +0 -0
  157. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/AsciiArt.tsx +0 -0
  158. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Base64Tool.tsx +0 -0
  159. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Browser.tsx +0 -0
  160. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Calculator.tsx +0 -0
  161. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Calendar.tsx +0 -0
  162. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Chat.tsx +0 -0
  163. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Chess.tsx +0 -0
  164. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Clock.tsx +0 -0
  165. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/CodeEditor.tsx +0 -0
  166. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ColorPalette.tsx +0 -0
  167. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ColorPicker.tsx +0 -0
  168. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Contacts.tsx +0 -0
  169. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/DocumentViewer.tsx +0 -0
  170. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Drawing.tsx +0 -0
  171. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Email.tsx +0 -0
  172. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/FileManager.tsx +0 -0
  173. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/FlappyBird.tsx +0 -0
  174. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/FtpClient.tsx +0 -0
  175. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Game2048.tsx +0 -0
  176. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/GitClient.tsx +0 -0
  177. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ImageGallery.tsx +0 -0
  178. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ImageViewer.tsx +0 -0
  179. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/JsonFormatter.tsx +0 -0
  180. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
  181. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/MatrixRain.tsx +0 -0
  182. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/MediaConverter.tsx +0 -0
  183. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Memory.tsx +0 -0
  184. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/MemoryManager.tsx +0 -0
  185. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Minesweeper.tsx +0 -0
  186. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/MusicPlayer.tsx +0 -0
  187. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/NetworkTools.tsx +0 -0
  188. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Notes.tsx +0 -0
  189. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/PasswordManager.tsx +0 -0
  190. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/PhotoEditor.tsx +0 -0
  191. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Pong.tsx +0 -0
  192. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/RegexTester.tsx +0 -0
  193. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Reminders.tsx +0 -0
  194. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/RssReader.tsx +0 -0
  195. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
  196. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Settings.tsx +0 -0
  197. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
  198. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Snake.tsx +0 -0
  199. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Solitaire.tsx +0 -0
  200. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Spreadsheet.tsx +0 -0
  201. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Sudoku.tsx +0 -0
  202. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/SystemMonitor.tsx +0 -0
  203. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/TaskManager.tsx +0 -0
  204. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Terminal.tsx +0 -0
  205. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Tetris.tsx +0 -0
  206. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/TextEditor.tsx +0 -0
  207. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/TicTacToe.tsx +0 -0
  208. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Todo.tsx +0 -0
  209. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/VideoPlayer.tsx +0 -0
  210. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
  211. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Weather.tsx +0 -0
  212. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/Whiteboard.tsx +0 -0
  213. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/apps/registry.ts +0 -0
  214. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/AppContainer.tsx +0 -0
  215. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/AppLauncher.tsx +0 -0
  216. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/BootSequence.tsx +0 -0
  217. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ContextMenu.tsx +0 -0
  218. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/Desktop.tsx +0 -0
  219. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/Dock.tsx +0 -0
  220. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/LoginScreen.tsx +0 -0
  221. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/NotImplemented.tsx +0 -0
  222. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/NotificationCenter.tsx +0 -0
  223. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/NotificationSystem.tsx +0 -0
  224. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/TopPanel.tsx +0 -0
  225. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/WindowFrame.tsx +0 -0
  226. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/WindowManager.tsx +0 -0
  227. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/accordion.tsx +0 -0
  228. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
  229. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/alert.tsx +0 -0
  230. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
  231. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/avatar.tsx +0 -0
  232. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/badge.tsx +0 -0
  233. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
  234. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/button-group.tsx +0 -0
  235. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/button.tsx +0 -0
  236. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/calendar.tsx +0 -0
  237. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/card.tsx +0 -0
  238. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/carousel.tsx +0 -0
  239. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/chart.tsx +0 -0
  240. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/checkbox.tsx +0 -0
  241. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/collapsible.tsx +0 -0
  242. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/command.tsx +0 -0
  243. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/context-menu.tsx +0 -0
  244. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/dialog.tsx +0 -0
  245. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/drawer.tsx +0 -0
  246. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
  247. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/empty.tsx +0 -0
  248. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/field.tsx +0 -0
  249. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/form.tsx +0 -0
  250. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/hover-card.tsx +0 -0
  251. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/input-group.tsx +0 -0
  252. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/input-otp.tsx +0 -0
  253. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/input.tsx +0 -0
  254. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/item.tsx +0 -0
  255. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/kbd.tsx +0 -0
  256. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/label.tsx +0 -0
  257. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/menubar.tsx +0 -0
  258. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
  259. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/pagination.tsx +0 -0
  260. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/popover.tsx +0 -0
  261. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/progress.tsx +0 -0
  262. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/radio-group.tsx +0 -0
  263. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/resizable.tsx +0 -0
  264. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/scroll-area.tsx +0 -0
  265. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/select.tsx +0 -0
  266. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/separator.tsx +0 -0
  267. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/sheet.tsx +0 -0
  268. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/sidebar.tsx +0 -0
  269. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/skeleton.tsx +0 -0
  270. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/slider.tsx +0 -0
  271. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/sonner.tsx +0 -0
  272. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/spinner.tsx +0 -0
  273. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/switch.tsx +0 -0
  274. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/table.tsx +0 -0
  275. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/tabs.tsx +0 -0
  276. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/textarea.tsx +0 -0
  277. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/toggle-group.tsx +0 -0
  278. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/toggle.tsx +0 -0
  279. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/components/ui/tooltip.tsx +0 -0
  280. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/index.ts +0 -0
  281. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/use-mobile.ts +0 -0
  282. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
  283. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusAgents.ts +0 -0
  284. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusChat.ts +0 -0
  285. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusEvents.ts +0 -0
  286. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusHealth.ts +0 -0
  287. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusMemory.ts +0 -0
  288. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusSkills.ts +0 -0
  289. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useDulusTasks.ts +0 -0
  290. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useFileSystem.ts +0 -0
  291. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useOSStore.tsx +0 -0
  292. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useSkillBridge.ts +0 -0
  293. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useSystemBattery.ts +0 -0
  294. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
  295. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/hooks/useSystemVolume.ts +0 -0
  296. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/index.css +0 -0
  297. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/lib/dulus-api.ts +0 -0
  298. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/lib/utils.ts +0 -0
  299. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/main.tsx +0 -0
  300. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/types/index.ts +0 -0
  301. {dulus-0.2.53 → dulus-0.2.55}/sandbox/src/utils/assets.ts +0 -0
  302. {dulus-0.2.53 → dulus-0.2.55}/sandbox/tailwind.config.js +0 -0
  303. {dulus-0.2.53 → dulus-0.2.55}/sandbox/tsconfig.app.json +0 -0
  304. {dulus-0.2.53 → dulus-0.2.55}/sandbox/tsconfig.json +0 -0
  305. {dulus-0.2.53 → dulus-0.2.55}/sandbox/tsconfig.node.json +0 -0
  306. {dulus-0.2.53 → dulus-0.2.55}/sandbox/vite.config.ts +0 -0
  307. {dulus-0.2.53 → dulus-0.2.55}/setup.cfg +0 -0
  308. {dulus-0.2.53 → dulus-0.2.55}/skill/__init__.py +0 -0
  309. {dulus-0.2.53 → dulus-0.2.55}/skill/builtin.py +0 -0
  310. {dulus-0.2.53 → dulus-0.2.55}/skill/clawhub.py +0 -0
  311. {dulus-0.2.53 → dulus-0.2.55}/skill/executor.py +0 -0
  312. {dulus-0.2.53 → dulus-0.2.55}/skill/loader.py +0 -0
  313. {dulus-0.2.53 → dulus-0.2.55}/skill/tools.py +0 -0
  314. {dulus-0.2.53 → dulus-0.2.55}/skills.py +0 -0
  315. {dulus-0.2.53 → dulus-0.2.55}/spinner.py +0 -0
  316. {dulus-0.2.53 → dulus-0.2.55}/string_utils.py +0 -0
  317. {dulus-0.2.53 → dulus-0.2.55}/subagent.py +0 -0
  318. {dulus-0.2.53 → dulus-0.2.55}/task/__init__.py +0 -0
  319. {dulus-0.2.53 → dulus-0.2.55}/task/store.py +0 -0
  320. {dulus-0.2.53 → dulus-0.2.55}/task/tools.py +0 -0
  321. {dulus-0.2.53 → dulus-0.2.55}/task/types.py +0 -0
  322. {dulus-0.2.53 → dulus-0.2.55}/tests/test_afk_yolo.py +0 -0
  323. {dulus-0.2.53 → dulus-0.2.55}/tests/test_approval_runtime.py +0 -0
  324. {dulus-0.2.53 → dulus-0.2.55}/tests/test_background_task_tools.py +0 -0
  325. {dulus-0.2.53 → dulus-0.2.55}/tests/test_background_tasks.py +0 -0
  326. {dulus-0.2.53 → dulus-0.2.55}/tests/test_checkpoint.py +0 -0
  327. {dulus-0.2.53 → dulus-0.2.55}/tests/test_clipboard_utils.py +0 -0
  328. {dulus-0.2.53 → dulus-0.2.55}/tests/test_compaction.py +0 -0
  329. {dulus-0.2.53 → dulus-0.2.55}/tests/test_diff_view.py +0 -0
  330. {dulus-0.2.53 → dulus-0.2.55}/tests/test_diff_visualization.py +0 -0
  331. {dulus-0.2.53 → dulus-0.2.55}/tests/test_display_blocks.py +0 -0
  332. {dulus-0.2.53 → dulus-0.2.55}/tests/test_export_import.py +0 -0
  333. {dulus-0.2.53 → dulus-0.2.55}/tests/test_hook_engine.py +0 -0
  334. {dulus-0.2.53 → dulus-0.2.55}/tests/test_injection_fix.py +0 -0
  335. {dulus-0.2.53 → dulus-0.2.55}/tests/test_license.py +0 -0
  336. {dulus-0.2.53 → dulus-0.2.55}/tests/test_mcp.py +0 -0
  337. {dulus-0.2.53 → dulus-0.2.55}/tests/test_memory.py +0 -0
  338. {dulus-0.2.53 → dulus-0.2.55}/tests/test_notification_manager.py +0 -0
  339. {dulus-0.2.53 → dulus-0.2.55}/tests/test_plugin.py +0 -0
  340. {dulus-0.2.53 → dulus-0.2.55}/tests/test_session_fork.py +0 -0
  341. {dulus-0.2.53 → dulus-0.2.55}/tests/test_shell_mode.py +0 -0
  342. {dulus-0.2.53 → dulus-0.2.55}/tests/test_skills.py +0 -0
  343. {dulus-0.2.53 → dulus-0.2.55}/tests/test_steer_input.py +0 -0
  344. {dulus-0.2.53 → dulus-0.2.55}/tests/test_subagent.py +0 -0
  345. {dulus-0.2.53 → dulus-0.2.55}/tests/test_task.py +0 -0
  346. {dulus-0.2.53 → dulus-0.2.55}/tests/test_telegram_buffer.py +0 -0
  347. {dulus-0.2.53 → dulus-0.2.55}/tests/test_think_tool.py +0 -0
  348. {dulus-0.2.53 → dulus-0.2.55}/tests/test_todo_tool.py +0 -0
  349. {dulus-0.2.53 → dulus-0.2.55}/tests/test_todo_visualization.py +0 -0
  350. {dulus-0.2.53 → dulus-0.2.55}/tests/test_tool_registry.py +0 -0
  351. {dulus-0.2.53 → dulus-0.2.55}/tests/test_wire_events.py +0 -0
  352. {dulus-0.2.53 → dulus-0.2.55}/tmux_offloader.py +0 -0
  353. {dulus-0.2.53 → dulus-0.2.55}/tmux_tools.py +0 -0
  354. {dulus-0.2.53 → dulus-0.2.55}/tool_registry.py +0 -0
  355. {dulus-0.2.53 → dulus-0.2.55}/tools.py +0 -0
  356. {dulus-0.2.53 → dulus-0.2.55}/ui/__init__.py +0 -0
  357. {dulus-0.2.53 → dulus-0.2.55}/ui/input.py +0 -0
  358. {dulus-0.2.53 → dulus-0.2.55}/ui/render.py +0 -0
  359. {dulus-0.2.53 → dulus-0.2.55}/voice/keyterms.py +0 -0
  360. {dulus-0.2.53 → dulus-0.2.55}/webchat.py +0 -0
  361. {dulus-0.2.53 → dulus-0.2.55}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.53
3
+ Version: 0.2.55
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -9,21 +9,22 @@
9
9
  "project": {
10
10
  "name": "Dulus Command Center",
11
11
  "repo_stats": {
12
- "files": 432,
13
- "lines": 205188,
12
+ "files": 439,
13
+ "lines": 206577,
14
14
  "languages": {
15
15
  ".example": 5,
16
- "no_ext": 1424,
17
- ".py": 60473,
16
+ "no_ext": 1471,
17
+ ".py": 61567,
18
18
  ".html": 14944,
19
19
  ".in": 3,
20
20
  ".toml": 129,
21
- ".md": 2161,
22
- ".txt": 463,
21
+ ".md": 2170,
22
+ ".txt": 473,
23
23
  ".lock": 1,
24
- ".json": 9513,
25
- ".whl": 21729,
26
- ".gz": 20908,
24
+ ".json": 9515,
25
+ ".TAG": 4,
26
+ ".whl": 21792,
27
+ ".gz": 21068,
27
28
  ".svg": 1123,
28
29
  ".png": 31639,
29
30
  ".sh": 144,
@@ -38,41 +39,42 @@
38
39
  },
39
40
  "recent_commits": [
40
41
  {
41
- "hash": "7fc805e",
42
- "subject": "chore: bump version 0.2.50 -> 0.2.51",
42
+ "hash": "d9c870b",
43
+ "subject": "bump: 0.2.53 → 0.2.54",
43
44
  "author": "KevRojo",
44
- "date": "2026-05-12"
45
+ "date": "2026-05-13"
45
46
  },
46
47
  {
47
- "hash": "360badc",
48
- "subject": "fix: GUI session responsiveness, sidebar incremental diff, async scan, default session IDs",
48
+ "hash": "3c7daf7",
49
+ "subject": "feat(voice): add wake word detection, audio utils, and voice module improvements",
49
50
  "author": "KevRojo",
50
- "date": "2026-05-12"
51
+ "date": "2026-05-13"
51
52
  },
52
53
  {
53
- "hash": "fae4455",
54
- "subject": "feat: MemoryManager overhaul, skill bridge polish, server sync",
54
+ "hash": "14c4f42",
55
+ "subject": "bump: 0.2.53",
55
56
  "author": "KevRojo",
56
57
  "date": "2026-05-12"
57
58
  },
58
59
  {
59
- "hash": "bf6455f",
60
- "subject": "feat: skill-to-chat bridge, MemPalace search API, sandbox UX polish",
60
+ "hash": "979df03",
61
+ "subject": "feat: skill system overhaul — awesome-remote install, interactive use, descriptions",
61
62
  "author": "KevRojo",
62
63
  "date": "2026-05-12"
63
64
  },
64
65
  {
65
- "hash": "9803f97",
66
- "subject": "fix: include sandbox/src and sandbox/public in wheel",
66
+ "hash": "39ee671",
67
+ "subject": "chore(release): bump v0.2.52",
67
68
  "author": "KevRojo",
68
69
  "date": "2026-05-12"
69
70
  }
70
71
  ],
71
72
  "recent_changes": [
72
- "data/context.json",
73
- "dulus.py",
74
- "gui/session_utils.py",
75
- "pyproject.toml"
73
+ "input.py",
74
+ "pyproject.toml",
75
+ "requirements.txt",
76
+ "voice/audio_utils.py",
77
+ "voice/tts.py"
76
78
  ]
77
79
  },
78
80
  "tasks": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.53
3
+ Version: 0.2.55
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -351,7 +351,9 @@ ui/__init__.py
351
351
  ui/input.py
352
352
  ui/render.py
353
353
  voice/__init__.py
354
+ voice/audio_utils.py
354
355
  voice/keyterms.py
355
356
  voice/recorder.py
356
357
  voice/stt.py
357
- voice/tts.py
358
+ voice/tts.py
359
+ voice/wake_word.py
@@ -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
- if config.pop("_auto_voice_next", False) and not in_batch_mode:
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