""" Enhanced Public Speaking Coach with PERSONALIZED LLM Tips and Avatar Voice Includes: Speech Analysis + OpenAI-Powered Personalized Tips + Text-to-Speech Avatar """ import os import io import json import logging import warnings import re import uuid from typing import Dict, List, Any, Optional, Tuple from dataclasses import dataclass, asdict from pathlib import Path import torch import librosa import numpy as np import soundfile as sf from scipy.signal import medfilt from scipy.stats import zscore import textstat from TTS.api import TTS # Suppress warnings os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" logging.getLogger("whisper").setLevel(logging.ERROR) logging.getLogger("transformers").setLevel(logging.ERROR) warnings.filterwarnings("ignore") # Validate Whisper installation try: import whisper if not hasattr(whisper, "load_model"): raise ImportError("Wrong whisper library installed") except ImportError: print("\n❌ CRITICAL: Install correct whisper library:") print(" pip uninstall -y whisper && pip install openai-whisper") exit(1) # Import transformers for LLM try: from transformers import ( pipeline, AutoTokenizer, AutoModel, AutoModelForSequenceClassification, AutoModelForCausalLM ) from sentence_transformers import SentenceTransformer except ImportError: print("\n❌ CRITICAL: Install required libraries:") print(" pip install transformers sentence-transformers torch") exit(1) # Import OpenAI for better tips generation try: import openai OPENAI_AVAILABLE = True except ImportError: print("\n⚠️ WARNING: OpenAI not installed. Using fallback tips.") print(" To enable better tips: pip install openai") OPENAI_AVAILABLE = False # Import TTS try: from TTS.api import TTS as CoquiTTS except ImportError: print("\n⚠️ WARNING: TTS not installed. Avatar voice will be disabled.") print(" To enable: pip install TTS") CoquiTTS = None # JSON Serialization Helper class NumpyEncoder(json.JSONEncoder): """Handles numpy types in JSON serialization""" def default(self, obj): if isinstance(obj, (np.integer, np.int64)): return int(obj) if isinstance(obj, (np.floating, np.float32, np.float64)): return float(obj) if isinstance(obj, np.ndarray): return obj.tolist() return super().default(obj) class EnhancedPublicSpeakingCoach: """ Complete speech analysis engine with LLM tips and avatar voice """ def __init__(self, whisper_model_size: str = "base", enable_tts: bool = True, openai_api_key: Optional[str] = None): """ Initialize the enhanced coach engine Args: whisper_model_size: Whisper model size (tiny/base/small/medium) enable_tts: Enable text-to-speech avatar voice generation openai_api_key: OpenAI API key for better tips (optional) """ self.device = "cuda" if torch.cuda.is_available() else "cpu" print(f"🚀 Initializing Enhanced Coach on {self.device}...") # Set up OpenAI if available self.use_openai = False if OPENAI_AVAILABLE and openai_api_key: openai.api_key = openai_api_key self.use_openai = True print(" ✅ OpenAI enabled for personalized tips") # Load Whisper for transcription print(f" Loading Whisper ({whisper_model_size})...") self.whisper = whisper.load_model(whisper_model_size, device=self.device) # Load sentiment analysis model (using a more reliable one) print(" Loading Sentiment Model...") try: # Using cardiffnlp/twitter-roberta-base-sentiment-latest - more accurate self.sentiment_analyzer = pipeline( "sentiment-analysis", model="cardiffnlp/twitter-roberta-base-sentiment-latest", device=0 if self.device == "cuda" else -1 ) print(" ✅ Using RoBERTa sentiment model") except Exception as e: print(f" ⚠️ Failed to load RoBERTa model, falling back to DistilBERT: {e}") self.sentiment_analyzer = pipeline( "sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english", device=0 if self.device == "cuda" else -1 ) # Load sentence transformer for semantic analysis print(" Loading Sentence Transformer...") self.sentence_model = SentenceTransformer('all-MiniLM-L6-v2') # Load TTS for avatar voice self.tts_enabled = False self.tts_model = None if enable_tts and CoquiTTS: try: print(" Loading TTS for Avatar Voice...") # Using lightweight TTS model self.tts_model = CoquiTTS(model_name="tts_models/en/ljspeech/tacotron2-DDC") self.tts_enabled = True print(" ✅ TTS enabled") except Exception as e: print(f" ⚠️ TTS initialization failed: {e}") self.tts_enabled = False # Linguistic patterns self.filler_patterns = { "um": r"\bum+h*\b", "uh": r"\buh+h*\b", "like": r"\blike\b", "you know": r"\byou know\b", "so": r"\bso+\b", "actually": r"\bactually\b", "basically": r"\bbasically\b", "literally": r"\bliterally\b", "i mean": r"\bi mean\b", "kind of": r"\bkind of\b", "sort of": r"\bsort of\b", "right": r"\bright\b", "okay": r"\bokay\b", "well": r"\bwell\b" } self.power_words = { "significant", "critical", "essential", "vital", "crucial", "important", "remarkable", "extraordinary", "exceptional", "achieve", "accomplish", "create", "develop", "innovate", "transform", "revolutionize", "enhance", "optimize", "evidence", "data", "research", "proven", "demonstrate", "validate", "verify", "confirm", "establish", "believe", "imagine", "discover", "realize", "understand", "recognize", "appreciate", "consider", "envision", "opportunity", "benefit", "advantage", "solution", "success", "excellence", "quality", "value", "impact", "results", "together", "collaborate", "participate", "engage", "contribute" } print("✅ Enhanced Coach Engine Ready!") def _calculate_overall_score( self, pacing: Dict, prosody: Dict, fillers: Dict, silences: Dict, sentiment: Dict, vocabulary: Dict, logical_flow: Dict, coherence: Dict, persuasion: Dict ) -> float: """ Calculate overall score (0-10 scale) based on all metrics Weighted scoring system: - Pacing: 10% - Prosody: 10% - Fillers: 15% (fewer is better) - Silences: 10% - Sentiment: 10% - Vocabulary: 15% - Logical Flow: 10% - Coherence: 10% - Persuasion: 10% """ total_score = 0.0 # 1. Pacing Score (10%) - 120-160 WPM is ideal wpm = pacing['words_per_minute'] if 120 <= wpm <= 160: pacing_score = 10.0 elif 100 <= wpm < 120 or 160 < wpm <= 180: pacing_score = 7.0 elif 80 <= wpm < 100 or 180 < wpm <= 200: pacing_score = 5.0 else: pacing_score = 3.0 total_score += pacing_score * 0.10 # 2. Prosody Score (10%) - dynamic is good if prosody['category'].lower() == 'dynamic': prosody_score = 10.0 elif prosody['category'].lower() == 'monotone': prosody_score = 4.0 else: prosody_score = 7.0 total_score += prosody_score * 0.10 # 3. Filler Words Score (15%) - fewer is better total_fillers = sum(fillers.values()) if total_fillers == 0: filler_score = 10.0 elif total_fillers <= 3: filler_score = 9.0 elif total_fillers <= 5: filler_score = 7.0 elif total_fillers <= 10: filler_score = 5.0 else: filler_score = max(2.0, 10.0 - (total_fillers * 0.3)) total_score += filler_score * 0.15 # 4. Silences Score (10%) - 2-5 pauses is ideal silence_count = silences['count'] if 2 <= silence_count <= 5: silence_score = 10.0 elif silence_count <= 8: silence_score = 8.0 elif silence_count == 0 or silence_count == 1: silence_score = 6.0 else: silence_score = max(3.0, 10.0 - (silence_count * 0.5)) total_score += silence_score * 0.10 # 5. Sentiment Score (10%) - positive is best sentiment_type = sentiment['dominant_sentiment'].lower() confidence = sentiment['confidence'] if sentiment_type == 'positive': sentiment_score = 8.0 + (confidence * 2.0) elif sentiment_type == 'neutral': sentiment_score = 6.0 + (confidence * 1.0) else: # negative sentiment_score = max(3.0, 7.0 - (confidence * 3.0)) total_score += sentiment_score * 0.10 # 6. Vocabulary Score (15%) - convert 0-100 to 0-10 vocab_score = vocabulary['score'] / 10.0 total_score += vocab_score * 0.15 # 7. Logical Flow Score (10%) - convert 0-100 to 0-10 flow_score = logical_flow['score'] / 10.0 total_score += flow_score * 0.10 # 8. Coherence Score (10%) - convert 0-100 to 0-10 coherence_score = coherence['score'] / 10.0 total_score += coherence_score * 0.10 # 9. Persuasion Score (10%) - convert 0-100 to 0-10 persuasion_score = persuasion['score'] / 10.0 total_score += persuasion_score * 0.10 # Ensure score is in 0-10 range final_score = max(0.0, min(10.0, total_score)) print(f" 📊 Overall Score Calculation:") print(f" Pacing: {pacing_score:.1f} (weight: 10%)") print(f" Prosody: {prosody_score:.1f} (weight: 10%)") print(f" Fillers: {filler_score:.1f} (weight: 15%)") print(f" Silences: {silence_score:.1f} (weight: 10%)") print(f" Sentiment: {sentiment_score:.1f} (weight: 10%)") print(f" Vocabulary: {vocab_score:.1f} (weight: 15%)") print(f" Flow: {flow_score:.1f} (weight: 10%)") print(f" Coherence: {coherence_score:.1f} (weight: 10%)") print(f" Persuasion: {persuasion_score:.1f} (weight: 10%)") print(f" ⭐ FINAL OVERALL SCORE: {final_score:.2f}/10") return round(final_score, 2) def analyze_speech(self, audio_path: str, output_dir: str = "/tmp/audio_outputs", enable_tts: bool = True, avatar_gender: str = 'male') -> Dict[str, Any]: """ Main analysis pipeline with LLM tips and avatar voice Args: audio_path: Path to audio file output_dir: Directory to save generated audio files Returns: Complete analysis as JSON-serializable dictionary with avatar audio """ # Validation if not os.path.exists(audio_path): return {"error": "Audio file not found"} # Create output directory os.makedirs(output_dir, exist_ok=True) print(f"\n🎤 Analyzing: {os.path.basename(audio_path)}") try: # Load audio audio, sr = self._load_audio(audio_path) duration = len(audio) / sr if duration < 1.0: return {"error": "Audio too short (minimum 1 second)"} print(f" Duration: {duration:.1f}s") # Step 1: Transcription print(" 📝 Transcribing...") transcript_data = self._transcribe_with_timestamps(audio) if not transcript_data['text'].strip(): return {"error": "No speech detected"} full_transcription = transcript_data['text'] words = transcript_data['words'] # Step 2-10: All analysis print(" ⚡ Running analysis...") pacing_result = self._analyze_pacing(words, duration) prosody_result = self._analyze_prosody(audio, sr) filler_result = self._detect_fillers_detailed(full_transcription) silence_result = self._detect_silences(words) sentiment_result = self._analyze_sentiment(full_transcription) vocabulary_result = self._analyze_vocabulary(full_transcription, words) logical_flow_result = self._analyze_logical_flow(full_transcription) coherence_result = self._analyze_coherence(full_transcription) persuasion_result = self._analyze_persuasion(full_transcription) # ⭐ NEW: Calculate overall score print(" 🎯 Calculating overall score...") overall_score = self._calculate_overall_score( pacing_result, prosody_result, filler_result, silence_result, sentiment_result, vocabulary_result, logical_flow_result, coherence_result, persuasion_result ) # Step 11: Generate personalized tips using LLM print(" 🤖 Generating personalized tips...") personalized_tips = self._generate_personalized_tips( full_transcription, pacing_result, prosody_result, filler_result, silence_result, sentiment_result, vocabulary_result, logical_flow_result, coherence_result, persuasion_result, overall_score ) # Step 12: Create improved version of transcript print(" ✨ Creating improved transcript...") improved_transcript = self._create_improved_transcript( full_transcription, filler_result ) # Step 13: Generate avatar voice (if enabled) - TWO SEPARATE AUDIOS avatar_audio_url = None tips_audio_url = None if self.tts_enabled and self.tts_model and enable_tts: # Generate audio for improved transcript print(" 🎙️ Generating avatar voice for improved transcript...") avatar_audio_url = self._generate_avatar_voice( improved_transcript, output_dir, gender=avatar_gender, prefix="improved" ) # Generate audio for coaching tips print(" 🎙️ Generating avatar voice for coaching tips...") tips_text = self._format_tips_for_audio(personalized_tips, avatar_gender) tips_audio_url = self._generate_avatar_voice( tips_text, output_dir, gender=avatar_gender, prefix="tips" ) # Compile final result result = { "transcription": full_transcription, "duration_seconds": round(duration, 2), "word_count": len(words), # ⭐ NEW: Overall score (0-10 scale) "overall_score": overall_score, "pacing": pacing_result, "prosody": prosody_result, "filler_words": filler_result, "silence_detection": silence_result, "sentiment_analysis": sentiment_result, "vocabulary": vocabulary_result, "logical_flow": logical_flow_result, "coherence": coherence_result, "persuasion": persuasion_result, # NEW: LLM-generated content "personalized_tips": personalized_tips, "improved_transcript": improved_transcript, # NEW: Separate audio URLs "avatar_audio_url": avatar_audio_url, # For improved transcript "tips_audio_url": tips_audio_url # For coaching tips } print("✅ Analysis complete!") return result except Exception as e: import traceback traceback.print_exc() return {"error": f"Analysis failed: {str(e)}"} def _load_audio(self, path: str) -> tuple: """Load and normalize audio to 16kHz mono""" try: audio, sr = librosa.load(path, sr=16000, mono=True) audio = librosa.util.normalize(audio) return audio, sr except Exception as e: raise ValueError(f"Failed to load audio: {e}") def _transcribe_with_timestamps(self, audio: np.ndarray) -> Dict: """Transcribe with word-level timestamps""" result = self.whisper.transcribe( audio, language='en', word_timestamps=True, fp16=(self.device == "cuda") ) words = [] for segment in result['segments']: if 'words' in segment: for word_info in segment['words']: words.append({ 'word': word_info['word'].strip(), 'start': word_info['start'], 'end': word_info['end'] }) return { 'text': result['text'].strip(), 'words': words } def _analyze_pacing(self, words: List[Dict], duration: float) -> Dict: """Analyze speaking pace""" word_count = len(words) wpm = (word_count / duration * 60) if duration > 0 else 0 if wpm < 120: category = "slow" elif wpm <= 160: category = "good" else: category = "fast" return { "category": category, "words_per_minute": round(wpm, 1) } def _analyze_prosody(self, audio: np.ndarray, sr: int) -> Dict: """Analyze prosody (pitch variation)""" try: f0 = librosa.yin(audio.astype(np.float64), fmin=80, fmax=400) f0_clean = f0[f0 > 0] if len(f0_clean) > 10: pitch_std = np.std(f0_clean) category = "monotone" if pitch_std < 25 else "dynamic" return { "category": category, "pitch_variation_hz": round(float(pitch_std), 1) } else: return {"category": "unknown", "pitch_variation_hz": 0.0} except Exception as e: logging.warning(f"Prosody analysis failed: {e}") return {"category": "unknown", "pitch_variation_hz": 0.0} def _detect_fillers_detailed(self, text: str) -> Dict: """Detect filler words with counts""" text_lower = text.lower() filler_counts = {} for filler_name, pattern in self.filler_patterns.items(): matches = re.findall(pattern, text_lower, re.IGNORECASE) count = len(matches) if count > 0: filler_counts[filler_name] = count return filler_counts def _detect_silences(self, words: List[Dict]) -> Dict: """Detect long pauses/silences""" if len(words) < 2: return {"count": 0, "total_silence_duration_seconds": 0.0} silence_threshold = 2.0 silence_count = 0 total_silence_duration = 0.0 for i in range(len(words) - 1): pause_duration = words[i+1]['start'] - words[i]['end'] if pause_duration >= silence_threshold: silence_count += 1 total_silence_duration += pause_duration return { "count": silence_count, "total_silence_duration_seconds": round(total_silence_duration, 2) } def _analyze_sentiment(self, text: str) -> Dict: """Analyze dominant sentiment with improved accuracy""" try: # Clean the text text = text.strip() if not text: return {"dominant_sentiment": "neutral", "confidence": 0.0} print(f" 🔍 Analyzing sentiment for text length: {len(text)} chars") # Split into sentences for better analysis sentences = re.split(r'[.!?]+', text) sentences = [s.strip() for s in sentences if len(s.strip()) > 5] if not sentences: return {"dominant_sentiment": "neutral", "confidence": 0.0} print(f" 📊 Processing {len(sentences)} sentences") # Analyze each sentence sentiment_scores = {"positive": 0, "neutral": 0, "negative": 0} for sentence in sentences: if len(sentence) < 5: continue try: # Truncate to model's max length sentence_truncated = sentence[:512] result = self.sentiment_analyzer(sentence_truncated)[0] label = result['label'].lower() score = result['score'] # Handle different model output formats if 'positive' in label or label == 'pos': sentiment_scores['positive'] += score elif 'negative' in label or label == 'neg': sentiment_scores['negative'] += score elif 'neutral' in label or label == 'neu': sentiment_scores['neutral'] += score else: # If label doesn't match expected format, treat as neutral sentiment_scores['neutral'] += 0.5 print(f" Sentence: '{sentence[:50]}...' -> {label} ({score:.3f})") except Exception as e: print(f" ⚠️ Failed to analyze sentence: {e}") sentiment_scores['neutral'] += 0.5 # Determine dominant sentiment dominant = max(sentiment_scores, key=sentiment_scores.get) total_score = sum(sentiment_scores.values()) confidence = sentiment_scores[dominant] / total_score if total_score > 0 else 0.0 print(f" 📈 Sentiment scores: {sentiment_scores}") print(f" 🎯 Dominant: {dominant} with confidence {confidence:.3f}") return { "dominant_sentiment": dominant, "confidence": round(confidence, 3) } except Exception as e: logging.error(f"Sentiment analysis failed: {e}") import traceback traceback.print_exc() return {"dominant_sentiment": "neutral", "confidence": 0.0} def _analyze_vocabulary(self, text: str, words: List[Dict]) -> Dict: """Analyze vocabulary quality""" word_list = [w['word'].lower().strip('.,!?;:') for w in words] good_words_found = [] for word in word_list: if word in self.power_words and word not in good_words_found: good_words_found.append(word) unique_words = len(set(word_list)) total_words = len(word_list) diversity_ratio = (unique_words / total_words) if total_words > 0 else 0 score = 0 score += min(40, len(good_words_found) * 5) score += min(40, diversity_ratio * 80) if unique_words >= 100: score += 20 elif unique_words >= 50: score += 15 elif unique_words >= 25: score += 10 else: score += 5 return { "score": round(score), "good_words_used": sorted(good_words_found) } def _analyze_logical_flow(self, text: str) -> Dict: """Analyze logical flow""" try: sentences = re.split(r'[.!?]+', text) sentences = [s.strip() for s in sentences if len(s.strip()) > 10] if len(sentences) < 2: return {"score": 50, "flow_quality": "insufficient_data"} embeddings = self.sentence_model.encode(sentences) similarities = [] for i in range(len(embeddings) - 1): similarity = np.dot(embeddings[i], embeddings[i + 1]) / ( np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i + 1]) ) similarities.append(similarity) avg_similarity = np.mean(similarities) if 0.3 <= avg_similarity <= 0.7: score = 70 + (30 * (1 - abs(avg_similarity - 0.5) / 0.2)) elif avg_similarity < 0.3: score = 40 + (avg_similarity / 0.3) * 30 else: score = 70 - ((avg_similarity - 0.7) / 0.3) * 30 score = max(0, min(100, score)) if score >= 80: quality = "excellent" elif score >= 65: quality = "good" elif score >= 50: quality = "moderate" else: quality = "needs_improvement" return {"score": round(score), "flow_quality": quality} except Exception as e: logging.warning(f"Logical flow analysis failed: {e}") return {"score": 50, "flow_quality": "error"} def _analyze_coherence(self, text: str) -> Dict: """Analyze coherence""" try: sentences = re.split(r'[.!?]+', text) sentences = [s.strip() for s in sentences if len(s.strip()) > 10] if len(sentences) < 2: return {"score": 50, "coherence_quality": "insufficient_data"} discourse_markers = [ "however", "therefore", "moreover", "furthermore", "additionally", "consequently", "nevertheless", "thus", "hence", "meanwhile", "first", "second", "third", "finally", "in conclusion", "for example", "for instance", "in particular", "specifically" ] text_lower = text.lower() marker_count = sum(1 for marker in discourse_markers if marker in text_lower) embeddings = self.sentence_model.encode(sentences) coherence_scores = [] for i in range(len(embeddings)): for j in range(i + 1, min(i + 3, len(embeddings))): similarity = np.dot(embeddings[i], embeddings[j]) / ( np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[j]) ) coherence_scores.append(similarity) avg_coherence = np.mean(coherence_scores) if coherence_scores else 0.5 score = 0 score += min(60, avg_coherence * 100) score += min(40, marker_count * 5) score = max(0, min(100, score)) if score >= 80: quality = "excellent" elif score >= 65: quality = "good" elif score >= 50: quality = "moderate" else: quality = "needs_improvement" return {"score": round(score), "coherence_quality": quality} except Exception as e: logging.warning(f"Coherence analysis failed: {e}") return {"score": 50, "coherence_quality": "error"} def _analyze_persuasion(self, text: str) -> Dict: """Analyze persuasive elements""" try: text_lower = text.lower() score = 0 logical_connectors = [ "therefore", "thus", "consequently", "hence", "accordingly", "because", "since", "as a result", "for this reason" ] evidence_markers = [ "research shows", "studies indicate", "data suggests", "according to", "evidence demonstrates", "proven by" ] appeal_markers = [ "imagine", "consider", "think about", "what if", "picture this", "envision" ] credibility_markers = [ "expert", "research", "study", "proven", "validated", "established", "recognized" ] score += min(25, sum(1 for c in logical_connectors if c in text_lower) * 5) score += min(25, sum(1 for m in evidence_markers if m in text_lower) * 8) score += min(25, sum(1 for m in appeal_markers if m in text_lower) * 6) score += min(25, sum(1 for m in credibility_markers if m in text_lower) * 5) score = max(0, min(100, score)) if score >= 80: level = "highly_persuasive" elif score >= 60: level = "persuasive" elif score >= 40: level = "moderately_persuasive" else: level = "needs_improvement" return {"score": round(score), "persuasion_level": level} except Exception as e: logging.warning(f"Persuasion analysis failed: {e}") return {"score": 50, "persuasion_level": "error"} def _generate_personalized_tips( self, transcript: str, pacing: Dict, prosody: Dict, fillers: Dict, silences: Dict, sentiment: Dict, vocabulary: Dict, logical_flow: Dict, coherence: Dict, persuasion: Dict, overall_score: float ) -> List[str]: """Generate truly personalized tips using OpenAI or enhanced fallback""" # Try OpenAI first if available if self.use_openai: try: tips = self._generate_openai_tips( transcript, pacing, prosody, fillers, silences, sentiment, vocabulary, logical_flow, coherence, persuasion, overall_score ) if tips and len(tips) >= 3: return tips except Exception as e: logging.warning(f"OpenAI tip generation failed: {e}") # Use enhanced fallback tips return self._generate_enhanced_fallback_tips( transcript, pacing, prosody, fillers, silences, sentiment, vocabulary, logical_flow, coherence, persuasion, overall_score ) def _generate_openai_tips( self, transcript: str, pacing: Dict, prosody: Dict, fillers: Dict, silences: Dict, sentiment: Dict, vocabulary: Dict, logical_flow: Dict, coherence: Dict, persuasion: Dict, overall_score: float ) -> List[str]: """Generate personalized tips using OpenAI API""" # Build detailed analysis summary analysis_summary = f"""Speech Performance Analysis: Overall Score: {overall_score}/10 Detailed Metrics: - Pacing: {pacing['category']} at {pacing['words_per_minute']} words per minute - Voice Variation: {prosody['category']} (pitch variation: {prosody['pitch_variation_hz']} Hz) - Filler Words: {sum(fillers.values())} total ({', '.join([f'{k}: {v}' for k, v in fillers.items()]) if fillers else 'none'}) - Pauses: {silences['count']} long pauses - Tone: {sentiment['dominant_sentiment']} ({sentiment['confidence']:.0%} confidence) - Vocabulary: {vocabulary['score']}/100 (used {len(vocabulary['good_words_used'])} power words) - Logical Flow: {logical_flow['flow_quality']} ({logical_flow['score']}/100) - Coherence: {coherence['coherence_quality']} ({coherence['score']}/100) - Persuasiveness: {persuasion['persuasion_level']} ({persuasion['score']}/100) Speech excerpt: "{transcript[:200]}..." """ # Create personalized prompt prompt = f"""{analysis_summary} You are a friendly, encouraging public speaking coach. Based on this person's speech analysis, provide 5 specific, actionable coaching tips. Requirements: 1. Be warm, supportive, and encouraging 2. Focus on the 2-3 weakest areas that need improvement 3. Give concrete examples for each tip (e.g., "Instead of saying 'um,' try pausing silently for 1-2 seconds") 4. Use conversational, friendly language as if speaking to a friend 5. Celebrate what they're doing well while gently addressing areas to improve 6. Make tips practical and easy to implement immediately Format each tip as a complete, friendly sentence. Number them 1-5.""" try: response = openai.ChatCompletion.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "You are an expert public speaking coach who gives personalized, friendly, actionable advice."}, {"role": "user", "content": prompt} ], max_tokens=500, temperature=0.8 ) content = response.choices[0].message.content.strip() # Parse tips tips = [] for line in content.split('\n'): line = line.strip() # Remove numbering line = re.sub(r'^\d+[\.\):\-]\s*', '', line) if len(line) > 20: # Valid tip tips.append(line) return tips[:5] except Exception as e: logging.error(f"OpenAI API error: {e}") return [] def _generate_enhanced_fallback_tips( self, transcript: str, pacing: Dict, prosody: Dict, fillers: Dict, silences: Dict, sentiment: Dict, vocabulary: Dict, logical_flow: Dict, coherence: Dict, persuasion: Dict, overall_score: float ) -> List[str]: """Generate personalized, friendly tips with examples (fallback)""" tips = [] # Calculate what needs improvement most scores = { 'pacing': self._get_pacing_score(pacing), 'prosody': self._get_prosody_score(prosody), 'fillers': self._get_filler_score(fillers), 'silences': self._get_silence_score(silences), 'vocabulary': vocabulary['score'] / 10.0, 'flow': logical_flow['score'] / 10.0, 'coherence': coherence['score'] / 10.0, 'persuasion': persuasion['score'] / 10.0 } # Sort by score (lowest first = needs most improvement) improvement_areas = sorted(scores.items(), key=lambda x: x[1]) # Generate tips for weakest areas wpm = pacing['words_per_minute'] total_fillers = sum(fillers.values()) for area, score in improvement_areas[:5]: # Top 5 areas needing improvement if area == 'pacing': if pacing['category'] == 'slow': tips.append(f"Your pace is currently {wpm} words per minute. Try speeding up to 130-140 WPM - imagine you're telling an exciting story to a friend! Practice by reading aloud with a timer.") elif pacing['category'] == 'fast': tips.append(f"You're speaking at {wpm} words per minute, which is pretty fast! Slow down to about 140-150 WPM. Take a breath between sentences - your audience needs time to absorb your ideas.") elif area == 'prosody': if prosody['category'] == 'monotone': tips.append(f"Add more vocal variety to keep your audience engaged! Try emphasizing key words - for example, if you say 'This is REALLY important,' make 'really' louder and higher pitched. Practice reading children's books out loud to build this skill.") elif area == 'fillers': if total_fillers > 5: most_used = max(fillers.items(), key=lambda x: x[1]) tips.append(f"You said '{most_used[0]}' {most_used[1]} times. When you feel the urge to say it, pause silently instead - it makes you sound more confident! Try counting to 2 in your head during pauses.") elif area == 'silences': if silences['count'] > 5: tips.append(f"You had {silences['count']} long pauses. That's okay! But try to keep pauses to 1-2 seconds. If you need to think, it's better to say 'Let me think about that...' than to go silent for too long.") elif silences['count'] < 2: tips.append(f"Don't be afraid to pause! Strategic 2-second pauses after important points give your audience time to process. Try pausing after questions like 'Why does this matter?' - it creates anticipation.") elif area == 'vocabulary': if vocabulary['score'] < 60: good_words = vocabulary['good_words_used'] if good_words: tips.append(f"Great job using power words like '{', '.join(good_words[:3])}'! Try adding more impact words like 'crucial,' 'remarkable,' or 'transform' to make your speech more memorable.") else: tips.append(f"Spice up your vocabulary! Instead of 'very good,' try 'excellent' or 'outstanding.' Instead of 'big problem,' say 'significant challenge.' Keep a list of power words on your phone!") elif area == 'flow': if logical_flow['score'] < 65: tips.append(f"Connect your ideas more smoothly! Use transition phrases like 'Building on that...', 'Here's why this matters...', or 'Let me give you an example...' - they're like road signs that guide your audience through your speech.") elif area == 'coherence': if coherence['score'] < 65: tips.append(f"Make your main message crystal clear! Try using signpost phrases: 'There are three reasons why...' or 'My main point is...' Then at the end, say 'To sum up...' and restate your key idea.") elif area == 'persuasion': if persuasion['score'] < 60: tips.append(f"Make your speech more convincing! Add phrases like 'Research shows that...' or 'Imagine if we could...' or 'The evidence is clear...' These make your points more compelling and credible.") # If we don't have 5 tips yet, add some positive encouragement if len(tips) < 5 and overall_score >= 7.0: tips.append(f"You're doing great with a {overall_score:.1f}/10 score! Keep practicing regularly - even 5 minutes a day of reading aloud can make a huge difference in your confidence and delivery.") # Always add one encouraging tip at the end if len(tips) < 5: if overall_score < 5.0: tips.append("Remember, every great speaker started somewhere! Focus on improving one thing at a time, and you'll see amazing progress. Record yourself weekly to track your improvement!") else: tips.append("You're making good progress! Keep recording yourself and listening back - you'll be surprised how quickly you improve. Consider joining a speaking group like Toastmasters to practice regularly!") return tips[:5] def _get_pacing_score(self, pacing: Dict) -> float: """Convert pacing to 0-10 score""" wpm = pacing['words_per_minute'] if 120 <= wpm <= 160: return 10.0 elif 100 <= wpm < 120 or 160 < wpm <= 180: return 7.0 else: return 4.0 def _get_prosody_score(self, prosody: Dict) -> float: """Convert prosody to 0-10 score""" return 10.0 if prosody['category'] == 'dynamic' else 4.0 def _get_filler_score(self, fillers: Dict) -> float: """Convert filler count to 0-10 score""" total = sum(fillers.values()) if total == 0: return 10.0 elif total <= 3: return 9.0 elif total <= 5: return 7.0 else: return max(2.0, 10.0 - (total * 0.3)) def _get_silence_score(self, silences: Dict) -> float: """Convert silence count to 0-10 score""" count = silences['count'] if 2 <= count <= 5: return 10.0 elif count <= 8: return 8.0 else: return max(3.0, 10.0 - (count * 0.5)) def _format_tips_for_audio(self, tips: List[str], gender: str) -> str: """Format tips in a natural, conversational way for audio""" avatar_name = "Alex" if gender == "male" else "Maya" # Create a friendly introduction intro = f"Hey there! I'm {avatar_name}, your speaking coach. I've analyzed your speech, and I have some personalized tips to help you shine even brighter!" # Add natural transitions between tips transitions = [ "First,", "Next up,", "Here's another tip:", "Also, I noticed that", "And finally," ] # Build the audio script audio_parts = [intro] for i, tip in enumerate(tips[:5]): if i < len(transitions): audio_parts.append(f"{transitions[i]} {tip}") else: audio_parts.append(tip) # Add encouraging conclusion conclusion = "You're making great progress! Keep practicing these tips, and you'll see amazing results. I'm cheering for you!" audio_parts.append(conclusion) return " ".join(audio_parts) def _create_improved_transcript(self, original: str, fillers: Dict) -> str: """Create improved version of transcript (remove fillers, clean up)""" improved = original # Remove filler words for filler_name, pattern in self.filler_patterns.items(): if filler_name in fillers: # Replace fillers with nothing or appropriate punctuation improved = re.sub(pattern, '', improved, flags=re.IGNORECASE) # Clean up multiple spaces improved = re.sub(r'\s+', ' ', improved) # Fix punctuation improved = re.sub(r'\s+([,.!?])', r'\1', improved) # Capitalize first letter of sentences improved = re.sub(r'(^|[.!?]\s+)([a-z])', lambda m: m.group(1) + m.group(2).upper(), improved) return improved.strip() def _generate_avatar_voice(self, text: str, output_dir: str, gender: str = "male", prefix: str = "avatar") -> Optional[str]: """ Generate avatar voice audio using TTS Args: text: Text to synthesize output_dir: Directory to save audio file gender: Avatar gender ("male" or "female") prefix: Filename prefix (e.g., "improved", "tips") Returns: Relative path to generated audio file or None if generation fails """ try: if not self.tts_enabled or not self.tts_model: print(" ⚠️ TTS not enabled, skipping avatar voice generation") return None # Generate unique filename with prefix audio_filename = f"{prefix}_{uuid.uuid4()}.wav" audio_path = os.path.join(output_dir, audio_filename) # Truncate text if too long (TTS models have limits) max_length = 1000 # characters if len(text) > max_length: text = text[:max_length] + "..." print(f" ⚠️ Text truncated to {max_length} characters for TTS") # Generate audio using TTS print(f" 🎙️ Generating {gender} {prefix} audio...") self.tts_model.tts_to_file(text=text, file_path=audio_path) # Return relative path (assuming output_dir is served) return f"/audio/{audio_filename}" except Exception as e: logging.error(f"Avatar voice generation failed: {e}") import traceback traceback.print_exc() return None # ================= MAIN ================= if __name__ == "__main__": print("\n" + "="*70) print("ENHANCED PUBLIC SPEAKING COACH - TEST") print("="*70 + "\n") test_file = "test_speech.wav" if not os.path.exists(test_file): print("⚠️ No test file found. Generating dummy audio...") sr = 16000 duration = 10 t = np.linspace(0, duration, sr * duration) audio = 0.3 * np.sin(2 * np.pi * 200 * t) + 0.2 * np.sin(2 * np.pi * 300 * t) audio += 0.1 * np.random.randn(len(audio)) sf.write(test_file, audio, sr) print(f"✅ Created {test_file}\n") try: # Get OpenAI API key from environment variable if available openai_key = os.getenv('OPENAI_API_KEY') coach = EnhancedPublicSpeakingCoach( whisper_model_size="base", enable_tts=True, openai_api_key=openai_key ) result = coach.analyze_speech(test_file) print("\n" + "="*70) print("ANALYSIS RESULTS (JSON)") print("="*70) print(json.dumps(result, indent=2, cls=NumpyEncoder)) output_file = "speech_analysis_result.json" with open(output_file, 'w') as f: json.dump(result, f, indent=2, cls=NumpyEncoder) print(f"\n✅ Results saved to: {output_file}") print("✅ Test completed successfully!") except Exception as e: print(f"\n❌ ERROR: {e}") import traceback traceback.print_exc()