refactor: define contrato de VAD e eventos de fala
This commit is contained in:
19
README.md
19
README.md
@@ -390,6 +390,7 @@ src/voice_assistant/
|
|||||||
audio_gateway/app/ WebSocket endpoint and protocol
|
audio_gateway/app/ WebSocket endpoint and protocol
|
||||||
application/ VoiceSession and transport-independent events
|
application/ VoiceSession and transport-independent events
|
||||||
domain/ Session state and domain rules
|
domain/ Session state and domain rules
|
||||||
|
vad/ VAD port and speech boundaries; runtime integration pending
|
||||||
audio/ Canonical audio models
|
audio/ Canonical audio models
|
||||||
stt/ STT contract, mock and NVIDIA adapter
|
stt/ STT contract, mock and NVIDIA adapter
|
||||||
agent/ Agent contract and mock adapter
|
agent/ Agent contract and mock adapter
|
||||||
@@ -415,3 +416,21 @@ tools/ Integration and smoke scripts
|
|||||||
**Status:** V1 development baseline · Python 3.12+ · WebSocket protocol 1.0
|
**Status:** V1 development baseline · Python 3.12+ · WebSocket protocol 1.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Backend VAD contract (integration pending)
|
||||||
|
|
||||||
|
`vad/adapter.py` defines one stateful detector per continuous input stream.
|
||||||
|
Its input is the canonical PCM S16LE, mono, 16 kHz audio format. Arbitrary
|
||||||
|
complete PCM frames are buffered into model-specific windows by the adapter.
|
||||||
|
`vad/models.py` defines `SpeechStarted` and `SpeechEnded`; their sample offsets
|
||||||
|
refer to input audio since initialization or reset, with inclusive starts
|
||||||
|
and exclusive ends. Detection confirmation may arrive later than its audio
|
||||||
|
boundary. Model padding never extends the input timeline.
|
||||||
|
|
||||||
|
`process()` returns ordered boundaries, `flush()` handles normal end of input,
|
||||||
|
`reset()` discards detection state and restarts the timeline, and `aclose()`
|
||||||
|
releases resources without emitting new speech events. Calls are serialized.
|
||||||
|
These contracts do not start turns, select providers, or carry transport IDs.
|
||||||
|
The Silero implementation, application integration, and browser VAD replacement
|
||||||
|
are subsequent tasks. This step leaves the active conversation flow unchanged.
|
||||||
|
|||||||
1
src/voice_assistant/vad/__init__.py
Normal file
1
src/voice_assistant/vad/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Voice activity detection contracts independent of model and channel."""
|
||||||
71
src/voice_assistant/vad/adapter.py
Normal file
71
src/voice_assistant/vad/adapter.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""Port for stateful, continuous voice activity detection."""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from voice_assistant.audio.models import AudioFormat, CANONICAL_AUDIO_FORMAT
|
||||||
|
from voice_assistant.vad.models import VADEvent
|
||||||
|
|
||||||
|
|
||||||
|
class VADAdapter(ABC):
|
||||||
|
"""One detector per continuous input stream, used sequentially.
|
||||||
|
|
||||||
|
Audio enters as PCM S16LE, mono, 16 kHz. Adapters buffer arbitrary
|
||||||
|
complete PCM frames into the windows required by their model, convert
|
||||||
|
samples internally, and return only speech-boundary events.
|
||||||
|
|
||||||
|
Model state must not be shared between streams.
|
||||||
|
Each stream starts outside speech, at sample zero. Offsets refer to
|
||||||
|
actual input audio; synthetic padding must not extend the timeline.
|
||||||
|
Events are ordered by audio position and alternate start/end. A call
|
||||||
|
may return no events or multiple boundaries when it consumes a large
|
||||||
|
chunk. Detection latency does not change a boundary's sample offset.
|
||||||
|
|
||||||
|
The caller serializes operations. Implementations must avoid blocking
|
||||||
|
the event loop and must retain ownership of any worker still running
|
||||||
|
after cancellation, so it cannot race with reset or resource cleanup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_format(self) -> AudioFormat:
|
||||||
|
return CANONICAL_AUDIO_FORMAT
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def process(self, audio: bytes) -> tuple[VADEvent, ...]:
|
||||||
|
"""Consume PCM once and return new speech boundaries.
|
||||||
|
|
||||||
|
Validate PCM before changing state. Empty or misaligned chunks
|
||||||
|
raise ValueError. Chunk sizes are independent of model windows.
|
||||||
|
Calls after flush require reset; calls after aclose fail.
|
||||||
|
Model errors and cancellation propagate to the caller.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def flush(self) -> tuple[VADEvent, ...]:
|
||||||
|
"""Finish input normally, processing any remaining partial window.
|
||||||
|
|
||||||
|
If speech remains active, emit SpeechEnded with reason
|
||||||
|
end_of_input at the last real sample's exclusive boundary.
|
||||||
|
A repeated flush returns no events. New audio requires reset.
|
||||||
|
This operation does not release the detector's resources.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def reset(self) -> None:
|
||||||
|
"""Discard buffered audio and detection state; restart at sample zero.
|
||||||
|
|
||||||
|
Emit no boundaries. Use for an explicit stream discontinuity,
|
||||||
|
rather than after every conversation turn. Reset after aclose fails.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
"""Release resources and discard pending audio without emitting events.
|
||||||
|
|
||||||
|
Safe to repeat, including after partial initialization. Use this
|
||||||
|
operation for abort/disconnect; flush may produce a final speech
|
||||||
|
event and is reserved for normal end of input.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
27
src/voice_assistant/vad/models.py
Normal file
27
src/voice_assistant/vad/models.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""Speech boundaries on the input audio timeline, independent of turns."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SpeechStarted:
|
||||||
|
"""Inclusive speech start, in samples since initialization or reset."""
|
||||||
|
|
||||||
|
sample_offset: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SpeechEnded:
|
||||||
|
"""Exclusive speech end on the same timeline as SpeechStarted.
|
||||||
|
|
||||||
|
The offset describes the audio boundary, not the later instant at which
|
||||||
|
enough silence has arrived to confirm it. Synthetic end-of-input padding
|
||||||
|
must not extend the offset beyond actual input audio.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sample_offset: int
|
||||||
|
reason: Literal["silence", "end_of_input"]
|
||||||
|
|
||||||
|
|
||||||
|
type VADEvent = SpeechStarted | SpeechEnded
|
||||||
Reference in New Issue
Block a user