fasr-service-fastapi 0.5.2__tar.gz

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.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.3
2
+ Name: fasr-service-fastapi
3
+ Version: 0.5.2
4
+ Summary: FastAPI service plugin for fasr
5
+ Author: osc
6
+ Author-email: osc <790990241@qq.com>
7
+ Requires-Dist: fasr>=0.5.1
8
+ Requires-Dist: fastapi>=0.115.4
9
+ Requires-Dist: uvicorn>=0.32.0
10
+ Requires-Python: >=3.10, <3.13
11
+ Description-Content-Type: text/markdown
12
+
13
+ # fasr-service-fastapi
14
+
15
+ FastAPI service plugin for fasr. This package provides the concrete online
16
+ service implementation while the core `fasr` package keeps only service base
17
+ classes and registries.
18
+
19
+ ## Features
20
+
21
+ - Demo web page:
22
+ - `GET /` (same as `GET /demo`)
23
+ - Optional batch transcription HTTP endpoints:
24
+ - `POST /transcribe`
25
+ - `POST /inference`
26
+ - Optional realtime websocket endpoints:
27
+ - `GET /v1/realtime`
28
+ - `GET /api-ws/v1/realtime`
29
+ - Health endpoint:
30
+ - `GET /health`
31
+ - Config-driven model and router initialization through fasr registries
32
+
33
+ ## Registered entry points
34
+
35
+ | Group | Name | Entry point |
36
+ |---|---|---|
37
+ | `fasr_services` | `fastapi.v1` | `fasr_service_fastapi.service:FastAPIASRService` |
38
+ | `fasr_service_routers` | `transcribe.v1` | `fasr_service_fastapi.routers.transcribe:transcribe_entrypoint` |
39
+ | `fasr_service_routers` | `realtime.v1` | `fasr_service_fastapi.routers.realtime:realtime_entrypoint` |
40
+
41
+ ## Configuration
42
+
43
+ The service owns model instances under `[service.models]`. Each router owns its
44
+ own `model_map`, which maps router argument names to model names from
45
+ `[service.models]`. Routers are enabled only when their config block exists, so
46
+ omitting `[service.transcribe_router]` skips batch HTTP routes and omitting
47
+ `[service.realtime_router]` skips realtime websocket routes.
48
+ Configuring neither router is invalid and `fasr serve` will fail fast with a
49
+ clear error.
50
+
51
+ ```toml
52
+ [service]
53
+ @services = "fastapi.v1"
54
+ debug_logging = false
55
+
56
+ [service.models.qwen3_asr]
57
+ @asr_models = "qwen3asr"
58
+ size = "small"
59
+
60
+ [service.models.marblenet]
61
+ @vad_models = "marblenet"
62
+
63
+ [service.models.fsmn_online]
64
+ @vad_models = "fsmn_online"
65
+
66
+ [service.transcribe_router]
67
+ @service_routers = "transcribe.v1"
68
+
69
+ [service.transcribe_router.model_map]
70
+ vad_model = "marblenet"
71
+ asr_model = "qwen3_asr"
72
+
73
+ [service.realtime_router]
74
+ @service_routers = "realtime.v1"
75
+
76
+ [service.realtime_router.model_map]
77
+ vad_model = "fsmn_online"
78
+ asr_model = "qwen3_asr"
79
+ ```
80
+
81
+ Set `debug_logging = true` if you want to keep the service's `DEBUG` logs
82
+ during development.
83
+
84
+ For a transcribe-only service, remove the realtime router block:
85
+
86
+ ```toml
87
+ [service]
88
+ @services = "fastapi.v1"
89
+
90
+ [service.models.paraformer]
91
+ @asr_models = "paraformer"
92
+
93
+ [service.models.marblenet]
94
+ @vad_models = "marblenet"
95
+
96
+ [service.models.ct_transformer]
97
+ @punc_models = "ct_transformer"
98
+
99
+ [service.transcribe_router]
100
+ @service_routers = "transcribe.v1"
101
+
102
+ [service.transcribe_router.model_map]
103
+ vad_model = "marblenet"
104
+ asr_model = "paraformer"
105
+ punc_model = "ct_transformer"
106
+ ```
107
+
108
+ Router entry points are factory functions. For example, `transcribe_entrypoint`
109
+ creates default `AudioLoader`, `VoiceDetector`, and `SpeechRecognizer`
110
+ instances when the config only provides `model_map`. This avoids requiring users
111
+ to spell out internal router components in simple service configs. When
112
+ `punc_model` is present in the transcribe router `model_map`, the router also
113
+ creates a default `SpeechSentencizer` and adds it after the recognizer.
114
+
115
+ ## CLI
116
+
117
+ Install the service plugin through the root project extra:
118
+
119
+ ```bash
120
+ uv sync --extra service
121
+ ```
122
+
123
+ Generate and run a default config:
124
+
125
+ ```bash
126
+ fasr init --cfg run.cfg
127
+ fasr serve --cfg run.cfg
128
+ ```
129
+
130
+ Inspect or write the default config without starting the server:
131
+
132
+ ```bash
133
+ fasr serve --print_default_config true
134
+ fasr serve --write_default_config run.cfg
135
+ ```
136
+
137
+ The same default config is available from:
138
+
139
+ ```python
140
+ from fasr_service_fastapi import FastAPIASRService
141
+
142
+ config = FastAPIASRService.model_construct().get_default_config()
143
+ ```
144
+
145
+ ## Development
146
+
147
+ Install from the repository root:
148
+
149
+ ```bash
150
+ uv sync
151
+ ```
152
+
153
+ Run service tests:
154
+
155
+ ```bash
156
+ uv run pytest tests/test_online_service.py -q
157
+ ```
158
+
159
+ Build the plugin:
160
+
161
+ ```bash
162
+ uv build --package fasr-service-fastapi
163
+ ```
@@ -0,0 +1,151 @@
1
+ # fasr-service-fastapi
2
+
3
+ FastAPI service plugin for fasr. This package provides the concrete online
4
+ service implementation while the core `fasr` package keeps only service base
5
+ classes and registries.
6
+
7
+ ## Features
8
+
9
+ - Demo web page:
10
+ - `GET /` (same as `GET /demo`)
11
+ - Optional batch transcription HTTP endpoints:
12
+ - `POST /transcribe`
13
+ - `POST /inference`
14
+ - Optional realtime websocket endpoints:
15
+ - `GET /v1/realtime`
16
+ - `GET /api-ws/v1/realtime`
17
+ - Health endpoint:
18
+ - `GET /health`
19
+ - Config-driven model and router initialization through fasr registries
20
+
21
+ ## Registered entry points
22
+
23
+ | Group | Name | Entry point |
24
+ |---|---|---|
25
+ | `fasr_services` | `fastapi.v1` | `fasr_service_fastapi.service:FastAPIASRService` |
26
+ | `fasr_service_routers` | `transcribe.v1` | `fasr_service_fastapi.routers.transcribe:transcribe_entrypoint` |
27
+ | `fasr_service_routers` | `realtime.v1` | `fasr_service_fastapi.routers.realtime:realtime_entrypoint` |
28
+
29
+ ## Configuration
30
+
31
+ The service owns model instances under `[service.models]`. Each router owns its
32
+ own `model_map`, which maps router argument names to model names from
33
+ `[service.models]`. Routers are enabled only when their config block exists, so
34
+ omitting `[service.transcribe_router]` skips batch HTTP routes and omitting
35
+ `[service.realtime_router]` skips realtime websocket routes.
36
+ Configuring neither router is invalid and `fasr serve` will fail fast with a
37
+ clear error.
38
+
39
+ ```toml
40
+ [service]
41
+ @services = "fastapi.v1"
42
+ debug_logging = false
43
+
44
+ [service.models.qwen3_asr]
45
+ @asr_models = "qwen3asr"
46
+ size = "small"
47
+
48
+ [service.models.marblenet]
49
+ @vad_models = "marblenet"
50
+
51
+ [service.models.fsmn_online]
52
+ @vad_models = "fsmn_online"
53
+
54
+ [service.transcribe_router]
55
+ @service_routers = "transcribe.v1"
56
+
57
+ [service.transcribe_router.model_map]
58
+ vad_model = "marblenet"
59
+ asr_model = "qwen3_asr"
60
+
61
+ [service.realtime_router]
62
+ @service_routers = "realtime.v1"
63
+
64
+ [service.realtime_router.model_map]
65
+ vad_model = "fsmn_online"
66
+ asr_model = "qwen3_asr"
67
+ ```
68
+
69
+ Set `debug_logging = true` if you want to keep the service's `DEBUG` logs
70
+ during development.
71
+
72
+ For a transcribe-only service, remove the realtime router block:
73
+
74
+ ```toml
75
+ [service]
76
+ @services = "fastapi.v1"
77
+
78
+ [service.models.paraformer]
79
+ @asr_models = "paraformer"
80
+
81
+ [service.models.marblenet]
82
+ @vad_models = "marblenet"
83
+
84
+ [service.models.ct_transformer]
85
+ @punc_models = "ct_transformer"
86
+
87
+ [service.transcribe_router]
88
+ @service_routers = "transcribe.v1"
89
+
90
+ [service.transcribe_router.model_map]
91
+ vad_model = "marblenet"
92
+ asr_model = "paraformer"
93
+ punc_model = "ct_transformer"
94
+ ```
95
+
96
+ Router entry points are factory functions. For example, `transcribe_entrypoint`
97
+ creates default `AudioLoader`, `VoiceDetector`, and `SpeechRecognizer`
98
+ instances when the config only provides `model_map`. This avoids requiring users
99
+ to spell out internal router components in simple service configs. When
100
+ `punc_model` is present in the transcribe router `model_map`, the router also
101
+ creates a default `SpeechSentencizer` and adds it after the recognizer.
102
+
103
+ ## CLI
104
+
105
+ Install the service plugin through the root project extra:
106
+
107
+ ```bash
108
+ uv sync --extra service
109
+ ```
110
+
111
+ Generate and run a default config:
112
+
113
+ ```bash
114
+ fasr init --cfg run.cfg
115
+ fasr serve --cfg run.cfg
116
+ ```
117
+
118
+ Inspect or write the default config without starting the server:
119
+
120
+ ```bash
121
+ fasr serve --print_default_config true
122
+ fasr serve --write_default_config run.cfg
123
+ ```
124
+
125
+ The same default config is available from:
126
+
127
+ ```python
128
+ from fasr_service_fastapi import FastAPIASRService
129
+
130
+ config = FastAPIASRService.model_construct().get_default_config()
131
+ ```
132
+
133
+ ## Development
134
+
135
+ Install from the repository root:
136
+
137
+ ```bash
138
+ uv sync
139
+ ```
140
+
141
+ Run service tests:
142
+
143
+ ```bash
144
+ uv run pytest tests/test_online_service.py -q
145
+ ```
146
+
147
+ Build the plugin:
148
+
149
+ ```bash
150
+ uv build --package fasr-service-fastapi
151
+ ```
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "fasr-service-fastapi"
3
+ version = "0.5.2"
4
+ description = "FastAPI service plugin for fasr"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "osc", email = "790990241@qq.com" }
8
+ ]
9
+ requires-python = ">=3.10, <3.13"
10
+ dependencies = [
11
+ "fasr>=0.5.1",
12
+ "fastapi>=0.115.4",
13
+ "uvicorn>=0.32.0",
14
+ ]
15
+
16
+ [tool.uv.sources]
17
+ fasr = { workspace = true }
18
+
19
+ [project.entry-points."fasr_services"]
20
+ "fastapi.v1" = "fasr_service_fastapi.service:FastAPIASRService"
21
+
22
+ [project.entry-points."fasr_service_routers"]
23
+ "transcribe.v1" = "fasr_service_fastapi.routers.transcribe:transcribe_entrypoint"
24
+ "realtime.v1" = "fasr_service_fastapi.routers.realtime:realtime_entrypoint"
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.10.11,<0.11.0"]
28
+ build-backend = "uv_build"
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from fasr.model import ASRModel, VADModel
6
+ from fasr_service_fastapi.routers.realtime import (
7
+ RealtimeRouter,
8
+ RealtimeWebSocketHandler,
9
+ )
10
+
11
+ from .service import FastAPIASRService, configure_service_logging
12
+
13
+
14
+ def create_realtime_app(
15
+ *,
16
+ vad_model: VADModel,
17
+ asr_model: ASRModel,
18
+ sample_rate: int = 16000,
19
+ vad_chunk_size_ms: int = 100,
20
+ vad_model_name: str = "fsmn_online",
21
+ asr_model_name: str = "stream_qwen3_0_6b",
22
+ debug_logging: bool = False,
23
+ ) -> FastAPI:
24
+ configure_service_logging(debug_logging)
25
+ handler = RealtimeWebSocketHandler(
26
+ sample_rate=sample_rate,
27
+ chunk_size_ms=vad_chunk_size_ms,
28
+ model_name=asr_model_name,
29
+ )
30
+ router = RealtimeRouter(
31
+ handler=handler,
32
+ model_map={
33
+ "vad_model": "vad_model",
34
+ "asr_model": "asr_model",
35
+ },
36
+ )
37
+ router.setup(
38
+ service_id="realtime_test",
39
+ models={
40
+ "vad_model": vad_model,
41
+ "asr_model": asr_model,
42
+ },
43
+ )
44
+
45
+ app = FastAPI(title="fasr realtime asr service")
46
+ app.include_router(router.create_router())
47
+
48
+ @app.get("/health")
49
+ async def health() -> dict[str, object]:
50
+ return {
51
+ "status": "ok",
52
+ "vad_model": vad_model_name,
53
+ "asr_model": asr_model_name,
54
+ "models_loaded": handler.vad_model is not None
55
+ and handler.asr_model is not None,
56
+ }
57
+
58
+ return app
59
+
60
+
61
+ __all__ = ["FastAPIASRService", "create_realtime_app"]
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import Future
4
+ from dataclasses import dataclass
5
+ from itertools import count
6
+ import os
7
+ from queue import PriorityQueue
8
+ import threading
9
+ from typing import Callable, List, TypeVar
10
+
11
+ from loguru import logger
12
+ from pydantic import Field
13
+
14
+ from fasr.config import Config
15
+ from fasr.data.audio import AudioChunk, AudioSpan
16
+ from fasr.model import ASRModel
17
+
18
+ T = TypeVar("T")
19
+
20
+ ASR_PRIORITY_REALTIME = 0
21
+ ASR_PRIORITY_TRANSCRIBE = 10
22
+
23
+
24
+ @dataclass
25
+ class ASRInferenceTask:
26
+ fn: Callable[[], object]
27
+ future: Future[object]
28
+ label: str
29
+
30
+
31
+ class ASRInferenceScheduler:
32
+ """Single-worker scheduler for shared ASR model inference calls."""
33
+
34
+ def __init__(self, service_id: str = "") -> None:
35
+ self.service_id = service_id
36
+ self._tasks: PriorityQueue[tuple[int, int, ASRInferenceTask]] = PriorityQueue()
37
+ self._task_sequence = count()
38
+ self._worker: threading.Thread | None = None
39
+
40
+ def start(self) -> None:
41
+ if self._worker is not None and self._worker.is_alive():
42
+ return
43
+ self._worker = threading.Thread(
44
+ target=self._worker_loop,
45
+ name="asr-inference-worker",
46
+ daemon=True,
47
+ )
48
+ self._worker.start()
49
+ logger.info(
50
+ "ASR inference scheduler started: service_id={}, pid={}, thread={}",
51
+ self.service_id,
52
+ os.getpid(),
53
+ self._worker.name,
54
+ )
55
+
56
+ def submit_sync(
57
+ self,
58
+ label: str,
59
+ fn: Callable[[], T],
60
+ priority: int = ASR_PRIORITY_TRANSCRIBE,
61
+ ) -> T:
62
+ self.start()
63
+ future: Future[object] = Future()
64
+ self._tasks.put(
65
+ (
66
+ priority,
67
+ next(self._task_sequence),
68
+ ASRInferenceTask(fn=fn, future=future, label=label),
69
+ )
70
+ )
71
+ logger.debug(
72
+ "ASR inference task queued: service_id={}, pid={}, label={}, priority={}, queue_size={}",
73
+ self.service_id,
74
+ os.getpid(),
75
+ label,
76
+ priority,
77
+ self._tasks.qsize(),
78
+ )
79
+ return future.result() # type: ignore[return-value]
80
+
81
+ def _worker_loop(self) -> None:
82
+ while True:
83
+ priority, _, task = self._tasks.get()
84
+ logger.debug(
85
+ "ASR inference task started: service_id={}, pid={}, thread={}, label={}, priority={}, queue_size={}",
86
+ self.service_id,
87
+ os.getpid(),
88
+ threading.current_thread().name,
89
+ task.label,
90
+ priority,
91
+ self._tasks.qsize(),
92
+ )
93
+ try:
94
+ result = task.fn()
95
+ except Exception as exc:
96
+ task.future.set_exception(exc)
97
+ else:
98
+ task.future.set_result(result)
99
+ finally:
100
+ logger.debug(
101
+ "ASR inference task finished: service_id={}, pid={}, thread={}, label={}",
102
+ self.service_id,
103
+ os.getpid(),
104
+ threading.current_thread().name,
105
+ task.label,
106
+ )
107
+ self._tasks.task_done()
108
+
109
+
110
+ class ScheduledASRModel(ASRModel):
111
+ """ASR model wrapper that serializes calls to a shared model instance."""
112
+
113
+ model: ASRModel = Field(..., exclude=True)
114
+ scheduler: ASRInferenceScheduler = Field(..., exclude=True)
115
+
116
+ def load_checkpoint(self, checkpoint_dir: str | None):
117
+ return None
118
+
119
+ def get_config(self) -> Config:
120
+ return self.model.get_config()
121
+
122
+ def transcribe(
123
+ self,
124
+ batch: List[AudioSpan],
125
+ **kwargs,
126
+ ) -> List[AudioSpan]:
127
+ return self.scheduler.submit_sync(
128
+ f"{type(self.model).__name__}.transcribe",
129
+ lambda: self.model.transcribe(batch, **kwargs),
130
+ priority=ASR_PRIORITY_TRANSCRIBE,
131
+ )
132
+
133
+ def push_chunk(self, chunk: AudioChunk) -> AudioSpan | None:
134
+ return self.scheduler.submit_sync(
135
+ f"{type(self.model).__name__}.push_chunk",
136
+ lambda: self.model.push_chunk(chunk),
137
+ priority=ASR_PRIORITY_REALTIME,
138
+ )
139
+
140
+ def reset(self):
141
+ return self.model.reset()
142
+
143
+ def remove_state(self, key: str):
144
+ return self.model.remove_state(key)
145
+
146
+ def get_state(self, key: str):
147
+ return self.model.get_state(key)
@@ -0,0 +1,10 @@
1
+ from .realtime import RealtimeRouter, RealtimeWebSocketHandler, realtime_entrypoint
2
+ from .transcribe import TranscribeRouter, transcribe_entrypoint
3
+
4
+ __all__ = [
5
+ "RealtimeRouter",
6
+ "RealtimeWebSocketHandler",
7
+ "TranscribeRouter",
8
+ "realtime_entrypoint",
9
+ "transcribe_entrypoint",
10
+ ]
@@ -0,0 +1,28 @@
1
+ from .handler import RealtimeWebSocketHandler
2
+ from .protocol import (
3
+ create_error_event,
4
+ create_session_created_event,
5
+ create_session_finished_event,
6
+ create_session_updated_event,
7
+ create_speech_started_event,
8
+ create_speech_stopped_event,
9
+ create_transcription_completed_event,
10
+ create_transcription_text_event,
11
+ )
12
+ from .router import RealtimeRouter, realtime_entrypoint
13
+ from .session import ASRSession
14
+
15
+ __all__ = [
16
+ "RealtimeRouter",
17
+ "realtime_entrypoint",
18
+ "RealtimeWebSocketHandler",
19
+ "ASRSession",
20
+ "create_error_event",
21
+ "create_session_created_event",
22
+ "create_session_finished_event",
23
+ "create_session_updated_event",
24
+ "create_speech_started_event",
25
+ "create_speech_stopped_event",
26
+ "create_transcription_completed_event",
27
+ "create_transcription_text_event",
28
+ ]