@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
|
@@ -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
|
},
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { buildAiConfigFromIntegration, NotificationService, SettingService } from '@hed-hog/core';
|
|
3
|
+
import { DatabaseQueueProvider, IJobHandler, NonRetryableError, 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 { CourseAiUsageService } from './course-ai-usage.service';
|
|
13
|
+
|
|
14
|
+
const BATCH_SIZE = 50;
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class CourseTranscriptionTranslationService implements OnModuleInit, IJobHandler {
|
|
18
|
+
private readonly logger = new Logger(CourseTranscriptionTranslationService.name);
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
@Inject(forwardRef(() => PrismaService))
|
|
22
|
+
private readonly prismaService: PrismaService,
|
|
23
|
+
@Inject(forwardRef(() => SettingService))
|
|
24
|
+
private readonly settingService: SettingService,
|
|
25
|
+
@Inject(forwardRef(() => NotificationService))
|
|
26
|
+
private readonly notificationService: NotificationService,
|
|
27
|
+
@Inject(forwardRef(() => QueueHandlerRegistry))
|
|
28
|
+
private readonly registry: QueueHandlerRegistry,
|
|
29
|
+
@Inject(forwardRef(() => DatabaseQueueProvider))
|
|
30
|
+
private readonly dbQueue: DatabaseQueueProvider,
|
|
31
|
+
@Inject(forwardRef(() => CourseAiUsageService))
|
|
32
|
+
private readonly aiUsageService: CourseAiUsageService,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
onModuleInit() {
|
|
36
|
+
this.registry.register('lms.transcription.translate', this);
|
|
37
|
+
this.logger.log('Registered handler for "lms.transcription.translate"');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async createProgressEvent(
|
|
41
|
+
queueJobId: number,
|
|
42
|
+
message: string,
|
|
43
|
+
metadata?: Record<string, unknown>,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
await (this.prismaService as any).queue_job_event.create({
|
|
47
|
+
data: {
|
|
48
|
+
queue_job_id: queueJobId,
|
|
49
|
+
event_type: 'started',
|
|
50
|
+
message,
|
|
51
|
+
...(metadata ? { metadata } : {}),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.logger.warn(
|
|
56
|
+
`Queue job ${queueJobId}: failed to persist translation progress event "${message}": ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async updateNotification(
|
|
62
|
+
userId: number,
|
|
63
|
+
notificationId: number,
|
|
64
|
+
progress: number,
|
|
65
|
+
body: string,
|
|
66
|
+
success?: boolean,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
await this.notificationService.updateProgress(userId, notificationId, {
|
|
70
|
+
progress,
|
|
71
|
+
body,
|
|
72
|
+
...(success != null ? { success } : {}),
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.logger.warn(
|
|
76
|
+
`Failed to update notification ${notificationId} for user ${userId}: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async translateBatch(
|
|
82
|
+
texts: string[],
|
|
83
|
+
targetLangName: string,
|
|
84
|
+
apiKey: string,
|
|
85
|
+
): Promise<{ translations: string[]; inputTokens: number; outputTokens: number }> {
|
|
86
|
+
const prompt = [
|
|
87
|
+
`Translate the following subtitle segments to ${targetLangName}.`,
|
|
88
|
+
`Return ONLY a JSON array of strings with the same number of elements, in the same order.`,
|
|
89
|
+
`Do not add explanations. Preserve punctuation and formatting.`,
|
|
90
|
+
``,
|
|
91
|
+
`Input:`,
|
|
92
|
+
JSON.stringify(texts),
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
const response = await axios.post(
|
|
96
|
+
'https://api.openai.com/v1/chat/completions',
|
|
97
|
+
{
|
|
98
|
+
model: 'gpt-4o-mini',
|
|
99
|
+
messages: [{ role: 'user', content: prompt }],
|
|
100
|
+
temperature: 0.2,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${apiKey}`,
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
},
|
|
107
|
+
timeout: 120_000,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const content: string = response.data?.choices?.[0]?.message?.content ?? '[]';
|
|
112
|
+
const start = content.indexOf('[');
|
|
113
|
+
const end = content.lastIndexOf(']');
|
|
114
|
+
if (start === -1 || end === -1) {
|
|
115
|
+
throw new Error(`AI response did not contain a JSON array: ${content.slice(0, 200)}`);
|
|
116
|
+
}
|
|
117
|
+
const parsed: unknown = JSON.parse(content.slice(start, end + 1));
|
|
118
|
+
if (!Array.isArray(parsed)) {
|
|
119
|
+
throw new Error('AI translation response is not an array');
|
|
120
|
+
}
|
|
121
|
+
const usage = response.data?.usage ?? {};
|
|
122
|
+
return {
|
|
123
|
+
translations: parsed.map((v) => String(v)),
|
|
124
|
+
inputTokens: Number(usage.prompt_tokens ?? 0),
|
|
125
|
+
outputTokens: Number(usage.completion_tokens ?? 0),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async handle(job: {
|
|
130
|
+
id: number;
|
|
131
|
+
type: string;
|
|
132
|
+
queue_name: string;
|
|
133
|
+
payload: Record<string, any>;
|
|
134
|
+
attempts: number;
|
|
135
|
+
max_attempts: number;
|
|
136
|
+
source_module?: string | null;
|
|
137
|
+
source_entity?: string | null;
|
|
138
|
+
source_entity_id?: string | null;
|
|
139
|
+
}): Promise<void> {
|
|
140
|
+
const { lessonId, sourceLocaleId, targetLocaleId, notificationId, notificationUserId } =
|
|
141
|
+
job.payload as {
|
|
142
|
+
lessonId: number;
|
|
143
|
+
sourceLocaleId: number | null;
|
|
144
|
+
targetLocaleId: number;
|
|
145
|
+
notificationId?: number;
|
|
146
|
+
notificationUserId?: number;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const hasNotification =
|
|
150
|
+
Number.isInteger(Number(notificationId)) &&
|
|
151
|
+
Number.isInteger(Number(notificationUserId));
|
|
152
|
+
|
|
153
|
+
const emitProgress = async (message: string, progress: number, success?: boolean) => {
|
|
154
|
+
await this.createProgressEvent(job.id, message);
|
|
155
|
+
if (hasNotification) {
|
|
156
|
+
await this.updateNotification(
|
|
157
|
+
Number(notificationUserId),
|
|
158
|
+
Number(notificationId),
|
|
159
|
+
progress,
|
|
160
|
+
message,
|
|
161
|
+
success,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const settings = await this.settingService.getSettingValues(['ai-openai-profile-id']);
|
|
167
|
+
const profileId = Number(settings['ai-openai-profile-id']);
|
|
168
|
+
let apiKey = '';
|
|
169
|
+
|
|
170
|
+
if (profileId) {
|
|
171
|
+
const profile = await (this.prismaService as any).integration_profile.findUnique({
|
|
172
|
+
where: { id: profileId },
|
|
173
|
+
include: { integration_provider: { select: { slug: true } } },
|
|
174
|
+
});
|
|
175
|
+
if (profile) {
|
|
176
|
+
apiKey = buildAiConfigFromIntegration(
|
|
177
|
+
profile.integration_provider.slug,
|
|
178
|
+
profile.config,
|
|
179
|
+
).apiKey;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!apiKey) {
|
|
184
|
+
const msg = profileId
|
|
185
|
+
? `Perfil de IA (id=${profileId}) não encontrado ou sem chave OpenAI configurada.`
|
|
186
|
+
: `Tradução requer um perfil OpenAI configurado. Acesse Settings → LMS → ai-openai-profile-id.`;
|
|
187
|
+
await emitProgress(msg, 100, false);
|
|
188
|
+
throw new NonRetryableError(msg);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const targetLocale = await (this.prismaService as any).locale.findUnique({
|
|
192
|
+
where: { id: targetLocaleId },
|
|
193
|
+
select: { id: true, code: true, name: true },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!targetLocale) {
|
|
197
|
+
const msg = `Locale alvo (id=${targetLocaleId}) não encontrado.`;
|
|
198
|
+
await emitProgress(msg, 100, false);
|
|
199
|
+
throw new NonRetryableError(msg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sourceWhere: Record<string, any> = { course_lesson_id: lessonId };
|
|
203
|
+
if (sourceLocaleId !== null && sourceLocaleId !== undefined) {
|
|
204
|
+
sourceWhere.locale_id = sourceLocaleId;
|
|
205
|
+
} else {
|
|
206
|
+
sourceWhere.locale_id = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sourceSegments = await (this.prismaService as any).course_lesson_transcription_segment.findMany({
|
|
210
|
+
where: sourceWhere,
|
|
211
|
+
orderBy: { start_seconds: 'asc' },
|
|
212
|
+
select: { id: true, start_seconds: true, end_seconds: true, text: true },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!sourceSegments.length) {
|
|
216
|
+
const msg = `Nenhum segmento de transcrição encontrado para a aula ${lessonId} no idioma fonte.`;
|
|
217
|
+
await emitProgress(msg, 100, false);
|
|
218
|
+
throw new NonRetryableError(msg);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await emitProgress(
|
|
222
|
+
`Iniciando tradução de ${sourceSegments.length} segmento(s) para ${targetLocale.name}...`,
|
|
223
|
+
10,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const targetLangName = targetLocale.name ?? targetLocale.code;
|
|
227
|
+
const translatedTexts: string[] = [];
|
|
228
|
+
const totalBatches = Math.ceil(sourceSegments.length / BATCH_SIZE);
|
|
229
|
+
let totalInputTokens = 0;
|
|
230
|
+
let totalOutputTokens = 0;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < sourceSegments.length; i += BATCH_SIZE) {
|
|
233
|
+
const batch = sourceSegments.slice(i, i + BATCH_SIZE);
|
|
234
|
+
const batchIndex = Math.floor(i / BATCH_SIZE) + 1;
|
|
235
|
+
|
|
236
|
+
await emitProgress(
|
|
237
|
+
`Traduzindo lote ${batchIndex}/${totalBatches}...`,
|
|
238
|
+
10 + Math.round((batchIndex / totalBatches) * 80),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const batchTexts: string[] = batch.map((s: any) => s.text);
|
|
242
|
+
const batchResult = await this.translateBatch(batchTexts, targetLangName, apiKey);
|
|
243
|
+
const batchTranslated = batchResult.translations;
|
|
244
|
+
totalInputTokens += batchResult.inputTokens;
|
|
245
|
+
totalOutputTokens += batchResult.outputTokens;
|
|
246
|
+
|
|
247
|
+
if (batchTranslated.length !== batchTexts.length) {
|
|
248
|
+
this.logger.warn(
|
|
249
|
+
`Batch ${batchIndex}: expected ${batchTexts.length} translations, got ${batchTranslated.length} — padding missing entries with source text`,
|
|
250
|
+
);
|
|
251
|
+
while (batchTranslated.length < batchTexts.length) {
|
|
252
|
+
batchTranslated.push(batchTexts[batchTranslated.length] ?? '');
|
|
253
|
+
}
|
|
254
|
+
batchTranslated.splice(batchTexts.length);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
translatedTexts.push(...batchTranslated);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await emitProgress('Salvando segmentos traduzidos...', 95);
|
|
261
|
+
|
|
262
|
+
const translatedSegments = sourceSegments.map((seg: any, idx: number) => ({
|
|
263
|
+
course_lesson_id: lessonId,
|
|
264
|
+
locale_id: targetLocaleId,
|
|
265
|
+
start_seconds: seg.start_seconds,
|
|
266
|
+
end_seconds: seg.end_seconds,
|
|
267
|
+
text: translatedTexts[idx] ?? seg.text,
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
await this.prismaService.$transaction([
|
|
271
|
+
(this.prismaService as any).course_lesson_transcription_segment.deleteMany({
|
|
272
|
+
where: { course_lesson_id: lessonId, locale_id: targetLocaleId },
|
|
273
|
+
}),
|
|
274
|
+
(this.prismaService as any).course_lesson_transcription_segment.createMany({
|
|
275
|
+
data: translatedSegments,
|
|
276
|
+
}),
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
// Record AI cost (gpt-4o-mini, billed per token).
|
|
280
|
+
await this.aiUsageService.recordChatUsage({
|
|
281
|
+
lessonId,
|
|
282
|
+
jobType: 'translation',
|
|
283
|
+
provider: 'openai',
|
|
284
|
+
model: 'gpt-4o-mini',
|
|
285
|
+
inputTokens: totalInputTokens,
|
|
286
|
+
outputTokens: totalOutputTokens,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const doneMsg = `Tradução concluída: ${translatedSegments.length} segmento(s) em ${targetLocale.name}.`;
|
|
290
|
+
this.logger.log(doneMsg);
|
|
291
|
+
await emitProgress(doneMsg, 100, true);
|
|
292
|
+
}
|
|
293
|
+
}
|