fff
This commit is contained in:
@@ -57,12 +57,38 @@ def analyze_audio(path: Path) -> AudioMetrics:
|
||||
|
||||
|
||||
def analyze_samples(samples: np.ndarray, sample_rate: int = SAMPLE_RATE) -> AudioMetrics:
|
||||
if samples.ndim != 1:
|
||||
samples = samples.reshape(-1)
|
||||
speech = speech_frame_mask(samples, sample_rate)
|
||||
speech_indexes = np.flatnonzero(speech)
|
||||
if speech_indexes.size == 0:
|
||||
raise AudioAnalysisError("No usable speech was detected in the recording.")
|
||||
|
||||
recording_duration_ms = max(1, int(round(samples.size / sample_rate * 1000)))
|
||||
window = max(1, int(round(sample_rate * 0.03)))
|
||||
frame_ms = window / sample_rate * 1000
|
||||
speech_duration_ms = max(1, int(round(speech.sum() * frame_ms)))
|
||||
first = int(speech_indexes[0])
|
||||
last = int(speech_indexes[-1])
|
||||
internal_frames = max(1, last - first + 1)
|
||||
internal_silence_frames = int((~speech[first : last + 1]).sum())
|
||||
internal_silence_ms = int(round(internal_silence_frames * frame_ms))
|
||||
internal_pause_ratio = internal_silence_frames / internal_frames
|
||||
return AudioMetrics(
|
||||
recording_duration_ms=recording_duration_ms,
|
||||
speech_duration_ms=speech_duration_ms,
|
||||
internal_silence_ms=internal_silence_ms,
|
||||
internal_pause_ratio=round(internal_pause_ratio, 4),
|
||||
)
|
||||
|
||||
|
||||
def speech_frame_mask(samples: np.ndarray, sample_rate: int = SAMPLE_RATE) -> np.ndarray:
|
||||
"""Per-frame speech mask (30 ms frames) using the same RMS threshold as analyze_samples."""
|
||||
if samples.ndim != 1:
|
||||
samples = samples.reshape(-1)
|
||||
if samples.size == 0 or sample_rate <= 0:
|
||||
raise AudioAnalysisError("The uploaded audio is empty.")
|
||||
|
||||
recording_duration_ms = max(1, int(round(samples.size / sample_rate * 1000)))
|
||||
window = max(1, int(round(sample_rate * 0.03)))
|
||||
complete_frames = int(np.ceil(samples.size / window))
|
||||
padded = np.pad(samples, (0, complete_frames * window - samples.size))
|
||||
@@ -79,24 +105,7 @@ def analyze_samples(samples: np.ndarray, sample_rate: int = SAMPLE_RATE) -> Audi
|
||||
# Treat very short gaps inside a word as speech, then reject short clicks.
|
||||
_bridge_false_runs(speech, max_frames=5)
|
||||
_remove_true_runs(speech, max_frames=2)
|
||||
speech_indexes = np.flatnonzero(speech)
|
||||
if speech_indexes.size == 0:
|
||||
raise AudioAnalysisError("No usable speech was detected in the recording.")
|
||||
|
||||
frame_ms = window / sample_rate * 1000
|
||||
speech_duration_ms = max(1, int(round(speech.sum() * frame_ms)))
|
||||
first = int(speech_indexes[0])
|
||||
last = int(speech_indexes[-1])
|
||||
internal_frames = max(1, last - first + 1)
|
||||
internal_silence_frames = int((~speech[first : last + 1]).sum())
|
||||
internal_silence_ms = int(round(internal_silence_frames * frame_ms))
|
||||
internal_pause_ratio = internal_silence_frames / internal_frames
|
||||
return AudioMetrics(
|
||||
recording_duration_ms=recording_duration_ms,
|
||||
speech_duration_ms=speech_duration_ms,
|
||||
internal_silence_ms=internal_silence_ms,
|
||||
internal_pause_ratio=round(internal_pause_ratio, 4),
|
||||
)
|
||||
return speech
|
||||
|
||||
|
||||
def _bridge_false_runs(values: np.ndarray, max_frames: int) -> None:
|
||||
|
||||
Reference in New Issue
Block a user