feat(training+controls): interactive video pipeline, training blocks, control generator, CE libraries
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 37s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped

Interactive Training Videos (CP-TRAIN):
- DB migration 022: training_checkpoints + checkpoint_progress tables
- NarratorScript generation via Anthropic (AI Teacher persona, German)
- TTS batch synthesis + interactive video pipeline (slides + checkpoint slides + FFmpeg)
- 4 new API endpoints: generate-interactive, interactive-manifest, checkpoint submit, checkpoint progress
- InteractiveVideoPlayer component (HTML5 Video, quiz overlay, seek protection, progress tracking)
- Learner portal integration with automatic completion on all checkpoints passed
- 30 new tests (handler validation + grading logic + manifest/progress + seek protection)

Training Blocks:
- Block generator, block store, block config CRUD + preview/generate endpoints
- Migration 021: training_blocks schema

Control Generator + Canonical Library:
- Control generator routes + service enhancements
- Canonical control library helpers, sidebar entry
- Citation backfill service + tests
- CE libraries data (hazard, protection, evidence, lifecycle, components)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-16 21:41:48 +01:00
parent d2133dbfa2
commit 4f6bc8f6f6
50 changed files with 17299 additions and 198 deletions

View File

@@ -16,8 +16,9 @@ import (
type MediaType string
const (
MediaTypeAudio MediaType = "audio"
MediaTypeVideo MediaType = "video"
MediaTypeAudio MediaType = "audio"
MediaTypeVideo MediaType = "video"
MediaTypeInteractiveVideo MediaType = "interactive_video"
)
// MediaStatus represents the processing status
@@ -169,6 +170,57 @@ func (c *TTSClient) GenerateVideo(ctx context.Context, req *TTSGenerateVideoRequ
return &result, nil
}
// PresignedURLRequest is the request to get a presigned URL
type PresignedURLRequest struct {
Bucket string `json:"bucket"`
ObjectKey string `json:"object_key"`
Expires int `json:"expires"`
}
// PresignedURLResponse is the response containing a presigned URL
type PresignedURLResponse struct {
URL string `json:"url"`
ExpiresIn int `json:"expires_in"`
}
// GetPresignedURL requests a presigned URL from the TTS service
func (c *TTSClient) GetPresignedURL(ctx context.Context, bucket, objectKey string) (string, error) {
reqBody := PresignedURLRequest{
Bucket: bucket,
ObjectKey: objectKey,
Expires: 3600,
}
body, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/presigned-url", bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return "", fmt.Errorf("TTS presigned URL request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("TTS presigned URL error (%d): %s", resp.StatusCode, string(respBody))
}
var result PresignedURLResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("parse presigned URL response: %w", err)
}
return result.URL, nil
}
// IsHealthy checks if the TTS service is responsive
func (c *TTSClient) IsHealthy(ctx context.Context) bool {
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil)
@@ -184,3 +236,115 @@ func (c *TTSClient) IsHealthy(ctx context.Context) bool {
return resp.StatusCode == http.StatusOK
}
// ============================================================================
// Interactive Video TTS Client Methods
// ============================================================================
// SynthesizeSectionsRequest is the request for batch section audio synthesis
type SynthesizeSectionsRequest struct {
Sections []SectionAudio `json:"sections"`
Voice string `json:"voice"`
ModuleID string `json:"module_id"`
}
// SectionAudio represents one section's text for audio synthesis
type SectionAudio struct {
Text string `json:"text"`
Heading string `json:"heading"`
}
// SynthesizeSectionsResponse is the response from batch section synthesis
type SynthesizeSectionsResponse struct {
Sections []SectionResult `json:"sections"`
TotalDuration float64 `json:"total_duration"`
}
// SectionResult is the result for one section's audio
type SectionResult struct {
Heading string `json:"heading"`
AudioPath string `json:"audio_path"`
AudioObjectKey string `json:"audio_object_key"`
Duration float64 `json:"duration"`
StartTimestamp float64 `json:"start_timestamp"`
}
// GenerateInteractiveVideoRequest is the request for interactive video generation
type GenerateInteractiveVideoRequest struct {
Script *NarratorScript `json:"script"`
Audio *SynthesizeSectionsResponse `json:"audio"`
ModuleID string `json:"module_id"`
}
// GenerateInteractiveVideoResponse is the response from interactive video generation
type GenerateInteractiveVideoResponse struct {
VideoID string `json:"video_id"`
Bucket string `json:"bucket"`
ObjectKey string `json:"object_key"`
DurationSeconds float64 `json:"duration_seconds"`
SizeBytes int64 `json:"size_bytes"`
}
// SynthesizeSections calls the TTS service to synthesize audio for multiple sections
func (c *TTSClient) SynthesizeSections(ctx context.Context, req *SynthesizeSectionsRequest) (*SynthesizeSectionsResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/synthesize-sections", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("TTS synthesize-sections request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("TTS synthesize-sections error (%d): %s", resp.StatusCode, string(respBody))
}
var result SynthesizeSectionsResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse TTS synthesize-sections response: %w", err)
}
return &result, nil
}
// GenerateInteractiveVideo calls the TTS service to create an interactive video with checkpoint slides
func (c *TTSClient) GenerateInteractiveVideo(ctx context.Context, req *GenerateInteractiveVideoRequest) (*GenerateInteractiveVideoResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/generate-interactive-video", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("TTS interactive video request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("TTS interactive video error (%d): %s", resp.StatusCode, string(respBody))
}
var result GenerateInteractiveVideoResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse TTS interactive video response: %w", err)
}
return &result, nil
}