@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.
Files changed (169) hide show
  1. package/dist/certificate/certificate.controller.d.ts +1 -1
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +4 -2
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +50 -0
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +73 -0
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/course/course-ai-usage.service.d.ts +58 -0
  10. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  11. package/dist/course/course-ai-usage.service.js +176 -0
  12. package/dist/course/course-ai-usage.service.js.map +1 -0
  13. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  14. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  15. package/dist/course/course-audio-transcription.service.js +381 -29
  16. package/dist/course/course-audio-transcription.service.js.map +1 -1
  17. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  18. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  19. package/dist/course/course-export-scorm12.service.js +141 -6
  20. package/dist/course/course-export-scorm12.service.js.map +1 -1
  21. package/dist/course/course-export.service.d.ts.map +1 -1
  22. package/dist/course/course-export.service.js +2 -1
  23. package/dist/course/course-export.service.js.map +1 -1
  24. package/dist/course/course-lesson.controller.d.ts +25 -3
  25. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  26. package/dist/course/course-lesson.controller.js +71 -8
  27. package/dist/course/course-lesson.controller.js.map +1 -1
  28. package/dist/course/course-structure.controller.d.ts +26 -5
  29. package/dist/course/course-structure.controller.d.ts.map +1 -1
  30. package/dist/course/course-structure.controller.js +31 -1
  31. package/dist/course/course-structure.controller.js.map +1 -1
  32. package/dist/course/course-structure.service.d.ts +37 -5
  33. package/dist/course/course-structure.service.d.ts.map +1 -1
  34. package/dist/course/course-structure.service.js +165 -20
  35. package/dist/course/course-structure.service.js.map +1 -1
  36. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  37. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  38. package/dist/course/course-transcription-translation.service.js +227 -0
  39. package/dist/course/course-transcription-translation.service.js.map +1 -0
  40. package/dist/course/course-video-agent-pipeline.service.js +7 -7
  41. package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
  42. package/dist/course/course.module.d.ts.map +1 -1
  43. package/dist/course/course.module.js +4 -0
  44. package/dist/course/course.module.js.map +1 -1
  45. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  46. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  47. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  48. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  49. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  50. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  51. package/dist/course/dto/create-course-export.dto.js +6 -0
  52. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  53. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  54. package/dist/course/lms-bulk-upload-automation.service.js +26 -13
  55. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  56. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  57. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  58. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  59. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  60. package/dist/course/lms-bulk-upload.service.js +48 -29
  61. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  62. package/dist/course/subtitle.util.d.ts +46 -0
  63. package/dist/course/subtitle.util.d.ts.map +1 -0
  64. package/dist/course/subtitle.util.js +206 -0
  65. package/dist/course/subtitle.util.js.map +1 -0
  66. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  67. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  68. package/dist/enterprise/training/training-student.service.js +197 -10
  69. package/dist/enterprise/training/training-student.service.js.map +1 -1
  70. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  71. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  72. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  73. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  74. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  75. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  76. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  77. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  78. package/dist/lms.module.d.ts.map +1 -1
  79. package/dist/lms.module.js +4 -0
  80. package/dist/lms.module.js.map +1 -1
  81. package/dist/platforma/platforma-performance.service.js +121 -121
  82. package/dist/platforma/platforma-video.service.d.ts +8 -0
  83. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  84. package/dist/platforma/platforma-video.service.js +45 -2
  85. package/dist/platforma/platforma-video.service.js.map +1 -1
  86. package/dist/platforma/platforma.controller.d.ts +99 -1
  87. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  88. package/dist/platforma/platforma.controller.js +111 -2
  89. package/dist/platforma/platforma.controller.js.map +1 -1
  90. package/dist/training/dto/create-training.dto.d.ts +9 -0
  91. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  92. package/dist/training/dto/create-training.dto.js +45 -1
  93. package/dist/training/dto/create-training.dto.js.map +1 -1
  94. package/dist/training/training.controller.d.ts +144 -0
  95. package/dist/training/training.controller.d.ts.map +1 -1
  96. package/dist/training/training.service.d.ts +149 -0
  97. package/dist/training/training.service.d.ts.map +1 -1
  98. package/dist/training/training.service.js +332 -167
  99. package/dist/training/training.service.js.map +1 -1
  100. package/hedhog/data/image_type.yaml +10 -0
  101. package/hedhog/data/route.yaml +251 -0
  102. package/hedhog/data/setting_group.yaml +97 -0
  103. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  104. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  105. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  106. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  107. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  108. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  110. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  111. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  112. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  113. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  114. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  115. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  116. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  117. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  118. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  119. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  120. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  121. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  122. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  123. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  124. package/hedhog/frontend/app/courses/page.tsx.ejs +26 -4
  125. package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
  126. package/hedhog/frontend/messages/en.json +23 -12
  127. package/hedhog/frontend/messages/pt.json +23 -12
  128. package/hedhog/query/triggers.sql +33 -0
  129. package/hedhog/table/course_ai_usage.yaml +46 -0
  130. package/hedhog/table/course_lesson.yaml +3 -0
  131. package/hedhog/table/course_lesson_answer.yaml +37 -0
  132. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  133. package/hedhog/table/learning_path.yaml +6 -0
  134. package/hedhog/table/learning_path_module.yaml +22 -0
  135. package/hedhog/table/learning_path_step.yaml +9 -6
  136. package/hedhog/table/lesson_view_event.yaml +66 -66
  137. package/package.json +9 -9
  138. package/src/certificate/certificate.controller.ts +2 -0
  139. package/src/certificate/certificate.service.ts +99 -0
  140. package/src/course/course-ai-usage.service.ts +221 -0
  141. package/src/course/course-audio-transcription.service.ts +471 -43
  142. package/src/course/course-export-scorm12.service.ts +149 -5
  143. package/src/course/course-export.service.ts +1 -0
  144. package/src/course/course-lesson.controller.ts +59 -6
  145. package/src/course/course-structure.controller.ts +16 -0
  146. package/src/course/course-structure.service.ts +184 -10
  147. package/src/course/course-transcription-translation.service.ts +293 -0
  148. package/src/course/course-video-agent-pipeline.service.ts +471 -471
  149. package/src/course/course.module.ts +4 -0
  150. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  151. package/src/course/dto/create-course-export.dto.ts +6 -0
  152. package/src/course/ffmpeg.util.ts +65 -65
  153. package/src/course/lms-bulk-upload-automation.service.ts +29 -7
  154. package/src/course/lms-bulk-upload.service.ts +20 -1
  155. package/src/course/subtitle.util.ts +220 -0
  156. package/src/enterprise/training/training-student.service.ts +224 -4
  157. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  158. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  159. package/src/lms.module.ts +4 -0
  160. package/src/platforma/dto/heartbeat.dto.ts +30 -30
  161. package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
  162. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
  163. package/src/platforma/platforma-heartbeat.service.ts +33 -33
  164. package/src/platforma/platforma-performance.service.ts +606 -606
  165. package/src/platforma/platforma-search.service.ts +48 -48
  166. package/src/platforma/platforma-video.service.ts +59 -3
  167. package/src/platforma/platforma.controller.ts +88 -0
  168. package/src/training/dto/create-training.dto.ts +36 -0
  169. package/src/training/training.service.ts +360 -163
@@ -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
+ }