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:
311
admin-compliance/lib/sdk/training/api.ts
Normal file
311
admin-compliance/lib/sdk/training/api.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Training Engine API Client
|
||||
* Communicates with the Go backend via Next.js API proxy at /api/sdk/v1/training/*
|
||||
*/
|
||||
|
||||
import type {
|
||||
ModuleListResponse,
|
||||
AssignmentListResponse,
|
||||
MatrixResponse,
|
||||
AuditLogResponse,
|
||||
EscalationResponse,
|
||||
DeadlineListResponse,
|
||||
TrainingModule,
|
||||
TrainingAssignment,
|
||||
ModuleContent,
|
||||
QuizQuestion,
|
||||
QuizAttempt,
|
||||
QuizSubmitResponse,
|
||||
TrainingStats,
|
||||
TrainingMedia,
|
||||
} from './types'
|
||||
|
||||
const BASE_URL = '/api/sdk/v1/training'
|
||||
|
||||
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': typeof window !== 'undefined'
|
||||
? (localStorage.getItem('bp-tenant-id') || 'default')
|
||||
: 'default',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(error.error || `API Error: ${res.status}`)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MODULES
|
||||
// =============================================================================
|
||||
|
||||
export async function getModules(filters?: {
|
||||
regulation_area?: string
|
||||
frequency_type?: string
|
||||
search?: string
|
||||
}): Promise<ModuleListResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.regulation_area) params.set('regulation_area', filters.regulation_area)
|
||||
if (filters?.frequency_type) params.set('frequency_type', filters.frequency_type)
|
||||
if (filters?.search) params.set('search', filters.search)
|
||||
const qs = params.toString()
|
||||
return apiFetch<ModuleListResponse>(`/modules${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
export async function getModule(id: string): Promise<{
|
||||
module: TrainingModule
|
||||
content: ModuleContent | null
|
||||
questions: QuizQuestion[]
|
||||
}> {
|
||||
return apiFetch(`/modules/${id}`)
|
||||
}
|
||||
|
||||
export async function createModule(data: {
|
||||
module_code: string
|
||||
title: string
|
||||
description?: string
|
||||
regulation_area: string
|
||||
frequency_type: string
|
||||
duration_minutes?: number
|
||||
pass_threshold?: number
|
||||
}): Promise<TrainingModule> {
|
||||
return apiFetch<TrainingModule>('/modules', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateModule(id: string, data: Record<string, unknown>): Promise<TrainingModule> {
|
||||
return apiFetch<TrainingModule>(`/modules/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MATRIX
|
||||
// =============================================================================
|
||||
|
||||
export async function getMatrix(): Promise<MatrixResponse> {
|
||||
return apiFetch<MatrixResponse>('/matrix')
|
||||
}
|
||||
|
||||
export async function getMatrixForRole(role: string): Promise<{
|
||||
role: string
|
||||
label: string
|
||||
entries: Array<{ module_id: string; module_code: string; module_title: string; is_mandatory: boolean; priority: number }>
|
||||
total: number
|
||||
}> {
|
||||
return apiFetch(`/matrix/${role}`)
|
||||
}
|
||||
|
||||
export async function setMatrixEntry(data: {
|
||||
role_code: string
|
||||
module_id: string
|
||||
is_mandatory: boolean
|
||||
priority: number
|
||||
}): Promise<unknown> {
|
||||
return apiFetch('/matrix', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteMatrixEntry(role: string, moduleId: string): Promise<unknown> {
|
||||
return apiFetch(`/matrix/${role}/${moduleId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ASSIGNMENTS
|
||||
// =============================================================================
|
||||
|
||||
export async function computeAssignments(data: {
|
||||
user_id: string
|
||||
user_name: string
|
||||
user_email: string
|
||||
roles: string[]
|
||||
trigger?: string
|
||||
}): Promise<{ assignments: TrainingAssignment[]; created: number }> {
|
||||
return apiFetch('/assignments/compute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getAssignments(filters?: {
|
||||
user_id?: string
|
||||
module_id?: string
|
||||
role?: string
|
||||
status?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<AssignmentListResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.user_id) params.set('user_id', filters.user_id)
|
||||
if (filters?.module_id) params.set('module_id', filters.module_id)
|
||||
if (filters?.role) params.set('role', filters.role)
|
||||
if (filters?.status) params.set('status', filters.status)
|
||||
if (filters?.limit) params.set('limit', String(filters.limit))
|
||||
if (filters?.offset) params.set('offset', String(filters.offset))
|
||||
const qs = params.toString()
|
||||
return apiFetch<AssignmentListResponse>(`/assignments${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
export async function getAssignment(id: string): Promise<TrainingAssignment> {
|
||||
return apiFetch<TrainingAssignment>(`/assignments/${id}`)
|
||||
}
|
||||
|
||||
export async function startAssignment(id: string): Promise<{ status: string }> {
|
||||
return apiFetch(`/assignments/${id}/start`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function updateAssignmentProgress(id: string, progress: number): Promise<{ status: string; progress: number }> {
|
||||
return apiFetch(`/assignments/${id}/progress`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ progress }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function completeAssignment(id: string): Promise<{ status: string }> {
|
||||
return apiFetch(`/assignments/${id}/complete`, { method: 'POST' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUIZ
|
||||
// =============================================================================
|
||||
|
||||
export async function getQuiz(moduleId: string): Promise<{ questions: QuizQuestion[]; total: number }> {
|
||||
return apiFetch(`/quiz/${moduleId}`)
|
||||
}
|
||||
|
||||
export async function submitQuiz(moduleId: string, data: {
|
||||
assignment_id: string
|
||||
answers: Array<{ question_id: string; selected_index: number }>
|
||||
duration_seconds?: number
|
||||
}): Promise<QuizSubmitResponse> {
|
||||
return apiFetch<QuizSubmitResponse>(`/quiz/${moduleId}/submit`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getQuizAttempts(assignmentId: string): Promise<{ attempts: QuizAttempt[]; total: number }> {
|
||||
return apiFetch(`/quiz/attempts/${assignmentId}`)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTENT GENERATION
|
||||
// =============================================================================
|
||||
|
||||
export async function generateContent(moduleId: string, language?: string): Promise<ModuleContent> {
|
||||
return apiFetch<ModuleContent>('/content/generate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ module_id: moduleId, language: language || 'de' }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateQuiz(moduleId: string, count?: number): Promise<{ questions: QuizQuestion[]; total: number }> {
|
||||
return apiFetch('/content/generate-quiz', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ module_id: moduleId, count: count || 5 }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getContent(moduleId: string): Promise<ModuleContent> {
|
||||
return apiFetch<ModuleContent>(`/content/${moduleId}`)
|
||||
}
|
||||
|
||||
export async function publishContent(contentId: string): Promise<{ status: string }> {
|
||||
return apiFetch(`/content/${contentId}/publish`, { method: 'POST' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEADLINES & ESCALATION
|
||||
// =============================================================================
|
||||
|
||||
export async function getDeadlines(limit?: number): Promise<DeadlineListResponse> {
|
||||
const qs = limit ? `?limit=${limit}` : ''
|
||||
return apiFetch<DeadlineListResponse>(`/deadlines${qs}`)
|
||||
}
|
||||
|
||||
export async function getOverdueDeadlines(): Promise<DeadlineListResponse> {
|
||||
return apiFetch<DeadlineListResponse>('/deadlines/overdue')
|
||||
}
|
||||
|
||||
export async function checkEscalation(): Promise<EscalationResponse> {
|
||||
return apiFetch<EscalationResponse>('/escalation/check', { method: 'POST' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDIT & STATS
|
||||
// =============================================================================
|
||||
|
||||
export async function getAuditLog(filters?: {
|
||||
action?: string
|
||||
entity_type?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<AuditLogResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.action) params.set('action', filters.action)
|
||||
if (filters?.entity_type) params.set('entity_type', filters.entity_type)
|
||||
if (filters?.limit) params.set('limit', String(filters.limit))
|
||||
if (filters?.offset) params.set('offset', String(filters.offset))
|
||||
const qs = params.toString()
|
||||
return apiFetch<AuditLogResponse>(`/audit-log${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
export async function getStats(): Promise<TrainingStats> {
|
||||
return apiFetch<TrainingStats>('/stats')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BULK GENERATION
|
||||
// =============================================================================
|
||||
|
||||
export async function generateAllContent(language?: string): Promise<{ generated: number; skipped: number; errors: string[] }> {
|
||||
const qs = language ? `?language=${language}` : ''
|
||||
return apiFetch(`/content/generate-all${qs}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function generateAllQuizzes(): Promise<{ generated: number; skipped: number; errors: string[] }> {
|
||||
return apiFetch('/content/generate-all-quiz', { method: 'POST' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MEDIA (Audio/Video)
|
||||
// =============================================================================
|
||||
|
||||
export async function generateAudio(moduleId: string): Promise<TrainingMedia> {
|
||||
return apiFetch<TrainingMedia>(`/content/${moduleId}/generate-audio`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function getModuleMedia(moduleId: string): Promise<{ media: TrainingMedia[]; total: number }> {
|
||||
return apiFetch(`/media/${moduleId}`)
|
||||
}
|
||||
|
||||
export async function getMediaURL(mediaId: string): Promise<{ bucket: string; object_key: string; mime_type: string }> {
|
||||
return apiFetch(`/media/${mediaId}/url`)
|
||||
}
|
||||
|
||||
export async function publishMedia(mediaId: string, publish?: boolean): Promise<{ status: string; is_published: boolean }> {
|
||||
return apiFetch(`/media/${mediaId}/publish`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ publish: publish !== false }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateVideo(moduleId: string): Promise<TrainingMedia> {
|
||||
return apiFetch<TrainingMedia>(`/content/${moduleId}/generate-video`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function previewVideoScript(moduleId: string): Promise<{ title: string; sections: Array<{ heading: string; text: string; bullet_points: string[] }> }> {
|
||||
return apiFetch(`/content/${moduleId}/preview-script`, { method: 'POST' })
|
||||
}
|
||||
309
admin-compliance/lib/sdk/training/types.ts
Normal file
309
admin-compliance/lib/sdk/training/types.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Compliance Training Engine Types
|
||||
* TypeScript definitions for the Training Matrix, Assignments, Quiz, and Content
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS / CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type RegulationArea = 'dsgvo' | 'nis2' | 'iso27001' | 'ai_act' | 'geschgehg' | 'hinschg'
|
||||
export type FrequencyType = 'onboarding' | 'annual' | 'event_trigger' | 'micro'
|
||||
export type AssignmentStatus = 'pending' | 'in_progress' | 'completed' | 'overdue' | 'expired'
|
||||
export type TriggerType = 'onboarding' | 'annual' | 'event' | 'manual'
|
||||
export type Difficulty = 'easy' | 'medium' | 'hard'
|
||||
export type AuditAction = 'assigned' | 'started' | 'completed' | 'quiz_submitted' | 'escalated' | 'certificate_issued' | 'content_generated'
|
||||
|
||||
export const REGULATION_LABELS: Record<RegulationArea, string> = {
|
||||
dsgvo: 'DSGVO',
|
||||
nis2: 'NIS-2',
|
||||
iso27001: 'ISO 27001',
|
||||
ai_act: 'AI Act',
|
||||
geschgehg: 'GeschGehG',
|
||||
hinschg: 'HinSchG',
|
||||
}
|
||||
|
||||
export const REGULATION_COLORS: Record<RegulationArea, { bg: string; text: string; border: string }> = {
|
||||
dsgvo: { bg: 'bg-blue-100', text: 'text-blue-700', border: 'border-blue-300' },
|
||||
nis2: { bg: 'bg-purple-100', text: 'text-purple-700', border: 'border-purple-300' },
|
||||
iso27001: { bg: 'bg-green-100', text: 'text-green-700', border: 'border-green-300' },
|
||||
ai_act: { bg: 'bg-orange-100', text: 'text-orange-700', border: 'border-orange-300' },
|
||||
geschgehg: { bg: 'bg-yellow-100', text: 'text-yellow-700', border: 'border-yellow-300' },
|
||||
hinschg: { bg: 'bg-red-100', text: 'text-red-700', border: 'border-red-300' },
|
||||
}
|
||||
|
||||
export const FREQUENCY_LABELS: Record<FrequencyType, string> = {
|
||||
onboarding: 'Onboarding',
|
||||
annual: 'Jaehrlich',
|
||||
event_trigger: 'Ereignisbasiert',
|
||||
micro: 'Micro-Training',
|
||||
}
|
||||
|
||||
export const STATUS_LABELS: Record<AssignmentStatus, string> = {
|
||||
pending: 'Ausstehend',
|
||||
in_progress: 'In Bearbeitung',
|
||||
completed: 'Abgeschlossen',
|
||||
overdue: 'Ueberfaellig',
|
||||
expired: 'Abgelaufen',
|
||||
}
|
||||
|
||||
export const STATUS_COLORS: Record<AssignmentStatus, { bg: string; text: string }> = {
|
||||
pending: { bg: 'bg-gray-100', text: 'text-gray-700' },
|
||||
in_progress: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
||||
completed: { bg: 'bg-green-100', text: 'text-green-700' },
|
||||
overdue: { bg: 'bg-red-100', text: 'text-red-700' },
|
||||
expired: { bg: 'bg-gray-200', text: 'text-gray-500' },
|
||||
}
|
||||
|
||||
export const ROLE_LABELS: Record<string, string> = {
|
||||
R1: 'Geschaeftsfuehrung',
|
||||
R2: 'IT-Leitung',
|
||||
R3: 'Datenschutzbeauftragter',
|
||||
R4: 'Informationssicherheitsbeauftragter',
|
||||
R5: 'HR / Personal',
|
||||
R6: 'Einkauf / Beschaffung',
|
||||
R7: 'Fachabteilung',
|
||||
R8: 'IT-Administration',
|
||||
R9: 'Alle Mitarbeiter',
|
||||
}
|
||||
|
||||
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9'] as const
|
||||
|
||||
// =============================================================================
|
||||
// MAIN ENTITIES
|
||||
// =============================================================================
|
||||
|
||||
export interface TrainingModule {
|
||||
id: string
|
||||
tenant_id: string
|
||||
academy_course_id?: string
|
||||
module_code: string
|
||||
title: string
|
||||
description?: string
|
||||
regulation_area: RegulationArea
|
||||
nis2_relevant: boolean
|
||||
iso_controls: string[]
|
||||
frequency_type: FrequencyType
|
||||
validity_days: number
|
||||
risk_weight: number
|
||||
content_type: string
|
||||
duration_minutes: number
|
||||
pass_threshold: number
|
||||
is_active: boolean
|
||||
sort_order: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface TrainingMatrixEntry {
|
||||
id: string
|
||||
tenant_id: string
|
||||
role_code: string
|
||||
module_id: string
|
||||
is_mandatory: boolean
|
||||
priority: number
|
||||
created_at: string
|
||||
module_code?: string
|
||||
module_title?: string
|
||||
}
|
||||
|
||||
export interface TrainingAssignment {
|
||||
id: string
|
||||
tenant_id: string
|
||||
module_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
user_email: string
|
||||
role_code?: string
|
||||
trigger_type: TriggerType
|
||||
trigger_event?: string
|
||||
status: AssignmentStatus
|
||||
progress_percent: number
|
||||
quiz_score?: number
|
||||
quiz_passed?: boolean
|
||||
quiz_attempts: number
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
deadline: string
|
||||
certificate_id?: string
|
||||
escalation_level: number
|
||||
last_escalation_at?: string
|
||||
enrollment_id?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
module_code?: string
|
||||
module_title?: string
|
||||
}
|
||||
|
||||
export interface QuizQuestion {
|
||||
id: string
|
||||
question: string
|
||||
options: string[]
|
||||
difficulty: Difficulty
|
||||
}
|
||||
|
||||
export interface QuizAttempt {
|
||||
id: string
|
||||
assignment_id: string
|
||||
user_id: string
|
||||
answers: QuizAnswer[]
|
||||
score: number
|
||||
passed: boolean
|
||||
correct_count: number
|
||||
total_count: number
|
||||
duration_seconds?: number
|
||||
attempted_at: string
|
||||
}
|
||||
|
||||
export interface QuizAnswer {
|
||||
question_id: string
|
||||
selected_index: number
|
||||
correct: boolean
|
||||
}
|
||||
|
||||
export interface ModuleContent {
|
||||
id: string
|
||||
module_id: string
|
||||
version: number
|
||||
content_format: string
|
||||
content_body: string
|
||||
summary?: string
|
||||
generated_by?: string
|
||||
llm_model?: string
|
||||
is_published: boolean
|
||||
reviewed_by?: string
|
||||
reviewed_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
id: string
|
||||
tenant_id: string
|
||||
user_id?: string
|
||||
action: AuditAction
|
||||
entity_type: string
|
||||
entity_id?: string
|
||||
details: Record<string, unknown>
|
||||
ip_address?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface DeadlineInfo {
|
||||
assignment_id: string
|
||||
module_code: string
|
||||
module_title: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
deadline: string
|
||||
days_left: number
|
||||
status: AssignmentStatus
|
||||
}
|
||||
|
||||
export interface EscalationResult {
|
||||
assignment_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
user_email: string
|
||||
module_title: string
|
||||
previous_level: number
|
||||
new_level: number
|
||||
days_overdue: number
|
||||
escalation_label: string
|
||||
}
|
||||
|
||||
export interface TrainingStats {
|
||||
total_modules: number
|
||||
total_assignments: number
|
||||
completion_rate: number
|
||||
overdue_count: number
|
||||
pending_count: number
|
||||
in_progress_count: number
|
||||
completed_count: number
|
||||
avg_quiz_score: number
|
||||
avg_completion_days: number
|
||||
upcoming_deadlines: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API RESPONSES
|
||||
// =============================================================================
|
||||
|
||||
export interface ModuleListResponse {
|
||||
modules: TrainingModule[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface AssignmentListResponse {
|
||||
assignments: TrainingAssignment[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface MatrixResponse {
|
||||
entries: Record<string, TrainingMatrixEntry[]>
|
||||
roles: Record<string, string>
|
||||
}
|
||||
|
||||
export interface AuditLogResponse {
|
||||
entries: AuditLogEntry[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface EscalationResponse {
|
||||
results: EscalationResult[]
|
||||
total_checked: number
|
||||
escalated: number
|
||||
}
|
||||
|
||||
export interface DeadlineListResponse {
|
||||
deadlines: DeadlineInfo[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface QuizSubmitResponse {
|
||||
attempt_id: string
|
||||
score: number
|
||||
passed: boolean
|
||||
correct_count: number
|
||||
total_count: number
|
||||
threshold: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MEDIA (Audio/Video)
|
||||
// =============================================================================
|
||||
|
||||
export type MediaType = 'audio' | 'video'
|
||||
export type MediaStatus = 'processing' | 'completed' | 'failed'
|
||||
|
||||
export interface TrainingMedia {
|
||||
id: string
|
||||
module_id: string
|
||||
content_id?: string
|
||||
media_type: MediaType
|
||||
status: MediaStatus
|
||||
bucket: string
|
||||
object_key: string
|
||||
file_size_bytes: number
|
||||
duration_seconds: number
|
||||
mime_type: string
|
||||
voice_model: string
|
||||
language: string
|
||||
metadata: Record<string, unknown>
|
||||
error_message?: string
|
||||
generated_by: string
|
||||
is_published: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface VideoScript {
|
||||
title: string
|
||||
sections: VideoScriptSection[]
|
||||
}
|
||||
|
||||
export interface VideoScriptSection {
|
||||
heading: string
|
||||
text: string
|
||||
bullet_points: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user