Files
breakpilot-compliance/ai-compliance-sdk/internal/training/assignment.go
Benjamin Boenisch 9b8b7ca073
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 48s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
feat(training): add Media Pipeline — TTS Audio, Presentation Video, Bulk Generation
Phase A: 8 new IT-Security training modules (SEC-PWD, SEC-DESK, SEC-KIAI,
SEC-BYOD, SEC-VIDEO, SEC-USB, SEC-INC, SEC-HOME) with CTM entries.
Bulk content and quiz generation endpoints for all 28 modules.

Phase B: Piper TTS service (Python/FastAPI) for local German speech synthesis.
training_media table, TTSClient in Go backend, audio generation endpoints,
AudioPlayer component in frontend. MinIO storage integration.

Phase C: FFmpeg presentation video pipeline — LLM generates slide scripts,
ImageMagick renders 1920x1080 slides, FFmpeg combines with audio to MP4.
VideoPlayer and ScriptPreview components in frontend.

New files: 15 created, 9 modified
- compliance-tts-service/ (Dockerfile, main.py, tts_engine.py, storage.py,
  slide_renderer.py, video_generator.py)
- migrations 014-016 (training engine, IT-security modules, media table)
- training package (models, store, content_generator, media, handlers)
- frontend (AudioPlayer, VideoPlayer, ScriptPreview, api, types, page)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:42:33 +01:00

184 lines
4.8 KiB
Go

package training
import (
"context"
"time"
"github.com/google/uuid"
)
// timeNow is a package-level function for testing
var timeNow = time.Now
// ComputeAssignments calculates all necessary assignments for a user based on
// their roles, existing assignments, and deadlines. Returns new assignments
// that need to be created.
func ComputeAssignments(ctx context.Context, store *Store, tenantID uuid.UUID,
userID uuid.UUID, userName, userEmail string, roleCodes []string, trigger string) ([]TrainingAssignment, error) {
if trigger == "" {
trigger = string(TriggerManual)
}
// Get all required modules for the user's roles
requiredModules, err := ComputeRequiredModules(ctx, store, tenantID, roleCodes)
if err != nil {
return nil, err
}
// Get existing active assignments for this user
existingAssignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{
UserID: &userID,
Limit: 1000,
})
if err != nil {
return nil, err
}
// Build a map of existing assignments by module_id for quick lookup
existingByModule := make(map[uuid.UUID]*TrainingAssignment)
for i := range existingAssignments {
a := &existingAssignments[i]
// Only consider non-expired, non-completed-and-expired assignments
if a.Status != AssignmentStatusExpired {
existingByModule[a.ModuleID] = a
}
}
var newAssignments []TrainingAssignment
now := timeNow().UTC()
for _, module := range requiredModules {
existing, hasExisting := existingByModule[module.ID]
// Skip if there's an active, valid assignment
if hasExisting {
switch existing.Status {
case AssignmentStatusCompleted:
// Check if the completed assignment is still valid
if existing.CompletedAt != nil {
validUntil := existing.CompletedAt.AddDate(0, 0, module.ValidityDays)
if validUntil.After(now) {
continue // Still valid, skip
}
}
case AssignmentStatusPending, AssignmentStatusInProgress:
continue // Assignment exists and is active
case AssignmentStatusOverdue:
continue // Already tracked as overdue
}
}
// Determine the role code for this assignment
roleCode := ""
for _, role := range roleCodes {
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.ModuleID == module.ID {
roleCode = role
break
}
}
if roleCode != "" {
break
}
}
// Calculate deadline based on frequency
var deadline time.Time
switch module.FrequencyType {
case FrequencyOnboarding:
deadline = now.AddDate(0, 0, 30) // 30 days for onboarding
case FrequencyMicro:
deadline = now.AddDate(0, 0, 14) // 14 days for micro
default:
deadline = now.AddDate(0, 0, 90) // 90 days default
}
assignment := TrainingAssignment{
TenantID: tenantID,
ModuleID: module.ID,
UserID: userID,
UserName: userName,
UserEmail: userEmail,
RoleCode: roleCode,
TriggerType: TriggerType(trigger),
Status: AssignmentStatusPending,
Deadline: deadline,
ModuleCode: module.ModuleCode,
ModuleTitle: module.Title,
}
// Create the assignment in the store
if err := store.CreateAssignment(ctx, &assignment); err != nil {
return nil, err
}
// Log the assignment
store.LogAction(ctx, &AuditLogEntry{
TenantID: tenantID,
UserID: &userID,
Action: AuditActionAssigned,
EntityType: AuditEntityAssignment,
EntityID: &assignment.ID,
Details: map[string]interface{}{
"module_code": module.ModuleCode,
"trigger": trigger,
"role_code": roleCode,
"deadline": deadline.Format(time.RFC3339),
},
})
newAssignments = append(newAssignments, assignment)
}
if newAssignments == nil {
newAssignments = []TrainingAssignment{}
}
return newAssignments, nil
}
// BulkAssign assigns a module to all users with specific roles
// Returns the number of assignments created
func BulkAssign(ctx context.Context, store *Store, tenantID uuid.UUID,
moduleID uuid.UUID, users []UserInfo, trigger string, deadline time.Time) (int, error) {
if trigger == "" {
trigger = string(TriggerManual)
}
count := 0
for _, user := range users {
assignment := TrainingAssignment{
TenantID: tenantID,
ModuleID: moduleID,
UserID: user.UserID,
UserName: user.UserName,
UserEmail: user.UserEmail,
RoleCode: user.RoleCode,
TriggerType: TriggerType(trigger),
Status: AssignmentStatusPending,
Deadline: deadline,
}
if err := store.CreateAssignment(ctx, &assignment); err != nil {
return count, err
}
count++
}
return count, nil
}
// UserInfo contains basic user information for bulk operations
type UserInfo struct {
UserID uuid.UUID `json:"user_id"`
UserName string `json:"user_name"`
UserEmail string `json:"user_email"`
RoleCode string `json:"role_code"`
}