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
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user