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
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
"""GPTZero AI content detection service."""
|
|
|
|
import logging
|
|
import time
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
import requests as _requests
|
|
except ImportError:
|
|
_requests = None
|
|
|
|
GPTZERO_BASE = "https://api.gptzero.me/v2"
|
|
|
|
|
|
class GPTZeroService:
|
|
"""Detect AI-generated content in student submissions."""
|
|
|
|
def __init__(self, env):
|
|
self.env = env
|
|
self._get_param = env["ir.config_parameter"].sudo().get_param
|
|
|
|
def _get_key(self):
|
|
key = self._get_param("encoach_ai.gptzero_api_key", "")
|
|
if not key:
|
|
import os
|
|
key = os.environ.get("GPT_ZERO_API_KEY", "")
|
|
if not key:
|
|
raise RuntimeError("GPTZero API key not configured — set in AI Settings")
|
|
return key
|
|
|
|
def _log(self, action, latency, status="success", error=None):
|
|
try:
|
|
self.env["encoach.ai.log"].sudo().create({
|
|
"service": "gptzero",
|
|
"action": action,
|
|
"latency_ms": latency,
|
|
"status": status,
|
|
"error_message": error,
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
def detect(self, text):
|
|
"""Check if text is AI-generated.
|
|
|
|
Returns:
|
|
dict with 'is_ai_generated' (bool), 'ai_probability' (float 0-1),
|
|
'human_probability' (float), 'sentences' (list of per-sentence scores)
|
|
"""
|
|
if not _requests:
|
|
raise RuntimeError("requests package not installed")
|
|
key = self._get_key()
|
|
t0 = time.time()
|
|
try:
|
|
resp = _requests.post(
|
|
f"{GPTZERO_BASE}/predict/text",
|
|
json={"document": text},
|
|
headers={"x-api-key": key, "Content-Type": "application/json"},
|
|
timeout=30,
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
doc = data.get("documents", [{}])[0] if data.get("documents") else {}
|
|
result = {
|
|
"is_ai_generated": doc.get("completely_generated_prob", 0) > 0.5,
|
|
"ai_probability": doc.get("completely_generated_prob", 0),
|
|
"human_probability": 1 - doc.get("completely_generated_prob", 0),
|
|
"mixed_probability": doc.get("average_generated_prob", 0),
|
|
"sentences": [
|
|
{
|
|
"text": s.get("sentence", ""),
|
|
"ai_probability": s.get("generated_prob", 0),
|
|
"is_ai": s.get("generated_prob", 0) > 0.5,
|
|
}
|
|
for s in doc.get("sentences", [])
|
|
],
|
|
}
|
|
self._log("detect", int((time.time() - t0) * 1000))
|
|
return result
|
|
except Exception as exc:
|
|
self._log("detect", int((time.time() - t0) * 1000), "error", str(exc))
|
|
raise
|
|
|
|
def detect_batch(self, texts):
|
|
"""Check multiple texts for AI generation."""
|
|
return [self.detect(t) for t in texts]
|