wfloat 2.0.0__py3-none-win_amd64.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.
wfloat/_stt_assets.py ADDED
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Mapping, Optional
7
+ from urllib.parse import urlparse
8
+
9
+ from ._assets import SttModelAssets
10
+ from ._download import download_file, verify_checksum
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CachedSttAssets:
15
+ model_name: str
16
+ family: str
17
+ cache_dir: Path
18
+ files: Mapping[str, Path]
19
+
20
+ def require(self, key: str) -> Path:
21
+ path = self.files.get(key)
22
+ if path is None:
23
+ raise ValueError(f"Missing required STT asset: {key}")
24
+ return path
25
+
26
+
27
+ def _normalize_model_dir_name(model_name: str) -> str:
28
+ return model_name.replace("/", "--").replace(" ", "-")
29
+
30
+
31
+ def _is_url(value: str) -> bool:
32
+ parsed = urlparse(value)
33
+ return parsed.scheme in {"http", "https", "file"}
34
+
35
+
36
+ def _copy_local_file(source: Path, destination: Path) -> None:
37
+ destination.parent.mkdir(parents=True, exist_ok=True)
38
+ shutil.copyfile(str(source), str(destination))
39
+
40
+
41
+ def _materialize_asset(
42
+ source: str | Path,
43
+ destination: Path,
44
+ *,
45
+ expected_checksum: Optional[str],
46
+ force_download: bool,
47
+ ) -> Path:
48
+ destination.parent.mkdir(parents=True, exist_ok=True)
49
+
50
+ if not force_download and destination.is_file():
51
+ if expected_checksum is None or verify_checksum(destination, expected_checksum):
52
+ return destination
53
+
54
+ if isinstance(source, Path):
55
+ _copy_local_file(source, destination)
56
+ else:
57
+ source_str = str(source)
58
+ if _is_url(source_str):
59
+ download_file(source_str, destination, expected_checksum=expected_checksum)
60
+ else:
61
+ _copy_local_file(Path(source_str), destination)
62
+
63
+ if expected_checksum is not None and not verify_checksum(destination, expected_checksum):
64
+ raise RuntimeError(f"Cached STT asset checksum mismatch for {destination}.")
65
+
66
+ return destination
67
+
68
+
69
+ def cache_stt_assets(
70
+ model_name: str,
71
+ *,
72
+ family: str,
73
+ sources: Mapping[str, str | Path | None],
74
+ checksums: Optional[Mapping[str, str]] = None,
75
+ cache_dir: Path,
76
+ force_download: bool = False,
77
+ ) -> CachedSttAssets:
78
+ model_dir = cache_dir / "models" / _normalize_model_dir_name(model_name)
79
+ model_dir.mkdir(parents=True, exist_ok=True)
80
+ checksums = checksums or {}
81
+
82
+ files: dict[str, Path] = {}
83
+ for key, source in sources.items():
84
+ if source is None:
85
+ continue
86
+
87
+ source_value = Path(source) if isinstance(source, Path) else str(source)
88
+ filename = Path(urlparse(str(source_value)).path).name or Path(str(source_value)).name
89
+ if not filename:
90
+ raise ValueError(f"Could not derive filename for STT asset {key}.")
91
+
92
+ destination = model_dir / filename
93
+ files[key] = _materialize_asset(
94
+ source_value,
95
+ destination,
96
+ expected_checksum=checksums.get(key),
97
+ force_download=force_download,
98
+ )
99
+
100
+ return CachedSttAssets(
101
+ model_name=model_name,
102
+ family=family,
103
+ cache_dir=cache_dir,
104
+ files=files,
105
+ )
106
+
107
+
108
+ def cache_stt_model_assets(
109
+ model_name: str,
110
+ assets: SttModelAssets,
111
+ *,
112
+ cache_dir: Path,
113
+ force_download: bool = False,
114
+ ) -> CachedSttAssets:
115
+ return cache_stt_assets(
116
+ model_name,
117
+ family=assets.family,
118
+ sources={
119
+ "model": assets.model,
120
+ "tokens": assets.tokens,
121
+ "preprocessor": assets.preprocessor,
122
+ "encoder": assets.encoder,
123
+ "decoder": assets.decoder,
124
+ "joiner": assets.joiner,
125
+ "uncached_decoder": assets.uncached_decoder,
126
+ "cached_decoder": assets.cached_decoder,
127
+ },
128
+ checksums={
129
+ key: value
130
+ for key, value in {
131
+ "model": assets.model_checksum,
132
+ "tokens": assets.tokens_checksum,
133
+ "preprocessor": assets.preprocessor_checksum,
134
+ "encoder": assets.encoder_checksum,
135
+ "decoder": assets.decoder_checksum,
136
+ "joiner": assets.joiner_checksum,
137
+ "uncached_decoder": assets.uncached_decoder_checksum,
138
+ "cached_decoder": assets.cached_decoder_checksum,
139
+ }.items()
140
+ if value is not None
141
+ },
142
+ cache_dir=cache_dir,
143
+ force_download=force_download,
144
+ )
wfloat/_stt_load.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from ._assets import fetch_stt_assets
7
+ from ._cache import get_default_cache_dir
8
+ from ._core import create_core_stt
9
+ from ._stt import SttModel
10
+ from ._stt_assets import cache_stt_model_assets
11
+
12
+
13
+ def load_stt_model(
14
+ model_name: str,
15
+ *,
16
+ cache_dir: Optional[Path] = None,
17
+ force_download: bool = False,
18
+ language: Optional[str] = None,
19
+ task: Optional[str] = None,
20
+ enable_token_timestamps: bool = False,
21
+ enable_segment_timestamps: bool = False,
22
+ ) -> SttModel:
23
+ resolved_cache_dir = Path(cache_dir) if cache_dir is not None else get_default_cache_dir()
24
+ assets = fetch_stt_assets(model_name)
25
+ cached = cache_stt_model_assets(
26
+ model_name,
27
+ assets,
28
+ cache_dir=resolved_cache_dir,
29
+ force_download=force_download,
30
+ )
31
+ family = assets.family
32
+
33
+ native_stt = create_core_stt(
34
+ model_name=model_name,
35
+ family=family,
36
+ model_path=cached.files.get("model"),
37
+ tokens_path=cached.require("tokens"),
38
+ preprocessor_path=cached.files.get("preprocessor"),
39
+ encoder_path=cached.files.get("encoder"),
40
+ decoder_path=cached.files.get("decoder"),
41
+ joiner_path=cached.files.get("joiner"),
42
+ uncached_decoder_path=cached.files.get("uncached_decoder"),
43
+ cached_decoder_path=cached.files.get("cached_decoder"),
44
+ language=language,
45
+ task=task,
46
+ enable_token_timestamps=enable_token_timestamps,
47
+ enable_segment_timestamps=enable_segment_timestamps,
48
+ )
49
+ return SttModel(model_id=model_name, _native_stt=native_stt)
50
+
51
+
52
+ def load_whisper_tiny_en(
53
+ *,
54
+ cache_dir: Optional[Path] = None,
55
+ force_download: bool = False,
56
+ ) -> SttModel:
57
+ return load_stt_model(
58
+ "openai/whisper-tiny-en",
59
+ cache_dir=cache_dir,
60
+ force_download=force_download,
61
+ language="en",
62
+ task="transcribe",
63
+ )
64
+
65
+
66
+ def load_moonshine_tiny_en(
67
+ *,
68
+ cache_dir: Optional[Path] = None,
69
+ force_download: bool = False,
70
+ ) -> SttModel:
71
+ return load_stt_model(
72
+ "UsefulSensors/moonshine-tiny",
73
+ cache_dir=cache_dir,
74
+ force_download=force_download,
75
+ )
wfloat/_vad.py ADDED
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import wave
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Optional, Sequence
8
+
9
+ from ._results import Audio, VadDetectionResult, VadSegment
10
+
11
+ DEFAULT_VAD_SAMPLE_RATE = 16000
12
+
13
+
14
+ def _coerce_audio_samples(audio: bytes | Sequence[float]) -> list[float]:
15
+ if isinstance(audio, (bytes, bytearray, memoryview)):
16
+ raise TypeError(
17
+ "Raw PCM bytes are not supported yet. Pass a float sequence or a WAV path."
18
+ )
19
+
20
+ samples = [float(sample) for sample in audio]
21
+ if any(not math.isfinite(sample) for sample in samples):
22
+ raise ValueError("audio samples must be finite numbers.")
23
+ return samples
24
+
25
+
26
+ def _load_wav_audio(path: Path) -> tuple[list[float], int]:
27
+ with wave.open(str(path), "rb") as wav_file:
28
+ channels = wav_file.getnchannels()
29
+ sample_width = wav_file.getsampwidth()
30
+ sample_rate = wav_file.getframerate()
31
+ frame_count = wav_file.getnframes()
32
+
33
+ if channels != 1:
34
+ raise ValueError("Only mono WAV files are supported.")
35
+ if sample_width != 2:
36
+ raise ValueError("Only 16-bit PCM WAV files are supported.")
37
+
38
+ frames = wav_file.readframes(frame_count)
39
+
40
+ samples: list[float] = []
41
+ for index in range(0, len(frames), 2):
42
+ sample = int.from_bytes(frames[index : index + 2], "little", signed=True)
43
+ samples.append(float(sample) / 32768.0)
44
+
45
+ return samples, sample_rate
46
+
47
+
48
+ @dataclass
49
+ class VadModel:
50
+ model_id: str
51
+ family: str
52
+ _native_vad: object
53
+ sample_rate: int = DEFAULT_VAD_SAMPLE_RATE
54
+
55
+ @property
56
+ def window_size(self) -> int:
57
+ return 256 if self.family.lower().replace("_", "-") in {"ten-vad", "tenvad"} else 512
58
+
59
+ def detect(
60
+ self,
61
+ *,
62
+ audio: bytes | Sequence[float] | str | Path,
63
+ sample_rate: Optional[int] = None,
64
+ ) -> VadDetectionResult:
65
+ if isinstance(audio, (str, Path)):
66
+ samples, resolved_sample_rate = _load_wav_audio(Path(audio))
67
+ else:
68
+ if sample_rate is None or sample_rate <= 0:
69
+ raise ValueError("sample_rate is required for in-memory audio.")
70
+ samples = _coerce_audio_samples(audio)
71
+ resolved_sample_rate = int(sample_rate)
72
+
73
+ if resolved_sample_rate != self.sample_rate:
74
+ raise ValueError(
75
+ f"VAD expects {self.sample_rate} Hz mono audio; got {resolved_sample_rate} Hz."
76
+ )
77
+
78
+ if not hasattr(self._native_vad, "accept_waveform"):
79
+ raise RuntimeError("Native VAD backend does not support accept_waveform().")
80
+
81
+ self._native_vad.reset()
82
+ window_size = self.window_size
83
+ for offset in range(0, len(samples), window_size):
84
+ self._native_vad.accept_waveform(samples[offset : offset + window_size])
85
+ self._native_vad.flush()
86
+
87
+ segments: list[VadSegment] = []
88
+ speech_sample_count = 0
89
+ while not self._native_vad.empty():
90
+ native_segment = self._native_vad.front
91
+ segment_samples = [float(sample) for sample in native_segment.samples]
92
+ start_sample = int(native_segment.start)
93
+ sample_count = len(segment_samples)
94
+ speech_sample_count += sample_count
95
+ duration_sec = sample_count / float(self.sample_rate)
96
+ start_sec = start_sample / float(self.sample_rate)
97
+ segments.append(
98
+ VadSegment(
99
+ start_sec=start_sec,
100
+ duration_sec=duration_sec,
101
+ end_sec=start_sec + duration_sec,
102
+ start_sample=start_sample,
103
+ sample_count=sample_count,
104
+ sample_rate=self.sample_rate,
105
+ audio=Audio(samples=segment_samples, sample_rate=self.sample_rate),
106
+ )
107
+ )
108
+ self._native_vad.pop()
109
+
110
+ speech_ratio = (
111
+ min(max(speech_sample_count / float(len(samples)), 0.0), 1.0)
112
+ if samples
113
+ else 0.0
114
+ )
115
+ return VadDetectionResult(
116
+ model_id=self.model_id,
117
+ segments=segments,
118
+ speech_ratio=speech_ratio,
119
+ )
wfloat/_vad_assets.py ADDED
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Mapping, Optional
7
+ from urllib.parse import urlparse
8
+
9
+ from ._assets import VadModelAssets
10
+ from ._download import download_file, verify_checksum
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CachedVadAssets:
15
+ model_name: str
16
+ family: str
17
+ cache_dir: Path
18
+ files: Mapping[str, Path]
19
+
20
+ def require(self, key: str) -> Path:
21
+ path = self.files.get(key)
22
+ if path is None:
23
+ raise ValueError(f"Missing required VAD asset: {key}")
24
+ return path
25
+
26
+
27
+ def _normalize_model_dir_name(model_name: str) -> str:
28
+ return model_name.replace("/", "--").replace(" ", "-")
29
+
30
+
31
+ def _is_url(value: str) -> bool:
32
+ parsed = urlparse(value)
33
+ return parsed.scheme in {"http", "https", "file"}
34
+
35
+
36
+ def _copy_local_file(source: Path, destination: Path) -> None:
37
+ destination.parent.mkdir(parents=True, exist_ok=True)
38
+ shutil.copyfile(str(source), str(destination))
39
+
40
+
41
+ def _materialize_asset(
42
+ source: str | Path,
43
+ destination: Path,
44
+ *,
45
+ expected_checksum: Optional[str],
46
+ force_download: bool,
47
+ ) -> Path:
48
+ destination.parent.mkdir(parents=True, exist_ok=True)
49
+
50
+ if not force_download and destination.is_file():
51
+ if expected_checksum is None or verify_checksum(destination, expected_checksum):
52
+ return destination
53
+
54
+ if isinstance(source, Path):
55
+ _copy_local_file(source, destination)
56
+ else:
57
+ source_str = str(source)
58
+ if _is_url(source_str):
59
+ download_file(source_str, destination, expected_checksum=expected_checksum)
60
+ else:
61
+ _copy_local_file(Path(source_str), destination)
62
+
63
+ if expected_checksum is not None and not verify_checksum(destination, expected_checksum):
64
+ raise RuntimeError(f"Cached VAD asset checksum mismatch for {destination}.")
65
+
66
+ return destination
67
+
68
+
69
+ def cache_vad_assets(
70
+ model_name: str,
71
+ *,
72
+ family: str,
73
+ sources: Mapping[str, str | Path | None],
74
+ checksums: Optional[Mapping[str, str]] = None,
75
+ cache_dir: Path,
76
+ force_download: bool = False,
77
+ ) -> CachedVadAssets:
78
+ model_dir = cache_dir / "models" / _normalize_model_dir_name(model_name)
79
+ model_dir.mkdir(parents=True, exist_ok=True)
80
+ checksums = checksums or {}
81
+
82
+ files: dict[str, Path] = {}
83
+ for key, source in sources.items():
84
+ if source is None:
85
+ continue
86
+
87
+ source_value = Path(source) if isinstance(source, Path) else str(source)
88
+ filename = Path(urlparse(str(source_value)).path).name or Path(str(source_value)).name
89
+ if not filename:
90
+ raise ValueError(f"Could not derive filename for VAD asset {key}.")
91
+
92
+ destination = model_dir / filename
93
+ files[key] = _materialize_asset(
94
+ source_value,
95
+ destination,
96
+ expected_checksum=checksums.get(key),
97
+ force_download=force_download,
98
+ )
99
+
100
+ return CachedVadAssets(
101
+ model_name=model_name,
102
+ family=family,
103
+ cache_dir=cache_dir,
104
+ files=files,
105
+ )
106
+
107
+
108
+ def cache_vad_model_assets(
109
+ model_name: str,
110
+ assets: VadModelAssets,
111
+ *,
112
+ cache_dir: Path,
113
+ force_download: bool = False,
114
+ ) -> CachedVadAssets:
115
+ return cache_vad_assets(
116
+ model_name,
117
+ family=assets.family,
118
+ sources={
119
+ "model": assets.model,
120
+ },
121
+ checksums={
122
+ key: value
123
+ for key, value in {
124
+ "model": assets.model_checksum,
125
+ }.items()
126
+ if value is not None
127
+ },
128
+ cache_dir=cache_dir,
129
+ force_download=force_download,
130
+ )
wfloat/_vad_load.py ADDED
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from ._assets import fetch_vad_assets
7
+ from ._cache import get_default_cache_dir
8
+ from ._core import create_core_vad
9
+ from ._vad import DEFAULT_VAD_SAMPLE_RATE, VadModel
10
+ from ._vad_assets import cache_vad_model_assets
11
+
12
+ DEFAULT_VAD_THRESHOLD = 0.5
13
+ DEFAULT_VAD_MIN_SILENCE_DURATION_SEC = 0.5
14
+ DEFAULT_VAD_MIN_SPEECH_DURATION_SEC = 0.25
15
+ DEFAULT_VAD_MAX_SPEECH_DURATION_SEC = 20.0
16
+ DEFAULT_VAD_BUFFER_SIZE_IN_SECONDS = 30.0
17
+
18
+
19
+ def _finite_float_or_default(value: Optional[float], default: float) -> float:
20
+ if value is None:
21
+ return default
22
+ resolved = float(value)
23
+ if not resolved == resolved or resolved in {float("inf"), float("-inf")}:
24
+ raise ValueError("VAD timing and threshold options must be finite numbers.")
25
+ return resolved
26
+
27
+
28
+ def load_vad_model(
29
+ model_name: str,
30
+ *,
31
+ cache_dir: Optional[Path] = None,
32
+ force_download: bool = False,
33
+ threshold: Optional[float] = None,
34
+ min_silence_duration_sec: Optional[float] = None,
35
+ min_speech_duration_sec: Optional[float] = None,
36
+ max_speech_duration_sec: Optional[float] = None,
37
+ buffer_size_in_seconds: Optional[float] = None,
38
+ ) -> VadModel:
39
+ resolved_cache_dir = Path(cache_dir) if cache_dir is not None else get_default_cache_dir()
40
+ assets = fetch_vad_assets(model_name)
41
+ cached = cache_vad_model_assets(
42
+ model_name,
43
+ assets,
44
+ cache_dir=resolved_cache_dir,
45
+ force_download=force_download,
46
+ )
47
+ family = assets.family
48
+
49
+ native_vad = create_core_vad(
50
+ model_name=model_name,
51
+ family=family,
52
+ model_path=cached.require("model"),
53
+ threshold=_finite_float_or_default(threshold, DEFAULT_VAD_THRESHOLD),
54
+ min_silence_duration_sec=_finite_float_or_default(
55
+ min_silence_duration_sec,
56
+ DEFAULT_VAD_MIN_SILENCE_DURATION_SEC,
57
+ ),
58
+ min_speech_duration_sec=_finite_float_or_default(
59
+ min_speech_duration_sec,
60
+ DEFAULT_VAD_MIN_SPEECH_DURATION_SEC,
61
+ ),
62
+ max_speech_duration_sec=_finite_float_or_default(
63
+ max_speech_duration_sec,
64
+ DEFAULT_VAD_MAX_SPEECH_DURATION_SEC,
65
+ ),
66
+ sample_rate=DEFAULT_VAD_SAMPLE_RATE,
67
+ buffer_size_in_seconds=_finite_float_or_default(
68
+ buffer_size_in_seconds,
69
+ DEFAULT_VAD_BUFFER_SIZE_IN_SECONDS,
70
+ ),
71
+ )
72
+ return VadModel(
73
+ model_id=model_name,
74
+ family=family,
75
+ _native_vad=native_vad,
76
+ sample_rate=DEFAULT_VAD_SAMPLE_RATE,
77
+ )
78
+
79
+
80
+ def load_silero_vad(
81
+ *,
82
+ cache_dir: Optional[Path] = None,
83
+ force_download: bool = False,
84
+ threshold: Optional[float] = None,
85
+ min_silence_duration_sec: Optional[float] = None,
86
+ min_speech_duration_sec: Optional[float] = None,
87
+ max_speech_duration_sec: Optional[float] = None,
88
+ ) -> VadModel:
89
+ return load_vad_model(
90
+ "snakers4/silero-vad",
91
+ cache_dir=cache_dir,
92
+ force_download=force_download,
93
+ threshold=threshold,
94
+ min_silence_duration_sec=min_silence_duration_sec,
95
+ min_speech_duration_sec=min_speech_duration_sec,
96
+ max_speech_duration_sec=max_speech_duration_sec,
97
+ )
wfloat/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
Binary file