From bbe8f9096a6b5d1a95c3a5e0bc88e3eefb61457d Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Sun, 19 Apr 2026 16:59:27 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=96=B0=E3=81=97=E3=81=84?= =?UTF-8?q?=E3=82=A6=E3=82=A7=E3=82=A4=E3=82=AF=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=83=AF=E3=83=BC=E3=83=89=E9=8C=B2=E9=9F=B3=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=82=B5=E3=83=B3=E3=83=97=E3=83=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example_apps/record_wakeup_word.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 example_apps/record_wakeup_word.py diff --git a/example_apps/record_wakeup_word.py b/example_apps/record_wakeup_word.py new file mode 100644 index 0000000..67b27ea --- /dev/null +++ b/example_apps/record_wakeup_word.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import logging +import os +from logging import getLogger +from this import d + +from dotenv import load_dotenv + +from stackchan_server.app import StackChanApp +from stackchan_server.ws_proxy import EmptyTranscriptError, WsProxy + +logger = getLogger(__name__) +logging.basicConfig( + level=os.getenv("STACKCHAN_LOG_LEVEL", "INFO"), + format="%(asctime)s.%(msecs)03d %(levelname)s:%(name)s:%(message)s", + datefmt="%H:%M:%S", +) + +load_dotenv() + + +app = StackChanApp() + + +@app.setup +async def setup(proxy: WsProxy): + logger.info("WebSocket connected") + + +@app.talk_session +async def talk_session(proxy: WsProxy): + while True: + try: + text = await proxy.listen() + except EmptyTranscriptError: + return + if not text: + return + logger.info("Heard: %s", text) + await proxy.speak(text) + +@app.webapi("/record_wakeup_word") +async def record_wakeup_word(proxy: WsProxy, args: dict): + logger.info("Recording wakeup word...") + await proxy.speak("これからウェイクアップワードの録音を開始します。ピッと鳴ったら、ウェイクアップワードを話してください。") + await proxy.tone(4000, 200) + raw_audio = await proxy.listen_raw(duration=3000) # 3秒間録音 + await proxy.tone(1000, 200) From 1c007068af629b58c73426d181d9937ae2d1017c Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Sun, 19 Apr 2026 17:22:46 +0900 Subject: [PATCH 2/4] feat: add tone command functionality and fixed duration recording - Implemented tone command handling in the firmware, allowing the device to play tones with specified frequency and duration. - Added support for fixed duration recording in the Listening class, enabling recording to stop after a specified time or after a period of silence. - Updated protobuf definitions to include ToneCommand and ToneDoneEvent messages. - Enhanced the WebSocket server to handle tone commands and notify when tones are completed. - Refactored the ListenHandler to support raw audio listening and improved result handling. --- .gitignore | 1 + example_apps/record_wakeup_word.py | 38 ++++++- firmware/include/listening.hpp | 6 ++ .../generated_protobuf/websocket-message.pb.c | 6 ++ .../generated_protobuf/websocket-message.pb.h | 64 +++++++++-- firmware/src/listening.cpp | 32 +++++- firmware/src/main.cpp | 79 ++++++++++++++ protobuf/websocket-message.proto | 14 +++ stackchan_server/app.py | 38 ++++++- .../websocket_message_pb2.py | 88 +++++++-------- stackchan_server/listen.py | 102 +++++++++++++++--- stackchan_server/protobuf_ws.py | 36 ++++++- stackchan_server/ws_proxy.py | 84 ++++++++++++++- 13 files changed, 513 insertions(+), 75 deletions(-) diff --git a/.gitignore b/.gitignore index 768ebf5..1e89031 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .vscode/launch.json .vscode/ipch .log +tmp/ firmware/include/config.h recordings ._.DS_Store diff --git a/example_apps/record_wakeup_word.py b/example_apps/record_wakeup_word.py index 67b27ea..b23bb5b 100644 --- a/example_apps/record_wakeup_word.py +++ b/example_apps/record_wakeup_word.py @@ -1,13 +1,17 @@ from __future__ import annotations +import asyncio import logging import os +import wave +from datetime import UTC, datetime from logging import getLogger -from this import d +from pathlib import Path from dotenv import load_dotenv from stackchan_server.app import StackChanApp +from stackchan_server.static import LISTEN_AUDIO_FORMAT from stackchan_server.ws_proxy import EmptyTranscriptError, WsProxy logger = getLogger(__name__) @@ -40,10 +44,36 @@ async def talk_session(proxy: WsProxy): logger.info("Heard: %s", text) await proxy.speak(text) + @app.webapi("/record_wakeup_word") async def record_wakeup_word(proxy: WsProxy, args: dict): - logger.info("Recording wakeup word...") - await proxy.speak("これからウェイクアップワードの録音を開始します。ピッと鳴ったら、ウェイクアップワードを話してください。") + duration_ms = int(args.get("duration_ms", args.get("duration", 3000))) + logger.info("Recording wakeup word duration_ms=%d", duration_ms) + await proxy.speak( + "これからウェイクアップワードの録音を開始します。ピッと鳴ったら、ウェイクアップワードを話してください。" + ) await proxy.tone(4000, 200) - raw_audio = await proxy.listen_raw(duration=3000) # 3秒間録音 + raw_audio = await proxy.listen_raw(duration=duration_ms) + await asyncio.sleep(0.5) await proxy.tone(1000, 200) + + output_dir = Path("tmp") + output_dir.mkdir(parents=True, exist_ok=True) + filename = f"wakeup_word_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S_%f')}.wav" + filepath = output_dir / filename + + with wave.open(str(filepath), "wb") as wav_fp: + wav_fp.setnchannels(LISTEN_AUDIO_FORMAT.channels) + wav_fp.setsampwidth(LISTEN_AUDIO_FORMAT.sample_width) + wav_fp.setframerate(LISTEN_AUDIO_FORMAT.sample_rate_hz) + wav_fp.writeframes(raw_audio) + + logger.info("Saved wakeup word recording to %s", filepath) + return { + "path": str(filepath), + "bytes": len(raw_audio), + "sample_rate": LISTEN_AUDIO_FORMAT.sample_rate_hz, + "channels": LISTEN_AUDIO_FORMAT.channels, + "sample_width": LISTEN_AUDIO_FORMAT.sample_width, + "duration_ms": duration_ms, + } diff --git a/firmware/include/listening.hpp b/firmware/include/listening.hpp index 0e18ba8..8d2932a 100644 --- a/firmware/include/listening.hpp +++ b/firmware/include/listening.hpp @@ -34,6 +34,10 @@ class Listening // 無音が所定時間続いているか判定 bool shouldStopForSilence() const; + // 固定時間録音モードを設定(0で通常の無音停止モード) + void setFixedDurationMs(uint32_t durationMs); + bool shouldStopForFixedDuration() const; + private: void updateLevelStats(const int16_t *samples, size_t sampleCount); bool sendPacket(stackchan_websocket_v1_MessageType type, const int16_t *samples, size_t sampleCount); @@ -60,6 +64,8 @@ class Listening // 無音判定関連 int32_t last_level_ = 0; uint32_t silence_since_ms_ = 0; + uint32_t stream_started_ms_ = 0; + uint32_t fixed_duration_ms_ = 0; static constexpr int32_t kSilenceLevelThreshold = 200; // 平均絶対値がこの値以下を無音とみなす static constexpr uint32_t kSilenceDurationMs = 3000; // 無音とみなす継続時間 }; diff --git a/firmware/lib/generated_protobuf/websocket-message.pb.c b/firmware/lib/generated_protobuf/websocket-message.pb.c index f70a79e..a25a717 100644 --- a/firmware/lib/generated_protobuf/websocket-message.pb.c +++ b/firmware/lib/generated_protobuf/websocket-message.pb.c @@ -45,6 +45,12 @@ PB_BIND(stackchan_websocket_v1_ServoCommand, stackchan_websocket_v1_ServoCommand PB_BIND(stackchan_websocket_v1_ServoDoneEvent, stackchan_websocket_v1_ServoDoneEvent, AUTO) +PB_BIND(stackchan_websocket_v1_ToneCommand, stackchan_websocket_v1_ToneCommand, AUTO) + + +PB_BIND(stackchan_websocket_v1_ToneDoneEvent, stackchan_websocket_v1_ToneDoneEvent, AUTO) + + PB_BIND(stackchan_websocket_v1_FirmwareMetadata, stackchan_websocket_v1_FirmwareMetadata, AUTO) diff --git a/firmware/lib/generated_protobuf/websocket-message.pb.h b/firmware/lib/generated_protobuf/websocket-message.pb.h index 8e0c222..60a5882 100644 --- a/firmware/lib/generated_protobuf/websocket-message.pb.h +++ b/firmware/lib/generated_protobuf/websocket-message.pb.h @@ -21,7 +21,9 @@ typedef enum _stackchan_websocket_v1_MessageKind { stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_CMD = 7, stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_DONE_EVT = 8, stackchan_websocket_v1_MessageKind_MESSAGE_KIND_FIRMWARE_METADATA = 9, - stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA = 10 + stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA = 10, + stackchan_websocket_v1_MessageKind_MESSAGE_KIND_TONE_CMD = 11, + stackchan_websocket_v1_MessageKind_MESSAGE_KIND_TONE_DONE_EVT = 12 } stackchan_websocket_v1_MessageKind; typedef enum _stackchan_websocket_v1_MessageType { @@ -83,6 +85,7 @@ typedef struct _stackchan_websocket_v1_AudioChunk { typedef struct _stackchan_websocket_v1_StateCommand { stackchan_websocket_v1_StackchanState state; + uint32_t listening_duration_ms; /* used when state=LISTENING, 0 means silence-based stop */ } stackchan_websocket_v1_StateCommand; typedef struct _stackchan_websocket_v1_WakeWordEvent { @@ -112,6 +115,15 @@ typedef struct _stackchan_websocket_v1_ServoDoneEvent { bool done; } stackchan_websocket_v1_ServoDoneEvent; +typedef struct _stackchan_websocket_v1_ToneCommand { + float frequency; + uint32_t duration_ms; +} stackchan_websocket_v1_ToneCommand; + +typedef struct _stackchan_websocket_v1_ToneDoneEvent { + bool done; +} stackchan_websocket_v1_ToneDoneEvent; + typedef struct _stackchan_websocket_v1_FirmwareMetadata { stackchan_websocket_v1_DeviceType device_type; uint32_t display_width; @@ -155,6 +167,8 @@ typedef struct _stackchan_websocket_v1_WebSocketMessage { stackchan_websocket_v1_ServoDoneEvent servo_done_evt; stackchan_websocket_v1_FirmwareMetadata firmware_metadata; stackchan_websocket_v1_ServerMetadata server_metadata; + stackchan_websocket_v1_ToneCommand tone_cmd; + stackchan_websocket_v1_ToneDoneEvent tone_done_evt; } body; } stackchan_websocket_v1_WebSocketMessage; @@ -165,8 +179,8 @@ extern "C" { /* Helper constants for enums */ #define _stackchan_websocket_v1_MessageKind_MIN stackchan_websocket_v1_MessageKind_MESSAGE_KIND_UNSPECIFIED -#define _stackchan_websocket_v1_MessageKind_MAX stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA -#define _stackchan_websocket_v1_MessageKind_ARRAYSIZE ((stackchan_websocket_v1_MessageKind)(stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA+1)) +#define _stackchan_websocket_v1_MessageKind_MAX stackchan_websocket_v1_MessageKind_MESSAGE_KIND_TONE_DONE_EVT +#define _stackchan_websocket_v1_MessageKind_ARRAYSIZE ((stackchan_websocket_v1_MessageKind)(stackchan_websocket_v1_MessageKind_MESSAGE_KIND_TONE_DONE_EVT+1)) #define _stackchan_websocket_v1_MessageType_MIN stackchan_websocket_v1_MessageType_MESSAGE_TYPE_UNSPECIFIED #define _stackchan_websocket_v1_MessageType_MAX stackchan_websocket_v1_MessageType_MESSAGE_TYPE_END @@ -206,6 +220,8 @@ extern "C" { #define stackchan_websocket_v1_ServoCommand_op_ENUMTYPE stackchan_websocket_v1_ServoOperation + + #define stackchan_websocket_v1_FirmwareMetadata_device_type_ENUMTYPE stackchan_websocket_v1_DeviceType #define stackchan_websocket_v1_FirmwareMetadata_servo_type_ENUMTYPE stackchan_websocket_v1_ServoType @@ -218,13 +234,15 @@ extern "C" { #define stackchan_websocket_v1_AudioWavStart_init_default {0, 0} #define stackchan_websocket_v1_AudioWavEnd_init_default {0} #define stackchan_websocket_v1_AudioChunk_init_default {{0, {0}}} -#define stackchan_websocket_v1_StateCommand_init_default {_stackchan_websocket_v1_StackchanState_MIN} +#define stackchan_websocket_v1_StateCommand_init_default {_stackchan_websocket_v1_StackchanState_MIN, 0} #define stackchan_websocket_v1_WakeWordEvent_init_default {0} #define stackchan_websocket_v1_StateEvent_init_default {_stackchan_websocket_v1_StackchanState_MIN} #define stackchan_websocket_v1_SpeakDoneEvent_init_default {0} #define stackchan_websocket_v1_ServoCommandSequence_init_default {0, {stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default}} #define stackchan_websocket_v1_ServoCommand_init_default {_stackchan_websocket_v1_ServoOperation_MIN, 0, 0} #define stackchan_websocket_v1_ServoDoneEvent_init_default {0} +#define stackchan_websocket_v1_ToneCommand_init_default {0, 0} +#define stackchan_websocket_v1_ToneDoneEvent_init_default {0} #define stackchan_websocket_v1_FirmwareMetadata_init_default {_stackchan_websocket_v1_DeviceType_MIN, 0, 0, 0, 0, _stackchan_websocket_v1_ServoType_MIN, 0, ""} #define stackchan_websocket_v1_ServerMetadata_init_default {0, ""} #define stackchan_websocket_v1_WebSocketMessage_init_zero {_stackchan_websocket_v1_MessageKind_MIN, _stackchan_websocket_v1_MessageType_MIN, 0, 0, {stackchan_websocket_v1_AudioPcmStart_init_zero}} @@ -233,13 +251,15 @@ extern "C" { #define stackchan_websocket_v1_AudioWavStart_init_zero {0, 0} #define stackchan_websocket_v1_AudioWavEnd_init_zero {0} #define stackchan_websocket_v1_AudioChunk_init_zero {{0, {0}}} -#define stackchan_websocket_v1_StateCommand_init_zero {_stackchan_websocket_v1_StackchanState_MIN} +#define stackchan_websocket_v1_StateCommand_init_zero {_stackchan_websocket_v1_StackchanState_MIN, 0} #define stackchan_websocket_v1_WakeWordEvent_init_zero {0} #define stackchan_websocket_v1_StateEvent_init_zero {_stackchan_websocket_v1_StackchanState_MIN} #define stackchan_websocket_v1_SpeakDoneEvent_init_zero {0} #define stackchan_websocket_v1_ServoCommandSequence_init_zero {0, {stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero}} #define stackchan_websocket_v1_ServoCommand_init_zero {_stackchan_websocket_v1_ServoOperation_MIN, 0, 0} #define stackchan_websocket_v1_ServoDoneEvent_init_zero {0} +#define stackchan_websocket_v1_ToneCommand_init_zero {0, 0} +#define stackchan_websocket_v1_ToneDoneEvent_init_zero {0} #define stackchan_websocket_v1_FirmwareMetadata_init_zero {_stackchan_websocket_v1_DeviceType_MIN, 0, 0, 0, 0, _stackchan_websocket_v1_ServoType_MIN, 0, ""} #define stackchan_websocket_v1_ServerMetadata_init_zero {0, ""} @@ -248,6 +268,7 @@ extern "C" { #define stackchan_websocket_v1_AudioWavStart_channels_tag 2 #define stackchan_websocket_v1_AudioChunk_pcm_bytes_tag 1 #define stackchan_websocket_v1_StateCommand_state_tag 1 +#define stackchan_websocket_v1_StateCommand_listening_duration_ms_tag 2 #define stackchan_websocket_v1_WakeWordEvent_detected_tag 1 #define stackchan_websocket_v1_StateEvent_state_tag 1 #define stackchan_websocket_v1_SpeakDoneEvent_done_tag 1 @@ -256,6 +277,9 @@ extern "C" { #define stackchan_websocket_v1_ServoCommand_duration_ms_tag 3 #define stackchan_websocket_v1_ServoCommandSequence_commands_tag 1 #define stackchan_websocket_v1_ServoDoneEvent_done_tag 1 +#define stackchan_websocket_v1_ToneCommand_frequency_tag 1 +#define stackchan_websocket_v1_ToneCommand_duration_ms_tag 2 +#define stackchan_websocket_v1_ToneDoneEvent_done_tag 1 #define stackchan_websocket_v1_FirmwareMetadata_device_type_tag 1 #define stackchan_websocket_v1_FirmwareMetadata_display_width_tag 2 #define stackchan_websocket_v1_FirmwareMetadata_display_height_tag 3 @@ -283,6 +307,8 @@ extern "C" { #define stackchan_websocket_v1_WebSocketMessage_servo_done_evt_tag 35 #define stackchan_websocket_v1_WebSocketMessage_firmware_metadata_tag 36 #define stackchan_websocket_v1_WebSocketMessage_server_metadata_tag 37 +#define stackchan_websocket_v1_WebSocketMessage_tone_cmd_tag 38 +#define stackchan_websocket_v1_WebSocketMessage_tone_done_evt_tag 39 /* Struct field encoding specification for nanopb */ #define stackchan_websocket_v1_WebSocketMessage_FIELDLIST(X, a) \ @@ -302,7 +328,9 @@ X(a, STATIC, ONEOF, MESSAGE, (body,speak_done_evt,body.speak_done_evt), 3 X(a, STATIC, ONEOF, MESSAGE, (body,servo_cmd,body.servo_cmd), 34) \ X(a, STATIC, ONEOF, MESSAGE, (body,servo_done_evt,body.servo_done_evt), 35) \ X(a, STATIC, ONEOF, MESSAGE, (body,firmware_metadata,body.firmware_metadata), 36) \ -X(a, STATIC, ONEOF, MESSAGE, (body,server_metadata,body.server_metadata), 37) +X(a, STATIC, ONEOF, MESSAGE, (body,server_metadata,body.server_metadata), 37) \ +X(a, STATIC, ONEOF, MESSAGE, (body,tone_cmd,body.tone_cmd), 38) \ +X(a, STATIC, ONEOF, MESSAGE, (body,tone_done_evt,body.tone_done_evt), 39) #define stackchan_websocket_v1_WebSocketMessage_CALLBACK NULL #define stackchan_websocket_v1_WebSocketMessage_DEFAULT NULL #define stackchan_websocket_v1_WebSocketMessage_body_audio_pcm_start_MSGTYPE stackchan_websocket_v1_AudioPcmStart @@ -319,6 +347,8 @@ X(a, STATIC, ONEOF, MESSAGE, (body,server_metadata,body.server_metadata), #define stackchan_websocket_v1_WebSocketMessage_body_servo_done_evt_MSGTYPE stackchan_websocket_v1_ServoDoneEvent #define stackchan_websocket_v1_WebSocketMessage_body_firmware_metadata_MSGTYPE stackchan_websocket_v1_FirmwareMetadata #define stackchan_websocket_v1_WebSocketMessage_body_server_metadata_MSGTYPE stackchan_websocket_v1_ServerMetadata +#define stackchan_websocket_v1_WebSocketMessage_body_tone_cmd_MSGTYPE stackchan_websocket_v1_ToneCommand +#define stackchan_websocket_v1_WebSocketMessage_body_tone_done_evt_MSGTYPE stackchan_websocket_v1_ToneDoneEvent #define stackchan_websocket_v1_AudioPcmStart_FIELDLIST(X, a) \ @@ -347,7 +377,8 @@ X(a, STATIC, SINGULAR, BYTES, pcm_bytes, 1) #define stackchan_websocket_v1_AudioChunk_DEFAULT NULL #define stackchan_websocket_v1_StateCommand_FIELDLIST(X, a) \ -X(a, STATIC, SINGULAR, UENUM, state, 1) +X(a, STATIC, SINGULAR, UENUM, state, 1) \ +X(a, STATIC, SINGULAR, UINT32, listening_duration_ms, 2) #define stackchan_websocket_v1_StateCommand_CALLBACK NULL #define stackchan_websocket_v1_StateCommand_DEFAULT NULL @@ -384,6 +415,17 @@ X(a, STATIC, SINGULAR, BOOL, done, 1) #define stackchan_websocket_v1_ServoDoneEvent_CALLBACK NULL #define stackchan_websocket_v1_ServoDoneEvent_DEFAULT NULL +#define stackchan_websocket_v1_ToneCommand_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, FLOAT, frequency, 1) \ +X(a, STATIC, SINGULAR, UINT32, duration_ms, 2) +#define stackchan_websocket_v1_ToneCommand_CALLBACK NULL +#define stackchan_websocket_v1_ToneCommand_DEFAULT NULL + +#define stackchan_websocket_v1_ToneDoneEvent_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, done, 1) +#define stackchan_websocket_v1_ToneDoneEvent_CALLBACK NULL +#define stackchan_websocket_v1_ToneDoneEvent_DEFAULT NULL + #define stackchan_websocket_v1_FirmwareMetadata_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, device_type, 1) \ X(a, STATIC, SINGULAR, UINT32, display_width, 2) \ @@ -415,6 +457,8 @@ extern const pb_msgdesc_t stackchan_websocket_v1_SpeakDoneEvent_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServoCommandSequence_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServoCommand_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServoDoneEvent_msg; +extern const pb_msgdesc_t stackchan_websocket_v1_ToneCommand_msg; +extern const pb_msgdesc_t stackchan_websocket_v1_ToneDoneEvent_msg; extern const pb_msgdesc_t stackchan_websocket_v1_FirmwareMetadata_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServerMetadata_msg; @@ -432,6 +476,8 @@ extern const pb_msgdesc_t stackchan_websocket_v1_ServerMetadata_msg; #define stackchan_websocket_v1_ServoCommandSequence_fields &stackchan_websocket_v1_ServoCommandSequence_msg #define stackchan_websocket_v1_ServoCommand_fields &stackchan_websocket_v1_ServoCommand_msg #define stackchan_websocket_v1_ServoDoneEvent_fields &stackchan_websocket_v1_ServoDoneEvent_msg +#define stackchan_websocket_v1_ToneCommand_fields &stackchan_websocket_v1_ToneCommand_msg +#define stackchan_websocket_v1_ToneDoneEvent_fields &stackchan_websocket_v1_ToneDoneEvent_msg #define stackchan_websocket_v1_FirmwareMetadata_fields &stackchan_websocket_v1_FirmwareMetadata_msg #define stackchan_websocket_v1_ServerMetadata_fields &stackchan_websocket_v1_ServerMetadata_msg @@ -448,8 +494,10 @@ extern const pb_msgdesc_t stackchan_websocket_v1_ServerMetadata_msg; #define stackchan_websocket_v1_ServoCommand_size 14 #define stackchan_websocket_v1_ServoDoneEvent_size 2 #define stackchan_websocket_v1_SpeakDoneEvent_size 2 -#define stackchan_websocket_v1_StateCommand_size 2 +#define stackchan_websocket_v1_StateCommand_size 8 #define stackchan_websocket_v1_StateEvent_size 2 +#define stackchan_websocket_v1_ToneCommand_size 11 +#define stackchan_websocket_v1_ToneDoneEvent_size 2 #define stackchan_websocket_v1_WakeWordEvent_size 2 #define stackchan_websocket_v1_WebSocketMessage_size 4113 diff --git a/firmware/src/listening.cpp b/firmware/src/listening.cpp index edb2e35..cb7866f 100644 --- a/firmware/src/listening.cpp +++ b/firmware/src/listening.cpp @@ -49,6 +49,7 @@ void Listening::end() { stopStreaming(); M5.Mic.end(); + fixed_duration_ms_ = 0; } bool Listening::startStreaming() @@ -57,6 +58,7 @@ bool Listening::startStreaming() seq_counter_ = 0; last_level_ = 0; silence_since_ms_ = 0; + stream_started_ms_ = millis(); streaming_ = true; return sendPacket(stackchan_websocket_v1_MessageType_MESSAGE_TYPE_START, nullptr, 0); } @@ -128,10 +130,18 @@ void Listening::loop() } } - // 無音が3秒続いたら終了 - if (shouldStopForSilence()) + // 固定時間録音 or 無音が3秒続いたら終了 + if ((fixed_duration_ms_ > 0 && shouldStopForFixedDuration()) || + (fixed_duration_ms_ == 0 && shouldStopForSilence())) { - log_i("Auto stop: silence detected (avg=%ld)", static_cast(last_level_)); + if (fixed_duration_ms_ > 0) + { + log_i("Auto stop: fixed duration reached (%lu ms)", static_cast(fixed_duration_ms_)); + } + else + { + log_i("Auto stop: silence detected (avg=%ld)", static_cast(last_level_)); + } if (!stopStreaming()) { log_i("WS send failed (tail/end)"); @@ -187,6 +197,22 @@ bool Listening::shouldStopForSilence() const return elapsed >= kSilenceDurationMs; } +void Listening::setFixedDurationMs(uint32_t durationMs) +{ + fixed_duration_ms_ = durationMs; +} + +bool Listening::shouldStopForFixedDuration() const +{ + if (fixed_duration_ms_ == 0 || stream_started_ms_ == 0) + { + return false; + } + + uint32_t elapsed = millis() - stream_started_ms_; + return elapsed >= fixed_duration_ms_; +} + bool Listening::sendPacket(stackchan_websocket_v1_MessageType type, const int16_t *samples, size_t sampleCount) { if ((WiFi.status() != WL_CONNECTED) || !ws_.isConnected()) diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 9756ec0..f35ad68 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -42,8 +42,10 @@ namespace uint32_t g_uplink_seq = 0; uint32_t g_last_comm_ms = 0; constexpr uint32_t kCommTimeoutMs = 60000; +constexpr int kToneChannel = 1; stackchan_websocket_v1_WebSocketMessage g_tx_message = stackchan_websocket_v1_WebSocketMessage_init_zero; stackchan_websocket_v1_WebSocketMessage g_rx_message = stackchan_websocket_v1_WebSocketMessage_init_zero; +bool g_tone_playing = false; void markCommunicationActive() { @@ -163,20 +165,39 @@ void notifyServoDone() } } +void notifyToneDone() +{ + auto &message = g_tx_message; + message = stackchan_websocket_v1_WebSocketMessage_init_zero; + message.kind = stackchan_websocket_v1_MessageKind_MESSAGE_KIND_TONE_DONE_EVT; + message.message_type = stackchan_websocket_v1_MessageType_MESSAGE_TYPE_DATA; + message.seq = g_uplink_seq++; + message.which_body = stackchan_websocket_v1_WebSocketMessage_tone_done_evt_tag; + message.body.tone_done_evt.done = true; + if (!sendUplinkMessage(message)) + { + log_w("Failed to send ToneDoneEvt"); + } +} + bool applyRemoteStateCommand(const stackchan_websocket_v1_StateCommand &command) { switch (command.state) { case stackchan_websocket_v1_StackchanState_STACKCHAN_STATE_IDLE: + listening.setFixedDurationMs(0); stateMachine.setState(StateMachine::Idle); return true; case stackchan_websocket_v1_StackchanState_STACKCHAN_STATE_LISTENING: + listening.setFixedDurationMs(command.listening_duration_ms); stateMachine.setState(StateMachine::Listening); return true; case stackchan_websocket_v1_StackchanState_STACKCHAN_STATE_THINKING: + listening.setFixedDurationMs(0); stateMachine.setState(StateMachine::Thinking); return true; case stackchan_websocket_v1_StackchanState_STACKCHAN_STATE_SPEAKING: + listening.setFixedDurationMs(0); stateMachine.setState(StateMachine::Speaking); return true; default: @@ -234,6 +255,52 @@ bool applyServoCommand(const stackchan_websocket_v1_ServoCommandSequence &sequen } return true; } + +bool applyToneCommand(const stackchan_websocket_v1_ToneCommand &command) +{ + if (command.frequency <= 0.0f) + { + log_w("ToneCmd frequency must be positive"); + return false; + } + if (command.duration_ms == 0) + { + log_w("ToneCmd duration must be positive"); + return false; + } + + if (!M5.Speaker.tone(command.frequency, command.duration_ms, kToneChannel, true)) + { + log_w( + "Failed to start tone frequency=%.1f duration=%lu", + command.frequency, + static_cast(command.duration_ms)); + return false; + } + + g_tone_playing = true; + log_i( + "ToneCmd frequency=%.1f duration=%lu", + command.frequency, + static_cast(command.duration_ms)); + return true; +} + +void pollTonePlayback() +{ + if (!g_tone_playing) + { + return; + } + + if (M5.Speaker.isPlaying(kToneChannel) != 0) + { + return; + } + + g_tone_playing = false; + notifyToneDone(); +} } // namespace void connectWiFi() @@ -335,6 +402,17 @@ void handleWsEvent(WStype_t type, uint8_t *payload, size_t length) log_w("ServoCmd protobuf body mismatch type=%u body=%u", (unsigned)rx.message_type, (unsigned)rx.which_body); } break; + case stackchan_websocket_v1_MessageKind_MESSAGE_KIND_TONE_CMD: + if (rx.message_type == stackchan_websocket_v1_MessageType_MESSAGE_TYPE_DATA && + rx.which_body == stackchan_websocket_v1_WebSocketMessage_tone_cmd_tag) + { + applyToneCommand(rx.body.tone_cmd); + } + else + { + log_w("ToneCmd protobuf body mismatch type=%u body=%u", (unsigned)rx.message_type, (unsigned)rx.which_body); + } + break; case stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA: if (rx.message_type == stackchan_websocket_v1_MessageType_MESSAGE_TYPE_DATA && rx.which_body == stackchan_websocket_v1_WebSocketMessage_server_metadata_tag) @@ -452,6 +530,7 @@ void loop() wsClient.loop(); handleCommunicationTimeout(); servo.loop(); + pollTonePlayback(); StateMachine::State current = stateMachine.getState(); switch (current) diff --git a/protobuf/websocket-message.proto b/protobuf/websocket-message.proto index c643673..742c131 100644 --- a/protobuf/websocket-message.proto +++ b/protobuf/websocket-message.proto @@ -31,6 +31,8 @@ message WebSocketMessage { ServoDoneEvent servo_done_evt = 35; FirmwareMetadata firmware_metadata = 36; ServerMetadata server_metadata = 37; + ToneCommand tone_cmd = 38; + ToneDoneEvent tone_done_evt = 39; } } @@ -46,6 +48,8 @@ enum MessageKind { MESSAGE_KIND_SERVO_DONE_EVT = 8; MESSAGE_KIND_FIRMWARE_METADATA = 9; MESSAGE_KIND_SERVER_METADATA = 10; + MESSAGE_KIND_TONE_CMD = 11; + MESSAGE_KIND_TONE_DONE_EVT = 12; } enum MessageType { @@ -99,6 +103,7 @@ message AudioChunk { message StateCommand { StackchanState state = 1; + uint32 listening_duration_ms = 2; // used when state=LISTENING, 0 means silence-based stop } message WakeWordEvent { @@ -127,6 +132,15 @@ message ServoDoneEvent { bool done = 1; } +message ToneCommand { + float frequency = 1; + uint32 duration_ms = 2; +} + +message ToneDoneEvent { + bool done = 1; +} + message FirmwareMetadata { DeviceType device_type = 1; uint32 display_width = 2; diff --git a/stackchan_server/app.py b/stackchan_server/app.py index 14496d2..93b6182 100644 --- a/stackchan_server/app.py +++ b/stackchan_server/app.py @@ -2,9 +2,9 @@ import asyncio from logging import getLogger -from typing import Awaitable, Callable, Optional +from typing import Any, Awaitable, Callable, Optional -from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect +from fastapi import Body, FastAPI, HTTPException, WebSocket, WebSocketDisconnect from pydantic import BaseModel from .speech_recognition import create_speech_recognizer @@ -35,6 +35,7 @@ def __init__( self.fastapi = FastAPI(title="StackChan WebSocket Server") self._setup_fn: Optional[Callable[[WsProxy], Awaitable[None]]] = None self._talk_session_fn: Optional[Callable[[WsProxy], Awaitable[None]]] = None + self._webapi_paths: set[str] = set() self._proxies: dict[str, WsProxy] = {} self._proxies_lock = asyncio.Lock() @@ -79,6 +80,39 @@ def talk_session(self, fn: Callable[["WsProxy"], Awaitable[None]]): self._talk_session_fn = fn return fn + def webapi( + self, path: str + ) -> Callable[[Callable[["WsProxy", dict[str, Any]], Awaitable[Any]]], Callable[["WsProxy", dict[str, Any]], Awaitable[Any]]]: + normalized_path = path if path.startswith("/") else f"/{path}" + route_path = f"/v1/stackchan/{{stackchan_ip}}{normalized_path}" + + def decorator( + fn: Callable[["WsProxy", dict[str, Any]], Awaitable[Any]] + ) -> Callable[["WsProxy", dict[str, Any]], Awaitable[Any]]: + if route_path in self._webapi_paths: + raise ValueError(f"webapi route already registered: {route_path}") + route_name = getattr(fn, "__name__", "webapi_handler") + + async def _webapi_handler( + stackchan_ip: str, + body: dict[str, Any] | None = Body(default=None), + ) -> Any: + proxy = await self._get_proxy(stackchan_ip) + if proxy is None: + raise HTTPException(status_code=404, detail="stackchan not connected") + return await fn(proxy, body or {}) + + self.fastapi.add_api_route( + route_path, + _webapi_handler, + methods=["GET"], + name=f"webapi_{route_name}", + ) + self._webapi_paths.add(route_path) + return fn + + return decorator + async def _handle_ws(self, websocket: WebSocket) -> None: await websocket.accept() client_ip = websocket.client.host if websocket.client else "unknown" diff --git a/stackchan_server/generated_protobuf/websocket_message_pb2.py b/stackchan_server/generated_protobuf/websocket_message_pb2.py index a7d7a4e..ce1ff52 100644 --- a/stackchan_server/generated_protobuf/websocket_message_pb2.py +++ b/stackchan_server/generated_protobuf/websocket_message_pb2.py @@ -24,53 +24,57 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17websocket-message.proto\x12\x16stackchan.websocket.v1\"\x96\x08\n\x10WebSocketMessage\x12\x31\n\x04kind\x18\x01 \x01(\x0e\x32#.stackchan.websocket.v1.MessageKind\x12\x39\n\x0cmessage_type\x18\x02 \x01(\x0e\x32#.stackchan.websocket.v1.MessageType\x12\x0b\n\x03seq\x18\x03 \x01(\r\x12@\n\x0f\x61udio_pcm_start\x18\n \x01(\x0b\x32%.stackchan.websocket.v1.AudioPcmStartH\x00\x12<\n\x0e\x61udio_pcm_data\x18\x0b \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_pcm_end\x18\x0c \x01(\x0b\x32#.stackchan.websocket.v1.AudioPcmEndH\x00\x12@\n\x0f\x61udio_wav_start\x18\x14 \x01(\x0b\x32%.stackchan.websocket.v1.AudioWavStartH\x00\x12<\n\x0e\x61udio_wav_data\x18\x15 \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_wav_end\x18\x16 \x01(\x0b\x32#.stackchan.websocket.v1.AudioWavEndH\x00\x12\x39\n\tstate_cmd\x18\x1e \x01(\x0b\x32$.stackchan.websocket.v1.StateCommandH\x00\x12>\n\rwake_word_evt\x18\x1f \x01(\x0b\x32%.stackchan.websocket.v1.WakeWordEventH\x00\x12\x37\n\tstate_evt\x18 \x01(\x0b\x32\".stackchan.websocket.v1.StateEventH\x00\x12@\n\x0espeak_done_evt\x18! \x01(\x0b\x32&.stackchan.websocket.v1.SpeakDoneEventH\x00\x12\x41\n\tservo_cmd\x18\" \x01(\x0b\x32,.stackchan.websocket.v1.ServoCommandSequenceH\x00\x12@\n\x0eservo_done_evt\x18# \x01(\x0b\x32&.stackchan.websocket.v1.ServoDoneEventH\x00\x12\x45\n\x11\x66irmware_metadata\x18$ \x01(\x0b\x32(.stackchan.websocket.v1.FirmwareMetadataH\x00\x12\x41\n\x0fserver_metadata\x18% \x01(\x0b\x32&.stackchan.websocket.v1.ServerMetadataH\x00\x42\x06\n\x04\x62ody\"\x0f\n\rAudioPcmStart\"\r\n\x0b\x41udioPcmEnd\"6\n\rAudioWavStart\x12\x13\n\x0bsample_rate\x18\x01 \x01(\r\x12\x10\n\x08\x63hannels\x18\x02 \x01(\r\"\r\n\x0b\x41udioWavEnd\"\x1f\n\nAudioChunk\x12\x11\n\tpcm_bytes\x18\x01 \x01(\x0c\"E\n\x0cStateCommand\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"!\n\rWakeWordEvent\x12\x10\n\x08\x64\x65tected\x18\x01 \x01(\x08\"C\n\nStateEvent\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"\x1e\n\x0eSpeakDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"N\n\x14ServoCommandSequence\x12\x36\n\x08\x63ommands\x18\x01 \x03(\x0b\x32$.stackchan.websocket.v1.ServoCommand\"f\n\x0cServoCommand\x12\x32\n\x02op\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.ServoOperation\x12\r\n\x05\x61ngle\x18\x02 \x01(\x11\x12\x13\n\x0b\x64uration_ms\x18\x03 \x01(\x11\"\x1e\n\x0eServoDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"\x99\x02\n\x10\x46irmwareMetadata\x12\x37\n\x0b\x64\x65vice_type\x18\x01 \x01(\x0e\x32\".stackchan.websocket.v1.DeviceType\x12\x15\n\rdisplay_width\x18\x02 \x01(\r\x12\x16\n\x0e\x64isplay_height\x18\x03 \x01(\r\x12\x1c\n\x14has_device_wake_word\x18\x04 \x01(\x08\x12\x0f\n\x07has_led\x18\x05 \x01(\x08\x12\x35\n\nservo_type\x18\x06 \x01(\x0e\x32!.stackchan.websocket.v1.ServoType\x12\x1d\n\x15supports_audio_duplex\x18\x07 \x01(\x08\x12\x18\n\x10\x66irmware_version\x18\x08 \x01(\t\"F\n\x0eServerMetadata\x12\x1c\n\x14has_server_wake_word\x18\x01 \x01(\x08\x12\x16\n\x0eserver_version\x18\x02 \x01(\t*\xdf\x02\n\x0bMessageKind\x12\x1c\n\x18MESSAGE_KIND_UNSPECIFIED\x10\x00\x12\x1a\n\x16MESSAGE_KIND_AUDIO_PCM\x10\x01\x12\x1a\n\x16MESSAGE_KIND_AUDIO_WAV\x10\x02\x12\x1a\n\x16MESSAGE_KIND_STATE_CMD\x10\x03\x12\x1e\n\x1aMESSAGE_KIND_WAKE_WORD_EVT\x10\x04\x12\x1a\n\x16MESSAGE_KIND_STATE_EVT\x10\x05\x12\x1f\n\x1bMESSAGE_KIND_SPEAK_DONE_EVT\x10\x06\x12\x1a\n\x16MESSAGE_KIND_SERVO_CMD\x10\x07\x12\x1f\n\x1bMESSAGE_KIND_SERVO_DONE_EVT\x10\x08\x12\"\n\x1eMESSAGE_KIND_FIRMWARE_METADATA\x10\t\x12 \n\x1cMESSAGE_KIND_SERVER_METADATA\x10\n*p\n\x0bMessageType\x12\x1c\n\x18MESSAGE_TYPE_UNSPECIFIED\x10\x00\x12\x16\n\x12MESSAGE_TYPE_START\x10\x01\x12\x15\n\x11MESSAGE_TYPE_DATA\x10\x02\x12\x14\n\x10MESSAGE_TYPE_END\x10\x03*\x85\x01\n\x0eStackchanState\x12\x18\n\x14STACKCHAN_STATE_IDLE\x10\x00\x12\x1d\n\x19STACKCHAN_STATE_LISTENING\x10\x01\x12\x1c\n\x18STACKCHAN_STATE_THINKING\x10\x02\x12\x1c\n\x18STACKCHAN_STATE_SPEAKING\x10\x03*c\n\x0eServoOperation\x12\x19\n\x15SERVO_OPERATION_SLEEP\x10\x00\x12\x1a\n\x16SERVO_OPERATION_MOVE_X\x10\x01\x12\x1a\n\x16SERVO_OPERATION_MOVE_Y\x10\x02*\x85\x01\n\nDeviceType\x12\x1b\n\x17\x44\x45VICE_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x44\x45VICE_TYPE_M5STACK_CORES3\x10\x01\x12\x1a\n\x16\x44\x45VICE_TYPE_M5ATOM_S3R\x10\x02\x12\x1e\n\x1a\x44\x45VICE_TYPE_M5ATOM_ECHOS3R\x10\x03*i\n\tServoType\x12\x1a\n\x16SERVO_TYPE_UNSPECIFIED\x10\x00\x12\x13\n\x0fSERVO_TYPE_NONE\x10\x01\x12\x13\n\x0fSERVO_TYPE_SG90\x10\x02\x12\x16\n\x12SERVO_TYPE_SCS0009\x10\x03\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17websocket-message.proto\x12\x16stackchan.websocket.v1\"\x8f\t\n\x10WebSocketMessage\x12\x31\n\x04kind\x18\x01 \x01(\x0e\x32#.stackchan.websocket.v1.MessageKind\x12\x39\n\x0cmessage_type\x18\x02 \x01(\x0e\x32#.stackchan.websocket.v1.MessageType\x12\x0b\n\x03seq\x18\x03 \x01(\r\x12@\n\x0f\x61udio_pcm_start\x18\n \x01(\x0b\x32%.stackchan.websocket.v1.AudioPcmStartH\x00\x12<\n\x0e\x61udio_pcm_data\x18\x0b \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_pcm_end\x18\x0c \x01(\x0b\x32#.stackchan.websocket.v1.AudioPcmEndH\x00\x12@\n\x0f\x61udio_wav_start\x18\x14 \x01(\x0b\x32%.stackchan.websocket.v1.AudioWavStartH\x00\x12<\n\x0e\x61udio_wav_data\x18\x15 \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_wav_end\x18\x16 \x01(\x0b\x32#.stackchan.websocket.v1.AudioWavEndH\x00\x12\x39\n\tstate_cmd\x18\x1e \x01(\x0b\x32$.stackchan.websocket.v1.StateCommandH\x00\x12>\n\rwake_word_evt\x18\x1f \x01(\x0b\x32%.stackchan.websocket.v1.WakeWordEventH\x00\x12\x37\n\tstate_evt\x18 \x01(\x0b\x32\".stackchan.websocket.v1.StateEventH\x00\x12@\n\x0espeak_done_evt\x18! \x01(\x0b\x32&.stackchan.websocket.v1.SpeakDoneEventH\x00\x12\x41\n\tservo_cmd\x18\" \x01(\x0b\x32,.stackchan.websocket.v1.ServoCommandSequenceH\x00\x12@\n\x0eservo_done_evt\x18# \x01(\x0b\x32&.stackchan.websocket.v1.ServoDoneEventH\x00\x12\x45\n\x11\x66irmware_metadata\x18$ \x01(\x0b\x32(.stackchan.websocket.v1.FirmwareMetadataH\x00\x12\x41\n\x0fserver_metadata\x18% \x01(\x0b\x32&.stackchan.websocket.v1.ServerMetadataH\x00\x12\x37\n\x08tone_cmd\x18& \x01(\x0b\x32#.stackchan.websocket.v1.ToneCommandH\x00\x12>\n\rtone_done_evt\x18\' \x01(\x0b\x32%.stackchan.websocket.v1.ToneDoneEventH\x00\x42\x06\n\x04\x62ody\"\x0f\n\rAudioPcmStart\"\r\n\x0b\x41udioPcmEnd\"6\n\rAudioWavStart\x12\x13\n\x0bsample_rate\x18\x01 \x01(\r\x12\x10\n\x08\x63hannels\x18\x02 \x01(\r\"\r\n\x0b\x41udioWavEnd\"\x1f\n\nAudioChunk\x12\x11\n\tpcm_bytes\x18\x01 \x01(\x0c\"d\n\x0cStateCommand\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\x12\x1d\n\x15listening_duration_ms\x18\x02 \x01(\r\"!\n\rWakeWordEvent\x12\x10\n\x08\x64\x65tected\x18\x01 \x01(\x08\"C\n\nStateEvent\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"\x1e\n\x0eSpeakDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"N\n\x14ServoCommandSequence\x12\x36\n\x08\x63ommands\x18\x01 \x03(\x0b\x32$.stackchan.websocket.v1.ServoCommand\"f\n\x0cServoCommand\x12\x32\n\x02op\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.ServoOperation\x12\r\n\x05\x61ngle\x18\x02 \x01(\x11\x12\x13\n\x0b\x64uration_ms\x18\x03 \x01(\x11\"\x1e\n\x0eServoDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"5\n\x0bToneCommand\x12\x11\n\tfrequency\x18\x01 \x01(\x02\x12\x13\n\x0b\x64uration_ms\x18\x02 \x01(\r\"\x1d\n\rToneDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"\x99\x02\n\x10\x46irmwareMetadata\x12\x37\n\x0b\x64\x65vice_type\x18\x01 \x01(\x0e\x32\".stackchan.websocket.v1.DeviceType\x12\x15\n\rdisplay_width\x18\x02 \x01(\r\x12\x16\n\x0e\x64isplay_height\x18\x03 \x01(\r\x12\x1c\n\x14has_device_wake_word\x18\x04 \x01(\x08\x12\x0f\n\x07has_led\x18\x05 \x01(\x08\x12\x35\n\nservo_type\x18\x06 \x01(\x0e\x32!.stackchan.websocket.v1.ServoType\x12\x1d\n\x15supports_audio_duplex\x18\x07 \x01(\x08\x12\x18\n\x10\x66irmware_version\x18\x08 \x01(\t\"F\n\x0eServerMetadata\x12\x1c\n\x14has_server_wake_word\x18\x01 \x01(\x08\x12\x16\n\x0eserver_version\x18\x02 \x01(\t*\x9a\x03\n\x0bMessageKind\x12\x1c\n\x18MESSAGE_KIND_UNSPECIFIED\x10\x00\x12\x1a\n\x16MESSAGE_KIND_AUDIO_PCM\x10\x01\x12\x1a\n\x16MESSAGE_KIND_AUDIO_WAV\x10\x02\x12\x1a\n\x16MESSAGE_KIND_STATE_CMD\x10\x03\x12\x1e\n\x1aMESSAGE_KIND_WAKE_WORD_EVT\x10\x04\x12\x1a\n\x16MESSAGE_KIND_STATE_EVT\x10\x05\x12\x1f\n\x1bMESSAGE_KIND_SPEAK_DONE_EVT\x10\x06\x12\x1a\n\x16MESSAGE_KIND_SERVO_CMD\x10\x07\x12\x1f\n\x1bMESSAGE_KIND_SERVO_DONE_EVT\x10\x08\x12\"\n\x1eMESSAGE_KIND_FIRMWARE_METADATA\x10\t\x12 \n\x1cMESSAGE_KIND_SERVER_METADATA\x10\n\x12\x19\n\x15MESSAGE_KIND_TONE_CMD\x10\x0b\x12\x1e\n\x1aMESSAGE_KIND_TONE_DONE_EVT\x10\x0c*p\n\x0bMessageType\x12\x1c\n\x18MESSAGE_TYPE_UNSPECIFIED\x10\x00\x12\x16\n\x12MESSAGE_TYPE_START\x10\x01\x12\x15\n\x11MESSAGE_TYPE_DATA\x10\x02\x12\x14\n\x10MESSAGE_TYPE_END\x10\x03*\x85\x01\n\x0eStackchanState\x12\x18\n\x14STACKCHAN_STATE_IDLE\x10\x00\x12\x1d\n\x19STACKCHAN_STATE_LISTENING\x10\x01\x12\x1c\n\x18STACKCHAN_STATE_THINKING\x10\x02\x12\x1c\n\x18STACKCHAN_STATE_SPEAKING\x10\x03*c\n\x0eServoOperation\x12\x19\n\x15SERVO_OPERATION_SLEEP\x10\x00\x12\x1a\n\x16SERVO_OPERATION_MOVE_X\x10\x01\x12\x1a\n\x16SERVO_OPERATION_MOVE_Y\x10\x02*\x85\x01\n\nDeviceType\x12\x1b\n\x17\x44\x45VICE_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x44\x45VICE_TYPE_M5STACK_CORES3\x10\x01\x12\x1a\n\x16\x44\x45VICE_TYPE_M5ATOM_S3R\x10\x02\x12\x1e\n\x1a\x44\x45VICE_TYPE_M5ATOM_ECHOS3R\x10\x03*i\n\tServoType\x12\x1a\n\x16SERVO_TYPE_UNSPECIFIED\x10\x00\x12\x13\n\x0fSERVO_TYPE_NONE\x10\x01\x12\x13\n\x0fSERVO_TYPE_SG90\x10\x02\x12\x16\n\x12SERVO_TYPE_SCS0009\x10\x03\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'websocket_message_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_MESSAGEKIND']._serialized_start=2016 - _globals['_MESSAGEKIND']._serialized_end=2367 - _globals['_MESSAGETYPE']._serialized_start=2369 - _globals['_MESSAGETYPE']._serialized_end=2481 - _globals['_STACKCHANSTATE']._serialized_start=2484 - _globals['_STACKCHANSTATE']._serialized_end=2617 - _globals['_SERVOOPERATION']._serialized_start=2619 - _globals['_SERVOOPERATION']._serialized_end=2718 - _globals['_DEVICETYPE']._serialized_start=2721 - _globals['_DEVICETYPE']._serialized_end=2854 - _globals['_SERVOTYPE']._serialized_start=2856 - _globals['_SERVOTYPE']._serialized_end=2961 + _globals['_MESSAGEKIND']._serialized_start=2254 + _globals['_MESSAGEKIND']._serialized_end=2664 + _globals['_MESSAGETYPE']._serialized_start=2666 + _globals['_MESSAGETYPE']._serialized_end=2778 + _globals['_STACKCHANSTATE']._serialized_start=2781 + _globals['_STACKCHANSTATE']._serialized_end=2914 + _globals['_SERVOOPERATION']._serialized_start=2916 + _globals['_SERVOOPERATION']._serialized_end=3015 + _globals['_DEVICETYPE']._serialized_start=3018 + _globals['_DEVICETYPE']._serialized_end=3151 + _globals['_SERVOTYPE']._serialized_start=3153 + _globals['_SERVOTYPE']._serialized_end=3258 _globals['_WEBSOCKETMESSAGE']._serialized_start=52 - _globals['_WEBSOCKETMESSAGE']._serialized_end=1098 - _globals['_AUDIOPCMSTART']._serialized_start=1100 - _globals['_AUDIOPCMSTART']._serialized_end=1115 - _globals['_AUDIOPCMEND']._serialized_start=1117 - _globals['_AUDIOPCMEND']._serialized_end=1130 - _globals['_AUDIOWAVSTART']._serialized_start=1132 - _globals['_AUDIOWAVSTART']._serialized_end=1186 - _globals['_AUDIOWAVEND']._serialized_start=1188 - _globals['_AUDIOWAVEND']._serialized_end=1201 - _globals['_AUDIOCHUNK']._serialized_start=1203 - _globals['_AUDIOCHUNK']._serialized_end=1234 - _globals['_STATECOMMAND']._serialized_start=1236 - _globals['_STATECOMMAND']._serialized_end=1305 - _globals['_WAKEWORDEVENT']._serialized_start=1307 - _globals['_WAKEWORDEVENT']._serialized_end=1340 - _globals['_STATEEVENT']._serialized_start=1342 - _globals['_STATEEVENT']._serialized_end=1409 - _globals['_SPEAKDONEEVENT']._serialized_start=1411 - _globals['_SPEAKDONEEVENT']._serialized_end=1441 - _globals['_SERVOCOMMANDSEQUENCE']._serialized_start=1443 - _globals['_SERVOCOMMANDSEQUENCE']._serialized_end=1521 - _globals['_SERVOCOMMAND']._serialized_start=1523 - _globals['_SERVOCOMMAND']._serialized_end=1625 - _globals['_SERVODONEEVENT']._serialized_start=1627 - _globals['_SERVODONEEVENT']._serialized_end=1657 - _globals['_FIRMWAREMETADATA']._serialized_start=1660 - _globals['_FIRMWAREMETADATA']._serialized_end=1941 - _globals['_SERVERMETADATA']._serialized_start=1943 - _globals['_SERVERMETADATA']._serialized_end=2013 + _globals['_WEBSOCKETMESSAGE']._serialized_end=1219 + _globals['_AUDIOPCMSTART']._serialized_start=1221 + _globals['_AUDIOPCMSTART']._serialized_end=1236 + _globals['_AUDIOPCMEND']._serialized_start=1238 + _globals['_AUDIOPCMEND']._serialized_end=1251 + _globals['_AUDIOWAVSTART']._serialized_start=1253 + _globals['_AUDIOWAVSTART']._serialized_end=1307 + _globals['_AUDIOWAVEND']._serialized_start=1309 + _globals['_AUDIOWAVEND']._serialized_end=1322 + _globals['_AUDIOCHUNK']._serialized_start=1324 + _globals['_AUDIOCHUNK']._serialized_end=1355 + _globals['_STATECOMMAND']._serialized_start=1357 + _globals['_STATECOMMAND']._serialized_end=1457 + _globals['_WAKEWORDEVENT']._serialized_start=1459 + _globals['_WAKEWORDEVENT']._serialized_end=1492 + _globals['_STATEEVENT']._serialized_start=1494 + _globals['_STATEEVENT']._serialized_end=1561 + _globals['_SPEAKDONEEVENT']._serialized_start=1563 + _globals['_SPEAKDONEEVENT']._serialized_end=1593 + _globals['_SERVOCOMMANDSEQUENCE']._serialized_start=1595 + _globals['_SERVOCOMMANDSEQUENCE']._serialized_end=1673 + _globals['_SERVOCOMMAND']._serialized_start=1675 + _globals['_SERVOCOMMAND']._serialized_end=1777 + _globals['_SERVODONEEVENT']._serialized_start=1779 + _globals['_SERVODONEEVENT']._serialized_end=1809 + _globals['_TONECOMMAND']._serialized_start=1811 + _globals['_TONECOMMAND']._serialized_end=1864 + _globals['_TONEDONEEVENT']._serialized_start=1866 + _globals['_TONEDONEEVENT']._serialized_end=1895 + _globals['_FIRMWAREMETADATA']._serialized_start=1898 + _globals['_FIRMWAREMETADATA']._serialized_end=2179 + _globals['_SERVERMETADATA']._serialized_start=2181 + _globals['_SERVERMETADATA']._serialized_end=2251 # @@protoc_insertion_point(module_scope) diff --git a/stackchan_server/listen.py b/stackchan_server/listen.py index a2c934d..a890a39 100644 --- a/stackchan_server/listen.py +++ b/stackchan_server/listen.py @@ -44,7 +44,10 @@ def __init__( self._message_ready = asyncio.Event() self._message_error: Optional[Exception] = None self._transcript: Optional[str] = None + self._raw_audio: Optional[bytes] = None self._speech_stream: Optional[StreamingSpeechSession] = None + self._result_mode: Optional[str] = None + self._session_lock = asyncio.Lock() async def close(self) -> None: await self._abort_speech_stream() @@ -52,43 +55,99 @@ async def close(self) -> None: async def listen( self, *, + send_listen_command: Callable[[int | None], Awaitable[None]], send_state_command: Callable[[int], Awaitable[None]], is_closed: Callable[[], bool], idle_state: int, - listening_state: int, ) -> str: - await send_state_command(listening_state) + async with self._session_lock: + self._prepare_wait("transcript") + await send_listen_command(None) + result = await self._wait_for_result( + is_closed=is_closed, + on_inactivity_timeout=lambda: send_state_command(idle_state), + ) + assert isinstance(result, str), "listen expected transcript result" + return result + + async def listen_raw( + self, + *, + duration_ms: int, + send_listen_command: Callable[[int | None], Awaitable[None]], + is_closed: Callable[[], bool], + ) -> bytes: + async with self._session_lock: + self._prepare_wait("raw") + await send_listen_command(duration_ms) + result = await self._wait_for_result(is_closed=is_closed) + assert not isinstance(result, str), "listen_raw expected raw audio result" + return result + + def _prepare_wait(self, mode: str) -> None: + self._message_ready.clear() + self._message_error = None + self._transcript = None + self._raw_audio = None + self._result_mode = mode + + async def _wait_for_result( + self, + *, + is_closed: Callable[[], bool], + on_inactivity_timeout: Optional[Callable[[], Awaitable[None]]] = None, + ) -> str | bytes: loop = asyncio.get_running_loop() last_counter = self._pcm_data_counter last_data_time = loop.time() while True: if self._message_error is not None: err = self._message_error - self._message_error = None + self._reset_pending_result() raise err if self._message_ready.is_set(): - text = self._transcript or "" - self._transcript = None - self._message_ready.clear() - return text + if self._result_mode == "raw": + raw_audio = self._raw_audio or b"" + self._reset_pending_result() + return raw_audio + + transcript = self._transcript or "" + self._reset_pending_result() + return transcript if is_closed(): + self._reset_pending_result() raise WebSocketDisconnect() if self._pcm_data_counter != last_counter: last_counter = self._pcm_data_counter last_data_time = loop.time() if (loop.time() - last_data_time) >= self.listen_audio_timeout_seconds: - if not is_closed(): - await send_state_command(idle_state) + if on_inactivity_timeout is not None and not is_closed(): + await on_inactivity_timeout() + self._reset_pending_result() raise TimeoutError("Timed out after audio data inactivity from firmware") await asyncio.sleep(0.05) + def _reset_pending_result(self) -> None: + self._message_ready.clear() + self._message_error = None + self._transcript = None + self._raw_audio = None + self._result_mode = None + async def handle_start(self, websocket: WebSocket) -> bool: logger.info("Received START") + if self._result_mode is None: + asyncio.create_task( + websocket.close(code=1003, reason="start received without pending listen request") + ) + return False await self._abort_speech_stream() self._pcm_buffer = bytearray() self._streaming = True self._message_error = None - if isinstance(self.speech_recognizer, StreamingSpeechRecognizer): + if self._result_mode == "transcript" and isinstance( + self.speech_recognizer, StreamingSpeechRecognizer + ): try: self._speech_stream = await self.speech_recognizer.start_stream() except Exception: @@ -151,10 +210,25 @@ async def handle_end( await websocket.close(code=1003, reason="invalid accumulated pcm length") return - await send_state_command(thinking_state) - frames = len(self._pcm_buffer) // (self.audio_format.sample_width * self.audio_format.channels) duration_seconds = frames / float(self.audio_format.sample_rate_hz) + pcm_bytes = bytes(self._pcm_buffer) + + if self._result_mode == "raw": + logger.info( + "Captured raw audio frames=%d duration=%.3fs bytes=%d", + frames, + duration_seconds, + len(pcm_bytes), + ) + self._streaming = False + self._pcm_buffer = bytearray() + self._raw_audio = pcm_bytes + self._message_ready.set() + return + + await send_state_command(thinking_state) + ws_meta = { "sample_rate": self.audio_format.sample_rate_hz, "frames": frames, @@ -162,7 +236,7 @@ async def handle_end( "duration_seconds": round(duration_seconds, 3), } if self.debug_recording: - _filepath, filename = self._save_wav(bytes(self._pcm_buffer)) + _filepath, filename = self._save_wav(pcm_bytes) ws_meta["text"] = f"Saved as {filename}" ws_meta["path"] = f"recordings/{filename}" else: @@ -170,7 +244,7 @@ async def handle_end( await websocket.send_json(ws_meta) - transcript = await self._transcribe_async(bytes(self._pcm_buffer)) + transcript = await self._transcribe_async(pcm_bytes) self._streaming = False self._pcm_buffer = bytearray() diff --git a/stackchan_server/protobuf_ws.py b/stackchan_server/protobuf_ws.py index 8569004..ab1cafc 100644 --- a/stackchan_server/protobuf_ws.py +++ b/stackchan_server/protobuf_ws.py @@ -92,13 +92,46 @@ def encode_audio_wav_end_message(seq: int) -> bytes: return message.SerializeToString() -def encode_state_command_message(seq: int, state_id: int) -> bytes: +def encode_state_command_message( + seq: int, + state_id: int, + *, + listening_duration_ms: int | None = None, +) -> bytes: message = _new_message( ws_pb2.MESSAGE_KIND_STATE_CMD, ws_pb2.MESSAGE_TYPE_DATA, seq, ) message.state_cmd.state = int(state_id) + if listening_duration_ms is not None: + _ensure_range( + int(listening_duration_ms), + minimum=0, + maximum=0xFFFFFFFF, + label="listening_duration_ms", + ) + message.state_cmd.listening_duration_ms = int(listening_duration_ms) + return message.SerializeToString() + + +def encode_tone_command_message(seq: int, frequency: float, duration_ms: int) -> bytes: + if float(frequency) <= 0: + raise ValueError(f"frequency must be positive: {frequency}") + _ensure_range( + int(duration_ms), + minimum=1, + maximum=0xFFFFFFFF, + label="tone duration_ms", + ) + + message = _new_message( + ws_pb2.MESSAGE_KIND_TONE_CMD, + ws_pb2.MESSAGE_TYPE_DATA, + seq, + ) + message.tone_cmd.frequency = float(frequency) + message.tone_cmd.duration_ms = int(duration_ms) return message.SerializeToString() @@ -176,6 +209,7 @@ def encode_servo_command_message(seq: int, commands: Sequence[ServoCommand]) -> "encode_audio_pcm_data_message", "encode_audio_pcm_end_message", "encode_audio_pcm_start_message", + "encode_tone_command_message", "encode_audio_wav_data_message", "encode_audio_wav_end_message", "encode_audio_wav_start_message", diff --git a/stackchan_server/ws_proxy.py b/stackchan_server/ws_proxy.py index 1c45236..cb1a6c0 100644 --- a/stackchan_server/ws_proxy.py +++ b/stackchan_server/ws_proxy.py @@ -21,6 +21,7 @@ encode_server_metadata_message, encode_servo_command_message, encode_state_command_message, + encode_tone_command_message, parse_websocket_message, ) from .speak import SpeakHandler @@ -144,6 +145,9 @@ def __init__( self._servo_done_counter = 0 self._servo_sent_counter = 0 self._pending_servo_wait_targets: deque[int] = deque() + self._tone_done_counter = 0 + self._tone_sent_counter = 0 + self._pending_tone_wait_targets: deque[int] = deque() @property def closed(self) -> bool: @@ -173,10 +177,10 @@ async def wait_for_talk_session(self) -> None: async def listen(self) -> str: return await self._listener.listen( + send_listen_command=self._send_listen_command, send_state_command=self.send_state_command, is_closed=lambda: self._closed, idle_state=FirmwareState.IDLE, - listening_state=FirmwareState.LISTENING, ) async def speak(self, text: str) -> None: @@ -188,6 +192,47 @@ async def speak(self, text: str) -> None: is_closed=lambda: self._closed, ) + async def tone( + self, + frequency: float, + duration: int, + *, + timeout_seconds: float | None = None, + ) -> None: + previous_counter = self._tone_sent_counter + target_counter = previous_counter + 1 + self._tone_sent_counter = target_counter + self._pending_tone_wait_targets.append(target_counter) + try: + await self.ws.send_bytes( + encode_tone_command_message( + self._next_down_seq(), + frequency=frequency, + duration_ms=duration, + ) + ) + except Exception: + if ( + self._pending_tone_wait_targets + and self._pending_tone_wait_targets[-1] == target_counter + ): + self._pending_tone_wait_targets.pop() + self._tone_sent_counter = previous_counter + raise + + await self.wait_tone_complete( + timeout_seconds=max((float(duration) / 1000.0) + 1.0, 5.0) + if timeout_seconds is None + else timeout_seconds + ) + + async def listen_raw(self, duration: int = 3000) -> bytes: + return await self._listener.listen_raw( + duration_ms=duration, + send_listen_command=self._send_listen_command, + is_closed=lambda: self._closed, + ) + async def send_state_command(self, state_id: int | FirmwareState) -> None: await self._send_state_command(state_id) @@ -226,6 +271,20 @@ async def wait_servo_complete(self, timeout_seconds: float | None = 120.0) -> No label="servo completed event", ) + async def wait_tone_complete(self, timeout_seconds: float | None = 30.0) -> None: + target_counter = ( + self._pending_tone_wait_targets.popleft() + if self._pending_tone_wait_targets + else self._tone_done_counter + 1 + ) + await self._wait_for_counter( + current=lambda: self._tone_done_counter, + min_counter=target_counter, + timeout_seconds=timeout_seconds, + is_closed=lambda: self._closed, + label="tone completed event", + ) + async def start(self) -> None: if self._receiving_task is None: self._receiving_task = asyncio.create_task(self._receive_loop()) @@ -309,6 +368,10 @@ async def _receive_loop(self) -> None: self._handle_servo_done_event(message) continue + if message.kind == ws_pb2.MESSAGE_KIND_TONE_DONE_EVT: + self._handle_tone_done_event(message) + continue + await self.ws.close(code=1003, reason="unsupported kind") break except WebSocketDisconnect: @@ -410,11 +473,30 @@ def _handle_servo_done_event(self, message: Any) -> None: self._servo_done_counter += 1 logger.info("Received servo done event") + def _handle_tone_done_event(self, message: Any) -> None: + if message.message_type != ws_pb2.MESSAGE_TYPE_DATA: + return + if message.WhichOneof("body") != "tone_done_evt": + return + if not message.tone_done_evt.done: + return + self._tone_done_counter += 1 + logger.info("Received tone done event") + async def _send_state_command(self, state_id: int | FirmwareState) -> None: await self.ws.send_bytes( encode_state_command_message(self._next_down_seq(), int(state_id)) ) + async def _send_listen_command(self, duration_ms: int | None) -> None: + await self.ws.send_bytes( + encode_state_command_message( + self._next_down_seq(), + int(FirmwareState.LISTENING), + listening_duration_ms=duration_ms, + ) + ) + async def _wait_for_counter( self, *, From 1d23fdccdc15682794464dd09b8c3219a518a324 Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Sun, 19 Apr 2026 17:51:09 +0900 Subject: [PATCH 3/4] feat: improve tone command handling and add listening state management --- firmware/include/wake_up_word.hpp | 5 ----- firmware/src/main.cpp | 28 ++++++++++++++++++++++++---- firmware/src/speaking.cpp | 18 ++++++++++++++++-- firmware/src/wake_up_word.cpp | 29 +++-------------------------- stackchan_server/ws_proxy.py | 20 +++++++++++++++++++- 5 files changed, 62 insertions(+), 38 deletions(-) diff --git a/firmware/include/wake_up_word.hpp b/firmware/include/wake_up_word.hpp index 2d58b49..3af6830 100644 --- a/firmware/include/wake_up_word.hpp +++ b/firmware/include/wake_up_word.hpp @@ -33,9 +33,4 @@ class WakeUpWord StateMachine &state_; const int sample_rate_; std::function on_wake_word_detected_; - - // Idle 時のログ用カウンタ - uint32_t loop_count_ = 0; - uint32_t error_count_ = 0; - uint32_t last_log_time_ = 0; }; diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index f35ad68..f873439 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -46,6 +46,8 @@ constexpr int kToneChannel = 1; stackchan_websocket_v1_WebSocketMessage g_tx_message = stackchan_websocket_v1_WebSocketMessage_init_zero; stackchan_websocket_v1_WebSocketMessage g_rx_message = stackchan_websocket_v1_WebSocketMessage_init_zero; bool g_tone_playing = false; +bool g_tone_restore_state_pending = false; +StateMachine::State g_tone_restore_state = StateMachine::Idle; void markCommunicationActive() { @@ -269,8 +271,25 @@ bool applyToneCommand(const stackchan_websocket_v1_ToneCommand &command) return false; } + StateMachine::State previous_state = stateMachine.getState(); + if (previous_state != StateMachine::Speaking) + { + g_tone_restore_state = previous_state; + g_tone_restore_state_pending = true; + stateMachine.setState(StateMachine::Speaking); + } + else + { + g_tone_restore_state_pending = false; + } + if (!M5.Speaker.tone(command.frequency, command.duration_ms, kToneChannel, true)) { + if (g_tone_restore_state_pending) + { + stateMachine.setState(g_tone_restore_state); + g_tone_restore_state_pending = false; + } log_w( "Failed to start tone frequency=%.1f duration=%lu", command.frequency, @@ -279,10 +298,6 @@ bool applyToneCommand(const stackchan_websocket_v1_ToneCommand &command) } g_tone_playing = true; - log_i( - "ToneCmd frequency=%.1f duration=%lu", - command.frequency, - static_cast(command.duration_ms)); return true; } @@ -299,6 +314,11 @@ void pollTonePlayback() } g_tone_playing = false; + if (g_tone_restore_state_pending) + { + stateMachine.setState(g_tone_restore_state); + g_tone_restore_state_pending = false; + } notifyToneDone(); } } // namespace diff --git a/firmware/src/speaking.cpp b/firmware/src/speaking.cpp index c1bb8d9..51cc1e8 100644 --- a/firmware/src/speaking.cpp +++ b/firmware/src/speaking.cpp @@ -23,7 +23,17 @@ void Speaking::init() void Speaking::begin() { // 念のためマイクを停止し、再生に集中させる + uint8_t current_volume = M5.Speaker.getVolume(); M5.Mic.end(); + delay(20); + M5.Speaker.end(); + delay(10); + bool speaker_ready = M5.Speaker.begin(); + M5.Speaker.setVolume(current_volume); + if (!speaker_ready) + { + log_w("Failed to initialize speaker"); + } } void Speaking::end() @@ -32,7 +42,6 @@ void Speaking::end() { M5.Speaker.stop(); } - M5.Speaker.end(); reset(); } @@ -106,7 +115,12 @@ void Speaking::handleWavEnd(uint32_t seq) const int16_t *samples = reinterpret_cast(buf.data()); size_t sample_len = buf.size() / sizeof(int16_t); bool stereo = channels_ > 1; - M5.Speaker.playRaw(samples, sample_len, sample_rate_, stereo, 1, 0); + bool accepted = M5.Speaker.playRaw(samples, sample_len, sample_rate_, stereo, 1, 0); + if (!accepted) + { + log_w("Failed to queue raw audio for playback"); + playing_ = false; + } } } diff --git a/firmware/src/wake_up_word.cpp b/firmware/src/wake_up_word.cpp index 426ab7b..39bd006 100644 --- a/firmware/src/wake_up_word.cpp +++ b/firmware/src/wake_up_word.cpp @@ -26,8 +26,10 @@ void WakeUpWord::begin() void WakeUpWord::end() { - M5.Mic.end(); ESP_SR_M5.pause(); + delay(10); + M5.Mic.end(); + delay(20); } void WakeUpWord::feedAudio(const int16_t *samples, size_t count) @@ -49,31 +51,6 @@ void WakeUpWord::loop() if (success) { feedAudio(audio_buf, kAudioSampleSize); - - uint32_t now = millis(); - if (now - last_log_time_ >= 1000) - { - int32_t sum = 0; - for (int i = 0; i < 10; i++) - { - sum += abs(audio_buf[i]); - } - log_i("idle loop: count=%lu, avg_level=%ld, errors=%lu, interval=%lu ms", - static_cast(loop_count_), - static_cast(sum / 10), - static_cast(error_count_), - static_cast(now - last_log_time_)); - last_log_time_ = now; - } - loop_count_++; - } - else - { - error_count_++; - if (error_count_ % 100 == 0) - { - log_w("WARNING: M5.Mic.record failed, count=%lu", static_cast(error_count_)); - } } } diff --git a/stackchan_server/ws_proxy.py b/stackchan_server/ws_proxy.py index cb1a6c0..c2f8abb 100644 --- a/stackchan_server/ws_proxy.py +++ b/stackchan_server/ws_proxy.py @@ -227,11 +227,13 @@ async def tone( ) async def listen_raw(self, duration: int = 3000) -> bytes: - return await self._listener.listen_raw( + raw_audio = await self._listener.listen_raw( duration_ms=duration, send_listen_command=self._send_listen_command, is_closed=lambda: self._closed, ) + await self._wait_for_listening_finished(timeout_seconds=max(duration / 1000.0 + 2.0, 5.0)) + return raw_audio async def send_state_command(self, state_id: int | FirmwareState) -> None: await self._send_state_command(state_id) @@ -517,6 +519,22 @@ async def _wait_for_counter( raise TimeoutError(f"Timed out waiting for {label}") await asyncio.sleep(0.05) + async def _wait_for_listening_finished( + self, + *, + timeout_seconds: float | None, + ) -> None: + loop = asyncio.get_running_loop() + deadline = (loop.time() + timeout_seconds) if timeout_seconds else None + while True: + if self._current_firmware_state != FirmwareState.LISTENING: + return + if self._closed: + raise WebSocketDisconnect() + if deadline and loop.time() >= deadline: + raise TimeoutError("Timed out waiting for firmware to leave listening state") + await asyncio.sleep(0.05) + def _next_down_seq(self) -> int: seq = self._down_seq self._down_seq += 1 From 03a18a0ecb89e562a856412ea118f1f14ca3b37f Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Sun, 19 Apr 2026 20:26:56 +0900 Subject: [PATCH 4/4] Refactor code structure for improved readability and maintainability --- example_apps/record_wakeup_word.py | 38 +++-- pyproject.toml | 1 + uv.lock | 252 +++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 13 deletions(-) diff --git a/example_apps/record_wakeup_word.py b/example_apps/record_wakeup_word.py index b23bb5b..fb3f0bf 100644 --- a/example_apps/record_wakeup_word.py +++ b/example_apps/record_wakeup_word.py @@ -47,28 +47,40 @@ async def talk_session(proxy: WsProxy): @app.webapi("/record_wakeup_word") async def record_wakeup_word(proxy: WsProxy, args: dict): - duration_ms = int(args.get("duration_ms", args.get("duration", 3000))) + duration_ms = 2500 logger.info("Recording wakeup word duration_ms=%d", duration_ms) await proxy.speak( "これからウェイクアップワードの録音を開始します。ピッと鳴ったら、ウェイクアップワードを話してください。" ) - await proxy.tone(4000, 200) - raw_audio = await proxy.listen_raw(duration=duration_ms) - await asyncio.sleep(0.5) - await proxy.tone(1000, 200) + await proxy.speak( + "50回録音します。トーンを変えたり、ちょっと遠くから話したりして、いろいろなパターンを録音してください。" + ) output_dir = Path("tmp") output_dir.mkdir(parents=True, exist_ok=True) - filename = f"wakeup_word_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S_%f')}.wav" - filepath = output_dir / filename - with wave.open(str(filepath), "wb") as wav_fp: - wav_fp.setnchannels(LISTEN_AUDIO_FORMAT.channels) - wav_fp.setsampwidth(LISTEN_AUDIO_FORMAT.sample_width) - wav_fp.setframerate(LISTEN_AUDIO_FORMAT.sample_rate_hz) - wav_fp.writeframes(raw_audio) + for i in range(50): + if i > 0 and i % 10 == 0: + await proxy.speak(f"あと{50 - i}回") + + await proxy.tone(2000, 200) + raw_audio = await proxy.listen_raw(duration=duration_ms) + + filename = f"wakeup_word_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S_%f')}.wav" + filepath = output_dir / filename + + with wave.open(str(filepath), "wb") as wav_fp: + wav_fp.setnchannels(LISTEN_AUDIO_FORMAT.channels) + wav_fp.setsampwidth(LISTEN_AUDIO_FORMAT.sample_width) + wav_fp.setframerate(LISTEN_AUDIO_FORMAT.sample_rate_hz) + wav_fp.writeframes(raw_audio) + + logger.info("Saved wakeup word recording to %s", filepath) + + await proxy.speak( + "お疲れ様でした" + ) - logger.info("Saved wakeup word recording to %s", filepath) return { "path": str(filepath), "bytes": len(raw_audio), diff --git a/pyproject.toml b/pyproject.toml index 90638f2..5c60d80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "python-dotenv>=1.2.1", "pydantic-settings>=2.13.1", "protobuf>=6.33.3", + "openwakeword>=0.4.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 1a6db32..db78256 100644 --- a/uv.lock +++ b/uv.lock @@ -342,6 +342,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094 }, ] +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -659,6 +667,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, ] +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -711,6 +728,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615 }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -792,6 +818,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 }, ] +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933 }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532 }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661 }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539 }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806 }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682 }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810 }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394 }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556 }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311 }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060 }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302 }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407 }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631 }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691 }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241 }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767 }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477 }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487 }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002 }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353 }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914 }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005 }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974 }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591 }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700 }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781 }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959 }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768 }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181 }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035 }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958 }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020 }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758 }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948 }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325 }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883 }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474 }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500 }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755 }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643 }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922 }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290 }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738 }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435 }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852 }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861 }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454 }, + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300 }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936 }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432 }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276 }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365 }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889 }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021 }, +] + +[[package]] +name = "openwakeword" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "onnxruntime" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/40/2afdab3146f08fa51ca74eba6f44ce4f26bd2bd88e255f34b42b0f495ef0/openwakeword-0.4.0.tar.gz", hash = "sha256:58714db20d9fdc6a089e4ef714c98807519ba00822bd32e156ad4f2a00ec9d23", size = 9289877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/0f/6b22ebf0808363d0c71f7e5d0880b1c13c6a2cdcae2dc2cb8adfbdbcc278/openwakeword-0.4.0-py3-none-any.whl", hash = "sha256:2c353f49a35d384507403efde3db439353d8281a52d08a0e42a5b493e5145075", size = 9285483 }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831 }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -1220,6 +1348,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649 }, ] +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770 }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458 }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341 }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022 }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409 }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760 }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045 }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324 }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651 }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045 }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994 }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518 }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667 }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524 }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133 }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223 }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518 }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546 }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305 }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257 }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673 }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467 }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395 }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647 }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199 }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001 }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719 }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429 }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952 }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063 }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449 }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943 }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621 }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708 }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135 }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977 }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601 }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667 }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159 }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771 }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910 }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980 }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543 }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510 }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131 }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032 }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766 }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007 }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333 }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066 }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763 }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984 }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877 }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750 }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858 }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723 }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098 }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397 }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163 }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291 }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317 }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327 }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165 }, +] + [[package]] name = "setuptools" version = "82.0.1" @@ -1263,6 +1480,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, +] + [[package]] name = "tenacity" version = "9.1.2" @@ -1272,6 +1501,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, ] +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374 }, +] + [[package]] name = "ty" version = "0.0.17" @@ -1454,6 +1704,7 @@ dependencies = [ { name = "fastapi" }, { name = "google-cloud-speech" }, { name = "google-genai" }, + { name = "openwakeword" }, { name = "protobuf" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, @@ -1476,6 +1727,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.128.0" }, { name = "google-cloud-speech", specifier = ">=2.35.0" }, { name = "google-genai", specifier = ">=1.59.0" }, + { name = "openwakeword", specifier = ">=0.4.0" }, { name = "protobuf", specifier = ">=6.33.3" }, { name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },