audio-validation 0.3.0__tar.gz → 0.3.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.
- {audio_validation-0.3.0 → audio_validation-0.3.2}/PKG-INFO +1 -1
- audio_validation-0.3.2/RELEASE_NOTES.md +19 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/_version.py +3 -3
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/criteria.py +137 -61
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/models.py +62 -6
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/validator.py +46 -24
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/PKG-INFO +1 -1
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/scm_file_list.json +23 -23
- audio_validation-0.3.2/src/audio_validation.egg-info/scm_version.json +8 -0
- audio_validation-0.3.0/RELEASE_NOTES.md +0 -11
- audio_validation-0.3.0/src/audio_validation.egg-info/scm_version.json +0 -8
- {audio_validation-0.3.0 → audio_validation-0.3.2}/.github/workflows/code_checker.yml +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/.github/workflows/publish_to_pypi.yml +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/.github/workflows/unit_tests.yml +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/.gitignore +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/.pylint_ignore +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/LICENSE +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/README.md +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/_ci/is_dev_version.sh +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/Makefile +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/conf.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/contribute.rst +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/index.rst +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/installation.rst +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/release_notes.rst +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/requirements.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/docs/tutorials/index.rst +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/pyproject.toml +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/requirements-dev.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/requirements-test.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/requirements.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/setup.cfg +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/__init__.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/audio_features.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/audio_verification.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/plots.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/recorder.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/retention.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/utils/audio_generation.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/SOURCES.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/dependency_links.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/requires.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/top_level.txt +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/tests/__init__.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/tests/test_template.py +0 -0
- {audio_validation-0.3.0 → audio_validation-0.3.2}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: audio_validation
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tools for validating audio data, including feature extraction and visualization.
|
|
5
5
|
Maintainer-email: Hubert Stepniewski <hubert.stepniewski@int2code.com>, Marcin Tomiczek <marcin.tomiczek@int2code.com>, Piotr Sznapka <piotr.sznapka@int2code.com>
|
|
6
6
|
License: MIT License
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
### 0.3.2
|
|
2
|
+
|
|
3
|
+
[PATCH] Add warmup chunks, improve failure reporting.
|
|
4
|
+
|
|
5
|
+
### 0.3.1
|
|
6
|
+
|
|
7
|
+
[PATCH] Fix printing Continous Audio Validator Failure Info.
|
|
8
|
+
|
|
9
|
+
### 0.3.0
|
|
10
|
+
|
|
11
|
+
[MINOR] Add Continous Audio Validator.
|
|
12
|
+
|
|
13
|
+
### 0.2.0
|
|
14
|
+
|
|
15
|
+
[MINOR] Add THD, THD+N calculation.
|
|
16
|
+
|
|
17
|
+
### 0.1.2
|
|
18
|
+
|
|
19
|
+
[MINOR] Initial release.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.3.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 3,
|
|
21
|
+
__version__ = version = '0.3.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'ge32c5a61b'
|
|
@@ -6,10 +6,14 @@ swapped atomically by reference while the analysis thread reads it, without
|
|
|
6
6
|
copying. :func:`evaluate_chunk` runs every configured check (skipping any left
|
|
7
7
|
at ``None``) and reports *all* failures, each check living in its own function
|
|
8
8
|
that logs a per-channel table.
|
|
9
|
+
|
|
10
|
+
Each check returns a :class:`CheckResult` that keeps its one-line *summary*
|
|
11
|
+
separate from its per-channel *failures* table, so callers can render a compact
|
|
12
|
+
overview (one row per chunk) without embedding a whole table in a single cell.
|
|
9
13
|
"""
|
|
10
14
|
|
|
11
15
|
import logging
|
|
12
|
-
from dataclasses import dataclass
|
|
16
|
+
from dataclasses import dataclass, field
|
|
13
17
|
from typing import Any, Callable, List, Optional, Tuple
|
|
14
18
|
|
|
15
19
|
import numpy as np
|
|
@@ -60,6 +64,74 @@ class AudioCriteria:
|
|
|
60
64
|
custom_check: Optional[Callable[[AudioFeatures], Tuple[bool, str]]] = None
|
|
61
65
|
|
|
62
66
|
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class CheckResult:
|
|
69
|
+
"""Outcome of a single check.
|
|
70
|
+
|
|
71
|
+
Keeping *summary* (one line) apart from *failures* (a table) lets the caller
|
|
72
|
+
choose the rendering: a compact column in a metrics table, or a full
|
|
73
|
+
multiline block in a failure report.
|
|
74
|
+
|
|
75
|
+
:ivar name: Short check identifier, e.g. ``"thd_n"``.
|
|
76
|
+
:ivar ok: Whether the check passed.
|
|
77
|
+
:ivar summary: Single-line reason; empty when the check passed.
|
|
78
|
+
:ivar failures: Failing per-channel rows, or ``None`` when not applicable.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
ok: bool
|
|
83
|
+
summary: str = ""
|
|
84
|
+
failures: Optional[pd.DataFrame] = None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_tuple(cls, name: str, result: Tuple[bool, str]) -> "CheckResult":
|
|
88
|
+
"""Adapt a legacy ``(ok, reason)`` pair (e.g. from ``custom_check``).
|
|
89
|
+
|
|
90
|
+
A multiline *reason* keeps its first line as the summary; the rest is
|
|
91
|
+
preserved as a single-column details table.
|
|
92
|
+
"""
|
|
93
|
+
ok, reason = result
|
|
94
|
+
head, _, rest = reason.partition("\n")
|
|
95
|
+
failures = pd.DataFrame({"detail": rest.splitlines()}) if rest.strip() else None
|
|
96
|
+
return cls(name=name, ok=ok, summary=head, failures=failures)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def detail(self) -> str:
|
|
100
|
+
"""Return the multiline ``summary`` + failing-rows table."""
|
|
101
|
+
if self.ok:
|
|
102
|
+
return ""
|
|
103
|
+
if self.failures is None or self.failures.empty:
|
|
104
|
+
return self.summary
|
|
105
|
+
return f"{self.summary}\n{self.failures.to_string()}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class ChunkVerdict:
|
|
110
|
+
"""Aggregated outcome of every check run against one chunk."""
|
|
111
|
+
|
|
112
|
+
results: List[CheckResult] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def failures(self) -> List[CheckResult]:
|
|
116
|
+
"""Return the failing checks, in evaluation order."""
|
|
117
|
+
return [result for result in self.results if not result.ok]
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def ok(self) -> bool:
|
|
121
|
+
"""Whether every check passed."""
|
|
122
|
+
return not self.failures
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def summary(self) -> str:
|
|
126
|
+
"""Single-line reason joining every failing check's summary."""
|
|
127
|
+
return "; ".join(result.summary for result in self.failures)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def detail(self) -> str:
|
|
131
|
+
"""Multiline reason with each failing check's per-channel table."""
|
|
132
|
+
return "\n".join(result.detail for result in self.failures)
|
|
133
|
+
|
|
134
|
+
|
|
63
135
|
def _channel_label(ch: int) -> str:
|
|
64
136
|
"""Return a stable, human-readable label for channel index *ch*."""
|
|
65
137
|
return f"ch{ch}"
|
|
@@ -87,13 +159,13 @@ def _format_list(freqs: Any, max_display: int = 5) -> str:
|
|
|
87
159
|
)
|
|
88
160
|
|
|
89
161
|
|
|
90
|
-
def check_silence(features: AudioFeatures, threshold: float) ->
|
|
162
|
+
def check_silence(features: AudioFeatures, threshold: float) -> CheckResult:
|
|
91
163
|
"""Check that every channel is silent (RMS at/below *threshold*).
|
|
92
164
|
|
|
93
165
|
:param features: Per-channel features to check.
|
|
94
166
|
:param threshold: RMS at/below which a channel counts as silent.
|
|
95
|
-
:return:
|
|
96
|
-
|
|
167
|
+
:return: A passing :class:`CheckResult`, or a failing one carrying the
|
|
168
|
+
non-silent channels.
|
|
97
169
|
"""
|
|
98
170
|
rows = [
|
|
99
171
|
{
|
|
@@ -104,18 +176,20 @@ def check_silence(features: AudioFeatures, threshold: float) -> Tuple[bool, str]
|
|
|
104
176
|
for ch, feat in enumerate(features.channel_features)
|
|
105
177
|
]
|
|
106
178
|
df = pd.DataFrame(rows).set_index("ch")
|
|
107
|
-
logger.debug("Silence check:\n%s", df.to_string())
|
|
108
179
|
|
|
109
180
|
non_silent_df = df[~df["silent"]]
|
|
110
181
|
if non_silent_df.empty:
|
|
111
|
-
return
|
|
112
|
-
return
|
|
182
|
+
return CheckResult("silence", True)
|
|
183
|
+
return CheckResult(
|
|
184
|
+
"silence",
|
|
185
|
+
False,
|
|
113
186
|
f"Expected silence (RMS <= {threshold}) but audio detected on "
|
|
114
|
-
f"{len(non_silent_df)} channel(s)
|
|
187
|
+
f"{len(non_silent_df)} channel(s)",
|
|
188
|
+
non_silent_df,
|
|
115
189
|
)
|
|
116
190
|
|
|
117
191
|
|
|
118
|
-
def check_audio_present(features: AudioFeatures, threshold: float) ->
|
|
192
|
+
def check_audio_present(features: AudioFeatures, threshold: float) -> CheckResult:
|
|
119
193
|
"""Check that every channel carries audio (RMS strictly above *threshold*).
|
|
120
194
|
|
|
121
195
|
Complement of :func:`check_silence`; mirrors the legacy
|
|
@@ -123,8 +197,8 @@ def check_audio_present(features: AudioFeatures, threshold: float) -> Tuple[bool
|
|
|
123
197
|
|
|
124
198
|
:param features: Per-channel features to check.
|
|
125
199
|
:param threshold: RMS strictly above which a channel counts as present.
|
|
126
|
-
:return:
|
|
127
|
-
|
|
200
|
+
:return: A passing :class:`CheckResult`, or a failing one carrying the
|
|
201
|
+
silent channels.
|
|
128
202
|
"""
|
|
129
203
|
rows = [
|
|
130
204
|
{
|
|
@@ -135,27 +209,28 @@ def check_audio_present(features: AudioFeatures, threshold: float) -> Tuple[bool
|
|
|
135
209
|
for ch, feat in enumerate(features.channel_features)
|
|
136
210
|
]
|
|
137
211
|
df = pd.DataFrame(rows).set_index("ch")
|
|
138
|
-
logger.debug("Audio presence check:\n%s", df.to_string())
|
|
139
212
|
|
|
140
213
|
silent_df = df[~df["audio_present"]]
|
|
141
214
|
if silent_df.empty:
|
|
142
|
-
return
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
215
|
+
return CheckResult("audio_present", True)
|
|
216
|
+
return CheckResult(
|
|
217
|
+
"audio_present",
|
|
218
|
+
False,
|
|
219
|
+
f"No audio detected (RMS <= {threshold}) on {len(silent_df)} channel(s)",
|
|
220
|
+
silent_df,
|
|
146
221
|
)
|
|
147
222
|
|
|
148
223
|
|
|
149
224
|
def check_rms(
|
|
150
225
|
features: AudioFeatures, expected_rms: List[float], tolerance: float
|
|
151
|
-
) ->
|
|
226
|
+
) -> CheckResult:
|
|
152
227
|
"""Check that each channel's RMS is within *tolerance* of its expected value.
|
|
153
228
|
|
|
154
229
|
:param features: Per-channel features to check.
|
|
155
230
|
:param expected_rms: Per-channel expected RMS values.
|
|
156
231
|
:param tolerance: Allowed absolute deviation from *expected_rms*.
|
|
157
|
-
:return:
|
|
158
|
-
|
|
232
|
+
:return: A passing :class:`CheckResult`, or a failing one carrying the
|
|
233
|
+
out-of-tolerance channels.
|
|
159
234
|
"""
|
|
160
235
|
rows = [
|
|
161
236
|
{
|
|
@@ -168,23 +243,25 @@ def check_rms(
|
|
|
168
243
|
for ch, feat in enumerate(features.channel_features)
|
|
169
244
|
]
|
|
170
245
|
df = pd.DataFrame(rows).set_index("ch")
|
|
171
|
-
logger.debug("RMS check:\n%s", df.to_string())
|
|
172
246
|
|
|
173
247
|
failed_df = df[~df["rms_ok"]]
|
|
174
248
|
if failed_df.empty:
|
|
175
|
-
return
|
|
176
|
-
return
|
|
177
|
-
|
|
249
|
+
return CheckResult("rms", True)
|
|
250
|
+
return CheckResult(
|
|
251
|
+
"rms",
|
|
252
|
+
False,
|
|
253
|
+
f"RMS out of tolerance on {len(failed_df)} channel(s)",
|
|
254
|
+
failed_df,
|
|
178
255
|
)
|
|
179
256
|
|
|
180
257
|
|
|
181
|
-
def check_frequency(features: AudioFeatures) ->
|
|
258
|
+
def check_frequency(features: AudioFeatures) -> CheckResult:
|
|
182
259
|
"""Check that the expected frequency was detected on every channel.
|
|
183
260
|
|
|
184
261
|
:param features: Per-channel features to check (``detected`` set by the
|
|
185
262
|
FFT stage of :meth:`AudioFeatures.compute`).
|
|
186
|
-
:return:
|
|
187
|
-
|
|
263
|
+
:return: A passing :class:`CheckResult`, or a failing one carrying the
|
|
264
|
+
channels that missed the expected frequency.
|
|
188
265
|
"""
|
|
189
266
|
rows = [
|
|
190
267
|
{
|
|
@@ -202,26 +279,27 @@ def check_frequency(features: AudioFeatures) -> Tuple[bool, str]:
|
|
|
202
279
|
for ch, feat in enumerate(features.channel_features)
|
|
203
280
|
]
|
|
204
281
|
df = pd.DataFrame(rows).set_index("ch")
|
|
205
|
-
logger.debug("Frequency presence check:\n%s", df.to_string())
|
|
206
282
|
|
|
207
283
|
failed_df = df[~df["detected"]]
|
|
208
284
|
if failed_df.empty:
|
|
209
|
-
return
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
return CheckResult("frequency", True)
|
|
286
|
+
return CheckResult(
|
|
287
|
+
"frequency",
|
|
288
|
+
False,
|
|
289
|
+
f"Expected frequency not detected on {len(failed_df)} channel(s)",
|
|
290
|
+
failed_df,
|
|
213
291
|
)
|
|
214
292
|
|
|
215
293
|
|
|
216
|
-
def check_thd(features: AudioFeatures, max_thd: float) ->
|
|
294
|
+
def check_thd(features: AudioFeatures, max_thd: float) -> CheckResult:
|
|
217
295
|
"""Check that each channel's THD is at/below *max_thd* percent.
|
|
218
296
|
|
|
219
297
|
Channels with THD not computed (``None``) are treated as passing.
|
|
220
298
|
|
|
221
299
|
:param features: Per-channel features to check.
|
|
222
300
|
:param max_thd: Maximum allowed THD as a percentage.
|
|
223
|
-
:return:
|
|
224
|
-
|
|
301
|
+
:return: A passing :class:`CheckResult`, or a failing one carrying the
|
|
302
|
+
channels above tolerance.
|
|
225
303
|
"""
|
|
226
304
|
rows = [
|
|
227
305
|
{
|
|
@@ -233,18 +311,19 @@ def check_thd(features: AudioFeatures, max_thd: float) -> Tuple[bool, str]:
|
|
|
233
311
|
for ch, feat in enumerate(features.channel_features)
|
|
234
312
|
]
|
|
235
313
|
df = pd.DataFrame(rows).set_index("ch")
|
|
236
|
-
logger.debug("THD check:\n%s", df.to_string())
|
|
237
314
|
|
|
238
315
|
failed_df = df[~df["thd_ok"]]
|
|
239
316
|
if failed_df.empty:
|
|
240
|
-
return
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
|
|
317
|
+
return CheckResult("thd", True)
|
|
318
|
+
return CheckResult(
|
|
319
|
+
"thd",
|
|
320
|
+
False,
|
|
321
|
+
f"THD above tolerance ({max_thd} %) on {len(failed_df)} channel(s)",
|
|
322
|
+
failed_df,
|
|
244
323
|
)
|
|
245
324
|
|
|
246
325
|
|
|
247
|
-
def check_thd_n(features: AudioFeatures, max_thd_n: float) ->
|
|
326
|
+
def check_thd_n(features: AudioFeatures, max_thd_n: float) -> CheckResult:
|
|
248
327
|
"""Check that each channel's THD+N is at/below *max_thd_n* percent.
|
|
249
328
|
|
|
250
329
|
THD+N accounts for broadband noise as well as harmonics, so it catches a
|
|
@@ -253,8 +332,8 @@ def check_thd_n(features: AudioFeatures, max_thd_n: float) -> Tuple[bool, str]:
|
|
|
253
332
|
|
|
254
333
|
:param features: Per-channel features to check.
|
|
255
334
|
:param max_thd_n: Maximum allowed THD+N as a percentage.
|
|
256
|
-
:return:
|
|
257
|
-
|
|
335
|
+
:return: A passing :class:`CheckResult`, or a failing one carrying the
|
|
336
|
+
channels above tolerance.
|
|
258
337
|
"""
|
|
259
338
|
rows = [
|
|
260
339
|
{
|
|
@@ -266,33 +345,31 @@ def check_thd_n(features: AudioFeatures, max_thd_n: float) -> Tuple[bool, str]:
|
|
|
266
345
|
for ch, feat in enumerate(features.channel_features)
|
|
267
346
|
]
|
|
268
347
|
df = pd.DataFrame(rows).set_index("ch")
|
|
269
|
-
logger.debug("THD+N check:\n%s", df.to_string())
|
|
270
348
|
|
|
271
349
|
failed_df = df[~df["thd_n_ok"]]
|
|
272
350
|
if failed_df.empty:
|
|
273
|
-
return
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
|
|
351
|
+
return CheckResult("thd_n", True)
|
|
352
|
+
return CheckResult(
|
|
353
|
+
"thd_n",
|
|
354
|
+
False,
|
|
355
|
+
f"THD+N above tolerance ({max_thd_n} %) on {len(failed_df)} channel(s)",
|
|
356
|
+
failed_df,
|
|
277
357
|
)
|
|
278
358
|
|
|
279
359
|
|
|
280
|
-
def evaluate_chunk(
|
|
281
|
-
features: AudioFeatures, criteria: AudioCriteria
|
|
282
|
-
) -> Tuple[bool, str]:
|
|
360
|
+
def evaluate_chunk(features: AudioFeatures, criteria: AudioCriteria) -> ChunkVerdict:
|
|
283
361
|
"""Evaluate one chunk's features against every configured check.
|
|
284
362
|
|
|
285
363
|
Unlike a short-circuiting check, this runs *all* checks enabled by
|
|
286
|
-
*criteria* and
|
|
287
|
-
|
|
364
|
+
*criteria* and collects every result, so a single chunk reports all its
|
|
365
|
+
problems at once.
|
|
288
366
|
|
|
289
367
|
:param features: Per-channel features from :meth:`AudioFeatures.compute`.
|
|
290
368
|
:param criteria: Active criteria snapshot.
|
|
291
|
-
:return:
|
|
292
|
-
``
|
|
293
|
-
check (one per line).
|
|
369
|
+
:return: A :class:`ChunkVerdict` holding each executed check's result;
|
|
370
|
+
``verdict.ok`` is ``True`` when every configured check passed.
|
|
294
371
|
"""
|
|
295
|
-
results: List[
|
|
372
|
+
results: List[CheckResult] = []
|
|
296
373
|
|
|
297
374
|
if criteria.require_audio_present:
|
|
298
375
|
results.append(check_audio_present(features, criteria.silence_rms_threshold))
|
|
@@ -309,9 +386,8 @@ def evaluate_chunk(
|
|
|
309
386
|
if criteria.max_thd_n is not None:
|
|
310
387
|
results.append(check_thd_n(features, criteria.max_thd_n))
|
|
311
388
|
if criteria.custom_check is not None:
|
|
312
|
-
results.append(
|
|
389
|
+
results.append(
|
|
390
|
+
CheckResult.from_tuple("custom", criteria.custom_check(features))
|
|
391
|
+
)
|
|
313
392
|
|
|
314
|
-
|
|
315
|
-
if failures:
|
|
316
|
-
return False, "\n".join(failures)
|
|
317
|
-
return True, ""
|
|
393
|
+
return ChunkVerdict(results)
|
|
@@ -5,12 +5,29 @@ short-lived (only referenced by the bounded retention buffer).
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from textwrap import indent
|
|
8
9
|
from typing import List, Optional
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
11
12
|
import pandas as pd
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def format_timestamp(seconds: float) -> str:
|
|
16
|
+
"""Render *seconds* since capture start as ``HH:MM:SS.mmm``.
|
|
17
|
+
|
|
18
|
+
Chunk boundaries are far easier to line up with a long run's wall clock as
|
|
19
|
+
``00:15:04.023`` than as ``904.023``. Hours are not wrapped at 24.
|
|
20
|
+
|
|
21
|
+
:param seconds: Offset from capture start in seconds.
|
|
22
|
+
:return: Zero-padded ``HH:MM:SS.mmm`` string.
|
|
23
|
+
"""
|
|
24
|
+
millis = round(seconds * 1000)
|
|
25
|
+
hours, millis = divmod(millis, 3_600_000)
|
|
26
|
+
minutes, millis = divmod(millis, 60_000)
|
|
27
|
+
secs, millis = divmod(millis, 1000)
|
|
28
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}"
|
|
29
|
+
|
|
30
|
+
|
|
14
31
|
@dataclass
|
|
15
32
|
class RawChunk:
|
|
16
33
|
"""One contiguous captured block, tagged with its position in the run.
|
|
@@ -41,13 +58,19 @@ class ChannelMetric:
|
|
|
41
58
|
|
|
42
59
|
@dataclass
|
|
43
60
|
class ChunkMetrics:
|
|
44
|
-
"""Per-chunk analysis result: timing, verdict and per-channel metrics.
|
|
61
|
+
"""Per-chunk analysis result: timing, verdict and per-channel metrics.
|
|
62
|
+
|
|
63
|
+
:ivar reason: Single-line failure summary, suited to a table column.
|
|
64
|
+
:ivar detail: Multiline failure report with the per-check channel tables;
|
|
65
|
+
never put this in a DataFrame cell, log it separately.
|
|
66
|
+
"""
|
|
45
67
|
|
|
46
68
|
index: int
|
|
47
69
|
start_s: float
|
|
48
70
|
end_s: float
|
|
49
71
|
ok: bool
|
|
50
72
|
reason: str
|
|
73
|
+
detail: str = ""
|
|
51
74
|
channels: List[ChannelMetric] = field(default_factory=list)
|
|
52
75
|
|
|
53
76
|
|
|
@@ -60,6 +83,17 @@ class FailureInfo:
|
|
|
60
83
|
reason: str
|
|
61
84
|
wav_offset_s: float
|
|
62
85
|
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
"""Return a human-readable, multiline failure summary."""
|
|
88
|
+
return (
|
|
89
|
+
"FailureInfo:\n"
|
|
90
|
+
f" chunk_index: {self.chunk_index}\n"
|
|
91
|
+
f" time_s: {self.time_s}\n"
|
|
92
|
+
f" wav_offset_s: {self.wav_offset_s}\n"
|
|
93
|
+
" reason:\n"
|
|
94
|
+
f"{indent(self.reason, ' ')}"
|
|
95
|
+
)
|
|
96
|
+
|
|
63
97
|
|
|
64
98
|
@dataclass
|
|
65
99
|
class ValidationResult: # pylint: disable=too-many-instance-attributes
|
|
@@ -83,22 +117,44 @@ class ValidationResult: # pylint: disable=too-many-instance-attributes
|
|
|
83
117
|
plot_path: Optional[str] = None
|
|
84
118
|
|
|
85
119
|
def metrics_dataframe(self) -> pd.DataFrame:
|
|
86
|
-
"""Flatten the metrics timeline to one row per (chunk, channel).
|
|
120
|
+
"""Flatten the metrics timeline to one row per (chunk, channel).
|
|
121
|
+
|
|
122
|
+
The ``reason`` column holds only the single-line failure summary; the
|
|
123
|
+
per-check channel tables live in :meth:`failure_details`, because a
|
|
124
|
+
multiline table crammed into one cell wrecks the table layout.
|
|
125
|
+
|
|
126
|
+
``start``/``end`` are rendered as ``HH:MM:SS.mmm`` offsets from capture
|
|
127
|
+
start rather than raw seconds.
|
|
128
|
+
"""
|
|
87
129
|
rows = []
|
|
88
130
|
for metric in self.metrics:
|
|
89
131
|
for ch_idx, ch_mertric in enumerate(metric.channels):
|
|
90
132
|
rows.append(
|
|
91
133
|
{
|
|
92
134
|
"index": metric.index,
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"ok": metric.ok,
|
|
96
|
-
"reason": metric.reason,
|
|
135
|
+
"start": format_timestamp(metric.start_s),
|
|
136
|
+
"end": format_timestamp(metric.end_s),
|
|
97
137
|
"ch": ch_idx,
|
|
98
138
|
"rms": ch_mertric.rms,
|
|
99
139
|
"thd": ch_mertric.thd,
|
|
100
140
|
"thd_n": ch_mertric.thd_n,
|
|
101
141
|
"detected": ch_mertric.detected,
|
|
142
|
+
"ok": metric.ok,
|
|
143
|
+
"reason": metric.reason,
|
|
102
144
|
}
|
|
103
145
|
)
|
|
104
146
|
return pd.DataFrame(rows)
|
|
147
|
+
|
|
148
|
+
def failure_details(self) -> str:
|
|
149
|
+
"""Render the multiline failure report of every failing chunk.
|
|
150
|
+
|
|
151
|
+
One block per failing chunk, each with the per-check channel tables
|
|
152
|
+
produced by the criteria checks. Empty when no chunk failed.
|
|
153
|
+
"""
|
|
154
|
+
blocks = [
|
|
155
|
+
f"chunk {metric.index} (t={metric.start_s:.1f}-{metric.end_s:.1f}s):\n"
|
|
156
|
+
f"{indent(metric.detail or metric.reason, ' ')}"
|
|
157
|
+
for metric in self.metrics
|
|
158
|
+
if not metric.ok
|
|
159
|
+
]
|
|
160
|
+
return "\n".join(blocks)
|
|
@@ -27,6 +27,7 @@ from scipy.io import wavfile
|
|
|
27
27
|
from audio_validation.audio_features import AudioFeatures
|
|
28
28
|
from audio_validation.continous_validation.criteria import (
|
|
29
29
|
AudioCriteria,
|
|
30
|
+
ChunkVerdict,
|
|
30
31
|
evaluate_chunk,
|
|
31
32
|
)
|
|
32
33
|
from audio_validation.continous_validation.models import (
|
|
@@ -56,8 +57,14 @@ class ValidatorConfig:
|
|
|
56
57
|
:param max_capture_s: Capture-time limit; ``None`` means unlimited.
|
|
57
58
|
:param pre_failure_s: Seconds retained before failure; ``None`` keeps all.
|
|
58
59
|
:param post_failure_s: Seconds retained/captured after a failure.
|
|
59
|
-
:param stop_on_failure: Stop the run on the first armed failure.
|
|
60
|
+
:param stop_on_failure: Stop the run on the first armed failure. When
|
|
61
|
+
``False`` the run continues to completion but the failure is still
|
|
62
|
+
reported in :attr:`ValidationResult.failure`.
|
|
60
63
|
:param failure_consecutive: Consecutive failing chunks required to report run failure.
|
|
64
|
+
:param warmup_chunks: Leading chunks whose features are computed and recorded
|
|
65
|
+
but not evaluated. Covers playback start-up, where the signal is still
|
|
66
|
+
ramping and THD / RMS are meaningless — especially with a small
|
|
67
|
+
``chunk_s``, where the ramp fills a whole chunk.
|
|
61
68
|
:param artifacts_dir: Directory for persisted artifacts (for example WAV).
|
|
62
69
|
:param wav_filename: Saved WAV filename.
|
|
63
70
|
:param wav_dtype: Retained/saved sample dtype.
|
|
@@ -74,6 +81,7 @@ class ValidatorConfig:
|
|
|
74
81
|
post_failure_s: int = 60
|
|
75
82
|
stop_on_failure: bool = True
|
|
76
83
|
failure_consecutive: int = 1
|
|
84
|
+
warmup_chunks: int = 0
|
|
77
85
|
artifacts_dir: str = "test_artifacts"
|
|
78
86
|
wav_filename: str = "continuous_capture.wav"
|
|
79
87
|
wav_dtype: str = "float32"
|
|
@@ -323,7 +331,12 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
|
|
|
323
331
|
self._abort.set()
|
|
324
332
|
|
|
325
333
|
def _validate_chunk_features(self, chunk: RawChunk) -> None:
|
|
326
|
-
"""Compute features for *chunk*, evaluate criteria, and arm on failure.
|
|
334
|
+
"""Compute features for *chunk*, evaluate criteria, and arm on failure.
|
|
335
|
+
|
|
336
|
+
Warm-up chunks still get their features computed and recorded — they
|
|
337
|
+
belong in the metrics table and the WAV — but no check is run against
|
|
338
|
+
them, so they can neither fail the run nor break a failing streak.
|
|
339
|
+
"""
|
|
327
340
|
with self._state_lock:
|
|
328
341
|
criteria = self._criteria
|
|
329
342
|
|
|
@@ -337,20 +350,16 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
|
|
|
337
350
|
skip_latency=chunk.index == 0,
|
|
338
351
|
activity_threshold=criteria.silence_rms_threshold,
|
|
339
352
|
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
chunk.index,
|
|
343
|
-
chunk.start_s,
|
|
344
|
-
chunk.end_s,
|
|
345
|
-
)
|
|
346
|
-
ok, reason = evaluate_chunk(features, criteria)
|
|
353
|
+
warmup = chunk.index < self._cfg.warmup_chunks
|
|
354
|
+
verdict = ChunkVerdict() if warmup else evaluate_chunk(features, criteria)
|
|
347
355
|
|
|
348
356
|
metric = ChunkMetrics(
|
|
349
357
|
index=chunk.index,
|
|
350
358
|
start_s=chunk.start_s,
|
|
351
359
|
end_s=chunk.end_s,
|
|
352
|
-
ok=ok,
|
|
353
|
-
reason=
|
|
360
|
+
ok=verdict.ok,
|
|
361
|
+
reason="warm-up (not evaluated)" if warmup else verdict.summary,
|
|
362
|
+
detail=verdict.detail,
|
|
354
363
|
channels=[
|
|
355
364
|
ChannelMetric(
|
|
356
365
|
rms=feat.rms,
|
|
@@ -372,26 +381,27 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
|
|
|
372
381
|
|
|
373
382
|
self._retention.add(chunk)
|
|
374
383
|
|
|
375
|
-
if ok:
|
|
384
|
+
if verdict.ok:
|
|
376
385
|
self._consec_fail = 0
|
|
377
386
|
return
|
|
378
|
-
if not self._cfg.stop_on_failure:
|
|
379
|
-
return
|
|
380
387
|
self._consec_fail += 1
|
|
381
388
|
if self._consec_fail < self._cfg.failure_consecutive:
|
|
382
389
|
return
|
|
383
|
-
|
|
390
|
+
arm_stop = False
|
|
384
391
|
with self._state_lock:
|
|
385
|
-
if self.
|
|
386
|
-
self._failure_time_s = chunk.end_s
|
|
392
|
+
if self._failure_info is None:
|
|
387
393
|
self._failure_info = FailureInfo(
|
|
388
394
|
chunk_index=chunk.index,
|
|
389
395
|
time_s=chunk.start_s,
|
|
390
|
-
reason=
|
|
396
|
+
reason=verdict.detail,
|
|
391
397
|
wav_offset_s=0.0, # will be updated when wav is written
|
|
392
398
|
)
|
|
393
|
-
|
|
394
|
-
|
|
399
|
+
# ``_failure_time_s`` arms the run teardown (reader break + retention
|
|
400
|
+
# freeze), so it is only set when the run is meant to stop.
|
|
401
|
+
if self._cfg.stop_on_failure and self._failure_time_s is None:
|
|
402
|
+
self._failure_time_s = chunk.end_s
|
|
403
|
+
arm_stop = True
|
|
404
|
+
if arm_stop:
|
|
395
405
|
self._retention.freeze(chunk.end_s)
|
|
396
406
|
|
|
397
407
|
# Finalising the run and assembling the result ------------------------------------------------
|
|
@@ -463,12 +473,20 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
|
|
|
463
473
|
error = self._error
|
|
464
474
|
metrics = list(self._metrics)
|
|
465
475
|
|
|
466
|
-
|
|
476
|
+
# Without ``stop_on_failure`` the retention window keeps rolling, so the
|
|
477
|
+
# failing chunk may have been evicted before the WAV was written.
|
|
478
|
+
failure_in_wav = (
|
|
479
|
+
failure is not None
|
|
480
|
+
and wav_start is not None
|
|
481
|
+
and wav_end is not None
|
|
482
|
+
and wav_start <= failure.time_s <= wav_end
|
|
483
|
+
)
|
|
484
|
+
if failure_in_wav:
|
|
467
485
|
failure = replace(failure, wav_offset_s=failure.time_s - wav_start)
|
|
468
486
|
|
|
469
487
|
if error is not None:
|
|
470
488
|
stopped_reason = "capture_error"
|
|
471
|
-
elif failure is not None:
|
|
489
|
+
elif failure is not None and self._cfg.stop_on_failure:
|
|
472
490
|
stopped_reason = "failure"
|
|
473
491
|
elif self._stop_requested:
|
|
474
492
|
stopped_reason = "stopped"
|
|
@@ -484,8 +502,12 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
|
|
|
484
502
|
(
|
|
485
503
|
f"; failure at t={failure.time_s:.1f}s = "
|
|
486
504
|
f"{failure.wav_offset_s:.1f}s into WAV."
|
|
487
|
-
if
|
|
488
|
-
else
|
|
505
|
+
if failure_in_wav
|
|
506
|
+
else (
|
|
507
|
+
f"; failure at t={failure.time_s:.1f}s is outside the WAV."
|
|
508
|
+
if failure is not None
|
|
509
|
+
else "."
|
|
510
|
+
)
|
|
489
511
|
),
|
|
490
512
|
)
|
|
491
513
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: audio_validation
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tools for validating audio data, including feature extraction and visualization.
|
|
5
5
|
Maintainer-email: Hubert Stepniewski <hubert.stepniewski@int2code.com>, Marcin Tomiczek <marcin.tomiczek@int2code.com>, Piotr Sznapka <piotr.sznapka@int2code.com>
|
|
6
6
|
License: MIT License
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/scm_file_list.json
RENAMED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
-
"README.md",
|
|
4
|
-
"requirements-test.txt",
|
|
5
3
|
"tox.ini",
|
|
6
|
-
"LICENSE",
|
|
7
|
-
".pylint_ignore",
|
|
8
|
-
"pyproject.toml",
|
|
9
4
|
"requirements.txt",
|
|
10
|
-
".
|
|
5
|
+
"requirements-test.txt",
|
|
11
6
|
"requirements-dev.txt",
|
|
7
|
+
"pyproject.toml",
|
|
8
|
+
".pylint_ignore",
|
|
9
|
+
"LICENSE",
|
|
12
10
|
"RELEASE_NOTES.md",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"docs/tutorials/index.rst",
|
|
11
|
+
"README.md",
|
|
12
|
+
".gitignore",
|
|
13
|
+
".github/workflows/code_checker.yml",
|
|
14
|
+
".github/workflows/unit_tests.yml",
|
|
15
|
+
".github/workflows/publish_to_pypi.yml",
|
|
16
|
+
"tests/__init__.py",
|
|
17
|
+
"tests/test_template.py",
|
|
21
18
|
"src/audio_validation/__init__.py",
|
|
22
19
|
"src/audio_validation/audio_features.py",
|
|
23
20
|
"src/audio_validation/audio_verification.py",
|
|
24
21
|
"src/audio_validation/utils/audio_generation.py",
|
|
25
|
-
"src/audio_validation/continous_validation/retention.py",
|
|
26
|
-
"src/audio_validation/continous_validation/validator.py",
|
|
27
22
|
"src/audio_validation/continous_validation/criteria.py",
|
|
23
|
+
"src/audio_validation/continous_validation/plots.py",
|
|
24
|
+
"src/audio_validation/continous_validation/validator.py",
|
|
28
25
|
"src/audio_validation/continous_validation/recorder.py",
|
|
26
|
+
"src/audio_validation/continous_validation/retention.py",
|
|
29
27
|
"src/audio_validation/continous_validation/models.py",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
28
|
+
"docs/requirements.txt",
|
|
29
|
+
"docs/index.rst",
|
|
30
|
+
"docs/conf.py",
|
|
31
|
+
"docs/release_notes.rst",
|
|
32
|
+
"docs/Makefile",
|
|
33
|
+
"docs/contribute.rst",
|
|
34
|
+
"docs/installation.rst",
|
|
35
|
+
"docs/tutorials/index.rst",
|
|
36
|
+
"_ci/is_dev_version.sh"
|
|
37
37
|
]
|
|
38
38
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/audio_verification.py
RENAMED
|
File without changes
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/continous_validation/plots.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation/utils/audio_generation.py
RENAMED
|
File without changes
|
|
File without changes
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/requires.txt
RENAMED
|
File without changes
|
{audio_validation-0.3.0 → audio_validation-0.3.2}/src/audio_validation.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|