audio-validation 0.3.2__tar.gz → 0.3.4__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.
Files changed (45) hide show
  1. {audio_validation-0.3.2 → audio_validation-0.3.4}/PKG-INFO +1 -1
  2. {audio_validation-0.3.2 → audio_validation-0.3.4}/RELEASE_NOTES.md +8 -0
  3. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/_version.py +3 -3
  4. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/continous_validation/models.py +24 -1
  5. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/continous_validation/validator.py +122 -17
  6. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation.egg-info/PKG-INFO +1 -1
  7. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation.egg-info/scm_file_list.json +24 -24
  8. audio_validation-0.3.4/src/audio_validation.egg-info/scm_version.json +8 -0
  9. audio_validation-0.3.2/src/audio_validation.egg-info/scm_version.json +0 -8
  10. {audio_validation-0.3.2 → audio_validation-0.3.4}/.github/workflows/code_checker.yml +0 -0
  11. {audio_validation-0.3.2 → audio_validation-0.3.4}/.github/workflows/publish_to_pypi.yml +0 -0
  12. {audio_validation-0.3.2 → audio_validation-0.3.4}/.github/workflows/unit_tests.yml +0 -0
  13. {audio_validation-0.3.2 → audio_validation-0.3.4}/.gitignore +0 -0
  14. {audio_validation-0.3.2 → audio_validation-0.3.4}/.pylint_ignore +0 -0
  15. {audio_validation-0.3.2 → audio_validation-0.3.4}/LICENSE +0 -0
  16. {audio_validation-0.3.2 → audio_validation-0.3.4}/README.md +0 -0
  17. {audio_validation-0.3.2 → audio_validation-0.3.4}/_ci/is_dev_version.sh +0 -0
  18. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/Makefile +0 -0
  19. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/conf.py +0 -0
  20. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/contribute.rst +0 -0
  21. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/index.rst +0 -0
  22. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/installation.rst +0 -0
  23. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/release_notes.rst +0 -0
  24. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/requirements.txt +0 -0
  25. {audio_validation-0.3.2 → audio_validation-0.3.4}/docs/tutorials/index.rst +0 -0
  26. {audio_validation-0.3.2 → audio_validation-0.3.4}/pyproject.toml +0 -0
  27. {audio_validation-0.3.2 → audio_validation-0.3.4}/requirements-dev.txt +0 -0
  28. {audio_validation-0.3.2 → audio_validation-0.3.4}/requirements-test.txt +0 -0
  29. {audio_validation-0.3.2 → audio_validation-0.3.4}/requirements.txt +0 -0
  30. {audio_validation-0.3.2 → audio_validation-0.3.4}/setup.cfg +0 -0
  31. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/__init__.py +0 -0
  32. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/audio_features.py +0 -0
  33. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/audio_verification.py +0 -0
  34. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/continous_validation/criteria.py +0 -0
  35. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/continous_validation/plots.py +0 -0
  36. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/continous_validation/recorder.py +0 -0
  37. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/continous_validation/retention.py +0 -0
  38. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation/utils/audio_generation.py +0 -0
  39. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation.egg-info/SOURCES.txt +0 -0
  40. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation.egg-info/dependency_links.txt +0 -0
  41. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation.egg-info/requires.txt +0 -0
  42. {audio_validation-0.3.2 → audio_validation-0.3.4}/src/audio_validation.egg-info/top_level.txt +0 -0
  43. {audio_validation-0.3.2 → audio_validation-0.3.4}/tests/__init__.py +0 -0
  44. {audio_validation-0.3.2 → audio_validation-0.3.4}/tests/test_template.py +0 -0
  45. {audio_validation-0.3.2 → audio_validation-0.3.4}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audio_validation
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -1,3 +1,11 @@
1
+ ### 0.3.4
2
+
3
+ [PATCH] Add non-blocking chunk validation skip to transition conditions.
4
+
5
+ ### 0.3.3
6
+
7
+ [PATCH] Add absolute timestamps in audio chunks.
8
+
1
9
  ### 0.3.2
2
10
 
3
11
  [PATCH] Add warmup chunks, improve failure reporting.
