swanada 1.0.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.
- swanada/__init__.py +102 -0
- swanada/audio.py +89 -0
- swanada/cli.py +203 -0
- swanada/core.py +117 -0
- swanada/decoder.py +441 -0
- swanada/denoise.py +291 -0
- swanada/en_lexicon.py +1854 -0
- swanada/engine.py +198 -0
- swanada/frontend.py +170 -0
- swanada/normalizer.py +256 -0
- swanada/streaming.py +602 -0
- swanada/vocab.py +212 -0
- swanada-1.0.0.dist-info/METADATA +128 -0
- swanada-1.0.0.dist-info/RECORD +18 -0
- swanada-1.0.0.dist-info/WHEEL +5 -0
- swanada-1.0.0.dist-info/entry_points.txt +2 -0
- swanada-1.0.0.dist-info/licenses/LICENSE +21 -0
- swanada-1.0.0.dist-info/top_level.txt +1 -0
swanada/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Swanada: In-browser & multi-runtime 16 kHz Multilingual Speech-to-Text."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.0"
|
|
4
|
+
|
|
5
|
+
from .audio import (
|
|
6
|
+
load_audio,
|
|
7
|
+
resample_audio,
|
|
8
|
+
parse_wav,
|
|
9
|
+
save_wav,
|
|
10
|
+
)
|
|
11
|
+
from .frontend import (
|
|
12
|
+
extract_log_mel_features,
|
|
13
|
+
extract_log_mel_onnx,
|
|
14
|
+
create_mel_filterbank,
|
|
15
|
+
create_padded_hann_window,
|
|
16
|
+
hz_to_mel,
|
|
17
|
+
mel_to_hz,
|
|
18
|
+
)
|
|
19
|
+
from .normalizer import (
|
|
20
|
+
dedup_marks,
|
|
21
|
+
repair_brahmic_orthography,
|
|
22
|
+
normalize_indic_text,
|
|
23
|
+
assemble_text,
|
|
24
|
+
join_subwords,
|
|
25
|
+
)
|
|
26
|
+
from .denoise import (
|
|
27
|
+
DenoiseTelemetry,
|
|
28
|
+
enhance_speech_with_telemetry,
|
|
29
|
+
denoise_pcm_16k,
|
|
30
|
+
)
|
|
31
|
+
from .decoder import (
|
|
32
|
+
decode_ctc,
|
|
33
|
+
decode_ctc_tokens,
|
|
34
|
+
is_special_token,
|
|
35
|
+
CTCDecodeResult,
|
|
36
|
+
CTCGreedyDecoder,
|
|
37
|
+
)
|
|
38
|
+
from .engine import (
|
|
39
|
+
AcousticEngine,
|
|
40
|
+
TranscriptionResult,
|
|
41
|
+
)
|
|
42
|
+
from .core import (
|
|
43
|
+
Swanada,
|
|
44
|
+
)
|
|
45
|
+
from .vocab import (
|
|
46
|
+
REGISTRY,
|
|
47
|
+
Vocabulary,
|
|
48
|
+
LanguageMeta,
|
|
49
|
+
detect_script,
|
|
50
|
+
devanagari_to_iast,
|
|
51
|
+
transliterate_iast,
|
|
52
|
+
SUPPORTED_LANGUAGES,
|
|
53
|
+
BLANK_TOKEN_ID,
|
|
54
|
+
UNK_TOKEN_ID,
|
|
55
|
+
)
|
|
56
|
+
from .streaming import (
|
|
57
|
+
StreamingCTCDecoder,
|
|
58
|
+
StreamingTranscript,
|
|
59
|
+
SwanadaStream,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"__version__",
|
|
64
|
+
"Swanada",
|
|
65
|
+
"SwanadaStream",
|
|
66
|
+
"StreamingCTCDecoder",
|
|
67
|
+
"StreamingTranscript",
|
|
68
|
+
"TranscriptionResult",
|
|
69
|
+
"AcousticEngine",
|
|
70
|
+
"REGISTRY",
|
|
71
|
+
"Vocabulary",
|
|
72
|
+
"LanguageMeta",
|
|
73
|
+
"detect_script",
|
|
74
|
+
"devanagari_to_iast",
|
|
75
|
+
"transliterate_iast",
|
|
76
|
+
"SUPPORTED_LANGUAGES",
|
|
77
|
+
"BLANK_TOKEN_ID",
|
|
78
|
+
"UNK_TOKEN_ID",
|
|
79
|
+
"load_audio",
|
|
80
|
+
"resample_audio",
|
|
81
|
+
"parse_wav",
|
|
82
|
+
"save_wav",
|
|
83
|
+
"extract_log_mel_features",
|
|
84
|
+
"extract_log_mel_onnx",
|
|
85
|
+
"create_mel_filterbank",
|
|
86
|
+
"create_padded_hann_window",
|
|
87
|
+
"hz_to_mel",
|
|
88
|
+
"mel_to_hz",
|
|
89
|
+
"dedup_marks",
|
|
90
|
+
"repair_brahmic_orthography",
|
|
91
|
+
"normalize_indic_text",
|
|
92
|
+
"assemble_text",
|
|
93
|
+
"join_subwords",
|
|
94
|
+
"DenoiseTelemetry",
|
|
95
|
+
"enhance_speech_with_telemetry",
|
|
96
|
+
"denoise_pcm_16k",
|
|
97
|
+
"decode_ctc",
|
|
98
|
+
"decode_ctc_tokens",
|
|
99
|
+
"is_special_token",
|
|
100
|
+
"CTCDecodeResult",
|
|
101
|
+
"CTCGreedyDecoder",
|
|
102
|
+
]
|
swanada/audio.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Audio loading, resampling, and WAV serialization for Swanada."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Tuple, Union
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resample_audio(
|
|
11
|
+
wav: np.ndarray,
|
|
12
|
+
src_sr: int,
|
|
13
|
+
target_sr: int = 16000,
|
|
14
|
+
) -> np.ndarray:
|
|
15
|
+
"""Resample audio signal to target sample rate.
|
|
16
|
+
|
|
17
|
+
Uses polyphase FIR resampling (scipy.signal.resample_poly) with GCD reduction,
|
|
18
|
+
falling back to linear interpolation if scipy is unavailable.
|
|
19
|
+
"""
|
|
20
|
+
if src_sr == target_sr:
|
|
21
|
+
return wav.astype(np.float32)
|
|
22
|
+
|
|
23
|
+
if len(wav) == 0:
|
|
24
|
+
return np.zeros(0, dtype=np.float32)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import scipy.signal
|
|
28
|
+
g = math.gcd(target_sr, src_sr)
|
|
29
|
+
up = target_sr // g
|
|
30
|
+
down = src_sr // g
|
|
31
|
+
resampled = scipy.signal.resample_poly(wav, up, down)
|
|
32
|
+
return resampled.astype(np.float32)
|
|
33
|
+
except Exception:
|
|
34
|
+
num_samples = max(1, int(round(len(wav) * target_sr / src_sr)))
|
|
35
|
+
t_in = np.arange(len(wav))
|
|
36
|
+
t_out = np.linspace(0, len(wav) - 1, num_samples)
|
|
37
|
+
return np.interp(t_out, t_in, wav).astype(np.float32)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_audio(
|
|
41
|
+
source: Union[str, Path, bytes, np.ndarray],
|
|
42
|
+
target_sr: int = 16000,
|
|
43
|
+
) -> np.ndarray:
|
|
44
|
+
"""Load audio from file path, bytes, or ndarray, converting to 16 kHz mono Float32."""
|
|
45
|
+
sr = target_sr
|
|
46
|
+
|
|
47
|
+
if isinstance(source, (str, Path)):
|
|
48
|
+
import soundfile as sf
|
|
49
|
+
wav, sr = sf.read(str(source), dtype="float32")
|
|
50
|
+
elif isinstance(source, (bytes, bytearray)):
|
|
51
|
+
import soundfile as sf
|
|
52
|
+
wav, sr = sf.read(io.BytesIO(source), dtype="float32")
|
|
53
|
+
elif isinstance(source, np.ndarray):
|
|
54
|
+
wav = source.astype(np.float32)
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError(f"Unsupported audio input type: {type(source)}")
|
|
57
|
+
|
|
58
|
+
# Downmix multi-channel to mono
|
|
59
|
+
if wav.ndim > 1:
|
|
60
|
+
wav = np.mean(wav, axis=1 if wav.shape[1] < wav.shape[0] else 0)
|
|
61
|
+
|
|
62
|
+
# Scale integer PCM if unnormalized
|
|
63
|
+
if np.issubdtype(wav.dtype, np.integer):
|
|
64
|
+
wav = wav.astype(np.float32) / 32768.0
|
|
65
|
+
|
|
66
|
+
# Resample if sample rate differs
|
|
67
|
+
if sr != target_sr:
|
|
68
|
+
wav = resample_audio(wav, sr, target_sr)
|
|
69
|
+
|
|
70
|
+
return wav.astype(np.float32)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_wav(data: bytes) -> Tuple[np.ndarray, int]:
|
|
74
|
+
"""Parse raw WAV byte stream into mono Float32 array and sample rate."""
|
|
75
|
+
import soundfile as sf
|
|
76
|
+
wav, sr = sf.read(io.BytesIO(data), dtype="float32")
|
|
77
|
+
if wav.ndim > 1:
|
|
78
|
+
wav = np.mean(wav, axis=1)
|
|
79
|
+
return wav.astype(np.float32), sr
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def save_wav(
|
|
83
|
+
path: Union[str, Path],
|
|
84
|
+
pcm: np.ndarray,
|
|
85
|
+
sample_rate: int = 16000,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Save Float32 audio as 16-bit PCM WAV."""
|
|
88
|
+
import soundfile as sf
|
|
89
|
+
sf.write(str(path), pcm.astype(np.float32), sample_rate, subtype="PCM_16")
|
swanada/cli.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Command-line interface for Swanada (Multilingual Speech-to-Text)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from .audio import load_audio
|
|
11
|
+
from .core import Swanada
|
|
12
|
+
from .engine import TranscriptionResult
|
|
13
|
+
from .vocab import SUPPORTED_LANGUAGES
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_cli_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="swanada",
|
|
19
|
+
description="Swanada (स्वनद) — 16 kHz Multilingual Indic Speech-to-Text CLI",
|
|
20
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
21
|
+
epilog=(
|
|
22
|
+
"Examples:\n"
|
|
23
|
+
" swanada audio.wav -l sa\n"
|
|
24
|
+
" swanada speech.wav -l hi --json -o output.json\n"
|
|
25
|
+
" cat stream.wav | swanada -l kn --stream\n"
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"input",
|
|
30
|
+
nargs="?",
|
|
31
|
+
default=None,
|
|
32
|
+
help="Input WAV audio file path or '-' for stdin pipe (default: stdin if piped)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-l",
|
|
36
|
+
"--language",
|
|
37
|
+
"--lang",
|
|
38
|
+
dest="language",
|
|
39
|
+
default="sa",
|
|
40
|
+
help="Target language code: sa | en | hi | kn | mr | bn | ta | te | ml | gu | pa | or (default: sa)",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-m",
|
|
44
|
+
"--model-dir",
|
|
45
|
+
"--model",
|
|
46
|
+
dest="model_dir",
|
|
47
|
+
default=None,
|
|
48
|
+
help="Path to custom model directory containing ONNX weights and vocabularies",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"-o",
|
|
52
|
+
"--output",
|
|
53
|
+
"--out",
|
|
54
|
+
dest="output",
|
|
55
|
+
type=Path,
|
|
56
|
+
default=None,
|
|
57
|
+
help="Output file path (default: stdout)",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--json",
|
|
61
|
+
action="store_true",
|
|
62
|
+
dest="json_output",
|
|
63
|
+
help="Emit structured JSON with text, durationMs, inferenceMs, rtf, and tokens",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--stream",
|
|
67
|
+
action="store_true",
|
|
68
|
+
dest="stream_mode",
|
|
69
|
+
help="Simulate real-time chunked streaming transcription (320ms / 5120 sample quanta)",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--threads",
|
|
73
|
+
type=int,
|
|
74
|
+
default=4,
|
|
75
|
+
dest="threads",
|
|
76
|
+
help="Number of inference execution threads (default: 4)",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--iast",
|
|
80
|
+
action="store_true",
|
|
81
|
+
dest="iast_mode",
|
|
82
|
+
help="Transliterate Classical Sanskrit output to Roman IAST with Vedic accents",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"-v",
|
|
86
|
+
"--version",
|
|
87
|
+
action="version",
|
|
88
|
+
version="swanada 1.0.0",
|
|
89
|
+
help="Display version information and exit",
|
|
90
|
+
)
|
|
91
|
+
return parser.parse_args(argv)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def result_to_dict(result: TranscriptionResult, mode: str = "batch", text_override: Optional[str] = None) -> Dict[str, Any]:
|
|
95
|
+
transcript_text = text_override if text_override is not None else result.text
|
|
96
|
+
return {
|
|
97
|
+
"text": transcript_text,
|
|
98
|
+
"language": result.language,
|
|
99
|
+
"durationMs": float(result.duration_ms),
|
|
100
|
+
"duration_ms": float(result.duration_ms),
|
|
101
|
+
"inferenceMs": float(result.inference_ms),
|
|
102
|
+
"inference_ms": float(result.inference_ms),
|
|
103
|
+
"rtf": round(float(result.rtf), 4),
|
|
104
|
+
"tokens": list(result.tokens),
|
|
105
|
+
"mode": mode,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
110
|
+
args = parse_cli_args(argv)
|
|
111
|
+
|
|
112
|
+
lang = args.language.lower().strip()
|
|
113
|
+
if lang not in SUPPORTED_LANGUAGES:
|
|
114
|
+
sys.stderr.write(
|
|
115
|
+
f"Error: Unsupported language '{lang}'. Supported languages: {', '.join(sorted(SUPPORTED_LANGUAGES))}\n"
|
|
116
|
+
)
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
# Resolve audio source
|
|
120
|
+
input_arg = args.input
|
|
121
|
+
raw_audio_bytes: Optional[bytes] = None
|
|
122
|
+
input_file_path: Optional[Path] = None
|
|
123
|
+
|
|
124
|
+
if input_arg == "-" or (input_arg is None and not sys.stdin.isatty()):
|
|
125
|
+
try:
|
|
126
|
+
raw_audio_bytes = sys.stdin.buffer.read()
|
|
127
|
+
if not raw_audio_bytes:
|
|
128
|
+
sys.stderr.write("Error: Empty standard input received.\n")
|
|
129
|
+
return 1
|
|
130
|
+
except Exception as e:
|
|
131
|
+
sys.stderr.write(f"Error reading from stdin: {e}\n")
|
|
132
|
+
return 1
|
|
133
|
+
elif input_arg:
|
|
134
|
+
input_file_path = Path(input_arg)
|
|
135
|
+
if not input_file_path.exists():
|
|
136
|
+
sys.stderr.write(f"Error: Input file not found: {input_file_path}\n")
|
|
137
|
+
return 1
|
|
138
|
+
else:
|
|
139
|
+
sys.stderr.write("Error: Missing input audio. Provide <input.wav> or pipe via stdin.\n")
|
|
140
|
+
sys.stderr.write("Usage: swanada -l <lang> [-m <model_dir>] [-o <output.txt>] [--json] [--stream] <input.wav>\n")
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
# Initialize Engine
|
|
144
|
+
try:
|
|
145
|
+
model_dir = Path(args.model_dir) if args.model_dir else None
|
|
146
|
+
stt = Swanada.create(language=lang, model_dir=model_dir, num_threads=args.threads)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
sys.stderr.write(f"Error initializing Swanada engine: {e}\n")
|
|
149
|
+
return 2
|
|
150
|
+
|
|
151
|
+
# Process Audio
|
|
152
|
+
try:
|
|
153
|
+
if raw_audio_bytes is not None:
|
|
154
|
+
pcm = load_audio(raw_audio_bytes, target_sr=16000)
|
|
155
|
+
else:
|
|
156
|
+
assert input_file_path is not None
|
|
157
|
+
pcm = load_audio(input_file_path, target_sr=16000)
|
|
158
|
+
|
|
159
|
+
if args.stream_mode:
|
|
160
|
+
stream_session = stt.stream(chunk_size_samples=5120)
|
|
161
|
+
chunk_size = 5120
|
|
162
|
+
total_samples = len(pcm)
|
|
163
|
+
for offset in range(0, total_samples, chunk_size):
|
|
164
|
+
chunk = pcm[offset : offset + chunk_size]
|
|
165
|
+
stream_session.push(chunk)
|
|
166
|
+
result = stream_session.finish()
|
|
167
|
+
mode = "streaming"
|
|
168
|
+
else:
|
|
169
|
+
result = stt.transcribe(pcm)
|
|
170
|
+
mode = "batch"
|
|
171
|
+
|
|
172
|
+
final_text = result.text
|
|
173
|
+
if args.iast_mode and lang == "sa" and final_text:
|
|
174
|
+
final_text = Swanada.transliterate_iast(final_text)
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
sys.stderr.write(f"Error during transcription: {e}\n")
|
|
178
|
+
return 2
|
|
179
|
+
|
|
180
|
+
# Format Output
|
|
181
|
+
if args.json_output:
|
|
182
|
+
res_dict = result_to_dict(result, mode=mode, text_override=final_text)
|
|
183
|
+
output_payload = json.dumps(res_dict, ensure_ascii=False, indent=2) + "\n"
|
|
184
|
+
else:
|
|
185
|
+
output_payload = final_text + "\n"
|
|
186
|
+
|
|
187
|
+
# Route Output
|
|
188
|
+
if args.output:
|
|
189
|
+
try:
|
|
190
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
args.output.write_text(output_payload, encoding="utf-8")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
sys.stderr.write(f"Error writing to output file {args.output}: {e}\n")
|
|
194
|
+
return 1
|
|
195
|
+
else:
|
|
196
|
+
sys.stdout.write(output_payload)
|
|
197
|
+
sys.stdout.flush()
|
|
198
|
+
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
sys.exit(main())
|
swanada/core.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""High-level Swanada Speech-to-Text Interface."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .audio import load_audio
|
|
8
|
+
from .engine import AcousticEngine, TranscriptionResult
|
|
9
|
+
from .vocab import REGISTRY, transliterate_iast
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_model_asset(filename: str, model_dir: Optional[Union[str, Path]] = None, subfolder: str = "") -> Optional[Path]:
|
|
13
|
+
candidates = []
|
|
14
|
+
if model_dir:
|
|
15
|
+
md = Path(model_dir)
|
|
16
|
+
if subfolder:
|
|
17
|
+
candidates.append(md / subfolder / filename)
|
|
18
|
+
candidates.append(md / filename)
|
|
19
|
+
# In case model_dir is e.g. models/onnx, check parent
|
|
20
|
+
if subfolder:
|
|
21
|
+
candidates.append(md.parent / subfolder / filename)
|
|
22
|
+
candidates.append(md.parent / filename)
|
|
23
|
+
|
|
24
|
+
# Standard repository locations
|
|
25
|
+
repo_models = Path(__file__).resolve().parents[3] / "models"
|
|
26
|
+
if subfolder:
|
|
27
|
+
candidates.append(repo_models / subfolder / filename)
|
|
28
|
+
candidates.append(repo_models / filename)
|
|
29
|
+
|
|
30
|
+
for c in candidates:
|
|
31
|
+
if c.exists():
|
|
32
|
+
return c
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Swanada:
|
|
37
|
+
"""Swanada Speech-to-Text Transcriber.
|
|
38
|
+
|
|
39
|
+
Zero server dependencies. Client-side inference via ONNX.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
language: str = "sa",
|
|
45
|
+
model_dir: Optional[Union[str, Path]] = None,
|
|
46
|
+
num_threads: int = 4,
|
|
47
|
+
preprocessor_mode: str = "analytical",
|
|
48
|
+
):
|
|
49
|
+
self.language = language
|
|
50
|
+
|
|
51
|
+
meta = REGISTRY.get(language)
|
|
52
|
+
model_name = meta.model_filename if meta else f"swanada_{language}_ctc_int8.onnx"
|
|
53
|
+
vocab_name = meta.vocab_filename if meta else f"{language}_vocab.json"
|
|
54
|
+
|
|
55
|
+
model_path = _find_model_asset(model_name, model_dir, "onnx")
|
|
56
|
+
if model_path is None and language == "sa":
|
|
57
|
+
model_path = _find_model_asset("sushrota_sanskrit_ctc_int8.onnx", model_dir, "onnx")
|
|
58
|
+
|
|
59
|
+
vocab_path = _find_model_asset(vocab_name, model_dir, "vocabularies")
|
|
60
|
+
if vocab_path is None and language == "sa":
|
|
61
|
+
vocab_path = _find_model_asset("sanskrit_vocab.json", model_dir, "vocabularies")
|
|
62
|
+
|
|
63
|
+
preprocessor_path = _find_model_asset("preprocessor.onnx", model_dir)
|
|
64
|
+
|
|
65
|
+
self.engine = AcousticEngine(
|
|
66
|
+
model_path=model_path,
|
|
67
|
+
vocab_path=vocab_path,
|
|
68
|
+
preprocessor_path=preprocessor_path,
|
|
69
|
+
language=language,
|
|
70
|
+
num_threads=num_threads,
|
|
71
|
+
preprocessor_mode=preprocessor_mode,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def transliterate_iast(text: str) -> str:
|
|
76
|
+
"""Transliterate Classical Sanskrit Devanagari text to Roman IAST with Vedic pitch accents."""
|
|
77
|
+
return transliterate_iast(text)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def create(
|
|
81
|
+
cls,
|
|
82
|
+
language: str = "sa",
|
|
83
|
+
model_dir: Optional[Union[str, Path]] = None,
|
|
84
|
+
num_threads: int = 4,
|
|
85
|
+
preprocessor_mode: str = "analytical",
|
|
86
|
+
) -> "Swanada":
|
|
87
|
+
"""Factory constructor matching TypeScript API."""
|
|
88
|
+
return cls(
|
|
89
|
+
language=language,
|
|
90
|
+
model_dir=model_dir,
|
|
91
|
+
num_threads=num_threads,
|
|
92
|
+
preprocessor_mode=preprocessor_mode,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def transcribe(
|
|
96
|
+
self,
|
|
97
|
+
audio: Union[str, Path, bytes, np.ndarray],
|
|
98
|
+
sample_rate: int = 16000,
|
|
99
|
+
) -> TranscriptionResult:
|
|
100
|
+
"""Transcribe audio input to normalized text."""
|
|
101
|
+
return self.engine.transcribe(audio, sample_rate=sample_rate)
|
|
102
|
+
|
|
103
|
+
def stream(
|
|
104
|
+
self,
|
|
105
|
+
chunk_size_samples: int = 5120,
|
|
106
|
+
left_context_samples: int = 30720,
|
|
107
|
+
max_buffer_samples: int = 61440,
|
|
108
|
+
) -> "SwanadaStream":
|
|
109
|
+
"""Creates a streaming audio transcription pipeline."""
|
|
110
|
+
from .streaming import SwanadaStream
|
|
111
|
+
return SwanadaStream(
|
|
112
|
+
self.engine,
|
|
113
|
+
chunk_size_samples=chunk_size_samples,
|
|
114
|
+
left_context_samples=left_context_samples,
|
|
115
|
+
max_buffer_samples=max_buffer_samples,
|
|
116
|
+
)
|
|
117
|
+
|