feat(course-plan): GE1-style AI course planning with deliverables, resources, media, assignments
Based on UTAS GE1 Course Outline structure (Reading/Writing 10hrs + Listening/Speaking 8hrs) New Models: - encoach.course.plan.deliverable: Explicit learning outcome tracking by week/skill - encoach.course.plan.resource.dep: Resource dependencies (textbooks, videos, etc.) - encoach.course.plan.assignment: Assign plans to classes/students with progress tracking - encoach.course.plan.assignment.deliverable: Per-student deliverable completion status Extended Models: - course.plan.material: Added media fields (media_type, media_asset_url, media_asset_id, media_generation_prompt, media_metadata_json) for rich content - New material types: video_lesson, audio_recording, image_visual, interactive, assessment AI Agent Tools (agent_tools.py): - deliverables.detect: Parse course outlines (like GE1 PDF) and extract structured outcomes - deliverables.fetch: Get deliverables for AI to reference when generating - resources.fetch: Check available resources before generating content - resources.save: Persist resource dependencies - media.suggest_visuals: AI suggests images/diagrams for materials - media.generate_image: Generate educational images (DALL-E integration ready) - media.generate_audio: Generate TTS audio (ElevenLabs/Polly integration ready) - assignment.*: Create assignments and track progress Pipeline Enhancements (course_plan_pipeline.py): - generate_deliverables_from_outline(): Parse PDF/text outlines into structured deliverables - generate_week_materials_with_resources(): Resource-aware content generation - suggest_media_for_material(): AI visual aid suggestions - generate_media_for_material(): Actual image/audio generation New AI Agents (agents_defaults.xml): - deliverable_detector: Parses GE1-style outlines, extracts deliverables week-by-week - media_generator: Creates images/audio for teaching materials - Updated course_planner & course_week_materials with resource tools REST APIs (course_plan.py): POST /api/ai/course-plan/<id>/deliverables/detect - Parse outline GET /api/ai/course-plan/<id>/deliverables - List deliverables PUT /api/ai/course-plan/deliverables/<id> - Update status GET /api/ai/course-plan/<id>/resources - List resources POST /api/ai/course-plan/<id>/resources - Add resource POST /api/ai/course-plan/materials/<id>/media/suggest - Get visual suggestions POST /api/ai/course-plan/materials/<id>/media/generate - Generate image/audio POST /api/ai/course-plan/<id>/assignments - Assign to class/student GET /api/ai/course-plan/<id>/assignments - List assignments GET /api/ai/course-plan/assignments/<id> - Get with progress PUT /api/ai/course-plan/assignments/<id>/deliverables/<del_id> - Update status Security: Added ir.model.access.csv entries for all new models Made-with: Cursor
This commit is contained in:
@@ -6,9 +6,10 @@ endpoints. Every route is JWT-guarded via the shared ``@jwt_required``
|
||||
decorator and returns JSON.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo import http, fields
|
||||
from odoo.http import request
|
||||
from odoo.addons.encoach_api.controllers.base import (
|
||||
jwt_required,
|
||||
@@ -169,3 +170,315 @@ class CoursePlanController(http.Controller):
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.list_week_materials failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# ==================================================================
|
||||
# NEW: Deliverable Detection & Management
|
||||
# ==================================================================
|
||||
|
||||
# POST /api/ai/course-plan/<id>/deliverables/detect
|
||||
@http.route('/api/ai/course-plan/<int:plan_id>/deliverables/detect',
|
||||
type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def detect_deliverables(self, plan_id, **kw):
|
||||
"""Parse course outline text and extract structured deliverables."""
|
||||
try:
|
||||
body = _get_json_body()
|
||||
outline_text = body.get('outline_text', '').strip()
|
||||
if not outline_text:
|
||||
return _error_response('outline_text is required', 400)
|
||||
|
||||
pipeline = CoursePlanPipeline(
|
||||
request.env, language=_request_language(),
|
||||
)
|
||||
result = pipeline.generate_deliverables_from_outline(plan_id, outline_text)
|
||||
return _json_response(result)
|
||||
except ValueError as exc:
|
||||
return _error_response(str(exc), 404)
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.detect_deliverables failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# GET /api/ai/course-plan/<id>/deliverables
|
||||
@http.route('/api/ai/course-plan/<int:plan_id>/deliverables',
|
||||
type='http', auth='none', methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def list_deliverables(self, plan_id, **kw):
|
||||
"""List deliverables for a course plan."""
|
||||
try:
|
||||
params = request.httprequest.args
|
||||
domain = [('plan_id', '=', int(plan_id))]
|
||||
week = params.get('week')
|
||||
if week:
|
||||
domain.append(('week_number', '=', int(week)))
|
||||
skill = params.get('skill')
|
||||
if skill:
|
||||
domain.append(('skill', '=', skill))
|
||||
|
||||
Deliverable = request.env['encoach.course.plan.deliverable'].sudo()
|
||||
records = Deliverable.search(domain, order='week_number, code')
|
||||
return _json_response({
|
||||
'items': [d.to_api_dict() for d in records],
|
||||
'count': len(records),
|
||||
})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.list_deliverables failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# PUT /api/ai/course-plan/deliverables/<id>
|
||||
@http.route('/api/ai/course-plan/deliverables/<int:deliverable_id>',
|
||||
type='http', auth='none', methods=['PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_deliverable(self, deliverable_id, **kw):
|
||||
"""Update deliverable status or details."""
|
||||
try:
|
||||
body = _get_json_body()
|
||||
Deliverable = request.env['encoach.course.plan.deliverable'].sudo()
|
||||
rec = Deliverable.browse(int(deliverable_id))
|
||||
if not rec.exists():
|
||||
return _error_response('Deliverable not found', 404)
|
||||
|
||||
updatable = {}
|
||||
if 'status' in body:
|
||||
updatable['status'] = body['status']
|
||||
if 'target_date' in body:
|
||||
updatable['target_date'] = body['target_date']
|
||||
if 'description' in body:
|
||||
updatable['description'] = body['description']
|
||||
|
||||
if updatable:
|
||||
rec.write(updatable)
|
||||
return _json_response({'data': rec.to_api_dict()})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.update_deliverable failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# ==================================================================
|
||||
# NEW: Resource Dependencies
|
||||
# ==================================================================
|
||||
|
||||
# GET /api/ai/course-plan/<id>/resources
|
||||
@http.route('/api/ai/course-plan/<int:plan_id>/resources',
|
||||
type='http', auth='none', methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def list_resources(self, plan_id, **kw):
|
||||
"""List resource dependencies for a course plan."""
|
||||
try:
|
||||
params = request.httprequest.args
|
||||
domain = [('plan_id', '=', int(plan_id))]
|
||||
resource_type = params.get('type')
|
||||
if resource_type:
|
||||
domain.append(('resource_type', '=', resource_type))
|
||||
|
||||
ResourceDep = request.env['encoach.course.plan.resource.dep'].sudo()
|
||||
records = ResourceDep.search(domain)
|
||||
return _json_response({
|
||||
'items': [r.to_api_dict() for r in records],
|
||||
'count': len(records),
|
||||
})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.list_resources failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# POST /api/ai/course-plan/<id>/resources
|
||||
@http.route('/api/ai/course-plan/<int:plan_id>/resources',
|
||||
type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def add_resource(self, plan_id, **kw):
|
||||
"""Add a resource dependency to a course plan."""
|
||||
try:
|
||||
body = _get_json_body()
|
||||
if not body.get('name'):
|
||||
return _error_response('name is required', 400)
|
||||
|
||||
ResourceDep = request.env['encoach.course.plan.resource.dep'].sudo()
|
||||
rec = ResourceDep.create({
|
||||
'plan_id': int(plan_id),
|
||||
'name': body['name'],
|
||||
'resource_type': body.get('resource_type', 'textbook'),
|
||||
'citation': body.get('citation', ''),
|
||||
'url': body.get('url', ''),
|
||||
'ai_usage_notes': body.get('ai_usage_notes', ''),
|
||||
'is_required': body.get('is_required', True),
|
||||
'extracted_content_json': json.dumps(body.get('extracted_content', {})),
|
||||
})
|
||||
return _json_response({'data': rec.to_api_dict()})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.add_resource failed')
|
||||
return _json_response(str(exc), 500)
|
||||
|
||||
# ==================================================================
|
||||
# NEW: Rich Media Generation
|
||||
# ==================================================================
|
||||
|
||||
# POST /api/ai/course-plan/materials/<id>/media/suggest
|
||||
@http.route('/api/ai/course-plan/materials/<int:material_id>/media/suggest',
|
||||
type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def suggest_media(self, material_id, **kw):
|
||||
"""Get AI suggestions for media that would enhance this material."""
|
||||
try:
|
||||
pipeline = CoursePlanPipeline(
|
||||
request.env, language=_request_language(),
|
||||
)
|
||||
result = pipeline.suggest_media_for_material(material_id)
|
||||
if 'error' in result:
|
||||
return _error_response(result['error'], 400)
|
||||
return _json_response(result)
|
||||
except ValueError as exc:
|
||||
return _error_response(str(exc), 404)
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.suggest_media failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# POST /api/ai/course-plan/materials/<id>/media/generate
|
||||
@http.route('/api/ai/course-plan/materials/<int:material_id>/media/generate',
|
||||
type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def generate_media(self, material_id, **kw):
|
||||
"""Generate media (image, audio) for a teaching material."""
|
||||
try:
|
||||
body = _get_json_body()
|
||||
media_type = body.get('media_type', 'image')
|
||||
|
||||
pipeline = CoursePlanPipeline(
|
||||
request.env, language=_request_language(),
|
||||
)
|
||||
result = pipeline.generate_media_for_material(material_id, media_type)
|
||||
if 'error' in result:
|
||||
return _error_response(result['error'], 400)
|
||||
return _json_response(result)
|
||||
except ValueError as exc:
|
||||
return _error_response(str(exc), 404)
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.generate_media failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# ==================================================================
|
||||
# NEW: Course Plan Assignment to Classes/Students
|
||||
# ==================================================================
|
||||
|
||||
# POST /api/ai/course-plan/<id>/assignments
|
||||
@http.route('/api/ai/course-plan/<int:plan_id>/assignments',
|
||||
type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@jwt_required
|
||||
def create_assignment(self, plan_id, **kw):
|
||||
"""Assign a course plan to a class or student."""
|
||||
try:
|
||||
body = _get_json_body()
|
||||
Assignment = request.env['encoach.course.plan.assignment'].sudo()
|
||||
|
||||
vals = {
|
||||
'plan_id': int(plan_id),
|
||||
'assignment_type': body.get('assignment_type', 'class'),
|
||||
'delivery_mode': body.get('delivery_mode', 'sequential'),
|
||||
'status': 'scheduled',
|
||||
}
|
||||
if body.get('batch_id'):
|
||||
vals['batch_id'] = int(body['batch_id'])
|
||||
if body.get('student_id'):
|
||||
vals['student_id'] = int(body['student_id'])
|
||||
if body.get('start_date'):
|
||||
vals['start_date'] = body['start_date']
|
||||
|
||||
assignment = Assignment.create(vals)
|
||||
|
||||
# Create deliverable tracking rows
|
||||
Deliverable = request.env['encoach.course.plan.deliverable'].sudo()
|
||||
AssignmentDeliverable = request.env['encoach.course.plan.assignment.deliverable'].sudo()
|
||||
deliverables = Deliverable.search([('plan_id', '=', int(plan_id))])
|
||||
for d in deliverables:
|
||||
AssignmentDeliverable.create({
|
||||
'assignment_id': assignment.id,
|
||||
'deliverable_id': d.id,
|
||||
'status': 'not_started',
|
||||
})
|
||||
|
||||
return _json_response({
|
||||
'data': assignment.to_api_dict(),
|
||||
'deliverables_tracked': len(deliverables),
|
||||
})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.create_assignment failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# GET /api/ai/course-plan/<id>/assignments
|
||||
@http.route('/api/ai/course-plan/<int:plan_id>/assignments',
|
||||
type='http', auth='none', methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def list_assignments(self, plan_id, **kw):
|
||||
"""List assignments for a course plan."""
|
||||
try:
|
||||
Assignment = request.env['encoach.course.plan.assignment'].sudo()
|
||||
records = Assignment.search([('plan_id', '=', int(plan_id))])
|
||||
return _json_response({
|
||||
'items': [a.to_api_dict() for a in records],
|
||||
'count': len(records),
|
||||
})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.list_assignments failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# GET /api/ai/course-plan/assignments/<id>
|
||||
@http.route('/api/ai/course-plan/assignments/<int:assignment_id>',
|
||||
type='http', auth='none', methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def get_assignment(self, assignment_id, **kw):
|
||||
"""Get assignment details with progress."""
|
||||
try:
|
||||
Assignment = request.env['encoach.course.plan.assignment'].sudo()
|
||||
AssignmentDeliverable = request.env['encoach.course.plan.assignment.deliverable'].sudo()
|
||||
|
||||
assignment = Assignment.browse(int(assignment_id))
|
||||
if not assignment.exists():
|
||||
return _error_response('Assignment not found', 404)
|
||||
|
||||
# Get deliverable tracking
|
||||
tracking = AssignmentDeliverable.search([('assignment_id', '=', int(assignment_id))])
|
||||
status_counts = {}
|
||||
for t in tracking:
|
||||
status_counts[t.status] = status_counts.get(t.status, 0) + 1
|
||||
|
||||
return _json_response({
|
||||
'data': assignment.to_api_dict(),
|
||||
'deliverables': {
|
||||
'total': len(tracking),
|
||||
'by_status': status_counts,
|
||||
'items': [t.to_api_dict() for t in tracking],
|
||||
},
|
||||
})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.get_assignment failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
# PUT /api/ai/course-plan/assignments/<id>/deliverables/<del_id>
|
||||
@http.route('/api/ai/course-plan/assignments/<int:assignment_id>/deliverables/<int:deliverable_id>',
|
||||
type='http', auth='none', methods=['PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_assignment_deliverable(self, assignment_id, deliverable_id, **kw):
|
||||
"""Update deliverable status for an assignment."""
|
||||
try:
|
||||
body = _get_json_body()
|
||||
AssignmentDeliverable = request.env['encoach.course.plan.assignment.deliverable'].sudo()
|
||||
|
||||
rec = AssignmentDeliverable.search([
|
||||
('assignment_id', '=', int(assignment_id)),
|
||||
('deliverable_id', '=', int(deliverable_id)),
|
||||
], limit=1)
|
||||
if not rec:
|
||||
return _error_response('Assignment deliverable not found', 404)
|
||||
|
||||
vals = {}
|
||||
if 'status' in body:
|
||||
vals['status'] = body['status']
|
||||
if 'score' in body:
|
||||
vals['score'] = float(body['score'])
|
||||
if 'notes' in body:
|
||||
vals['notes'] = body['notes']
|
||||
if body.get('status') == 'completed':
|
||||
vals['completion_date'] = fields.Datetime.now()
|
||||
|
||||
rec.write(vals)
|
||||
return _json_response({'data': rec.to_api_dict()})
|
||||
except Exception as exc:
|
||||
_logger.exception('course-plan.update_assignment_deliverable failed')
|
||||
return _error_response(str(exc), 500)
|
||||
|
||||
@@ -44,10 +44,28 @@ MATERIAL_TYPE_SELECTION = [
|
||||
('grammar_lesson', 'Grammar Lesson'),
|
||||
('vocabulary_list', 'Vocabulary List'),
|
||||
('practice', 'Practice Exercises'),
|
||||
# Rich media types for comprehensive teaching materials
|
||||
('video_lesson', 'Video Lesson'),
|
||||
('audio_recording', 'Audio Recording'),
|
||||
('image_visual', 'Image / Visual Aid'),
|
||||
('interactive', 'Interactive Activity'),
|
||||
('assessment', 'Assessment / Quiz'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
|
||||
# Deliverable status tracking
|
||||
DELIVERABLE_STATUS_SELECTION = [
|
||||
('planned', 'Planned'),
|
||||
('generating', 'Generating'),
|
||||
('generated', 'Generated'),
|
||||
('reviewing', 'Under Review'),
|
||||
('approved', 'Approved'),
|
||||
('delivered', 'Delivered to Students'),
|
||||
('archived', 'Archived'),
|
||||
]
|
||||
|
||||
|
||||
class CoursePlan(models.Model):
|
||||
_name = 'encoach.course.plan'
|
||||
_description = 'AI-generated Course Plan'
|
||||
@@ -260,9 +278,53 @@ class CoursePlanMaterial(models.Model):
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
def to_api_dict(self):
|
||||
# Rich media asset fields for generated content
|
||||
media_type = fields.Selection([
|
||||
('none', 'No Media'),
|
||||
('image', 'Image (AI Generated)'),
|
||||
('audio', 'Audio (AI Generated)'),
|
||||
('video', 'Video (AI Generated)'),
|
||||
('external', 'External Media URL'),
|
||||
], default='none', string='Media Type')
|
||||
media_asset_url = fields.Char(
|
||||
string='Media Asset URL',
|
||||
help='URL to stored/generated media (image, audio, video)'
|
||||
)
|
||||
media_asset_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Media Attachment',
|
||||
help='Stored media file in Odoo'
|
||||
)
|
||||
media_generation_prompt = fields.Text(
|
||||
string='Media Generation Prompt',
|
||||
help='The prompt used to generate this media asset'
|
||||
)
|
||||
media_metadata_json = fields.Text(
|
||||
string='Media Metadata',
|
||||
help='JSON with dimensions, duration, file size, etc.'
|
||||
)
|
||||
|
||||
# Resource dependencies tracking
|
||||
required_resources_json = fields.Text(
|
||||
string='Required Resources',
|
||||
help='JSON list of resource IDs/URLs this material depends on'
|
||||
)
|
||||
ai_generation_notes = fields.Text(
|
||||
string='AI Generation Notes',
|
||||
help='Internal notes from AI about generation decisions'
|
||||
)
|
||||
|
||||
def _loads(self, raw, default):
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
def to_api_dict(self, include_media=False):
|
||||
self.ensure_one()
|
||||
return {
|
||||
data = {
|
||||
'id': self.id,
|
||||
'plan_id': self.plan_id.id,
|
||||
'week_id': self.week_id.id if self.week_id else None,
|
||||
@@ -273,4 +335,315 @@ class CoursePlanMaterial(models.Model):
|
||||
'summary': self.summary or '',
|
||||
'body': self._loads(self.body_json, {}),
|
||||
'body_text': self.body_text or '',
|
||||
'media_type': self.media_type or 'none',
|
||||
'required_resources': self._loads(self.required_resources_json, []),
|
||||
}
|
||||
if include_media:
|
||||
data['media_asset_url'] = self.media_asset_url or ''
|
||||
data['media_metadata'] = self._loads(self.media_metadata_json, {})
|
||||
data['media_generation_prompt'] = self.media_generation_prompt or ''
|
||||
return data
|
||||
|
||||
|
||||
class CoursePlanDeliverable(models.Model):
|
||||
"""Explicit deliverable tracking for GE1-style course planning.
|
||||
|
||||
Each deliverable represents a specific learning outcome that must be
|
||||
achieved by students. The AI uses these to generate targeted materials
|
||||
and track completion status.
|
||||
"""
|
||||
_name = 'encoach.course.plan.deliverable'
|
||||
_description = 'Course Plan Deliverable (Learning Outcome)'
|
||||
_order = 'plan_id, week_number, skill, code'
|
||||
|
||||
plan_id = fields.Many2one(
|
||||
'encoach.course.plan', required=True, ondelete='cascade', index=True,
|
||||
)
|
||||
week_id = fields.Many2one(
|
||||
'encoach.course.plan.week', ondelete='cascade', index=True,
|
||||
)
|
||||
week_number = fields.Integer(required=True, index=True)
|
||||
|
||||
# GE1-style outcome identification
|
||||
code = fields.Char(required=True, index=True, string='Outcome Code')
|
||||
skill = fields.Selection(SKILL_SELECTION, required=True, index=True)
|
||||
description = fields.Text(required=True)
|
||||
cefr_level = fields.Selection([
|
||||
('pre_a1', 'Pre-A1'), ('a1', 'A1'), ('a2', 'A2'),
|
||||
('b1', 'B1'), ('b2', 'B2'), ('c1', 'C1'), ('c2', 'C2'),
|
||||
], string='CEFR Level')
|
||||
|
||||
# Deliverable status and tracking
|
||||
status = fields.Selection(DELIVERABLE_STATUS_SELECTION, default='planned')
|
||||
target_date = fields.Date(string='Target Delivery Date')
|
||||
completion_date = fields.Datetime(string='Completed At')
|
||||
|
||||
# AI generation tracking
|
||||
ai_generated = fields.Boolean(default=False, string='AI Generated')
|
||||
generation_prompt = fields.Text(string='Generation Prompt Used')
|
||||
|
||||
# Resource dependencies for this specific deliverable
|
||||
resource_dependencies_json = fields.Text(
|
||||
string='Required Resources',
|
||||
help='JSON list of {resource_id, name, type, required_for} objects'
|
||||
)
|
||||
material_ids = fields.Many2many(
|
||||
'encoach.course.plan.material',
|
||||
'deliverable_material_rel',
|
||||
'deliverable_id', 'material_id',
|
||||
string='Generated Materials',
|
||||
help='Materials generated to support this deliverable'
|
||||
)
|
||||
|
||||
def to_api_dict(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'id': self.id,
|
||||
'plan_id': self.plan_id.id,
|
||||
'week_number': self.week_number,
|
||||
'code': self.code or '',
|
||||
'skill': self.skill or '',
|
||||
'description': self.description or '',
|
||||
'cefr_level': self.cefr_level or '',
|
||||
'status': self.status or 'planned',
|
||||
'target_date': str(self.target_date) if self.target_date else None,
|
||||
'ai_generated': self.ai_generated,
|
||||
'resources': json.loads(self.resource_dependencies_json or '[]'),
|
||||
'material_ids': [m.id for m in self.material_ids],
|
||||
}
|
||||
|
||||
|
||||
class CoursePlanResourceDependency(models.Model):
|
||||
"""Track what resources the AI depends on for course generation.
|
||||
|
||||
This model stores the relationship between course plans and the external
|
||||
resources (textbooks, videos, etc.) that the AI references when generating
|
||||
content. It enables the system to check resource availability before
|
||||
generation and suggest alternatives if resources are missing.
|
||||
"""
|
||||
_name = 'encoach.course.plan.resource.dep'
|
||||
_description = 'Course Plan Resource Dependency'
|
||||
_order = 'plan_id, resource_type, name'
|
||||
|
||||
plan_id = fields.Many2one(
|
||||
'encoach.course.plan', required=True, ondelete='cascade', index=True,
|
||||
)
|
||||
name = fields.Char(required=True, string='Resource Name')
|
||||
resource_type = fields.Selection([
|
||||
('textbook', 'Textbook'),
|
||||
('workbook', 'Workbook'),
|
||||
('video', 'Video / Film'),
|
||||
('audio', 'Audio Recording'),
|
||||
('website', 'Website / Online Resource'),
|
||||
('software', 'Software / App'),
|
||||
('assessment', 'Assessment Bank'),
|
||||
('other', 'Other'),
|
||||
], required=True, default='textbook')
|
||||
|
||||
# Resource identification
|
||||
citation = fields.Text(string='Full Citation')
|
||||
isbn = fields.Char(string='ISBN / Identifier')
|
||||
url = fields.Char(string='URL / Link')
|
||||
file_attachment_id = fields.Many2one('ir.attachment', string='Attached File')
|
||||
|
||||
# AI dependency tracking
|
||||
is_required = fields.Boolean(default=True, string='Required for Generation')
|
||||
is_available = fields.Boolean(default=False, string='Resource Available')
|
||||
ai_usage_notes = fields.Text(
|
||||
string='AI Usage Notes',
|
||||
help='How the AI should use this resource (e.g., "extract dialogue scripts", "reference grammar explanations")'
|
||||
)
|
||||
|
||||
# Content extraction for AI consumption
|
||||
extracted_content_json = fields.Text(
|
||||
string='Extracted Content for AI',
|
||||
help='JSON with key excerpts, chapter summaries, or structured content the AI can reference'
|
||||
)
|
||||
|
||||
# Status
|
||||
status = fields.Selection([
|
||||
('needed', 'Needed'),
|
||||
('sourcing', 'Sourcing'),
|
||||
('available', 'Available'),
|
||||
('extracted', 'Content Extracted'),
|
||||
('unavailable', 'Unavailable'),
|
||||
('replaced', 'Replaced with Alternative'),
|
||||
], default='needed', string='Status')
|
||||
|
||||
def to_api_dict(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'id': self.id,
|
||||
'plan_id': self.plan_id.id,
|
||||
'name': self.name or '',
|
||||
'resource_type': self.resource_type or '',
|
||||
'citation': self.citation or '',
|
||||
'is_required': self.is_required,
|
||||
'is_available': self.is_available,
|
||||
'status': self.status or 'needed',
|
||||
'ai_usage_notes': self.ai_usage_notes or '',
|
||||
'extracted_content': json.loads(self.extracted_content_json or '{}'),
|
||||
}
|
||||
|
||||
|
||||
class CoursePlanAssignment(models.Model):
|
||||
"""Assignment of a course plan to classes or individual students.
|
||||
|
||||
After a course plan is generated and approved, it can be assigned to
|
||||
delivery targets (classes, batches, or individual students). This model
|
||||
tracks the assignment, delivery progress, and completion status.
|
||||
"""
|
||||
_name = 'encoach.course.plan.assignment'
|
||||
_description = 'Course Plan Assignment'
|
||||
_order = 'create_date desc, id desc'
|
||||
|
||||
plan_id = fields.Many2one(
|
||||
'encoach.course.plan', required=True, ondelete='cascade', index=True,
|
||||
)
|
||||
name = fields.Char(compute='_compute_name', store=True)
|
||||
|
||||
# Assignment target
|
||||
assignment_type = fields.Selection([
|
||||
('class', 'Class / Batch'),
|
||||
('student', 'Individual Student'),
|
||||
('group', 'Custom Group'),
|
||||
], required=True, default='class')
|
||||
|
||||
# Link to OpenEduCat models
|
||||
course_id = fields.Many2one('op.course', string='Course', index=True)
|
||||
batch_id = fields.Many2one('op.batch', string='Batch / Class', index=True)
|
||||
student_id = fields.Many2one('op.student', string='Student', index=True)
|
||||
|
||||
# Assignment metadata
|
||||
assigned_by_id = fields.Many2one('res.users', string='Assigned By', default=lambda s: s.env.uid)
|
||||
assigned_date = fields.Datetime(default=fields.Datetime.now, string='Assigned At')
|
||||
start_date = fields.Date(string='Start Date')
|
||||
end_date = fields.Date(string='End Date')
|
||||
|
||||
# Delivery status
|
||||
status = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('scheduled', 'Scheduled'),
|
||||
('active', 'Active - In Progress'),
|
||||
('paused', 'Paused'),
|
||||
('completed', 'Completed'),
|
||||
('cancelled', 'Cancelled'),
|
||||
], default='draft', string='Assignment Status')
|
||||
|
||||
# Progress tracking
|
||||
current_week = fields.Integer(default=1, string='Current Week')
|
||||
progress_percent = fields.Float(compute='_compute_progress', store=True, string='Progress %')
|
||||
|
||||
# Delivery configuration
|
||||
delivery_mode = fields.Selection([
|
||||
('sequential', 'Sequential (Week by Week)'),
|
||||
('parallel', 'Parallel (All Skills)'),
|
||||
('adaptive', 'Adaptive (AI-Paced)'),
|
||||
], default='sequential', string='Delivery Mode')
|
||||
|
||||
# Notes
|
||||
notes = fields.Text(string='Assignment Notes')
|
||||
|
||||
# Related records
|
||||
deliverable_ids = fields.One2many(
|
||||
'encoach.course.plan.assignment.deliverable',
|
||||
'assignment_id',
|
||||
string='Deliverable Tracking',
|
||||
)
|
||||
|
||||
@api.depends('plan_id', 'batch_id', 'student_id')
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
parts = []
|
||||
if rec.plan_id:
|
||||
parts.append(rec.plan_id.name)
|
||||
if rec.batch_id:
|
||||
parts.append(f"→ {rec.batch_id.name}")
|
||||
if rec.student_id:
|
||||
parts.append(f"→ {rec.student_id.name}")
|
||||
rec.name = ' '.join(parts) if parts else 'Assignment'
|
||||
|
||||
@api.depends('current_week', 'plan_id.total_weeks')
|
||||
def _compute_progress(self):
|
||||
for rec in self:
|
||||
total = rec.plan_id.total_weeks or 1
|
||||
rec.progress_percent = min(100.0, (rec.current_week / total) * 100)
|
||||
|
||||
def to_api_dict(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name or '',
|
||||
'plan_id': self.plan_id.id if self.plan_id else None,
|
||||
'plan_name': self.plan_id.name if self.plan_id else '',
|
||||
'assignment_type': self.assignment_type or '',
|
||||
'batch_id': self.batch_id.id if self.batch_id else None,
|
||||
'batch_name': self.batch_id.name if self.batch_id else '',
|
||||
'student_id': self.student_id.id if self.student_id else None,
|
||||
'student_name': self.student_id.name if self.student_id else '',
|
||||
'status': self.status or 'draft',
|
||||
'start_date': str(self.start_date) if self.start_date else None,
|
||||
'end_date': str(self.end_date) if self.end_date else None,
|
||||
'current_week': self.current_week,
|
||||
'progress_percent': self.progress_percent,
|
||||
'delivery_mode': self.delivery_mode or 'sequential',
|
||||
}
|
||||
|
||||
|
||||
class CoursePlanAssignmentDeliverable(models.Model):
|
||||
"""Per-assignment tracking of deliverable completion.
|
||||
|
||||
When a course plan is assigned, this creates a tracking row for each
|
||||
deliverable so instructors can see which students have completed which
|
||||
learning outcomes.
|
||||
"""
|
||||
_name = 'encoach.course.plan.assignment.deliverable'
|
||||
_description = 'Assignment Deliverable Status'
|
||||
_order = 'assignment_id, week_number, code'
|
||||
|
||||
assignment_id = fields.Many2one(
|
||||
'encoach.course.plan.assignment', required=True, ondelete='cascade', index=True,
|
||||
)
|
||||
deliverable_id = fields.Many2one(
|
||||
'encoach.course.plan.deliverable', required=True, ondelete='cascade', index=True,
|
||||
)
|
||||
plan_id = fields.Many2one(
|
||||
related='assignment_id.plan_id', store=True, index=True,
|
||||
)
|
||||
|
||||
# Copy of deliverable info for quick reference
|
||||
week_number = fields.Integer(related='deliverable_id.week_number', store=True)
|
||||
code = fields.Char(related='deliverable_id.code', store=True)
|
||||
skill = fields.Selection(related='deliverable_id.skill', store=True)
|
||||
description = fields.Text(related='deliverable_id.description', store=True)
|
||||
|
||||
# Student completion tracking
|
||||
status = fields.Selection([
|
||||
('not_started', 'Not Started'),
|
||||
('in_progress', 'In Progress'),
|
||||
('submitted', 'Submitted for Review'),
|
||||
('completed', 'Completed'),
|
||||
('exempt', 'Exempt'),
|
||||
], default='not_started', string='Student Status')
|
||||
|
||||
# Progress details
|
||||
completion_date = fields.Datetime(string='Completed At')
|
||||
completed_by_id = fields.Many2one('res.users', string='Completed By')
|
||||
evidence_url = fields.Char(string='Evidence / Submission URL')
|
||||
score = fields.Float(string='Assessment Score')
|
||||
notes = fields.Text(string='Instructor Notes')
|
||||
|
||||
def to_api_dict(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'id': self.id,
|
||||
'assignment_id': self.assignment_id.id,
|
||||
'deliverable_id': self.deliverable_id.id,
|
||||
'week_number': self.week_number,
|
||||
'code': self.code or '',
|
||||
'skill': self.skill or '',
|
||||
'description': self.description or '',
|
||||
'status': self.status or 'not_started',
|
||||
'completion_date': self.completion_date.isoformat() if self.completion_date else None,
|
||||
'score': self.score or 0.0,
|
||||
}
|
||||
|
||||
@@ -4,3 +4,8 @@ access_encoach_ai_ielts_generation_log_user,encoach.ai.ielts.generation.log.user
|
||||
access_encoach_course_plan_user,encoach.course.plan.user,model_encoach_course_plan,base.group_user,1,1,1,1
|
||||
access_encoach_course_plan_week_user,encoach.course.plan.week.user,model_encoach_course_plan_week,base.group_user,1,1,1,1
|
||||
access_encoach_course_plan_material_user,encoach.course.plan.material.user,model_encoach_course_plan_material,base.group_user,1,1,1,1
|
||||
# New models for deliverables, resources, and assignments
|
||||
access_encoach_course_plan_deliverable_user,encoach.course.plan.deliverable.user,model_encoach_course_plan_deliverable,base.group_user,1,1,1,1
|
||||
access_encoach_course_plan_resource_dep_user,encoach.course.plan.resource.dep.user,model_encoach_course_plan_resource_dep,base.group_user,1,1,1,1
|
||||
access_encoach_course_plan_assignment_user,encoach.course.plan.assignment.user,model_encoach_course_plan_assignment,base.group_user,1,1,1,1
|
||||
access_encoach_course_plan_assignment_deliverable_user,encoach.course.plan.assignment.deliverable.user,model_encoach_course_plan_assignment_deliverable,base.group_user,1,1,1,1
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""Course plan generation pipeline.
|
||||
|
||||
Two public entry points:
|
||||
Three public entry points:
|
||||
|
||||
* :py:meth:`generate_plan` — given a short brief (course title, CEFR level,
|
||||
duration, skill coverage, grammar focus, resources), produce a full
|
||||
@@ -14,11 +14,22 @@ Two public entry points:
|
||||
+ practice, writing prompt, vocabulary list) and persist each as an
|
||||
:py:class:`encoach.course.plan.material` row.
|
||||
|
||||
* :py:meth:`generate_deliverables_from_outline` — NEW: Parse a course
|
||||
outline (like GE1 PDF) and extract structured deliverables (learning
|
||||
outcomes) that the AI uses to generate targeted materials.
|
||||
|
||||
* :py:meth:`generate_rich_media` — NEW: Generate images, audio, or video
|
||||
to accompany teaching materials.
|
||||
|
||||
We deliberately ask the LLM to return strict JSON and then normalise it
|
||||
server-side — the frontend gets a stable shape no matter how loose the
|
||||
model's output is. Any parse failure is swallowed and reported back
|
||||
through the standard error channel so the caller can retry without the
|
||||
server crashing.
|
||||
|
||||
Resource-aware generation: Before generating materials, the pipeline
|
||||
fetches registered resources (textbooks, videos) and includes them in the
|
||||
prompt so the AI can reference real content instead of hallucinating.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -188,6 +199,12 @@ class CoursePlanPipeline:
|
||||
# Decide once per instance whether to route through the LangGraph
|
||||
# AgentRuntime or fall back to the direct chat_json path.
|
||||
self._use_agent = self._resolve_agent_flag(env)
|
||||
# Import agent tools for resource-aware generation
|
||||
try:
|
||||
from odoo.addons.encoach_ai.services.agent_tools import invoke as agent_invoke
|
||||
self._agent_invoke = agent_invoke
|
||||
except ImportError:
|
||||
self._agent_invoke = None
|
||||
|
||||
@staticmethod
|
||||
def _resolve_agent_flag(env):
|
||||
@@ -494,3 +511,291 @@ class CoursePlanPipeline:
|
||||
elif isinstance(value, dict):
|
||||
lines.append(f"{key}: " + json.dumps(value, ensure_ascii=False))
|
||||
return "\n".join(lines)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# NEW: Deliverable detection from course outlines (GE1-style)
|
||||
# ------------------------------------------------------------------
|
||||
def generate_deliverables_from_outline(self, plan_id, course_outline_text):
|
||||
"""Parse a course outline (PDF/text) and extract structured deliverables.
|
||||
|
||||
Creates :py:class:`encoach.course.plan.deliverable` rows that
|
||||
the AI uses when generating week materials.
|
||||
"""
|
||||
plan = self.env['encoach.course.plan'].sudo().browse(int(plan_id))
|
||||
if not plan.exists():
|
||||
raise ValueError('Plan not found')
|
||||
|
||||
# Use agent tool to detect deliverables
|
||||
if self._agent_invoke:
|
||||
result = self._agent_invoke(
|
||||
self.env, "deliverables.detect",
|
||||
{
|
||||
"course_outline_text": course_outline_text,
|
||||
"cefr_level": plan.cefr_level,
|
||||
"total_weeks": plan.total_weeks,
|
||||
}
|
||||
)
|
||||
if result.get("ok"):
|
||||
# Create deliverable records
|
||||
Deliverable = self.env['encoach.course.plan.deliverable'].sudo()
|
||||
created = []
|
||||
for d in result.get("deliverables", []):
|
||||
try:
|
||||
rec = Deliverable.create({
|
||||
'plan_id': plan.id,
|
||||
'week_number': int(d.get('week_number', 1)),
|
||||
'code': d.get('code', ''),
|
||||
'skill': d.get('skill', 'integrated'),
|
||||
'description': d.get('description', ''),
|
||||
'cefr_level': d.get('cefr_level', plan.cefr_level),
|
||||
'resource_dependencies_json': json.dumps(d.get('resource_hints', []), ensure_ascii=False),
|
||||
'ai_generated': True,
|
||||
'generation_prompt': f"Extracted from course outline: {course_outline_text[:200]}...",
|
||||
})
|
||||
created.append(rec)
|
||||
except Exception as exc:
|
||||
_logger.warning("Failed to create deliverable: %s", exc)
|
||||
|
||||
# Save resource dependencies too
|
||||
ResourceDep = self.env['encoach.course.plan.resource.dep'].sudo()
|
||||
for r in result.get("resources_needed", []):
|
||||
try:
|
||||
ResourceDep.create({
|
||||
'plan_id': plan.id,
|
||||
'name': r.get('title', 'Unknown Resource'),
|
||||
'resource_type': r.get('type', 'other'),
|
||||
'citation': r.get('citation', ''),
|
||||
'ai_usage_notes': r.get('purpose', ''),
|
||||
'is_required': True,
|
||||
})
|
||||
except Exception as exc:
|
||||
_logger.warning("Failed to save resource: %s", exc)
|
||||
|
||||
return {
|
||||
'deliverables_created': len(created),
|
||||
'resources_needed': len(result.get('resources_needed', [])),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError(result.get('error', 'Deliverable detection failed'))
|
||||
else:
|
||||
raise RuntimeError('Agent tools not available for deliverable detection')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# NEW: Resource-aware week material generation
|
||||
# ------------------------------------------------------------------
|
||||
def generate_week_materials_with_resources(self, plan_id, week_number):
|
||||
"""Generate materials using registered resource dependencies."""
|
||||
plan = self.env['encoach.course.plan'].sudo().browse(int(plan_id))
|
||||
if not plan.exists():
|
||||
raise ValueError('Plan not found')
|
||||
|
||||
week = plan.week_ids.filtered(lambda w: w.week_number == int(week_number))
|
||||
if not week:
|
||||
raise ValueError(f'Week {week_number} not found')
|
||||
week = week[0]
|
||||
|
||||
# Fetch available resources
|
||||
resources = []
|
||||
if self._agent_invoke:
|
||||
res_result = self._agent_invoke(
|
||||
self.env, "resources.fetch",
|
||||
{"plan_id": plan.id, "is_available": True}
|
||||
)
|
||||
if res_result.get("ok"):
|
||||
resources = res_result.get("resources", [])
|
||||
|
||||
# Fetch deliverables for this week
|
||||
deliverables = []
|
||||
if self._agent_invoke:
|
||||
del_result = self._agent_invoke(
|
||||
self.env, "deliverables.fetch",
|
||||
{"plan_id": plan.id, "week_number": int(week_number)}
|
||||
)
|
||||
if del_result.get("ok"):
|
||||
deliverables = del_result.get("deliverables", [])
|
||||
|
||||
# Build enhanced prompt with resources and deliverables
|
||||
resource_context = ""
|
||||
if resources:
|
||||
resource_context = "\n\nAvailable Resources:\n" + json.dumps(resources, indent=2, ensure_ascii=False)
|
||||
|
||||
deliverable_context = ""
|
||||
if deliverables:
|
||||
deliverable_context = "\n\nTarget Deliverables (must be addressed in materials):\n" + json.dumps(deliverables, indent=2, ensure_ascii=False)
|
||||
|
||||
# Call base generation with enhanced context
|
||||
outcomes = plan._loads(plan.outcomes_json, {})
|
||||
items = week._loads(week.items_json, [])
|
||||
|
||||
system_msg = (
|
||||
"You are an expert English language teacher creating ready-to-use classroom materials. "
|
||||
"Your output MUST be valid JSON matching the schema. USE the available resources "
|
||||
"provided below. Address ALL deliverables listed."
|
||||
)
|
||||
user_msg = (
|
||||
f"Course: {plan.name}\n"
|
||||
f"CEFR: {(plan.cefr_level or '').upper()}\n"
|
||||
f"Week {week.week_number} — {week.date_label or ''}\n"
|
||||
f"Unit: {week.unit or ''}\n"
|
||||
f"Focus: {week.focus or ''}\n\n"
|
||||
f"Week items:\n{json.dumps(items, indent=2, ensure_ascii=False)}\n\n"
|
||||
f"Full outcome catalogue:\n{json.dumps(outcomes, indent=2, ensure_ascii=False)}"
|
||||
f"{resource_context}"
|
||||
f"{deliverable_context}\n\n"
|
||||
+ _WEEK_JSON_HINT
|
||||
)
|
||||
|
||||
content = self._invoke_agent_or_chat(
|
||||
agent_key="course_week_materials",
|
||||
system_msg=system_msg,
|
||||
user_msg=user_msg,
|
||||
variables={
|
||||
"course": plan.name,
|
||||
"cefr_level": (plan.cefr_level or "").lower(),
|
||||
"week_number": week.week_number,
|
||||
},
|
||||
temperature=0.6,
|
||||
max_tokens=6000,
|
||||
action="course_plan.generate_week_with_resources",
|
||||
)
|
||||
|
||||
if content is None or 'error' in content:
|
||||
raise RuntimeError(
|
||||
(content or {}).get('error', 'AI generation failed.')
|
||||
)
|
||||
|
||||
# Wipe previous materials
|
||||
existing = self.env['encoach.course.plan.material'].sudo().search([
|
||||
('plan_id', '=', plan.id), ('week_id', '=', week.id),
|
||||
])
|
||||
if existing:
|
||||
existing.unlink()
|
||||
|
||||
# Create new materials with resource tracking
|
||||
Material = self.env['encoach.course.plan.material'].sudo()
|
||||
created = []
|
||||
for m in content.get('materials') or []:
|
||||
try:
|
||||
rec = Material.create({
|
||||
'plan_id': plan.id,
|
||||
'week_id': week.id,
|
||||
'skill': (m.get('skill') or 'integrated').strip().lower(),
|
||||
'material_type': (m.get('material_type') or 'other').strip(),
|
||||
'title': (m.get('title') or '').strip() or 'Untitled',
|
||||
'summary': (m.get('summary') or '').strip(),
|
||||
'body_json': json.dumps(m.get('body') or {}, ensure_ascii=False),
|
||||
'body_text': self._flatten_body(m.get('body') or {}),
|
||||
'required_resources_json': json.dumps(m.get('resources_used', []), ensure_ascii=False),
|
||||
'ai_generation_notes': m.get('generation_notes', ''),
|
||||
})
|
||||
created.append(rec)
|
||||
except Exception as exc:
|
||||
_logger.warning("Skipping bad material row: %s", exc)
|
||||
|
||||
return created
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# NEW: Rich media generation for materials
|
||||
# ------------------------------------------------------------------
|
||||
def suggest_media_for_material(self, material_id):
|
||||
"""Suggest what media (images, audio) would enhance a material."""
|
||||
material = self.env['encoach.course.plan.material'].sudo().browse(int(material_id))
|
||||
if not material.exists():
|
||||
raise ValueError('Material not found')
|
||||
|
||||
if not self._agent_invoke:
|
||||
raise RuntimeError('Agent tools not available')
|
||||
|
||||
# Get visual suggestions
|
||||
result = self._agent_invoke(
|
||||
self.env, "media.suggest_visuals",
|
||||
{
|
||||
"content_description": material.body_text or material.summary or '',
|
||||
"material_type": material.material_type,
|
||||
"target_audience": f"{material.plan_id.cefr_level} level learners",
|
||||
}
|
||||
)
|
||||
|
||||
if result.get("ok"):
|
||||
return {
|
||||
'suggestions': result.get('visuals', []),
|
||||
'material_id': material_id,
|
||||
}
|
||||
return {'error': result.get('error', 'Could not generate suggestions')}
|
||||
|
||||
def generate_media_for_material(self, material_id, media_type='image'):
|
||||
"""Generate actual media (image/audio) for a material."""
|
||||
material = self.env['encoach.course.plan.material'].sudo().browse(int(material_id))
|
||||
if not material.exists():
|
||||
raise ValueError('Material not found')
|
||||
|
||||
if not self._agent_invoke:
|
||||
raise RuntimeError('Agent tools not available')
|
||||
|
||||
if media_type == 'image':
|
||||
# Get suggestions first
|
||||
suggestions = self.suggest_media_for_material(material_id)
|
||||
if 'error' in suggestions:
|
||||
return suggestions
|
||||
|
||||
# Use first suggestion's prompt
|
||||
visuals = suggestions.get('suggestions', [])
|
||||
if not visuals:
|
||||
return {'error': 'No visual suggestions available'}
|
||||
|
||||
prompt = visuals[0].get('prompt_for_ai', visuals[0].get('description', ''))
|
||||
|
||||
result = self._agent_invoke(
|
||||
self.env, "media.generate_image",
|
||||
{
|
||||
"prompt": prompt,
|
||||
"material_id": material_id,
|
||||
"style": visuals[0].get('complexity', 'educational'),
|
||||
}
|
||||
)
|
||||
|
||||
if result.get("ok"):
|
||||
# Update material with media info
|
||||
material.write({
|
||||
'media_type': 'image',
|
||||
'media_generation_prompt': result.get('prompt_used', prompt),
|
||||
'media_asset_url': result.get('note', ''), # Would be actual URL after generation
|
||||
})
|
||||
return {
|
||||
'media_type': 'image',
|
||||
'prompt': prompt,
|
||||
'suggestion': visuals[0],
|
||||
'material_id': material_id,
|
||||
}
|
||||
return {'error': result.get('error', 'Image generation failed')}
|
||||
|
||||
elif media_type == 'audio':
|
||||
# For listening scripts, generate audio
|
||||
body = material._loads(material.body_json, {})
|
||||
text = body.get('script', body.get('text', ''))
|
||||
|
||||
if not text:
|
||||
return {'error': 'No text available for audio generation'}
|
||||
|
||||
result = self._agent_invoke(
|
||||
self.env, "media.generate_audio",
|
||||
{
|
||||
"text": text[:3000], # Limit length
|
||||
"material_id": material_id,
|
||||
"purpose": "listening_exercise" if material.material_type == 'listening_script' else "pronunciation_guide",
|
||||
}
|
||||
)
|
||||
|
||||
if result.get("ok"):
|
||||
material.write({
|
||||
'media_type': 'audio',
|
||||
'media_generation_prompt': f"Audio for: {text[:200]}...",
|
||||
})
|
||||
return {
|
||||
'media_type': 'audio',
|
||||
'service': result.get('service'),
|
||||
'material_id': material_id,
|
||||
}
|
||||
return {'error': result.get('error', 'Audio generation failed')}
|
||||
|
||||
return {'error': f'Unsupported media type: {media_type}'}
|
||||
|
||||
Reference in New Issue
Block a user