"""REST endpoints for AI media generation — TTS, avatar videos.""" import base64 import json import logging from odoo import http from odoo.http import request, Response _logger = logging.getLogger(__name__) def _json_response(data, status=200): return Response(json.dumps(data, default=str), status=status, content_type="application/json") def _get_json(): try: return json.loads(request.httprequest.data or "{}") except Exception: return {} class MediaController(http.Controller): """Handles /api/exam/*/media and avatar endpoints from media.service.ts.""" def _get_tts_provider(self): return request.env["ir.config_parameter"].sudo().get_param("encoach_ai.tts_provider", "polly") def _get_tts(self): """Get the configured TTS provider.""" provider = self._get_tts_provider() if provider == "elevenlabs": from odoo.addons.encoach_ai.services.elevenlabs_service import ElevenLabsService return ElevenLabsService(request.env) from odoo.addons.encoach_ai.services.polly_service import PollyService return PollyService(request.env) def _synthesize(self, text, body): """Dispatch TTS call with correct kwargs for each provider.""" tts = self._get_tts() provider = self._get_tts_provider() if provider == "elevenlabs": gender = body.get("gender", "female") language = body.get("language", "en-GB") voice_key = f"{gender}_{'british' if 'GB' in language else 'american'}" return tts.synthesize(text, voice_id=body.get("voice_id"), voice_key=voice_key) return tts.synthesize( text, voice=body.get("voice"), language=body.get("language", "en-GB"), gender=body.get("gender", "female"), ) # ── POST /api/exam/listening/media — generate listening audio ── @http.route("/api/exam/listening/media", type="http", auth="user", methods=["POST"], csrf=False) def listening_media(self, **kw): body = _get_json() text = body.get("text", "") if not text: return _json_response({"error": "No text provided"}, 400) try: result = self._synthesize(text, body) audio_b64 = base64.b64encode(result["audio"]).decode() return _json_response({ "audio_base64": audio_b64, "content_type": result["content_type"], "voice": result.get("voice") or result.get("voice_id"), "characters": result["characters"], }) except Exception as e: _logger.exception("Listening media generation failed") return _json_response({"error": str(e)}, 500) # ── POST /api/exam/speaking/media — generate speaking prompt audio ── @http.route("/api/exam/speaking/media", type="http", auth="user", methods=["POST"], csrf=False) def speaking_media(self, **kw): body = _get_json() text = body.get("text", "") if not text: return _json_response({"error": "No text provided"}, 400) try: result = self._synthesize(text, body) audio_b64 = base64.b64encode(result["audio"]).decode() return _json_response({ "audio_base64": audio_b64, "content_type": result["content_type"], }) except Exception as e: return _json_response({"error": str(e)}, 500) # ── GET /api/exam/avatars — list ELAI avatars ── @http.route("/api/exam/avatars", type="http", auth="user", methods=["GET"], csrf=False) def list_avatars(self, **kw): try: from odoo.addons.encoach_ai.services.elai_service import ElaiService elai = ElaiService(request.env) avatars = elai.list_avatars() return _json_response({"avatars": avatars}) except Exception as e: return _json_response({"avatars": [], "note": str(e)}) # ── POST /api/exam/avatar/video — create avatar video ── @http.route("/api/exam/avatar/video", type="http", auth="user", methods=["POST"], csrf=False) def create_avatar_video(self, **kw): body = _get_json() try: from odoo.addons.encoach_ai.services.elai_service import ElaiService elai = ElaiService(request.env) result = elai.create_video( body.get("script", ""), avatar_id=body.get("avatar_id"), title=body.get("title", "EnCoach Video"), ) return _json_response(result) except Exception as e: return _json_response({"error": str(e)}, 500) # ── GET /api/exam/avatar/video/:id — check video status ── @http.route("/api/exam/avatar/video/", type="http", auth="user", methods=["GET"], csrf=False) def video_status(self, video_id, **kw): try: from odoo.addons.encoach_ai.services.elai_service import ElaiService elai = ElaiService(request.env) return _json_response(elai.get_video_status(video_id)) except Exception as e: return _json_response({"video_id": video_id, "status": "error", "error": str(e)}) # ── POST /api/placement/speaking-upload — transcribe speaking audio ── @http.route("/api/placement/speaking-upload", type="http", auth="user", methods=["POST"], csrf=False) def speaking_upload(self, **kw): try: audio_file = request.httprequest.files.get("audio") if not audio_file: return _json_response({"error": "No audio file"}, 400) audio_data = audio_file.read() from odoo.addons.encoach_ai.services.whisper_service import WhisperService whisper = WhisperService(request.env) transcript = whisper.transcribe(audio_data, use_api=True) from odoo.addons.encoach_ai.services.openai_service import OpenAIService ai = OpenAIService(request.env) grade = ai.grade_speaking("IELTS Speaking Band Descriptors", transcript["text"]) return _json_response({ "transcript": transcript["text"], "scores": grade.get("scores", {}), "overall_band": grade.get("overall_band", 0), "feedback": grade.get("feedback", ""), "status": "completed", }) except Exception as e: _logger.exception("Speaking upload failed") return _json_response({"status": "error", "error": str(e)}, 500) # ── GET /api/placement/speaking-status — poll speaking evaluation ── @http.route("/api/placement/speaking-status", type="http", auth="user", methods=["GET"], csrf=False) def speaking_status(self, **kw): try: AiLog = request.env.get("encoach.ai.log") if AiLog: log = AiLog.sudo().search([ ("action", "=", "grade_speaking"), ("create_uid", "=", request.env.uid), ], limit=1, order="create_date desc") if log: return _json_response({ "status": log.status or "completed", "log_id": log.id, "latency_ms": log.latency_ms, "created_at": log.create_date.isoformat() if log.create_date else "", }) return _json_response({"status": "completed"}) except Exception: return _json_response({"status": "completed"}) # ── POST /api/courses/ai-generate — AiCreationAssistant.tsx ── @http.route("/api/courses/ai-generate", type="http", auth="user", methods=["POST"], csrf=False) def ai_generate_course(self, **kw): body = _get_json() try: from odoo.addons.encoach_ai.services.openai_service import OpenAIService ai = OpenAIService(request.env) messages = [ {"role": "system", "content": ( "Generate a complete course structure. Return JSON: " "{\"title\": string, \"description\": string, \"modules\": " "[{\"title\": string, \"skill\": string, \"estimated_hours\": number, " "\"topics\": [string], \"resources\": [{\"title\": string, \"type\": string}]}], " "\"duration_weeks\": number}" )}, {"role": "user", "content": json.dumps(body)}, ] result = ai.chat_json(messages, action="generate_course", max_tokens=4096) return _json_response(result) except Exception as e: return _json_response({"error": str(e)}, 500)