File size: 8,072 Bytes
1ba09d2
 
93dd448
1ba09d2
 
15c1fed
 
1ba09d2
 
 
 
15c1fed
39e1ecc
15c1fed
93dd448
 
1ba09d2
15c1fed
b5e7f5f
15c1fed
1ba09d2
 
 
 
93dd448
 
1ba09d2
 
93dd448
15c1fed
 
93dd448
1ba09d2
15c1fed
 
 
 
93dd448
 
 
 
 
 
 
1ba09d2
cd7efca
1ba09d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93dd448
1ba09d2
 
 
 
 
 
 
 
 
 
15c1fed
 
 
1ba09d2
 
c5a8295
1ba09d2
 
 
c5a8295
 
1ba09d2
93dd448
 
 
 
1ba09d2
 
 
 
 
c5a8295
1ba09d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93dd448
1ba09d2
 
 
15c1fed
1ba09d2
 
 
 
 
 
93dd448
1ba09d2
 
 
 
 
d24f8d3
15c1fed
93dd448
15c1fed
1ba09d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46c44f7
1ba09d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd7fbd9
 
1ba09d2
 
 
 
 
 
 
 
cd7efca
1ba09d2
0e0a1bb
1ba09d2
 
 
 
 
46c44f7
1ba09d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46c44f7
15c1fed
1ba09d2
 
 
 
c5a8295
15c1fed
93dd448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ba09d2
c5a8295
1ba09d2
 
 
 
 
 
 
 
 
 
46c44f7
c5a8295
1ba09d2
 
 
 
 
 
 
 
 
 
 
 
 
15c1fed
1ba09d2
 
15c1fed
 
1ba09d2
 
 
 
