@hed-hog/lms 0.0.365 → 0.0.370
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/certificate/certificate.controller.d.ts +1 -1
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +4 -2
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +50 -0
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +73 -0
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/class-group/class-group.controller.d.ts +1 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.service.d.ts +1 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/course/course-ai-usage.service.d.ts +58 -0
- package/dist/course/course-ai-usage.service.d.ts.map +1 -0
- package/dist/course/course-ai-usage.service.js +176 -0
- package/dist/course/course-ai-usage.service.js.map +1 -0
- package/dist/course/course-audio-transcription.service.d.ts +65 -1
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +381 -29
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12.service.d.ts +3 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
- package/dist/course/course-export-scorm12.service.js +141 -6
- package/dist/course/course-export-scorm12.service.js.map +1 -1
- package/dist/course/course-export.service.d.ts.map +1 -1
- package/dist/course/course-export.service.js +2 -1
- package/dist/course/course-export.service.js.map +1 -1
- package/dist/course/course-lesson.controller.d.ts +25 -3
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +71 -8
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +30 -7
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +37 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +37 -5
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +165 -20
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-transcription-translation.service.d.ts +31 -0
- package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
- package/dist/course/course-transcription-translation.service.js +227 -0
- package/dist/course/course-transcription-translation.service.js.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +398 -0
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
- package/dist/course/course-video-hls.service.d.ts +14 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -1
- package/dist/course/course-video-hls.service.js +25 -8
- package/dist/course/course-video-hls.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +2 -0
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +9 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +2 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +36 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
- package/dist/course/dto/create-course-export.dto.d.ts +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-export.dto.js +6 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -1
- package/dist/course/ffmpeg.util.d.ts +10 -0
- package/dist/course/ffmpeg.util.d.ts.map +1 -0
- package/dist/course/ffmpeg.util.js +79 -0
- package/dist/course/ffmpeg.util.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +33 -16
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +3 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +48 -29
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/subtitle.util.d.ts +46 -0
- package/dist/course/subtitle.util.d.ts.map +1 -0
- package/dist/course/subtitle.util.js +206 -0
- package/dist/course/subtitle.util.js.map +1 -0
- package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +2 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +27 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +197 -10
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +14 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
- package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
- package/dist/platforma/dto/heartbeat.dto.js +50 -0
- package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
- package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.js +50 -0
- package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
- package/dist/platforma/platforma-performance.service.d.ts +121 -0
- package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
- package/dist/platforma/platforma-performance.service.js +500 -0
- package/dist/platforma/platforma-performance.service.js.map +1 -0
- package/dist/platforma/platforma-search.service.d.ts +21 -0
- package/dist/platforma/platforma-search.service.d.ts.map +1 -0
- package/dist/platforma/platforma-search.service.js +64 -0
- package/dist/platforma/platforma-search.service.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +8 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -1
- package/dist/platforma/platforma-video.service.js +45 -2
- package/dist/platforma/platforma-video.service.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +213 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +159 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +2 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.controller.js +31 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.service.d.ts +1 -1
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.service.js.map +1 -1
- package/dist/training/dto/create-training.dto.d.ts +9 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -1
- package/dist/training/dto/create-training.dto.js +45 -1
- package/dist/training/dto/create-training.dto.js.map +1 -1
- package/dist/training/training.controller.d.ts +144 -0
- package/dist/training/training.controller.d.ts.map +1 -1
- package/dist/training/training.service.d.ts +149 -0
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +332 -167
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/data/image_type.yaml +10 -0
- package/hedhog/data/route.yaml +251 -0
- package/hedhog/data/setting_group.yaml +97 -0
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
- package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
- package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
- package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
- package/hedhog/frontend/app/courses/page.tsx.ejs +66 -13
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
- package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
- package/hedhog/frontend/app/paths/page.tsx.ejs +650 -168
- package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
- package/hedhog/frontend/messages/en.json +41 -12
- package/hedhog/frontend/messages/pt.json +44 -13
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -0
- package/hedhog/table/course_enrollment.yaml +3 -0
- package/hedhog/table/course_lesson.yaml +3 -0
- package/hedhog/table/course_lesson_answer.yaml +37 -0
- package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
- package/hedhog/table/learning_path.yaml +6 -0
- package/hedhog/table/learning_path_module.yaml +22 -0
- package/hedhog/table/learning_path_step.yaml +9 -6
- package/hedhog/table/lesson_view_event.yaml +66 -0
- package/package.json +8 -7
- package/src/certificate/certificate.controller.ts +2 -0
- package/src/certificate/certificate.service.ts +99 -0
- package/src/course/course-ai-usage.service.ts +221 -0
- package/src/course/course-audio-transcription.service.ts +471 -43
- package/src/course/course-export-scorm12.service.ts +149 -5
- package/src/course/course-export.service.ts +1 -0
- package/src/course/course-lesson.controller.ts +59 -6
- package/src/course/course-structure.controller.ts +19 -1
- package/src/course/course-structure.service.ts +184 -10
- package/src/course/course-transcription-translation.service.ts +293 -0
- package/src/course/course-video-agent-pipeline.service.ts +471 -0
- package/src/course/course-video-hls.service.ts +30 -10
- package/src/course/course.module.ts +9 -0
- package/src/course/course.service.ts +46 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +6 -0
- package/src/course/ffmpeg.util.ts +65 -0
- package/src/course/lms-bulk-upload-automation.service.ts +33 -8
- package/src/course/lms-bulk-upload.service.ts +20 -1
- package/src/course/subtitle.util.ts +220 -0
- package/src/enterprise/training/training-student.service.ts +224 -4
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
- package/src/lms.module.ts +14 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -0
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
- package/src/platforma/platforma-heartbeat.service.ts +33 -0
- package/src/platforma/platforma-performance.service.ts +606 -0
- package/src/platforma/platforma-search.service.ts +48 -0
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +130 -0
- package/src/realtime/lms-realtime.controller.ts +27 -1
- package/src/realtime/lms-realtime.service.ts +2 -1
- package/src/training/dto/create-training.dto.ts +36 -0
- package/src/training/training.service.ts +360 -163
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { buildAiConfigFromIntegration, FileService, SettingService } from '@hed-hog/core';
|
|
3
|
+
import {
|
|
4
|
+
DatabaseQueueProvider,
|
|
5
|
+
IJobHandler,
|
|
6
|
+
NonRetryableError,
|
|
7
|
+
QueueHandlerRegistry,
|
|
8
|
+
} from '@hed-hog/queue';
|
|
9
|
+
import { AgentRuntimeService } from '@hed-hog/agent';
|
|
10
|
+
import { Inject, Injectable, Logger, OnModuleInit, forwardRef } from '@nestjs/common';
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import { execFile } from 'child_process';
|
|
13
|
+
import { promises as fs } from 'fs';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
import { basename, join } from 'path';
|
|
16
|
+
import { promisify } from 'util';
|
|
17
|
+
import { CourseVideoHlsService } from './course-video-hls.service';
|
|
18
|
+
import { getFfmpegCommand } from './ffmpeg.util';
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
export const VIDEO_PROCESSING_AGENT_SLUG = 'lms-video-processing';
|
|
23
|
+
|
|
24
|
+
export const LMS_AUDIO_EXTRACT_JOB = 'lms.audio.extract';
|
|
25
|
+
export const LMS_AUDIO_SPLIT_JOB = 'lms.audio.split';
|
|
26
|
+
export const LMS_AUDIO_TRANSCRIBE_CHUNKS_JOB = 'lms.audio.transcribe.chunks';
|
|
27
|
+
export const LMS_TRANSCRIPTION_SAVE_JOB = 'lms.transcription.save';
|
|
28
|
+
|
|
29
|
+
const CHUNK_SECONDS = 60;
|
|
30
|
+
|
|
31
|
+
type JobInput = {
|
|
32
|
+
id: number;
|
|
33
|
+
type: string;
|
|
34
|
+
queue_name: string;
|
|
35
|
+
payload: Record<string, any>;
|
|
36
|
+
attempts: number;
|
|
37
|
+
max_attempts: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Decomposed, queue-backed steps of the LMS video transcription pipeline, orchestrated by the
|
|
42
|
+
* seeded `lms-video-processing` agent flow via `tool.queue_dispatch` (dispatch-and-wait).
|
|
43
|
+
*
|
|
44
|
+
* Each step is its own job so it is visible both in the queue dashboard and the agent run
|
|
45
|
+
* timeline. Intermediates are persisted (audio + chunk + transcript files) and only small
|
|
46
|
+
* file-id references travel through the agent state between steps. The existing monolithic
|
|
47
|
+
* `lms.audio.transcribe` job and legacy paths are left untouched.
|
|
48
|
+
*/
|
|
49
|
+
@Injectable()
|
|
50
|
+
export class CourseVideoAgentPipelineService implements OnModuleInit, IJobHandler {
|
|
51
|
+
private readonly logger = new Logger(CourseVideoAgentPipelineService.name);
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
@Inject(forwardRef(() => PrismaService))
|
|
55
|
+
private readonly prisma: PrismaService,
|
|
56
|
+
@Inject(forwardRef(() => FileService))
|
|
57
|
+
private readonly fileService: FileService,
|
|
58
|
+
@Inject(forwardRef(() => SettingService))
|
|
59
|
+
private readonly settingService: SettingService,
|
|
60
|
+
@Inject(forwardRef(() => QueueHandlerRegistry))
|
|
61
|
+
private readonly registry: QueueHandlerRegistry,
|
|
62
|
+
@Inject(forwardRef(() => DatabaseQueueProvider))
|
|
63
|
+
private readonly dbQueue: DatabaseQueueProvider,
|
|
64
|
+
@Inject(forwardRef(() => AgentRuntimeService))
|
|
65
|
+
private readonly agentRuntime: AgentRuntimeService,
|
|
66
|
+
@Inject(forwardRef(() => CourseVideoHlsService))
|
|
67
|
+
private readonly hlsService: CourseVideoHlsService,
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
private get db() {
|
|
71
|
+
return this.prisma as any;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onModuleInit() {
|
|
75
|
+
for (const type of [
|
|
76
|
+
LMS_AUDIO_EXTRACT_JOB,
|
|
77
|
+
LMS_AUDIO_SPLIT_JOB,
|
|
78
|
+
LMS_AUDIO_TRANSCRIBE_CHUNKS_JOB,
|
|
79
|
+
LMS_TRANSCRIPTION_SAVE_JOB,
|
|
80
|
+
]) {
|
|
81
|
+
this.registry.register(type, this);
|
|
82
|
+
}
|
|
83
|
+
this.logger.log(
|
|
84
|
+
`Registered handlers for ${LMS_AUDIO_EXTRACT_JOB}, ${LMS_AUDIO_SPLIT_JOB}, ${LMS_AUDIO_TRANSCRIBE_CHUNKS_JOB}, ${LMS_TRANSCRIPTION_SAVE_JOB}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ───────────────────────────── orchestration entry ─────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validates the lesson/original, registers the `video_original` file, and starts the seeded
|
|
92
|
+
* `lms-video-processing` agent run (async). Falls back to the legacy monolithic HLS pipeline
|
|
93
|
+
* when the agent flow is not seeded yet, so the feature degrades gracefully during rollout.
|
|
94
|
+
*/
|
|
95
|
+
async startProcessing(params: {
|
|
96
|
+
userId: number;
|
|
97
|
+
courseId: number;
|
|
98
|
+
sessionId: number;
|
|
99
|
+
lessonId: number;
|
|
100
|
+
originalFileId: number;
|
|
101
|
+
}): Promise<{ agentRunId: number | null; status: string }> {
|
|
102
|
+
const agent = await this.db.agent.findUnique({
|
|
103
|
+
where: { slug: VIDEO_PROCESSING_AGENT_SLUG },
|
|
104
|
+
select: { id: true, status: true },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!agent || agent.status === 'archived') {
|
|
108
|
+
this.logger.warn(
|
|
109
|
+
`Agent "${VIDEO_PROCESSING_AGENT_SLUG}" unavailable — falling back to legacy enqueueHls.`,
|
|
110
|
+
);
|
|
111
|
+
const legacy = await this.hlsService.enqueueHls(params);
|
|
112
|
+
return { agentRunId: null, status: legacy.status };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await this.hlsService.prepareLessonForProcessing({
|
|
116
|
+
courseId: params.courseId,
|
|
117
|
+
sessionId: params.sessionId,
|
|
118
|
+
lessonId: params.lessonId,
|
|
119
|
+
originalFileId: params.originalFileId,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const settings = await this.settingService.getSettingValues([
|
|
123
|
+
'lms-audio-transcription-enabled',
|
|
124
|
+
]);
|
|
125
|
+
const transcriptionEnabled = settings['lms-audio-transcription-enabled'] !== false;
|
|
126
|
+
|
|
127
|
+
const course = await this.db.course.findUnique({
|
|
128
|
+
where: { id: params.courseId },
|
|
129
|
+
include: { locale: true },
|
|
130
|
+
});
|
|
131
|
+
const localeCode = course?.locale?.code ?? 'pt';
|
|
132
|
+
|
|
133
|
+
const run = await this.agentRuntime.startManualRun(
|
|
134
|
+
agent.id,
|
|
135
|
+
{
|
|
136
|
+
course_id: params.courseId,
|
|
137
|
+
session_id: params.sessionId,
|
|
138
|
+
lesson_id: params.lessonId,
|
|
139
|
+
original_file_id: params.originalFileId,
|
|
140
|
+
user_id: params.userId,
|
|
141
|
+
transcription_enabled: transcriptionEnabled,
|
|
142
|
+
locale_code: localeCode,
|
|
143
|
+
},
|
|
144
|
+
params.userId,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
this.logger.log(
|
|
148
|
+
`Started agent run ${run?.id} for lesson ${params.lessonId} (transcription=${transcriptionEnabled}).`,
|
|
149
|
+
);
|
|
150
|
+
return { agentRunId: run?.id ?? null, status: 'started' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ───────────────────────────── job dispatch ─────────────────────────────
|
|
154
|
+
|
|
155
|
+
async handle(job: JobInput): Promise<any> {
|
|
156
|
+
switch (job.type) {
|
|
157
|
+
case LMS_AUDIO_EXTRACT_JOB:
|
|
158
|
+
return this.handleExtract(job);
|
|
159
|
+
case LMS_AUDIO_SPLIT_JOB:
|
|
160
|
+
return this.handleSplit(job);
|
|
161
|
+
case LMS_AUDIO_TRANSCRIBE_CHUNKS_JOB:
|
|
162
|
+
return this.handleTranscribeChunks(job);
|
|
163
|
+
case LMS_TRANSCRIPTION_SAVE_JOB:
|
|
164
|
+
return this.handleSave(job);
|
|
165
|
+
default:
|
|
166
|
+
throw new NonRetryableError(`Unsupported job type "${job.type}"`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Step: extract a 16 kHz mono mp3 from the original video and persist it as lesson_audio. */
|
|
171
|
+
private async handleExtract(job: JobInput): Promise<{ audioFileId: number }> {
|
|
172
|
+
const courseId = Number(job.payload?.courseId);
|
|
173
|
+
const lessonId = Number(job.payload?.lessonId);
|
|
174
|
+
const originalFileId = Number(job.payload?.originalFileId);
|
|
175
|
+
if (!lessonId || !originalFileId) {
|
|
176
|
+
throw new NonRetryableError('lms.audio.extract: lessonId and originalFileId are required.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const workDir = await fs.mkdtemp(join(tmpdir(), `lms-extract-${job.id}-`));
|
|
180
|
+
const inputPath = join(workDir, `original-${originalFileId}`);
|
|
181
|
+
const mp3Path = join(workDir, `lesson_${lessonId}_audio.mp3`);
|
|
182
|
+
try {
|
|
183
|
+
await this.fileService.downloadToPath(originalFileId, inputPath);
|
|
184
|
+
await execFileAsync(
|
|
185
|
+
getFfmpegCommand(),
|
|
186
|
+
['-y', '-i', inputPath, '-vn', '-ar', '16000', '-ac', '1', '-c:a', 'libmp3lame', '-b:a', '32k', mp3Path],
|
|
187
|
+
{ maxBuffer: 1024 * 1024 * 20, windowsHide: true },
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const course = await this.db.course.findUnique({
|
|
191
|
+
where: { id: courseId },
|
|
192
|
+
select: { locale_id: true },
|
|
193
|
+
});
|
|
194
|
+
const uploaded = await this.fileService.uploadFromPath('lms/lessons/audio', mp3Path, {
|
|
195
|
+
originalname: basename(mp3Path),
|
|
196
|
+
mimetype: 'audio/mp3',
|
|
197
|
+
});
|
|
198
|
+
const defaultLocale = await this.db.locale.findFirst({
|
|
199
|
+
where: { OR: [{ code: 'pt-BR' }, { code: 'pt' }] },
|
|
200
|
+
select: { id: true },
|
|
201
|
+
orderBy: { id: 'asc' },
|
|
202
|
+
});
|
|
203
|
+
const resolvedLocaleId = course?.locale_id ?? defaultLocale?.id ?? null;
|
|
204
|
+
|
|
205
|
+
const existing = await this.db.course_lesson_file.findFirst({
|
|
206
|
+
where: { course_lesson_id: lessonId, type: 'lesson_audio' },
|
|
207
|
+
select: { id: true },
|
|
208
|
+
});
|
|
209
|
+
if (existing) {
|
|
210
|
+
await this.db.course_lesson_file.update({
|
|
211
|
+
where: { id: existing.id },
|
|
212
|
+
data: { file_id: uploaded.id, locale_id: resolvedLocaleId, title: 'Audio Original.mp3', type: 'lesson_audio', is_public: false },
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
await this.db.course_lesson_file.create({
|
|
216
|
+
data: { course_lesson_id: lessonId, file_id: uploaded.id, title: 'Audio Original.mp3', type: 'lesson_audio', is_public: false, locale_id: resolvedLocaleId },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.logger.log(`[extract job=${job.id}] lesson ${lessonId} audio fileId=${uploaded.id}`);
|
|
221
|
+
return { audioFileId: uploaded.id };
|
|
222
|
+
} finally {
|
|
223
|
+
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Step: split the lesson audio into 60s chunks, uploaded to a staging prefix. */
|
|
228
|
+
private async handleSplit(
|
|
229
|
+
job: JobInput,
|
|
230
|
+
): Promise<{ chunkFileIds: number[]; chunkCount: number }> {
|
|
231
|
+
const lessonId = Number(job.payload?.lessonId);
|
|
232
|
+
const audioFileId = Number(job.payload?.audioFileId);
|
|
233
|
+
if (!lessonId || !audioFileId) {
|
|
234
|
+
throw new NonRetryableError('lms.audio.split: lessonId and audioFileId are required.');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const workDir = await fs.mkdtemp(join(tmpdir(), `lms-split-${job.id}-`));
|
|
238
|
+
const sourceAudioPath = join(workDir, `audio_${lessonId}.source`);
|
|
239
|
+
const normalizedAudioPath = join(workDir, `audio_${lessonId}.normalized.mp3`);
|
|
240
|
+
try {
|
|
241
|
+
await this.fileService.downloadToPath(audioFileId, sourceAudioPath);
|
|
242
|
+
await execFileAsync(
|
|
243
|
+
getFfmpegCommand(),
|
|
244
|
+
['-y', '-i', sourceAudioPath, '-ar', '16000', '-ac', '1', '-c:a', 'libmp3lame', '-b:a', '32k', normalizedAudioPath],
|
|
245
|
+
{ maxBuffer: 1024 * 1024 * 20, windowsHide: true },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const chunkPattern = join(workDir, 'chunk_%04d.mp3');
|
|
249
|
+
await execFileAsync(
|
|
250
|
+
getFfmpegCommand(),
|
|
251
|
+
['-y', '-i', normalizedAudioPath, '-f', 'segment', '-segment_time', String(CHUNK_SECONDS), '-ar', '16000', '-ac', '1', '-c:a', 'libmp3lame', '-b:a', '32k', chunkPattern],
|
|
252
|
+
{ maxBuffer: 1024 * 1024 * 20, windowsHide: true },
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const chunkNames = (await fs.readdir(workDir))
|
|
256
|
+
.filter((n) => n.startsWith('chunk_') && n.endsWith('.mp3'))
|
|
257
|
+
.sort((a, b) => a.localeCompare(b));
|
|
258
|
+
|
|
259
|
+
const chunkFileIds: number[] = [];
|
|
260
|
+
for (const name of chunkNames) {
|
|
261
|
+
const uploaded = await this.fileService.uploadFromPath(
|
|
262
|
+
`lms/lessons/transcribe-chunks/${lessonId}`,
|
|
263
|
+
join(workDir, name),
|
|
264
|
+
{ originalname: name, mimetype: 'audio/mpeg' },
|
|
265
|
+
);
|
|
266
|
+
chunkFileIds.push(uploaded.id);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.logger.log(`[split job=${job.id}] lesson ${lessonId} → ${chunkFileIds.length} chunk(s)`);
|
|
270
|
+
return { chunkFileIds, chunkCount: chunkFileIds.length };
|
|
271
|
+
} finally {
|
|
272
|
+
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Step: transcribe each chunk via OpenAI Whisper and persist the merged transcript JSON. */
|
|
277
|
+
private async handleTranscribeChunks(
|
|
278
|
+
job: JobInput,
|
|
279
|
+
): Promise<{ transcriptFileId: number; segmentCount: number }> {
|
|
280
|
+
const courseId = Number(job.payload?.courseId);
|
|
281
|
+
const lessonId = Number(job.payload?.lessonId);
|
|
282
|
+
const chunkFileIds = this.parseIdArray(job.payload?.chunkFileIds);
|
|
283
|
+
if (!lessonId || chunkFileIds.length === 0) {
|
|
284
|
+
throw new NonRetryableError('lms.audio.transcribe.chunks: lessonId and chunkFileIds are required.');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const apiKey = await this.resolveOpenAiKey();
|
|
288
|
+
const course = await this.db.course.findUnique({
|
|
289
|
+
where: { id: courseId },
|
|
290
|
+
include: { locale: true },
|
|
291
|
+
});
|
|
292
|
+
const language = course?.locale?.code ?? 'pt';
|
|
293
|
+
|
|
294
|
+
const workDir = await fs.mkdtemp(join(tmpdir(), `lms-transcribe-${job.id}-`));
|
|
295
|
+
try {
|
|
296
|
+
const segments: Array<{
|
|
297
|
+
course_lesson_id: number;
|
|
298
|
+
start_seconds: number;
|
|
299
|
+
end_seconds: number;
|
|
300
|
+
text: string;
|
|
301
|
+
}> = [];
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < chunkFileIds.length; i += 1) {
|
|
304
|
+
const chunkPath = join(workDir, `chunk_${i}.mp3`);
|
|
305
|
+
await this.fileService.downloadToPath(chunkFileIds[i], chunkPath);
|
|
306
|
+
const offsetSeconds = i * CHUNK_SECONDS;
|
|
307
|
+
const buffer = await fs.readFile(chunkPath);
|
|
308
|
+
|
|
309
|
+
const formData = new FormData();
|
|
310
|
+
formData.append('file', new Blob([buffer], { type: 'audio/mpeg' }), `chunk_${i}.mp3`);
|
|
311
|
+
formData.append('model', 'whisper-1');
|
|
312
|
+
formData.append('response_format', 'verbose_json');
|
|
313
|
+
formData.append('language', language);
|
|
314
|
+
|
|
315
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${apiKey}` };
|
|
316
|
+
const maybeHeaders = (formData as any).getHeaders?.();
|
|
317
|
+
if (maybeHeaders && typeof maybeHeaders === 'object') Object.assign(headers, maybeHeaders);
|
|
318
|
+
|
|
319
|
+
const response = await axios.post(
|
|
320
|
+
'https://api.openai.com/v1/audio/transcriptions',
|
|
321
|
+
formData,
|
|
322
|
+
{ headers },
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
for (const segment of response.data?.segments ?? []) {
|
|
326
|
+
const text = String(segment?.text ?? '').trim();
|
|
327
|
+
if (!text) continue;
|
|
328
|
+
segments.push({
|
|
329
|
+
course_lesson_id: lessonId,
|
|
330
|
+
start_seconds: offsetSeconds + Number(segment?.start ?? 0),
|
|
331
|
+
end_seconds: offsetSeconds + Number(segment?.end ?? 0),
|
|
332
|
+
text,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const transcriptPath = join(workDir, `transcript_${lessonId}.json`);
|
|
338
|
+
await fs.writeFile(transcriptPath, JSON.stringify({ lessonId, segments }), 'utf8');
|
|
339
|
+
const uploaded = await this.fileService.uploadFromPath(
|
|
340
|
+
`lms/lessons/transcripts/${lessonId}`,
|
|
341
|
+
transcriptPath,
|
|
342
|
+
{ originalname: `transcript_${lessonId}.json`, mimetype: 'application/json', skipMimeValidation: true },
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
this.logger.log(`[transcribe job=${job.id}] lesson ${lessonId} → ${segments.length} segment(s)`);
|
|
346
|
+
return { transcriptFileId: uploaded.id, segmentCount: segments.length };
|
|
347
|
+
} finally {
|
|
348
|
+
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Step: join + persist the transcript segments and trigger XP calculation. */
|
|
353
|
+
private async handleSave(job: JobInput): Promise<{ saved: number }> {
|
|
354
|
+
const lessonId = Number(job.payload?.lessonId);
|
|
355
|
+
const transcriptFileId = Number(job.payload?.transcriptFileId);
|
|
356
|
+
const userId = Number(job.payload?.userId) || 0;
|
|
357
|
+
if (!lessonId || !transcriptFileId) {
|
|
358
|
+
throw new NonRetryableError('lms.transcription.save: lessonId and transcriptFileId are required.');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const workDir = await fs.mkdtemp(join(tmpdir(), `lms-tsave-${job.id}-`));
|
|
362
|
+
try {
|
|
363
|
+
const transcriptPath = join(workDir, 'transcript.json');
|
|
364
|
+
await this.fileService.downloadToPath(transcriptFileId, transcriptPath);
|
|
365
|
+
const parsed = JSON.parse(await fs.readFile(transcriptPath, 'utf8'));
|
|
366
|
+
const segments: Array<Record<string, any>> = Array.isArray(parsed?.segments)
|
|
367
|
+
? parsed.segments
|
|
368
|
+
: [];
|
|
369
|
+
|
|
370
|
+
await this.prisma.$transaction([
|
|
371
|
+
this.db.course_lesson_transcription_segment.deleteMany({
|
|
372
|
+
where: { course_lesson_id: lessonId },
|
|
373
|
+
}),
|
|
374
|
+
this.db.course_lesson_transcription_segment.createMany({
|
|
375
|
+
data: segments.map((s) => ({
|
|
376
|
+
course_lesson_id: lessonId,
|
|
377
|
+
start_seconds: Number(s?.start_seconds ?? 0),
|
|
378
|
+
end_seconds: Number(s?.end_seconds ?? 0),
|
|
379
|
+
text: String(s?.text ?? ''),
|
|
380
|
+
})),
|
|
381
|
+
}),
|
|
382
|
+
]);
|
|
383
|
+
|
|
384
|
+
this.logger.log(`[save job=${job.id}] lesson ${lessonId} saved ${segments.length} segment(s)`);
|
|
385
|
+
await this.triggerXpCalculation(lessonId, userId);
|
|
386
|
+
return { saved: segments.length };
|
|
387
|
+
} finally {
|
|
388
|
+
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ───────────────────────────── helpers ─────────────────────────────
|
|
393
|
+
|
|
394
|
+
private parseIdArray(raw: unknown): number[] {
|
|
395
|
+
let arr: unknown = raw;
|
|
396
|
+
if (typeof raw === 'string') {
|
|
397
|
+
try {
|
|
398
|
+
arr = JSON.parse(raw);
|
|
399
|
+
} catch {
|
|
400
|
+
arr = [];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!Array.isArray(arr)) return [];
|
|
404
|
+
return arr.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async resolveOpenAiKey(): Promise<string> {
|
|
408
|
+
const settings = await this.settingService.getSettingValues(['ai-openai-profile-id']);
|
|
409
|
+
const profileId = Number(settings['ai-openai-profile-id']);
|
|
410
|
+
let apiKey = '';
|
|
411
|
+
if (profileId) {
|
|
412
|
+
const profile = await this.db.integration_profile.findUnique({
|
|
413
|
+
where: { id: profileId },
|
|
414
|
+
include: { integration_provider: { select: { slug: true } } },
|
|
415
|
+
});
|
|
416
|
+
if (profile) {
|
|
417
|
+
apiKey = buildAiConfigFromIntegration(profile.integration_provider.slug, profile.config).apiKey;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!apiKey) {
|
|
421
|
+
throw new NonRetryableError(
|
|
422
|
+
'Transcrição de áudio requer um perfil OpenAI configurado (Settings → LMS → ai-openai-profile-id).',
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return apiKey;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private async triggerXpCalculation(lessonId: number, userId: number): Promise<void> {
|
|
429
|
+
try {
|
|
430
|
+
const existingMap = await this.db.lesson_xp_map.findUnique({
|
|
431
|
+
where: { course_lesson_id: lessonId },
|
|
432
|
+
select: { id: true },
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
let mapId: number;
|
|
436
|
+
if (existingMap) {
|
|
437
|
+
mapId = existingMap.id;
|
|
438
|
+
await this.prisma.$executeRawUnsafe(
|
|
439
|
+
`UPDATE lesson_xp_map
|
|
440
|
+
SET status = 'processing'::lesson_xp_map_status_d4e5f6a7b8_enum,
|
|
441
|
+
processing_error = NULL,
|
|
442
|
+
updated_at = NOW()
|
|
443
|
+
WHERE id = $1`,
|
|
444
|
+
mapId,
|
|
445
|
+
);
|
|
446
|
+
} else {
|
|
447
|
+
const rows = await this.prisma.$queryRawUnsafe<{ id: number }[]>(
|
|
448
|
+
`INSERT INTO lesson_xp_map (course_lesson_id, version, total_xp, status, created_at, updated_at)
|
|
449
|
+
VALUES ($1, 1, 0, 'processing'::lesson_xp_map_status_d4e5f6a7b8_enum, NOW(), NOW())
|
|
450
|
+
RETURNING id`,
|
|
451
|
+
lessonId,
|
|
452
|
+
);
|
|
453
|
+
mapId = rows[0].id;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await this.dbQueue.enqueue({
|
|
457
|
+
type: 'lms.lesson.xp.calculate',
|
|
458
|
+
queueName: 'lms.lesson.xp.calculate',
|
|
459
|
+
payload: { lessonId, mapId, userId },
|
|
460
|
+
maxAttempts: 3,
|
|
461
|
+
sourceModule: 'lms',
|
|
462
|
+
sourceEntity: 'course_lesson',
|
|
463
|
+
sourceEntityId: String(lessonId),
|
|
464
|
+
});
|
|
465
|
+
} catch (err) {
|
|
466
|
+
this.logger.warn(
|
|
467
|
+
`Failed to enqueue XP calculation for lesson ${lessonId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -70,13 +70,17 @@ export class CourseVideoHlsService implements OnModuleInit, IJobHandler {
|
|
|
70
70
|
this.logger.log(`Registered handler for "${LMS_VIDEO_HLS_JOB}"`);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Validates a lesson is an eligible file-storage video and registers the uploaded original
|
|
75
|
+
* as its `video_original` file. Shared by the legacy `enqueueHls` path and the new
|
|
76
|
+
* agent-orchestrated pipeline (CourseVideoAgentPipelineService.startProcessing).
|
|
77
|
+
*/
|
|
78
|
+
async prepareLessonForProcessing(params: {
|
|
75
79
|
courseId: number;
|
|
76
80
|
sessionId: number;
|
|
77
81
|
lessonId: number;
|
|
78
82
|
originalFileId: number;
|
|
79
|
-
}) {
|
|
83
|
+
}): Promise<{ content: Record<string, unknown> | null; original: any }> {
|
|
80
84
|
const lesson = await (this.prisma as any).course_lesson.findFirst({
|
|
81
85
|
where: {
|
|
82
86
|
id: params.lessonId,
|
|
@@ -118,6 +122,18 @@ export class CourseVideoHlsService implements OnModuleInit, IJobHandler {
|
|
|
118
122
|
overwrite: true,
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
return { content, original };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async enqueueHls(params: {
|
|
129
|
+
userId: number;
|
|
130
|
+
courseId: number;
|
|
131
|
+
sessionId: number;
|
|
132
|
+
lessonId: number;
|
|
133
|
+
originalFileId: number;
|
|
134
|
+
}) {
|
|
135
|
+
const { content } = await this.prepareLessonForProcessing(params);
|
|
136
|
+
|
|
121
137
|
const asyncNotification = await this.notificationService.create({
|
|
122
138
|
user_id: params.userId,
|
|
123
139
|
title: 'Geração de HLS em andamento',
|
|
@@ -189,12 +205,16 @@ export class CourseVideoHlsService implements OnModuleInit, IJobHandler {
|
|
|
189
205
|
source_entity?: string | null;
|
|
190
206
|
source_entity_id?: string | null;
|
|
191
207
|
}): Promise<any> {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
// Ids are coerced because agent-dispatched payloads template values as strings.
|
|
209
|
+
const courseId = Number(job.payload?.courseId);
|
|
210
|
+
const sessionId = Number(job.payload?.sessionId);
|
|
211
|
+
const lessonId = Number(job.payload?.lessonId);
|
|
212
|
+
const originalFileId = Number(job.payload?.originalFileId);
|
|
213
|
+
// Set by the agent pipeline: HLS-only, since audio extraction + transcription are
|
|
214
|
+
// dispatched as their own visible steps. Legacy callers omit it (full behaviour).
|
|
215
|
+
const skipAudioTranscription =
|
|
216
|
+
job.payload?.skipAudioTranscription === true ||
|
|
217
|
+
job.payload?.skipAudioTranscription === 'true';
|
|
198
218
|
|
|
199
219
|
const notificationContext: NotificationContext | undefined =
|
|
200
220
|
Number.isInteger(Number(job.payload?.notificationId)) &&
|
|
@@ -359,7 +379,7 @@ export class CourseVideoHlsService implements OnModuleInit, IJobHandler {
|
|
|
359
379
|
this.logger.debug(`[HLS job=${job.id}] frame extraction disabled — skipping`);
|
|
360
380
|
}
|
|
361
381
|
|
|
362
|
-
if (transcriptionEnabled) {
|
|
382
|
+
if (transcriptionEnabled && !skipAudioTranscription) {
|
|
363
383
|
this.logger.debug(`[HLS job=${job.id}] starting audio extraction for transcription`);
|
|
364
384
|
await emitProgress('Extraindo áudio do vídeo...', { phase: 'extract_audio', lessonId });
|
|
365
385
|
const audioFileId = await this.extractAndUploadLessonAudio({
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { AgentModule } from '@hed-hog/agent';
|
|
1
2
|
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
2
3
|
import { CoreModule } from '@hed-hog/core';
|
|
3
4
|
import { QueueModule } from '@hed-hog/queue';
|
|
4
5
|
import { forwardRef, Module } from '@nestjs/common';
|
|
5
6
|
import { InstructorModule } from '../instructor/instructor.module';
|
|
7
|
+
import { CourseAiUsageService } from './course-ai-usage.service';
|
|
6
8
|
import { CourseLessonController } from './course-lesson.controller';
|
|
7
9
|
import { CourseOperationsIntegrationService } from './course-operations-integration.service';
|
|
8
10
|
import { CourseOperationsController } from './course-operations.controller';
|
|
9
11
|
import { CourseStructureController } from './course-structure.controller';
|
|
10
12
|
import { CourseStructureService } from './course-structure.service';
|
|
13
|
+
import { CourseVideoAgentPipelineService } from './course-video-agent-pipeline.service';
|
|
11
14
|
import { CourseVideoConversionService } from './course-video-conversion.service';
|
|
12
15
|
import { CourseVideoHlsService } from './course-video-hls.service';
|
|
13
16
|
import { CourseExportScorm12Service } from './course-export-scorm12.service';
|
|
@@ -22,6 +25,7 @@ import { LmsBulkUploadController } from './lms-bulk-upload.controller';
|
|
|
22
25
|
import { LmsBulkUploadService } from './lms-bulk-upload.service';
|
|
23
26
|
import { LmsOperationsTaskSubscriber } from './lms-operations-task.subscriber';
|
|
24
27
|
import { LmsSettingController } from './lms-setting.controller';
|
|
28
|
+
import { PlatformaVideoService } from '../platforma/platforma-video.service';
|
|
25
29
|
|
|
26
30
|
@Module({
|
|
27
31
|
imports: [
|
|
@@ -29,6 +33,7 @@ import { LmsSettingController } from './lms-setting.controller';
|
|
|
29
33
|
forwardRef(() => InstructorModule),
|
|
30
34
|
forwardRef(() => CoreModule),
|
|
31
35
|
forwardRef(() => QueueModule),
|
|
36
|
+
forwardRef(() => AgentModule),
|
|
32
37
|
],
|
|
33
38
|
controllers: [
|
|
34
39
|
CourseController,
|
|
@@ -39,12 +44,14 @@ import { LmsSettingController } from './lms-setting.controller';
|
|
|
39
44
|
LmsBulkUploadController,
|
|
40
45
|
],
|
|
41
46
|
providers: [
|
|
47
|
+
CourseAiUsageService,
|
|
42
48
|
CourseExportService,
|
|
43
49
|
CourseExportScorm12Service,
|
|
44
50
|
CourseExportScorm12WorkerService,
|
|
45
51
|
CourseOperationsIntegrationService,
|
|
46
52
|
CourseService,
|
|
47
53
|
CourseStructureService,
|
|
54
|
+
CourseVideoAgentPipelineService,
|
|
48
55
|
CourseVideoConversionService,
|
|
49
56
|
CourseVideoHlsService,
|
|
50
57
|
LmsBulkUploadAutomationService,
|
|
@@ -52,12 +59,14 @@ import { LmsSettingController } from './lms-setting.controller';
|
|
|
52
59
|
LmsBulkUploadService,
|
|
53
60
|
LmsCoursesMcpTools,
|
|
54
61
|
LmsOperationsTaskSubscriber,
|
|
62
|
+
PlatformaVideoService,
|
|
55
63
|
],
|
|
56
64
|
exports: [
|
|
57
65
|
forwardRef(() => CourseService),
|
|
58
66
|
forwardRef(() => CourseStructureService),
|
|
59
67
|
forwardRef(() => CourseVideoConversionService),
|
|
60
68
|
forwardRef(() => CourseVideoHlsService),
|
|
69
|
+
CourseVideoAgentPipelineService,
|
|
61
70
|
],
|
|
62
71
|
})
|
|
63
72
|
export class CourseModule {}
|