@hed-hog/lms 0.0.353 → 0.0.355

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.
Files changed (135) hide show
  1. package/dist/course/course-audio-transcription.service.d.ts +29 -0
  2. package/dist/course/course-audio-transcription.service.d.ts.map +1 -0
  3. package/dist/course/course-audio-transcription.service.js +291 -0
  4. package/dist/course/course-audio-transcription.service.js.map +1 -0
  5. package/dist/course/course-lesson.controller.d.ts +10 -0
  6. package/dist/course/course-lesson.controller.d.ts.map +1 -0
  7. package/dist/course/course-lesson.controller.js +62 -0
  8. package/dist/course/course-lesson.controller.js.map +1 -0
  9. package/dist/course/course-structure.controller.d.ts +41 -15
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.controller.js +50 -6
  12. package/dist/course/course-structure.controller.js.map +1 -1
  13. package/dist/course/course-structure.service.d.ts +50 -15
  14. package/dist/course/course-structure.service.d.ts.map +1 -1
  15. package/dist/course/course-structure.service.js +238 -73
  16. package/dist/course/course-structure.service.js.map +1 -1
  17. package/dist/course/course-video-conversion.service.d.ts +20 -2
  18. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  19. package/dist/course/course-video-conversion.service.js +730 -10
  20. package/dist/course/course-video-conversion.service.js.map +1 -1
  21. package/dist/course/course.controller.d.ts +24 -8
  22. package/dist/course/course.controller.d.ts.map +1 -1
  23. package/dist/course/course.module.d.ts.map +1 -1
  24. package/dist/course/course.module.js +5 -3
  25. package/dist/course/course.module.js.map +1 -1
  26. package/dist/course/course.service.d.ts +24 -8
  27. package/dist/course/course.service.d.ts.map +1 -1
  28. package/dist/course/course.service.js +112 -176
  29. package/dist/course/course.service.js.map +1 -1
  30. package/dist/course/dto/create-course-lesson-frame.dto.d.ts +5 -0
  31. package/dist/course/dto/create-course-lesson-frame.dto.d.ts.map +1 -0
  32. package/dist/course/dto/create-course-lesson-frame.dto.js +30 -0
  33. package/dist/course/dto/create-course-lesson-frame.dto.js.map +1 -0
  34. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +4 -2
  35. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  36. package/dist/course/dto/create-course-structure-lesson.dto.js +10 -3
  37. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  38. package/dist/course/dto/create-course.dto.d.ts +1 -1
  39. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  40. package/dist/course/dto/create-course.dto.js +6 -6
  41. package/dist/course/dto/create-course.dto.js.map +1 -1
  42. package/dist/course/dto/update-course-lesson-frame.dto.d.ts +5 -0
  43. package/dist/course/dto/update-course-lesson-frame.dto.d.ts.map +1 -0
  44. package/dist/course/dto/update-course-lesson-frame.dto.js +32 -0
  45. package/dist/course/dto/update-course-lesson-frame.dto.js.map +1 -0
  46. package/dist/course/dto/update-course-resources.dto.d.ts +4 -2
  47. package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -1
  48. package/dist/course/dto/update-course-resources.dto.js +10 -3
  49. package/dist/course/dto/update-course-resources.dto.js.map +1 -1
  50. package/dist/course/dto/update-transcription-segments.dto.d.ts +10 -0
  51. package/dist/course/dto/update-transcription-segments.dto.d.ts.map +1 -0
  52. package/dist/course/dto/update-transcription-segments.dto.js +38 -0
  53. package/dist/course/dto/update-transcription-segments.dto.js.map +1 -0
  54. package/dist/course/lms-setting.controller.d.ts +13 -0
  55. package/dist/course/lms-setting.controller.d.ts.map +1 -0
  56. package/dist/course/lms-setting.controller.js +53 -0
  57. package/dist/course/lms-setting.controller.js.map +1 -0
  58. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  59. package/dist/enterprise/training/training-admin.service.js +74 -33
  60. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/lms.module.d.ts.map +1 -1
  66. package/dist/lms.module.js +6 -0
  67. package/dist/lms.module.js.map +1 -1
  68. package/hedhog/data/route.yaml +63 -0
  69. package/hedhog/data/setting_group.yaml +76 -0
  70. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +3 -0
  71. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +10 -1
  72. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +7 -2
  73. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +5 -2
  74. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +7 -1
  75. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +3 -0
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +95 -50
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +11 -36
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +19 -5
  79. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  80. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +30 -10
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs +95 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +29 -11
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +42 -31
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +10 -1
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -9
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +25 -22
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +12 -9
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +315 -229
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +3220 -534
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +20 -15
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/icon-action-tooltip.tsx.ejs +35 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +76 -67
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +13 -10
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +18 -16
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +6 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +1 -1
  97. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +23 -11
  98. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +48 -9
  99. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +11 -2
  100. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +121 -15
  101. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +69 -14
  102. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +11 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +31 -0
  104. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +32 -6
  105. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +57 -3
  106. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +46 -6
  107. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +14 -0
  108. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-audio-files.ts.ejs +23 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +34 -0
  110. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +76 -0
  111. package/hedhog/frontend/messages/en.json +39 -3
  112. package/hedhog/frontend/messages/pt.json +39 -3
  113. package/hedhog/table/course.yaml +8 -0
  114. package/hedhog/table/course_lesson_file.yaml +12 -4
  115. package/hedhog/table/course_lesson_transcription_segment.yaml +22 -0
  116. package/hedhog/table/course_lesson_video_frame.yaml +25 -0
  117. package/package.json +8 -8
  118. package/src/course/course-audio-transcription.service.ts +393 -0
  119. package/src/course/course-lesson.controller.ts +28 -0
  120. package/src/course/course-structure.controller.ts +49 -3
  121. package/src/course/course-structure.service.ts +294 -32
  122. package/src/course/course-video-conversion.service.ts +972 -6
  123. package/src/course/course.module.ts +5 -3
  124. package/src/course/course.service.ts +87 -139
  125. package/src/course/dto/create-course-lesson-frame.dto.ts +14 -0
  126. package/src/course/dto/create-course-structure-lesson.dto.ts +19 -3
  127. package/src/course/dto/create-course.dto.ts +5 -5
  128. package/src/course/dto/update-course-lesson-frame.dto.ts +16 -0
  129. package/src/course/dto/update-course-resources.dto.ts +18 -3
  130. package/src/course/dto/update-transcription-segments.dto.ts +20 -0
  131. package/src/course/lms-setting.controller.ts +30 -0
  132. package/src/enterprise/training/training-admin.service.ts +77 -24
  133. package/src/index.ts +2 -0
  134. package/src/lms.module.ts +6 -0
  135. package/hedhog/table/course_instructor.yaml +0 -27