93dd448
1ba09d2
cd7efca
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""
Production FastAPI Server for Public Speaking Coach
With LLM Tips and Avatar Voice Support
"""

import os
import shutil
import tempfile
from pathlib import Path
from typing import Optional

import uvicorn
from fastapi import FastAPI, UploadFile, File, HTTPException, status, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from kid_coach_pipeline import EnhancedPublicSpeakingCoach

# ================= APP CONFIGURATION =================

app = FastAPI(
    title="Public Speaking Coach API",
    description="AI-powered speech analysis with LLM tips and avatar voice",
    version="3.0.0"
)

# CORS Configuration
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Audio output directory
AUDIO_OUTPUT_DIR = "/tmp/audio_outputs"
os.makedirs(AUDIO_OUTPUT_DIR, exist_ok=True)

# Mount static files for audio serving
app.mount("/audio", StaticFiles(directory=AUDIO_OUTPUT_DIR), name="audio")

# Global engine instance
coach_engine: Optional[EnhancedPublicSpeakingCoach] = None

# Supported audio formats
SUPPORTED_FORMATS = {
    '.wav', '.mp3', '.m4a', '.flac', '.ogg', 
    '.wma', '.aac', '.mp4', '.webm'
}

# Maximum file size (50MB)
MAX_FILE_SIZE = 50 * 1024 * 1024


# ================= RESPONSE MODELS =================

class HealthResponse(BaseModel):
    """Health check response"""
    status: str
    engine_loaded: bool
    tts_enabled: bool
    supported_formats: list


class ErrorResponse(BaseModel):
    """Error response format"""
    error: str
    detail: Optional[str] = None


# ================= STARTUP/SHUTDOWN =================

@app.on_event("startup")
async def startup_event():
    """Initialize the coach engine on server start"""
    global coach_engine
    
    print("\n" + "="*60)
    print("๐Ÿš€ PUBLIC SPEAKING COACH API - STARTING")
    print("="*60)
    
    try:
        print("\n๐Ÿ“ฆ Loading AI models...")
        coach_engine = EnhancedPublicSpeakingCoach(
            whisper_model_size="base",
            enable_tts=True
        )
        print("โœ… Coach engine ready!")
        print("\n" + "="*60)
        print("๐ŸŽค API is ready to analyze speeches!")
        print("="*60 + "\n")
        
    except Exception as e:
        print(f"\nโŒ STARTUP FAILED: {e}")
        print("Server will start but analysis will not work.\n")
        coach_engine = None


@app.on_event("shutdown")
async def shutdown_event():
    """Cleanup on server shutdown"""
    print("\n๐Ÿ‘‹ Shutting down Public Speaking Coach API...")


# ================= ENDPOINTS =================

@app.get("/", response_model=HealthResponse)
async def root():
    """Root endpoint - API info"""
    return {
        "status": "online",
        "engine_loaded": coach_engine is not None,
        "tts_enabled": coach_engine.tts_enabled if coach_engine else False,
        "supported_formats": list(SUPPORTED_FORMATS)
    }


@app.get("/health", response_model=HealthResponse)
async def health_check():
    """Health check endpoint"""
    return {
        "status": "healthy" if coach_engine else "degraded",
        "engine_loaded": coach_engine is not None,
        "tts_enabled": coach_engine.tts_enabled if coach_engine else False,
        "supported_formats": list(SUPPORTED_FORMATS)
    }


@app.post("/coach")
async def analyze_speech(file: UploadFile = File(...), avatar_gender: str = Form('male')):
    """
    Main endpoint: Upload audio file and receive comprehensive analysis
    """
    
    # Check if engine is loaded
    if coach_engine is None:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Coach engine not initialized. Please contact administrator."
        )
    
    # Validate file exists
    if not file:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="No file provided"
        )
    
    # Validate filename
    if not file.filename:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid filename"
        )
    
    # Get file extension
    file_ext = Path(file.filename).suffix.lower()
    
    # Validate format
    if file_ext not in SUPPORTED_FORMATS:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Unsupported format '{file_ext}'. Supported: {', '.join(SUPPORTED_FORMATS)}"
        )
    
    # Create temporary file
    temp_file = None
    
    try:
        # Read file content
        content = await file.read()
        
        # Check file size
        if len(content) > MAX_FILE_SIZE:
            raise HTTPException(
                status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
                detail=f"File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB"
            )
        
        # Create temporary file with proper extension
        with tempfile.NamedTemporaryFile(
            delete=False,
            suffix=file_ext
        ) as temp:
            temp.write(content)
            temp_file = temp.name
        
        print(f"\n๐Ÿ“ Processing: {file.filename} ({len(content) / 1024:.1f} KB)")
        
        # Run analysis (FIXED: using temp_file instead of audio_path)
        result = coach_engine.analyze_speech(temp_file, enable_tts=True, avatar_gender=avatar_gender)
        
        # Check for analysis errors
        if "error" in result:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=result["error"]
            )
        
        print(f"โœ… Analysis complete")
        
        return JSONResponse(content=result)
    
    except HTTPException:
        # Re-raise HTTP exceptions
        raise
    
    except Exception as e:
        # Log unexpected errors
        import traceback
        print(f"\nโŒ ANALYSIS ERROR:")
        traceback.print_exc()
        
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Analysis failed: {str(e)}"
        )
    
    finally:
        # Cleanup temporary file
        if temp_file and os.path.exists(temp_file):
            try:
                os.remove(temp_file)
            except Exception as e:
                print(f"โš ๏ธ  Failed to delete temp file: {e}")


@app.post("/analyze")
async def analyze_speech_alias(file: UploadFile = File(...)):
    """Alias endpoint for /coach (for compatibility)"""
    return await analyze_speech(file)


@app.get("/audio/{filename}")
async def get_audio(filename: str):
    """Serve generated avatar audio files"""
    file_path = os.path.join(AUDIO_OUTPUT_DIR, filename)
    
    if not os.path.exists(file_path):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Audio file not found"
        )
    
    return FileResponse(
        file_path,
        media_type="audio/wav",
        filename=filename
    )


# ================= ERROR HANDLERS =================

@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    """Custom HTTP exception handler"""
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": exc.detail,
            "status_code": exc.status_code
        }
    )


@app.exception_handler(Exception)
async def general_exception_handler(request, exc):
    """Catch-all exception handler"""
    import traceback
    traceback.print_exc()
    
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={
            "error": "Internal server error",
            "detail": str(exc)
        }
    )


# ================= MAIN =================

if __name__ == "__main__":
    # For local development
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        log_level="info"
    )