import base64 import json import logging from odoo import http from odoo.http import request, Response from .base import EncoachMixin _logger = logging.getLogger(__name__) class MediaController(EncoachMixin, http.Controller): # ------------------------------------------- listening MP3 @http.route( '/api/exam/listening/media', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*', ) def generate_listening_mp3(self, **kwargs): """Generate listening MP3 audio from a dialog/monologue via AWS Polly.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() conversation = body.get('conversation') monologue = body.get('monologue') if not conversation and not monologue: return self._error_response('conversation or monologue is required', 400) MediaService = request.env['encoach.ai.media'].sudo() mp3_data = MediaService.generate_listening_audio( conversation=conversation, monologue=monologue, ) return Response( mp3_data, status=200, content_type='audio/mpeg', headers={ 'Access-Control-Allow-Origin': '*', 'Content-Disposition': 'attachment; filename="listening.mp3"', }, ) except Exception: _logger.exception("Generate listening MP3 error") return self._error_response('Internal server error', 500) # ---------------------------------------- transcribe audio @http.route( '/api/exam/listening/transcribe', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*', ) def transcribe_listening(self, **kwargs): """Transcribe audio using Whisper and return dialog object.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) audio_file = request.httprequest.files.get('audio') if not audio_file: return self._error_response('audio file is required', 400) audio_data = audio_file.read() MediaService = request.env['encoach.ai.media'].sudo() result = MediaService.transcribe_to_dialog( audio_data=audio_data, filename=audio_file.filename, ) return self._json_response(result) except Exception: _logger.exception("Transcribe listening error") return self._error_response('Internal server error', 500) # --------------------------------- listening instructions MP3 @http.route( '/api/exam/listening/instructions', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*', ) def generate_instructions_mp3(self, **kwargs): """Generate MP3 for listening instructions text via AWS Polly.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() text = body.get('text', '') if not text: return self._error_response('text is required', 400) MediaService = request.env['encoach.ai.media'].sudo() mp3_data = MediaService.generate_instructions_audio(text=text) return Response( mp3_data, status=200, content_type='audio/mpeg', headers={ 'Access-Control-Allow-Origin': '*', 'Content-Disposition': 'attachment; filename="instructions.mp3"', }, ) except Exception: _logger.exception("Generate instructions MP3 error") return self._error_response('Internal server error', 500) # ------------------------------------------ speaking video @http.route( '/api/exam/speaking/media', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*', ) def generate_speaking_video(self, **kwargs): """Start AI avatar video generation via ELAI (async).""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() text = body.get('text', '') avatar = body.get('avatar', '') if not text: return self._error_response('text is required', 400) MediaService = request.env['encoach.ai.media'].sudo() result = MediaService.create_speaking_video( text=text, avatar_code=avatar, ) return self._json_response(result) except Exception: _logger.exception("Generate speaking video error") return self._error_response('Internal server error', 500) # --------------------------------- poll speaking video status @http.route( '/api/exam/speaking/media/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*', ) def poll_speaking_video(self, vid_id, **kwargs): """Poll ELAI video generation status.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) MediaService = request.env['encoach.ai.media'].sudo() result = MediaService.poll_video_status(video_id=vid_id) return self._json_response(result) except Exception: _logger.exception("Poll speaking video error") return self._error_response('Internal server error', 500) # ----------------------------------------- speaking avatars @http.route( '/api/exam/speaking/avatars', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*', ) def list_speaking_avatars(self, **kwargs): try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) avatars = request.env['encoach.elai.avatar'].sudo().search([]) data = [{ 'name': a.name, 'avatar_code': a.avatar_code, 'avatar_url': a.avatar_url or '', 'gender': a.gender or '', 'canvas': getattr(a, 'canvas', '') or '', 'voice_id': getattr(a, 'voice_id', '') or '', 'voice_provider': getattr(a, 'voice_provider', '') or '', } for a in avatars] return self._json_response(data) except Exception: _logger.exception("List speaking avatars error") return self._error_response('Internal server error', 500) # ---------------------------------------- general transcribe @http.route('/api/transcribe', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') def transcribe(self, **kwargs): """Transcribe an audio file using Whisper.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) audio_file = request.httprequest.files.get('audio') or request.httprequest.files.get('file') if not audio_file: return self._error_response('audio file is required', 400) audio_data = audio_file.read() MediaService = request.env['encoach.ai.media'].sudo() result = MediaService.transcribe_audio( audio_data=audio_data, filename=audio_file.filename, ) return self._json_response(result) except Exception: _logger.exception("Transcribe error") return self._error_response('Internal server error', 500)