Files
bornetime/backend/app/services/stt.py
T
oval 6851b26923 Initial commit: Børnetime app scaffold
FastAPI + Celery + Svelte app for English→Danish video translation
with voice cloning. Includes diarization, STT, translation, TTS,
and audio mixing pipeline.
2026-07-10 22:42:23 +02:00

142 lines
4.7 KiB
Python

import torch
import numpy as np
import soundfile as sf
import librosa
import re
import logging
from functools import partial
from dataclasses import asdict
from typing import Optional
from transformers import (
AutoConfig,
AutoModel,
AutoTokenizer,
WhisperProcessor,
)
from boson_multimodal.data_collator.higgs_audio_collator import HiggsAudioSampleCollator
from boson_multimodal.data_types import ChatMLSample, AudioContent, Message
from boson_multimodal.dataset.chatml_dataset import (
ChatMLDatasetSample,
prepare_chatml_sample_qwen,
)
from ..config import settings
logger = logging.getLogger(__name__)
_model: Optional[AutoModel] = None
_tokenizer: Optional[AutoTokenizer] = None
_collator: Optional[HiggsAudioSampleCollator] = None
_config: Optional[AutoConfig] = None
def _load_model():
global _model, _tokenizer, _collator, _config
if _model is not None:
return
logger.info("Loading Higgs STT on %s", settings.stt_device)
dtype = torch.bfloat16 if settings.stt_dtype == "bfloat16" else torch.float16
_config = AutoConfig.from_pretrained(settings.stt_model_id, trust_remote_code=True)
_model = AutoModel.from_pretrained(
settings.stt_model_id,
torch_dtype=dtype,
trust_remote_code=True,
attn_implementation="eager",
device_map=settings.stt_device,
)
_model.eval()
_tokenizer = AutoTokenizer.from_pretrained(settings.stt_model_id)
_model.audio_out_bos_token_id = _tokenizer.convert_tokens_to_ids(
"<|audio_out_bos|>"
)
_model.audio_eos_token_id = _tokenizer.convert_tokens_to_ids("<|audio_eos|>")
whisper_proc = WhisperProcessor.from_pretrained("openai/whisper-large-v3")
_collator = HiggsAudioSampleCollator(
whisper_processor=whisper_proc,
audio_in_token_id=_config.audio_in_token_idx,
audio_out_token_id=_config.audio_out_token_idx,
audio_stream_bos_id=_config.audio_stream_bos_id,
audio_stream_eos_id=_config.audio_stream_eos_id,
encode_whisper_embed=_config.encode_whisper_embed,
pad_token_id=_config.pad_token_id,
return_audio_in_tokens=_config.encode_audio_in_tokens,
use_delay_pattern=_config.use_delay_pattern,
round_to=1,
audio_num_codebooks=_config.audio_num_codebooks,
chunk_size_seconds=getattr(_config, "chunk_size_seconds", 30),
encoder_padding_method=getattr(_config, "encoder_padding_method", "max_length"),
)
def transcribe_segment(audio_path: str, start_time: float, end_time: float) -> str:
_load_model()
audio_np, sr = sf.read(audio_path)
if sr != 16000:
audio_np = librosa.resample(audio_np, orig_sr=sr, target_sr=16000)
start_sample = int(start_time * 16000)
end_sample = int(end_time * 16000)
segment_audio = audio_np[start_sample:end_sample]
if len(segment_audio) == 0:
return ""
prompt = "Transcribe the speech. Output only the spoken words in lowercase with no punctuation."
messages = [
Message(role="user", content=[prompt, AudioContent(audio_url="placeholder")])
]
chatml = ChatMLSample(messages=messages)
prep_fn = partial(prepare_chatml_sample_qwen, enable_thinking=True)
input_tokens, _, _, _ = prep_fn(chatml, _tokenizer, add_generation_prompt=True)
sample = ChatMLDatasetSample(
input_ids=torch.LongTensor(input_tokens),
label_ids=None,
audio_ids_concat=None,
audio_ids_start=None,
audio_waveforms_concat=torch.tensor(segment_audio, dtype=torch.float32),
audio_waveforms_start=torch.tensor([0]),
audio_sample_rate=torch.tensor([16000]),
audio_speaker_indices=torch.tensor([0]),
)
batch = asdict(_collator([sample]))
device = next(_model.parameters()).device
batch = {
k: v.to(device).contiguous() if isinstance(v, torch.Tensor) else v
for k, v in batch.items()
}
with torch.inference_mode():
outputs = _model.generate(
**batch,
max_new_tokens=1024,
use_cache=True,
do_sample=False,
stop_strings=["<|im_end|>", "<|endoftext|>"],
tokenizer=_tokenizer,
)
output_ids = outputs[0] if isinstance(outputs, tuple) else outputs
full_text = _tokenizer.decode(output_ids[0], skip_special_tokens=False)
parts = full_text.split("assistant\n")
hyp = parts[-1] if len(parts) > 1 else full_text
hyp = re.sub(r"<think>.*?</think>", "", hyp, flags=re.DOTALL)
hyp = re.sub(r"<\|.*?\|>", "", hyp).strip()
return hyp
def unload_model():
global _model, _tokenizer, _collator, _config
_model = None
_tokenizer = None
_collator = None
_config = None
torch.cuda.empty_cache()