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.
Files changed (118) hide show
  1. openspeech/__init__.py +75 -0
  2. openspeech/__main__.py +5 -0
  3. openspeech/cli.py +413 -0
  4. openspeech/client/__init__.py +4 -0
  5. openspeech/client/client.py +145 -0
  6. openspeech/config.py +212 -0
  7. openspeech/core/__init__.py +0 -0
  8. openspeech/core/base.py +75 -0
  9. openspeech/core/enums.py +39 -0
  10. openspeech/core/models.py +61 -0
  11. openspeech/core/registry.py +37 -0
  12. openspeech/core/settings.py +8 -0
  13. openspeech/demo.py +675 -0
  14. openspeech/dispatch/__init__.py +0 -0
  15. openspeech/dispatch/context.py +34 -0
  16. openspeech/dispatch/dispatcher.py +661 -0
  17. openspeech/dispatch/executors/__init__.py +0 -0
  18. openspeech/dispatch/executors/base.py +34 -0
  19. openspeech/dispatch/executors/in_process.py +66 -0
  20. openspeech/dispatch/executors/remote.py +64 -0
  21. openspeech/dispatch/executors/subprocess_exec.py +446 -0
  22. openspeech/dispatch/fanout.py +95 -0
  23. openspeech/dispatch/filters.py +73 -0
  24. openspeech/dispatch/lifecycle.py +178 -0
  25. openspeech/dispatch/watcher.py +82 -0
  26. openspeech/engine_catalog.py +236 -0
  27. openspeech/engine_registry.yaml +347 -0
  28. openspeech/exceptions.py +51 -0
  29. openspeech/factory.py +325 -0
  30. openspeech/local_engines/__init__.py +12 -0
  31. openspeech/local_engines/aim_resolver.py +91 -0
  32. openspeech/local_engines/backends/__init__.py +1 -0
  33. openspeech/local_engines/backends/docker_backend.py +490 -0
  34. openspeech/local_engines/backends/native_backend.py +902 -0
  35. openspeech/local_engines/base.py +30 -0
  36. openspeech/local_engines/engines/__init__.py +1 -0
  37. openspeech/local_engines/engines/faster_whisper.py +36 -0
  38. openspeech/local_engines/engines/fish_speech.py +33 -0
  39. openspeech/local_engines/engines/sherpa_onnx.py +56 -0
  40. openspeech/local_engines/engines/whisper.py +41 -0
  41. openspeech/local_engines/engines/whisperlivekit.py +60 -0
  42. openspeech/local_engines/manager.py +208 -0
  43. openspeech/local_engines/models.py +50 -0
  44. openspeech/local_engines/progress.py +69 -0
  45. openspeech/local_engines/registry.py +19 -0
  46. openspeech/local_engines/task_store.py +52 -0
  47. openspeech/local_engines/tasks.py +71 -0
  48. openspeech/logging_config.py +607 -0
  49. openspeech/observe/__init__.py +0 -0
  50. openspeech/observe/base.py +79 -0
  51. openspeech/observe/debug.py +44 -0
  52. openspeech/observe/latency.py +19 -0
  53. openspeech/observe/metrics.py +47 -0
  54. openspeech/observe/tracing.py +44 -0
  55. openspeech/observe/usage.py +27 -0
  56. openspeech/providers/__init__.py +0 -0
  57. openspeech/providers/_template.py +101 -0
  58. openspeech/providers/stt/__init__.py +0 -0
  59. openspeech/providers/stt/alibaba.py +86 -0
  60. openspeech/providers/stt/assemblyai.py +135 -0
  61. openspeech/providers/stt/azure_speech.py +99 -0
  62. openspeech/providers/stt/baidu.py +135 -0
  63. openspeech/providers/stt/deepgram.py +311 -0
  64. openspeech/providers/stt/elevenlabs.py +385 -0
  65. openspeech/providers/stt/faster_whisper.py +211 -0
  66. openspeech/providers/stt/google_cloud.py +106 -0
  67. openspeech/providers/stt/iflytek.py +427 -0
  68. openspeech/providers/stt/macos_speech.py +226 -0
  69. openspeech/providers/stt/openai.py +84 -0
  70. openspeech/providers/stt/sherpa_onnx.py +353 -0
  71. openspeech/providers/stt/tencent.py +212 -0
  72. openspeech/providers/stt/volcengine.py +107 -0
  73. openspeech/providers/stt/whisper.py +153 -0
  74. openspeech/providers/stt/whisperlivekit.py +530 -0
  75. openspeech/providers/stt/windows_speech.py +249 -0
  76. openspeech/providers/tts/__init__.py +0 -0
  77. openspeech/providers/tts/alibaba.py +95 -0
  78. openspeech/providers/tts/azure_speech.py +123 -0
  79. openspeech/providers/tts/baidu.py +143 -0
  80. openspeech/providers/tts/coqui.py +64 -0
  81. openspeech/providers/tts/cosyvoice.py +90 -0
  82. openspeech/providers/tts/deepgram.py +174 -0
  83. openspeech/providers/tts/elevenlabs.py +311 -0
  84. openspeech/providers/tts/fish_speech.py +158 -0
  85. openspeech/providers/tts/google_cloud.py +107 -0
  86. openspeech/providers/tts/iflytek.py +209 -0
  87. openspeech/providers/tts/macos_say.py +251 -0
  88. openspeech/providers/tts/minimax.py +122 -0
  89. openspeech/providers/tts/openai.py +104 -0
  90. openspeech/providers/tts/piper.py +104 -0
  91. openspeech/providers/tts/tencent.py +189 -0
  92. openspeech/providers/tts/volcengine.py +117 -0
  93. openspeech/providers/tts/windows_sapi.py +234 -0
  94. openspeech/server/__init__.py +1 -0
  95. openspeech/server/app.py +72 -0
  96. openspeech/server/auth.py +42 -0
  97. openspeech/server/middleware.py +75 -0
  98. openspeech/server/routes/__init__.py +1 -0
  99. openspeech/server/routes/management.py +848 -0
  100. openspeech/server/routes/stt.py +121 -0
  101. openspeech/server/routes/tts.py +159 -0
  102. openspeech/server/routes/webui.py +29 -0
  103. openspeech/server/webui/app.js +2649 -0
  104. openspeech/server/webui/index.html +216 -0
  105. openspeech/server/webui/styles.css +617 -0
  106. openspeech/server/ws/__init__.py +1 -0
  107. openspeech/server/ws/stt_stream.py +263 -0
  108. openspeech/server/ws/tts_stream.py +207 -0
  109. openspeech/telemetry/__init__.py +21 -0
  110. openspeech/telemetry/perf.py +307 -0
  111. openspeech/utils/__init__.py +5 -0
  112. openspeech/utils/audio_converter.py +406 -0
  113. openspeech/utils/audio_playback.py +156 -0
  114. openspeech/vendor_registry.yaml +74 -0
  115. openspeechapi-0.1.0.dist-info/METADATA +101 -0
  116. openspeechapi-0.1.0.dist-info/RECORD +118 -0
  117. openspeechapi-0.1.0.dist-info/WHEEL +4 -0
  118. openspeechapi-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,263 @@
