add examples/whisper.youtube: YouTube → speech-to-text pipeline
- yt_transcribe.py: yt-dlp + ffmpeg + whisper-cli (large-v3) wrapper - parakeet_transcribe.py: alternative NVIDIA Parakeet ASR backend - parakeet: shell launcher for parakeet via local venv - transcriptions/q1CluXct_VI.txt: sample output (8h audiobook) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8452ea1650
commit
7a4ff7ec36
|
|
@ -0,0 +1,4 @@
|
|||
videos/
|
||||
.venv-parakeet/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
# Parakeet V3 transcription launcher.
|
||||
# Usage:
|
||||
# ./parakeet <wav-file>
|
||||
# ./parakeet <youtube-url>
|
||||
set -euo pipefail
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$DIR/.venv-parakeet/bin/python" "$DIR/parakeet_transcribe.py" "$@"
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Transcribe audio with NVIDIA Parakeet-tdt-0.6b-v3 (multilingual)."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
MODEL_NAME = "nvidia/parakeet-tdt-0.6b-v3"
|
||||
|
||||
|
||||
def download_audio(url: str, out_path: str):
|
||||
subprocess.run([
|
||||
"yt-dlp", "-x", "--audio-format", "wav",
|
||||
"-o", out_path, url,
|
||||
], check=True)
|
||||
|
||||
|
||||
def convert_wav(src: str, dst: str):
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-i", src,
|
||||
"-ar", "16000", "-ac", "1", "-f", "wav", dst,
|
||||
], check=True, capture_output=True)
|
||||
|
||||
|
||||
def transcribe(wav_path: str, long_audio: bool = True) -> str:
|
||||
import nemo.collections.asr as nemo_asr
|
||||
|
||||
print(f"Loading {MODEL_NAME} ...", flush=True)
|
||||
asr = nemo_asr.models.ASRModel.from_pretrained(MODEL_NAME)
|
||||
asr = asr.cuda() if __import__("torch").cuda.is_available() else asr
|
||||
|
||||
if long_audio:
|
||||
asr.change_attention_model(
|
||||
self_attention_model="rel_pos_local_attn",
|
||||
att_context_size=[256, 256],
|
||||
)
|
||||
|
||||
print(f"Transcribing {wav_path} ...", flush=True)
|
||||
out = asr.transcribe([wav_path], timestamps=False)
|
||||
return out[0].text
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <wav-file-or-youtube-url>")
|
||||
sys.exit(1)
|
||||
|
||||
src = sys.argv[1]
|
||||
|
||||
if src.startswith(("http://", "https://", "youtu", "www.")):
|
||||
with tempfile.TemporaryDirectory(prefix="parakeet-") as tmp:
|
||||
raw = os.path.join(tmp, "audio.%(ext)s")
|
||||
wav = os.path.join(tmp, "audio_16k.wav")
|
||||
print(f"Downloading {src} ...", flush=True)
|
||||
download_audio(src, raw)
|
||||
downloaded = [f for f in os.listdir(tmp) if f.startswith("audio.")]
|
||||
print("Converting to 16kHz WAV ...", flush=True)
|
||||
convert_wav(os.path.join(tmp, downloaded[0]), wav)
|
||||
text = transcribe(wav)
|
||||
else:
|
||||
text = transcribe(src)
|
||||
|
||||
print("\n=== Transcription ===")
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Download audio from YouTube and transcribe with local whisper-cli."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
WHISPER_CLI = REPO_ROOT / "build" / "bin" / "whisper-cli"
|
||||
MODEL = REPO_ROOT / "models" / "ggml-large-v3.bin"
|
||||
|
||||
|
||||
def download_audio(url: str, out_path: str):
|
||||
"""Download audio from YouTube URL using yt-dlp."""
|
||||
subprocess.run([
|
||||
"yt-dlp", "-x", "--audio-format", "wav",
|
||||
"-o", out_path, url,
|
||||
], check=True)
|
||||
|
||||
|
||||
def convert_wav(src: str, dst: str):
|
||||
"""Convert audio to 16kHz mono WAV for whisper."""
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-i", src,
|
||||
"-ar", "16000", "-ac", "1", "-f", "wav", dst,
|
||||
], check=True, capture_output=True)
|
||||
|
||||
|
||||
def transcribe(wav_path: str) -> str:
|
||||
"""Run whisper-cli on a WAV file and return text."""
|
||||
result = subprocess.run([
|
||||
str(WHISPER_CLI), "-m", str(MODEL),
|
||||
"-f", wav_path, "-nt", "-np", "-l", "auto",
|
||||
], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"whisper-cli error: {result.stderr[:500]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return result.stdout.strip().replace("[BLANK_AUDIO]", "").strip()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <youtube-url>")
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
|
||||
if not WHISPER_CLI.exists():
|
||||
print(f"whisper-cli not found: {WHISPER_CLI}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not MODEL.exists():
|
||||
print(f"Model not found: {MODEL}\nRun: bash models/download-ggml-model.sh base", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="yt-whisper-") as tmpdir:
|
||||
raw_audio = os.path.join(tmpdir, "audio.%(ext)s")
|
||||
wav_16k = os.path.join(tmpdir, "audio_16k.wav")
|
||||
|
||||
print(f"Downloading audio from {url} ...")
|
||||
download_audio(url, raw_audio)
|
||||
|
||||
# find the downloaded file (yt-dlp replaces %(ext)s)
|
||||
downloaded = [f for f in os.listdir(tmpdir) if f.startswith("audio.")]
|
||||
if not downloaded:
|
||||
print("Download failed: no audio file found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raw_path = os.path.join(tmpdir, downloaded[0])
|
||||
|
||||
print("Converting to 16kHz WAV ...")
|
||||
convert_wav(raw_path, wav_16k)
|
||||
|
||||
print("Transcribing ...")
|
||||
text = transcribe(wav_16k)
|
||||
|
||||
print("\n=== Transcription ===")
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue