erik-at-boson commited on
Commit
c658265
·
verified ·
1 Parent(s): c55f5f6

fix: dual-mode imports + complete ChatMLDatasetSample + transcribe output parsing

Browse files
Files changed (1) hide show
  1. transcribe.py +147 -0
transcribe.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone transcription helper for Higgs Audio v3 models.
2
+
3
+ Bundles all preprocessing needed to run inference without boson_multimodal.
4
+
5
+ Usage:
6
+ from transcribe import transcribe
7
+
8
+ model = AutoModel.from_pretrained("bosonai/higgs-audio-v3-8b-stt", ...)
9
+ tokenizer = AutoTokenizer.from_pretrained("bosonai/higgs-audio-v3-8b-stt")
10
+
11
+ text = transcribe(model, tokenizer, audio_np, sample_rate=16000)
12
+ """
13
+
14
+ import torch
15
+ import numpy as np
16
+ from dataclasses import dataclass, asdict, field
17
+ from typing import List, Optional
18
+ from transformers import AutoConfig, WhisperProcessor
19
+
20
+ try:
21
+ from .higgs_audio_collator import HiggsAudioSampleCollator, ChatMLDatasetSample
22
+ except ImportError:
23
+ from higgs_audio_collator import HiggsAudioSampleCollator, ChatMLDatasetSample
24
+
25
+
26
+ def _build_input_tokens(tokenizer, user_prompt, enable_thinking=True):
27
+ """Build input token sequence for transcription."""
28
+ # Construct ChatML format manually
29
+ audio_token = "<|audio_in_bos|><|AUDIO|><|audio_in_eos|>"
30
+
31
+ messages = [
32
+ {"role": "user", "content": f"{user_prompt}\n{audio_token}"}
33
+ ]
34
+
35
+ text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
36
+ if enable_thinking:
37
+ text += "<think>\n"
38
+
39
+ input_ids = tokenizer.encode(text, add_special_tokens=False)
40
+ return input_ids
41
+
42
+
43
+ def _create_collator(config):
44
+ """Create audio collator from model config."""
45
+ whisper_proc = WhisperProcessor.from_pretrained("openai/whisper-large-v3")
46
+
47
+ return HiggsAudioSampleCollator(
48
+ whisper_processor=whisper_proc,
49
+ audio_in_token_id=config.audio_in_token_idx,
50
+ audio_out_token_id=config.audio_out_token_idx,
51
+ audio_stream_bos_id=config.audio_stream_bos_id,
52
+ audio_stream_eos_id=config.audio_stream_eos_id,
53
+ encode_whisper_embed=config.encode_whisper_embed,
54
+ pad_token_id=config.pad_token_id,
55
+ return_audio_in_tokens=config.encode_audio_in_tokens,
56
+ use_delay_pattern=config.use_delay_pattern,
57
+ round_to=1,
58
+ audio_num_codebooks=config.audio_num_codebooks,
59
+ chunk_size_seconds=getattr(config, "chunk_size_seconds", 30),
60
+ encoder_padding_method=getattr(config, "encoder_padding_method", "max_length"),
61
+ )
62
+
63
+
64
+ _cached_collator = None
65
+
66
+
67
+ def transcribe(model, tokenizer, audio_np, sample_rate=16000,
68
+ user_prompt="Transcribe the speech. Output only the spoken words in lowercase with no punctuation.",
69
+ enable_thinking=True, max_new_tokens=1024):
70
+ """Transcribe audio using a Higgs Audio model.
71
+
72
+ Args:
73
+ model: loaded HiggsAudio3Model
74
+ tokenizer: loaded tokenizer
75
+ audio_np: numpy array of audio samples (float32, mono)
76
+ sample_rate: audio sample rate (default 16000)
77
+ user_prompt: transcription prompt
78
+ enable_thinking: use chain-of-thought (recommended)
79
+ max_new_tokens: max generation length
80
+
81
+ Returns:
82
+ str: transcribed text
83
+ """
84
+ global _cached_collator
85
+
86
+ device = next(model.parameters()).device
87
+ config = model.config
88
+
89
+ # Resample if needed
90
+ if sample_rate != 16000:
91
+ import torchaudio
92
+ audio_tensor = torch.tensor(audio_np).unsqueeze(0)
93
+ audio_tensor = torchaudio.functional.resample(audio_tensor, sample_rate, 16000)
94
+ audio_np = audio_tensor.squeeze(0).numpy()
95
+
96
+ audio_np = np.array(audio_np, dtype=np.float32)
97
+
98
+ # Build input tokens
99
+ input_ids = _build_input_tokens(tokenizer, user_prompt, enable_thinking)
100
+
101
+ # Create sample
102
+ sample = ChatMLDatasetSample(
103
+ input_ids=torch.LongTensor(input_ids),
104
+ label_ids=torch.LongTensor([-100] * len(input_ids)),
105
+ audio_waveforms_concat=torch.tensor(audio_np, dtype=torch.float32),
106
+ audio_waveforms_start=torch.tensor([0]),
107
+ audio_sample_rate=torch.tensor([sample_rate]),
108
+ audio_speaker_indices=torch.tensor([0]),
109
+ )
110
+
111
+ # Collate
112
+ if _cached_collator is None:
113
+ _cached_collator = _create_collator(config)
114
+
115
+ batch = asdict(_cached_collator([sample]))
116
+ batch = {k: v.to(device).contiguous() if isinstance(v, torch.Tensor) else v
117
+ for k, v in batch.items()}
118
+
119
+ # Generate
120
+ import re
121
+ with torch.inference_mode():
122
+ outputs = model.generate(
123
+ **batch,
124
+ max_new_tokens=max_new_tokens,
125
+ use_cache=True,
126
+ do_sample=False,
127
+ stop_strings=["<|im_end|>", "<|endoftext|>"],
128
+ tokenizer=tokenizer,
129
+ )
130
+
131
+ output_ids = outputs[0] if isinstance(outputs, tuple) else outputs
132
+ full_text = tokenizer.decode(output_ids[0], skip_special_tokens=False)
133
+
134
+ # Extract assistant response
135
+ parts = full_text.split("assistant\n")
136
+ hyp = parts[-1] if len(parts) > 1 else full_text
137
+
138
+ # Remove complete <think>...</think> blocks (keep text outside)
139
+ hyp = re.sub(r"<think>.*?</think>", "", hyp, flags=re.DOTALL)
140
+
141
+ # Handle open <think> without </think> — the transcription IS the think content
142
+ if "<think>" in hyp:
143
+ hyp = hyp[hyp.index("<think>") + len("<think>"):]
144
+
145
+ # Remove special tokens
146
+ hyp = re.sub(r"<\|.*?\|>", "", hyp)
147
+ return hyp.strip()