1
+ """WebSocket STT streaming endpoint."""
2
+ from __future__ import annotations
3
+ import asyncio
4
+ import json
5
+ import uuid
6
+ from openspeech.logging_config import logger
7
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
8
+
9
+ from openspeech.core.enums import Capability
10
+ from openspeech.logging_config import bind_context
11
+ from openspeech.telemetry.perf import Event, PerfTimer, milestone
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.websocket("/stream")
17
+ async def stt_stream(
18
+ websocket: WebSocket,
19
+ provider: str = "faster-whisper",
20
+ language: str | None = None,
21
+ sample_rate: int = 16000,
22
+ ):
23
+ # Establish request_id early so all log lines from this connection are
24
+ # correlated, including auth-failure paths.
25
+ request_id = (
26
+ websocket.query_params.get("request_id")
27
+ or websocket.headers.get("x-request-id")
28
+ or uuid.uuid4().hex[:12]
29
+ )
30
+
31
+ with bind_context(request_id=request_id, provider=provider, engine=provider):
32
+ # WebSocket auth via query param
33
+ server_config = getattr(websocket.app.state, "server_config", None)
34
+ if server_config is not None and server_config.auth_enabled:
35
+ token = websocket.query_params.get("token", "")
36
+ if token not in server_config.api_keys:
37
+ milestone(Event.WS_ERROR, reason="unauthorized", scope="stt")
38
+ await websocket.close(code=4001, reason="Unauthorized")
39
+ return
40
+
41
+ with PerfTimer(Event.WS_TOTAL, scope="stt", provider=provider) as ws_timer:
42
+ await websocket.accept()
43
+ milestone(
44
+ Event.WS_ACCEPT,
45
+ scope="stt",
46
+ provider=provider,
47
+ language=language,
48
+ sample_rate=sample_rate,
49
+ )
50
+ dispatcher = websocket.app.state.dispatcher
51
+
52
+ # Determine if the provider supports real streaming
53
+ handle = dispatcher._handles.get(provider)
54
+ provider_cls = handle.provider_cls if handle else None
55
+ supports_streaming = (
56
+ provider_cls is not None
57
+ and Capability.STREAMING in getattr(provider_cls, "capabilities", set())
58
+ )
59
+
60
+ # Send meta message so frontend knows batch vs streaming mode
61
+ await websocket.send_json({
62
+ "type": "meta",
63
+ "streaming": supports_streaming,
64
+ "provider": provider,
65
+ "request_id": request_id,
66
+ })
67
+ milestone(
68
+ Event.WS_META_SENT,
69
+ level="verbose",
70
+ scope="stt",
71
+ streaming=supports_streaming,
72
+ )
73
+
74
+ try:
75
+ if supports_streaming:
76
+ await _run_streaming(
77
+ websocket=websocket,
78
+ dispatcher=dispatcher,
79
+ provider=provider,
80
+ language=language,
81
+ ws_timer=ws_timer,
82
+ )
83
+ else:
84
+ await _run_batch(
85
+ websocket=websocket,
86
+ dispatcher=dispatcher,
87
+ provider=provider,
88
+ language=language,
89
+ sample_rate=sample_rate,
90
+ ws_timer=ws_timer,
91
+ )
92
+ except WebSocketDisconnect:
93
+ milestone(Event.WS_CLOSED, scope="stt", reason="client_disconnect")
94
+ except Exception as exc:
95
+ milestone(
96
+ Event.WS_ERROR,
97
+ scope="stt",
98
+ error_type=type(exc).__name__,
99
+ error_message=str(exc),
100
+ )
101
+ logger.exception("STT WS error")
102
+ try:
103
+ await websocket.send_json({"type": "error", "detail": str(exc)})
104
+ except Exception:
105
+ pass
106
+
107
+
108
+ async def _run_streaming(
109
+ *,
110
+ websocket: WebSocket,
111
+ dispatcher,
112
+ provider: str,
113
+ language: str | None,
114
+ ws_timer: PerfTimer,
115
+ ) -> None:
116
+ """Pipe real-time audio chunks through ``transcribe_stream``."""
117
+ audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
118
+ first_audio_logged = False
119
+ first_response_logged = False
120
+
121
+ async def audio_source():
122
+ while True:
123
+ chunk = await audio_queue.get()
124
+ if chunk is None:
125
+ break
126
+ yield chunk
127
+
128
+ async def receive_audio():
129
+ """Receive WebSocket messages and push audio into the queue."""
130
+ nonlocal first_audio_logged
131
+ frame_count = 0
132
+ total_bytes = 0
133
+ try:
134
+ while True:
135
+ message = await websocket.receive()
136
+ if message.get("type") == "websocket.disconnect":
137
+ break
138
+ if message.get("bytes") is not None:
139
+ if not first_audio_logged:
140
+ first_audio_logged = True
141
+ ws_timer.emit_milestone(
142
+ Event.WS_FIRST_AUDIO_FRAME,
143
+ scope="stt",
144
+ bytes=len(message["bytes"]),
145
+ )
146
+ frame_count += 1
147
+ total_bytes += len(message["bytes"])
148
+ await audio_queue.put(message["bytes"])
149
+ elif message.get("text") is not None:
150
+ data = json.loads(message["text"])
151
+ if data.get("type") == "stop":
152
+ break
153
+ finally:
154
+ ws_timer.add(frames_received=frame_count, bytes_received=total_bytes)
155
+ await audio_queue.put(None)
156
+
157
+ recv_task = asyncio.create_task(receive_audio())
158
+
159
+ # Ensure provider is started before streaming (lazy-load)
160
+ await dispatcher._lifecycle.ensure_ready(provider)
161
+
162
+ # Stream via executor.invoke_stream
163
+ handle = dispatcher._handles[provider]
164
+ partial_count = 0
165
+ final_count = 0
166
+ async for transcription in handle.executor.invoke_stream(
167
+ "transcribe_stream", stream=audio_source()
168
+ ):
169
+ is_partial = getattr(transcription, "is_partial", True)
170
+ msg_type = "final" if not is_partial else "partial"
171
+ if not first_response_logged:
172
+ first_response_logged = True
173
+ ws_timer.mark_ttfb()
174
+ ws_timer.emit_milestone(
175
+ Event.WS_FIRST_RESPONSE,
176
+ scope="stt",
177
+ msg_type=msg_type,
178
+ )
179
+ if is_partial:
180
+ partial_count += 1
181
+ else:
182
+ final_count += 1
183
+ ws_timer.emit_milestone(
184
+ Event.WS_FINAL_SENT,
185
+ scope="stt",
186
+ text_preview=(transcription.text or "")[:80],
187
+ )
188
+ await websocket.send_json({
189
+ "type": msg_type,
190
+ "text": transcription.text,
191
+ "confidence": transcription.confidence,
192
+ "language": transcription.language,
193
+ })
194
+
195
+ # Give receive_audio a chance to finish (client may have already
196
+ # disconnected after receiving "final"); cancel if it doesn't complete
197
+ # promptly so we don't hang forever.
198
+ recv_task.cancel()
199
+ try:
200
+ await recv_task
201
+ except asyncio.CancelledError:
202
+ pass
203
+ ws_timer.add(partials=partial_count, finals=final_count, mode="streaming")
204
+ await websocket.send_json({"type": "closed"})
205
+ milestone(Event.WS_CLOSED, scope="stt", mode="streaming")
206
+
207
+
208
+ async def _run_batch(
209
+ *,
210
+ websocket: WebSocket,
211
+ dispatcher,
212
+ provider: str,
213
+ language: str | None,
214
+ sample_rate: int,
215
+ ws_timer: PerfTimer,
216
+ ) -> None:
217
+ """Accumulate all audio then transcribe in one batch call (fallback)."""
218
+ audio_buffer = bytearray()
219
+ logger.info("STT WS batch mode start")
220
+
221
+ while True:
222
+ message = await websocket.receive()
223
+ if message.get("type") == "websocket.disconnect":
224
+ break
225
+ if message.get("bytes") is not None:
226
+ audio_buffer.extend(message["bytes"])
227
+ elif message.get("text") is not None:
228
+ data = json.loads(message["text"])
229
+ if data.get("type") == "stop":
230
+ break
231
+
232
+ if audio_buffer:
233
+ from openspeech.core.enums import AudioFormat
234
+ from openspeech.core.models import AudioData, STTOptions
235
+
236
+ audio_data = AudioData(
237
+ data=bytes(audio_buffer),
238
+ sample_rate=sample_rate,
239
+ channels=1,
240
+ format=AudioFormat.PCM_16K,
241
+ )
242
+ opts = STTOptions(language=language)
243
+ ws_timer.add(bytes_received=len(audio_buffer), mode="batch")
244
+ with PerfTimer(Event.DISPATCH_TOTAL, scope="stt", provider=provider, method="transcribe"):
245
+ result = await dispatcher.stt.transcribe(provider, audio_data, opts)
246
+ ws_timer.mark_ttfb()
247
+ if result:
248
+ ws_timer.emit_milestone(
249
+ Event.WS_FINAL_SENT,
250
+ scope="stt",
251
+ text_preview=(result.text or "")[:80],
252
+ )
253
+ await websocket.send_json({
254
+ "type": "final",
255
+ "text": result.text,
256
+ "confidence": result.confidence,
257
+ "language": result.language,
258
+ })
259
+ else:
260
+ logger.warning("STT WS batch: empty audio buffer, skipping transcription")
261
+
262
+ await websocket.send_json({"type": "closed"})
263
+ milestone(Event.WS_CLOSED, scope="stt", mode="batch")
@@ -0,0 +1,207 @@
1
+ """WebSocket TTS streaming endpoint."""
2
+ from __future__ import annotations
3
+ import json
4
+ import uuid
5
+ from openspeech.logging_config import logger
6
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
7
+
8
+ from openspeech.core.enums import Capability
9
+ from openspeech.logging_config import bind_context
10
+ from openspeech.telemetry.perf import Event, PerfTimer, milestone
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.websocket("/stream")
16
+ async def tts_stream(websocket: WebSocket):
17
+ request_id = (
18
+ websocket.query_params.get("request_id")
19
+ or websocket.headers.get("x-request-id")
20
+ or uuid.uuid4().hex[:12]
21
+ )
22
+
23
+ with bind_context(request_id=request_id):
24
+ # WebSocket auth via query param
25
+ server_config = getattr(websocket.app.state, "server_config", None)
26
+ if server_config is not None and server_config.auth_enabled:
27
+ token = websocket.query_params.get("token", "")
28
+ if token not in server_config.api_keys:
29
+ milestone(Event.WS_ERROR, reason="unauthorized", scope="tts")
30
+ await websocket.close(code=4001, reason="Unauthorized")
31
+ return
32
+
33
+ with PerfTimer(Event.WS_TOTAL, scope="tts") as ws_timer:
34
+ await websocket.accept()
35
+ milestone(Event.WS_ACCEPT, scope="tts")
36
+ dispatcher = websocket.app.state.dispatcher
37
+
38
+ try:
39
+ # Receive synthesis request
40
+ message = await websocket.receive_text()
41
+ data = json.loads(message)
42
+
43
+ provider = data.get("provider", "openai-tts")
44
+ text = data.get("text", "")
45
+ voice = data.get("voice")
46
+ speed = data.get("speed", 1.0)
47
+ model = data.get("model")
48
+ stream_transport = data.get("stream_transport")
49
+
50
+ # Bind provider now that we know it.
51
+ with bind_context(provider=provider, engine=provider):
52
+ ws_timer.add(provider=provider, text_len=len(text))
53
+
54
+ from openspeech.core.models import TTSOptions
55
+ opts = TTSOptions(
56
+ voice=voice,
57
+ speed=float(speed),
58
+ model=str(model).strip() if model else None,
59
+ stream_transport=str(stream_transport).strip() if stream_transport else None,
60
+ )
61
+
62
+ # Check if provider supports true streaming
63
+ handle = dispatcher._get_handle(provider)
64
+ provider_cls = handle.provider_cls
65
+ has_streaming = (
66
+ Capability.STREAMING in getattr(provider_cls, "capabilities", set())
67
+ )
68
+
69
+ # Detect output format from provider settings when possible
70
+ # (fallback to wav for PCM-like providers).
71
+ audio_format = "wav"
72
+ try:
73
+ settings = getattr(handle, "settings_dict", {}) or {}
74
+ if "output_format" in settings and settings["output_format"]:
75
+ # e.g. mp3_44100_128 -> mp3
76
+ audio_format = str(settings["output_format"]).split("_", 1)[0]
77
+ elif hasattr(provider_cls, "name") and "iflytek" in provider_cls.name:
78
+ audio_format = "mp3"
79
+ except Exception:
80
+ pass
81
+
82
+ if has_streaming:
83
+ await _run_streaming(
84
+ websocket=websocket,
85
+ dispatcher=dispatcher,
86
+ provider=provider,
87
+ text=text,
88
+ opts=opts,
89
+ audio_format=audio_format,
90
+ ws_timer=ws_timer,
91
+ )
92
+ else:
93
+ await _run_batch(
94
+ websocket=websocket,
95
+ dispatcher=dispatcher,
96
+ provider=provider,
97
+ text=text,
98
+ opts=opts,
99
+ ws_timer=ws_timer,
100
+ )
101
+ except WebSocketDisconnect:
102
+ milestone(Event.WS_CLOSED, scope="tts", reason="client_disconnect")
103
+ except Exception as exc:
104
+ milestone(
105
+ Event.WS_ERROR,
106
+ scope="tts",
107
+ error_type=type(exc).__name__,
108
+ error_message=str(exc),
109
+ )
110
+ logger.exception("TTS WS error")
111
+ try:
112
+ await websocket.send_json({"type": "error", "detail": str(exc)})
113
+ except Exception:
114
+ pass
115
+
116
+
117
+ async def _run_streaming(
118
+ *,
119
+ websocket: WebSocket,
120
+ dispatcher,
121
+ provider: str,
122
+ text: str,
123
+ opts,
124
+ audio_format: str,
125
+ ws_timer: PerfTimer,
126
+ ) -> None:
127
+ """True streaming: yield chunks as they're produced."""
128
+ await websocket.send_json({
129
+ "type": "meta",
130
+ "streaming": True,
131
+ "audio_format": audio_format,
132
+ })
133
+ milestone(Event.WS_META_SENT, level="verbose", scope="tts", streaming=True)
134
+
135
+ total_bytes = 0
136
+ chunk_seq = 0
137
+ async for chunk in dispatcher.tts.synthesize_stream(provider, text, opts):
138
+ if chunk.data:
139
+ if chunk_seq == 0:
140
+ ws_timer.mark_ttfb()
141
+ ws_timer.emit_milestone(
142
+ Event.WS_FIRST_RESPONSE,
143
+ scope="tts",
144
+ chunk_bytes=len(chunk.data),
145
+ )
146
+ await websocket.send_bytes(chunk.data)
147
+ total_bytes += len(chunk.data)
148
+ chunk_seq += 1
149
+ if chunk.is_final:
150
+ break
151
+
152
+ ws_timer.add(chunks_sent=chunk_seq, bytes_sent=total_bytes, mode="streaming")
153
+ await websocket.send_json({
154
+ "type": "done",
155
+ "total_bytes": total_bytes,
156
+ "streaming": True,
157
+ "audio_format": audio_format,
158
+ })
159
+ milestone(Event.WS_CLOSED, scope="tts", mode="streaming")
160
+
161
+
162
+ async def _run_batch(
163
+ *,
164
+ websocket: WebSocket,
165
+ dispatcher,
166
+ provider: str,
167
+ text: str,
168
+ opts,
169
+ ws_timer: PerfTimer,
170
+ ) -> None:
171
+ """Batch synthesize, then stream chunks to client."""
172
+ await dispatcher._lifecycle.ensure_ready(provider)
173
+ with PerfTimer(Event.DISPATCH_TOTAL, scope="tts", method="synthesize"):
174
+ result = await dispatcher.tts.synthesize(provider, text, opts)
175
+
176
+ if not result:
177
+ return
178
+
179
+ ws_timer.mark_ttfb()
180
+ fmt = getattr(result, "format", "wav") or "wav"
181
+ await websocket.send_json({
182
+ "type": "meta",
183
+ "sample_rate": result.sample_rate,
184
+ "channels": result.channels,
185
+ "duration_ms": result.duration_ms or 0,
186
+ "total_bytes": len(result.data),
187
+ "audio_format": str(fmt),
188
+ })
189
+ milestone(Event.WS_META_SENT, level="verbose", scope="tts", streaming=False)
190
+
191
+ chunk_size = 4096
192
+ total_bytes = len(result.data)
193
+ chunk_seq = 0
194
+ for i in range(0, total_bytes, chunk_size):
195
+ chunk_data = result.data[i : i + chunk_size]
196
+ await websocket.send_bytes(chunk_data)
197
+ chunk_seq += 1
198
+
199
+ ws_timer.add(chunks_sent=chunk_seq, bytes_sent=total_bytes, mode="batch")
200
+ await websocket.send_json({
201
+ "type": "done",
202
+ "total_bytes": total_bytes,
203
+ "sample_rate": result.sample_rate,
204
+ "streaming": False,
205
+ "audio_format": str(fmt),
206
+ })
207
+ milestone(Event.WS_CLOSED, scope="tts", mode="batch")
@@ -0,0 +1,21 @@
1
+ """Telemetry helpers for structured performance logging."""
2
+
3
+ from openspeech.telemetry.perf import (
4
+ PerfTimer,
5
+ Event,
6
+ milestone,
7
+ perf_event,
8
+ perf_enabled,
9
+ timed,
10
+ timed_async,
11
+ )
12
+
13
+ __all__ = [
14
+ "Event",
15
+ "PerfTimer",
16
+ "milestone",
17
+ "perf_enabled",
18
+ "perf_event",
19
+ "timed",
20
+ "timed_async",
21
+ ]