@@ -0,0 +1,393 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { FileService, NotificationService, SettingService } from '@hed-hog/core';
3
+ import { IJobHandler, QueueHandlerRegistry } from '@hed-hog/queue';
4
+ import {
5
+ Inject,
6
+ Injectable,
7
+ Logger,
8
+ OnModuleInit,
9
+ forwardRef,
10
+ } 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 { join } from 'path';
16
+ import { promisify } from 'util';
17
+
18
+ const execFileAsync = promisify(execFile);
19
+
20
+ type NotificationContext = {
21
+ userId: number;
22
+ notificationId: number;
23
+ };
24
+
25
+ @Injectable()
26
+ export class CourseAudioTranscriptionService implements OnModuleInit, IJobHandler {
27
+ private readonly logger = new Logger(CourseAudioTranscriptionService.name);
28
+
29
+ private async createProgressEvent(
30
+ queueJobId: number,
31
+ message: string,
32
+ metadata?: Record<string, unknown>,
33
+ ): Promise<void> {
34
+ try {
35
+ await (this.prismaService as any).queue_job_event.create({
36
+ data: {
37
+ queue_job_id: queueJobId,
38
+ event_type: 'started',
39
+ message,
40
+ ...(metadata ? { metadata } : {}),
41
+ },
42
+ });
43
+ } catch (error) {
44
+ this.logger.warn(
45
+ `Queue job ${queueJobId}: failed to persist transcription progress event "${message}": ${error instanceof Error ? error.message : 'unknown error'}`,
46
+ );
47
+ }
48
+ }
49
+
50
+ private resolveNotificationProgress(metadata?: Record<string, unknown>): number {
51
+ const phase = String(metadata?.phase ?? '');
52
+
53
+ if (phase === 'transcription_download_audio') return 92;
54
+ if (phase === 'transcription_split_audio') return 94;
55
+ if (phase === 'transcription_split_done') return 96;
56
+ if (phase === 'transcription_ai_chunk') {
57
+ const chunkIndex = Number(metadata?.chunkIndex ?? 0);
58
+ const chunkCount = Number(metadata?.chunkCount ?? 0);
59
+ if (chunkIndex > 0 && chunkCount > 0) {
60
+ const ratio = Math.min(1, Math.max(0, chunkIndex / chunkCount));
61
+ return 96 + Math.round(ratio * 3);
62
+ }
63
+ return 98;
64
+ }
65
+ if (phase === 'transcription_save') return 99;
66
+ if (phase === 'transcription_done') return 100;
67
+
68
+ return 95;
69
+ }
70
+
71
+ private async updateAsyncNotification(
72
+ context: NotificationContext,
73
+ progress: number,
74
+ body: string,
75
+ success?: boolean,
76
+ ): Promise<void> {
77
+ try {
78
+ await this.notificationService.updateProgress(
79
+ context.userId,
80
+ context.notificationId,
81
+ {
82
+ progress,
83
+ body,
84
+ ...(success != null ? { success } : {}),
85
+ },
86
+ );
87
+ } catch (error) {
88
+ this.logger.warn(
89
+ `Failed to update async notification ${context.notificationId} for user ${context.userId}: ${error instanceof Error ? error.message : 'unknown error'}`,
90
+ );
91
+ }
92
+ }
93
+
94
+ constructor(
95
+ @Inject(forwardRef(() => PrismaService))
96
+ private readonly prismaService: PrismaService,
97
+ @Inject(forwardRef(() => SettingService))
98
+ private readonly settingService: SettingService,
99
+ @Inject(forwardRef(() => FileService))
100
+ private readonly fileService: FileService,
101
+ @Inject(forwardRef(() => NotificationService))
102
+ private readonly notificationService: NotificationService,
103
+ @Inject(forwardRef(() => QueueHandlerRegistry))
104
+ private readonly registry: QueueHandlerRegistry,
105
+ ) {}
106
+
107
+ onModuleInit() {
108
+ this.registry.register('lms.audio.transcribe', this);
109
+ this.logger.log('Registered handler for "lms.audio.transcribe"');
110
+ }
111
+
112
+ async handle(job: {
113
+ id: number;
114
+ type: string;
115
+ queue_name: string;
116
+ payload: Record<string, any>;
117
+ attempts: number;
118
+ max_attempts: number;
119
+ source_module?: string | null;
120
+ source_entity?: string | null;
121
+ source_entity_id?: string | null;
122
+ }): Promise<void> {
123
+ const { courseId, lessonId, audioFileId, parentJobId, notificationId, notificationUserId } = job.payload as {
124
+ courseId: number;
125
+ lessonId: number;
126
+ audioFileId: number;
127
+ parentJobId?: number;
128
+ notificationId?: number;
129
+ notificationUserId?: number;
130
+ };
131
+
132
+ const notificationContext: NotificationContext | undefined =
133
+ Number.isInteger(Number(notificationId)) &&
134
+ Number.isInteger(Number(notificationUserId))
135
+ ? {
136
+ notificationId: Number(notificationId),
137
+ userId: Number(notificationUserId),
138
+ }
139
+ : undefined;
140
+
141
+ const emitProgress = async (message: string, metadata?: Record<string, unknown>) => {
142
+ await this.createProgressEvent(job.id, message, metadata);
143
+ if (parentJobId) {
144
+ await this.createProgressEvent(parentJobId, message, metadata);
145
+ }
146
+ if (notificationContext) {
147
+ await this.updateAsyncNotification(
148
+ notificationContext,
149
+ this.resolveNotificationProgress(metadata),
150
+ message,
151
+ );
152
+ }
153
+ };
154
+
155
+ const settings = await this.settingService.getSettingValues([
156
+ 'ai-openai-api-key',
157
+ 'ai-openai-api-key-enabled',
158
+ 'lms-audio-transcription-enabled',
159
+ ]);
160
+ const apiKey = settings['ai-openai-api-key'] as string;
161
+ const enabled = settings['ai-openai-api-key-enabled'];
162
+ const transcriptionEnabled = settings['lms-audio-transcription-enabled'] !== false;
163
+
164
+ if (!transcriptionEnabled) {
165
+ this.logger.warn(
166
+ `Transcription skipped for lesson ${lessonId}: disabled by lms-audio-transcription-enabled setting`,
167
+ );
168
+ if (notificationContext) {
169
+ await this.updateAsyncNotification(
170
+ notificationContext,
171
+ 100,
172
+ 'Transcrição desabilitada nas configurações do LMS.',
173
+ false,
174
+ );
175
+ }
176
+ return;
177
+ }
178
+
179
+ if (!enabled || !apiKey) {
180
+ this.logger.warn(
181
+ `Transcription skipped for lesson ${lessonId}: OpenAI key not configured`,
182
+ );
183
+ if (notificationContext) {
184
+ await this.updateAsyncNotification(
185
+ notificationContext,
186
+ 100,
187
+ 'Transcrição não iniciada: chave da IA não configurada.',
188
+ false,
189
+ );
190
+ }
191
+ return;
192
+ }
193
+
194
+ const course = await (this.prismaService as any).course.findUnique({
195
+ where: { id: courseId },
196
+ include: { locale: true },
197
+ });
198
+ const language = course?.locale?.code ?? 'pt';
199
+
200
+ const tempDir = join(tmpdir(), `lms-transcribe-${lessonId}-${Date.now()}`);
201
+ await fs.mkdir(tempDir, { recursive: true });
202
+
203
+ try {
204
+ const sourceAudioPath = join(tempDir, `audio_${lessonId}.source`);
205
+ const normalizedAudioPath = join(tempDir, `audio_${lessonId}.normalized.mp3`);
206
+ await emitProgress('Baixando áudio para transcrição...', {
207
+ phase: 'transcription_download_audio',
208
+ lessonId,
209
+ audioFileId,
210
+ });
211
+ await this.fileService.downloadToPath(audioFileId, sourceAudioPath);
212
+
213
+ await emitProgress('Normalizando áudio para transcrição...', {
214
+ phase: 'transcription_split_audio',
215
+ lessonId,
216
+ });
217
+ await execFileAsync(
218
+ 'ffmpeg',
219
+ [
220
+ '-y',
221
+ '-i',
222
+ sourceAudioPath,
223
+ '-ar',
224
+ '16000',
225
+ '-ac',
226
+ '1',
227
+ '-c:a',
228
+ 'libmp3lame',
229
+ '-b:a',
230
+ '32k',
231
+ normalizedAudioPath,
232
+ ],
233
+ {
234
+ maxBuffer: 1024 * 1024 * 20,
235
+ windowsHide: true,
236
+ },
237
+ );
238
+
239
+ const chunkPattern = join(tempDir, 'chunk_%04d.mp3');
240
+ await emitProgress('Dividindo áudio em partes para transcrição...', {
241
+ phase: 'transcription_split_audio',
242
+ lessonId,
243
+ });
244
+ await execFileAsync(
245
+ 'ffmpeg',
246
+ [
247
+ '-y',
248
+ '-i',
249
+ normalizedAudioPath,
250
+ '-f',
251
+ 'segment',
252
+ '-segment_time',
253
+ '60',
254
+ '-ar',
255
+ '16000',
256
+ '-ac',
257
+ '1',
258
+ '-c:a',
259
+ 'libmp3lame',
260
+ '-b:a',
261
+ '32k',
262
+ chunkPattern,
263
+ ],
264
+ {
265
+ maxBuffer: 1024 * 1024 * 20,
266
+ windowsHide: true,
267
+ },
268
+ );
269
+
270
+ const allFiles = await fs.readdir(tempDir);
271
+ const chunks = allFiles
272
+ .filter((name) => name.startsWith('chunk_') && name.endsWith('.mp3'))
273
+ .sort((a, b) => a.localeCompare(b));
274
+ await emitProgress(`Áudio dividido em ${chunks.length} parte(s).`, {
275
+ phase: 'transcription_split_done',
276
+ lessonId,
277
+ chunks: chunks.length,
278
+ });
279
+
280
+ const allSegments: Array<{
281
+ course_lesson_id: number;
282
+ start_seconds: number;
283
+ end_seconds: number;
284
+ text: string;
285
+ }> = [];
286
+
287
+ for (let i = 0; i < chunks.length; i += 1) {
288
+ const chunkName = chunks[i];
289
+ if (!chunkName) continue;
290
+ await emitProgress(
291
+ `Transcrevendo com IA a parte ${i + 1} de ${chunks.length}...`,
292
+ {
293
+ phase: 'transcription_ai_chunk',
294
+ lessonId,
295
+ chunkIndex: i + 1,
296
+ chunkCount: chunks.length,
297
+ },
298
+ );
299
+
300
+ const chunkPath = join(tempDir, chunkName);
301
+ const offsetSeconds = i * 60;
302
+ const chunkBuffer = await fs.readFile(chunkPath);
303
+
304
+ const formData = new FormData();
305
+ formData.append(
306
+ 'file',
307
+ new Blob([chunkBuffer], { type: 'audio/mpeg' }),
308
+ chunkName,
309
+ );
310
+ formData.append('model', 'whisper-1');
311
+ formData.append('response_format', 'verbose_json');
312
+ formData.append('language', language);
313
+
314
+ const headers: Record<string, string> = {
315
+ Authorization: `Bearer ${apiKey}`,
316
+ };
317
+ const maybeHeaders = (formData as any).getHeaders?.();
318
+ if (maybeHeaders && typeof maybeHeaders === 'object') {
319
+ Object.assign(headers, maybeHeaders);
320
+ }
321
+
322
+ const response = await axios.post(
323
+ 'https://api.openai.com/v1/audio/transcriptions',
324
+ formData,
325
+ { headers },
326
+ );
327
+
328
+ const segments = response.data?.segments ?? [];
329
+ for (const segment of segments) {
330
+ const text = String(segment?.text ?? '').trim();
331
+ if (!text) continue;
332
+
333
+ allSegments.push({
334
+ course_lesson_id: lessonId,
335
+ start_seconds: offsetSeconds + Number(segment?.start ?? 0),
336
+ end_seconds: offsetSeconds + Number(segment?.end ?? 0),
337
+ text,
338
+ });
339
+ }
340
+ }
341
+
342
+ await emitProgress('Salvando transcrição gerada pela IA...', {
343
+ phase: 'transcription_save',
344
+ lessonId,
345
+ segments: allSegments.length,
346
+ });
347
+ await this.prismaService.$transaction([
348
+ (this.prismaService as any).course_lesson_transcription_segment.deleteMany({
349
+ where: { course_lesson_id: lessonId },
350
+ }),
351
+ (this.prismaService as any).course_lesson_transcription_segment.createMany({
352
+ data: allSegments,
353
+ }),
354
+ ]);
355
+
356
+ this.logger.log(
357
+ `Transcription saved for lesson ${lessonId}: ${allSegments.length} segments`,
358
+ );
359
+ await emitProgress(
360
+ `Transcrição concluída com ${allSegments.length} segmento(s).`,
361
+ {
362
+ phase: 'transcription_done',
363
+ lessonId,
364
+ segments: allSegments.length,
365
+ },
366
+ );
367
+ if (notificationContext) {
368
+ await this.updateAsyncNotification(
369
+ notificationContext,
370
+ 100,
371
+ `Transcrição concluída com ${allSegments.length} segmento(s).`,
372
+ true,
373
+ );
374
+ }
375
+ } catch (error) {
376
+ if (notificationContext) {
377
+ const message =
378
+ error instanceof Error && error.message
379
+ ? error.message
380
+ : 'Falha ao transcrever o áudio.';
381
+ await this.updateAsyncNotification(
382
+ notificationContext,
383
+ 100,
384
+ `Falha na transcrição: ${message}`,
385
+ false,
386
+ );
387
+ }
388
+ throw error;
389
+ } finally {
390
+ await fs.rm(tempDir, { recursive: true, force: true });
391
+ }
392
+ }
393
+ }
@@ -0,0 +1,28 @@
1
+ import { Role } from '@hed-hog/api';
2
+ import { Body, Controller, Get, Param, ParseIntPipe, Put } from '@nestjs/common';
3
+ import { CourseStructureService } from './course-structure.service';
4
+ import { UpdateTranscriptionSegmentsDTO } from './dto/update-transcription-segments.dto';
5
+
6
+ @Role()
7
+ @Controller('lms/lessons')
8
+ export class CourseLessonController {
9
+ constructor(private readonly courseStructureService: CourseStructureService) {}
10
+
11
+ @Get(':id/transcription-segments')
12
+ getTranscriptionSegments(@Param('id', ParseIntPipe) id: number) {
13
+ return this.courseStructureService.getTranscriptionSegments(id);
14
+ }
15
+
16
+ @Put(':id/transcription-segments')
17
+ updateTranscriptionSegments(
18
+ @Param('id', ParseIntPipe) id: number,
19
+ @Body() dto: UpdateTranscriptionSegmentsDTO,
20
+ ) {
21
+ return this.courseStructureService.updateTranscriptionSegments(id, dto);
22
+ }
23
+
24
+ @Get(':id/audio-files')
25
+ getAudioFiles(@Param('id', ParseIntPipe) id: number) {
26
+ return this.courseStructureService.getAudioFiles(id);
27
+ }
28
+ }
@@ -1,4 +1,4 @@
1
- import { Role } from '@hed-hog/api';
1
+ import { Role, User } from '@hed-hog/api';
2
2
  import {
3
3
  Body,
4
4
  Controller,
@@ -11,15 +11,17 @@ import {
11
11
  } from '@nestjs/common';
12
12
  import { CourseStructureService } from './course-structure.service';
13
13
  import { CourseVideoConversionService } from './course-video-conversion.service';
14
+ import { CreateCourseLessonFrameDto } from './dto/create-course-lesson-frame.dto';
14
15
  import {
15
- CreateCourseStructureLessonDto,
16
- CreateLessonVideoConversionDto,
16
+ CreateCourseStructureLessonDto,
17
+ CreateLessonVideoConversionDto,
17
18
  } from './dto/create-course-structure-lesson.dto';
18
19
  import { CreateCourseStructureSessionDto } from './dto/create-course-structure-session.dto';
19
20
  import { MoveLessonDto } from './dto/move-lesson.dto';
20
21
  import { PasteLessonsDto } from './dto/paste-lessons.dto';
21
22
  import { ReorderLessonsDto } from './dto/reorder-lessons.dto';
22
23
  import { ReorderSessionsDto } from './dto/reorder-sessions.dto';
24
+ import { UpdateCourseLessonFrameDto } from './dto/update-course-lesson-frame.dto';
23
25
  import { UpdateCourseResourcesDto } from './dto/update-course-resources.dto';
24
26
  import { UpdateCourseStructureLessonDto } from './dto/update-course-structure-lesson.dto';
25
27
  import { UpdateCourseStructureSessionDto } from './dto/update-course-structure-session.dto';
@@ -151,12 +153,14 @@ export class CourseStructureController {
151
153
 
152
154
  @Post('sessions/:sessionId/lessons/:lessonId/video-conversions')
153
155
  createLessonVideoConversion(
156
+ @User() { id: userId },
154
157
  @Param('id', ParseIntPipe) courseId: number,
155
158
  @Param('sessionId', ParseIntPipe) sessionId: number,
156
159
  @Param('lessonId', ParseIntPipe) lessonId: number,
157
160
  @Body() dto: CreateLessonVideoConversionDto,
158
161
  ) {
159
162
  return this.courseVideoConversionService.enqueueConversion({
163
+ userId,
160
164
  courseId,
161
165
  sessionId,
162
166
  lessonId,
@@ -173,6 +177,48 @@ export class CourseStructureController {
173
177
  return this.courseStructureService.deleteLesson(courseId, sessionId, lessonId);
174
178
  }
175
179
 
180
+ @Delete('sessions/:sessionId/lessons/:lessonId/frames/:frameId')
181
+ deleteLessonFrame(
182
+ @Param('id', ParseIntPipe) courseId: number,
183
+ @Param('sessionId', ParseIntPipe) sessionId: number,
184
+ @Param('lessonId', ParseIntPipe) lessonId: number,
185
+ @Param('frameId', ParseIntPipe) frameId: number,
186
+ ) {
187
+ return this.courseStructureService.deleteLessonFrame(courseId, sessionId, lessonId, frameId);
188
+ }
189
+
190
+ @Post('sessions/:sessionId/lessons/:lessonId/frames')
191
+ createLessonFrame(
192
+ @Param('id', ParseIntPipe) courseId: number,
193
+ @Param('sessionId', ParseIntPipe) sessionId: number,
194
+ @Param('lessonId', ParseIntPipe) lessonId: number,
195
+ @Body() dto: CreateCourseLessonFrameDto,
196
+ ) {
197
+ return this.courseStructureService.createLessonFrame(
198
+ courseId,
199
+ sessionId,
200
+ lessonId,
201
+ dto,
202
+ );
203
+ }
204
+
205
+ @Patch('sessions/:sessionId/lessons/:lessonId/frames/:frameId')
206
+ updateLessonFrame(
207
+ @Param('id', ParseIntPipe) courseId: number,
208
+ @Param('sessionId', ParseIntPipe) sessionId: number,
209
+ @Param('lessonId', ParseIntPipe) lessonId: number,
210
+ @Param('frameId', ParseIntPipe) frameId: number,
211
+ @Body() dto: UpdateCourseLessonFrameDto,
212
+ ) {
213
+ return this.courseStructureService.updateLessonFrame(
214
+ courseId,
215
+ sessionId,
216
+ lessonId,
217
+ frameId,
218
+ dto,
219
+ );
220
+ }
221
+
176
222
  @Post('sessions/:sessionId/lessons/:lessonId/duplicate')
177
223
  duplicateLesson(
178
224
  @Param('id', ParseIntPipe) courseId: number,