@@ -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.2'
22
- __version_tuple__ = version_tuple = (0, 3, 2)
21
+ __version__ = version = '0.3.4'
22
+ __version_tuple__ = version_tuple = (0, 3, 4)
23
23
 
24
- __commit_id__ = commit_id = 'ge32c5a61b'
24
+ __commit_id__ = commit_id = 'gc5b840998'
@@ -5,12 +5,15 @@ short-lived (only referenced by the bounded retention buffer).
5
5
  """
6
6
 
7
7
  from dataclasses import dataclass, field
8
+ from datetime import datetime
8
9
  from textwrap import indent
9
10
  from typing import List, Optional
10
11
 
11
12
  import numpy as np
12
13
  import pandas as pd
13
14
 
15
+ WALL_CLOCK_UNKNOWN = "-"
16
+
14
17
 
15
18
  def format_timestamp(seconds: float) -> str:
16
19
  """Render *seconds* since capture start as ``HH:MM:SS.mmm``.
@@ -28,6 +31,18 @@ def format_timestamp(seconds: float) -> str:
28
31
  return f"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}"
29
32
 
30
33
 
34
+ def format_wall_clock(timestamp: Optional[datetime]) -> str:
35
+ """Render an absolute chunk timestamp as ``YYYY-MM-DD HH:MM:SS.mmm``.
36
+
37
+ :param timestamp: Absolute chunk time, or ``None`` when not recorded.
38
+ :return: Millisecond-precision timestamp, or ``"-"`` when *timestamp* is
39
+ ``None``.
40
+ """
41
+ if timestamp is None:
42
+ return WALL_CLOCK_UNKNOWN
43
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
44
+
45
+
31
46
  @dataclass
32
47
  class RawChunk:
33
48
  """One contiguous captured block, tagged with its position in the run.
@@ -36,12 +51,15 @@ class RawChunk:
36
51
  :ivar start_s: Start time in seconds relative to capture start.
37
52
  :ivar end_s: End time in seconds relative to capture start.
38
53
  :ivar samples: 2-D array ``(n_samples, channels)``.
54
+ :ivar start_timestamp: Absolute wall-clock time of the chunk's first sample,
55
+ or ``None`` when the capture start time is unknown.
39
56
  """
40
57
 
41
58
  index: int
42
59
  start_s: float
43
60
  end_s: float
44
61
  samples: np.ndarray
62
+ start_timestamp: Optional[datetime] = None
45
63
 
46
64
 
47
65
  @dataclass
@@ -63,6 +81,8 @@ class ChunkMetrics:
63
81
  :ivar reason: Single-line failure summary, suited to a table column.
64
82
  :ivar detail: Multiline failure report with the per-check channel tables;
65
83
  never put this in a DataFrame cell, log it separately.
84
+ :ivar start_timestamp: Absolute wall-clock time of the chunk's first sample,
85
+ or ``None`` when the capture start time is unknown.
66
86
  """
67
87
 
68
88
  index: int
@@ -72,6 +92,7 @@ class ChunkMetrics:
72
92
  reason: str
73
93
  detail: str = ""
74
94
  channels: List[ChannelMetric] = field(default_factory=list)
95
+ start_timestamp: Optional[datetime] = None
75
96
 
76
97
 
77
98
  @dataclass
@@ -124,7 +145,8 @@ class ValidationResult: # pylint: disable=too-many-instance-attributes
124
145
  multiline table crammed into one cell wrecks the table layout.
125
146
 
126
147
  ``start``/``end`` are rendered as ``HH:MM:SS.mmm`` offsets from capture
127
- start rather than raw seconds.
148
+ start rather than raw seconds; ``timestamp`` is the absolute wall-clock
149
+ time of the chunk's first sample, for lining chunks up with DUT logs.
128
150
  """
129
151
  rows = []
130
152
  for metric in self.metrics:
