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,30 @@
1
+ """Abstractions for local engine backends."""
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC, abstractmethod
5
+
6
+ from openspeech.local_engines.models import EngineSpec, EngineStatus, RuntimeConfig
7
+
8
+
9
+ class RuntimeBackend(ABC):
10
+ """Backend contract for local runtime operations (docker/native/etc)."""
11
+
12
+ runtime_name: str
13
+
14
+ @abstractmethod
15
+ def install(self, spec: EngineSpec, cfg: RuntimeConfig, report) -> None: ...
16
+
17
+ @abstractmethod
18
+ def update(self, spec: EngineSpec, cfg: RuntimeConfig, report) -> None: ...
19
+
20
+ @abstractmethod
21
+ def start(self, spec: EngineSpec, cfg: RuntimeConfig, report) -> None: ...
22
+
23
+ @abstractmethod
24
+ def stop(self, spec: EngineSpec, cfg: RuntimeConfig, report) -> None: ...
25
+
26
+ @abstractmethod
27
+ def status(self, spec: EngineSpec, cfg: RuntimeConfig) -> EngineStatus: ...
28
+
29
+ @abstractmethod
30
+ def logs(self, spec: EngineSpec, cfg: RuntimeConfig, lines: int = 100) -> str: ...
@@ -0,0 +1 @@
1
+ """Engine spec definitions."""
@@ -0,0 +1,36 @@
1
+ """Faster-Whisper local model engine spec."""
2
+ from __future__ import annotations
3
+
4
+ from openspeech.local_engines.models import EngineSpec
5
+
6
+
7
+ FASTER_WHISPER_SPEC = EngineSpec(
8
+ name="faster-whisper",
9
+ default_runtime="native",
10
+ description="Faster-Whisper local STT model assets",
11
+ options={
12
+ "native_model_only": True,
13
+ "native_install_script": "scripts/engines/faster-whisper/native/install.sh",
14
+ "native_model_dir": "models/faster-whisper",
15
+ "native_model_marker": "current",
16
+ "native_simulate_download": True,
17
+ "native_use_aim": True,
18
+ "native_aim_model_ids": [
19
+ "faster-whisper-large-v3-turbo",
20
+ ],
21
+ "native_aim_downloads": [
22
+ {
23
+ "model_id": "faster-whisper-large-v3-turbo",
24
+ "source": "hf:mobiuslabsgmbh/faster-whisper-large-v3-turbo",
25
+ "category": "asr",
26
+ }
27
+ ],
28
+ "native_aim_provision_engine": "whisper",
29
+ "native_aim_model_kind": "dir",
30
+ "native_existing_model_paths": [
31
+ "~/AI/whisper/faster-whisper-large-v3-turbo",
32
+ "~/AI/whisper/models--mobiuslabsgmbh--faster-whisper-large-v3-turbo",
33
+ "~/.cache/huggingface/hub/models--Systran--faster-whisper-large-v3-turbo",
34
+ ],
35
+ },
36
+ )
@@ -0,0 +1,33 @@
1
+ """Fish-Speech engine spec."""
2
+ from __future__ import annotations
3
+
4
+ from openspeech.local_engines.models import EngineSpec
5
+
6
+
7
+ FISH_SPEECH_SPEC = EngineSpec(
8
+ name="fish-speech",
9
+ default_runtime="docker",
10
+ description="Fish-Speech local TTS engine",
11
+ options={
12
+ "docker_image": "fishaudio/fish-speech:latest",
13
+ "container_name": "openspeech-fish-speech",
14
+ "platform": "linux/amd64",
15
+ "container_port": 8080,
16
+ "host_port": 8080,
17
+ "health_path": "/health",
18
+ # Override this if your image requires a custom startup command.
19
+ "start_command": "",
20
+ # Native runtime defaults.
21
+ "native_repo_url": "https://github.com/fishaudio/fish-speech.git",
22
+ "native_repo_ref": "main",
23
+ "native_install_script": "scripts/engines/fish-speech/native/install.sh",
24
+ "native_install_target": ".[cpu]",
25
+ "native_device": "mps",
26
+ "native_model_repo": "fishaudio/s2-pro",
27
+ "native_model_dir": "checkpoints/s2-pro",
28
+ "native_hf_max_workers": 4,
29
+ "native_llama_checkpoint_path": "checkpoints/s2-pro",
30
+ "native_decoder_checkpoint_path": "checkpoints/s2-pro/codec.pth",
31
+ "native_decoder_config_name": "modded_dac_vq",
32
+ },
33
+ )
@@ -0,0 +1,56 @@
1
+ """Sherpa-ONNX local STT engine spec."""
2
+ from __future__ import annotations
3
+
4
+ from openspeech.local_engines.models import EngineSpec
5
+
6
+
7
+ SHERPA_ONNX_SPEC = EngineSpec(
8
+ name="sherpa-onnx",
9
+ default_runtime="native",
10
+ description="Sherpa-ONNX local streaming STT engine",
11
+ options={
12
+ "native_install_script": "scripts/engines/sherpa-onnx/native/install.sh",
13
+ "native_repo_url": "https://github.com/k2-fsa/sherpa-onnx.git",
14
+ "native_repo_ref": "master",
15
+ "native_model_only": False,
16
+ "native_model_dir": "models/sherpa-onnx",
17
+ "native_model_marker": "metadata.json",
18
+ "native_use_aim": True,
19
+ "native_aim_model_ids": [
20
+ "sherpa-onnx-paraformer-zh-en",
21
+ "sherpa-onnx-zipformer-zh-en",
22
+ ],
23
+ "native_aim_downloads": [
24
+ {
25
+ "model_id": "sherpa-onnx-zipformer-zh-en",
26
+ "source": "hf:csukuangfj/sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20",
27
+ "category": "asr",
28
+ },
29
+ {
30
+ "model_id": "sherpa-onnx-paraformer-zh-en",
31
+ "source": "hf:k2-fsa/sherpa-onnx-paraformer-zh-en",
32
+ "category": "asr",
33
+ },
34
+ ],
35
+ "native_aim_provision_engine": "whisper",
36
+ "native_aim_model_kind": "dir",
37
+ "native_existing_model_paths": [
38
+ "~/AI/store/asr/model/sherpa-onnx-paraformer-zh-en",
39
+ "~/AI/store/asr/model/sherpa-onnx-zipformer-zh-en",
40
+ ],
41
+ "native_simulate_download": True,
42
+ "native_model_repo": "",
43
+ "native_start_cmd": [
44
+ "{venv_python}",
45
+ "{project_root}/scripts/engines/sherpa-onnx/native/run_streaming_server.py",
46
+ "--repo-dir",
47
+ "{repo_dir}",
48
+ "--host",
49
+ "{api_host}",
50
+ "--port",
51
+ "{api_port}",
52
+ "--model-dir",
53
+ "{model_dir}/current",
54
+ ],
55
+ },
56
+ )
@@ -0,0 +1,41 @@
1
+ """Whisper local model engine spec."""
2
+ from __future__ import annotations
3
+
4
+ from openspeech.local_engines.models import EngineSpec
5
+
6
+
7
+ WHISPER_SPEC = EngineSpec(
8
+ name="whisper",
9
+ default_runtime="native",
10
+ description="OpenAI Whisper local STT model assets",
11
+ options={
12
+ "native_model_only": True,
13
+ "native_install_script": "scripts/engines/whisper/native/install.sh",
14
+ "native_model_dir": "models/whisper",
15
+ "native_model_marker": "current.pt",
16
+ "native_simulate_download": True,
17
+ "native_use_aim": True,
18
+ "native_aim_model_ids": [
19
+ "whisper-large-v3-turbo",
20
+ "whisper-large-v3",
21
+ ],
22
+ "native_aim_downloads": [
23
+ {
24
+ "model_id": "whisper-large-v3-turbo",
25
+ "source": "hf:openai/whisper-large-v3-turbo",
26
+ "category": "asr",
27
+ },
28
+ {
29
+ "model_id": "whisper-large-v3",
30
+ "source": "hf:openai/whisper-large-v3",
31
+ "category": "asr",
32
+ },
33
+ ],
34
+ "native_aim_provision_engine": "whisper",
35
+ "native_aim_model_kind": "file",
36
+ "native_existing_model_paths": [
37
+ "~/AI/whisper/large-v3-turbo.pt",
38
+ "~/AI/whisper/large-v3.pt",
39
+ ],
40
+ },
41
+ )
@@ -0,0 +1,60 @@
1
+ """WhisperLiveKit local STT engine spec."""
2
+ from __future__ import annotations
3
+
4
+ from openspeech.local_engines.models import EngineSpec
5
+
6
+
7
+ WHISPERLIVEKIT_SPEC = EngineSpec(
8
+ name="whisperlivekit",
9
+ default_runtime="native",
10
+ description="WhisperLiveKit local streaming STT engine (mlx-whisper backend)",
11
+ options={
12
+ "native_install_script": "scripts/engines/whisperlivekit/native/install.sh",
13
+ "native_repo_url": "https://github.com/QuentinFuxa/WhisperLiveKit.git",
14
+ "native_repo_ref": "main",
15
+ "native_model_dir": "models/whisperlivekit",
16
+ "native_use_aim": True,
17
+ "native_aim_model_ids": [
18
+ "whisper-large-v3-turbo",
19
+ "whisper-large-v3",
20
+ ],
21
+ "native_aim_downloads": [
22
+ {
23
+ "model_id": "whisper-large-v3-turbo",
24
+ "source": "hf:openai/whisper-large-v3-turbo",
25
+ "category": "asr",
26
+ },
27
+ {
28
+ "model_id": "whisper-large-v3",
29
+ "source": "hf:openai/whisper-large-v3",
30
+ "category": "asr",
31
+ },
32
+ ],
33
+ "native_aim_provision_engine": "whisper",
34
+ "native_aim_model_kind": "file",
35
+ "native_existing_model_paths": [
36
+ "~/AI/whisper/large-v3-turbo.pt",
37
+ "~/AI/whisper/large-v3.pt",
38
+ ],
39
+ # Override with cfg.options.native_start_cmd if WhisperLiveKit CLI changes.
40
+ "native_start_cmd": [
41
+ "{service_dir}/.venv/bin/wlk",
42
+ "serve",
43
+ "--host",
44
+ "{api_host}",
45
+ "--port",
46
+ "{api_port}",
47
+ "--backend",
48
+ "mlx-whisper",
49
+ "--pcm-input",
50
+ "--no-vac",
51
+ "--no-vad",
52
+ "--min-chunk-size",
53
+ "0.2",
54
+ "--never-fire",
55
+ "--protocol-log",
56
+ "--protocol-log-max-chars",
57
+ "300",
58
+ ],
59
+ },
60
+ )
@@ -0,0 +1,208 @@
1
+ """Task-based local engine manager with progress events."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ import threading
6
+ from typing import Callable
7
+
8
+ from openspeech.local_engines.backends.docker_backend import DockerBackend
9
+ from openspeech.local_engines.backends.native_backend import NativeBackend
10
+ from openspeech.local_engines.models import EngineAction, EngineStatus, RuntimeConfig, TaskStatus
11
+ from openspeech.local_engines.progress import ProgressEmitter, ProgressEvent
12
+ from openspeech.local_engines.registry import default_engine_registry
13
+ from openspeech.local_engines.task_store import JsonTaskStore
14
+ from openspeech.local_engines.tasks import EngineTask
15
+
16
+
17
+ def _utc_now() -> datetime:
18
+ return datetime.now(timezone.utc)
19
+
20
+
21
+ class _TaskCancelled(RuntimeError):
22
+ """Internal cancellation sentinel."""
23
+
24
+
25
+ class EngineManager:
26
+ """Coordinates local engine actions with structured progress updates."""
27
+
28
+ def __init__(self) -> None:
29
+ self._engines = default_engine_registry()
30
+ self._backends = {
31
+ "docker": DockerBackend(),
32
+ "native": NativeBackend(),
33
+ }
34
+ self._tasks: dict[str, EngineTask] = {}
35
+ self._task_threads: dict[str, threading.Thread] = {}
36
+ self._lock = threading.Lock()
37
+ self._store = JsonTaskStore()
38
+ self.emitter = ProgressEmitter()
39
+
40
+ def _resolve_engine(self, name: str):
41
+ if name not in self._engines:
42
+ available = ", ".join(sorted(self._engines))
43
+ raise ValueError(f"Unknown engine '{name}'. Available: {available}")
44
+ return self._engines[name]
45
+
46
+ def _resolve_runtime(self, runtime: str):
47
+ if runtime not in self._backends:
48
+ available = ", ".join(sorted(self._backends))
49
+ raise ValueError(f"Unknown runtime '{runtime}'. Available: {available}")
50
+ return self._backends[runtime]
51
+
52
+ def _emit(self, task: EngineTask) -> None:
53
+ self._store.save(task)
54
+ self.emitter.emit(
55
+ ProgressEvent(
56
+ task_id=task.task_id,
57
+ engine=task.engine,
58
+ action=task.action.value,
59
+ runtime=task.runtime,
60
+ phase=task.phase,
61
+ message=task.message,
62
+ progress=task.progress,
63
+ eta_seconds=task.eta_seconds,
64
+ status=task.status,
65
+ metadata=dict(task.metadata),
66
+ )
67
+ )
68
+
69
+ def _raise_if_cancelled(self, task: EngineTask) -> None:
70
+ if bool(task.metadata.get("cancel_requested", False)):
71
+ raise _TaskCancelled("Task was cancelled by user.")
72
+
73
+ def _reporter(self, task: EngineTask) -> Callable:
74
+ def _report(
75
+ phase: str,
76
+ message: str,
77
+ progress: float | None = None,
78
+ *,
79
+ eta_seconds: int | None = None,
80
+ ) -> None:
81
+ self._raise_if_cancelled(task)
82
+ task.phase = phase
83
+ task.message = message
84
+ task.progress = progress
85
+ task.eta_seconds = eta_seconds
86
+ task.updated_at = _utc_now()
87
+ self._emit(task)
88
+
89
+ return _report
90
+
91
+ def _create_task(self, engine: str, action: EngineAction, runtime: str) -> EngineTask:
92
+ task = EngineTask(engine=engine, action=action, runtime=runtime)
93
+ with self._lock:
94
+ self._tasks[task.task_id] = task
95
+ self._emit(task)
96
+ return task
97
+
98
+ def get_task(self, task_id: str) -> EngineTask | None:
99
+ with self._lock:
100
+ if task_id in self._tasks:
101
+ return self._tasks[task_id]
102
+ return self._store.get(task_id)
103
+
104
+ def list_tasks(self, engine: str | None = None, limit: int = 20) -> list[EngineTask]:
105
+ return self._store.list(engine=engine, limit=limit)
106
+
107
+ def cancel_task(self, task_id: str) -> EngineTask | None:
108
+ task = self.get_task(task_id)
109
+ if task is None:
110
+ return None
111
+ if task.status in {TaskStatus.SUCCEEDED, TaskStatus.FAILED, TaskStatus.CANCELLED}:
112
+ return task
113
+ task.metadata["cancel_requested"] = True
114
+ task.phase = "cancel_requested"
115
+ task.message = "Cancellation requested. Waiting for safe stop point..."
116
+ task.updated_at = _utc_now()
117
+ self._emit(task)
118
+ return task
119
+
120
+ def _execute_task(self, task: EngineTask, cfg: RuntimeConfig) -> EngineTask:
121
+ spec = self._resolve_engine(task.engine)
122
+ backend = self._resolve_runtime(task.runtime)
123
+ self._raise_if_cancelled(task)
124
+
125
+ task.status = TaskStatus.RUNNING
126
+ task.phase = "starting"
127
+ task.message = f"{task.action.value} started."
128
+ task.progress = 0.0
129
+ task.updated_at = _utc_now()
130
+ self._emit(task)
131
+
132
+ report = self._reporter(task)
133
+ try:
134
+ if task.action == EngineAction.INSTALL:
135
+ backend.install(spec, cfg, report)
136
+ elif task.action == EngineAction.UPDATE:
137
+ backend.update(spec, cfg, report)
138
+ elif task.action == EngineAction.START:
139
+ backend.start(spec, cfg, report)
140
+ elif task.action == EngineAction.STOP:
141
+ backend.stop(spec, cfg, report)
142
+ else:
143
+ raise ValueError(f"Unsupported action: {task.action}")
144
+ task.status = TaskStatus.SUCCEEDED
145
+ task.phase = "completed"
146
+ task.message = f"{task.action.value} completed."
147
+ task.progress = 100.0
148
+ task.finished_at = _utc_now()
149
+ task.updated_at = task.finished_at
150
+ self._emit(task)
151
+ return task
152
+ except _TaskCancelled:
153
+ task.status = TaskStatus.CANCELLED
154
+ task.phase = "cancelled"
155
+ task.message = "Task cancelled by user."
156
+ task.error = None
157
+ task.finished_at = _utc_now()
158
+ task.updated_at = task.finished_at
159
+ self._emit(task)
160
+ return task
161
+ except Exception as exc:
162
+ task.status = TaskStatus.FAILED
163
+ task.phase = "failed"
164
+ task.message = str(exc)
165
+ task.error = str(exc)
166
+ task.finished_at = _utc_now()
167
+ task.updated_at = task.finished_at
168
+ self._emit(task)
169
+ raise
170
+
171
+ def run_action(self, engine: str, action: EngineAction, cfg: RuntimeConfig) -> EngineTask:
172
+ spec = self._resolve_engine(engine)
173
+ runtime = cfg.runtime or spec.default_runtime
174
+ task = self._create_task(engine=engine, action=action, runtime=runtime)
175
+ return self._execute_task(task, cfg)
176
+
177
+ def run_action_async(self, engine: str, action: EngineAction, cfg: RuntimeConfig) -> EngineTask:
178
+ spec = self._resolve_engine(engine)
179
+ runtime = cfg.runtime or spec.default_runtime
180
+ task = self._create_task(engine=engine, action=action, runtime=runtime)
181
+
182
+ def _worker() -> None:
183
+ try:
184
+ self._execute_task(task, cfg)
185
+ except Exception:
186
+ # _execute_task already records terminal state and error details.
187
+ pass
188
+ finally:
189
+ with self._lock:
190
+ self._task_threads.pop(task.task_id, None)
191
+
192
+ t = threading.Thread(target=_worker, daemon=True)
193
+ with self._lock:
194
+ self._task_threads[task.task_id] = t
195
+ t.start()
196
+ return task
197
+
198
+ def status(self, engine: str, cfg: RuntimeConfig) -> EngineStatus:
199
+ spec = self._resolve_engine(engine)
200
+ runtime = cfg.runtime or spec.default_runtime
201
+ backend = self._resolve_runtime(runtime)
202
+ return backend.status(spec, cfg)
203
+
204
+ def logs(self, engine: str, cfg: RuntimeConfig, lines: int = 100) -> str:
205
+ spec = self._resolve_engine(engine)
206
+ runtime = cfg.runtime or spec.default_runtime
207
+ backend = self._resolve_runtime(runtime)
208
+ return backend.logs(spec, cfg, lines=lines)
@@ -0,0 +1,50 @@
1
+ """Models for local engine lifecycle management."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+
9
+ class TaskStatus(str, Enum):
10
+ QUEUED = "queued"
11
+ RUNNING = "running"
12
+ SUCCEEDED = "succeeded"
13
+ FAILED = "failed"
14
+ CANCELLED = "cancelled"
15
+
16
+
17
+ class EngineAction(str, Enum):
18
+ INSTALL = "install"
19
+ UPDATE = "update"
20
+ START = "start"
21
+ STOP = "stop"
22
+
23
+
24
+ @dataclass
25
+ class RuntimeConfig:
26
+ runtime: str = "docker"
27
+ api_url: str = "http://127.0.0.1:8080"
28
+ install_dir: str = "~/AI/services"
29
+ work_dir: str = ".openspeech/engines"
30
+ timeout_s: float = 120.0
31
+ retries: int = 0
32
+ options: dict[str, Any] = field(default_factory=dict)
33
+
34
+
35
+ @dataclass
36
+ class EngineSpec:
37
+ name: str
38
+ default_runtime: str = "docker"
39
+ description: str = ""
40
+ options: dict[str, Any] = field(default_factory=dict)
41
+
42
+
43
+ @dataclass
44
+ class EngineStatus:
45
+ engine: str
46
+ runtime: str
47
+ running: bool
48
+ healthy: bool
49
+ detail: str = ""
50
+ metadata: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,69 @@
1
+ """Progress events for long-running local engine operations."""
2
+ from __future__ import annotations
3
+
4
+ import queue
5
+ import threading
6
+ from collections import defaultdict
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+
11
+ from openspeech.local_engines.models import TaskStatus
12
+
13
+
14
+ @dataclass
15
+ class ProgressEvent:
16
+ task_id: str
17
+ engine: str
18
+ action: str
19
+ runtime: str
20
+ phase: str
21
+ message: str
22
+ status: TaskStatus
23
+ progress: float | None = None
24
+ eta_seconds: int | None = None
25
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
26
+ metadata: dict[str, Any] = field(default_factory=dict)
27
+
28
+
29
+ class ProgressEmitter:
30
+ """In-process pub/sub emitter for progress events."""
31
+
32
+ def __init__(self) -> None:
33
+ self._lock = threading.Lock()
34
+ self._watchers: dict[str, list[queue.Queue[ProgressEvent]]] = defaultdict(list)
35
+ self._global_watchers: list[queue.Queue[ProgressEvent]] = []
36
+
37
+ def subscribe(self, task_id: str) -> "queue.Queue[ProgressEvent]":
38
+ q: queue.Queue[ProgressEvent] = queue.Queue()
39
+ with self._lock:
40
+ self._watchers[task_id].append(q)
41
+ return q
42
+
43
+ def unsubscribe(self, task_id: str, q: "queue.Queue[ProgressEvent]") -> None:
44
+ with self._lock:
45
+ watchers = self._watchers.get(task_id, [])
46
+ if q in watchers:
47
+ watchers.remove(q)
48
+ if not watchers and task_id in self._watchers:
49
+ del self._watchers[task_id]
50
+
51
+ def subscribe_all(self) -> "queue.Queue[ProgressEvent]":
52
+ q: queue.Queue[ProgressEvent] = queue.Queue()
53
+ with self._lock:
54
+ self._global_watchers.append(q)
55
+ return q
56
+
57
+ def unsubscribe_all(self, q: "queue.Queue[ProgressEvent]") -> None:
58
+ with self._lock:
59
+ if q in self._global_watchers:
60
+ self._global_watchers.remove(q)
61
+
62
+ def emit(self, event: ProgressEvent) -> None:
63
+ with self._lock:
64
+ queues = list(self._watchers.get(event.task_id, []))
65
+ global_queues = list(self._global_watchers)
66
+ for q in queues:
67
+ q.put(event)
68
+ for q in global_queues:
69
+ q.put(event)
@@ -0,0 +1,19 @@
1
+ """Engine registry for local runtime manager."""
2
+ from __future__ import annotations
3
+
4
+ from openspeech.local_engines.engines.faster_whisper import FASTER_WHISPER_SPEC
5
+ from openspeech.local_engines.engines.fish_speech import FISH_SPEECH_SPEC
6
+ from openspeech.local_engines.engines.sherpa_onnx import SHERPA_ONNX_SPEC
7
+ from openspeech.local_engines.engines.whisper import WHISPER_SPEC
8
+ from openspeech.local_engines.engines.whisperlivekit import WHISPERLIVEKIT_SPEC
9
+ from openspeech.local_engines.models import EngineSpec
10
+
11
+
12
+ def default_engine_registry() -> dict[str, EngineSpec]:
13
+ return {
14
+ FISH_SPEECH_SPEC.name: FISH_SPEECH_SPEC,
15
+ FASTER_WHISPER_SPEC.name: FASTER_WHISPER_SPEC,
16
+ WHISPER_SPEC.name: WHISPER_SPEC,
17
+ WHISPERLIVEKIT_SPEC.name: WHISPERLIVEKIT_SPEC,
18
+ SHERPA_ONNX_SPEC.name: SHERPA_ONNX_SPEC,
19
+ }
@@ -0,0 +1,52 @@
1
+ """Persistent task store for local engine operations."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import threading
6
+ from pathlib import Path
7
+
8
+ from openspeech.local_engines.tasks import EngineTask
9
+
10
+
11
+ class JsonTaskStore:
12
+ """Stores each task as a JSON file for cross-process access."""
13
+
14
+ def __init__(self, root_dir: Path | None = None) -> None:
15
+ base = root_dir or (Path.home() / ".openspeech" / "engine_tasks")
16
+ self._dir = base.expanduser().resolve()
17
+ self._dir.mkdir(parents=True, exist_ok=True)
18
+ self._lock = threading.Lock()
19
+
20
+ def _path(self, task_id: str) -> Path:
21
+ return self._dir / f"{task_id}.json"
22
+
23
+ def save(self, task: EngineTask) -> None:
24
+ path = self._path(task.task_id)
25
+ payload = task.snapshot()
26
+ tmp = path.with_suffix(".tmp")
27
+ with self._lock:
28
+ tmp.write_text(json.dumps(payload, ensure_ascii=True, indent=2))
29
+ tmp.replace(path)
30
+
31
+ def get(self, task_id: str) -> EngineTask | None:
32
+ path = self._path(task_id)
33
+ if not path.exists():
34
+ return None
35
+ data = json.loads(path.read_text())
36
+ return EngineTask.from_snapshot(data)
37
+
38
+ def list(self, engine: str | None = None, limit: int = 20) -> list[EngineTask]:
39
+ files = sorted(self._dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
40
+ tasks: list[EngineTask] = []
41
+ for p in files:
42
+ try:
43
+ data = json.loads(p.read_text())
44
+ task = EngineTask.from_snapshot(data)
45
+ except Exception:
46
+ continue
47
+ if engine and task.engine != engine:
48
+ continue
49
+ tasks.append(task)
50
+ if len(tasks) >= max(1, limit):
51
+ break
52
+ return tasks