feat: Generation Page AI workflows + AI/Vector modules + exam session fixes
Generation Page (complete rebuild): - Full production-parity exam generation wizard with 4 IELTS modules - Reading: AI passage gen, 5 exercise types (MCQ, Fill, Write, T/F, Match) - Listening: 4 section types, AI context gen, TTS audio gen (ElevenLabs) - Writing: Task 1/2, AI instruction gen, word limits, marks - Speaking: 3 parts, AI script gen, avatar video gen (7 avatars) - Per-module config: timer, CEFR difficulty, access, approval, rubrics - Exam submission workflow (draft/published) Exam Structures: - New encoach.exam.structure model + CRUD controller - ExamStructuresPage wired to real API AI Module (encoach_ai): - OpenAI service, ElevenLabs TTS, AWS Polly, ELAI avatars - AI settings model with Odoo config parameters - 7 generation endpoints (passage, exercises, instructions, scripts, context) Vector Module (encoach_vector): - pgvector integration for RAG-based content search - Embedding service with sentence-transformers Exam Session Fixes: - Fixed ExamSession.tsx field mapping (question_type→type, exam_title→title) - Fixed submit payload to include attempt_id and answers - Fixed normalizeType to handle null/undefined Tested: 12/12 API tests passed, browser-verified with real OpenAI calls Made-with: Cursor
This commit is contained in:
108
custom_addons/encoach_ai/services/elai_service.py
Normal file
108
custom_addons/encoach_ai/services/elai_service.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""ELAI avatar video generation service."""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import requests as _requests
|
||||
except ImportError:
|
||||
_requests = None
|
||||
|
||||
ELAI_BASE = "https://apis.elai.io/api/v1"
|
||||
|
||||
|
||||
class ElaiService:
|
||||
"""Generate avatar videos for listening exercises and instructional content."""
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
self._get_param = env["ir.config_parameter"].sudo().get_param
|
||||
|
||||
def _get_token(self):
|
||||
token = self._get_param("encoach_ai.elai_token", "")
|
||||
if not token:
|
||||
import os
|
||||
token = os.environ.get("ELAI_TOKEN", "")
|
||||
if not token:
|
||||
raise RuntimeError("ELAI token not configured — set in AI Settings")
|
||||
return token
|
||||
|
||||
def _headers(self):
|
||||
return {
|
||||
"Authorization": f"Bearer {self._get_token()}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def _log(self, action, latency, status="success", error=None):
|
||||
try:
|
||||
self.env["encoach.ai.log"].sudo().create({
|
||||
"service": "elai",
|
||||
"action": action,
|
||||
"latency_ms": latency,
|
||||
"status": status,
|
||||
"error_message": error,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def list_avatars(self):
|
||||
"""List available ELAI avatars."""
|
||||
if not _requests:
|
||||
raise RuntimeError("requests package not installed")
|
||||
resp = _requests.get(f"{ELAI_BASE}/avatars", headers=self._headers(), timeout=15)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def create_video(self, script, *, avatar_id=None, title="EnCoach Video", language="en"):
|
||||
"""Create an avatar video from a script.
|
||||
|
||||
Returns:
|
||||
dict with 'video_id', 'status'
|
||||
"""
|
||||
if not _requests:
|
||||
raise RuntimeError("requests package not installed")
|
||||
payload = {
|
||||
"name": title,
|
||||
"slides": [
|
||||
{
|
||||
"speech": script,
|
||||
"avatar": avatar_id or "default",
|
||||
"language": language,
|
||||
}
|
||||
],
|
||||
}
|
||||
t0 = time.time()
|
||||
try:
|
||||
resp = _requests.post(
|
||||
f"{ELAI_BASE}/videos",
|
||||
json=payload,
|
||||
headers=self._headers(),
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
self._log("create_video", int((time.time() - t0) * 1000))
|
||||
return {"video_id": data.get("_id", data.get("id")), "status": data.get("status", "pending")}
|
||||
except Exception as exc:
|
||||
self._log("create_video", int((time.time() - t0) * 1000), "error", str(exc))
|
||||
raise
|
||||
|
||||
def get_video_status(self, video_id):
|
||||
"""Check video generation status."""
|
||||
if not _requests:
|
||||
raise RuntimeError("requests package not installed")
|
||||
resp = _requests.get(
|
||||
f"{ELAI_BASE}/videos/{video_id}",
|
||||
headers=self._headers(),
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return {
|
||||
"video_id": video_id,
|
||||
"status": data.get("status", "unknown"),
|
||||
"url": data.get("url", ""),
|
||||
"duration": data.get("duration"),
|
||||
}
|
||||
Reference in New Issue
Block a user