mirror of
https://github.com/mozilla/DeepSpeech.git
synced 2025-10-26 11:19:39 +00:00
Prerequisites ------------- ~/Deepspeech$ sudo apt install virtualenv ~/Deepspeech$ cd examples/vad_transcriber ~/Deepspeech/examples/vad_transcriber$ virtualenv -p python3 venv ~/Deepspeech/examples/vad_transcriber$ source venv/bin/activate (venv) ~/Deepspeech/examples/vad_transcriber$ pip3 install -r requirements.txt Command line tool ----------------- The command line tool processes a wav file of any duration and returns a trancript which will the saved in the same directory as the input audio file. (venv) ~/Deepspeech/examples/vad_transcriber $ python3 audioTranscript_cmd.py --aggressive 1 --audio ./audio/guido-van-rossum.wav --model ./models/0.2.0/ Minimalistic GUI ---------------- The GUI tool does the same job as the CLI tool. The VAD is fixed at an aggressiveness of 1. The output is displayed in the transcription window and saved into the directory as the input audio file as well. (venv) ~/Deepspeech/examples/vad_transcriber $ python3 audioTranscript_gui.py Changes(v1): 1. Using Deepspeech python module instead of subprocess 2. Moved VAD code to a module 3. Moved all files to bin/ and renamed README.md to Audio_Transcription.md Changes(v2): Renamed files Changes (v2.1): 1. Refactoring between CMD and GUI code 2. Documenting pre-requisites with a virtualenv 3. Loading model only once per long wav file 4. CMD and GUI tool do the same job, perform VAD and consolidate the output. 5. Chunks are not saved in the disk. Using a numpy interger array to store them. Changes (v2.2): 1. Argparse module for command line arguments 2. Everything in virtualenv, with a requirements.txt 3. Older APIs aligned with 0.2.0 release 4. Moved all files into examples/vad_transcriber Changes (v2.3) 1. Updated requirements.txt
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
import collections
|
|
import contextlib
|
|
import wave
|
|
|
|
|
|
def read_wave(path):
|
|
"""Reads a .wav file.
|
|
|
|
Takes the path, and returns (PCM audio data, sample rate).
|
|
"""
|
|
with contextlib.closing(wave.open(path, 'rb')) as wf:
|
|
num_channels = wf.getnchannels()
|
|
assert num_channels == 1
|
|
sample_width = wf.getsampwidth()
|
|
assert sample_width == 2
|
|
sample_rate = wf.getframerate()
|
|
assert sample_rate in (8000, 16000, 32000)
|
|
frames = wf.getnframes()
|
|
pcm_data = wf.readframes(frames)
|
|
duration = frames / sample_rate
|
|
return pcm_data, sample_rate, duration
|
|
|
|
|
|
def write_wave(path, audio, sample_rate):
|
|
"""Writes a .wav file.
|
|
|
|
Takes path, PCM audio data, and sample rate.
|
|
"""
|
|
with contextlib.closing(wave.open(path, 'wb')) as wf:
|
|
wf.setnchannels(1)
|
|
wf.setsampwidth(2)
|
|
wf.setframerate(sample_rate)
|
|
wf.writeframes(audio)
|
|
|
|
|
|
class Frame(object):
|
|
"""Represents a "frame" of audio data."""
|
|
def __init__(self, bytes, timestamp, duration):
|
|
self.bytes = bytes
|
|
self.timestamp = timestamp
|
|
self.duration = duration
|
|
|
|
|
|
def frame_generator(frame_duration_ms, audio, sample_rate):
|
|
"""Generates audio frames from PCM audio data.
|
|
|
|
Takes the desired frame duration in milliseconds, the PCM data, and
|
|
the sample rate.
|
|
|
|
Yields Frames of the requested duration.
|
|
"""
|
|
n = int(sample_rate * (frame_duration_ms / 1000.0) * 2)
|
|
offset = 0
|
|
timestamp = 0.0
|
|
duration = (float(n) / sample_rate) / 2.0
|
|
while offset + n < len(audio):
|
|
yield Frame(audio[offset:offset + n], timestamp, duration)
|
|
timestamp += duration
|
|
offset += n
|
|
|
|
|
|
def vad_collector(sample_rate, frame_duration_ms,
|
|
padding_duration_ms, vad, frames):
|
|
"""Filters out non-voiced audio frames.
|
|
|
|
Given a webrtcvad.Vad and a source of audio frames, yields only
|
|
the voiced audio.
|
|
|
|
Uses a padded, sliding window algorithm over the audio frames.
|
|
When more than 90% of the frames in the window are voiced (as
|
|
reported by the VAD), the collector triggers and begins yielding
|
|
audio frames. Then the collector waits until 90% of the frames in
|
|
the window are unvoiced to detrigger.
|
|
|
|
The window is padded at the front and back to provide a small
|
|
amount of silence or the beginnings/endings of speech around the
|
|
voiced frames.
|
|
|
|
Arguments:
|
|
|
|
sample_rate - The audio sample rate, in Hz.
|
|
frame_duration_ms - The frame duration in milliseconds.
|
|
padding_duration_ms - The amount to pad the window, in milliseconds.
|
|
vad - An instance of webrtcvad.Vad.
|
|
frames - a source of audio frames (sequence or generator).
|
|
|
|
Returns: A generator that yields PCM audio data.
|
|
"""
|
|
num_padding_frames = int(padding_duration_ms / frame_duration_ms)
|
|
# We use a deque for our sliding window/ring buffer.
|
|
ring_buffer = collections.deque(maxlen=num_padding_frames)
|
|
# We have two states: TRIGGERED and NOTTRIGGERED. We start in the
|
|
# NOTTRIGGERED state.
|
|
triggered = False
|
|
|
|
voiced_frames = []
|
|
for frame in frames:
|
|
is_speech = vad.is_speech(frame.bytes, sample_rate)
|
|
|
|
if not triggered:
|
|
ring_buffer.append((frame, is_speech))
|
|
num_voiced = len([f for f, speech in ring_buffer if speech])
|
|
# If we're NOTTRIGGERED and more than 90% of the frames in
|
|
# the ring buffer are voiced frames, then enter the
|
|
# TRIGGERED state.
|
|
if num_voiced > 0.9 * ring_buffer.maxlen:
|
|
triggered = True
|
|
# We want to yield all the audio we see from now until
|
|
# we are NOTTRIGGERED, but we have to start with the
|
|
# audio that's already in the ring buffer.
|
|
for f, s in ring_buffer:
|
|
voiced_frames.append(f)
|
|
ring_buffer.clear()
|
|
else:
|
|
# We're in the TRIGGERED state, so collect the audio data
|
|
# and add it to the ring buffer.
|
|
voiced_frames.append(frame)
|
|
ring_buffer.append((frame, is_speech))
|
|
num_unvoiced = len([f for f, speech in ring_buffer if not speech])
|
|
# If more than 90% of the frames in the ring buffer are
|
|
# unvoiced, then enter NOTTRIGGERED and yield whatever
|
|
# audio we've collected.
|
|
if num_unvoiced > 0.9 * ring_buffer.maxlen:
|
|
triggered = False
|
|
yield b''.join([f.bytes for f in voiced_frames])
|
|
ring_buffer.clear()
|
|
voiced_frames = []
|
|
if triggered:
|
|
pass
|
|
# If we have any leftover voiced audio when we run out of input,
|
|
# yield it.
|
|
if voiced_frames:
|
|
yield b''.join([f.bytes for f in voiced_frames])
|
|
|