TwiSpeechModel / src /huggingface_model_adapter.py
ProvLeon's picture
Upload folder using huggingface_hub
714b1d5 verified
Raw
History Blame Contribute Delete
27.5 kB
#!/usr/bin/env python3
"""
HuggingFace Model Adapter for Twi Speech Recognition
==================================================
This module provides an adapter layer to seamlessly work with both
single-task (transcription only) and multi-task (transcription + intent)
models from HuggingFace Hub.
Key Features:
1. Automatic model type detection
2. Unified interface for both model types
3. Dynamic loading and configuration
4. Fallback mechanisms for missing components
5. Performance optimization for HF models
Author: AI Assistant
Date: 2025-11-11
"""
import json
import logging
import os
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import numpy as np
import torch
from transformers import (
AutoConfig,
AutoModelForSpeechSeq2Seq,
AutoProcessor,
AutoTokenizer,
WhisperForConditionalGeneration,
WhisperProcessor,
pipeline,
)
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class HuggingFaceModelAdapter:
"""Adapter for HuggingFace models supporting both single and multi-task configurations."""
def __init__(self, model_path: str, device: str = "auto"):
"""
Initialize the HuggingFace model adapter.
Args:
model_path: Path to the HuggingFace model directory
device: Device to use ('auto', 'cpu', 'cuda')
"""
self.model_path = Path(model_path)
self.device = self._get_device(device)
self.model_type = None # 'single' or 'multi'
# Model components
self.transcription_model = None
self.processor = None
self.tokenizer = None
self.intent_classifier = None
# Configuration
self.config = None
self.intent_labels = {}
self.supported_languages = ["tw", "en", "auto"]
# Performance tracking
self.stats = {
"total_requests": 0,
"transcription_requests": 0,
"intent_requests": 0,
"avg_transcription_time": 0.0,
"avg_intent_time": 0.0,
}
self._initialize_model()
def _get_device(self, device: str) -> str:
"""Determine the appropriate device."""
if device == "auto":
return "cuda" if torch.cuda.is_available() else "cpu"
return device
def _initialize_model(self):
"""Initialize and load the model components."""
logger.info(f"🚀 Initializing HuggingFace model from: {self.model_path}")
try:
# Load configuration
self._load_configuration()
# Detect model type
self._detect_model_type()
# Load model components based on type
if self.model_type == "multi":
self._load_multitask_model()
else:
self._load_singletask_model()
# Apply optimizations
self._apply_optimizations()
logger.info(f"✅ Model initialized successfully ({self.model_type}-task)")
except Exception as e:
logger.error(f"❌ Model initialization failed: {e}")
raise
def _load_configuration(self):
"""Load model configuration."""
config_path = self.model_path / "config.json"
if config_path.exists():
with open(config_path, "r") as f:
self.config = json.load(f)
logger.info("✅ Configuration loaded from config.json")
else:
# Try to load using AutoConfig
try:
self.config = AutoConfig.from_pretrained(str(self.model_path))
logger.info("✅ Configuration loaded using AutoConfig")
except Exception as e:
logger.warning(f"⚠️ Could not load config: {e}")
self.config = {}
def _detect_model_type(self):
"""Detect whether this is a single-task or multi-task model."""
logger.info("🔍 Detecting model type...")
# Check for multi-task indicators in config
multi_task_indicators = [
"num_labels",
"custom_model",
"task_types",
"intent_classifier",
"classification_head",
]
config_has_multi = any(
indicator in str(self.config) for indicator in multi_task_indicators
)
# Check for multi-task files
multi_task_files = [
"intent_labels.json",
"label_map.json",
"classification_head.bin",
"pytorch_model.bin", # Custom model weights
"intent_config.json",
]
files_have_multi = any(
(self.model_path / file).exists() for file in multi_task_files
)
# Check model architecture name
model_name = self.config.get("_name_or_path", "").lower()
arch_name = self.config.get("architectures", [""])[0].lower()
arch_has_multi = any(
indicator in model_name or indicator in arch_name
for indicator in ["multitask", "intent", "classification"]
)
if config_has_multi or files_have_multi or arch_has_multi:
self.model_type = "multi"
logger.info("🎯 Detected multi-task model (transcription + intent)")
else:
self.model_type = "single"
logger.info("📝 Detected single-task model (transcription only)")
def _load_singletask_model(self):
"""Load single-task (transcription only) model."""
logger.info("📝 Loading single-task transcription model...")
try:
# Try to load as Whisper model first
try:
self.transcription_model = (
WhisperForConditionalGeneration.from_pretrained(
str(self.model_path),
torch_dtype=torch.float16
if self.device == "cuda"
else torch.float32,
)
)
self.processor = WhisperProcessor.from_pretrained(str(self.model_path))
logger.info("✅ Loaded as Whisper model")
except Exception:
# Fallback to generic speech-to-text model
self.transcription_model = AutoModelForSpeechSeq2Seq.from_pretrained(
str(self.model_path),
torch_dtype=torch.float16
if self.device == "cuda"
else torch.float32,
)
self.processor = AutoProcessor.from_pretrained(str(self.model_path))
logger.info("✅ Loaded as generic speech-to-text model")
# Move to device
self.transcription_model = self.transcription_model.to(self.device)
self.tokenizer = (
self.processor.tokenizer
if hasattr(self.processor, "tokenizer")
else AutoTokenizer.from_pretrained(str(self.model_path))
)
except Exception as e:
logger.error(f"❌ Failed to load single-task model: {e}")
raise
def _load_multitask_model(self):
"""Load multi-task (transcription + intent) model."""
logger.info("🎯 Loading multi-task model...")
try:
# Load transcription component (same as single-task)
self._load_singletask_model()
# Load intent classification component
self._load_intent_classifier()
# Load intent labels
self._load_intent_labels()
except Exception as e:
logger.error(f"❌ Failed to load multi-task model: {e}")
raise
def _load_intent_classifier(self):
"""Load intent classification component."""
try:
# Try to load as a separate classification model
classifier_path = self.model_path / "intent_classifier"
if classifier_path.exists():
# Separate intent classifier directory
self.intent_classifier = pipeline(
"text-classification",
model=str(classifier_path),
tokenizer=str(classifier_path),
device=0 if self.device == "cuda" else -1,
)
logger.info("✅ Loaded separate intent classifier")
else:
# Try to use the main model for classification
# Don't try to use Whisper model for text classification
# Whisper models are for speech-to-text, not text classification
logger.info(
"🎤 Main model is Whisper (speech-to-text), skipping text classification"
)
logger.info(
"💡 For intent classification, train a separate text classifier or use fallback"
)
self.intent_classifier = None
except Exception as e:
logger.warning(f"⚠️ Intent classifier loading failed: {e}")
self.intent_classifier = None
def _load_intent_labels(self):
"""Load intent label mappings."""
label_files = ["intent_labels.json", "label_map.json", "labels.json"]
for label_file in label_files:
label_path = self.model_path / label_file
if label_path.exists():
try:
with open(label_path, "r") as f:
self.intent_labels = json.load(f)
logger.info(f"✅ Loaded intent labels from {label_file}")
break
except Exception as e:
logger.warning(f"⚠️ Could not load {label_file}: {e}")
if not self.intent_labels:
logger.warning("⚠️ No intent labels found, using default mapping")
self.intent_labels = {"label_to_id": {}, "id_to_label": {}}
def _apply_optimizations(self):
"""Apply performance optimizations."""
logger.info("⚡ Applying performance optimizations...")
try:
if self.transcription_model and self.device == "cuda":
# Enable half precision if on GPU
try:
self.transcription_model = self.transcription_model.half()
logger.info("✅ Half precision enabled")
except Exception as e:
logger.warning(f"⚠️ Could not enable half precision: {e}")
# Enable optimized attention if available
try:
if hasattr(torch.nn.functional, "scaled_dot_product_attention"):
torch.backends.cuda.enable_flash_sdp(True)
logger.info("✅ Flash attention enabled")
except Exception:
pass
# Set to evaluation mode
if self.transcription_model:
self.transcription_model.eval()
# Compile model if PyTorch 2.0+
try:
if hasattr(torch, "compile") and self.transcription_model:
self.transcription_model = torch.compile(
self.transcription_model, mode="reduce-overhead"
)
logger.info("✅ Model compilation enabled")
except Exception as e:
logger.warning(f"⚠️ Model compilation failed: {e}")
except Exception as e:
logger.warning(f"⚠️ Optimization failed: {e}")
def transcribe(self, audio_path: str, language: str = None) -> Dict[str, Any]:
"""
Transcribe audio to text.
Args:
audio_path: Path to audio file
language: Language code ('tw', 'en', 'auto')
Returns:
Dictionary with transcription results
"""
start_time = time.time()
self.stats["transcription_requests"] += 1
try:
if not self.transcription_model:
raise ValueError("Transcription model not loaded")
# Load and preprocess audio
import librosa
audio, sr = librosa.load(audio_path, sr=16000, mono=True)
# Process with model
inputs = self.processor(audio, sampling_rate=16000, return_tensors="pt")
inputs = inputs.to(self.device)
# Generate transcription
with torch.no_grad():
if language and language != "auto":
# Force specific language if specified
forced_decoder_ids = self.processor.get_decoder_prompt_ids(
language=language, task="transcribe"
)
predicted_ids = self.transcription_model.generate(
inputs.input_features,
forced_decoder_ids=forced_decoder_ids,
max_length=448,
num_beams=1,
do_sample=False,
)
else:
# Auto-detect language
predicted_ids = self.transcription_model.generate(
inputs.input_features,
max_length=448,
num_beams=1,
do_sample=False,
)
# Decode transcription
transcription = self.processor.batch_decode(
predicted_ids, skip_special_tokens=True
)[0]
processing_time = time.time() - start_time
self._update_transcription_stats(processing_time)
return {
"text": transcription.strip(),
"language": language or "auto",
"confidence": 0.95, # HF models typically have good confidence
"processing_time": processing_time,
"model_type": "huggingface",
"segments": [],
}
except Exception as e:
logger.error(f"❌ Transcription failed: {e}")
return {
"text": "",
"language": language or "auto",
"confidence": 0.0,
"processing_time": time.time() - start_time,
"error": str(e),
}
def classify_intent(self, text: str) -> Dict[str, Any]:
"""
Classify intent from text.
Args:
text: Input text to classify
Returns:
Dictionary with intent classification results
"""
start_time = time.time()
self.stats["intent_requests"] += 1
try:
# Always use fallback for single-task Whisper models
# Whisper models are designed for speech-to-text, not text classification
if self.model_type == "single" or not self.intent_classifier:
logger.info(
f"🔤 Using fallback keyword-based intent classification for: '{text[:50]}...'"
)
return self._fallback_intent_classification(text)
# Use trained intent classifier
results = self.intent_classifier(text, top_k=5)
# Format results
alternatives = []
for result in results:
alternatives.append(
{
"label": result["label"],
"score": float(result["score"]),
}
)
processing_time = time.time() - start_time
self._update_intent_stats(processing_time)
return {
"intent": alternatives[0]["label"] if alternatives else "unknown",
"confidence": alternatives[0]["score"] if alternatives else 0.0,
"alternatives": alternatives,
"processing_time": processing_time,
"method": "huggingface_classifier",
}
except Exception as e:
logger.error(f"❌ Intent classification failed: {e}")
return self._fallback_intent_classification(text)
def _fallback_intent_classification(self, text: str) -> Dict[str, Any]:
"""Fallback intent classification using simple keyword matching."""
logger.debug(f"🔍 Running keyword matching for text: '{text}'")
# Twi e-commerce intent keywords based on prompts_lean.csv
intent_keywords = {
# Navigation
"go_home": ["kɔ fie", "kɔ home", "kɔ homepage"],
"go_back": ["san w'akyi", "san kɔ"],
"continue": ["kɔ w'anim"],
"show_cart": ["kɔ cart", "cart no mu"],
"open_account": ["kɔ me account", "kɔ me akawnt"],
"open_orders": ["kɔ me orders"],
"open_wishlist": ["kɔ wishlist"],
# Search & Discovery
"search": ["hwehwɛ", "hwɛ"],
"apply_filter": ["fa filter", "fa to so"],
"clear_filter": ["yi filter"],
"sort_items": ["sort by", "fa di kan"],
# Product Info
"show_description": ["kyerɛ", "ho nsɛm", "kenkan"],
"show_price": ["kyerɛ", "boɔ"],
"show_reviews": ["kyerɛ reviews"],
"show_similar_items": ["kyerɛ nea ɛte sɛ", "te sɛ yei"],
# Cart Operations
"add_to_cart": ["fa yei to cart", "fa yei ka me cart"],
"remove_from_cart": ["yi yei firi cart", "yi yei firi me cart"],
"save_for_later": ["fa yei to wishlist", "fa yei ka me wishlist"],
"change_quantity": ["fa baako ka ho", "yi baako", "fa baako bio"],
"set_quantity": ["hyɛ dodow no yɛ"],
"clear_cart": ["pepa cart", "pepa me cart"],
# Checkout & Payment
"checkout": ["fa me kɔ checkout", "kɔ checkout"],
"confirm_order": ["pintim me order"],
"make_payment": [
"fa card",
"fa mobile money",
"fa momo",
"tua ka",
"tua seesei",
],
"cancel_order": ["gyae order", "twa order no mu"],
# Post-Purchase
"show_orders": ["kyerɛ me orders", "hwɛ me orders"],
"show_order_status": ["kyerɛ me order status"],
"track_order": ["trake me order"],
"return_item": ["mepɛ sɛ mesan de adeɛ no ba"],
"exchange_item": ["mepɛ sɛ mesesa adeɛ no"],
# Addresses
"show_addresses": ["kyerɛ me addresses"],
"add_address": ["fa address foforɔ ka ho"],
"remove_address": ["yi address"],
"set_default_address": ["fa default address"],
# Promotions
"apply_coupon": ["fa coupon to so"],
"remove_coupon": ["yi coupon"],
# Notifications
"enable_order_updates": ["sɔ order updates"],
"disable_order_updates": ["gyae order updates"],
"enable_price_alert": ["sɔ price alert"],
"disable_price_alert": ["gyae price alert"],
# Support
"help": ["boa me", "sɔ mmoa"],
"start_live_chat": ["frɛ live chat", "kɔ live chat"],
"show_faqs": ["kyerɛ faqs"],
# Variant Selection
"select_color": ["fa color"],
"change_color": ["sesa color"],
"select_size": ["fa size"],
"change_size": ["sesa size"],
}
text_lower = text.lower()
scores = {}
for intent, keywords in intent_keywords.items():
score = 0.0
matches = 0
for keyword in keywords:
if keyword.lower() in text_lower:
# Give higher weight to exact matches
if keyword.lower() == text_lower.strip():
score += 2.0 / len(keywords)
else:
score += 1.0 / len(keywords)
matches += 1
# Boost score for multiple keyword matches
if matches > 1:
score *= 1.5
if score > 0:
scores[intent] = score
# Sort by score
sorted_intents = sorted(scores.items(), key=lambda x: x[1], reverse=True)
if sorted_intents:
best_intent, best_score = sorted_intents[0]
alternatives = [
{"label": intent, "score": min(0.9, score)} # Cap at 90%
for intent, score in sorted_intents[:5]
]
logger.info(
f"✅ Keyword match found: '{best_intent}' (confidence: {best_score:.2f})"
)
else:
best_intent = "search" # Default fallback for unknown commands
best_score = 0.1
alternatives = [{"label": "search", "score": 0.1}]
logger.warning(
f"⚠️ No keyword matches found for '{text}', defaulting to 'search'"
)
result = {
"intent": best_intent,
"confidence": min(0.8, best_score), # Cap at 80% for fallback
"alternatives": alternatives,
"processing_time": 0.01,
"method": "fallback_keywords",
}
logger.debug(f"🎯 Fallback classification result: {result}")
return result
def recognize(self, audio_path: str, language: str = None) -> Dict[str, Any]:
"""
Complete recognition pipeline (transcription + intent if available).
Args:
audio_path: Path to audio file
language: Language code
Returns:
Dictionary with complete recognition results
"""
start_time = time.time()
self.stats["total_requests"] += 1
try:
# Step 1: Transcription
transcription_result = self.transcribe(audio_path, language)
if not transcription_result.get("text"):
return {
"status": "failed",
"error": "Transcription failed",
"processing_time": time.time() - start_time,
}
# Step 2: Intent classification (if available)
intent_result = self.classify_intent(transcription_result["text"])
# Step 3: Combine results
return {
"status": "success",
"transcription": transcription_result,
"intent": intent_result,
"processing_time": time.time() - start_time,
"model_info": {
"model_path": str(self.model_path),
"model_type": self.model_type,
"device": self.device,
},
}
except Exception as e:
logger.error(f"❌ Recognition failed: {e}")
return {
"status": "failed",
"error": str(e),
"processing_time": time.time() - start_time,
}
def _update_transcription_stats(self, processing_time: float):
"""Update transcription performance statistics."""
current_avg = self.stats["avg_transcription_time"]
count = self.stats["transcription_requests"]
self.stats["avg_transcription_time"] = (
current_avg * (count - 1) + processing_time
) / count
def _update_intent_stats(self, processing_time: float):
"""Update intent classification performance statistics."""
current_avg = self.stats["avg_intent_time"]
count = self.stats["intent_requests"]
self.stats["avg_intent_time"] = (
current_avg * (count - 1) + processing_time
) / count
def get_model_info(self) -> Dict[str, Any]:
"""Get model information and capabilities."""
return {
"model_path": str(self.model_path),
"model_type": self.model_type,
"device": self.device,
"capabilities": {
"transcription": self.transcription_model is not None,
"intent_classification": self.intent_classifier is not None,
"multi_task": self.model_type == "multi",
},
"supported_languages": self.supported_languages,
"intent_labels": list(self.intent_labels.get("id_to_label", {}).values()),
"statistics": self.stats,
}
def get_supported_intents(self) -> List[Dict[str, str]]:
"""Get list of supported intents."""
if self.model_type == "multi" and self.intent_labels:
intents = []
id_to_label = self.intent_labels.get("id_to_label", {})
for intent_id, intent_name in id_to_label.items():
intents.append(
{
"intent": intent_name,
"id": intent_id,
"description": f"Twi e-commerce intent: {intent_name}",
}
)
return intents
else:
# Fallback intents for single-task models based on e-commerce functionality
return [
{"intent": "search", "description": "Search for products"},
{"intent": "add_to_cart", "description": "Add items to shopping cart"},
{"intent": "show_cart", "description": "View shopping cart"},
{"intent": "checkout", "description": "Proceed to checkout"},
{"intent": "show_orders", "description": "View order history"},
{"intent": "track_order", "description": "Track order status"},
{"intent": "help", "description": "Request for assistance"},
{"intent": "go_home", "description": "Navigate to homepage"},
{"intent": "go_back", "description": "Go back to previous page"},
{"intent": "apply_filter", "description": "Apply product filters"},
{"intent": "show_description", "description": "Show product details"},
{"intent": "make_payment", "description": "Process payment"},
{"intent": "return_item", "description": "Return purchased items"},
{"intent": "add_address", "description": "Add delivery address"},
{"intent": "apply_coupon", "description": "Apply discount coupon"},
]
def health_check(self) -> Dict[str, Any]:
"""Perform health check on the model."""
return {
"status": "healthy" if self.transcription_model else "unhealthy",
"model_type": self.model_type,
"device": self.device,
"components": {
"transcription": "healthy" if self.transcription_model else "missing",
"intent_classification": "healthy"
if self.intent_classifier
else "fallback",
"processor": "healthy" if self.processor else "missing",
},
"statistics": self.stats,
}
def create_huggingface_adapter(
model_path: str, device: str = "auto"
) -> HuggingFaceModelAdapter:
"""
Create and initialize a HuggingFace model adapter.
Args:
model_path: Path to the HuggingFace model
device: Device to use ('auto', 'cpu', 'cuda')
Returns:
Initialized HuggingFace model adapter
"""
return HuggingFaceModelAdapter(model_path, device)