224 lines
8.1 KiB
Python
224 lines
8.1 KiB
Python
"""Protocol event models matching the original active-call API."""
|
|
|
|
from typing import Optional, Dict, Any
|
|
from pydantic import BaseModel, Field
|
|
from datetime import datetime
|
|
|
|
|
|
def current_timestamp_ms() -> int:
|
|
"""Get current timestamp in milliseconds."""
|
|
return int(datetime.now().timestamp() * 1000)
|
|
|
|
|
|
# Base Event Model
|
|
class BaseEvent(BaseModel):
|
|
"""Base event model."""
|
|
|
|
event: str = Field(..., description="Event type")
|
|
track_id: str = Field(..., description="Unique track identifier")
|
|
timestamp: int = Field(default_factory=current_timestamp_ms, description="Event timestamp in milliseconds")
|
|
|
|
|
|
# Lifecycle Events
|
|
class IncomingEvent(BaseEvent):
|
|
"""Incoming call event (SIP only)."""
|
|
|
|
event: str = Field(default="incoming", description="Event type")
|
|
caller: Optional[str] = Field(default=None, description="Caller's SIP URI")
|
|
callee: Optional[str] = Field(default=None, description="Callee's SIP URI")
|
|
sdp: Optional[str] = Field(default=None, description="SDP offer from caller")
|
|
|
|
|
|
class AnswerEvent(BaseEvent):
|
|
"""Call answered event."""
|
|
|
|
event: str = Field(default="answer", description="Event type")
|
|
sdp: Optional[str] = Field(default=None, description="SDP answer from server")
|
|
|
|
|
|
class RejectEvent(BaseEvent):
|
|
"""Call rejected event."""
|
|
|
|
event: str = Field(default="reject", description="Event type")
|
|
reason: Optional[str] = Field(default=None, description="Rejection reason")
|
|
code: Optional[int] = Field(default=None, description="SIP response code")
|
|
|
|
|
|
class RingingEvent(BaseEvent):
|
|
"""Call ringing event."""
|
|
|
|
event: str = Field(default="ringing", description="Event type")
|
|
early_media: bool = Field(default=False, description="Early media available")
|
|
|
|
|
|
class HangupEvent(BaseModel):
|
|
"""Call hangup event."""
|
|
|
|
event: str = Field(default="hangup", description="Event type")
|
|
timestamp: int = Field(default_factory=current_timestamp_ms, description="Event timestamp")
|
|
reason: Optional[str] = Field(default=None, description="Hangup reason")
|
|
initiator: Optional[str] = Field(default=None, description="Who initiated hangup")
|
|
start_time: Optional[str] = Field(default=None, description="Call start time (ISO 8601)")
|
|
hangup_time: Optional[str] = Field(default=None, description="Hangup time (ISO 8601)")
|
|
answer_time: Optional[str] = Field(default=None, description="Answer time (ISO 8601)")
|
|
ringing_time: Optional[str] = Field(default=None, description="Ringing time (ISO 8601)")
|
|
from_: Optional[Dict[str, Any]] = Field(default=None, alias="from", description="Caller info")
|
|
to: Optional[Dict[str, Any]] = Field(default=None, description="Callee info")
|
|
extra: Optional[Dict[str, Any]] = Field(default=None, description="Additional metadata")
|
|
|
|
class Config:
|
|
populate_by_name = True
|
|
|
|
|
|
# VAD Events
|
|
class SpeakingEvent(BaseEvent):
|
|
"""Speech detected event."""
|
|
|
|
event: str = Field(default="speaking", description="Event type")
|
|
start_time: int = Field(default_factory=current_timestamp_ms, description="Speech start time")
|
|
|
|
|
|
class SilenceEvent(BaseEvent):
|
|
"""Silence detected event."""
|
|
|
|
event: str = Field(default="silence", description="Event type")
|
|
start_time: int = Field(default_factory=current_timestamp_ms, description="Silence start time")
|
|
duration: int = Field(default=0, description="Silence duration in milliseconds")
|
|
|
|
|
|
# AI/ASR Events
|
|
class AsrFinalEvent(BaseEvent):
|
|
"""ASR final transcription event."""
|
|
|
|
event: str = Field(default="asrFinal", description="Event type")
|
|
index: int = Field(..., description="ASR result sequence number")
|
|
start_time: Optional[int] = Field(default=None, description="Speech start time")
|
|
end_time: Optional[int] = Field(default=None, description="Speech end time")
|
|
text: str = Field(..., description="Transcribed text")
|
|
|
|
|
|
class AsrDeltaEvent(BaseEvent):
|
|
"""ASR partial transcription event (streaming)."""
|
|
|
|
event: str = Field(default="asrDelta", description="Event type")
|
|
index: int = Field(..., description="ASR result sequence number")
|
|
start_time: Optional[int] = Field(default=None, description="Speech start time")
|
|
end_time: Optional[int] = Field(default=None, description="Speech end time")
|
|
text: str = Field(..., description="Partial transcribed text")
|
|
|
|
|
|
class EouEvent(BaseEvent):
|
|
"""End of utterance detection event."""
|
|
|
|
event: str = Field(default="eou", description="Event type")
|
|
completed: bool = Field(default=True, description="Whether utterance was completed")
|
|
|
|
|
|
# Audio Track Events
|
|
class TrackStartEvent(BaseEvent):
|
|
"""Audio track start event."""
|
|
|
|
event: str = Field(default="trackStart", description="Event type")
|
|
play_id: Optional[str] = Field(default=None, description="Play ID from TTS/Play command")
|
|
|
|
|
|
class TrackEndEvent(BaseEvent):
|
|
"""Audio track end event."""
|
|
|
|
event: str = Field(default="trackEnd", description="Event type")
|
|
duration: int = Field(..., description="Track duration in milliseconds")
|
|
ssrc: int = Field(..., description="RTP SSRC identifier")
|
|
play_id: Optional[str] = Field(default=None, description="Play ID from TTS/Play command")
|
|
|
|
|
|
class InterruptionEvent(BaseEvent):
|
|
"""Playback interruption event."""
|
|
|
|
event: str = Field(default="interruption", description="Event type")
|
|
play_id: Optional[str] = Field(default=None, description="Play ID that was interrupted")
|
|
subtitle: Optional[str] = Field(default=None, description="TTS text being played")
|
|
position: Optional[int] = Field(default=None, description="Word index position")
|
|
total_duration: Optional[int] = Field(default=None, description="Total TTS duration")
|
|
current: Optional[int] = Field(default=None, description="Elapsed time when interrupted")
|
|
|
|
|
|
# System Events
|
|
class ErrorEvent(BaseEvent):
|
|
"""Error event."""
|
|
|
|
event: str = Field(default="error", description="Event type")
|
|
sender: str = Field(..., description="Component that generated the error")
|
|
error: str = Field(..., description="Error message")
|
|
code: Optional[int] = Field(default=None, description="Error code")
|
|
|
|
|
|
class MetricsEvent(BaseModel):
|
|
"""Performance metrics event."""
|
|
|
|
event: str = Field(default="metrics", description="Event type")
|
|
timestamp: int = Field(default_factory=current_timestamp_ms, description="Event timestamp")
|
|
key: str = Field(..., description="Metric key")
|
|
duration: int = Field(..., description="Duration in milliseconds")
|
|
data: Optional[Dict[str, Any]] = Field(default=None, description="Additional metric data")
|
|
|
|
|
|
class AddHistoryEvent(BaseModel):
|
|
"""Conversation history entry added event."""
|
|
|
|
event: str = Field(default="addHistory", description="Event type")
|
|
timestamp: int = Field(default_factory=current_timestamp_ms, description="Event timestamp")
|
|
sender: Optional[str] = Field(default=None, description="Component that added history")
|
|
speaker: str = Field(..., description="Speaker identifier")
|
|
text: str = Field(..., description="Conversation text")
|
|
|
|
|
|
class DTMFEvent(BaseEvent):
|
|
"""DTMF tone detected event."""
|
|
|
|
event: str = Field(default="dtmf", description="Event type")
|
|
digit: str = Field(..., description="DTMF digit (0-9, *, #, A-D)")
|
|
|
|
|
|
# Event type mapping
|
|
EVENT_TYPES = {
|
|
"incoming": IncomingEvent,
|
|
"answer": AnswerEvent,
|
|
"reject": RejectEvent,
|
|
"ringing": RingingEvent,
|
|
"hangup": HangupEvent,
|
|
"speaking": SpeakingEvent,
|
|
"silence": SilenceEvent,
|
|
"asrFinal": AsrFinalEvent,
|
|
"asrDelta": AsrDeltaEvent,
|
|
"eou": EouEvent,
|
|
"trackStart": TrackStartEvent,
|
|
"trackEnd": TrackEndEvent,
|
|
"interruption": InterruptionEvent,
|
|
"error": ErrorEvent,
|
|
"metrics": MetricsEvent,
|
|
"addHistory": AddHistoryEvent,
|
|
"dtmf": DTMFEvent,
|
|
}
|
|
|
|
|
|
def create_event(event_type: str, **kwargs) -> BaseModel:
|
|
"""
|
|
Create an event model.
|
|
|
|
Args:
|
|
event_type: Type of event to create
|
|
**kwargs: Event fields
|
|
|
|
Returns:
|
|
Event model instance
|
|
|
|
Raises:
|
|
ValueError: If event type is unknown
|
|
"""
|
|
event_class = EVENT_TYPES.get(event_type)
|
|
|
|
if not event_class:
|
|
raise ValueError(f"Unknown event type: {event_type}")
|
|
|
|
return event_class(event=event_type, **kwargs)
|