@hed-hog/lms 0.0.361 → 0.0.365
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/bitcode-wallet/bitcode-wallet.service.d.ts +66 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +91 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
- package/dist/class-group/class-group.controller.d.ts +16 -16
- package/dist/class-group/class-group.service.d.ts +12 -12
- package/dist/course/course-audio-transcription.service.d.ts +3 -2
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +49 -8
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-lesson.controller.d.ts +4 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +10 -0
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +24 -9
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +30 -3
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +25 -3
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +234 -24
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +8 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +87 -51
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course-video-hls.service.d.ts +57 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +767 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +115 -11
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +66 -28
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +13 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +112 -11
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +682 -72
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js +34 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
- package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +5 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.js +26 -0
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts +54 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.js +537 -0
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts +32 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.js +301 -0
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
- package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
- package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.constants.js +7 -0
- package/dist/course/lms-bulk-upload.constants.js.map +1 -0
- package/dist/course/lms-bulk-upload.controller.d.ts +144 -1
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +114 -4
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +153 -3
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +659 -21
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +6 -2
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +25 -8
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +20 -20
- package/dist/enterprise/enterprise.service.d.ts +20 -20
- package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
- package/dist/enterprise/training/training-admin.service.d.ts +11 -11
- package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
- package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
- package/dist/enterprise/training/training-student.controller.d.ts +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +52 -1
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
- package/dist/evaluation/evaluation.controller.d.ts +8 -8
- package/dist/evaluation/evaluation.service.d.ts +26 -8
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +30 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +440 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +21 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
- package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
- package/dist/platforma/dto/update-profile.dto.js +87 -0
- package/dist/platforma/dto/update-profile.dto.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +182 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +243 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/platforma/platforma.service.d.ts +27 -0
- package/dist/platforma/platforma.service.d.ts.map +1 -0
- package/dist/platforma/platforma.service.js +274 -0
- package/dist/platforma/platforma.service.js.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +56 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
- package/dist/student-xp/student-xp.controller.js +138 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -0
- package/dist/student-xp/student-xp.module.d.ts +3 -0
- package/dist/student-xp/student-xp.module.d.ts.map +1 -0
- package/dist/student-xp/student-xp.module.js +25 -0
- package/dist/student-xp/student-xp.module.js.map +1 -0
- package/dist/student-xp/student-xp.service.d.ts +81 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -0
- package/dist/student-xp/student-xp.service.js +247 -0
- package/dist/student-xp/student-xp.service.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
- package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.controller.js +105 -0
- package/dist/xp-catalog/xp-area.controller.js.map +1 -0
- package/dist/xp-catalog/xp-area.service.d.ts +35 -0
- package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.service.js +168 -0
- package/dist/xp-catalog/xp-area.service.js.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.js +29 -0
- package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
- package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.js +146 -0
- package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.js +113 -0
- package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
- package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
- package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.service.js +174 -0
- package/dist/xp-catalog/xp-skill.service.js.map +1 -0
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +91 -7
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +511 -29
- package/hedhog/data/setting_group.yaml +20 -20
- package/hedhog/data/xp_area.yaml +164 -0
- package/hedhog/data/xp_learning_type.yaml +131 -0
- package/hedhog/data/xp_skill.yaml +1834 -0
- package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1749 -0
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
- package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +73 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1172 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +87 -46
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +618 -480
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +672 -737
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +24 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +53 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +80 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
- package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
- package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +445 -230
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
- package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
- package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
- package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
- package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
- package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
- package/hedhog/frontend/messages/en.json +412 -31
- package/hedhog/frontend/messages/pt.json +412 -31
- package/hedhog/table/course_export.yaml +62 -0
- package/hedhog/table/lesson_xp_map.yaml +50 -0
- package/hedhog/table/lesson_xp_segment.yaml +40 -0
- package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
- package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
- package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
- package/hedhog/table/student_area_xp.yaml +30 -0
- package/hedhog/table/student_learning_type_xp.yaml +30 -0
- package/hedhog/table/student_skill_xp.yaml +30 -0
- package/hedhog/table/student_xp_event.yaml +34 -0
- package/hedhog/table/xp_area.yaml +39 -0
- package/hedhog/table/xp_learning_type.yaml +34 -0
- package/hedhog/table/xp_skill.yaml +39 -0
- package/package.json +13 -8
- package/src/bitcode-wallet/bitcode-wallet.service.ts +152 -0
- package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
- package/src/course/course-audio-transcription.service.ts +58 -21
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-lesson.controller.ts +6 -1
- package/src/course/course-structure.controller.ts +23 -1
- package/src/course/course-structure.service.ts +273 -7
- package/src/course/course-video-conversion.service.ts +113 -75
- package/src/course/course-video-hls.service.ts +946 -0
- package/src/course/course.controller.ts +54 -21
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +13 -0
- package/src/course/course.service.ts +906 -76
- package/src/course/dto/cleanup-course-storage.dto.ts +23 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
- package/src/course/dto/create-course-bulk-job.dto.ts +10 -0
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/lms-bulk-upload-automation.service.ts +707 -0
- package/src/course/lms-bulk-upload-infra.service.ts +360 -0
- package/src/course/lms-bulk-upload.constants.ts +5 -0
- package/src/course/lms-bulk-upload.controller.ts +110 -4
- package/src/course/lms-bulk-upload.service.ts +1092 -204
- package/src/course/lms-setting.controller.ts +26 -8
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
- package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +570 -0
- package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
- package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
- package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
- package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +21 -5
- package/src/platforma/dto/update-profile.dto.ts +59 -0
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +152 -3
- package/src/platforma/platforma.service.ts +268 -0
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +92 -0
- package/src/student-xp/student-xp.module.ts +12 -0
- package/src/student-xp/student-xp.service.ts +318 -0
- package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
- package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
- package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
- package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
- package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
- package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
- package/src/xp-catalog/xp-area.controller.ts +64 -0
- package/src/xp-catalog/xp-area.service.ts +196 -0
- package/src/xp-catalog/xp-catalog.module.ts +16 -0
- package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
- package/src/xp-catalog/xp-learning-type.service.ts +170 -0
- package/src/xp-catalog/xp-skill.controller.ts +71 -0
- package/src/xp-catalog/xp-skill.service.ts +205 -0
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var CourseVideoHlsService_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.CourseVideoHlsService = exports.LMS_VIDEO_HLS_JOB = void 0;
|
|
17
|
+
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
18
|
+
const core_1 = require("@hed-hog/core");
|
|
19
|
+
const queue_1 = require("@hed-hog/queue");
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
const fs_1 = require("fs");
|
|
23
|
+
const os_1 = require("os");
|
|
24
|
+
const path_1 = require("path");
|
|
25
|
+
const util_1 = require("util");
|
|
26
|
+
exports.LMS_VIDEO_HLS_JOB = 'lms.video.hls';
|
|
27
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
28
|
+
const HLS_LADDER_PRESETS = {
|
|
29
|
+
480: { width: 854, height: 480, videoBitrate: '1500k', audioBitrate: '96k', bandwidth: 1500000 },
|
|
30
|
+
720: { width: 1280, height: 720, videoBitrate: '2800k', audioBitrate: '128k', bandwidth: 2800000 },
|
|
31
|
+
1080: { width: 1920, height: 1080, videoBitrate: '5000k', audioBitrate: '192k', bandwidth: 5000000 },
|
|
32
|
+
};
|
|
33
|
+
let CourseVideoHlsService = CourseVideoHlsService_1 = class CourseVideoHlsService {
|
|
34
|
+
constructor(prisma, fileService, settingService, notificationService, registry, queueJob) {
|
|
35
|
+
this.prisma = prisma;
|
|
36
|
+
this.fileService = fileService;
|
|
37
|
+
this.settingService = settingService;
|
|
38
|
+
this.notificationService = notificationService;
|
|
39
|
+
this.registry = registry;
|
|
40
|
+
this.queueJob = queueJob;
|
|
41
|
+
this.logger = new common_1.Logger(CourseVideoHlsService_1.name);
|
|
42
|
+
}
|
|
43
|
+
onModuleInit() {
|
|
44
|
+
this.registry.register(exports.LMS_VIDEO_HLS_JOB, this);
|
|
45
|
+
this.logger.log(`Registered handler for "${exports.LMS_VIDEO_HLS_JOB}"`);
|
|
46
|
+
}
|
|
47
|
+
async enqueueHls(params) {
|
|
48
|
+
var _a, _b, _c;
|
|
49
|
+
const lesson = await this.prisma.course_lesson.findFirst({
|
|
50
|
+
where: {
|
|
51
|
+
id: params.lessonId,
|
|
52
|
+
course_module_id: params.sessionId,
|
|
53
|
+
course_module: { course_id: params.courseId },
|
|
54
|
+
},
|
|
55
|
+
include: { course_module: true },
|
|
56
|
+
});
|
|
57
|
+
if (!lesson)
|
|
58
|
+
throw new common_1.NotFoundException('Lesson not found for this course session');
|
|
59
|
+
const content = this.parseContent(lesson.content);
|
|
60
|
+
if (lesson.type !== 'video' || (content === null || content === void 0 ? void 0 : content.sourceType) !== 'video') {
|
|
61
|
+
throw new common_1.BadRequestException('HLS processing is only available for video lessons');
|
|
62
|
+
}
|
|
63
|
+
if ((content === null || content === void 0 ? void 0 : content.videoProvedor) !== 'file_storage') {
|
|
64
|
+
throw new common_1.BadRequestException('HLS processing requires File Storage provider');
|
|
65
|
+
}
|
|
66
|
+
const original = await this.prisma.file.findUnique({
|
|
67
|
+
where: { id: params.originalFileId },
|
|
68
|
+
include: { file_mimetype: true },
|
|
69
|
+
});
|
|
70
|
+
if (!original)
|
|
71
|
+
throw new common_1.NotFoundException('Original video file not found');
|
|
72
|
+
const mimetype = (_b = (_a = original.file_mimetype) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '';
|
|
73
|
+
if (!this.isVideoFile(mimetype, original.filename)) {
|
|
74
|
+
throw new common_1.BadRequestException('Original file must be a video');
|
|
75
|
+
}
|
|
76
|
+
await this.upsertLessonFile({
|
|
77
|
+
lessonId: params.lessonId,
|
|
78
|
+
type: 'video_original',
|
|
79
|
+
fileId: params.originalFileId,
|
|
80
|
+
title: (_c = original.filename) !== null && _c !== void 0 ? _c : `Original video ${params.originalFileId}`,
|
|
81
|
+
public: false,
|
|
82
|
+
overwrite: true,
|
|
83
|
+
});
|
|
84
|
+
const asyncNotification = await this.notificationService.create({
|
|
85
|
+
user_id: params.userId,
|
|
86
|
+
title: 'Geração de HLS em andamento',
|
|
87
|
+
body: 'Preparando pipeline de processamento HLS do vídeo...',
|
|
88
|
+
type: 'progress',
|
|
89
|
+
progress: 1,
|
|
90
|
+
started_at: new Date().toISOString(),
|
|
91
|
+
auto_remove: false,
|
|
92
|
+
action_type: 'url',
|
|
93
|
+
action_url: '/queue/jobs',
|
|
94
|
+
action_data: {
|
|
95
|
+
source: 'lms-video-hls',
|
|
96
|
+
courseId: params.courseId,
|
|
97
|
+
lessonId: params.lessonId,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const job = await this.queueJob.enqueue({
|
|
101
|
+
type: exports.LMS_VIDEO_HLS_JOB,
|
|
102
|
+
queueName: exports.LMS_VIDEO_HLS_JOB,
|
|
103
|
+
payload: {
|
|
104
|
+
courseId: params.courseId,
|
|
105
|
+
sessionId: params.sessionId,
|
|
106
|
+
lessonId: params.lessonId,
|
|
107
|
+
originalFileId: params.originalFileId,
|
|
108
|
+
notificationId: asyncNotification.id,
|
|
109
|
+
notificationUserId: params.userId,
|
|
110
|
+
},
|
|
111
|
+
sourceModule: 'lms',
|
|
112
|
+
sourceEntity: 'course_lesson',
|
|
113
|
+
sourceEntityId: String(params.lessonId),
|
|
114
|
+
maxAttempts: 3,
|
|
115
|
+
});
|
|
116
|
+
await this.prisma.notification.update({
|
|
117
|
+
where: { id: asyncNotification.id },
|
|
118
|
+
data: {
|
|
119
|
+
action_data: {
|
|
120
|
+
source: 'lms-video-hls',
|
|
121
|
+
courseId: params.courseId,
|
|
122
|
+
lessonId: params.lessonId,
|
|
123
|
+
queueJobId: job.id,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await this.prisma.course_lesson.update({
|
|
128
|
+
where: { id: params.lessonId },
|
|
129
|
+
data: {
|
|
130
|
+
content: JSON.stringify(Object.assign(Object.assign({}, (content !== null && content !== void 0 ? content : {})), { sourceType: 'video', videoConversionJobId: job.id })),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return { queueJobId: job.id, status: 'queued' };
|
|
134
|
+
}
|
|
135
|
+
async handle(job) {
|
|
136
|
+
var _a, _b, _c, _d;
|
|
137
|
+
const { courseId, sessionId, lessonId, originalFileId } = job.payload;
|
|
138
|
+
const notificationContext = Number.isInteger(Number((_a = job.payload) === null || _a === void 0 ? void 0 : _a.notificationId)) &&
|
|
139
|
+
Number.isInteger(Number((_b = job.payload) === null || _b === void 0 ? void 0 : _b.notificationUserId))
|
|
140
|
+
? { notificationId: Number(job.payload.notificationId), userId: Number(job.payload.notificationUserId) }
|
|
141
|
+
: undefined;
|
|
142
|
+
const emitProgress = async (message, metadata) => {
|
|
143
|
+
await this.createProgressEvent(job.id, message, metadata, notificationContext);
|
|
144
|
+
};
|
|
145
|
+
if (!courseId || !sessionId || !lessonId || !originalFileId) {
|
|
146
|
+
throw new Error('Invalid LMS HLS job payload');
|
|
147
|
+
}
|
|
148
|
+
this.logger.debug(`[HLS job=${job.id}] payload OK — courseId=${courseId} lessonId=${lessonId} originalFileId=${originalFileId} attempt=${job.attempts}/${job.max_attempts}`);
|
|
149
|
+
const maxInputBytes = this.getPositiveIntegerEnv('LMS_VIDEO_MAX_INPUT_BYTES');
|
|
150
|
+
const ffmpegTimeoutMs = (_c = this.getPositiveIntegerEnv('LMS_VIDEO_FFMPEG_TIMEOUT_MS')) !== null && _c !== void 0 ? _c : 1000 * 60 * 60 * 2;
|
|
151
|
+
this.logger.debug(`[HLS job=${job.id}] loading settings...`);
|
|
152
|
+
const settings = await this.settingService.getSettingValues([
|
|
153
|
+
'lms-hls-resolutions',
|
|
154
|
+
'lms-hls-segment-duration',
|
|
155
|
+
'lms-video-frame-capture-interval-seconds',
|
|
156
|
+
'lms-image-extraction-enabled',
|
|
157
|
+
'lms-audio-transcription-enabled',
|
|
158
|
+
]);
|
|
159
|
+
this.logger.debug(`[HLS job=${job.id}] settings loaded — resolutions=${settings['lms-hls-resolutions']} segmentDuration=${settings['lms-hls-segment-duration']}`);
|
|
160
|
+
const hlsResolutions = this.parseResolutions(settings['lms-hls-resolutions']);
|
|
161
|
+
const segmentDuration = Math.max(2, Number(settings['lms-hls-segment-duration'] || 6));
|
|
162
|
+
const imageExtractionEnabled = settings['lms-image-extraction-enabled'] !== false;
|
|
163
|
+
const transcriptionEnabled = settings['lms-audio-transcription-enabled'] !== false;
|
|
164
|
+
const frameIntervalSeconds = this.resolveFrameIntervalSeconds(settings['lms-video-frame-capture-interval-seconds']);
|
|
165
|
+
this.logger.debug(`[HLS job=${job.id}] resolutions=${hlsResolutions.join(',')} segmentDuration=${segmentDuration}s imageExtraction=${imageExtractionEnabled} transcription=${transcriptionEnabled}`);
|
|
166
|
+
const workDir = await fs_1.promises.mkdtemp((0, path_1.join)((0, os_1.tmpdir)(), `lms-hls-${job.id}-`));
|
|
167
|
+
const inputPath = (0, path_1.join)(workDir, `original-${originalFileId}.mp4`);
|
|
168
|
+
const hlsOutDir = (0, path_1.join)(workDir, 'hls');
|
|
169
|
+
await fs_1.promises.mkdir(hlsOutDir, { recursive: true });
|
|
170
|
+
this.logger.debug(`[HLS job=${job.id}] workDir=${workDir}`);
|
|
171
|
+
const jobStartedAt = Date.now();
|
|
172
|
+
this.logger.log(`Queue job ${job.id}: starting HLS generation (lesson=${lessonId})`);
|
|
173
|
+
await emitProgress('Baixando vídeo original...', { phase: 'download_original', lessonId });
|
|
174
|
+
this.logger.debug(`[HLS job=${job.id}] downloading original file ${originalFileId} to ${inputPath}...`);
|
|
175
|
+
const downloaded = await this.fileService.downloadToPath(originalFileId, inputPath, maxInputBytes ? { maxBytes: maxInputBytes } : undefined);
|
|
176
|
+
const inputSizeMb = parseFloat((downloaded.size / (1024 * 1024)).toFixed(2));
|
|
177
|
+
this.logger.debug(`[HLS job=${job.id}] download complete — ${inputSizeMb} MB in ${Date.now() - jobStartedAt}ms`);
|
|
178
|
+
await emitProgress(`Vídeo baixado (${inputSizeMb} MB). Lendo dimensões e duração...`, {
|
|
179
|
+
phase: 'probe_duration',
|
|
180
|
+
lessonId,
|
|
181
|
+
inputSizeMb,
|
|
182
|
+
});
|
|
183
|
+
let probedDuration = null;
|
|
184
|
+
try {
|
|
185
|
+
this.logger.debug(`[HLS job=${job.id}] probing duration...`);
|
|
186
|
+
probedDuration = await this.probeDurationSeconds(inputPath, ffmpegTimeoutMs);
|
|
187
|
+
this.logger.debug(`[HLS job=${job.id}] duration=${probedDuration}s`);
|
|
188
|
+
if (probedDuration !== null && probedDuration > 0) {
|
|
189
|
+
await this.prisma.course_lesson.update({
|
|
190
|
+
where: { id: lessonId },
|
|
191
|
+
data: { duration_seconds: probedDuration },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
this.logger.warn(`Queue job ${job.id}: failed to probe duration: ${(_d = err === null || err === void 0 ? void 0 : err.message) !== null && _d !== void 0 ? _d : err}`);
|
|
197
|
+
}
|
|
198
|
+
this.logger.debug(`[HLS job=${job.id}] probing source height...`);
|
|
199
|
+
const sourceHeight = await this.probeVideoHeight(inputPath);
|
|
200
|
+
this.logger.debug(`[HLS job=${job.id}] sourceHeight=${sourceHeight !== null && sourceHeight !== void 0 ? sourceHeight : 'unknown'}`);
|
|
201
|
+
const variants = this.buildHlsVariants(hlsResolutions, sourceHeight);
|
|
202
|
+
if (variants.length === 0) {
|
|
203
|
+
throw new Error(`No suitable HLS variants for source height ${sourceHeight !== null && sourceHeight !== void 0 ? sourceHeight : 'unknown'}`);
|
|
204
|
+
}
|
|
205
|
+
this.logger.log(`Queue job ${job.id}: building HLS with variants: ${variants.map((v) => v.height + 'p').join(', ')}`);
|
|
206
|
+
this.logger.debug(`[HLS job=${job.id}] ffmpeg encode starting — variants=${variants.map((v) => `${v.height}p@${v.videoBitrate}`).join(', ')} segmentDuration=${segmentDuration}s outDir=${hlsOutDir}`);
|
|
207
|
+
await emitProgress(`Gerando HLS (${variants.map((v) => v.height + 'p').join(', ')})...`, {
|
|
208
|
+
phase: 'hls_encode',
|
|
209
|
+
lessonId,
|
|
210
|
+
variants: variants.map((v) => v.height),
|
|
211
|
+
});
|
|
212
|
+
const encodeStart = Date.now();
|
|
213
|
+
await this.generateHls({ inputPath, outDir: hlsOutDir, variants, segmentDuration, timeoutMs: ffmpegTimeoutMs });
|
|
214
|
+
this.logger.debug(`[HLS job=${job.id}] ffmpeg encode complete in ${Date.now() - encodeStart}ms`);
|
|
215
|
+
// Count generated segments to log
|
|
216
|
+
let totalSegmentCount = 0;
|
|
217
|
+
for (const v of variants) {
|
|
218
|
+
const variantDir = (0, path_1.join)(hlsOutDir, `stream_${v.variantIndex}`);
|
|
219
|
+
const files = (await fs_1.promises.readdir(variantDir)).filter((f) => f.endsWith('.ts'));
|
|
220
|
+
this.logger.debug(`[HLS job=${job.id}] variant stream_${v.variantIndex} (${v.height}p): ${files.length} segments`);
|
|
221
|
+
totalSegmentCount += files.length;
|
|
222
|
+
}
|
|
223
|
+
this.logger.debug(`[HLS job=${job.id}] total segments to upload: ${totalSegmentCount} across ${variants.length} variants`);
|
|
224
|
+
await emitProgress('HLS gerado. Enviando arquivos para storage...', {
|
|
225
|
+
phase: 'hls_upload',
|
|
226
|
+
lessonId,
|
|
227
|
+
});
|
|
228
|
+
this.logger.debug(`[HLS job=${job.id}] starting upload of ${totalSegmentCount} segments + ${variants.length} playlists + 1 master`);
|
|
229
|
+
const uploadStart = Date.now();
|
|
230
|
+
const masterManifestFileId = await this.uploadHlsDirectory({
|
|
231
|
+
lessonId,
|
|
232
|
+
hlsOutDir,
|
|
233
|
+
variants,
|
|
234
|
+
emitProgress,
|
|
235
|
+
});
|
|
236
|
+
this.logger.debug(`[HLS job=${job.id}] upload complete in ${Date.now() - uploadStart}ms — masterFileId=${masterManifestFileId}`);
|
|
237
|
+
this.logger.debug(`[HLS job=${job.id}] upserting video_hls lesson file...`);
|
|
238
|
+
await this.upsertLessonFile({
|
|
239
|
+
lessonId,
|
|
240
|
+
type: 'video_hls',
|
|
241
|
+
fileId: masterManifestFileId,
|
|
242
|
+
title: 'HLS Stream',
|
|
243
|
+
public: false,
|
|
244
|
+
overwrite: true,
|
|
245
|
+
});
|
|
246
|
+
this.logger.log(`Queue job ${job.id}: HLS uploaded for lesson ${lessonId} (masterFileId=${masterManifestFileId})`);
|
|
247
|
+
let frameResults = [];
|
|
248
|
+
try {
|
|
249
|
+
if (imageExtractionEnabled) {
|
|
250
|
+
this.logger.debug(`[HLS job=${job.id}] starting frame extraction — interval=${frameIntervalSeconds}s`);
|
|
251
|
+
await emitProgress('Extraindo imagens do vídeo...', { phase: 'extract_frames', lessonId });
|
|
252
|
+
const frameStart = Date.now();
|
|
253
|
+
frameResults = await this.extractAndUploadLessonFrames({
|
|
254
|
+
jobId: job.id,
|
|
255
|
+
lessonId,
|
|
256
|
+
inputPath,
|
|
257
|
+
workDir,
|
|
258
|
+
intervalSeconds: frameIntervalSeconds,
|
|
259
|
+
timeoutMs: ffmpegTimeoutMs,
|
|
260
|
+
});
|
|
261
|
+
this.logger.debug(`[HLS job=${job.id}] frame extraction done — ${frameResults.length} frames in ${Date.now() - frameStart}ms`);
|
|
262
|
+
await emitProgress(frameResults.length > 0
|
|
263
|
+
? `Imagens extraídas com sucesso (${frameResults.length}).`
|
|
264
|
+
: 'Nenhuma imagem foi extraída do vídeo.', { phase: 'extract_frames_done', lessonId, frames: frameResults.length });
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.logger.debug(`[HLS job=${job.id}] frame extraction disabled — skipping`);
|
|
268
|
+
}
|
|
269
|
+
if (transcriptionEnabled) {
|
|
270
|
+
this.logger.debug(`[HLS job=${job.id}] starting audio extraction for transcription`);
|
|
271
|
+
await emitProgress('Extraindo áudio do vídeo...', { phase: 'extract_audio', lessonId });
|
|
272
|
+
const audioFileId = await this.extractAndUploadLessonAudio({
|
|
273
|
+
courseId,
|
|
274
|
+
lessonId,
|
|
275
|
+
localInputPath: inputPath,
|
|
276
|
+
tempDir: workDir,
|
|
277
|
+
});
|
|
278
|
+
if (audioFileId) {
|
|
279
|
+
this.logger.debug(`[HLS job=${job.id}] audio extracted — fileId=${audioFileId}, enqueuing transcription`);
|
|
280
|
+
await emitProgress('Áudio extraído. Agendando transcrição...', {
|
|
281
|
+
phase: 'queue_transcription',
|
|
282
|
+
lessonId,
|
|
283
|
+
audioFileId,
|
|
284
|
+
});
|
|
285
|
+
const transcriptionJob = await this.queueJob.enqueue({
|
|
286
|
+
type: 'lms.audio.transcribe',
|
|
287
|
+
queueName: 'lms.audio.transcribe',
|
|
288
|
+
payload: {
|
|
289
|
+
courseId,
|
|
290
|
+
sessionId,
|
|
291
|
+
lessonId,
|
|
292
|
+
audioFileId,
|
|
293
|
+
parentJobId: job.id,
|
|
294
|
+
notificationId: notificationContext === null || notificationContext === void 0 ? void 0 : notificationContext.notificationId,
|
|
295
|
+
notificationUserId: notificationContext === null || notificationContext === void 0 ? void 0 : notificationContext.userId,
|
|
296
|
+
},
|
|
297
|
+
maxAttempts: 3,
|
|
298
|
+
});
|
|
299
|
+
await emitProgress(`Transcrição agendada no job #${transcriptionJob.id}.`, {
|
|
300
|
+
phase: 'queue_transcription_done',
|
|
301
|
+
lessonId,
|
|
302
|
+
transcriptionJobId: transcriptionJob.id,
|
|
303
|
+
});
|
|
304
|
+
this.logger.debug(`[HLS job=${job.id}] transcription job enqueued — transcriptionJobId=${transcriptionJob.id}`);
|
|
305
|
+
const totalMs = Date.now() - jobStartedAt;
|
|
306
|
+
this.logger.log(`[HLS job=${job.id}] pipeline complete in ${totalMs}ms — lessonId=${lessonId} masterFileId=${masterManifestFileId} frames=${frameResults.length} transcriptionJob=${transcriptionJob.id}`);
|
|
307
|
+
return {
|
|
308
|
+
lessonId,
|
|
309
|
+
probedDurationSeconds: probedDuration,
|
|
310
|
+
masterManifestFileId,
|
|
311
|
+
variants: variants.length,
|
|
312
|
+
extractedFrames: frameResults.length,
|
|
313
|
+
transcriptionJobId: transcriptionJob.id,
|
|
314
|
+
totalMs,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
this.logger.debug(`[HLS job=${job.id}] audio extraction returned no file — skipping transcription`);
|
|
319
|
+
await emitProgress('Áudio não disponível. Encerrando pipeline sem transcrição.', {
|
|
320
|
+
phase: 'queue_transcription_skipped',
|
|
321
|
+
lessonId,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
this.logger.error(`[HLS job=${job.id}] pipeline FAILED after ${Date.now() - jobStartedAt}ms — ${error instanceof Error ? error.stack : String(error)}`);
|
|
328
|
+
if (notificationContext) {
|
|
329
|
+
const message = error instanceof Error ? error.message : 'Falha no pipeline HLS.';
|
|
330
|
+
await this.updateAsyncNotification(notificationContext, 100, `Falha: ${message}`, false);
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
this.logger.debug(`[HLS job=${job.id}] cleaning workDir ${workDir}`);
|
|
336
|
+
await fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
337
|
+
this.logger.log(`Queue job ${job.id}: cleaned temporary folder ${workDir}`);
|
|
338
|
+
}
|
|
339
|
+
if (notificationContext) {
|
|
340
|
+
await this.updateAsyncNotification(notificationContext, 100, 'HLS gerado com sucesso.', true);
|
|
341
|
+
}
|
|
342
|
+
const totalMs = Date.now() - jobStartedAt;
|
|
343
|
+
this.logger.log(`[HLS job=${job.id}] pipeline complete in ${totalMs}ms — lessonId=${lessonId} masterFileId=${masterManifestFileId} frames=${frameResults.length} transcription=skipped`);
|
|
344
|
+
return {
|
|
345
|
+
lessonId,
|
|
346
|
+
probedDurationSeconds: probedDuration,
|
|
347
|
+
masterManifestFileId,
|
|
348
|
+
variants: variants.length,
|
|
349
|
+
extractedFrames: frameResults.length,
|
|
350
|
+
transcriptionJobId: null,
|
|
351
|
+
totalMs,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// HLS Generation
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
async generateHls(params) {
|
|
358
|
+
const { inputPath, outDir, variants, segmentDuration, timeoutMs } = params;
|
|
359
|
+
const ffmpegCmd = this.getFfmpegCommand();
|
|
360
|
+
// Create output directories for each variant
|
|
361
|
+
for (const v of variants) {
|
|
362
|
+
await fs_1.promises.mkdir((0, path_1.join)(outDir, `stream_${v.variantIndex}`), { recursive: true });
|
|
363
|
+
}
|
|
364
|
+
const args = ['-y', '-i', inputPath];
|
|
365
|
+
if (variants.length > 1) {
|
|
366
|
+
args.push('-filter_complex', `[0:v]split=${variants.length}${variants.map((v) => `[v${v.variantIndex}]`).join('')}`);
|
|
367
|
+
for (const v of variants) {
|
|
368
|
+
args.push(`-map`, `[v${v.variantIndex}]`, `-map`, `0:a:0`, `-c:v:${v.variantIndex}`, `libx264`, `-b:v:${v.variantIndex}`, v.videoBitrate, `-s:v:${v.variantIndex}`, `${v.width}x${v.height}`, `-c:a:${v.variantIndex}`, `aac`, `-b:a:${v.variantIndex}`, v.audioBitrate);
|
|
369
|
+
}
|
|
370
|
+
args.push('-var_stream_map', variants.map((v) => `v:${v.variantIndex},a:${v.variantIndex}`).join(' '));
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const v = variants[0];
|
|
374
|
+
args.push(`-c:v`, `libx264`, `-b:v`, v.videoBitrate, `-s`, `${v.width}x${v.height}`, `-c:a`, `aac`, `-b:a`, v.audioBitrate, `-var_stream_map`, `v:0,a:0`);
|
|
375
|
+
}
|
|
376
|
+
args.push(`-master_pl_name`, `master.m3u8`, `-f`, `hls`, `-hls_time`, String(segmentDuration), `-hls_list_size`, `0`, `-hls_segment_filename`, (0, path_1.join)(outDir, 'stream_%v', 'seg%03d.ts'), (0, path_1.join)(outDir, 'stream_%v', 'playlist.m3u8'));
|
|
377
|
+
try {
|
|
378
|
+
await execFileAsync(ffmpegCmd, args, {
|
|
379
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
380
|
+
timeout: timeoutMs,
|
|
381
|
+
windowsHide: true,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === 'ENOENT') {
|
|
386
|
+
throw new Error(`FFmpeg binary not found at "${ffmpegCmd}"`);
|
|
387
|
+
}
|
|
388
|
+
if ((error === null || error === void 0 ? void 0 : error.killed) && (error === null || error === void 0 ? void 0 : error.signal) === 'SIGTERM') {
|
|
389
|
+
throw new Error(`FFmpeg HLS generation timed out after ${timeoutMs}ms`);
|
|
390
|
+
}
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async uploadHlsDirectory(params) {
|
|
395
|
+
const { lessonId, hlsOutDir, variants, emitProgress } = params;
|
|
396
|
+
// Upload all segment files in batches
|
|
397
|
+
const BATCH_SIZE = 20;
|
|
398
|
+
const segmentUploads = [];
|
|
399
|
+
for (const v of variants) {
|
|
400
|
+
const variantDir = (0, path_1.join)(hlsOutDir, `stream_${v.variantIndex}`);
|
|
401
|
+
const files = await fs_1.promises.readdir(variantDir);
|
|
402
|
+
for (const filename of files) {
|
|
403
|
+
if (filename === 'playlist.m3u8')
|
|
404
|
+
continue; // upload manifests separately
|
|
405
|
+
segmentUploads.push({
|
|
406
|
+
localPath: (0, path_1.join)(variantDir, filename),
|
|
407
|
+
location: `lms/lessons/hls/${lessonId}/stream_${v.variantIndex}`,
|
|
408
|
+
filename,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const totalBatches = Math.ceil(segmentUploads.length / BATCH_SIZE);
|
|
413
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] ${segmentUploads.length} segments → ${totalBatches} batches of ${BATCH_SIZE}`);
|
|
414
|
+
for (let i = 0; i < segmentUploads.length; i += BATCH_SIZE) {
|
|
415
|
+
const batch = segmentUploads.slice(i, i + BATCH_SIZE);
|
|
416
|
+
const batchIndex = Math.floor(i / BATCH_SIZE) + 1;
|
|
417
|
+
const batchStart = Date.now();
|
|
418
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] batch ${batchIndex}/${totalBatches} — uploading ${batch.map((s) => s.filename).join(', ')}`);
|
|
419
|
+
await Promise.all(batch.map((seg) => this.fileService.uploadFromPath(seg.location, seg.localPath, {
|
|
420
|
+
originalname: seg.filename,
|
|
421
|
+
mimetype: 'video/MP2T',
|
|
422
|
+
})));
|
|
423
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] batch ${batchIndex}/${totalBatches} done in ${Date.now() - batchStart}ms`);
|
|
424
|
+
await emitProgress(`Enviando segmentos HLS... (${batchIndex}/${totalBatches})`, { phase: 'hls_upload', lessonId, batchIndex, totalBatches });
|
|
425
|
+
}
|
|
426
|
+
// Upload variant playlists
|
|
427
|
+
for (const v of variants) {
|
|
428
|
+
const playlistPath = (0, path_1.join)(hlsOutDir, `stream_${v.variantIndex}`, 'playlist.m3u8');
|
|
429
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] uploading playlist stream_${v.variantIndex}/playlist.m3u8`);
|
|
430
|
+
await this.fileService.uploadFromPath(`lms/lessons/hls/${lessonId}/stream_${v.variantIndex}`, playlistPath, { originalname: 'playlist.m3u8', mimetype: 'application/x-mpegURL' });
|
|
431
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] playlist stream_${v.variantIndex} uploaded`);
|
|
432
|
+
}
|
|
433
|
+
// Upload master manifest last, keep its file ID
|
|
434
|
+
const masterPath = (0, path_1.join)(hlsOutDir, 'master.m3u8');
|
|
435
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] uploading master.m3u8`);
|
|
436
|
+
const masterFile = await this.fileService.uploadFromPath(`lms/lessons/hls/${lessonId}`, masterPath, { originalname: 'master.m3u8', mimetype: 'application/x-mpegURL' });
|
|
437
|
+
this.logger.debug(`[HLS upload lessonId=${lessonId}] master.m3u8 uploaded — fileId=${masterFile.id}`);
|
|
438
|
+
return masterFile.id;
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Ladder helpers
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
parseResolutions(raw) {
|
|
444
|
+
const str = typeof raw === 'string' ? raw : '480,720,1080';
|
|
445
|
+
const parsed = str
|
|
446
|
+
.split(',')
|
|
447
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
448
|
+
.filter((n) => !isNaN(n) && HLS_LADDER_PRESETS[n] !== undefined);
|
|
449
|
+
return parsed.length > 0 ? [...new Set(parsed)].sort((a, b) => a - b) : [480, 720, 1080];
|
|
450
|
+
}
|
|
451
|
+
buildHlsVariants(resolutions, sourceHeight) {
|
|
452
|
+
const effective = resolutions.filter((h) => sourceHeight === null || h <= sourceHeight);
|
|
453
|
+
// Always include at least the lowest quality or the source if it's below all configured resolutions
|
|
454
|
+
if (effective.length === 0 && sourceHeight !== null) {
|
|
455
|
+
const closest = resolutions.reduce((prev, curr) => Math.abs(curr - sourceHeight) < Math.abs(prev - sourceHeight) ? curr : prev);
|
|
456
|
+
effective.push(closest);
|
|
457
|
+
}
|
|
458
|
+
return effective.map((height, idx) => {
|
|
459
|
+
const preset = HLS_LADDER_PRESETS[height];
|
|
460
|
+
return Object.assign({ variantIndex: idx, height }, preset);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// FFprobe helpers
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
async probeVideoHeight(inputPath) {
|
|
467
|
+
const ffprobeCmd = this.getFfprobeCommand();
|
|
468
|
+
try {
|
|
469
|
+
const { stdout } = await execFileAsync(ffprobeCmd, ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=height', '-of', 'default=noprint_wrappers=1:nokey=1', inputPath], { maxBuffer: 1024 * 1024, timeout: 30000, windowsHide: true });
|
|
470
|
+
const h = parseInt(stdout.trim(), 10);
|
|
471
|
+
return Number.isFinite(h) && h > 0 ? h : null;
|
|
472
|
+
}
|
|
473
|
+
catch (_a) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async probeDurationSeconds(inputPath, timeoutMs) {
|
|
478
|
+
const ffprobeCmd = this.getFfprobeCommand();
|
|
479
|
+
try {
|
|
480
|
+
const { stdout } = await execFileAsync(ffprobeCmd, ['-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', inputPath], { maxBuffer: 1024 * 1024, timeout: Math.min(timeoutMs, 30000), windowsHide: true });
|
|
481
|
+
const parsed = parseFloat(stdout.trim());
|
|
482
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : null;
|
|
483
|
+
}
|
|
484
|
+
catch (_a) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Frame extraction (same logic as CourseVideoConversionService)
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
async extractAndUploadLessonFrames(params) {
|
|
492
|
+
const intervalSeconds = Math.max(1, Math.floor(params.intervalSeconds));
|
|
493
|
+
const framesDir = (0, path_1.join)(params.workDir, 'frames');
|
|
494
|
+
await fs_1.promises.mkdir(framesDir, { recursive: true });
|
|
495
|
+
const pattern = (0, path_1.join)(framesDir, 'frame-%08d.jpg');
|
|
496
|
+
const ffmpegCmd = this.getFfmpegCommand();
|
|
497
|
+
try {
|
|
498
|
+
await execFileAsync(ffmpegCmd, ['-y', '-i', params.inputPath, '-vf', `fps=1/${intervalSeconds}`, '-q:v', '3', '-qmin', '2', '-qmax', '4', pattern], { maxBuffer: 1024 * 1024 * 20, timeout: params.timeoutMs, windowsHide: true });
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === 'ENOENT')
|
|
502
|
+
throw new Error(`FFmpeg binary not found`);
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
const frameFiles = (await fs_1.promises.readdir(framesDir))
|
|
506
|
+
.filter((n) => n.toLowerCase().endsWith('.jpg'))
|
|
507
|
+
.sort((a, b) => a.localeCompare(b));
|
|
508
|
+
if (frameFiles.length === 0) {
|
|
509
|
+
await this.prisma.course_lesson_video_frame.deleteMany({ where: { course_lesson_id: params.lessonId } });
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
const uploads = [];
|
|
513
|
+
for (let i = 0; i < frameFiles.length; i++) {
|
|
514
|
+
const fileName = frameFiles[i];
|
|
515
|
+
if (!fileName)
|
|
516
|
+
continue;
|
|
517
|
+
const framePath = (0, path_1.join)(framesDir, fileName);
|
|
518
|
+
const stats = await fs_1.promises.stat(framePath);
|
|
519
|
+
const uploaded = await this.fileService.uploadFromPath('lms/lessons/frames', framePath, {
|
|
520
|
+
originalname: fileName,
|
|
521
|
+
mimetype: 'image/jpeg',
|
|
522
|
+
});
|
|
523
|
+
uploads.push({ fileId: uploaded.id, timeSeconds: i * intervalSeconds, outputBytes: stats.size });
|
|
524
|
+
}
|
|
525
|
+
await this.prisma.course_lesson_video_frame.deleteMany({ where: { course_lesson_id: params.lessonId } });
|
|
526
|
+
await this.prisma.course_lesson_video_frame.createMany({
|
|
527
|
+
data: uploads.map((item) => ({
|
|
528
|
+
course_lesson_id: params.lessonId,
|
|
529
|
+
file_id: item.fileId,
|
|
530
|
+
time_seconds: item.timeSeconds,
|
|
531
|
+
})),
|
|
532
|
+
});
|
|
533
|
+
return uploads;
|
|
534
|
+
}
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// Audio extraction (same logic as CourseVideoConversionService)
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
async extractAndUploadLessonAudio(params) {
|
|
539
|
+
var _a, _b;
|
|
540
|
+
const ffmpegCmd = this.getFfmpegCommand();
|
|
541
|
+
const mp3Path = (0, path_1.join)(params.tempDir, `lesson_${params.lessonId}_audio.mp3`);
|
|
542
|
+
try {
|
|
543
|
+
await execFileAsync(ffmpegCmd, ['-y', '-i', params.localInputPath, '-vn', '-ar', '16000', '-ac', '1', '-c:a', 'libmp3lame', '-b:a', '32k', mp3Path], { maxBuffer: 1024 * 1024 * 20, windowsHide: true });
|
|
544
|
+
}
|
|
545
|
+
catch (_c) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const course = await this.prisma.course.findUnique({
|
|
550
|
+
where: { id: params.courseId },
|
|
551
|
+
select: { locale_id: true },
|
|
552
|
+
});
|
|
553
|
+
const uploaded = await this.fileService.uploadFromPath('lms/lessons/audio', mp3Path, {
|
|
554
|
+
originalname: (0, path_1.basename)(mp3Path),
|
|
555
|
+
mimetype: 'audio/mp3',
|
|
556
|
+
});
|
|
557
|
+
const defaultLocale = await this.prisma.locale.findFirst({
|
|
558
|
+
where: { OR: [{ code: 'pt-BR' }, { code: 'pt' }] },
|
|
559
|
+
select: { id: true },
|
|
560
|
+
orderBy: { id: 'asc' },
|
|
561
|
+
});
|
|
562
|
+
const resolvedLocaleId = (_b = (_a = course === null || course === void 0 ? void 0 : course.locale_id) !== null && _a !== void 0 ? _a : defaultLocale === null || defaultLocale === void 0 ? void 0 : defaultLocale.id) !== null && _b !== void 0 ? _b : null;
|
|
563
|
+
const existing = await this.prisma.course_lesson_file.findFirst({
|
|
564
|
+
where: { course_lesson_id: params.lessonId, type: 'lesson_audio' },
|
|
565
|
+
select: { id: true },
|
|
566
|
+
});
|
|
567
|
+
if (existing) {
|
|
568
|
+
await this.prisma.course_lesson_file.update({
|
|
569
|
+
where: { id: existing.id },
|
|
570
|
+
data: { file_id: uploaded.id, locale_id: resolvedLocaleId, title: 'Audio Original.mp3', type: 'lesson_audio', is_public: false },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
await this.prisma.course_lesson_file.create({
|
|
575
|
+
data: {
|
|
576
|
+
course_lesson_id: params.lessonId,
|
|
577
|
+
file_id: uploaded.id,
|
|
578
|
+
title: 'Audio Original.mp3',
|
|
579
|
+
type: 'lesson_audio',
|
|
580
|
+
is_public: false,
|
|
581
|
+
locale_id: resolvedLocaleId,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return uploaded.id;
|
|
586
|
+
}
|
|
587
|
+
catch (_d) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
await fs_1.promises.rm(mp3Path, { force: true }).catch(() => undefined);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
// Shared helpers
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
async upsertLessonFile(params) {
|
|
598
|
+
if (params.overwrite) {
|
|
599
|
+
await this.prisma.course_lesson_file.deleteMany({
|
|
600
|
+
where: { course_lesson_id: params.lessonId, type: params.type },
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return this.prisma.course_lesson_file.create({
|
|
604
|
+
data: {
|
|
605
|
+
course_lesson_id: params.lessonId,
|
|
606
|
+
file_id: params.fileId,
|
|
607
|
+
title: params.title,
|
|
608
|
+
type: params.type,
|
|
609
|
+
is_public: params.public,
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async createProgressEvent(queueJobId, message, metadata, notificationContext) {
|
|
614
|
+
try {
|
|
615
|
+
await this.prisma.queue_job_event.create({
|
|
616
|
+
data: Object.assign({ queue_job_id: queueJobId, event_type: 'started', message }, (metadata ? { metadata } : {})),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
this.logger.warn(`Failed to persist progress event: ${error instanceof Error ? error.message : 'unknown'}`);
|
|
621
|
+
}
|
|
622
|
+
if (notificationContext) {
|
|
623
|
+
const progress = this.resolveNotificationProgress(metadata);
|
|
624
|
+
await this.updateAsyncNotification(notificationContext, progress, message);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
resolveNotificationProgress(metadata) {
|
|
628
|
+
var _a;
|
|
629
|
+
const phase = String((_a = metadata === null || metadata === void 0 ? void 0 : metadata.phase) !== null && _a !== void 0 ? _a : '');
|
|
630
|
+
if (phase === 'download_original')
|
|
631
|
+
return 5;
|
|
632
|
+
if (phase === 'probe_duration')
|
|
633
|
+
return 12;
|
|
634
|
+
if (phase === 'hls_encode')
|
|
635
|
+
return 20;
|
|
636
|
+
if (phase === 'hls_upload')
|
|
637
|
+
return 65;
|
|
638
|
+
if (phase === 'extract_frames')
|
|
639
|
+
return 72;
|
|
640
|
+
if (phase === 'extract_frames_done')
|
|
641
|
+
return 78;
|
|
642
|
+
if (phase === 'extract_audio')
|
|
643
|
+
return 83;
|
|
644
|
+
if (phase === 'queue_transcription')
|
|
645
|
+
return 90;
|
|
646
|
+
if (phase === 'queue_transcription_done')
|
|
647
|
+
return 92;
|
|
648
|
+
if (phase === 'queue_transcription_skipped')
|
|
649
|
+
return 100;
|
|
650
|
+
return 15;
|
|
651
|
+
}
|
|
652
|
+
async updateAsyncNotification(context, progress, body, success) {
|
|
653
|
+
try {
|
|
654
|
+
await this.notificationService.updateProgress(context.userId, context.notificationId, Object.assign({ progress,
|
|
655
|
+
body }, (success != null ? { success } : {})));
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
this.logger.warn(`Failed to update notification ${context.notificationId}: ${error instanceof Error ? error.message : 'unknown'}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
parseContent(content) {
|
|
662
|
+
if (!content)
|
|
663
|
+
return null;
|
|
664
|
+
try {
|
|
665
|
+
return JSON.parse(content);
|
|
666
|
+
}
|
|
667
|
+
catch (_a) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
isVideoFile(mimetype, filename) {
|
|
672
|
+
var _a, _b;
|
|
673
|
+
if (mimetype.startsWith('video/'))
|
|
674
|
+
return true;
|
|
675
|
+
const ext = (_b = (_a = (filename !== null && filename !== void 0 ? filename : '').split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
|
|
676
|
+
return ['mp4', 'm4v', 'mov', 'mkv', 'webm', 'avi', 'wmv', 'flv', 'ogv', 'm2ts', 'ts'].includes(ext);
|
|
677
|
+
}
|
|
678
|
+
resolveFrameIntervalSeconds(raw) {
|
|
679
|
+
const n = Number(raw);
|
|
680
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 10;
|
|
681
|
+
}
|
|
682
|
+
getPositiveIntegerEnv(name) {
|
|
683
|
+
const raw = process.env[name];
|
|
684
|
+
if (!raw)
|
|
685
|
+
return undefined;
|
|
686
|
+
const n = parseInt(raw, 10);
|
|
687
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
688
|
+
}
|
|
689
|
+
getFfmpegCommand() {
|
|
690
|
+
var _a;
|
|
691
|
+
const fromEnv = (_a = process.env.FFMPEG_PATH) === null || _a === void 0 ? void 0 : _a.trim();
|
|
692
|
+
if (fromEnv)
|
|
693
|
+
return fromEnv;
|
|
694
|
+
if (process.platform === 'win32') {
|
|
695
|
+
const found = this.findWindowsBinary('ffmpeg');
|
|
696
|
+
if (found)
|
|
697
|
+
return found;
|
|
698
|
+
}
|
|
699
|
+
return 'ffmpeg';
|
|
700
|
+
}
|
|
701
|
+
getFfprobeCommand() {
|
|
702
|
+
var _a, _b;
|
|
703
|
+
const fromEnv = (_a = process.env.FFPROBE_PATH) === null || _a === void 0 ? void 0 : _a.trim();
|
|
704
|
+
if (fromEnv)
|
|
705
|
+
return fromEnv;
|
|
706
|
+
const ffmpegEnv = (_b = process.env.FFMPEG_PATH) === null || _b === void 0 ? void 0 : _b.trim();
|
|
707
|
+
if (ffmpegEnv) {
|
|
708
|
+
const candidate = ffmpegEnv.replace(/ffmpeg(\.exe)?$/i, (_, ext) => `ffprobe${ext !== null && ext !== void 0 ? ext : ''}`);
|
|
709
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
710
|
+
return candidate;
|
|
711
|
+
}
|
|
712
|
+
if (process.platform === 'win32') {
|
|
713
|
+
const found = this.findWindowsBinary('ffprobe');
|
|
714
|
+
if (found)
|
|
715
|
+
return found;
|
|
716
|
+
}
|
|
717
|
+
return 'ffprobe';
|
|
718
|
+
}
|
|
719
|
+
findWindowsBinary(name) {
|
|
720
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
721
|
+
if (!localAppData)
|
|
722
|
+
return null;
|
|
723
|
+
const packagesRoot = (0, path_1.join)(localAppData, 'Microsoft', 'WinGet', 'Packages');
|
|
724
|
+
try {
|
|
725
|
+
const packageDirs = (0, fs_1.readdirSync)(packagesRoot, { withFileTypes: true })
|
|
726
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('Gyan.FFmpeg'))
|
|
727
|
+
.map((e) => (0, path_1.join)(packagesRoot, e.name))
|
|
728
|
+
.sort((a, b) => b.localeCompare(a));
|
|
729
|
+
for (const dir of packageDirs) {
|
|
730
|
+
const direct = (0, path_1.join)(dir, 'bin', `${name}.exe`);
|
|
731
|
+
if ((0, fs_1.existsSync)(direct))
|
|
732
|
+
return direct;
|
|
733
|
+
try {
|
|
734
|
+
const versionDirs = (0, fs_1.readdirSync)(dir, { withFileTypes: true })
|
|
735
|
+
.filter((e) => e.isDirectory() && e.name.toLowerCase().startsWith('ffmpeg-'))
|
|
736
|
+
.map((e) => (0, path_1.join)(dir, e.name))
|
|
737
|
+
.sort((a, b) => b.localeCompare(a));
|
|
738
|
+
for (const vd of versionDirs) {
|
|
739
|
+
const candidate = (0, path_1.join)(vd, 'bin', `${name}.exe`);
|
|
740
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
741
|
+
return candidate;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
catch ( /* skip */_a) { /* skip */ }
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch ( /* skip */_b) { /* skip */ }
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
exports.CourseVideoHlsService = CourseVideoHlsService;
|
|
752
|
+
exports.CourseVideoHlsService = CourseVideoHlsService = CourseVideoHlsService_1 = __decorate([
|
|
753
|
+
(0, common_1.Injectable)(),
|
|
754
|
+
__param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_prisma_1.PrismaService))),
|
|
755
|
+
__param(1, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
|
|
756
|
+
__param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.SettingService))),
|
|
757
|
+
__param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.NotificationService))),
|
|
758
|
+
__param(4, (0, common_1.Inject)((0, common_1.forwardRef)(() => queue_1.QueueHandlerRegistry))),
|
|
759
|
+
__param(5, (0, common_1.Inject)((0, common_1.forwardRef)(() => queue_1.QueueJobService))),
|
|
760
|
+
__metadata("design:paramtypes", [api_prisma_1.PrismaService,
|
|
761
|
+
core_1.FileService,
|
|
762
|
+
core_1.SettingService,
|
|
763
|
+
core_1.NotificationService,
|
|
764
|
+
queue_1.QueueHandlerRegistry,
|
|
765
|
+
queue_1.QueueJobService])
|
|
766
|
+
], CourseVideoHlsService);
|
|
767
|
+
//# sourceMappingURL=course-video-hls.service.js.map
|