vocal_helper.cloud_media module
vocal_helper.cloud_media
Preparing media for a paid call, and stitching the answer back together.
Three concerns that every hosted provider shares, kept in one place so neither
vocal_helper.asr_cloud nor vocal_helper.diar_cloud reimplements them:
What gets uploaded. Always a 16 kHz mono MP3 of the waveform, never the source container as it stands.
How a file longer than a provider’s cap is handled. Overlapping chunks, then a stitch that keeps speaker labels globally consistent.
Who spoke during an interval. The overlap arithmetic both the stitch and the fusion need.
Why the upload is always converted, and always to the same thing
A hosted transcriber or diarizer only ever needs the waveform. Sending the source file as it stands uploads a video track that will be discarded, or a 320 kbit/s stereo master whose extra bits change no word. Converting unconditionally — not merely above a size threshold — also makes the upload size and the processing time predictable whatever the source happens to be, which matters when a run has hundreds of files and a budget.
The conversion goes through video-helper for a video container and
audio-helper otherwise, never through a hand-written ffmpeg command line:
those two libraries own the encoder flags for the whole suite, and a local copy of
them drifts.
Why chunks overlap, and why the seam is the overlap’s middle
Providers cap a single file (Gladia at ~135 minutes). Splitting at the cap and concatenating gives two problems at every seam: a word cut in half, and two independent sets of speaker labels with nothing relating them.
Overlapping the chunks solves both. The same voice speaks at the same absolute
time in both chunks, so the speaker who co-occurs most across the shared window is
the same person — that is what match_speakers_across_overlap() measures, and
it makes per-chunk labels globally consistent. Appending only the turns past the
middle of the overlap then dedupes the seam without losing anything: each
second of audio is contributed by exactly one chunk, and the one that contributes
it had context on both sides.
Usage example
>>> plan_chunks(duration_s=100.0, cap_s=8000.0, overlap_s=120.0)
[0.0]
>>> plan_chunks(duration_s=20000.0, cap_s=8000.0, overlap_s=120.0)
[0.0, 7880.0, 15760.0]
>>> speaker_by_overlap([{"start": 0.0, "end": 5.0, "speaker": "A"},
... {"start": 4.0, "end": 9.0, "speaker": "B"}], 4.5, 8.0)
'B'
- vocal_helper.cloud_media.audio_duration(path)[source]
Duration of
pathin seconds, viaaudio-helper.
- vocal_helper.cloud_media.extract_clip(audio, start, length, out_path)[source]
Extract
[start, start + length]ofaudiointoout_path.Goes through
audio-helper’s chunk extraction rather than a hand-rolledffmpegcall, and takes an explicit destination rather than minting its own temporary file: a long recording chunks into many clips, and a function that mints paths it does not own is how a process ends up leaking descriptors.
- vocal_helper.cloud_media.match_speakers_across_overlap(base, new, win_start, win_end)[source]
Map each
new-chunk speaker onto thebasespeaker it co-occurs with.Both chunks transcribed the same audio inside
[win_start, win_end], so one voice speaks at the same absolute time in both. Thenewlabel whose turns overlap abaselabel’s turns most is the same person.- Parameters:
- Returns:
{new_label: base_label}, containing only labels that were observed in the window. A caller must give an unmatched label a chunk-scoped name of its own rather than guessing.- Return type:
Examples
>>> base = [{"start": 0.0, "end": 4.0, "speaker": "S0"}] >>> new = [{"start": 0.5, "end": 3.5, "speaker": "S1"}] >>> match_speakers_across_overlap(base, new, 0.0, 4.0) {'S1': 'S0'}
- vocal_helper.cloud_media.plan_chunks(*, duration_s, cap_s, overlap_s)[source]
Offsets of the overlapping chunks covering
duration_s.- Parameters:
duration_s (float, keyword-only) – Total media duration in seconds.
cap_s (float, keyword-only) – The provider’s per-file cap, in seconds. Stay a little under the published value: a cap hit is a rejected job, not a truncated one.
overlap_s (float, keyword-only) – How much consecutive chunks share. It must be long enough for several speaker turns to occur inside it, or the label matching has nothing to measure.
- Returns:
Chunk start offsets. A single
[0.0]when the media fits in one call.- Return type:
Examples
>>> plan_chunks(duration_s=60.0, cap_s=8000.0, overlap_s=120.0) [0.0] >>> plan_chunks(duration_s=16000.0, cap_s=8000.0, overlap_s=120.0) [0.0, 7880.0, 15760.0]
The last chunk is deliberately allowed to be short: three chunks of 7880 s of fresh audio each are needed to reach 16000 s, and the third one carries only 240 s. Two would stop at 15760 s and lose the tail.
- vocal_helper.cloud_media.speaker_by_overlap(segments, t0, t1)[source]
The speaker whose segments overlap
[t0, t1]most, orNone.Scans every segment rather than bisecting around
t0’s sorted position. A hosted diarizer returns genuinely overlapping segments on cross-talk, so a long segment starting well beforet0can still cover it; a “start neart0” shortcut silently drops exactly those.- Parameters:
- Returns:
The dominant speaker label, or
Nonewhen nothing overlaps.- Return type:
str or None
Examples
>>> segs = [{"start": 0.0, "end": 10.0, "speaker": "A"}, ... {"start": 2.0, "end": 3.0, "speaker": "B"}] >>> speaker_by_overlap(segs, 2.0, 3.0) 'A' >>> speaker_by_overlap(segs, 50.0, 60.0) is None True
- vocal_helper.cloud_media.upload_ready(audio, tmp_dir)[source]
Return a 16 kHz mono MP3 of
audio, written insidetmp_dir.- Parameters:
audio (str) – Source media: an audio file, or a video container whose video track is dropped.
tmp_dir (str) – Directory the converted file is written into. The caller owns its lifetime — wrap the call in
os_helper.temporary_folder()and cleanup happens on the way out, including on an exception, with no explicit delete here.
- Returns:
Path of the converted file.
- Return type: