b681eac2df
- REST :10085 / MQTT 控制 / SSE 回退 / AI+语音 - 数据源规划:接核心服务端 + 算力引擎真实数据
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
# ruff: noqa: I001
|
|
|
|
import logging
|
|
from queue import Empty, Queue
|
|
from threading import Event
|
|
from time import perf_counter
|
|
from typing import Any, Generic, Iterator, TypeVar, cast
|
|
|
|
import numpy as np
|
|
|
|
from speech_to_speech.pipeline.control import PipelineControlMessage, is_control_message, SESSION_END
|
|
from speech_to_speech.pipeline.log_context import pipeline_log_ctx
|
|
from speech_to_speech.pipeline.messages import PIPELINE_END, AudioOutput, EndOfResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
InT = TypeVar("InT")
|
|
OutT = TypeVar("OutT")
|
|
|
|
|
|
class BaseHandler(Generic[InT, OutT]):
|
|
"""
|
|
Base class for pipeline parts. Each part of the pipeline has an input and an output queue.
|
|
The `setup` method along with `setup_args` and `setup_kwargs` can be used to address the specific requirements of the implemented pipeline part.
|
|
To stop a handler properly, set the stop_event and, to avoid queue deadlocks, place PIPELINE_END in the input queue.
|
|
Objects placed in the input queue will be processed by the `process` method, and the yielded results will be placed in the output queue.
|
|
`SESSION_END` is a soft control message used to reset per-session state without stopping the handler thread.
|
|
The cleanup method handles stopping the handler, and PIPELINE_END is placed in the output queue.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
stop_event: Event,
|
|
queue_in: Queue[InT | PipelineControlMessage | bytes],
|
|
queue_out: Queue[OutT | PipelineControlMessage | bytes],
|
|
setup_args: tuple[Any, ...] = (),
|
|
setup_kwargs: dict[str, Any] = {},
|
|
) -> None:
|
|
self.stop_event = stop_event
|
|
self.queue_in = queue_in
|
|
self.queue_out = queue_out
|
|
self.pipeline_index: int | None = None
|
|
self.setup(*setup_args, **setup_kwargs)
|
|
self._times: list[float] = []
|
|
|
|
def setup(self, *arg: Any, **kwargs: Any) -> None:
|
|
pass
|
|
|
|
def process(self, input: InT) -> Iterator[OutT]:
|
|
raise NotImplementedError
|
|
|
|
def should_process_input(self, item: InT) -> bool:
|
|
cancel_scope = getattr(self, "cancel_scope", None)
|
|
cancel_generation = getattr(item, "cancel_generation", None)
|
|
if (
|
|
cancel_scope is not None
|
|
and cancel_generation is not None
|
|
and not isinstance(item, EndOfResponse)
|
|
and cancel_scope.is_stale(cancel_generation)
|
|
):
|
|
logger.debug(
|
|
"%s: dropping stale input for cancel generation %s",
|
|
self.__class__.__name__,
|
|
cancel_generation,
|
|
)
|
|
return False
|
|
return True
|
|
|
|
def should_emit_output(self, output: OutT) -> bool:
|
|
return True
|
|
|
|
def before_emit_output(self, output: OutT) -> None:
|
|
pass
|
|
|
|
def output_for_queue(self, output: OutT, source_input: InT) -> OutT | AudioOutput:
|
|
cancel_generation = getattr(source_input, "cancel_generation", None)
|
|
if cancel_generation is not None and (isinstance(output, bytes) or hasattr(output, "tobytes")):
|
|
audio = cast(bytes | np.ndarray, output)
|
|
return AudioOutput(audio=audio, cancel_generation=cancel_generation)
|
|
return output
|
|
|
|
def run(self) -> None:
|
|
if self.pipeline_index is not None:
|
|
pipeline_log_ctx.set(self.pipeline_index)
|
|
logger.debug(f"{self.__class__.__name__}: Handler thread started")
|
|
while not self.stop_event.is_set():
|
|
try:
|
|
# Use timeout to check stop_event periodically
|
|
item = self.queue_in.get(timeout=0.1)
|
|
except Empty:
|
|
continue
|
|
|
|
if isinstance(item, PipelineControlMessage) and is_control_message(item, SESSION_END.kind):
|
|
logger.debug(f"{self.__class__.__name__}: session end received")
|
|
try:
|
|
self.on_session_end()
|
|
except Exception as e:
|
|
logger.error(
|
|
f"{self.__class__.__name__}: Error in on_session_end(): {type(e).__name__}: {e}",
|
|
exc_info=True,
|
|
)
|
|
self.queue_out.put(item)
|
|
continue
|
|
|
|
if isinstance(item, bytes) and item == PIPELINE_END:
|
|
# sentinel signal to avoid queue deadlock
|
|
logger.debug("Stopping thread")
|
|
break
|
|
|
|
if isinstance(item, PipelineControlMessage):
|
|
logger.warning("%s: unexpected control message kind: %s", self.__class__.__name__, item.kind)
|
|
continue
|
|
|
|
typed_item = cast(InT, item)
|
|
if not self.should_process_input(typed_item):
|
|
continue
|
|
|
|
start_time = perf_counter()
|
|
try:
|
|
for output in self.process(typed_item):
|
|
if not self.should_emit_output(output):
|
|
start_time = perf_counter()
|
|
continue
|
|
self._times.append(perf_counter() - start_time)
|
|
if self.should_log_timing(output):
|
|
logger.log(self.timing_log_level, "%s: %.3f s", self.__class__.__name__, self.last_time)
|
|
self.before_emit_output(output)
|
|
queued_output = cast(
|
|
OutT | PipelineControlMessage | bytes,
|
|
self.output_for_queue(output, typed_item),
|
|
)
|
|
self.queue_out.put(queued_output)
|
|
start_time = perf_counter()
|
|
except Exception as e:
|
|
logger.error(f"{self.__class__.__name__}: Error in process(): {type(e).__name__}: {e}", exc_info=True)
|
|
|
|
self.cleanup()
|
|
self.queue_out.put(PIPELINE_END)
|
|
|
|
@property
|
|
def last_time(self) -> float:
|
|
return self._times[-1]
|
|
|
|
@property
|
|
def min_time_to_debug(self) -> float:
|
|
return 0.001
|
|
|
|
@property
|
|
def timing_log_level(self) -> int:
|
|
return logging.DEBUG
|
|
|
|
def should_log_timing(self, output: OutT) -> bool:
|
|
return self.last_time > self.min_time_to_debug
|
|
|
|
def cleanup(self) -> None:
|
|
pass
|
|
|
|
def on_session_end(self) -> None:
|
|
pass
|