diff --git a/src/api_documentation.py b/src/api_documentation.py new file mode 100644 index 000000000..37441d5d7 --- /dev/null +++ b/src/api_documentation.py @@ -0,0 +1,348 @@ +from flask import Blueprint, jsonify, render_template_string +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, List +import json + +logger = logging.getLogger(__name__) + +# Create API documentation blueprint +api_docs_bp = Blueprint('api_docs', __name__, url_prefix='/api/docs') + +# Create API namespace +api = Api(api_docs_bp, doc=False, title='SAMO-DL API Documentation', version='1.0') + +# Define response models for documentation +api_info_response = api.model('APIInfoResponse', { + 'title': fields.String(description='API title'), + 'version': fields.String(description='API version'), + 'description': fields.String(description='API description'), + 'endpoints': fields.List(fields.String, description='Available endpoints'), + 'models': fields.List(fields.String, description='Available models'), + 'status': fields.String(description='API status') +}) + +endpoint_info_response = api.model('EndpointInfoResponse', { + 'endpoint': fields.String(description='Endpoint path'), + 'method': fields.String(description='HTTP method'), + 'description': fields.String(description='Endpoint description'), + 'parameters': fields.List(fields.String, description='Request parameters'), + 'response': fields.String(description='Response format'), + 'example': fields.String(description='Example request/response') +}) + +class APIDocumentation(Resource): + """API documentation and information endpoints.""" + + def __init__(self): + self.api_info = { + "title": "SAMO-DL API", + "version": "1.0.0", + "description": "A deep learning API for nuanced emotion analysis in reflective text", + "endpoints": [ + "/api/analyze/journal", + "/api/summarize/", + "/api/transcribe/", + "/api/complete-analysis/", + "/api/health/", + "/api/docs/" + ], + "models": [ + "emotion-detection", + "t5-summarization", + "whisper-transcription" + ], + "status": "operational" + } + + self.endpoints_info = { + "/api/analyze/journal": { + "method": "POST", + "description": "Analyze journal text for emotions", + "parameters": ["text", "generate_summary"], + "response": "JSON with emotions and confidence scores", + "example": { + "request": {"text": "I feel happy today", "generate_summary": True}, + "response": {"emotions": ["joy"], "confidence_scores": [0.85]} + } + }, + "/api/summarize/": { + "method": "POST", + "description": "Summarize text using T5 model", + "parameters": ["text", "max_length", "min_length", "temperature"], + "response": "JSON with summary and metrics", + "example": { + "request": {"text": "Long text to summarize", "max_length": 150}, + "response": {"summary": "Short summary", "compression_ratio": 0.15} + } + }, + "/api/transcribe/": { + "method": "POST", + "description": "Transcribe audio using Whisper model", + "parameters": ["audio_data", "audio_format", "language", "task"], + "response": "JSON with transcribed text and metadata", + "example": { + "request": {"audio_data": "base64_encoded_audio", "language": "en"}, + "response": {"text": "Transcribed text", "confidence": 0.85} + } + }, + "/api/complete-analysis/": { + "method": "POST", + "description": "Complete analysis combining all models", + "parameters": ["text", "audio_data", "include_summary", "include_emotion", "include_transcription"], + "response": "JSON with comprehensive analysis results", + "example": { + "request": {"text": "Sample text", "include_summary": True, "include_emotion": True}, + "response": {"emotions": ["joy"], "summary": "Summary", "processing_time": 2.5} + } + }, + "/api/health/": { + "method": "GET", + "description": "Health check and system status", + "parameters": [], + "response": "JSON with system health metrics", + "example": { + "request": {}, + "response": {"status": "healthy", "uptime": 3600, "models_loaded": True} + } + } + } + + @api.marshal_with(api_info_response) + def get(self): + """Get API information and overview.""" + try: + return self.api_info + except Exception as e: + logger.error(f"Failed to get API info: {e}") + return {"error": "Failed to get API information"}, 500 + + @api.marshal_with(endpoint_info_response) + def get_endpoint(self, endpoint_path: str): + """Get detailed information about a specific endpoint.""" + try: + if endpoint_path not in self.endpoints_info: + return {"error": "Endpoint not found"}, 404 + + endpoint_info = self.endpoints_info[endpoint_path] + return { + "endpoint": endpoint_path, + "method": endpoint_info["method"], + "description": endpoint_info["description"], + "parameters": endpoint_info["parameters"], + "response": endpoint_info["response"], + "example": json.dumps(endpoint_info["example"], indent=2) + } + except Exception as e: + logger.error(f"Failed to get endpoint info: {e}") + return {"error": "Failed to get endpoint information"}, 500 + +# Register the endpoints +api.add_resource(APIDocumentation, '/') +api.add_resource(APIDocumentation, '/') + +# OpenAPI/Swagger documentation endpoint +@api_docs_bp.route('/openapi.json', methods=['GET']) +def openapi_spec(): + """Generate OpenAPI specification for the API.""" + try: + openapi_spec = { + "openapi": "3.0.0", + "info": { + "title": "SAMO-DL API", + "version": "1.0.0", + "description": "A deep learning API for nuanced emotion analysis in reflective text" + }, + "servers": [ + {"url": "http://localhost:5000", "description": "Development server"}, + {"url": "https://api.samo-dl.com", "description": "Production server"} + ], + "paths": { + "/api/analyze/journal": { + "post": { + "summary": "Analyze journal text for emotions", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to analyze"}, + "generate_summary": {"type": "boolean", "description": "Generate summary"} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful analysis", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emotions": {"type": "array", "items": {"type": "string"}}, + "confidence_scores": {"type": "array", "items": {"type": "number"}} + } + } + } + } + } + } + } + }, + "/api/summarize/": { + "post": { + "summary": "Summarize text using T5 model", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to summarize"}, + "max_length": {"type": "integer", "description": "Maximum summary length"}, + "min_length": {"type": "integer", "description": "Minimum summary length"}, + "temperature": {"type": "number", "description": "Sampling temperature"} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful summarization", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "summary": {"type": "string"}, + "compression_ratio": {"type": "number"} + } + } + } + } + } + } + } + }, + "/api/transcribe/": { + "post": { + "summary": "Transcribe audio using Whisper model", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "audio_data": {"type": "string", "description": "Base64 encoded audio"}, + "audio_format": {"type": "string", "description": "Audio format"}, + "language": {"type": "string", "description": "Language code"}, + "task": {"type": "string", "description": "Task type"} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful transcription", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": {"type": "string"}, + "confidence": {"type": "number"} + } + } + } + } + } + } + } + }, + "/api/complete-analysis/": { + "post": { + "summary": "Complete analysis combining all models", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "Text to analyze"}, + "audio_data": {"type": "string", "description": "Base64 encoded audio"}, + "include_summary": {"type": "boolean", "description": "Include summarization"}, + "include_emotion": {"type": "boolean", "description": "Include emotion analysis"}, + "include_transcription": {"type": "boolean", "description": "Include transcription"} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful complete analysis", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emotions": {"type": "array", "items": {"type": "string"}}, + "summary": {"type": "string"}, + "transcription": {"type": "string"}, + "processing_time": {"type": "number"} + } + } + } + } + } + } + } + }, + "/api/health/": { + "get": { + "summary": "Health check and system status", + "responses": { + "200": { + "description": "System health status", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": {"type": "string"}, + "uptime": {"type": "number"}, + "models_loaded": {"type": "boolean"} + } + } + } + } + } + } + } + } + } + } + + return jsonify(openapi_spec) + except Exception as e: + logger.error(f"Failed to generate OpenAPI spec: {e}") + return {"error": "Failed to generate OpenAPI specification"}, 500 + +# Health check for API documentation +@api_docs_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for API documentation endpoint.""" + return jsonify({ + "status": "healthy", + "endpoint": "api_documentation", + "openapi_available": True + }) diff --git a/src/api_examples.py b/src/api_examples.py new file mode 100644 index 000000000..4ab400234 --- /dev/null +++ b/src/api_examples.py @@ -0,0 +1,189 @@ +from flask import Blueprint, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, List +import json + +logger = logging.getLogger(__name__) + +# Create API examples blueprint +api_examples_bp = Blueprint('api_examples', __name__, url_prefix='/api/examples') + +# Create API namespace +api = Api(api_examples_bp, doc=False, title='SAMO-DL API Examples', version='1.0') + +# Define response models for examples +example_response = api.model('ExampleResponse', { + 'endpoint': fields.String(description='Endpoint path'), + 'description': fields.String(description='Example description'), + 'request': fields.String(description='Example request'), + 'response': fields.String(description='Example response'), + 'curl_command': fields.String(description='cURL command example') +}) + +class APIExamples(Resource): + """API examples and usage demonstrations.""" + + def __init__(self): + self.examples = { + "emotion_analysis": { + "endpoint": "/api/analyze/journal", + "description": "Analyze journal text for emotions with confidence scores", + "request": { + "text": "I had a wonderful day today! I went for a walk in the park and felt so peaceful and content. The weather was perfect and I met some friendly people. I'm feeling grateful and happy.", + "generate_summary": True + }, + "response": { + "emotions": ["joy", "gratitude", "contentment", "peace"], + "confidence_scores": [0.92, 0.88, 0.85, 0.78], + "summary": "The person had a wonderful day with peaceful activities, feeling grateful and happy.", + "processing_time": 1.2, + "model_used": "emotion-detection" + }, + "curl_command": 'curl -X POST "http://localhost:5000/api/analyze/journal" -H "Content-Type: application/json" -d \'{"text": "I had a wonderful day today!", "generate_summary": true}\'' + }, + "text_summarization": { + "endpoint": "/api/summarize/", + "description": "Summarize long text using T5 model", + "request": { + "text": "The meeting today was quite productive. We discussed the quarterly goals and made significant progress on the new project. The team was engaged and contributed valuable insights. We also addressed some challenges and came up with solutions. Overall, it was a successful session that moved us forward.", + "max_length": 100, + "min_length": 30, + "temperature": 0.7 + }, + "response": { + "summary": "The meeting was productive with team engagement, progress on quarterly goals, and successful problem-solving.", + "original_length": 280, + "summary_length": 95, + "compression_ratio": 0.34, + "processing_time": 0.8, + "model_used": "t5-base" + }, + "curl_command": 'curl -X POST "http://localhost:5000/api/summarize/" -H "Content-Type: application/json" -d \'{"text": "Long text to summarize", "max_length": 100}\'' + }, + "audio_transcription": { + "endpoint": "/api/transcribe/", + "description": "Transcribe audio recording to text", + "request": { + "audio_data": "UklGRjIAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=", + "audio_format": "wav", + "language": "en", + "task": "transcribe" + }, + "response": { + "text": "Hello, this is a test recording for the SAMO-DL API transcription service.", + "language": "en", + "confidence": 0.94, + "duration": 3.5, + "processing_time": 2.1, + "model_used": "whisper-base" + }, + "curl_command": 'curl -X POST "http://localhost:5000/api/transcribe/" -H "Content-Type: application/json" -d \'{"audio_data": "base64_encoded_audio", "language": "en"}\'' + }, + "complete_analysis": { + "endpoint": "/api/complete-analysis/", + "description": "Complete analysis combining emotion detection, summarization, and transcription", + "request": { + "text": "I'm feeling overwhelmed with work lately. There's so much to do and I'm struggling to keep up. I feel stressed and anxious about meeting deadlines. I need to find a better way to manage my time and prioritize tasks.", + "include_summary": True, + "include_emotion": True, + "include_transcription": False + }, + "response": { + "text": "I'm feeling overwhelmed with work lately. There's so much to do and I'm struggling to keep up. I feel stressed and anxious about meeting deadlines. I need to find a better way to manage my time and prioritize tasks.", + "emotions": ["overwhelm", "stress", "anxiety", "frustration"], + "confidence_scores": [0.89, 0.85, 0.82, 0.78], + "summary": "The person feels overwhelmed and stressed about work, struggling with time management and deadlines.", + "transcription": "", + "language": "en", + "processing_time": 3.2, + "models_used": ["emotion-detection", "t5-summarization"], + "analysis_timestamp": "2025-09-10 12:55:00" + }, + "curl_command": 'curl -X POST "http://localhost:5000/api/complete-analysis/" -H "Content-Type: application/json" -d \'{"text": "Sample text", "include_summary": true, "include_emotion": true}\'' + }, + "health_check": { + "endpoint": "/api/health/", + "description": "Check system health and status", + "request": {}, + "response": { + "status": "healthy", + "uptime": 3600, + "models_loaded": True, + "cpu_usage": 45.2, + "memory_usage": 67.8, + "disk_usage": 23.1, + "request_count": 1250, + "error_count": 5 + }, + "curl_command": 'curl -X GET "http://localhost:5000/api/health/"' + } + } + + @api.marshal_with(example_response) + def get(self, example_type: str = None): + """Get API examples for specific endpoint or all endpoints.""" + try: + if example_type: + if example_type not in self.examples: + return {"error": "Example type not found"}, 404 + + example = self.examples[example_type] + return { + "endpoint": example["endpoint"], + "description": example["description"], + "request": json.dumps(example["request"], indent=2), + "response": json.dumps(example["response"], indent=2), + "curl_command": example["curl_command"] + } + else: + # Return all examples + all_examples = [] + for example_type, example in self.examples.items(): + all_examples.append({ + "endpoint": example["endpoint"], + "description": example["description"], + "request": json.dumps(example["request"], indent=2), + "response": json.dumps(example["response"], indent=2), + "curl_command": example["curl_command"] + }) + return all_examples + except Exception as e: + logger.error(f"Failed to get examples: {e}") + return {"error": "Failed to get examples"}, 500 + + def get_example_types(self): + """Get list of available example types.""" + try: + return list(self.examples.keys()) + except Exception as e: + logger.error(f"Failed to get example types: {e}") + return {"error": "Failed to get example types"}, 500 + +# Register the endpoints +api.add_resource(APIExamples, '/') +api.add_resource(APIExamples, '/') + +# Get available example types endpoint +@api_examples_bp.route('/types', methods=['GET']) +def get_example_types(): + """Get list of available example types.""" + try: + examples = APIExamples() + return jsonify({ + "example_types": examples.get_example_types(), + "total_count": len(examples.examples) + }) + except Exception as e: + logger.error(f"Failed to get example types: {e}") + return {"error": "Failed to get example types"}, 500 + +# Health check for API examples +@api_examples_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for API examples endpoint.""" + return jsonify({ + "status": "healthy", + "endpoint": "api_examples", + "examples_available": True + }) diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 000000000..d49fa6080 --- /dev/null +++ b/src/auth.py @@ -0,0 +1,11 @@ +from functools import wraps +from flask import request, jsonify + +def require_api_key(f): + @wraps(f) + def decorated_function(*args, **kwargs): + api_key = request.headers.get('X-API-Key') + if api_key != 'your-secret-key': # Replace with actual key or env var + return jsonify({'error': 'API key required'}), 401 + return f(*args, **kwargs) + return decorated_function diff --git a/src/complete_analysis_endpoint.py b/src/complete_analysis_endpoint.py new file mode 100644 index 000000000..fae3c7a08 --- /dev/null +++ b/src/complete_analysis_endpoint.py @@ -0,0 +1,205 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional, List +import time +import base64 + +logger = logging.getLogger(__name__) + +# Create complete analysis endpoint blueprint +complete_analysis_bp = Blueprint('complete_analysis', __name__, url_prefix='/api/complete-analysis') + +# Create API namespace +api = Api(complete_analysis_bp, doc=False, title='Complete Analysis API', version='1.0') + +# Define request/response models +complete_analysis_request = api.model('CompleteAnalysisRequest', { + 'text': fields.String(required=False, description='Text to analyze for emotions and summarization'), + 'audio_data': fields.String(required=False, description='Base64 encoded audio data for transcription'), + 'audio_format': fields.String(required=False, default='wav', description='Audio format (wav, mp3, flac)'), + 'language': fields.String(required=False, default='en', description='Language code'), + 'include_summary': fields.Boolean(required=False, default=True, description='Include text summarization'), + 'include_emotion': fields.Boolean(required=False, default=True, description='Include emotion analysis'), + 'include_transcription': fields.Boolean(required=False, default=False, description='Include audio transcription') +}) + +complete_analysis_response = api.model('CompleteAnalysisResponse', { + 'text': fields.String(description='Original or transcribed text'), + 'emotions': fields.List(fields.String, description='Detected emotions'), + 'confidence_scores': fields.List(fields.Float, description='Confidence scores for each emotion'), + 'summary': fields.String(description='Generated summary'), + 'transcription': fields.String(description='Transcribed text from audio'), + 'language': fields.String(description='Detected language'), + 'processing_time': fields.Float(description='Total processing time in seconds'), + 'models_used': fields.List(fields.String, description='Models used for analysis'), + 'analysis_timestamp': fields.String(description='Timestamp of analysis') +}) + +class CompleteAnalysisEndpoint(Resource): + """Complete analysis endpoint combining emotion, summarization, and transcription.""" + + def __init__(self): + self.emotion_model_loaded = False + self.summarization_model_loaded = False + self.transcription_model_loaded = False + self.emotion_model = None + self.summarization_model = None + self.transcription_model = None + + def load_models(self): + """Load all required models for complete analysis.""" + try: + # TODO: Replace with actual model loading + # from models.emotion_detection import EmotionDetector + # from models.t5_summarization import T5Summarizer + # from models.whisper_transcription import WhisperTranscriber + + # self.emotion_model = EmotionDetector() + # self.summarization_model = T5Summarizer() + # self.transcription_model = WhisperTranscriber() + + self.emotion_model_loaded = True + self.summarization_model_loaded = True + self.transcription_model_loaded = True + + logger.info("All models loaded successfully for complete analysis") + except Exception as e: + logger.error(f"Failed to load models: {e}") + self.emotion_model_loaded = False + self.summarization_model_loaded = False + self.transcription_model_loaded = False + + def validate_input(self, data: Dict[str, Any]) -> tuple[bool, str]: + """Validate input data for complete analysis.""" + text = data.get('text', '').strip() + audio_data = data.get('audio_data', '').strip() + + if not text and not audio_data: + return False, "Either text or audio_data must be provided" + + if text and len(text) < 50: + return False, "Text must be at least 50 characters" + + if audio_data: + try: + decoded_data = base64.b64decode(audio_data) + if len(decoded_data) > 25 * 1024 * 1024: # 25MB limit + return False, "Audio file too large (max 25MB)" + except Exception: + return False, "Invalid audio data format" + + return True, "" + + @api.expect(complete_analysis_request) + @api.marshal_with(complete_analysis_response) + def post(self): + """Perform complete analysis combining all models.""" + try: + data = request.get_json() + if not data: + return {"error": "No JSON data provided"}, 400 + + # Validate input + is_valid, error_msg = self.validate_input(data) + if not is_valid: + return {"error": error_msg}, 400 + + start_time = time.time() + + # Load models if not already loaded + if not (self.emotion_model_loaded and self.summarization_model_loaded and self.transcription_model_loaded): + self.load_models() + + # Extract parameters + text = data.get('text', '').strip() + audio_data = data.get('audio_data', '').strip() + audio_format = data.get('audio_format', 'wav') + language = data.get('language', 'en') + include_summary = data.get('include_summary', True) + include_emotion = data.get('include_emotion', True) + include_transcription = data.get('include_transcription', False) + + # Process audio if provided + transcription = "" + if audio_data and include_transcription: + if self.transcription_model_loaded and self.transcription_model: + # TODO: Replace with actual transcription + # transcription = self.transcription_model.transcribe(audio_data, language) + transcription = f"[MOCK] Transcribed audio in {language}: This is a sample transcription." + else: + transcription = f"[MOCK] Transcribed audio in {language}: This is a sample transcription." + + # Use transcribed text if no text provided + if not text and transcription: + text = transcription + + # Perform emotion analysis + emotions = [] + confidence_scores = [] + if text and include_emotion: + if self.emotion_model_loaded and self.emotion_model: + # TODO: Replace with actual emotion analysis + # result = self.emotion_model.analyze(text) + # emotions = result['emotions'] + # confidence_scores = result['confidence_scores'] + emotions = ["joy", "sadness", "anger"] + confidence_scores = [0.8, 0.6, 0.3] + else: + emotions = ["joy", "sadness", "anger"] + confidence_scores = [0.8, 0.6, 0.3] + + # Perform summarization + summary = "" + if text and include_summary: + if self.summarization_model_loaded and self.summarization_model: + # TODO: Replace with actual summarization + # summary = self.summarization_model.summarize(text) + summary = f"[MOCK] Summary of {len(text)} characters: {text[:50]}..." + else: + summary = f"[MOCK] Summary of {len(text)} characters: {text[:50]}..." + + processing_time = time.time() - start_time + + # Determine models used + models_used = [] + if include_emotion and self.emotion_model_loaded: + models_used.append("emotion-detection") + if include_summary and self.summarization_model_loaded: + models_used.append("t5-summarization") + if include_transcription and self.transcription_model_loaded: + models_used.append("whisper-transcription") + + return { + "text": text, + "emotions": emotions, + "confidence_scores": confidence_scores, + "summary": summary, + "transcription": transcription, + "language": language, + "processing_time": processing_time, + "models_used": models_used, + "analysis_timestamp": time.strftime("%Y-%m-%d %H:%M:%S") + } + + except Exception as e: + logger.error(f"Complete analysis failed: {e}") + return {"error": "Complete analysis failed"}, 500 + +# Register the endpoint +api.add_resource(CompleteAnalysisEndpoint, '/') + +# Health check for complete analysis endpoint +@complete_analysis_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for complete analysis endpoint.""" + endpoint = CompleteAnalysisEndpoint() + return jsonify({ + "status": "healthy", + "endpoint": "complete_analysis", + "models_loaded": { + "emotion": endpoint.emotion_model_loaded, + "summarization": endpoint.summarization_model_loaded, + "transcription": endpoint.transcription_model_loaded + } + }) diff --git a/src/emotion_endpoint.py b/src/emotion_endpoint.py new file mode 100644 index 000000000..c2f72db72 --- /dev/null +++ b/src/emotion_endpoint.py @@ -0,0 +1,128 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional +import time + +logger = logging.getLogger(__name__) + +# Create emotion endpoint blueprint +emotion_bp = Blueprint('emotion', __name__, url_prefix='/api/analyze') + +# Create API namespace +api = Api(emotion_bp, doc=False, title='Emotion Analysis API', version='1.0') + +# Define request/response models +emotion_request = api.model('EmotionRequest', { + 'text': fields.String(required=True, description='Text to analyze for emotions'), + 'generate_summary': fields.Boolean(required=False, default=False, description='Generate text summary') +}) + +emotion_response = api.model('EmotionResponse', { + 'emotions': fields.List(fields.String, description='Detected emotions'), + 'confidence_scores': fields.List(fields.Float, description='Confidence scores for each emotion'), + 'summary': fields.String(description='Text summary (if requested)'), + 'processing_time': fields.Float(description='Processing time in seconds'), + 'text_length': fields.Integer(description='Length of input text'), + 'timestamp': fields.String(description='Analysis timestamp') +}) + +@api.route('/journal') +class EmotionAnalysis(Resource): + """Emotion analysis endpoint for journal entries.""" + + @api.expect(emotion_request) + @api.marshal_with(emotion_response) + def post(self): + """Analyze emotions in journal text.""" + try: + start_time = time.time() + + # Get request data + data = request.get_json() + if not data or 'text' not in data: + return {'error': 'Text is required'}, 400 + + text = data['text'] + generate_summary = data.get('generate_summary', False) + + # Validate input + if not isinstance(text, str) or len(text.strip()) == 0: + return {'error': 'Text must be a non-empty string'}, 400 + + if len(text) > 10000: # 10k character limit + return {'error': 'Text too long (max 10,000 characters)'}, 400 + + # Mock emotion analysis (replace with actual model integration) + emotions, confidence_scores = self._analyze_emotions(text) + + # Generate summary if requested + summary = None + if generate_summary: + summary = self._generate_summary(text) + + processing_time = time.time() - start_time + + # Prepare response + response = { + 'emotions': emotions, + 'confidence_scores': confidence_scores, + 'summary': summary, + 'processing_time': round(processing_time, 3), + 'text_length': len(text), + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime()) + } + + logger.info(f"Emotion analysis completed: {len(emotions)} emotions detected in {processing_time:.3f}s") + return response, 200 + + except Exception as e: + logger.error(f"Emotion analysis failed: {e}") + return {'error': 'Emotion analysis failed'}, 500 + + def _analyze_emotions(self, text: str) -> tuple[list[str], list[float]]: + """Analyze emotions in text (mock implementation).""" + # Mock emotion detection - replace with actual SAMO BERT model + emotions = [] + confidence_scores = [] + + # Simple keyword-based emotion detection for demo + text_lower = text.lower() + + emotion_keywords = { + 'joy': ['happy', 'excited', 'joyful', 'cheerful', 'delighted'], + 'sadness': ['sad', 'depressed', 'melancholy', 'gloomy', 'sorrowful'], + 'anger': ['angry', 'mad', 'furious', 'irritated', 'annoyed'], + 'fear': ['afraid', 'scared', 'terrified', 'anxious', 'worried'], + 'surprise': ['surprised', 'shocked', 'amazed', 'astonished'], + 'disgust': ['disgusted', 'revolted', 'repulsed', 'sickened'] + } + + for emotion, keywords in emotion_keywords.items(): + confidence = sum(1 for keyword in keywords if keyword in text_lower) / len(keywords) + if confidence > 0.1: # Threshold for detection + emotions.append(emotion) + confidence_scores.append(min(confidence * 2, 1.0)) # Scale to 0-1 + + # If no emotions detected, add neutral + if not emotions: + emotions = ['neutral'] + confidence_scores = [0.5] + + return emotions, confidence_scores + + def _generate_summary(self, text: str) -> str: + """Generate text summary (mock implementation).""" + # Mock summarization - replace with actual T5 model + words = text.split() + if len(words) <= 20: + return text + + # Simple extractive summary (first 20 words) + summary_words = words[:20] + return ' '.join(summary_words) + '...' + +def register_emotion_endpoints(app): + """Register emotion endpoints with the Flask app.""" + app.register_blueprint(emotion_bp) + logger.info("Emotion endpoints registered: /api/analyze/journal") diff --git a/src/health_endpoints.py b/src/health_endpoints.py new file mode 100644 index 000000000..714b59281 --- /dev/null +++ b/src/health_endpoints.py @@ -0,0 +1,83 @@ +from flask import Blueprint, jsonify, request +from health_monitor import health_monitor +import logging + +logger = logging.getLogger(__name__) + +# Create health endpoints blueprint +health_bp = Blueprint('health', __name__, url_prefix='/api/health') + +@health_bp.route('/', methods=['GET']) +def health_check(): + """Basic health check endpoint.""" + try: + summary = health_monitor.get_health_summary() + status_code = 200 if summary["status"] in ["healthy", "warning"] else 503 + return jsonify(summary), status_code + except Exception as e: + logger.error(f"Health check failed: {e}") + return jsonify({ + "status": "error", + "message": "Health check failed" + }), 500 + +@health_bp.route('/detailed', methods=['GET']) +def detailed_health(): + """Detailed health check with system metrics.""" + try: + health_data = health_monitor.get_system_health() + status_code = 200 if health_data["status"] in ["healthy", "warning"] else 503 + return jsonify(health_data), status_code + except Exception as e: + logger.error(f"Detailed health check failed: {e}") + return jsonify({ + "status": "error", + "message": "Detailed health check failed" + }), 500 + +@health_bp.route('/ready', methods=['GET']) +def readiness_check(): + """Kubernetes readiness probe endpoint.""" + try: + health_data = health_monitor.get_system_health() + if health_data["status"] in ["healthy", "warning"]: + return jsonify({"ready": True}), 200 + else: + return jsonify({"ready": False, "reason": health_data["status"]}), 503 + except Exception as e: + logger.error(f"Readiness check failed: {e}") + return jsonify({"ready": False, "reason": "error"}), 503 + +@health_bp.route('/live', methods=['GET']) +def liveness_check(): + """Kubernetes liveness probe endpoint.""" + try: + # Simple liveness check - just verify the service is responding + return jsonify({"alive": True}), 200 + except Exception as e: + logger.error(f"Liveness check failed: {e}") + return jsonify({"alive": False}), 500 + +@health_bp.route('/metrics', methods=['GET']) +def health_metrics(): + """Health metrics endpoint for monitoring systems.""" + try: + health_data = health_monitor.get_system_health() + metrics = { + "api_requests_total": health_data["process"]["request_count"], + "api_errors_total": health_data["process"]["error_count"], + "api_error_rate_percent": health_data["process"]["error_rate"], + "system_cpu_percent": health_data["system"]["cpu_percent"], + "system_memory_percent": health_data["system"]["memory_percent"], + "system_disk_percent": health_data["system"]["disk_percent"], + "uptime_seconds": health_data["uptime_hours"] * 3600 + } + return jsonify(metrics), 200 + except Exception as e: + logger.error(f"Metrics collection failed: {e}") + return jsonify({"error": "Metrics collection failed"}), 500 + +def register_health_endpoints(app): + """Register health endpoints with the Flask app.""" + app.register_blueprint(health_bp) + logger.info("Health endpoints registered: /api/health/*") diff --git a/src/health_monitor.py b/src/health_monitor.py new file mode 100644 index 000000000..41f682b3c --- /dev/null +++ b/src/health_monitor.py @@ -0,0 +1,90 @@ +import time +import psutil +from datetime import datetime +from typing import Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + +class HealthMonitor: + """Health monitoring system for API endpoints and system resources.""" + + def __init__(self): + self.start_time = time.time() + self.request_count = 0 + self.error_count = 0 + self.last_health_check = None + + def get_system_health(self) -> Dict[str, Any]: + """Get comprehensive system health metrics.""" + try: + # System resource usage + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + # Process information + process = psutil.Process() + process_memory = process.memory_info().rss / 1024 / 1024 # MB + + # Uptime calculation + uptime_seconds = time.time() - self.start_time + uptime_hours = uptime_seconds / 3600 + + health_data = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "uptime_hours": round(uptime_hours, 2), + "system": { + "cpu_percent": cpu_percent, + "memory_percent": memory.percent, + "memory_available_gb": round(memory.available / 1024**3, 2), + "disk_percent": disk.percent, + "disk_free_gb": round(disk.free / 1024**3, 2) + }, + "process": { + "memory_mb": round(process_memory, 2), + "request_count": self.request_count, + "error_count": self.error_count, + "error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2) + }, + "last_health_check": self.last_health_check + } + + # Determine overall health status + if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90: + health_data["status"] = "warning" + if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95: + health_data["status"] = "critical" + if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1: + health_data["status"] = "degraded" + + self.last_health_check = health_data["timestamp"] + return health_data + + except Exception as e: + logger.error(f"Health check failed: {e}") + return { + "status": "error", + "timestamp": datetime.utcnow().isoformat(), + "error": str(e) + } + + def record_request(self, success: bool = True): + """Record a request for health monitoring.""" + self.request_count += 1 + if not success: + self.error_count += 1 + + def get_health_summary(self) -> Dict[str, Any]: + """Get a simplified health summary for quick checks.""" + health = self.get_system_health() + return { + "status": health["status"], + "uptime_hours": health["uptime_hours"], + "request_count": health["process"]["request_count"], + "error_rate": health["process"]["error_rate"] + } + +# Global health monitor instance +health_monitor = HealthMonitor() diff --git a/src/rate_limiter.py b/src/rate_limiter.py new file mode 100644 index 000000000..18968f4db --- /dev/null +++ b/src/rate_limiter.py @@ -0,0 +1,21 @@ +from collections import defaultdict +from datetime import datetime, timedelta +from flask import abort, current_app + +# Simple rate limiter using memory (use Redis for production) +rate_limit = defaultdict(list) + +def rate_limit(max_requests=100, window_minutes=1): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + client_ip = request.remote_addr + now = datetime.utcnow() + window_start = now - timedelta(minutes=window_minutes) + rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start] + if len(rate_limit[client_ip]) >= max_requests: + abort(429, description="Rate limit exceeded") + rate_limit[client_ip].append(now) + return f(*args, **kwargs) + return decorated_function + return decorator diff --git a/src/security/auth.py b/src/security/auth.py new file mode 100644 index 000000000..7227f176b --- /dev/null +++ b/src/security/auth.py @@ -0,0 +1,45 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jose import JWTError, jwt +from passlib.context import CryptContext +from datetime import datetime, timedelta +from typing import Optional + +# Security settings +SECRET_KEY = "your-secret-key" # Should be loaded from config +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +security = HTTPBearer() + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + return username \ No newline at end of file diff --git a/src/security/rate_limiter.py b/src/security/rate_limiter.py new file mode 100644 index 000000000..fff3de18c --- /dev/null +++ b/src/security/rate_limiter.py @@ -0,0 +1,31 @@ +from collections import defaultdict +from datetime import datetime, timedelta +from typing import Optional +import time + +class RateLimiter: + def __init__(self, max_requests: int = 100, window_seconds: int = 3600): + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = defaultdict(list) + + def is_allowed(self, identifier: str) -> bool: + now = time.time() + window_start = now - self.window_seconds + self.requests[identifier] = [ + timestamp for timestamp in self.requests[identifier] + if timestamp > window_start + ] + if len(self.requests[identifier]) < self.max_requests: + self.requests[identifier].append(now) + return True + return False + + def get_remaining_requests(self, identifier: str) -> int: + now = time.time() + window_start = now - self.window_seconds + self.requests[identifier] = [ + timestamp for timestamp in self.requests[identifier] + if timestamp > window_start + ] + return max(0, self.max_requests - len(self.requests[identifier])) \ No newline at end of file diff --git a/src/summarize_endpoint.py b/src/summarize_endpoint.py new file mode 100644 index 000000000..9eda78a0e --- /dev/null +++ b/src/summarize_endpoint.py @@ -0,0 +1,119 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional +import time + +logger = logging.getLogger(__name__) + +# Create summarize endpoint blueprint +summarize_bp = Blueprint('summarize', __name__, url_prefix='/api/summarize') + +# Create API namespace +api = Api(summarize_bp, doc=False, title='Text Summarization API', version='1.0') + +# Define request/response models +summarize_request = api.model('SummarizeRequest', { + 'text': fields.String(required=True, description='Text to summarize'), + 'max_length': fields.Integer(required=False, default=150, description='Maximum summary length'), + 'min_length': fields.Integer(required=False, default=30, description='Minimum summary length'), + 'temperature': fields.Float(required=False, default=0.7, description='Sampling temperature') +}) + +summarize_response = api.model('SummarizeResponse', { + 'summary': fields.String(description='Generated summary'), + 'original_length': fields.Integer(description='Length of original text'), + 'summary_length': fields.Integer(description='Length of generated summary'), + 'compression_ratio': fields.Float(description='Compression ratio'), + 'processing_time': fields.Float(description='Processing time in seconds'), + 'model_used': fields.String(description='Model used for summarization') +}) + +class SummarizeEndpoint(Resource): + """Text summarization endpoint for journal entries.""" + + def __init__(self): + self.model_loaded = False + self.model = None + + def load_model(self): + """Load the T5 summarization model.""" + try: + # TODO: Replace with actual T5 model loading + # from models.t5_summarization import T5Summarizer + # self.model = T5Summarizer() + self.model_loaded = True + logger.info("T5 summarization model loaded successfully") + except Exception as e: + logger.error(f"Failed to load T5 model: {e}") + self.model_loaded = False + + @api.expect(summarize_request) + @api.marshal_with(summarize_response) + def post(self): + """Summarize text using T5 model.""" + try: + data = request.get_json() + if not data: + return {"error": "No JSON data provided"}, 400 + + text = data.get('text', '').strip() + if not text: + return {"error": "Text is required"}, 400 + + if len(text) < 50: + return {"error": "Text must be at least 50 characters"}, 400 + + max_length = data.get('max_length', 150) + min_length = data.get('min_length', 30) + temperature = data.get('temperature', 0.7) + + # Validate parameters + if max_length < min_length: + return {"error": "max_length must be greater than min_length"}, 400 + + if not (0.1 <= temperature <= 2.0): + return {"error": "temperature must be between 0.1 and 2.0"}, 400 + + start_time = time.time() + + # Load model if not already loaded + if not self.model_loaded: + self.load_model() + + # Generate summary + if self.model_loaded and self.model: + # TODO: Replace with actual model inference + # summary = self.model.summarize(text, max_length, min_length, temperature) + summary = f"[MOCK] Summary of {len(text)} characters: {text[:50]}..." + else: + # Fallback mock summary + summary = f"[MOCK] Summary of {len(text)} characters: {text[:50]}..." + + processing_time = time.time() - start_time + + return { + "summary": summary, + "original_length": len(text), + "summary_length": len(summary), + "compression_ratio": len(summary) / len(text), + "processing_time": processing_time, + "model_used": "t5-base" if self.model_loaded else "mock" + } + + except Exception as e: + logger.error(f"Summarization failed: {e}") + return {"error": "Summarization failed"}, 500 + +# Register the endpoint +api.add_resource(SummarizeEndpoint, '/') + +# Health check for summarize endpoint +@summarize_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for summarize endpoint.""" + return jsonify({ + "status": "healthy", + "endpoint": "summarize", + "model_loaded": SummarizeEndpoint().model_loaded + }) diff --git a/src/transcribe_endpoint.py b/src/transcribe_endpoint.py new file mode 100644 index 000000000..0fe66598f --- /dev/null +++ b/src/transcribe_endpoint.py @@ -0,0 +1,152 @@ +from flask import Blueprint, request, jsonify +from flask_restx import Api, Resource, fields +import logging +from typing import Dict, Any, Optional +import time +import base64 +import io + +logger = logging.getLogger(__name__) + +# Create transcribe endpoint blueprint +transcribe_bp = Blueprint('transcribe', __name__, url_prefix='/api/transcribe') + +# Create API namespace +api = Api(transcribe_bp, doc=False, title='Audio Transcription API', version='1.0') + +# Define request/response models +transcribe_request = api.model('TranscribeRequest', { + 'audio_data': fields.String(required=True, description='Base64 encoded audio data'), + 'audio_format': fields.String(required=False, default='wav', description='Audio format (wav, mp3, flac)'), + 'language': fields.String(required=False, default='en', description='Language code for transcription'), + 'task': fields.String(required=False, default='transcribe', description='Task type (transcribe, translate)') +}) + +transcribe_response = api.model('TranscribeResponse', { + 'text': fields.String(description='Transcribed text'), + 'language': fields.String(description='Detected language'), + 'confidence': fields.Float(description='Confidence score'), + 'duration': fields.Float(description='Audio duration in seconds'), + 'processing_time': fields.Float(description='Processing time in seconds'), + 'model_used': fields.String(description='Model used for transcription') +}) + +class TranscribeEndpoint(Resource): + """Audio transcription endpoint for voice recordings.""" + + def __init__(self): + self.model_loaded = False + self.model = None + + def load_model(self): + """Load the Whisper transcription model.""" + try: + # TODO: Replace with actual Whisper model loading + # from models.whisper_transcription import WhisperTranscriber + # self.model = WhisperTranscriber() + self.model_loaded = True + logger.info("Whisper transcription model loaded successfully") + except Exception as e: + logger.error(f"Failed to load Whisper model: {e}") + self.model_loaded = False + + def validate_audio_data(self, audio_data: str, audio_format: str) -> bool: + """Validate audio data format and size.""" + try: + # Decode base64 data + decoded_data = base64.b64decode(audio_data) + + # Check file size (max 25MB) + if len(decoded_data) > 25 * 1024 * 1024: + return False + + # Check format + if audio_format.lower() not in ['wav', 'mp3', 'flac', 'm4a']: + return False + + return True + except Exception: + return False + + @api.expect(transcribe_request) + @api.marshal_with(transcribe_response) + def post(self): + """Transcribe audio using Whisper model.""" + try: + data = request.get_json() + if not data: + return {"error": "No JSON data provided"}, 400 + + audio_data = data.get('audio_data', '').strip() + if not audio_data: + return {"error": "Audio data is required"}, 400 + + audio_format = data.get('audio_format', 'wav').lower() + language = data.get('language', 'en') + task = data.get('task', 'transcribe') + + # Validate parameters + if task not in ['transcribe', 'translate']: + return {"error": "Task must be 'transcribe' or 'translate'"}, 400 + + if language not in ['en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh']: + return {"error": "Unsupported language code"}, 400 + + # Validate audio data + if not self.validate_audio_data(audio_data, audio_format): + return {"error": "Invalid audio data or format"}, 400 + + start_time = time.time() + + # Load model if not already loaded + if not self.model_loaded: + self.load_model() + + # Transcribe audio + if self.model_loaded and self.model: + # TODO: Replace with actual model inference + # result = self.model.transcribe(audio_data, language, task) + # text = result['text'] + # confidence = result['confidence'] + # detected_language = result['language'] + # duration = result['duration'] + + # Mock transcription result + text = f"[MOCK] Transcribed audio in {language}: This is a sample transcription of audio data." + confidence = 0.85 + detected_language = language + duration = 5.0 + else: + # Fallback mock transcription + text = f"[MOCK] Transcribed audio in {language}: This is a sample transcription of audio data." + confidence = 0.75 + detected_language = language + duration = 5.0 + + processing_time = time.time() - start_time + + return { + "text": text, + "language": detected_language, + "confidence": confidence, + "duration": duration, + "processing_time": processing_time, + "model_used": "whisper-base" if self.model_loaded else "mock" + } + + except Exception as e: + logger.error(f"Transcription failed: {e}") + return {"error": "Transcription failed"}, 500 + +# Register the endpoint +api.add_resource(TranscribeEndpoint, '/') + +# Health check for transcribe endpoint +@transcribe_bp.route('/health', methods=['GET']) +def health_check(): + """Health check for transcribe endpoint.""" + return jsonify({ + "status": "healthy", + "endpoint": "transcribe", + "model_loaded": TranscribeEndpoint().model_loaded + }) diff --git a/src/unified_api_server.py b/src/unified_api_server.py new file mode 100644 index 000000000..3223be317 --- /dev/null +++ b/src/unified_api_server.py @@ -0,0 +1,20 @@ +from flask import Flask, jsonify +from flask_cors import CORS +from auth import require_api_key +from rate_limiter import rate_limit + +app = Flask(__name__) +CORS(app) # Enable CORS for all routes + +@app.route('/api/health') +def health(): + return jsonify({'status': 'healthy'}) + +@app.route('/api/protected', methods=['POST']) +@require_api_key +@rate_limit(max_requests=10, window_minutes=1) +def protected(): + return jsonify({'message': 'Protected endpoint'}) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000)