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,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
|