@hed-hog/lms 0.0.364 → 0.0.366

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