openspeechapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openspeech/__init__.py +75 -0
- openspeech/__main__.py +5 -0
- openspeech/cli.py +413 -0
- openspeech/client/__init__.py +4 -0
- openspeech/client/client.py +145 -0
- openspeech/config.py +212 -0
- openspeech/core/__init__.py +0 -0
- openspeech/core/base.py +75 -0
- openspeech/core/enums.py +39 -0
- openspeech/core/models.py +61 -0
- openspeech/core/registry.py +37 -0
- openspeech/core/settings.py +8 -0
- openspeech/demo.py +675 -0
- openspeech/dispatch/__init__.py +0 -0
- openspeech/dispatch/context.py +34 -0
- openspeech/dispatch/dispatcher.py +661 -0
- openspeech/dispatch/executors/__init__.py +0 -0
- openspeech/dispatch/executors/base.py +34 -0
- openspeech/dispatch/executors/in_process.py +66 -0
- openspeech/dispatch/executors/remote.py +64 -0
- openspeech/dispatch/executors/subprocess_exec.py +446 -0
- openspeech/dispatch/fanout.py +95 -0
- openspeech/dispatch/filters.py +73 -0
- openspeech/dispatch/lifecycle.py +178 -0
- openspeech/dispatch/watcher.py +82 -0
- openspeech/engine_catalog.py +236 -0
- openspeech/engine_registry.yaml +347 -0
- openspeech/exceptions.py +51 -0
- openspeech/factory.py +325 -0
- openspeech/local_engines/__init__.py +12 -0
- openspeech/local_engines/aim_resolver.py +91 -0
- openspeech/local_engines/backends/__init__.py +1 -0
- openspeech/local_engines/backends/docker_backend.py +490 -0
- openspeech/local_engines/backends/native_backend.py +902 -0
- openspeech/local_engines/base.py +30 -0
- openspeech/local_engines/engines/__init__.py +1 -0
- openspeech/local_engines/engines/faster_whisper.py +36 -0
- openspeech/local_engines/engines/fish_speech.py +33 -0
- openspeech/local_engines/engines/sherpa_onnx.py +56 -0
- openspeech/local_engines/engines/whisper.py +41 -0
- openspeech/local_engines/engines/whisperlivekit.py +60 -0
- openspeech/local_engines/manager.py +208 -0
- openspeech/local_engines/models.py +50 -0
- openspeech/local_engines/progress.py +69 -0
- openspeech/local_engines/registry.py +19 -0
- openspeech/local_engines/task_store.py +52 -0
- openspeech/local_engines/tasks.py +71 -0
- openspeech/logging_config.py +607 -0
- openspeech/observe/__init__.py +0 -0
- openspeech/observe/base.py +79 -0
- openspeech/observe/debug.py +44 -0
- openspeech/observe/latency.py +19 -0
- openspeech/observe/metrics.py +47 -0
- openspeech/observe/tracing.py +44 -0
- openspeech/observe/usage.py +27 -0
- openspeech/providers/__init__.py +0 -0
- openspeech/providers/_template.py +101 -0
- openspeech/providers/stt/__init__.py +0 -0
- openspeech/providers/stt/alibaba.py +86 -0
- openspeech/providers/stt/assemblyai.py +135 -0
- openspeech/providers/stt/azure_speech.py +99 -0
- openspeech/providers/stt/baidu.py +135 -0
- openspeech/providers/stt/deepgram.py +311 -0
- openspeech/providers/stt/elevenlabs.py +385 -0
- openspeech/providers/stt/faster_whisper.py +211 -0
- openspeech/providers/stt/google_cloud.py +106 -0
- openspeech/providers/stt/iflytek.py +427 -0
- openspeech/providers/stt/macos_speech.py +226 -0
- openspeech/providers/stt/openai.py +84 -0
- openspeech/providers/stt/sherpa_onnx.py +353 -0
- openspeech/providers/stt/tencent.py +212 -0
- openspeech/providers/stt/volcengine.py +107 -0
- openspeech/providers/stt/whisper.py +153 -0
- openspeech/providers/stt/whisperlivekit.py +530 -0
- openspeech/providers/stt/windows_speech.py +249 -0
- openspeech/providers/tts/__init__.py +0 -0
- openspeech/providers/tts/alibaba.py +95 -0
- openspeech/providers/tts/azure_speech.py +123 -0
- openspeech/providers/tts/baidu.py +143 -0
- openspeech/providers/tts/coqui.py +64 -0
- openspeech/providers/tts/cosyvoice.py +90 -0
- openspeech/providers/tts/deepgram.py +174 -0
- openspeech/providers/tts/elevenlabs.py +311 -0
- openspeech/providers/tts/fish_speech.py +158 -0
- openspeech/providers/tts/google_cloud.py +107 -0
- openspeech/providers/tts/iflytek.py +209 -0
- openspeech/providers/tts/macos_say.py +251 -0
- openspeech/providers/tts/minimax.py +122 -0
- openspeech/providers/tts/openai.py +104 -0
- openspeech/providers/tts/piper.py +104 -0
- openspeech/providers/tts/tencent.py +189 -0
- openspeech/providers/tts/volcengine.py +117 -0
- openspeech/providers/tts/windows_sapi.py +234 -0
- openspeech/server/__init__.py +1 -0
- openspeech/server/app.py +72 -0
- openspeech/server/auth.py +42 -0
- openspeech/server/middleware.py +75 -0
- openspeech/server/routes/__init__.py +1 -0
- openspeech/server/routes/management.py +848 -0
- openspeech/server/routes/stt.py +121 -0
- openspeech/server/routes/tts.py +159 -0
- openspeech/server/routes/webui.py +29 -0
- openspeech/server/webui/app.js +2649 -0
- openspeech/server/webui/index.html +216 -0
- openspeech/server/webui/styles.css +617 -0
- openspeech/server/ws/__init__.py +1 -0
- openspeech/server/ws/stt_stream.py +263 -0
- openspeech/server/ws/tts_stream.py +207 -0
- openspeech/telemetry/__init__.py +21 -0
- openspeech/telemetry/perf.py +307 -0
- openspeech/utils/__init__.py +5 -0
- openspeech/utils/audio_converter.py +406 -0
- openspeech/utils/audio_playback.py +156 -0
- openspeech/vendor_registry.yaml +74 -0
- openspeechapi-0.1.0.dist-info/METADATA +101 -0
- openspeechapi-0.1.0.dist-info/RECORD +118 -0
- openspeechapi-0.1.0.dist-info/WHEEL +4 -0
- openspeechapi-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Structured performance / milestone logging primitives.
|
|
2
|
+
|
|
3
|
+
Two integration points are exposed:
|
|
4
|
+
|
|
5
|
+
1. :class:`PerfTimer` — context manager that measures an operation's wall
|
|
6
|
+
time and emits an ``event`` record on exit. Use for *phases* that have
|
|
7
|
+
a clear start/end (WS accept, provider init, streaming total, ...).
|
|
8
|
+
|
|
9
|
+
2. :func:`milestone` — one-shot event emitter (TTFB, first frame, ...).
|
|
10
|
+
|
|
11
|
+
Both helpers obey the global ``perf`` level configured in
|
|
12
|
+
:mod:`openspeech.logging_config`:
|
|
13
|
+
|
|
14
|
+
- ``off``: :class:`PerfTimer` / :func:`milestone` become no-ops.
|
|
15
|
+
- ``basic``: milestones tagged ``basic`` (the default) are emitted.
|
|
16
|
+
- ``verbose``: both ``basic`` and ``verbose`` milestones are emitted.
|
|
17
|
+
|
|
18
|
+
An :class:`Event` enum holds the full event taxonomy. New events should
|
|
19
|
+
be added there and documented in ``docs/architecture/logging-spec.md`` so
|
|
20
|
+
consumers (dashboards, LLM log analysis) have a stable vocabulary.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import functools
|
|
25
|
+
import time
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Any, Awaitable, Callable, Iterator, TypeVar
|
|
29
|
+
|
|
30
|
+
from openspeech.logging_config import logger
|
|
31
|
+
|
|
32
|
+
from openspeech.logging_config import get_log_settings
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Event taxonomy
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Event(str, Enum):
|
|
40
|
+
"""Canonical event names used in structured logs.
|
|
41
|
+
|
|
42
|
+
Names use dotted namespaces: ``<layer>.<phase>``. Values are stable
|
|
43
|
+
strings — never rename once shipped; add new ones instead.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# ---- WebSocket layer -------------------------------------------------
|
|
47
|
+
WS_ACCEPT = "ws.accept"
|
|
48
|
+
WS_META_SENT = "ws.meta_sent"
|
|
49
|
+
WS_FIRST_AUDIO_FRAME = "ws.first_audio_frame"
|
|
50
|
+
WS_FIRST_RESPONSE = "ws.first_response"
|
|
51
|
+
WS_PARTIAL_SENT = "ws.partial_sent"
|
|
52
|
+
WS_FINAL_SENT = "ws.final_sent"
|
|
53
|
+
WS_CLOSED = "ws.closed"
|
|
54
|
+
WS_ERROR = "ws.error"
|
|
55
|
+
WS_TOTAL = "ws.total"
|
|
56
|
+
|
|
57
|
+
# ---- HTTP layer ------------------------------------------------------
|
|
58
|
+
HTTP_REQUEST_START = "http.request_start"
|
|
59
|
+
HTTP_REQUEST_END = "http.request_end"
|
|
60
|
+
|
|
61
|
+
# ---- Dispatcher layer -----------------------------------------------
|
|
62
|
+
DISPATCH_QUEUE_WAIT = "dispatch.queue_wait"
|
|
63
|
+
DISPATCH_EXECUTOR_ACQUIRE = "dispatch.executor_acquire"
|
|
64
|
+
DISPATCH_INVOKE_START = "dispatch.invoke_start"
|
|
65
|
+
DISPATCH_INVOKE_END = "dispatch.invoke_end"
|
|
66
|
+
DISPATCH_INVOKE_ERROR = "dispatch.invoke_error"
|
|
67
|
+
DISPATCH_TOTAL = "dispatch.total"
|
|
68
|
+
DISPATCH_STREAM_START = "dispatch.stream_start"
|
|
69
|
+
DISPATCH_STREAM_END = "dispatch.stream_end"
|
|
70
|
+
|
|
71
|
+
# ---- Lifecycle layer -------------------------------------------------
|
|
72
|
+
LIFECYCLE_PROVIDER_INIT = "lifecycle.provider_init"
|
|
73
|
+
LIFECYCLE_PROVIDER_WARMUP = "lifecycle.provider_warmup"
|
|
74
|
+
LIFECYCLE_PROVIDER_READY = "lifecycle.provider_ready"
|
|
75
|
+
LIFECYCLE_PROVIDER_STOP = "lifecycle.provider_stop"
|
|
76
|
+
LIFECYCLE_IDLE_RECYCLE = "lifecycle.idle_recycle"
|
|
77
|
+
|
|
78
|
+
# ---- Provider layer --------------------------------------------------
|
|
79
|
+
PROVIDER_CONNECT = "provider.connect"
|
|
80
|
+
PROVIDER_FIRST_CHUNK_SENT = "provider.first_chunk_sent"
|
|
81
|
+
PROVIDER_FIRST_PARTIAL = "provider.first_partial"
|
|
82
|
+
PROVIDER_FINAL = "provider.final"
|
|
83
|
+
PROVIDER_TOTAL = "provider.total"
|
|
84
|
+
PROVIDER_ERROR = "provider.error"
|
|
85
|
+
|
|
86
|
+
# ---- Audio utilities -------------------------------------------------
|
|
87
|
+
AUDIO_DECODE = "audio.decode"
|
|
88
|
+
AUDIO_ENCODE = "audio.encode"
|
|
89
|
+
AUDIO_RESAMPLE = "audio.resample"
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str: # noqa: D401
|
|
92
|
+
return self.value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Perf-level gating
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
_LEVELS = {"off": 0, "basic": 1, "verbose": 2}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def perf_enabled(level: str = "basic") -> bool:
|
|
103
|
+
"""Return True if a record at the given verbosity should be emitted."""
|
|
104
|
+
current = get_log_settings().perf
|
|
105
|
+
return _LEVELS.get(current, 1) >= _LEVELS.get(level, 1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Core helpers
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _coerce_event(event: Event | str) -> str:
|
|
114
|
+
if isinstance(event, Event):
|
|
115
|
+
return event.value
|
|
116
|
+
return str(event)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def perf_event(
|
|
120
|
+
event: Event | str,
|
|
121
|
+
*,
|
|
122
|
+
level: str = "basic",
|
|
123
|
+
message: str | None = None,
|
|
124
|
+
**fields: Any,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Emit a structured perf event without a duration.
|
|
127
|
+
|
|
128
|
+
Equivalent to :func:`milestone`. Thin wrapper kept for readability when
|
|
129
|
+
the call site is clearly a "phase marker" rather than a "milestone".
|
|
130
|
+
"""
|
|
131
|
+
milestone(event, level=level, message=message, **fields)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def milestone(
|
|
135
|
+
event: Event | str,
|
|
136
|
+
*,
|
|
137
|
+
level: str = "basic",
|
|
138
|
+
message: str | None = None,
|
|
139
|
+
**fields: Any,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Emit a structured milestone event.
|
|
142
|
+
|
|
143
|
+
The event's ``level`` (``basic`` or ``verbose``) gates whether the
|
|
144
|
+
record is produced, based on the global ``perf`` setting.
|
|
145
|
+
"""
|
|
146
|
+
if not perf_enabled(level):
|
|
147
|
+
return
|
|
148
|
+
evt_str = _coerce_event(event)
|
|
149
|
+
# ``event`` is a reserved attribute on loguru records, so we pass it as
|
|
150
|
+
# bound extra. Same for optional elapsed/ttfb fields.
|
|
151
|
+
bindings = {"event": evt_str}
|
|
152
|
+
for k in ("elapsed_ms", "ttfb_ms", "phase"):
|
|
153
|
+
if k in fields and fields[k] is not None:
|
|
154
|
+
bindings[k] = fields.pop(k)
|
|
155
|
+
msg = message or evt_str
|
|
156
|
+
logger.bind(**bindings, **fields).info(msg)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# PerfTimer
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PerfTimer:
|
|
165
|
+
"""Context manager that times a phase and emits a structured event.
|
|
166
|
+
|
|
167
|
+
Usage::
|
|
168
|
+
|
|
169
|
+
with PerfTimer(Event.WS_TOTAL, level="basic", request_id=rid) as t:
|
|
170
|
+
...
|
|
171
|
+
t.add(bytes_received=1024)
|
|
172
|
+
...
|
|
173
|
+
# On exit, emits: event=ws.total elapsed_ms=...
|
|
174
|
+
|
|
175
|
+
Inside the block, :meth:`mark` records intermediate milestones (e.g.
|
|
176
|
+
TTFB) relative to the timer's start.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
__slots__ = ("_event", "_level", "_fields", "_start_ns", "_ttfb_ns", "_active", "_message")
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
event: Event | str,
|
|
184
|
+
*,
|
|
185
|
+
level: str = "basic",
|
|
186
|
+
message: str | None = None,
|
|
187
|
+
**fields: Any,
|
|
188
|
+
) -> None:
|
|
189
|
+
self._event = _coerce_event(event)
|
|
190
|
+
self._level = level
|
|
191
|
+
self._message = message
|
|
192
|
+
self._fields: dict[str, Any] = dict(fields)
|
|
193
|
+
self._start_ns: int = 0
|
|
194
|
+
self._ttfb_ns: int | None = None
|
|
195
|
+
self._active = False
|
|
196
|
+
|
|
197
|
+
# -- context manager ---------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def __enter__(self) -> "PerfTimer":
|
|
200
|
+
self._start_ns = time.perf_counter_ns()
|
|
201
|
+
self._active = perf_enabled(self._level)
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: D401
|
|
205
|
+
self._emit(error=exc_val)
|
|
206
|
+
|
|
207
|
+
# -- accessors ---------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def elapsed_ms(self) -> float:
|
|
211
|
+
return (time.perf_counter_ns() - self._start_ns) / 1_000_000
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def ttfb_ms(self) -> float | None:
|
|
215
|
+
if self._ttfb_ns is None:
|
|
216
|
+
return None
|
|
217
|
+
return (self._ttfb_ns - self._start_ns) / 1_000_000
|
|
218
|
+
|
|
219
|
+
def add(self, **fields: Any) -> None:
|
|
220
|
+
"""Attach extra fields to be emitted on exit."""
|
|
221
|
+
self._fields.update(fields)
|
|
222
|
+
|
|
223
|
+
def mark_ttfb(self) -> float:
|
|
224
|
+
"""Record first-byte/first-response time; returns ms since start."""
|
|
225
|
+
if self._ttfb_ns is None:
|
|
226
|
+
self._ttfb_ns = time.perf_counter_ns()
|
|
227
|
+
return self.ttfb_ms # type: ignore[return-value]
|
|
228
|
+
|
|
229
|
+
def emit_milestone(
|
|
230
|
+
self,
|
|
231
|
+
event: Event | str,
|
|
232
|
+
*,
|
|
233
|
+
level: str = "basic",
|
|
234
|
+
**fields: Any,
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Emit an intermediate milestone with ``elapsed_ms`` relative to this timer."""
|
|
237
|
+
elapsed = self.elapsed_ms
|
|
238
|
+
milestone(event, level=level, elapsed_ms=elapsed, **fields)
|
|
239
|
+
|
|
240
|
+
# -- internal ----------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
def _emit(self, *, error: BaseException | None = None) -> None:
|
|
243
|
+
if not self._active:
|
|
244
|
+
return
|
|
245
|
+
elapsed = self.elapsed_ms
|
|
246
|
+
fields = dict(self._fields)
|
|
247
|
+
if self._ttfb_ns is not None:
|
|
248
|
+
fields["ttfb_ms"] = self.ttfb_ms
|
|
249
|
+
if error is not None:
|
|
250
|
+
fields["error_type"] = type(error).__name__
|
|
251
|
+
fields["error_message"] = str(error)
|
|
252
|
+
milestone(
|
|
253
|
+
self._event,
|
|
254
|
+
level=self._level,
|
|
255
|
+
message=self._message,
|
|
256
|
+
elapsed_ms=elapsed,
|
|
257
|
+
**fields,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Decorators
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def timed(event: Event | str, *, level: str = "basic"):
|
|
269
|
+
"""Decorator: time a sync function and emit an event on return."""
|
|
270
|
+
|
|
271
|
+
def _wrap(fn: F) -> F:
|
|
272
|
+
@functools.wraps(fn)
|
|
273
|
+
def inner(*args: Any, **kwargs: Any) -> Any:
|
|
274
|
+
with PerfTimer(event, level=level):
|
|
275
|
+
return fn(*args, **kwargs)
|
|
276
|
+
return inner # type: ignore[return-value]
|
|
277
|
+
|
|
278
|
+
return _wrap
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def timed_async(event: Event | str, *, level: str = "basic"):
|
|
282
|
+
"""Decorator: time an async coroutine and emit an event on return."""
|
|
283
|
+
|
|
284
|
+
def _wrap(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
|
|
285
|
+
@functools.wraps(fn)
|
|
286
|
+
async def inner(*args: Any, **kwargs: Any) -> Any:
|
|
287
|
+
with PerfTimer(event, level=level):
|
|
288
|
+
return await fn(*args, **kwargs)
|
|
289
|
+
return inner
|
|
290
|
+
|
|
291
|
+
return _wrap
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
__all__ = [
|
|
295
|
+
"Event",
|
|
296
|
+
"PerfTimer",
|
|
297
|
+
"milestone",
|
|
298
|
+
"perf_enabled",
|
|
299
|
+
"perf_event",
|
|
300
|
+
"timed",
|
|
301
|
+
"timed_async",
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@contextmanager
|
|
306
|
+
def _noop() -> Iterator[None]:
|
|
307
|
+
yield
|