@@ -132,6 +154,7 @@ class ValidationResult: # pylint: disable=too-many-instance-attributes
132
154
  rows.append(
133
155
  {
134
156
  "index": metric.index,
157
+ "timestamp": format_wall_clock(metric.start_timestamp),
135
158
  "start": format_timestamp(metric.start_s),
136
159
  "end": format_timestamp(metric.end_s),
137
160
  "ch": ch_idx,
@@ -19,7 +19,7 @@ import threading
19
19
  from concurrent.futures import CancelledError, Future, ThreadPoolExecutor
20
20
  from concurrent.futures import wait as futures_wait
21
21
  from dataclasses import dataclass, replace
22
- import time
22
+ from datetime import datetime, timedelta
23
23
  from typing import Callable, List, Optional
24
24
 
25
25
  from scipy.io import wavfile
@@ -149,6 +149,11 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
149
149
  self._error: Optional[str] = None
150
150
 
151
151
  self._captured_time_s = 0.0
152
+ self._capture_started_at: Optional[datetime] = None
153
+ self._chunks_read = 0
154
+ self._skip_from_index = 0
155
+ self._skip_until_index: Optional[int] = None
156
+ self._pending_criteria: Optional[AudioCriteria] = None
152
157
  self._consec_fail = 0
153
158
  self._stop_requested = False
154
159
  self._play_stopped = False
@@ -180,19 +185,57 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
180
185
  empty_criteria = AudioCriteria() # no check is performed
181
186
  self.update_criteria(empty_criteria)
182
187
 
188
+ def skip_chunks(
189
+ self, skip_chunks: int, criteria: Optional[AudioCriteria] = None
190
+ ) -> int:
191
+ """Leave the next *skip_chunks* chunks unevaluated, then swap criteria.
192
+
193
+ **Returns immediately** — the caller keeps the audio timeline running and
194
+ is free to disturb the DUT (install a network delay, power-cycle, change
195
+ volume, ...) *inside* the skip window it just opened. The window and the
196
+ criteria swap are honoured by the analysis worker, keyed on chunk index,
197
+ so a queue backlog cannot shift them.
198
+
199
+ :param skip_chunks: Number of chunks to leave unevaluated. ``0`` skips
200
+ nothing and applies *criteria* to the next analysed chunk.
201
+ :param criteria: Criteria to activate once the window has passed.
202
+ ``None`` keeps the active criteria (skip only).
203
+ :return: Index of the first chunk that will be evaluated again.
204
+ """
205
+ with self._state_lock:
206
+ # The chunk being captured right now is ``_chunks_read``; the change
207
+ # the caller is about to make lands inside it, so it is skipped too.
208
+ first_skipped = self._chunks_read
209
+ skip_until = first_skipped + max(skip_chunks, 0)
210
+ if self._skip_until_index is not None:
211
+ # Extending an open window: keep its (earlier) lower bound.
212
+ first_skipped = self._skip_from_index
213
+ skip_until = max(skip_until, self._skip_until_index)
214
+ self._skip_from_index = first_skipped
215
+ self._skip_until_index = skip_until
216
+ if criteria is not None:
217
+ self._pending_criteria = criteria
218
+ swap = "; criteria swapped afterwards" if criteria is not None else ""
219
+ if skip_until > first_skipped:
220
+ logger.info(
221
+ "Skipping checks for chunks %d-%d%s",
222
+ first_skipped,
223
+ skip_until - 1,
224
+ swap,
225
+ )
226
+ else:
227
+ logger.info("Skipping no chunks%s", swap)
228
+ return skip_until
229
+
183
230
  def skip_check_and_update_criteria(
184
231
  self, skip_chunks: int, criteria: AudioCriteria
185
232
  ) -> None:
186
- """Set the active criteria to an empty one, effectively skipping checks.
233
+ """Skip given number of chunks and then set the new criteria. Thread-safe.
187
234
 
188
- Then update the criteria to the provided one after a short delay.
189
- This is useful for skipping checks during a known transient event.
235
+ :param skip_chunks: Number of chunks to leave unevaluated.
236
+ :param criteria: Criteria to activate once the window has passed.
190
237
  """
191
- self.set_empty_criteria()
192
- time.sleep(
193
- skip_chunks * self._cfg.chunk_s
194
- ) # skip the specified number of chunks
195
- self.update_criteria(criteria)
238
+ self.skip_chunks(skip_chunks, criteria)
196
239
 
197
240
  def snapshot_metrics(self) -> List[ChunkMetrics]:
198
241
  """Return a copy of the metrics timeline collected so far."""
@@ -258,6 +301,17 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
258
301
  except Exception: # pylint: disable=broad-except
259
302
  logger.exception("recorder.stop_capture() raised")
260
303
 
304
+ def _chunk_timestamp(self, chunk_start_s: float) -> Optional[datetime]:
305
+ """Return the absolute time of a chunk starting *chunk_start_s* into the run.
306
+ Chunk timestamps are derived from capture start plus the sample-count offset,
307
+
308
+ :param chunk_start_s: Chunk start offset from capture start, in seconds.
309
+ :return: Wall-clock chunk start, or ``None`` before capture started.
310
+ """
311
+ if self._capture_started_at is None:
312
+ return None
313
+ return self._capture_started_at + timedelta(seconds=chunk_start_s)
314
+
261
315
  def _read_chunks(self) -> None:
262
316
  """
263
317
  Read chunks from the recorder and put them into the queue.
@@ -266,6 +320,7 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
266
320
  total_samples = 0
267
321
  try:
268
322
  self._recorder.start_capture()
323
+ self._capture_started_at = datetime.now()
269
324
  while not self._stop.is_set() and not self._abort.is_set():
270
325
 
271
326
  if (
@@ -293,9 +348,17 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
293
348
  chunk_end_s = total_samples / self._sample_rate
294
349
  self._captured_time_s = chunk_end_s
295
350
  self._enqueue_chunk(
296
- RawChunk(chunk_idx, chunk_start_s, chunk_end_s, chunk)
351
+ RawChunk(
352
+ chunk_idx,
353
+ chunk_start_s,
354
+ chunk_end_s,
355
+ chunk,
356
+ self._chunk_timestamp(chunk_start_s),
357
+ )
297
358
  )
298
359
  chunk_idx += 1
360
+ with self._state_lock:
361
+ self._chunks_read = chunk_idx
299
362
  except Exception as exc: # pylint: disable=broad-except
300
363
  with self._state_lock:
301
364
  self._error = f"{type(exc).__name__}: {exc}"
@@ -330,15 +393,48 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
330
393
  finally:
331
394
  self._abort.set()
332
395
 
333
- def _validate_chunk_features(self, chunk: RawChunk) -> None:
334
- """Compute features for *chunk*, evaluate criteria, and arm on failure.
396
+ def _criteria_for_chunk(self, chunk_index: int) -> tuple[AudioCriteria, bool]:
397
+ """Resolve the criteria to use for *chunk_index* and whether to skip it.
398
+
399
+ Closes a :meth:`skip_chunks` window and applies its deferred criteria as
400
+ soon as a chunk from beyond the window arrives.
401
+
402
+ The window is bounded on both sides: chunks already queued when
403
+ :meth:`skip_chunks` was called before the caller's change, so they are
404
+ still evaluated against the criteria that were active while they were
405
+ captured.
335
406
 
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.
407
+ :param chunk_index: Index of the chunk about to be analysed.
408
+ :return: Tuple of the active criteria and whether the chunk is inside a
409
+ skip window (features computed, no checks run).
339
410
  """
411
+ applied = False
340
412
  with self._state_lock:
413
+ if self._skip_until_index is None:
414
+ return self._criteria, False
415
+ if chunk_index < self._skip_until_index:
416
+ return self._criteria, chunk_index >= self._skip_from_index
417
+ self._skip_until_index = None
418
+ if self._pending_criteria is not None:
419
+ self._criteria = self._pending_criteria
420
+ self._pending_criteria = None
421
+ applied = True
341
422
  criteria = self._criteria
423
+ if applied:
424
+ logger.info(
425
+ "Skip window over, new criteria active from chunk %d", chunk_index
426
+ )
427
+ return criteria, False
428
+
429
+ def _validate_chunk_features(self, chunk: RawChunk) -> None:
430
+ """Compute features for *chunk*, evaluate criteria, and arm on failure.
431
+
432
+ Warm-up chunks and chunks inside a :meth:`skip_chunks` window still get
433
+ their features computed and recorded — they belong in the metrics table
434
+ and the WAV — but no check is run against them, so they can neither fail
435
+ the run nor break a failing streak.
436
+ """
437
+ criteria, skipped = self._criteria_for_chunk(chunk.index)
342
438
 
343
439
  use_fft = criteria.expected_frequencies is not None
344
440
  features = AudioFeatures.compute(
@@ -351,14 +447,23 @@ class ContinuousAudioValidator: # pylint: disable=too-many-instance-attributes
351
447
  activity_threshold=criteria.silence_rms_threshold,
352
448
  )
353
449
  warmup = chunk.index < self._cfg.warmup_chunks
354
- verdict = ChunkVerdict() if warmup else evaluate_chunk(features, criteria)
450
+ if warmup:
451
+ not_evaluated = "warm-up (not evaluated)"
452
+ elif skipped:
453
+ not_evaluated = "skipped (criteria transition)"
454
+ else:
455
+ not_evaluated = ""
456
+ verdict = (
457
+ ChunkVerdict() if not_evaluated else evaluate_chunk(features, criteria)
458
+ )
355
459
 
356
460
  metric = ChunkMetrics(
357
461
  index=chunk.index,
358
462
  start_s=chunk.start_s,
359
463
  end_s=chunk.end_s,
464
+ start_timestamp=chunk.start_timestamp,
360
465
  ok=verdict.ok,
361
- reason="warm-up (not evaluated)" if warmup else verdict.summary,
466
+ reason=not_evaluated or verdict.summary,
362
467
  detail=verdict.detail,
363
468
  channels=[
364
469
  ChannelMetric(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audio_validation
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -1,38 +1,38 @@
1
1
  {
2
2
  "files": [
3
- "tox.ini",
4
- "requirements.txt",
5
- "requirements-test.txt",
6
- "requirements-dev.txt",
7
- "pyproject.toml",
3
+ ".github/workflows/code_checker.yml",
4
+ ".github/workflows/publish_to_pypi.yml",
5
+ ".github/workflows/unit_tests.yml",
6
+ ".gitignore",
8
7
  ".pylint_ignore",
9
8
  "LICENSE",
10
- "RELEASE_NOTES.md",
11
9
  "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",
10
+ "RELEASE_NOTES.md",
11
+ "_ci/is_dev_version.sh",
12
+ "docs/Makefile",
13
+ "docs/conf.py",
14
+ "docs/contribute.rst",
15
+ "docs/index.rst",
16
+ "docs/installation.rst",
17
+ "docs/release_notes.rst",
18
+ "docs/requirements.txt",
19
+ "docs/tutorials/index.rst",
20
+ "pyproject.toml",
21
+ "requirements-dev.txt",
22
+ "requirements-test.txt",
23
+ "requirements.txt",
18
24
  "src/audio_validation/__init__.py",
19
25
  "src/audio_validation/audio_features.py",
20
26
  "src/audio_validation/audio_verification.py",
21
- "src/audio_validation/utils/audio_generation.py",
22
27
  "src/audio_validation/continous_validation/criteria.py",
28
+ "src/audio_validation/continous_validation/models.py",
23
29
  "src/audio_validation/continous_validation/plots.py",
24
- "src/audio_validation/continous_validation/validator.py",
25
30
  "src/audio_validation/continous_validation/recorder.py",
26
31
  "src/audio_validation/continous_validation/retention.py",
27
- "src/audio_validation/continous_validation/models.py",
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"
32
+ "src/audio_validation/continous_validation/validator.py",
33
+ "src/audio_validation/utils/audio_generation.py",
34
+ "tests/__init__.py",
35
+ "tests/test_template.py",
36
+ "tox.ini"
37
37
  ]
38
38
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.4",
3
+ "distance": 0,
4
+ "node": "gc5b840998cee69b55769a7f8f90667191a0749af",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-12"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.3.2",
3
- "distance": 0,
4
- "node": "ge32c5a61ba2f0d187518620dd508fd43f8e2e24f",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-06"
8
- }