feat(training): add Media Pipeline — TTS Audio, Presentation Video, Bulk Generation
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
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
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>
This commit is contained in:
177
ai-compliance-sdk/internal/training/escalation.go
Normal file
177
ai-compliance-sdk/internal/training/escalation.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Escalation level thresholds (days overdue)
|
||||
const (
|
||||
EscalationThresholdL1 = 7 // Reminder to user
|
||||
EscalationThresholdL2 = 14 // Notify team lead
|
||||
EscalationThresholdL3 = 30 // Notify management
|
||||
EscalationThresholdL4 = 45 // Notify compliance officer
|
||||
)
|
||||
|
||||
// EscalationLabels maps levels to human-readable labels
|
||||
var EscalationLabels = map[int]string{
|
||||
0: "Keine Eskalation",
|
||||
1: "Erinnerung an Mitarbeiter",
|
||||
2: "Benachrichtigung Teamleitung",
|
||||
3: "Benachrichtigung Management",
|
||||
4: "Benachrichtigung Compliance Officer",
|
||||
}
|
||||
|
||||
// CheckEscalations checks all overdue assignments and escalates as needed
|
||||
func CheckEscalations(ctx context.Context, store *Store, tenantID uuid.UUID) ([]EscalationResult, error) {
|
||||
overdueAssignments, err := store.ListOverdueAssignments(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []EscalationResult
|
||||
now := timeNow().UTC()
|
||||
|
||||
for _, assignment := range overdueAssignments {
|
||||
daysOverdue := int(math.Floor(now.Sub(assignment.Deadline).Hours() / 24))
|
||||
if daysOverdue < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine new escalation level
|
||||
newLevel := 0
|
||||
if daysOverdue >= EscalationThresholdL4 {
|
||||
newLevel = 4
|
||||
} else if daysOverdue >= EscalationThresholdL3 {
|
||||
newLevel = 3
|
||||
} else if daysOverdue >= EscalationThresholdL2 {
|
||||
newLevel = 2
|
||||
} else if daysOverdue >= EscalationThresholdL1 {
|
||||
newLevel = 1
|
||||
}
|
||||
|
||||
// Only escalate if the level has increased
|
||||
if newLevel <= assignment.EscalationLevel {
|
||||
continue
|
||||
}
|
||||
|
||||
previousLevel := assignment.EscalationLevel
|
||||
|
||||
// Update the assignment
|
||||
nowTime := now
|
||||
_, err := store.pool.Exec(ctx, `
|
||||
UPDATE training_assignments SET
|
||||
escalation_level = $2,
|
||||
last_escalation_at = $3,
|
||||
status = 'overdue',
|
||||
updated_at = $3
|
||||
WHERE id = $1
|
||||
`, assignment.ID, newLevel, nowTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Log the escalation
|
||||
assignmentID := assignment.ID
|
||||
store.LogAction(ctx, &AuditLogEntry{
|
||||
TenantID: tenantID,
|
||||
UserID: &assignment.UserID,
|
||||
Action: AuditActionEscalated,
|
||||
EntityType: AuditEntityAssignment,
|
||||
EntityID: &assignmentID,
|
||||
Details: map[string]interface{}{
|
||||
"previous_level": previousLevel,
|
||||
"new_level": newLevel,
|
||||
"days_overdue": daysOverdue,
|
||||
"label": EscalationLabels[newLevel],
|
||||
},
|
||||
})
|
||||
|
||||
results = append(results, EscalationResult{
|
||||
AssignmentID: assignment.ID,
|
||||
UserID: assignment.UserID,
|
||||
UserName: assignment.UserName,
|
||||
UserEmail: assignment.UserEmail,
|
||||
ModuleTitle: assignment.ModuleTitle,
|
||||
PreviousLevel: previousLevel,
|
||||
NewLevel: newLevel,
|
||||
DaysOverdue: daysOverdue,
|
||||
EscalationLabel: EscalationLabels[newLevel],
|
||||
})
|
||||
}
|
||||
|
||||
if results == nil {
|
||||
results = []EscalationResult{}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetOverdueDeadlines returns all overdue assignments with deadline info
|
||||
func GetOverdueDeadlines(ctx context.Context, store *Store, tenantID uuid.UUID) ([]DeadlineInfo, error) {
|
||||
rows, err := store.pool.Query(ctx, `
|
||||
SELECT
|
||||
ta.id, m.module_code, m.title,
|
||||
ta.user_id, ta.user_name, ta.deadline, ta.status,
|
||||
EXTRACT(DAY FROM (NOW() - ta.deadline))::INT AS days_overdue
|
||||
FROM training_assignments ta
|
||||
JOIN training_modules m ON m.id = ta.module_id
|
||||
WHERE ta.tenant_id = $1
|
||||
AND ta.status IN ('pending', 'in_progress', 'overdue')
|
||||
AND ta.deadline < NOW()
|
||||
ORDER BY ta.deadline ASC
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var deadlines []DeadlineInfo
|
||||
for rows.Next() {
|
||||
var d DeadlineInfo
|
||||
var status string
|
||||
|
||||
err := rows.Scan(
|
||||
&d.AssignmentID, &d.ModuleCode, &d.ModuleTitle,
|
||||
&d.UserID, &d.UserName, &d.Deadline, &status,
|
||||
&d.DaysLeft,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.Status = AssignmentStatus(status)
|
||||
d.DaysLeft = -d.DaysLeft // Negative means overdue
|
||||
deadlines = append(deadlines, d)
|
||||
}
|
||||
|
||||
if deadlines == nil {
|
||||
deadlines = []DeadlineInfo{}
|
||||
}
|
||||
|
||||
return deadlines, nil
|
||||
}
|
||||
|
||||
// VerifyCertificate verifies a certificate by checking the assignment status
|
||||
func VerifyCertificate(ctx context.Context, store *Store, certificateID uuid.UUID) (bool, *TrainingAssignment, error) {
|
||||
// Find assignment with this certificate
|
||||
var assignmentID uuid.UUID
|
||||
err := store.pool.QueryRow(ctx,
|
||||
"SELECT id FROM training_assignments WHERE certificate_id = $1",
|
||||
certificateID).Scan(&assignmentID)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
assignment, err := store.GetAssignment(ctx, assignmentID)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if assignment == nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return assignment.Status == AssignmentStatusCompleted, assignment, nil
|
||||
}
|
||||
Reference in New Issue
Block a user