Download handler.py from athenahq/ACE-classifier-doc2vec-2025_09_22: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/athenahq/ACE-classifier-doc2vec-2025_09_22/resolve/main/handler.py
- Command line
-
hf download hf://athenahq/ACE-classifier-doc2vec-2025_09_22/handler.py
-
curl -L -o handler.py https://huggingface.co/athenahq/ACE-classifier-doc2vec-2025_09_22/resolve/main/handler.py
10 kB
| import pandas as pd | |
| import numpy as np | |
| from typing import Dict, Any | |
| import os | |
| import pickle | |
| import re | |
| from gensim.models.doc2vec import Doc2Vec | |
| import json | |
| class EndpointHandler: | |
| """ | |
| Custom handler for Hugging Face Inference Endpoints. | |
| Handles Doc2Vec + Classifier content classification model. | |
| """ | |
| def __init__(self, path: str = "."): | |
| """ | |
| Initialize the handler. | |
| Args: | |
| path (str): Path to the model files | |
| """ | |
| self.path = path | |
| # Try to load model metadata - first try new format, then fallback to old format | |
| new_summary_path = os.path.join(path, "best_model_summary.json") | |
| old_summary_path = os.path.join(path, "model_summary.json") | |
| best_model_info = None | |
| try: | |
| # Try new format first (best_model_summary.json) | |
| if os.path.exists(new_summary_path): | |
| with open(new_summary_path, 'r') as f: | |
| summary = json.load(f) | |
| best_model_info = summary['best_model'] | |
| print(f"π Using new format: best_model_summary.json") | |
| # Fallback to old format (model_summary.json) | |
| elif os.path.exists(old_summary_path): | |
| with open(old_summary_path, 'r') as f: | |
| summary = json.load(f) | |
| best_model_info = summary['top_3_models'][0] # rank 1 is the best | |
| print(f"π Using old format: model_summary.json") | |
| # If no summary file, try to find model files directly | |
| else: | |
| print(f"π No summary file found, searching for model files directly...") | |
| model_files = [f for f in os.listdir(path) if f.endswith('.pkl')] | |
| doc2vec_files = [f for f in os.listdir(path) if f.endswith('.model')] | |
| if not model_files or not doc2vec_files: | |
| raise Exception(f"No model files found in {path}") | |
| # Use the first found files (assuming single best model) | |
| classifier_file = model_files[0] | |
| doc2vec_file = doc2vec_files[0] | |
| # Create a minimal model info structure | |
| best_model_info = { | |
| 'files': { | |
| 'classifier': classifier_file, | |
| 'doc2vec': doc2vec_file | |
| }, | |
| 'config': { | |
| 'vector_size': 100 # Default, will be verified from actual model | |
| }, | |
| 'accuracy': 0.0, # Unknown | |
| 'algorithm_type': 'Unknown' | |
| } | |
| print(f"π Using direct file discovery: {classifier_file}, {doc2vec_file}") | |
| # Load the classifier | |
| classifier_path = os.path.join(path, best_model_info['files']['classifier']) | |
| with open(classifier_path, 'rb') as f: | |
| self.classifier = pickle.load(f) | |
| # Load the MATCHING Doc2Vec model (same one used to train this classifier) | |
| doc2vec_path = os.path.join(path, best_model_info['files']['doc2vec']) | |
| print(f"π Loading Doc2Vec from: {doc2vec_path}") | |
| # Check if associated files exist | |
| for suffix in ['.wv.vectors.npy', '.syn1neg.npy']: | |
| associated_file = doc2vec_path + suffix | |
| if os.path.exists(associated_file): | |
| print(f"β Found associated file: {associated_file}") | |
| else: | |
| print(f"β οΈ Missing associated file: {associated_file}") | |
| try: | |
| self.doc2vec_model = Doc2Vec.load(doc2vec_path) | |
| except Exception as e: | |
| print(f"β Failed to load Doc2Vec model: {str(e)}") | |
| print(f"β Current working directory: {os.getcwd()}") | |
| print(f"β Files in path {path}:") | |
| if os.path.exists(path): | |
| for f in os.listdir(path): | |
| print(f" - {f}") | |
| raise Exception(f"Failed to load Doc2Vec model from {doc2vec_path}: {str(e)}") | |
| # Verify feature dimensions match | |
| expected_features = self.doc2vec_model.vector_size | |
| print(f"β Doc2Vec vector size: {expected_features}") | |
| # Update config with actual vector size if it was unknown | |
| if best_model_info['config']['vector_size'] != expected_features: | |
| print(f"π Updating vector size in config from {best_model_info['config']['vector_size']} to {expected_features}") | |
| best_model_info['config']['vector_size'] = expected_features | |
| print(f"β Model config: {best_model_info['config']}") | |
| print(f"β Vector dimensions verified: {expected_features} features") | |
| # Additional debugging - test a sample vector | |
| test_text = "test sample text for debugging" | |
| test_vector = self.get_document_vector_for_test(test_text) | |
| print(f"π Test vector shape: {test_vector.shape}") | |
| print(f"π Classifier type: {type(self.classifier).__name__}") | |
| # Store model info for reference | |
| self.model_info = best_model_info | |
| classifier_name = best_model_info['files']['classifier'] | |
| algorithm_type = best_model_info.get('algorithm_type', 'Unknown') | |
| accuracy = best_model_info.get('accuracy', 0.0) | |
| print(f"β Loaded best model: {classifier_name} ({algorithm_type})") | |
| print(f"β Model accuracy: {accuracy:.4f}") | |
| print(f"β Doc2Vec config: {best_model_info['config']}") | |
| except Exception as e: | |
| print(f"β Error details: {str(e)}") | |
| print(f"β Error type: {type(e)}") | |
| raise Exception(f"β Failed to load models: {str(e)}") | |
| def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: | |
| """ | |
| Process inference request. | |
| Args: | |
| data (Dict): Request payload | |
| Expected input formats: | |
| Single prediction: | |
| { | |
| "inputs": { | |
| "content": "text content", | |
| "meta_description": "meta text" | |
| } | |
| } | |
| Returns: | |
| Dict: Prediction results | |
| """ | |
| try: | |
| # Extract inputs from request | |
| inputs = data.get("inputs", {}) | |
| if not inputs: | |
| return {"error": "Missing 'inputs' in request data"} | |
| # Handle single request | |
| if isinstance(inputs, dict): | |
| # Single prediction | |
| return self._predict_single(inputs) | |
| else: | |
| return {"error": "Invalid input format. Expected dict."} | |
| except Exception as e: | |
| return {"error": f"Prediction failed: {str(e)}"} | |
| def _predict_single(self, inputs: Dict[str, str]) -> Dict[str, Any]: | |
| """Handle single prediction request""" | |
| content = inputs.get("content", "") | |
| meta_description = inputs.get("meta_description", "") | |
| if not content: | |
| return {"error": "Missing required field: 'content'"} | |
| # Ensure inputs are strings | |
| content = str(content) | |
| meta_description = str(meta_description) | |
| try: | |
| # Combine text features (same as training) | |
| combined_text = content + ' ' + meta_description | |
| # Preprocess text for Doc2Vec (same as training preprocessing) | |
| preprocessed_words = self._preprocess_text(combined_text) | |
| if not preprocessed_words: | |
| # Handle empty text case | |
| doc_vector = np.zeros(self.doc2vec_model.vector_size) | |
| else: | |
| # Get document vector from Doc2Vec model | |
| doc_vector = self.doc2vec_model.infer_vector(preprocessed_words) | |
| # Reshape for classifier (expects 2D array) | |
| doc_vector = doc_vector.reshape(1, -1) | |
| # Get predictions from classifier | |
| probabilities = self.classifier.predict_proba(doc_vector)[0] | |
| # Get class names from classifier | |
| classes = self.classifier.classes_ | |
| # Create results in the same format as original handler | |
| results = [] | |
| for i, label in enumerate(classes): | |
| results.append({ | |
| "label": label, | |
| "score": float(probabilities[i]) | |
| }) | |
| return results | |
| except Exception as e: | |
| return {"error": f"Single prediction failed: {str(e)}"} | |
| def _preprocess_text(self, text): | |
| """Clean and preprocess text for Doc2Vec (same as training)""" | |
| if pd.isna(text) or not text: | |
| return [] | |
| # Convert to lowercase and remove special characters | |
| text = str(text).lower() | |
| text = re.sub(r'[^a-zA-Z\s]', '', text) | |
| text = re.sub(r'\s+', ' ', text).strip() | |
| # Split into words | |
| words = text.split() | |
| # Remove very short words | |
| words = [word for word in words if len(word) > 2] | |
| return words | |
| def get_document_vector_for_test(self, text): | |
| """Get document vector for testing (same as _preprocess_text + infer_vector)""" | |
| words = self._preprocess_text(text) | |
| if not words: | |
| return np.zeros(self.doc2vec_model.vector_size) | |
| else: | |
| return self.doc2vec_model.infer_vector(words) | |
| # For local testing | |
| if __name__ == "__main__": | |
| # Test the handler locally | |
| handler = EndpointHandler(".") | |
| # Test single prediction | |
| test_single = { | |
| "inputs": { | |
| "content": "Machine learning models for content attribution analysis using Doc2Vec embeddings", | |
| "meta_description": "A comprehensive guide to ML-based content classification with neural networks" | |
| } | |
| } | |
| print("=== Single Prediction Test ===") | |
| result = handler(test_single) | |
| print(result) |