CoolFace
Apppublic

Ari123er/Ai_Speech_Synthesis

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
caption_generator.py176 linesDownload Raw Back to root
1"""
2Caption Generator — Vosk Speech-to-Text
3========================================
4Generates word-level captions (JSON) from audio chunks using Vosk.
5Optimized for HuggingFace Free Tier (16GB RAM, 2 vCPU).
6
7Usage (imported by app.py):
8    from caption_generator import generate_captions
9    captions = generate_captions(trimmed_chunks, sr)
10"""
11
12import os
13import gc
14import json
15import wave
16import struct
17import tempfile
18import numpy as np
19
20# ═══════════════════════════════════════════════════════════════════════════
21# CONFIGURATION
22# ═══════════════════════════════════════════════════════════════════════════
23
24# Vosk model — smallest English model (~40MB download, ~300MB RAM)
25VOSK_MODEL_NAME = "vosk-model-small-en-us-0.15"
26VOSK_MODEL_DIR = os.path.join(tempfile.gettempdir(), "vosk-models")
27VOSK_SR = 16000  # Vosk requires 16kHz mono 16-bit PCM
28
29# Singleton model instance
30_vosk_model = None
31
32
33# ═══════════════════════════════════════════════════════════════════════════
34# MODEL LOADING
35# ═══════════════════════════════════════════════════════════════════════════
36
37def _load_vosk_model():
38    """
39    Lazy-load Vosk model. Downloads on first call, caches on disk.
40    Returns the Model instance.
41    """
42    global _vosk_model
43    if _vosk_model is not None:
44        return _vosk_model
45
46    from vosk import Model, SetLogLevel
47
48    # Suppress Vosk's verbose logging
49    SetLogLevel(-1)
50
51    model_path = os.path.join(VOSK_MODEL_DIR, VOSK_MODEL_NAME)
52
53    if not os.path.isdir(model_path):
54        print(f"[VOSK] Downloading model '{VOSK_MODEL_NAME}'...")
55        os.makedirs(VOSK_MODEL_DIR, exist_ok=True)
56
57        # vosk.Model auto-downloads if given just the model name
58        _vosk_model = Model(model_name=VOSK_MODEL_NAME)
59        print(f"[VOSK] Model downloaded and loaded.")
60    else:
61        print(f"[VOSK] Loading cached model from {model_path}")
62        _vosk_model = Model(model_path=model_path)
63        print(f"[VOSK] Model loaded.")
64
65    return _vosk_model
66
67
68# ═══════════════════════════════════════════════════════════════════════════
69# AUDIO CONVERSION
70# ═══════════════════════════════════════════════════════════════════════════
71
72def _audio_to_pcm16(audio: np.ndarray, sr: int) -> bytes:
73    """
74    Convert float32 numpy audio to 16kHz mono 16-bit PCM bytes.
75    Resamples if sr != 16000. This is what Vosk expects.
76    """
77    # Resample to 16kHz if needed
78    if sr != VOSK_SR:
79        import librosa
80        audio = librosa.resample(audio, orig_sr=sr, target_sr=VOSK_SR)
81
82    # Ensure mono
83    if audio.ndim > 1:
84        audio = np.mean(audio, axis=1)
85
86    # Clip and convert float32 → int16
87    audio = np.clip(audio, -1.0, 1.0)
88    pcm_data = (audio * 32767).astype(np.int16)
89
90    return pcm_data.tobytes()
91
92
93# ═══════════════════════════════════════════════════════════════════════════
94# CAPTION GENERATION
95# ═══════════════════════════════════════════════════════════════════════════
96
97def generate_captions(trimmed_chunks: list, sr: int) -> list:
98    """
99    Generate word-level captions from trimmed audio chunks using Vosk.
100
101    Args:
102        trimmed_chunks: List of dicts with keys:
103            - 'audio': np.ndarray (trimmed float32 audio at original sr)
104            - 'text':  str (original script text for this chunk)
105            - 'tag':   str (style tag, e.g. 'SHOCK', 'FAST')
106        sr: Sample rate of the audio (e.g. 24000)
107
108    Returns:
109        Flat list of word dicts:
110        [
111            {"word": "Dragon", "start": 0.0, "end": 0.32, "conf": 0.98},
112            {"word": "Ball", "start": 0.32, "end": 0.55, "conf": 0.99},
113            ...
114        ]
115    """
116    from vosk import KaldiRecognizer
117
118    model = _load_vosk_model()
119
120    print(f"\n{'='*60}")
121    print(f"  CAPTION GENERATOR — Vosk STT")
122    print(f"{'='*60}")
123
124    all_words = []
125    time_offset = 0.0  # Running offset for multi-chunk positioning
126
127    for i, chunk in enumerate(trimmed_chunks):
128        audio = chunk['audio']
129        script_text = chunk['text']
130        tag = chunk['tag']
131
132        chunk_duration = len(audio) / sr
133        print(f"  ▸ [{i+1}/{len(trimmed_chunks)}] [{tag}] {chunk_duration:.2f}s — \"{script_text[:50]}...\"")
134
135        # Convert to 16kHz PCM
136        pcm_bytes = _audio_to_pcm16(audio, sr)
137
138        # Create recognizer with word-level timestamps
139        rec = KaldiRecognizer(model, VOSK_SR)
140        rec.SetWords(True)
141
142        # Feed audio in small chunks (4000 bytes ≈ 125ms at 16kHz 16-bit)
143        FEED_SIZE = 4000
144        for pos in range(0, len(pcm_bytes), FEED_SIZE):
145            rec.AcceptWaveform(pcm_bytes[pos:pos + FEED_SIZE])
146
147        # Get final result
148        result = json.loads(rec.FinalResult())
149
150        # Extract words with offset-adjusted timestamps
151        if 'result' in result:
152            for w in result['result']:
153                all_words.append({
154                    "word": w['word'],
155                    "start": round(w['start'] + time_offset, 3),
156                    "end": round(w['end'] + time_offset, 3),
157                    "conf": round(w.get('conf', 0.0), 3)
158                })
159
160        time_offset += chunk_duration
161
162        # Free recognizer memory
163        del rec, pcm_bytes
164        gc.collect()
165
166        print(f"    → {len(all_words)} total words so far")
167
168    print(f"{'='*60}")
169    print(f"  DONE — {len(all_words)} total words across {len(trimmed_chunks)} chunk(s)")
170    print(f"{'='*60}\n")
171
172    return all_words
173"""
174Standalone module for Vosk STT captions.
175Uses the smallest English model for efficient CPU inference.
176"""