File size: 6,522 Bytes
c8672f7
487ed33
 
 
 
 
 
7f74ea0
487ed33
 
 
 
 
 
 
 
f4c725a
8488f1e
 
 
 
 
 
 
 
 
 
 
 
 
 
487ed33
 
 
f4c725a
487ed33
 
 
 
f4c725a
487ed33
 
f4c725a
487ed33
 
 
 
 
 
f4c725a
487ed33
 
 
 
 
 
 
 
 
 
 
 
 
 
f4c725a
 
7f74ea0
487ed33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f74ea0
 
 
f4c725a
487ed33
 
f4c725a
487ed33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4c725a
487ed33
 
 
f4c725a
487ed33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f56a57
487ed33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import spaces
from typing import Optional
import logging
import time
import threading

import torch
import librosa
from transformers import pipeline, AutoProcessor, AutoModelForCausalLM, Pipeline
from accelerate import Accelerator

# Set up logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

try:
    import subprocess

    subprocess.run(
        "pip install flash-attn --no-build-isolation",
        env={"FLASH_ATTENTION_SKIP_CUDA_BUILD": "TRUE"},
        shell=True,
    )
    logger.info("Flash Attention installed successfully.")
    USE_FA = True
except:
    USE_FA = False
    logger.warning("Flash Attention not available. Using standard attention instead.")

# Model constants
MODEL_ID = "JacobLinCool/whisper-large-v3-turbo-common_voice_19_0-zh-TW"
PHI_MODEL_ID = "JacobLinCool/Phi-4-multimodal-instruct-commonvoice-zh-tw"

# Model instances (initialized lazily)
pipe: Optional[Pipeline] = None
phi_model = None
phi_processor = None

# Lock for thread-safe model loading
model_loading_lock = threading.Lock()


def load_model() -> None:
    """
    Load the Whisper model for transcription.
    Uses GPU if available.
    """
    global pipe
    if pipe is not None:
        return  # Model already loaded

    try:
        start_time = time.time()
        logger.info(f"Loading Whisper model {MODEL_ID}...")
        device = Accelerator().device
        pipe = pipeline("automatic-speech-recognition", model=MODEL_ID, device=device)
        logger.info(
            f"Model loaded successfully in {time.time() - start_time:.2f} seconds"
        )
    except Exception as e:
        logger.error(f"Failed to load Whisper model: {str(e)}")
        raise


def get_gpu_duration(audio: str) -> int:
    """
    Calculate required GPU allocation time based on audio duration.

    Args:
        audio: Path to audio file

    Returns:
        GPU allocation time in seconds
    """
    try:
        y, sr = librosa.load(audio)
        duration = librosa.get_duration(y=y, sr=sr) / 60.0
        gpu_duration = max(1.0, (duration + 59.0) // 60.0) * 60.0
        logger.info(
            f"Audio duration: {duration:.2f} min, Allocated GPU time: {gpu_duration:.2f} min"
        )
        return int(gpu_duration)
    except Exception as e:
        logger.error(f"Failed to calculate GPU duration: {str(e)}")
        return 60  # Default to 1 minute if calculation fails


@spaces.GPU(duration=get_gpu_duration)
def transcribe_audio_local(audio: str) -> str:
    """
    Transcribe audio using the Whisper model.

    Args:
        audio: Path to audio file

    Returns:
        Transcribed text
    """
    try:
        logger.info(f"Transcribing audio with Whisper: {audio}")
        if pipe is None:
            load_model()

        out = pipe(audio, return_timestamps=True)
        return out.get("text", "No transcription generated")
    except Exception as e:
        logger.error(f"Whisper transcription error: {str(e)}")
        raise


def load_phi_model() -> None:
    """
    Load the Phi-4 model and processor.
    Uses GPU with Flash Attention if available.
    """
    global phi_model, phi_processor
    if phi_model is not None and phi_processor is not None:
        return  # Model already loaded

    try:
        start_time = time.time()
        logger.info(f"Loading Phi-4 model {PHI_MODEL_ID}...")

        phi_processor = AutoProcessor.from_pretrained(
            PHI_MODEL_ID, trust_remote_code=True
        )

        device = "cuda" if torch.cuda.is_available() else "cpu"
        dtype = torch.bfloat16 if USE_FA else torch.float32
        attn_implementation = "flash_attention_2" if USE_FA else "sdpa"

        phi_model = AutoModelForCausalLM.from_pretrained(
            PHI_MODEL_ID,
            torch_dtype=dtype,
            _attn_implementation=attn_implementation,
            trust_remote_code=True,
        ).to(device)

        logger.info(
            f"Phi-4 model loaded successfully in {time.time() - start_time:.2f} seconds"
        )
    except Exception as e:
        logger.error(f"Failed to load Phi-4 model: {str(e)}")
        raise


@spaces.GPU(duration=get_gpu_duration)
def transcribe_audio_phi(audio: str) -> str:
    """
    Transcribe audio using the Phi-4 model.

    Args:
        audio: Path to audio file

    Returns:
        Transcribed text
    """
    try:
        logger.info(f"Transcribing audio with Phi-4: {audio}")
        load_phi_model()

        # Load and resample audio to 16kHz
        y, sr = librosa.load(audio, sr=16000)

        # Prepare the user message and generate the prompt
        user_message = {
            "role": "user",
            "content": "<|audio_1|> Transcribe the audio clip into text.",
        }
        prompt = phi_processor.tokenizer.apply_chat_template(
            [user_message], tokenize=False, add_generation_prompt=True
        )

        # Build inputs for the model
        inputs = phi_processor(text=prompt, audios=[(y, sr)], return_tensors="pt")
        inputs = {
            k: v.to(phi_model.device) if hasattr(v, "to") else v
            for k, v in inputs.items()
        }

        # Generate transcription without gradients
        with torch.no_grad():
            generated_ids = phi_model.generate(
                **inputs,
                eos_token_id=phi_processor.tokenizer.eos_token_id,
                max_new_tokens=256,  # Increased for longer transcriptions
                do_sample=False,
            )

        # Decode the generated token IDs into text
        transcription = phi_processor.decode(
            generated_ids[0, inputs["input_ids"].shape[1] :],
            skip_special_tokens=True,
            clean_up_tokenization_spaces=False,
        )

        logger.info(f"Phi-4 transcription completed successfully")
        return transcription
    except Exception as e:
        logger.error(f"Phi-4 transcription error: {str(e)}")
        raise


def preload_models() -> None:
    """
    Preload models into memory to reduce cold start time.
    This function can be called at application startup.
    """
    try:
        logger.info("Preloading models to reduce cold start time")
        # Load Whisper model first as it's the default
        load_model()
        # Then load Phi model
        load_phi_model()
        logger.info("All models preloaded successfully")
    except Exception as e:
        logger.error(f"Error during model preloading: {str(e)}")