neoagent 2.3.1-beta.6 → 2.3.1-beta.60
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.
- package/.env.example +45 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +6 -5
- package/docs/hardware.md +1 -1
- package/docs/integrations.md +2 -3
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +111 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +171 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +804 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +569 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +157 -0
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +233 -0
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
- package/flutter_app/lib/main.dart +103 -0
- package/flutter_app/lib/main_account_settings.dart +1250 -0
- package/flutter_app/lib/main_admin.dart +886 -0
- package/flutter_app/lib/main_app_shell.dart +2134 -0
- package/flutter_app/lib/main_chat.dart +3316 -0
- package/flutter_app/lib/main_controller.dart +6836 -0
- package/flutter_app/lib/main_devices.dart +2396 -0
- package/flutter_app/lib/main_integrations.dart +909 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3539 -0
- package/flutter_app/lib/main_navigation.dart +188 -0
- package/flutter_app/lib/main_operations.dart +4851 -0
- package/flutter_app/lib/main_recordings.dart +920 -0
- package/flutter_app/lib/main_runtime.dart +838 -0
- package/flutter_app/lib/main_settings.dart +2040 -0
- package/flutter_app/lib/main_shared.dart +3147 -0
- package/flutter_app/lib/main_theme.dart +257 -0
- package/flutter_app/lib/main_voice_assistant.dart +957 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1837 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
- package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
- package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +44 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1224 -0
- package/flutter_app/pubspec.yaml +57 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/install_helpers.js +31 -0
- package/lib/manager.js +201 -6
- package/package.json +5 -3
- package/server/db/database.js +125 -1
- package/server/http/middleware.js +55 -2
- package/server/http/routes.js +1 -0
- package/server/http/static.js +41 -1
- package/server/index.js +3 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +281 -1
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +86314 -82363
- package/server/routes/auth.js +26 -5
- package/server/routes/integrations.js +108 -1
- package/server/routes/memory.js +11 -2
- package/server/routes/settings.js +84 -2
- package/server/routes/wearable.js +67 -0
- package/server/services/ai/engine.js +3 -2
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/toolSelector.js +54 -1
- package/server/services/ai/tools.js +88 -5
- package/server/services/desktop/screenRecorder.js +65 -9
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/figma/provider.js +1 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/google/provider.js +1 -0
- package/server/services/integrations/manager.js +88 -12
- package/server/services/integrations/microsoft/provider.js +1 -0
- package/server/services/integrations/oauth_provider.js +25 -8
- package/server/services/integrations/provider_config_store.js +85 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/integrations/spotify/provider.js +1 -0
- package/server/services/integrations/trello/provider.js +842 -0
- package/server/services/manager.js +46 -1
- package/server/services/mcp/client.js +120 -23
- package/server/services/memory/manager.js +39 -2
- package/server/services/messaging/access_policy.js +10 -0
- package/server/services/messaging/manager.js +49 -0
- package/server/services/messaging/meshtastic.js +277 -0
- package/server/services/messaging/meshtastic_env.js +40 -0
- package/server/services/messaging/meshtastic_protocol.js +530 -0
- package/server/services/messaging/meshtastic_protocol.test.js +33 -0
- package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/openaiSpeech.js +6 -1
- package/server/services/voice/providers.js +144 -15
- package/server/services/voice/runtimeManager.js +156 -19
- package/server/services/voice/turnRunner.js +29 -9
- package/server/services/wearable/firmware_manifest.js +353 -0
- package/server/services/wearable/gateway.js +350 -0
- package/server/services/wearable/protocol.js +45 -0
- package/server/services/wearable/service.js +240 -0
- package/server/services/widgets/service.js +3 -0
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
- package/server/services/integrations/home_assistant/provider.js +0 -350
|
@@ -6,6 +6,7 @@ const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
|
6
6
|
const { getOpenAiClient } = require('./openaiClient');
|
|
7
7
|
const { synthesizeSpeechBuffer } = require('./openaiSpeech');
|
|
8
8
|
const { transcribeChunkWithDeepgram } = require('../recordings/deepgram');
|
|
9
|
+
const { decryptLocalValue } = require('../../utils/local_secrets');
|
|
9
10
|
|
|
10
11
|
const DEFAULT_STT_PROVIDER = 'openai';
|
|
11
12
|
const DEFAULT_TTS_PROVIDER = 'openai';
|
|
@@ -36,6 +37,20 @@ const DEFAULT_GEMINI_TRANSCRIPTION_PROMPT =
|
|
|
36
37
|
'Transcribe this audio verbatim. Return only the transcript text.';
|
|
37
38
|
const EMOJI_SPEECH_REGEX =
|
|
38
39
|
/[\p{Extended_Pictographic}\p{Emoji_Presentation}\p{Regional_Indicator}\u200D\uFE0F\u20E3]/gu;
|
|
40
|
+
const WEARABLE_SAFE_AUDIO_FORMAT = Object.freeze({
|
|
41
|
+
responseFormat: 'wav',
|
|
42
|
+
mimeType: 'audio/wav',
|
|
43
|
+
streamResponseFormat: 'pcm',
|
|
44
|
+
streamMimeType: 'audio/wav',
|
|
45
|
+
pcmSampleRate: 24000,
|
|
46
|
+
pcmChannels: 1,
|
|
47
|
+
pcmBitsPerSample: 16,
|
|
48
|
+
deepgramEncoding: 'linear16',
|
|
49
|
+
deepgramContainer: 'wav',
|
|
50
|
+
deepgramStreamContainer: 'none',
|
|
51
|
+
});
|
|
52
|
+
const MIN_STREAM_PCM_CHUNK_BYTES = 24000;
|
|
53
|
+
const MAX_STREAM_PCM_CHUNK_BYTES = 48000;
|
|
39
54
|
|
|
40
55
|
function withTimeout(promise, timeoutMs, label) {
|
|
41
56
|
const normalizedTimeout = Number(timeoutMs);
|
|
@@ -94,7 +109,7 @@ function resolveApiKey(candidates = []) {
|
|
|
94
109
|
const snake = lower.replace(/[^a-z0-9]+/g, '_');
|
|
95
110
|
const variants = [key, lower, snake];
|
|
96
111
|
for (const variant of variants) {
|
|
97
|
-
const value = keys[variant];
|
|
112
|
+
const value = decryptLocalValue(keys[variant]);
|
|
98
113
|
if (typeof value === 'string' && value.trim()) {
|
|
99
114
|
return value.trim();
|
|
100
115
|
}
|
|
@@ -138,9 +153,16 @@ function normalizeVoiceSynthesisOptions(options = {}) {
|
|
|
138
153
|
provider,
|
|
139
154
|
model: resolveTtsModel(provider, options.model),
|
|
140
155
|
voice: resolveTtsVoice(provider, options.voice),
|
|
156
|
+
responseFormat: String(options.responseFormat || '').trim().toLowerCase(),
|
|
157
|
+
transport: String(options.transport || '').trim().toLowerCase(),
|
|
141
158
|
};
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
function resolveWearableSafeAudioOptions(options = {}) {
|
|
162
|
+
return String(options.transport || '').trim().toLowerCase() === 'wearable'
|
|
163
|
+
|| String(options.responseFormat || '').trim().toLowerCase() === 'wav';
|
|
164
|
+
}
|
|
165
|
+
|
|
144
166
|
function requireApiKey(settingLabel, candidates = []) {
|
|
145
167
|
const apiKey = resolveApiKey(candidates);
|
|
146
168
|
if (!apiKey) {
|
|
@@ -184,10 +206,11 @@ async function fetchAudioStreamOrThrow(url, init, errorPrefix, defaultMimeType =
|
|
|
184
206
|
while (true) {
|
|
185
207
|
const { done, value } = await reader.read();
|
|
186
208
|
if (done) break;
|
|
187
|
-
if (value?.length)
|
|
209
|
+
if (value?.length) {
|
|
210
|
+
chunks.push(Buffer.from(value));
|
|
211
|
+
}
|
|
188
212
|
}
|
|
189
|
-
|
|
190
|
-
await onChunk({ audioBytes, mimeType });
|
|
213
|
+
await onChunk({ audioBytes: Buffer.concat(chunks), mimeType });
|
|
191
214
|
}
|
|
192
215
|
|
|
193
216
|
function guessExtFromMimeType(mimeType) {
|
|
@@ -248,6 +271,49 @@ function wrapPcmAsWav(audioBytes, format) {
|
|
|
248
271
|
return Buffer.concat([header, data]);
|
|
249
272
|
}
|
|
250
273
|
|
|
274
|
+
async function streamPcmAsWavChunks(readable, format, onChunk) {
|
|
275
|
+
const source = readable && typeof readable.getReader === 'function'
|
|
276
|
+
? readable
|
|
277
|
+
: null;
|
|
278
|
+
let pending = Buffer.alloc(0);
|
|
279
|
+
|
|
280
|
+
async function flushPending(force = false) {
|
|
281
|
+
while (pending.length >= MIN_STREAM_PCM_CHUNK_BYTES || (force && pending.length > 0)) {
|
|
282
|
+
const targetLength = force
|
|
283
|
+
? pending.length
|
|
284
|
+
: Math.min(pending.length, MAX_STREAM_PCM_CHUNK_BYTES);
|
|
285
|
+
const evenLength = targetLength - (targetLength % 2);
|
|
286
|
+
if (evenLength <= 0) return;
|
|
287
|
+
const pcmChunk = pending.subarray(0, evenLength);
|
|
288
|
+
pending = pending.subarray(evenLength);
|
|
289
|
+
await onChunk({
|
|
290
|
+
audioBytes: wrapPcmAsWav(pcmChunk, format),
|
|
291
|
+
mimeType: WEARABLE_SAFE_AUDIO_FORMAT.streamMimeType,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (source) {
|
|
297
|
+
const reader = source.getReader();
|
|
298
|
+
while (true) {
|
|
299
|
+
const { done, value } = await reader.read();
|
|
300
|
+
if (done) break;
|
|
301
|
+
if (value?.length) {
|
|
302
|
+
pending = Buffer.concat([pending, Buffer.from(value)]);
|
|
303
|
+
await flushPending(false);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
for await (const chunk of readable) {
|
|
308
|
+
if (chunk?.length) {
|
|
309
|
+
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
310
|
+
await flushPending(false);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
await flushPending(true);
|
|
315
|
+
}
|
|
316
|
+
|
|
251
317
|
async function transcribeWithOpenAi(filePath, model, options = {}) {
|
|
252
318
|
const client = getOpenAiClient({
|
|
253
319
|
apiKey: typeof options.apiKey === 'string' ? options.apiKey.trim() : '',
|
|
@@ -341,10 +407,15 @@ async function synthesizeWithOpenAi(text, model, voice, options = {}) {
|
|
|
341
407
|
if (!client) {
|
|
342
408
|
throw new Error('OpenAI TTS is selected but OPENAI_API_KEY is not configured.');
|
|
343
409
|
}
|
|
344
|
-
const
|
|
410
|
+
const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
|
|
411
|
+
const audioBytes = await synthesizeSpeechBuffer(client, text, {
|
|
412
|
+
model,
|
|
413
|
+
voice,
|
|
414
|
+
responseFormat: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.responseFormat : 'mp3',
|
|
415
|
+
});
|
|
345
416
|
return {
|
|
346
417
|
audioBytes,
|
|
347
|
-
mimeType: 'audio/mpeg',
|
|
418
|
+
mimeType: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
|
|
348
419
|
};
|
|
349
420
|
}
|
|
350
421
|
|
|
@@ -356,25 +427,49 @@ async function streamWithOpenAi(text, model, voice, options = {}, onChunk) {
|
|
|
356
427
|
if (!client) {
|
|
357
428
|
throw new Error('OpenAI TTS is selected but OPENAI_API_KEY is not configured.');
|
|
358
429
|
}
|
|
430
|
+
const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
|
|
359
431
|
const response = await client.audio.speech.create({
|
|
360
432
|
model: String(model || 'gpt-4o-mini-tts').trim() || 'gpt-4o-mini-tts',
|
|
361
433
|
voice: String(voice || 'alloy').trim() || 'alloy',
|
|
362
434
|
input: text,
|
|
363
|
-
response_format: 'mp3',
|
|
435
|
+
response_format: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.streamResponseFormat : 'mp3',
|
|
364
436
|
});
|
|
437
|
+
if (useWearableSafeAudio) {
|
|
438
|
+
await streamPcmAsWavChunks(
|
|
439
|
+
response.body,
|
|
440
|
+
{
|
|
441
|
+
bitsPerSample: WEARABLE_SAFE_AUDIO_FORMAT.pcmBitsPerSample,
|
|
442
|
+
sampleRate: WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate,
|
|
443
|
+
channels: WEARABLE_SAFE_AUDIO_FORMAT.pcmChannels,
|
|
444
|
+
},
|
|
445
|
+
onChunk,
|
|
446
|
+
);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
365
449
|
const chunks = [];
|
|
366
450
|
for await (const chunk of response.body) {
|
|
367
451
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
368
452
|
}
|
|
369
453
|
const audioBytes = Buffer.concat(chunks);
|
|
370
|
-
await onChunk({
|
|
454
|
+
await onChunk({
|
|
455
|
+
audioBytes,
|
|
456
|
+
mimeType: useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
|
|
457
|
+
});
|
|
371
458
|
}
|
|
372
459
|
|
|
373
|
-
async function synthesizeWithDeepgram(text, model) {
|
|
460
|
+
async function synthesizeWithDeepgram(text, model, options = {}) {
|
|
374
461
|
const apiKey = requireApiKey('Deepgram TTS', ['DEEPGRAM_API_KEY']);
|
|
462
|
+
const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
|
|
463
|
+
const searchParams = new URLSearchParams({
|
|
464
|
+
model,
|
|
465
|
+
});
|
|
466
|
+
if (useWearableSafeAudio) {
|
|
467
|
+
searchParams.set('encoding', WEARABLE_SAFE_AUDIO_FORMAT.deepgramEncoding);
|
|
468
|
+
searchParams.set('container', WEARABLE_SAFE_AUDIO_FORMAT.deepgramContainer);
|
|
469
|
+
}
|
|
375
470
|
|
|
376
471
|
return fetchAudioOrThrow(
|
|
377
|
-
`https://api.deepgram.com/v1/speak
|
|
472
|
+
`https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
|
|
378
473
|
{
|
|
379
474
|
method: 'POST',
|
|
380
475
|
headers: {
|
|
@@ -384,13 +479,47 @@ async function synthesizeWithDeepgram(text, model) {
|
|
|
384
479
|
body: JSON.stringify({ text }),
|
|
385
480
|
},
|
|
386
481
|
'Deepgram TTS request failed',
|
|
482
|
+
useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
|
|
387
483
|
);
|
|
388
484
|
}
|
|
389
485
|
|
|
390
|
-
async function streamWithDeepgram(text, model, onChunk) {
|
|
486
|
+
async function streamWithDeepgram(text, model, options = {}, onChunk) {
|
|
391
487
|
const apiKey = requireApiKey('Deepgram TTS', ['DEEPGRAM_API_KEY']);
|
|
488
|
+
const useWearableSafeAudio = resolveWearableSafeAudioOptions(options);
|
|
489
|
+
const searchParams = new URLSearchParams({
|
|
490
|
+
model,
|
|
491
|
+
});
|
|
492
|
+
if (useWearableSafeAudio) {
|
|
493
|
+
searchParams.set('encoding', WEARABLE_SAFE_AUDIO_FORMAT.deepgramEncoding);
|
|
494
|
+
searchParams.set('container', WEARABLE_SAFE_AUDIO_FORMAT.deepgramStreamContainer);
|
|
495
|
+
searchParams.set('sample_rate', String(WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate));
|
|
496
|
+
const response = await fetch(
|
|
497
|
+
`https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
|
|
498
|
+
{
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: {
|
|
501
|
+
Authorization: `Token ${apiKey}`,
|
|
502
|
+
'Content-Type': 'application/json',
|
|
503
|
+
},
|
|
504
|
+
body: JSON.stringify({ text }),
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
if (!response.ok) {
|
|
508
|
+
await throwResponseError(response, 'Deepgram TTS stream failed');
|
|
509
|
+
}
|
|
510
|
+
await streamPcmAsWavChunks(
|
|
511
|
+
response.body,
|
|
512
|
+
{
|
|
513
|
+
bitsPerSample: WEARABLE_SAFE_AUDIO_FORMAT.pcmBitsPerSample,
|
|
514
|
+
sampleRate: WEARABLE_SAFE_AUDIO_FORMAT.pcmSampleRate,
|
|
515
|
+
channels: WEARABLE_SAFE_AUDIO_FORMAT.pcmChannels,
|
|
516
|
+
},
|
|
517
|
+
onChunk,
|
|
518
|
+
);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
392
521
|
await fetchAudioStreamOrThrow(
|
|
393
|
-
`https://api.deepgram.com/v1/speak
|
|
522
|
+
`https://api.deepgram.com/v1/speak?${searchParams.toString()}`,
|
|
394
523
|
{
|
|
395
524
|
method: 'POST',
|
|
396
525
|
headers: {
|
|
@@ -400,7 +529,7 @@ async function streamWithDeepgram(text, model, onChunk) {
|
|
|
400
529
|
body: JSON.stringify({ text }),
|
|
401
530
|
},
|
|
402
531
|
'Deepgram TTS stream failed',
|
|
403
|
-
'audio/mpeg',
|
|
532
|
+
useWearableSafeAudio ? WEARABLE_SAFE_AUDIO_FORMAT.mimeType : 'audio/mpeg',
|
|
404
533
|
onChunk,
|
|
405
534
|
);
|
|
406
535
|
}
|
|
@@ -586,7 +715,7 @@ async function synthesizeVoiceReply(text, options = {}) {
|
|
|
586
715
|
if (provider === 'openai') {
|
|
587
716
|
request = synthesizeWithOpenAi(content, model, voice, options);
|
|
588
717
|
} else if (provider === 'deepgram') {
|
|
589
|
-
request = synthesizeWithDeepgram(content, model);
|
|
718
|
+
request = synthesizeWithDeepgram(content, model, options);
|
|
590
719
|
} else {
|
|
591
720
|
request = synthesizeWithGemini(content, model, voice, options);
|
|
592
721
|
}
|
|
@@ -668,7 +797,7 @@ async function synthesizeVoiceReplyStream(text, options = {}, onChunk) {
|
|
|
668
797
|
if (provider === 'openai') {
|
|
669
798
|
await streamWithOpenAi(chunk, model, voice, options, onChunk);
|
|
670
799
|
} else if (provider === 'deepgram') {
|
|
671
|
-
await streamWithDeepgram(chunk, model, onChunk);
|
|
800
|
+
await streamWithDeepgram(chunk, model, options, onChunk);
|
|
672
801
|
} else {
|
|
673
802
|
await streamWithGemini(chunk, model, voice, options, onChunk);
|
|
674
803
|
}
|
|
@@ -34,11 +34,17 @@ class VoiceRuntimeManager {
|
|
|
34
34
|
return this.sessions.get(String(sessionId || '').trim()) || null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
async openSession({
|
|
38
|
+
userId,
|
|
39
|
+
agentId = null,
|
|
40
|
+
sessionId = null,
|
|
41
|
+
platform = 'voice_live',
|
|
42
|
+
sink,
|
|
43
|
+
outputMode = 'audio_and_text',
|
|
44
|
+
} = {}) {
|
|
45
|
+
if (!sink) {
|
|
46
|
+
throw new Error('A voice session sink is required.');
|
|
40
47
|
}
|
|
41
|
-
|
|
42
48
|
const voiceSettings = getVoiceRuntimeSettings(userId, agentId);
|
|
43
49
|
const liveProviderRuntime = this.#getProviderRuntime(
|
|
44
50
|
userId,
|
|
@@ -53,16 +59,37 @@ class VoiceRuntimeManager {
|
|
|
53
59
|
id: resolvedSessionId,
|
|
54
60
|
userId,
|
|
55
61
|
agentId,
|
|
56
|
-
platform
|
|
62
|
+
platform,
|
|
57
63
|
voiceSettings: {
|
|
58
64
|
...voiceSettings,
|
|
59
65
|
liveApiKey: liveProviderRuntime.apiKey,
|
|
60
66
|
liveBaseUrl: liveProviderRuntime.baseUrl,
|
|
61
67
|
},
|
|
68
|
+
sink,
|
|
69
|
+
outputMode,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
session.adapter = adapter;
|
|
73
|
+
this.sessions.set(resolvedSessionId, session);
|
|
74
|
+
await session.publishReady();
|
|
75
|
+
return session;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async openFlutterSession({ userId, agentId = null, socket, sessionId = null } = {}) {
|
|
79
|
+
if (!socket) {
|
|
80
|
+
throw new Error('Socket is required to open a Flutter voice session.');
|
|
81
|
+
}
|
|
82
|
+
return this.openSession({
|
|
83
|
+
userId,
|
|
84
|
+
agentId,
|
|
85
|
+
sessionId,
|
|
86
|
+
platform: 'voice_live',
|
|
87
|
+
outputMode: 'audio_and_text',
|
|
62
88
|
sink: {
|
|
63
89
|
publishReady: async (_session, extra = {}) => {
|
|
90
|
+
const voiceSettings = getVoiceRuntimeSettings(userId, agentId);
|
|
64
91
|
socket.emit('voice:session_ready', {
|
|
65
|
-
sessionId:
|
|
92
|
+
sessionId: String(sessionId || _session.id).trim(),
|
|
66
93
|
runtimeMode: voiceSettings.runtimeMode,
|
|
67
94
|
provider: voiceSettings.liveProvider,
|
|
68
95
|
model: voiceSettings.liveModel,
|
|
@@ -72,52 +99,58 @@ class VoiceRuntimeManager {
|
|
|
72
99
|
},
|
|
73
100
|
setState: async (_session, state, extra = {}) => {
|
|
74
101
|
socket.emit('voice:assistant_state', {
|
|
75
|
-
sessionId:
|
|
102
|
+
sessionId: _session.id,
|
|
76
103
|
state,
|
|
77
104
|
...extra,
|
|
78
105
|
});
|
|
79
106
|
},
|
|
80
107
|
publishTranscriptPartial: async (_session, content) => {
|
|
81
108
|
socket.emit('voice:transcript_partial', {
|
|
82
|
-
sessionId:
|
|
109
|
+
sessionId: _session.id,
|
|
83
110
|
content,
|
|
84
111
|
});
|
|
85
112
|
},
|
|
86
113
|
publishTranscriptFinal: async (_session, content) => {
|
|
87
114
|
socket.emit('voice:transcript_final', {
|
|
88
|
-
sessionId:
|
|
115
|
+
sessionId: _session.id,
|
|
89
116
|
content,
|
|
90
117
|
});
|
|
91
118
|
},
|
|
92
119
|
publishAssistantOutput: async (_session, content, options = {}) => {
|
|
93
|
-
await this.#deliverFlutterAssistantOutput(socket,
|
|
120
|
+
await this.#deliverFlutterAssistantOutput(socket, _session.id, _session, content, options);
|
|
94
121
|
},
|
|
95
|
-
interruptOutput: async () => {
|
|
122
|
+
interruptOutput: async (_session) => {
|
|
96
123
|
socket.emit('voice:assistant_state', {
|
|
97
|
-
sessionId:
|
|
124
|
+
sessionId: _session.id,
|
|
98
125
|
state: 'interrupted',
|
|
99
126
|
});
|
|
100
127
|
},
|
|
101
128
|
publishError: async (_session, message, extra = {}) => {
|
|
102
129
|
socket.emit('voice:error', {
|
|
103
|
-
sessionId:
|
|
130
|
+
sessionId: _session.id,
|
|
104
131
|
error: message,
|
|
105
132
|
...extra,
|
|
106
133
|
});
|
|
107
134
|
},
|
|
108
|
-
close: async () => {
|
|
135
|
+
close: async (_session) => {
|
|
109
136
|
socket.emit('voice:assistant_state', {
|
|
110
|
-
sessionId:
|
|
137
|
+
sessionId: _session.id,
|
|
111
138
|
state: 'closed',
|
|
112
139
|
});
|
|
113
140
|
},
|
|
114
141
|
},
|
|
115
142
|
});
|
|
143
|
+
}
|
|
116
144
|
|
|
117
|
-
|
|
118
|
-
this.
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
async openWearableSession({ userId, agentId = null, sessionId = null, sink } = {}) {
|
|
146
|
+
return this.openSession({
|
|
147
|
+
userId,
|
|
148
|
+
agentId,
|
|
149
|
+
sessionId,
|
|
150
|
+
platform: 'wearable_live',
|
|
151
|
+
outputMode: 'audio_and_text',
|
|
152
|
+
sink,
|
|
153
|
+
});
|
|
121
154
|
}
|
|
122
155
|
|
|
123
156
|
async closeSession(sessionId, reason = 'closed') {
|
|
@@ -446,6 +479,7 @@ class VoiceRuntimeManager {
|
|
|
446
479
|
for (const attempt of ttsAttempts) {
|
|
447
480
|
index = 0;
|
|
448
481
|
streamError = null;
|
|
482
|
+
let attemptChunks = 0;
|
|
449
483
|
try {
|
|
450
484
|
await synthesizeVoiceReplyStream(
|
|
451
485
|
spokenContent,
|
|
@@ -459,13 +493,21 @@ class VoiceRuntimeManager {
|
|
|
459
493
|
audioBase64: audioBytes.toString('base64'),
|
|
460
494
|
mimeType,
|
|
461
495
|
});
|
|
496
|
+
attemptChunks += 1;
|
|
462
497
|
index += 1;
|
|
463
498
|
},
|
|
464
499
|
);
|
|
500
|
+
if (attemptChunks === 0) {
|
|
501
|
+
throw new Error(`${attempt.provider} TTS produced no audio chunks.`);
|
|
502
|
+
}
|
|
465
503
|
streamError = null;
|
|
466
504
|
break;
|
|
467
505
|
} catch (error) {
|
|
468
506
|
streamError = String(error?.message || error || 'Voice playback failed.');
|
|
507
|
+
console.warn(`[VoiceRuntime] ${attempt.provider} TTS failed for flutter session ${sessionId}: ${streamError}`);
|
|
508
|
+
if (attemptChunks > 0) {
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
469
511
|
}
|
|
470
512
|
}
|
|
471
513
|
} catch (error) {
|
|
@@ -490,6 +532,99 @@ class VoiceRuntimeManager {
|
|
|
490
532
|
}
|
|
491
533
|
}
|
|
492
534
|
|
|
535
|
+
async deliverWearableAssistantOutput(ws, sessionId, content, options = {}) {
|
|
536
|
+
const session = this.getSession(sessionId);
|
|
537
|
+
if (!session) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const kind = String(options.kind || 'final').trim() || 'final';
|
|
541
|
+
ws.send(JSON.stringify({
|
|
542
|
+
type: 'voice:assistant_text',
|
|
543
|
+
sessionId,
|
|
544
|
+
content,
|
|
545
|
+
kind,
|
|
546
|
+
}));
|
|
547
|
+
|
|
548
|
+
if (kind === 'final') {
|
|
549
|
+
await session.setState('speaking', { kind });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const voiceOptions = normalizeVoiceSynthesisOptions({
|
|
553
|
+
provider: session.voiceSettings?.liveProvider,
|
|
554
|
+
model: session.voiceSettings?.liveTtsModel,
|
|
555
|
+
voice: session.voiceSettings?.liveVoice,
|
|
556
|
+
transport: 'wearable',
|
|
557
|
+
responseFormat: 'wav',
|
|
558
|
+
});
|
|
559
|
+
const spokenContent = sanitizeSpeechText(content);
|
|
560
|
+
let index = 0;
|
|
561
|
+
let streamError = null;
|
|
562
|
+
const ttsAttempts = this.#buildTtsAttemptOrder(session, voiceOptions);
|
|
563
|
+
|
|
564
|
+
if (spokenContent) {
|
|
565
|
+
try {
|
|
566
|
+
for (const attempt of ttsAttempts) {
|
|
567
|
+
index = 0;
|
|
568
|
+
streamError = null;
|
|
569
|
+
let attemptChunks = 0;
|
|
570
|
+
try {
|
|
571
|
+
await synthesizeVoiceReplyStream(
|
|
572
|
+
spokenContent,
|
|
573
|
+
attempt,
|
|
574
|
+
async ({ audioBytes, mimeType }) => {
|
|
575
|
+
if (session.closed || session.interrupted) return;
|
|
576
|
+
ws.send(JSON.stringify({
|
|
577
|
+
type: 'voice:audio_chunk',
|
|
578
|
+
sessionId,
|
|
579
|
+
kind,
|
|
580
|
+
index,
|
|
581
|
+
audioBase64: audioBytes.toString('base64'),
|
|
582
|
+
mimeType,
|
|
583
|
+
}));
|
|
584
|
+
attemptChunks += 1;
|
|
585
|
+
index += 1;
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
if (attemptChunks === 0) {
|
|
589
|
+
throw new Error(`${attempt.provider} TTS produced no audio chunks.`);
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
} catch (error) {
|
|
593
|
+
streamError = String(error?.message || error || 'Voice playback failed.');
|
|
594
|
+
console.warn(`[VoiceRuntime] ${attempt.provider} TTS failed for wearable session ${sessionId}: ${streamError}`);
|
|
595
|
+
if (attemptChunks > 0) {
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (error) {
|
|
601
|
+
streamError = String(error?.message || error || 'Voice playback failed.');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!streamError && !session.closed && !session.interrupted) {
|
|
606
|
+
ws.send(JSON.stringify({
|
|
607
|
+
type: 'voice:audio_done',
|
|
608
|
+
sessionId,
|
|
609
|
+
kind,
|
|
610
|
+
totalChunks: index,
|
|
611
|
+
}));
|
|
612
|
+
} else if (kind === 'final' && !session.closed && !session.interrupted) {
|
|
613
|
+
ws.send(JSON.stringify({
|
|
614
|
+
type: 'voice:error',
|
|
615
|
+
sessionId,
|
|
616
|
+
error: streamError,
|
|
617
|
+
recoverable: true,
|
|
618
|
+
phase: 'tts',
|
|
619
|
+
}));
|
|
620
|
+
await session.setState('degraded', { kind, phase: 'tts' });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (kind === 'final' && !streamError) {
|
|
624
|
+
await session.setState('idle');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
493
628
|
#buildTtsAttemptOrder(session, voiceOptions) {
|
|
494
629
|
const attempts = [];
|
|
495
630
|
const providers = [
|
|
@@ -501,6 +636,8 @@ class VoiceRuntimeManager {
|
|
|
501
636
|
provider,
|
|
502
637
|
model: provider === voiceOptions.provider ? voiceOptions.model : null,
|
|
503
638
|
voice: provider === voiceOptions.provider ? voiceOptions.voice : null,
|
|
639
|
+
transport: voiceOptions.transport,
|
|
640
|
+
responseFormat: voiceOptions.responseFormat,
|
|
504
641
|
});
|
|
505
642
|
const runtime = provider === voiceOptions.provider
|
|
506
643
|
? {
|
|
@@ -110,8 +110,35 @@ async function runVoiceTranscriptTurn({
|
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
const replyText = String(runResult?.content || '').trim();
|
|
113
|
+
const assistantMetadata = {
|
|
114
|
+
...persistedMetadata,
|
|
115
|
+
platform,
|
|
116
|
+
tokens: runResult?.totalTokens || 0,
|
|
117
|
+
screenshotIncluded: Boolean(screenshotContext),
|
|
118
|
+
screenshotVisionProvider: screenshotContext?.provider || null,
|
|
119
|
+
screenshotVisionModel: screenshotContext?.model || null,
|
|
120
|
+
};
|
|
113
121
|
if (!replyText) {
|
|
114
|
-
|
|
122
|
+
db.prepare('INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)')
|
|
123
|
+
.run(
|
|
124
|
+
userId,
|
|
125
|
+
agentId,
|
|
126
|
+
runResult?.runId || null,
|
|
127
|
+
'assistant',
|
|
128
|
+
'',
|
|
129
|
+
JSON.stringify(assistantMetadata),
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
runId: runResult?.runId || null,
|
|
133
|
+
transcript: transcriptText,
|
|
134
|
+
replyText: '',
|
|
135
|
+
ttsProvider: voiceOptions.provider,
|
|
136
|
+
ttsModel: voiceOptions.model,
|
|
137
|
+
ttsVoice: voiceOptions.voice,
|
|
138
|
+
audioMimeType: 'audio/mpeg',
|
|
139
|
+
audioBase64: '',
|
|
140
|
+
ttsError: null,
|
|
141
|
+
};
|
|
115
142
|
}
|
|
116
143
|
|
|
117
144
|
db.prepare('INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)')
|
|
@@ -121,14 +148,7 @@ async function runVoiceTranscriptTurn({
|
|
|
121
148
|
runResult?.runId || null,
|
|
122
149
|
'assistant',
|
|
123
150
|
replyText,
|
|
124
|
-
JSON.stringify(
|
|
125
|
-
...persistedMetadata,
|
|
126
|
-
platform,
|
|
127
|
-
tokens: runResult?.totalTokens || 0,
|
|
128
|
-
screenshotIncluded: Boolean(screenshotContext),
|
|
129
|
-
screenshotVisionProvider: screenshotContext?.provider || null,
|
|
130
|
-
screenshotVisionModel: screenshotContext?.model || null,
|
|
131
|
-
}),
|
|
151
|
+
JSON.stringify(assistantMetadata),
|
|
132
152
|
);
|
|
133
153
|
|
|
134
154
|
let synthesized;
|