@hed-hog/lms 0.0.366 → 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/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 +26 -5
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +31 -1
- 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.js +7 -7
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -0
- package/dist/course/course.module.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/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +26 -13
- 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-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 +4 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma-performance.service.js +121 -121
- 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 +99 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +111 -2
- package/dist/platforma/platforma.controller.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/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/detail-course.tsx.ejs +69 -1
- 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/_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-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 +26 -4
- package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
- package/hedhog/frontend/messages/en.json +23 -12
- package/hedhog/frontend/messages/pt.json +23 -12
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -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 -66
- package/package.json +9 -9
- 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 +16 -0
- 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 -471
- package/src/course/course.module.ts +4 -0
- 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 -65
- package/src/course/lms-bulk-upload-automation.service.ts +29 -7
- 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 +4 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -30
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
- package/src/platforma/platforma-heartbeat.service.ts +33 -33
- package/src/platforma/platforma-performance.service.ts +606 -606
- package/src/platforma/platforma-search.service.ts +48 -48
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +88 -0
- package/src/training/dto/create-training.dto.ts +36 -0
- package/src/training/training.service.ts +360 -163
|
@@ -7,6 +7,7 @@ import { promises as fs } from 'fs';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { finished } from 'stream/promises';
|
|
9
9
|
import { CourseExportService, VideoLessonWithFile } from './course-export.service';
|
|
10
|
+
import { balanceSubtitleLines } from './subtitle.util';
|
|
10
11
|
import {
|
|
11
12
|
ADLCP_ROOTV1P2_XSD,
|
|
12
13
|
IMSCP_ROOTV1P1P2_XSD,
|
|
@@ -25,7 +26,7 @@ export type ScormVisualOptions = {
|
|
|
25
26
|
|
|
26
27
|
export type Scorm12BuildParams = {
|
|
27
28
|
courseId: number;
|
|
28
|
-
settings: { visual?: ScormVisualOptions };
|
|
29
|
+
settings: { visual?: ScormVisualOptions; subtitleLocaleIds?: number[] };
|
|
29
30
|
workDir: string;
|
|
30
31
|
onProgress: (pct: number, msg: string) => Promise<void>;
|
|
31
32
|
};
|
|
@@ -35,6 +36,7 @@ type LessonData = {
|
|
|
35
36
|
title: string;
|
|
36
37
|
duration: number | null;
|
|
37
38
|
src: string;
|
|
39
|
+
subtitles: Array<{ code: string; name: string; vttFile: string }>;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
type ModuleData = {
|
|
@@ -128,7 +130,39 @@ export class CourseExportScorm12Service {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
const
|
|
133
|
+
const subtitleLocaleIds = settings.subtitleLocaleIds ?? [];
|
|
134
|
+
const subtitleMap: Record<number, Array<{ code: string; name: string; vttFile: string }>> = {};
|
|
135
|
+
const vttFileList: string[] = [];
|
|
136
|
+
|
|
137
|
+
if (subtitleLocaleIds.length > 0) {
|
|
138
|
+
await onProgress(72, 'Gerando arquivos de legenda...');
|
|
139
|
+
const locales = await (this.prisma as any).locale.findMany({
|
|
140
|
+
where: { id: { in: subtitleLocaleIds } },
|
|
141
|
+
select: { id: true, code: true, name: true },
|
|
142
|
+
}) as Array<{ id: number; code: string; name: string }>;
|
|
143
|
+
const localeMap = new Map(locales.map((l: { id: number; code: string; name: string }) => [l.id, l]));
|
|
144
|
+
|
|
145
|
+
for (const lesson of lessons) {
|
|
146
|
+
for (const localeId of subtitleLocaleIds) {
|
|
147
|
+
const locale = localeMap.get(localeId);
|
|
148
|
+
if (!locale) continue;
|
|
149
|
+
const segments = await (this.prisma as any).course_lesson_transcription_segment.findMany({
|
|
150
|
+
where: { course_lesson_id: lesson.id, locale_id: localeId },
|
|
151
|
+
select: { start_seconds: true, end_seconds: true, text: true },
|
|
152
|
+
orderBy: { start_seconds: 'asc' },
|
|
153
|
+
}) as Array<{ start_seconds: number; end_seconds: number; text: string }>;
|
|
154
|
+
if (segments.length === 0) continue;
|
|
155
|
+
const vttContent = this.generateVttContent(segments);
|
|
156
|
+
const vttName = `assets/lesson_${lesson.id}_${locale.code}.vtt`;
|
|
157
|
+
archive.append(Buffer.from(vttContent, 'utf-8'), { name: vttName });
|
|
158
|
+
vttFileList.push(vttName);
|
|
159
|
+
if (!subtitleMap[lesson.id]) subtitleMap[lesson.id] = [];
|
|
160
|
+
subtitleMap[lesson.id].push({ code: locale.code, name: locale.name, vttFile: vttName });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const courseData = this.buildCourseData(courseTitle, lessons, videoSrcMap, subtitleMap);
|
|
132
166
|
|
|
133
167
|
archive.append(Buffer.from(this.generateScormApiJs()), { name: 'SCORM12API.js' });
|
|
134
168
|
archive.append(Buffer.from(IMS_XML_XSD), { name: 'ims_xml.xsd' });
|
|
@@ -140,7 +174,7 @@ export class CourseExportScorm12Service {
|
|
|
140
174
|
);
|
|
141
175
|
archive.append(Buffer.from(this.generateShellHtml(courseData, visual)), { name: 'index.html' });
|
|
142
176
|
|
|
143
|
-
const manifestXml = this.generateManifest(courseTitle, embeddedLessonIds);
|
|
177
|
+
const manifestXml = this.generateManifest(courseTitle, embeddedLessonIds, vttFileList);
|
|
144
178
|
archive.append(Buffer.from(manifestXml), { name: 'imsmanifest.xml' });
|
|
145
179
|
|
|
146
180
|
await onProgress(80, 'Finalizando pacote ZIP...');
|
|
@@ -166,6 +200,7 @@ export class CourseExportScorm12Service {
|
|
|
166
200
|
courseTitle: string,
|
|
167
201
|
lessons: VideoLessonWithFile[],
|
|
168
202
|
videoSrcMap: Record<number, string>,
|
|
203
|
+
subtitleMap: Record<number, Array<{ code: string; name: string; vttFile: string }>>,
|
|
169
204
|
): CourseData {
|
|
170
205
|
const sessionMap = new Map<number, ModuleData>();
|
|
171
206
|
|
|
@@ -184,6 +219,7 @@ export class CourseExportScorm12Service {
|
|
|
184
219
|
title: lesson.title,
|
|
185
220
|
duration: lesson.duration_seconds,
|
|
186
221
|
src: videoSrcMap[lesson.id] || '',
|
|
222
|
+
subtitles: subtitleMap[lesson.id] ?? [],
|
|
187
223
|
});
|
|
188
224
|
}
|
|
189
225
|
|
|
@@ -204,10 +240,13 @@ export class CourseExportScorm12Service {
|
|
|
204
240
|
private generateManifest(
|
|
205
241
|
courseTitle: string,
|
|
206
242
|
embeddedLessonIds: number[],
|
|
243
|
+
vttFiles: string[],
|
|
207
244
|
): string {
|
|
208
245
|
const manifestId = `manifest-${Date.now()}`;
|
|
209
|
-
const
|
|
210
|
-
|
|
246
|
+
const videoLines = embeddedLessonIds.map((id) => ` <file href="assets/lesson_${id}.mp4"/>`);
|
|
247
|
+
const vttLines = vttFiles.map((f) => ` <file href="${f}"/>`);
|
|
248
|
+
const assetFiles = [...videoLines, ...vttLines].length
|
|
249
|
+
? [...videoLines, ...vttLines].join('\n') + '\n'
|
|
211
250
|
: '';
|
|
212
251
|
|
|
213
252
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -347,6 +386,10 @@ ${this.generateCss(visual)}
|
|
|
347
386
|
<div class="footer-meta" id="footerMeta"></div>
|
|
348
387
|
</div>
|
|
349
388
|
<div class="footer-nav">
|
|
389
|
+
<div class="sub-ctrl" id="subCtrl" style="display:none">
|
|
390
|
+
<button class="nav-btn" id="subtitleBtn" title="Legendas">CC</button>
|
|
391
|
+
<div id="subtitleMenu" class="subtitle-menu"></div>
|
|
392
|
+
</div>
|
|
350
393
|
<button class="nav-btn" id="prevBtn" disabled>← Anterior</button>
|
|
351
394
|
<button class="nav-btn primary" id="nextBtn" disabled>Próxima →</button>
|
|
352
395
|
</div>
|
|
@@ -436,6 +479,11 @@ ${this.generateJs(courseJson, visual.progressStyle)}
|
|
|
436
479
|
.nav-btn.primary:hover:not(:disabled){filter:brightness(.85)}
|
|
437
480
|
.menu-toggle{display:none;position:fixed;top:10px;z-index:30;width:34px;height:34px;border-radius:8px;background:rgba(0,0,0,.75);color:#fff;border:none;cursor:pointer;font-size:1rem;align-items:center;justify-content:center}
|
|
438
481
|
.overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9}
|
|
482
|
+
.sub-ctrl{position:relative}
|
|
483
|
+
.subtitle-menu{display:none;position:absolute;bottom:calc(100% + 6px);right:0;background:#1f2937;border:1px solid #374151;border-radius:6px;min-width:140px;overflow:hidden;z-index:20}
|
|
484
|
+
.sub-item{padding:7px 12px;cursor:pointer;font-size:${fs.xs};color:#f3f4f6;white-space:nowrap}
|
|
485
|
+
.sub-item:hover{background:#374151}
|
|
486
|
+
.sub-item.active-sub{color:${pri};font-weight:600}
|
|
439
487
|
@media(max-width:768px){
|
|
440
488
|
.menu-toggle{display:flex}
|
|
441
489
|
.sidebar{position:fixed;top:0;bottom:0;${isRtl ? 'right:0;left:auto' : 'left:0;right:auto'};transform:${isRtl ? 'translateX(100%)' : 'translateX(-100%)'}}
|
|
@@ -453,6 +501,7 @@ ${this.generateJs(courseJson, visual.progressStyle)}
|
|
|
453
501
|
var currentIdx=-1;
|
|
454
502
|
var allLessons=[];
|
|
455
503
|
var _api=null;
|
|
504
|
+
var currentSubCode=null;
|
|
456
505
|
|
|
457
506
|
COURSE.modules.forEach(function(m){
|
|
458
507
|
m.lessons.forEach(function(l){l._mod=m.title;allLessons.push(l);});
|
|
@@ -554,6 +603,63 @@ ${this.generateJs(courseJson, visual.progressStyle)}
|
|
|
554
603
|
});
|
|
555
604
|
}
|
|
556
605
|
|
|
606
|
+
function applySubtitle(video,code){
|
|
607
|
+
var tracks=video.textTracks;
|
|
608
|
+
for(var i=0;i<tracks.length;i++){
|
|
609
|
+
tracks[i].mode=(code&&tracks[i].language===code)?'showing':'hidden';
|
|
610
|
+
}
|
|
611
|
+
var btn=document.getElementById('subtitleBtn');
|
|
612
|
+
if(btn)btn.style.opacity=code?'1':'0.5';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function highlightSubMenu(){
|
|
616
|
+
var menu=document.getElementById('subtitleMenu');
|
|
617
|
+
if(!menu)return;
|
|
618
|
+
menu.querySelectorAll('.sub-item').forEach(function(el){
|
|
619
|
+
el.classList.toggle('active-sub',(el.getAttribute('data-code')||null)===currentSubCode);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildSubMenu(subtitles,video){
|
|
624
|
+
var menu=document.getElementById('subtitleMenu');
|
|
625
|
+
if(!menu)return;
|
|
626
|
+
var html='<div class="sub-item" data-code="">Desligado</div>';
|
|
627
|
+
subtitles.forEach(function(sub){
|
|
628
|
+
html+='<div class="sub-item" data-code="'+sub.code+'">'+esc(sub.name)+'</div>';
|
|
629
|
+
});
|
|
630
|
+
menu.innerHTML=html;
|
|
631
|
+
menu.querySelectorAll('.sub-item').forEach(function(el){
|
|
632
|
+
el.addEventListener('click',function(e){
|
|
633
|
+
e.stopPropagation();
|
|
634
|
+
var code=el.getAttribute('data-code')||null;
|
|
635
|
+
currentSubCode=code;
|
|
636
|
+
applySubtitle(video,currentSubCode);
|
|
637
|
+
menu.style.display='none';
|
|
638
|
+
highlightSubMenu();
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
highlightSubMenu();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function updateSubtitleTracks(lesson,video){
|
|
645
|
+
var oldTracks=video.querySelectorAll('track');
|
|
646
|
+
oldTracks.forEach(function(t){video.removeChild(t);});
|
|
647
|
+
var subCtrl=document.getElementById('subCtrl');
|
|
648
|
+
if(!subCtrl)return;
|
|
649
|
+
if(!lesson.subtitles||!lesson.subtitles.length){subCtrl.style.display='none';return;}
|
|
650
|
+
subCtrl.style.display='';
|
|
651
|
+
lesson.subtitles.forEach(function(sub){
|
|
652
|
+
var t=document.createElement('track');
|
|
653
|
+
t.kind='subtitles';
|
|
654
|
+
t.src=sub.vttFile;
|
|
655
|
+
t.srclang=sub.code;
|
|
656
|
+
t.label=sub.name;
|
|
657
|
+
video.appendChild(t);
|
|
658
|
+
});
|
|
659
|
+
applySubtitle(video,currentSubCode);
|
|
660
|
+
buildSubMenu(lesson.subtitles,video);
|
|
661
|
+
}
|
|
662
|
+
|
|
557
663
|
function navigateTo(idx){
|
|
558
664
|
if(idx<0||idx>=allLessons.length)return;
|
|
559
665
|
if(currentIdx>=0&&idx!==currentIdx)markCompleted(allLessons[currentIdx].id);
|
|
@@ -572,6 +678,7 @@ ${this.generateJs(courseJson, visual.progressStyle)}
|
|
|
572
678
|
empty.textContent='Esta aula n\\u00e3o possui v\\u00eddeo dispon\\u00edvel';
|
|
573
679
|
empty.style.display='block';
|
|
574
680
|
}
|
|
681
|
+
updateSubtitleTracks(lesson,video);
|
|
575
682
|
document.getElementById('footerTitle').textContent=lesson.title;
|
|
576
683
|
var meta=[];
|
|
577
684
|
if(lesson._mod)meta.push(lesson._mod);
|
|
@@ -610,6 +717,21 @@ ${this.generateJs(courseJson, visual.progressStyle)}
|
|
|
610
717
|
document.getElementById('prevBtn').addEventListener('click',function(){navigateTo(currentIdx-1);});
|
|
611
718
|
document.getElementById('nextBtn').addEventListener('click',function(){navigateTo(currentIdx+1);});
|
|
612
719
|
|
|
720
|
+
(function(){
|
|
721
|
+
var sbtn=document.getElementById('subtitleBtn');
|
|
722
|
+
var smenu=document.getElementById('subtitleMenu');
|
|
723
|
+
if(sbtn&&smenu){
|
|
724
|
+
sbtn.addEventListener('click',function(e){
|
|
725
|
+
e.stopPropagation();
|
|
726
|
+
smenu.style.display=smenu.style.display==='block'?'none':'block';
|
|
727
|
+
highlightSubMenu();
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
document.addEventListener('click',function(){
|
|
731
|
+
if(smenu)smenu.style.display='none';
|
|
732
|
+
});
|
|
733
|
+
})();
|
|
734
|
+
|
|
613
735
|
(function(){
|
|
614
736
|
var sidebar=document.getElementById('sidebar');
|
|
615
737
|
var overlay=document.getElementById('overlay');
|
|
@@ -650,6 +772,28 @@ ${this.generateJs(courseJson, visual.progressStyle)}
|
|
|
650
772
|
});`;
|
|
651
773
|
}
|
|
652
774
|
|
|
775
|
+
private generateVttContent(
|
|
776
|
+
segments: Array<{ start_seconds: number; end_seconds: number; text: string }>,
|
|
777
|
+
): string {
|
|
778
|
+
const lines = ['WEBVTT', ''];
|
|
779
|
+
for (const seg of segments) {
|
|
780
|
+
lines.push(
|
|
781
|
+
`${this.secondsToVttTime(seg.start_seconds)} --> ${this.secondsToVttTime(seg.end_seconds)}`,
|
|
782
|
+
balanceSubtitleLines(seg.text),
|
|
783
|
+
'',
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
return lines.join('\n');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private secondsToVttTime(s: number): string {
|
|
790
|
+
const total = Math.max(0, s);
|
|
791
|
+
const h = Math.floor(total / 3600);
|
|
792
|
+
const m = Math.floor((total % 3600) / 60);
|
|
793
|
+
const sec = total % 60;
|
|
794
|
+
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${sec.toFixed(3).padStart(6, '0')}`;
|
|
795
|
+
}
|
|
796
|
+
|
|
653
797
|
private escapeXml(str: string): string {
|
|
654
798
|
return str
|
|
655
799
|
.replace(/&/g, '&')
|
|
@@ -1,24 +1,65 @@
|
|
|
1
1
|
import { Role } from '@hed-hog/api';
|
|
2
|
-
import { Body, Controller, Get, Param, ParseIntPipe, Post, Put } from '@nestjs/common';
|
|
2
|
+
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query } from '@nestjs/common';
|
|
3
|
+
import { PlatformaVideoService } from '../platforma/platforma-video.service';
|
|
3
4
|
import { CourseStructureService } from './course-structure.service';
|
|
4
5
|
import { UpdateTranscriptionSegmentsDTO } from './dto/update-transcription-segments.dto';
|
|
5
6
|
|
|
6
7
|
@Role()
|
|
7
8
|
@Controller('lms/lessons')
|
|
8
9
|
export class CourseLessonController {
|
|
9
|
-
constructor(
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly courseStructureService: CourseStructureService,
|
|
12
|
+
private readonly platformaVideoService: PlatformaVideoService,
|
|
13
|
+
) {}
|
|
10
14
|
|
|
11
15
|
@Get(':id/transcription-segments')
|
|
12
|
-
getTranscriptionSegments(
|
|
13
|
-
|
|
16
|
+
getTranscriptionSegments(
|
|
17
|
+
@Param('id', ParseIntPipe) id: number,
|
|
18
|
+
@Query('locale_id') localeId?: string,
|
|
19
|
+
) {
|
|
20
|
+
const parsedLocaleId =
|
|
21
|
+
localeId === 'null' ? null : localeId !== undefined ? parseInt(localeId, 10) : undefined;
|
|
22
|
+
return this.courseStructureService.getTranscriptionSegments(id, parsedLocaleId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Get(':id/transcription-locales')
|
|
26
|
+
getTranscriptionLocales(@Param('id', ParseIntPipe) id: number) {
|
|
27
|
+
return this.courseStructureService.getAvailableTranscriptionLocales(id);
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
@Put(':id/transcription-segments')
|
|
17
31
|
updateTranscriptionSegments(
|
|
18
32
|
@Param('id', ParseIntPipe) id: number,
|
|
19
33
|
@Body() dto: UpdateTranscriptionSegmentsDTO,
|
|
34
|
+
@Query('locale_id') localeId?: string,
|
|
35
|
+
) {
|
|
36
|
+
const parsedLocaleId =
|
|
37
|
+
localeId === 'null' ? null : localeId !== undefined ? parseInt(localeId, 10) : undefined;
|
|
38
|
+
return this.courseStructureService.updateTranscriptionSegments(id, dto, parsedLocaleId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Delete(':id/transcription-segments')
|
|
42
|
+
deleteTranscriptionLocale(
|
|
43
|
+
@Param('id', ParseIntPipe) id: number,
|
|
44
|
+
@Query('locale_id') localeId?: string,
|
|
45
|
+
) {
|
|
46
|
+
const parsedLocaleId =
|
|
47
|
+
localeId === 'null' ? null : localeId !== undefined ? parseInt(localeId, 10) : null;
|
|
48
|
+
return this.courseStructureService.deleteTranscriptionLocale(id, parsedLocaleId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Get(':id/preview/hls-token')
|
|
52
|
+
getAdminHlsToken(@Param('id', ParseIntPipe) id: number) {
|
|
53
|
+
return this.platformaVideoService.generateAdminHlsToken(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Get(':id/preview/subtitles-token')
|
|
57
|
+
getAdminSubtitlesToken(
|
|
58
|
+
@Param('id', ParseIntPipe) id: number,
|
|
59
|
+
@Query('locale_id') localeId?: string,
|
|
20
60
|
) {
|
|
21
|
-
|
|
61
|
+
const parsedLocaleId = localeId !== undefined ? parseInt(localeId, 10) : undefined;
|
|
62
|
+
return this.platformaVideoService.generateAdminSubtitlesToken(id, parsedLocaleId);
|
|
22
63
|
}
|
|
23
64
|
|
|
24
65
|
@Get(':id/audio-files')
|
|
@@ -30,4 +71,16 @@ export class CourseLessonController {
|
|
|
30
71
|
startTranscription(@Param('id', ParseIntPipe) id: number) {
|
|
31
72
|
return this.courseStructureService.startTranscription(id);
|
|
32
73
|
}
|
|
33
|
-
|
|
74
|
+
|
|
75
|
+
@Post(':id/transcription/translate')
|
|
76
|
+
translateTranscription(
|
|
77
|
+
@Param('id', ParseIntPipe) id: number,
|
|
78
|
+
@Body() body: { targetLocaleId: number; sourceLocaleId?: number | null },
|
|
79
|
+
) {
|
|
80
|
+
return this.courseStructureService.translateTranscription(
|
|
81
|
+
id,
|
|
82
|
+
body.sourceLocaleId ?? null,
|
|
83
|
+
body.targetLocaleId,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -238,6 +238,21 @@ export class CourseStructureController {
|
|
|
238
238
|
return this.courseStructureService.getVideoProcessingStats(courseId);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
@Get('transcription-locales')
|
|
242
|
+
getCourseTranscriptionLocales(@Param('id', ParseIntPipe) courseId: number) {
|
|
243
|
+
return this.courseStructureService.getCourseTranscriptionLocales(courseId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Delete('transcription-segments')
|
|
247
|
+
deleteAllCourseTranscriptions(@Param('id', ParseIntPipe) courseId: number) {
|
|
248
|
+
return this.courseStructureService.deleteAllCourseTranscriptions(courseId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@Get('ai-costs')
|
|
252
|
+
getCourseAiCosts(@Param('id', ParseIntPipe) courseId: number) {
|
|
253
|
+
return this.courseStructureService.getCourseAiCosts(courseId);
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
@Post('bulk-jobs')
|
|
242
257
|
createBulkJobs(
|
|
243
258
|
@User() user: any,
|
|
@@ -249,6 +264,7 @@ export class CourseStructureController {
|
|
|
249
264
|
dto.jobType,
|
|
250
265
|
user?.id ?? 0,
|
|
251
266
|
dto.reprocessAlreadyProcessed,
|
|
267
|
+
dto.targetLocaleId,
|
|
252
268
|
);
|
|
253
269
|
}
|
|
254
270
|
}
|
|
@@ -3,6 +3,7 @@ import { FileService } from '@hed-hog/core';
|
|
|
3
3
|
import { QueueJobService } from '@hed-hog/queue';
|
|
4
4
|
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
5
5
|
import { InstructorService } from '../instructor/instructor.service';
|
|
6
|
+
import { CourseAiUsageService } from './course-ai-usage.service';
|
|
6
7
|
import { CourseOperationsIntegrationService } from './course-operations-integration.service';
|
|
7
8
|
import { CourseVideoHlsService } from './course-video-hls.service';
|
|
8
9
|
import { CreateCourseLessonFrameDto } from './dto/create-course-lesson-frame.dto';
|
|
@@ -45,8 +46,13 @@ export class CourseStructureService {
|
|
|
45
46
|
private readonly operationsIntegration: CourseOperationsIntegrationService,
|
|
46
47
|
private readonly queueJob: QueueJobService,
|
|
47
48
|
private readonly courseVideoHlsService: CourseVideoHlsService,
|
|
49
|
+
private readonly aiUsageService: CourseAiUsageService,
|
|
48
50
|
) {}
|
|
49
51
|
|
|
52
|
+
getCourseAiCosts(courseId: number) {
|
|
53
|
+
return this.aiUsageService.getCourseAiCosts(courseId);
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
async getStructure(courseId: number) {
|
|
51
57
|
await this.ensureCourseExists(courseId);
|
|
52
58
|
|
|
@@ -61,7 +67,9 @@ export class CourseStructureService {
|
|
|
61
67
|
course_lesson: {
|
|
62
68
|
orderBy: { order: 'asc' },
|
|
63
69
|
include: {
|
|
64
|
-
course_lesson_file:
|
|
70
|
+
course_lesson_file: {
|
|
71
|
+
include: { file: { select: { size: true } } },
|
|
72
|
+
},
|
|
65
73
|
course_lesson_video_frame: {
|
|
66
74
|
orderBy: { id: 'asc' },
|
|
67
75
|
include: {
|
|
@@ -162,11 +170,12 @@ export class CourseStructureService {
|
|
|
162
170
|
parsedContent?.statusProducao,
|
|
163
171
|
),
|
|
164
172
|
published: lessonPublishedById.get(lesson.id) ?? false,
|
|
173
|
+
temTranscricao: Boolean((lesson as any).has_transcription),
|
|
165
174
|
recursos: lesson.course_lesson_file.map((fileLink) => ({
|
|
166
175
|
id: String(fileLink.id),
|
|
167
176
|
nome: fileLink.title,
|
|
168
177
|
fileId: fileLink.file_id,
|
|
169
|
-
tamanho:
|
|
178
|
+
tamanho: fileLink.file?.size ?? 0,
|
|
170
179
|
type: this.readLessonFileType(fileLink),
|
|
171
180
|
is_public: this.readLessonFileVisibility(fileLink),
|
|
172
181
|
})),
|
|
@@ -1114,12 +1123,17 @@ export class CourseStructureService {
|
|
|
1114
1123
|
return { lessons: result };
|
|
1115
1124
|
}
|
|
1116
1125
|
|
|
1117
|
-
async getTranscriptionSegments(lessonId: number) {
|
|
1126
|
+
async getTranscriptionSegments(lessonId: number, localeId?: number | null) {
|
|
1127
|
+
const where: Record<string, any> = { course_lesson_id: lessonId };
|
|
1128
|
+
if (localeId !== undefined) {
|
|
1129
|
+
where.locale_id = localeId ?? null;
|
|
1130
|
+
}
|
|
1118
1131
|
return (this.prisma as any).course_lesson_transcription_segment.findMany({
|
|
1119
|
-
where
|
|
1132
|
+
where,
|
|
1120
1133
|
orderBy: { start_seconds: 'asc' },
|
|
1121
1134
|
select: {
|
|
1122
1135
|
id: true,
|
|
1136
|
+
locale_id: true,
|
|
1123
1137
|
start_seconds: true,
|
|
1124
1138
|
end_seconds: true,
|
|
1125
1139
|
text: true,
|
|
@@ -1127,17 +1141,44 @@ export class CourseStructureService {
|
|
|
1127
1141
|
});
|
|
1128
1142
|
}
|
|
1129
1143
|
|
|
1144
|
+
async getAvailableTranscriptionLocales(lessonId: number) {
|
|
1145
|
+
const rows = await (this.prisma as any).course_lesson_transcription_segment.findMany({
|
|
1146
|
+
where: { course_lesson_id: lessonId },
|
|
1147
|
+
select: { locale_id: true },
|
|
1148
|
+
distinct: ['locale_id'],
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
const localeIds: (number | null)[] = rows.map((r: any) => r.locale_id);
|
|
1152
|
+
|
|
1153
|
+
const locales = await Promise.all(
|
|
1154
|
+
localeIds.map(async (id) => {
|
|
1155
|
+
if (id === null) return { id: null, code: null, name: null, region: null };
|
|
1156
|
+
const locale = await (this.prisma as any).locale.findUnique({
|
|
1157
|
+
where: { id },
|
|
1158
|
+
select: { id: true, code: true, name: true, region: true },
|
|
1159
|
+
});
|
|
1160
|
+
return locale ?? { id, code: null, name: null, region: null };
|
|
1161
|
+
}),
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
return locales;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1130
1167
|
async updateTranscriptionSegments(
|
|
1131
1168
|
lessonId: number,
|
|
1132
1169
|
dto: UpdateTranscriptionSegmentsDTO,
|
|
1170
|
+
localeId?: number | null,
|
|
1133
1171
|
) {
|
|
1172
|
+
const where: Record<string, any> = { course_lesson_id: lessonId };
|
|
1173
|
+
if (localeId !== undefined) {
|
|
1174
|
+
where.locale_id = localeId ?? null;
|
|
1175
|
+
}
|
|
1134
1176
|
return this.prisma.$transaction([
|
|
1135
|
-
(this.prisma as any).course_lesson_transcription_segment.deleteMany({
|
|
1136
|
-
where: { course_lesson_id: lessonId },
|
|
1137
|
-
}),
|
|
1177
|
+
(this.prisma as any).course_lesson_transcription_segment.deleteMany({ where }),
|
|
1138
1178
|
(this.prisma as any).course_lesson_transcription_segment.createMany({
|
|
1139
1179
|
data: (dto.segments ?? []).map((segment) => ({
|
|
1140
1180
|
course_lesson_id: lessonId,
|
|
1181
|
+
locale_id: localeId ?? null,
|
|
1141
1182
|
start_seconds: segment.startSeconds,
|
|
1142
1183
|
end_seconds: segment.endSeconds,
|
|
1143
1184
|
text: segment.text,
|
|
@@ -1146,6 +1187,69 @@ export class CourseStructureService {
|
|
|
1146
1187
|
]);
|
|
1147
1188
|
}
|
|
1148
1189
|
|
|
1190
|
+
async deleteTranscriptionLocale(lessonId: number, localeId: number | null) {
|
|
1191
|
+
const result = await (this.prisma as any).course_lesson_transcription_segment.deleteMany({
|
|
1192
|
+
where: { course_lesson_id: lessonId, locale_id: localeId },
|
|
1193
|
+
});
|
|
1194
|
+
return { success: true, deleted: result.count };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async deleteAllCourseTranscriptions(courseId: number) {
|
|
1198
|
+
const lessons = await (this.prisma as any).course_lesson.findMany({
|
|
1199
|
+
where: { course_module: { course_id: courseId } },
|
|
1200
|
+
select: { id: true },
|
|
1201
|
+
});
|
|
1202
|
+
const lessonIds = lessons.map((l: any) => l.id);
|
|
1203
|
+
if (lessonIds.length === 0) {
|
|
1204
|
+
return { success: true, deleted: 0 };
|
|
1205
|
+
}
|
|
1206
|
+
const result = await (this.prisma as any).course_lesson_transcription_segment.deleteMany({
|
|
1207
|
+
where: { course_lesson_id: { in: lessonIds } },
|
|
1208
|
+
});
|
|
1209
|
+
return { success: true, deleted: result.count };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
async translateTranscription(
|
|
1213
|
+
lessonId: number,
|
|
1214
|
+
sourceLocaleId: number | null,
|
|
1215
|
+
targetLocaleId: number,
|
|
1216
|
+
notificationId?: number,
|
|
1217
|
+
notificationUserId?: number,
|
|
1218
|
+
) {
|
|
1219
|
+
const lesson = await (this.prisma as any).course_lesson.findUnique({
|
|
1220
|
+
where: { id: lessonId },
|
|
1221
|
+
select: { id: true },
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
if (!lesson) {
|
|
1225
|
+
throw new NotFoundException(`Lesson ${lessonId} not found`);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const job = await this.queueJob.enqueue({
|
|
1229
|
+
type: 'lms.transcription.translate',
|
|
1230
|
+
queueName: 'lms.transcription.translate',
|
|
1231
|
+
payload: {
|
|
1232
|
+
lessonId,
|
|
1233
|
+
sourceLocaleId,
|
|
1234
|
+
targetLocaleId,
|
|
1235
|
+
...(notificationId != null ? { notificationId } : {}),
|
|
1236
|
+
...(notificationUserId != null ? { notificationUserId } : {}),
|
|
1237
|
+
},
|
|
1238
|
+
sourceModule: 'lms',
|
|
1239
|
+
sourceEntity: 'course_lesson',
|
|
1240
|
+
sourceEntityId: `${lessonId}:${targetLocaleId}`,
|
|
1241
|
+
maxAttempts: 3,
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const startedOnDemand = await this.startTranscriptionOnDemandIfNeeded(job.id);
|
|
1245
|
+
|
|
1246
|
+
if (startedOnDemand) {
|
|
1247
|
+
return { queueJobId: job.id, status: 'processing' };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return { queueJobId: job.id, status: job.status };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1149
1253
|
async getAudioFiles(lessonId: number) {
|
|
1150
1254
|
return (this.prisma as any).course_lesson_file.findMany({
|
|
1151
1255
|
where: {
|
|
@@ -1270,9 +1374,10 @@ export class CourseStructureService {
|
|
|
1270
1374
|
|
|
1271
1375
|
async bulkEnqueueJobs(
|
|
1272
1376
|
courseId: number,
|
|
1273
|
-
jobType: 'transcription' | 'xp_recalculation' | 'video_processing',
|
|
1377
|
+
jobType: 'transcription' | 'xp_recalculation' | 'video_processing' | 'translate_transcription',
|
|
1274
1378
|
userId: number,
|
|
1275
1379
|
reprocessAlreadyProcessed?: boolean,
|
|
1380
|
+
targetLocaleId?: number,
|
|
1276
1381
|
): Promise<{
|
|
1277
1382
|
queued: number;
|
|
1278
1383
|
skipped: number;
|
|
@@ -1363,7 +1468,7 @@ export class CourseStructureService {
|
|
|
1363
1468
|
skipped++;
|
|
1364
1469
|
}
|
|
1365
1470
|
}
|
|
1366
|
-
} else {
|
|
1471
|
+
} else if (jobType === 'xp_recalculation') {
|
|
1367
1472
|
for (const lesson of lessons) {
|
|
1368
1473
|
const segments = await (this.prisma as any).course_lesson_transcription_segment.findFirst({
|
|
1369
1474
|
where: { course_lesson_id: lesson.id },
|
|
@@ -1409,11 +1514,78 @@ export class CourseStructureService {
|
|
|
1409
1514
|
`${skippedWithoutTranscription} aula(s) sem transcrição foram ignoradas. Gere a transcrição antes de recalcular o XP.`,
|
|
1410
1515
|
);
|
|
1411
1516
|
}
|
|
1517
|
+
} else if (jobType === 'translate_transcription') {
|
|
1518
|
+
if (!targetLocaleId) {
|
|
1519
|
+
warnings.push('targetLocaleId é obrigatório para o job de tradução em massa.');
|
|
1520
|
+
return { queued, skipped, skippedWithoutTranscription, warnings };
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
for (const lesson of lessons) {
|
|
1524
|
+
const sourceSegment = await (this.prisma as any).course_lesson_transcription_segment.findFirst({
|
|
1525
|
+
where: {
|
|
1526
|
+
course_lesson_id: lesson.id,
|
|
1527
|
+
NOT: { locale_id: targetLocaleId },
|
|
1528
|
+
},
|
|
1529
|
+
select: { id: true, locale_id: true },
|
|
1530
|
+
orderBy: { id: 'asc' },
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
if (!sourceSegment) {
|
|
1534
|
+
skipped++;
|
|
1535
|
+
skippedWithoutTranscription++;
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
await this.queueJob.enqueue({
|
|
1540
|
+
type: 'lms.transcription.translate',
|
|
1541
|
+
queueName: 'lms.transcription.translate',
|
|
1542
|
+
payload: {
|
|
1543
|
+
lessonId: lesson.id,
|
|
1544
|
+
sourceLocaleId: sourceSegment.locale_id ?? null,
|
|
1545
|
+
targetLocaleId,
|
|
1546
|
+
},
|
|
1547
|
+
sourceModule: 'lms',
|
|
1548
|
+
sourceEntity: 'course_lesson',
|
|
1549
|
+
sourceEntityId: `${lesson.id}:${targetLocaleId}`,
|
|
1550
|
+
maxAttempts: 3,
|
|
1551
|
+
});
|
|
1552
|
+
queued++;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (skippedWithoutTranscription > 0) {
|
|
1556
|
+
warnings.push(
|
|
1557
|
+
`${skippedWithoutTranscription} aula(s) sem transcrição foram ignoradas.`,
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1412
1560
|
}
|
|
1413
1561
|
|
|
1414
1562
|
return { queued, skipped, skippedWithoutTranscription, warnings };
|
|
1415
1563
|
}
|
|
1416
1564
|
|
|
1565
|
+
async getCourseTranscriptionLocales(courseId: number) {
|
|
1566
|
+
const rows = await (this.prisma as any).course_lesson_transcription_segment.findMany({
|
|
1567
|
+
where: {
|
|
1568
|
+
course_lesson: {
|
|
1569
|
+
course_module: { course_id: courseId },
|
|
1570
|
+
},
|
|
1571
|
+
},
|
|
1572
|
+
select: { locale_id: true },
|
|
1573
|
+
distinct: ['locale_id'],
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
const localeIds = (rows as Array<{ locale_id: number | null }>)
|
|
1577
|
+
.map((r) => r.locale_id)
|
|
1578
|
+
.filter((id): id is number => id !== null);
|
|
1579
|
+
|
|
1580
|
+
if (localeIds.length === 0) return [];
|
|
1581
|
+
|
|
1582
|
+
return (this.prisma as any).locale.findMany({
|
|
1583
|
+
where: { id: { in: localeIds } },
|
|
1584
|
+
select: { id: true, code: true, name: true, region: true },
|
|
1585
|
+
orderBy: { name: 'asc' },
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1417
1589
|
private async syncLessonRelations(
|
|
1418
1590
|
lessonId: number,
|
|
1419
1591
|
dto: Partial<
|
|
@@ -1510,7 +1682,9 @@ export class CourseStructureService {
|
|
|
1510
1682
|
},
|
|
1511
1683
|
},
|
|
1512
1684
|
},
|
|
1513
|
-
course_lesson_file:
|
|
1685
|
+
course_lesson_file: {
|
|
1686
|
+
include: { file: { select: { size: true } } },
|
|
1687
|
+
},
|
|
1514
1688
|
course_lesson_question: {
|
|
1515
1689
|
orderBy: { order: 'asc' },
|
|
1516
1690
|
},
|