EnCoach Odoo 19 custom modules
Full backend implementation with custom Odoo modules: - encoach_api: Core API, user management, JWT auth - encoach_exam: Exam generation (reading, writing, listening, speaking) - encoach_evaluate: AI-powered evaluation (writing, speaking) - encoach_training: Training tips and walkthrough - encoach_storage: File storage management - encoach_payment: Stripe, PayPal, Paymob integration - encoach_mail: Email notifications Made-with: Cursor
This commit is contained in:
213
encoach_api/controllers/media.py
Normal file
213
encoach_api/controllers/media.py
Normal file
@@ -0,0 +1,213 @@
|
||||
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/<string:vid_id>', 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)
|
||||
Reference in New Issue
Block a user