coursecode 0.1.0

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 (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Google Cloud TTS Provider (Chirp)
3
+ *
4
+ * Google's neural TTS with Chirp voices for natural speech synthesis.
5
+ *
6
+ * Authentication:
7
+ * GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
8
+ *
9
+ * Setup:
10
+ * 1. Go to: https://console.cloud.google.com/iam-admin/serviceaccounts
11
+ * 2. Create a service account with "Cloud Text-to-Speech API User" role
12
+ * 3. Create a JSON key and download it
13
+ * 4. Set GOOGLE_APPLICATION_CREDENTIALS to the path
14
+ *
15
+ * Environment variables:
16
+ * GOOGLE_APPLICATION_CREDENTIALS - Path to service account JSON (required)
17
+ * GOOGLE_VOICE - Optional: Default voice (e.g., en-US-Chirp3-HD-Achernar)
18
+ * GOOGLE_LANGUAGE - Optional: Language code (default: en-US)
19
+ *
20
+ * Voice settings (per-narration):
21
+ * voice - Voice name (e.g., en-US-Chirp3-HD-Achernar, en-US-Neural2-F)
22
+ * language - Language code (e.g., en-US, en-GB)
23
+ * speed - Speaking rate (0.25-4.0, default: 1.0)
24
+ * pitch - Pitch adjustment (-20.0 to 20.0 semitones, default: 0)
25
+ *
26
+ * Chirp 3 HD voices (highest quality):
27
+ * en-US-Chirp3-HD-Achernar, en-US-Chirp3-HD-Aoede, en-US-Chirp3-HD-Charon,
28
+ * en-US-Chirp3-HD-Fenrir, en-US-Chirp3-HD-Kore, en-US-Chirp3-HD-Leda,
29
+ * en-US-Chirp3-HD-Orus, en-US-Chirp3-HD-Puck, en-US-Chirp3-HD-Schedar
30
+ */
31
+
32
+ import { BaseTTSProvider } from './base-provider.js';
33
+ import fs from 'fs';
34
+ import crypto from 'crypto';
35
+
36
+ // Default settings
37
+ const DEFAULT_VOICE = 'en-US-Neural2-J';
38
+ const DEFAULT_LANGUAGE = 'en-US';
39
+ const DEFAULT_SPEED = 1.0;
40
+ const DEFAULT_PITCH = 0;
41
+
42
+ // Popular Chirp 3 HD voices (reference for documentation)
43
+ const _CHIRP_VOICES = [
44
+ { id: 'en-US-Chirp3-HD-Achernar', name: 'Achernar', description: 'Chirp 3 HD - Warm and steady' },
45
+ { id: 'en-US-Chirp3-HD-Aoede', name: 'Aoede', description: 'Chirp 3 HD - Bright and clear' },
46
+ { id: 'en-US-Chirp3-HD-Charon', name: 'Charon', description: 'Chirp 3 HD - Deep and calm' },
47
+ { id: 'en-US-Chirp3-HD-Fenrir', name: 'Fenrir', description: 'Chirp 3 HD - Strong and confident' },
48
+ { id: 'en-US-Chirp3-HD-Kore', name: 'Kore', description: 'Chirp 3 HD - Soft and gentle' },
49
+ { id: 'en-US-Chirp3-HD-Leda', name: 'Leda', description: 'Chirp 3 HD - Professional and friendly' },
50
+ { id: 'en-US-Chirp3-HD-Orus', name: 'Orus', description: 'Chirp 3 HD - Authoritative' },
51
+ { id: 'en-US-Chirp3-HD-Puck', name: 'Puck', description: 'Chirp 3 HD - Energetic and upbeat' },
52
+ { id: 'en-US-Chirp3-HD-Schedar', name: 'Schedar', description: 'Chirp 3 HD - Smooth and reassuring' }
53
+ ];
54
+
55
+ export class GoogleProvider extends BaseTTSProvider {
56
+ constructor(config = {}) {
57
+ super(config);
58
+ this.name = 'google';
59
+ this.serviceAccountPath = config.serviceAccountPath || process.env.GOOGLE_APPLICATION_CREDENTIALS;
60
+ this._accessToken = null;
61
+ this._tokenExpiry = null;
62
+ }
63
+
64
+ static getRequiredEnvVars() {
65
+ return [
66
+ { name: 'GOOGLE_APPLICATION_CREDENTIALS', required: true, description: 'Path to service account JSON' },
67
+ { name: 'GOOGLE_VOICE', required: false, description: 'Default voice (e.g., en-US-Chirp3-HD-Leda)' },
68
+ { name: 'GOOGLE_LANGUAGE', required: false, description: 'Language code (default: en-US)' }
69
+ ];
70
+ }
71
+
72
+ validateConfig() {
73
+ if (!this.serviceAccountPath) {
74
+ throw new Error('GOOGLE_APPLICATION_CREDENTIALS must be set to the path of your service account JSON file');
75
+ }
76
+
77
+ if (!fs.existsSync(this.serviceAccountPath)) {
78
+ throw new Error(`Service account file not found: ${this.serviceAccountPath}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get OAuth2 access token from service account
84
+ */
85
+ async _getAccessToken() {
86
+ // Return cached token if still valid
87
+ if (this._accessToken && this._tokenExpiry && Date.now() < this._tokenExpiry) {
88
+ return this._accessToken;
89
+ }
90
+
91
+ const serviceAccount = JSON.parse(fs.readFileSync(this.serviceAccountPath, 'utf-8'));
92
+
93
+ // Create JWT
94
+ const now = Math.floor(Date.now() / 1000);
95
+ const header = { alg: 'RS256', typ: 'JWT' };
96
+ const payload = {
97
+ iss: serviceAccount.client_email,
98
+ scope: 'https://www.googleapis.com/auth/cloud-platform',
99
+ aud: 'https://oauth2.googleapis.com/token',
100
+ iat: now,
101
+ exp: now + 3600
102
+ };
103
+
104
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
105
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
106
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
107
+
108
+ const sign = crypto.createSign('RSA-SHA256');
109
+ sign.update(signatureInput);
110
+ const signature = sign.sign(serviceAccount.private_key, 'base64url');
111
+
112
+ const jwt = `${signatureInput}.${signature}`;
113
+
114
+ // Exchange JWT for access token
115
+ const response = await fetch('https://oauth2.googleapis.com/token', {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
118
+ body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`
119
+ });
120
+
121
+ if (!response.ok) {
122
+ const error = await response.text();
123
+ throw new Error(`Failed to get access token: ${error}`);
124
+ }
125
+
126
+ const data = await response.json();
127
+ this._accessToken = data.access_token;
128
+ this._tokenExpiry = Date.now() + (data.expires_in - 60) * 1000; // Refresh 1 min early
129
+
130
+ return this._accessToken;
131
+ }
132
+
133
+ getDefaultVoiceId() {
134
+ return process.env.GOOGLE_VOICE || DEFAULT_VOICE;
135
+ }
136
+
137
+ getDefaultSettings() {
138
+ return {
139
+ voice: this.getDefaultVoiceId(),
140
+ language: process.env.GOOGLE_LANGUAGE || DEFAULT_LANGUAGE,
141
+ speed: DEFAULT_SPEED,
142
+ pitch: DEFAULT_PITCH
143
+ };
144
+ }
145
+
146
+ normalizeSettings(settings) {
147
+ const normalized = { ...settings };
148
+
149
+ // Map 'voice_id' to 'voice'
150
+ if (settings.voice_id && !settings.voice) {
151
+ normalized.voice = settings.voice_id;
152
+ delete normalized.voice_id;
153
+ }
154
+
155
+ // Map 'rate' to 'speed'
156
+ if (settings.rate && !settings.speed) {
157
+ // Convert percentage to multiplier if needed
158
+ const rate = settings.rate;
159
+ if (typeof rate === 'string' && rate.includes('%')) {
160
+ const percent = parseFloat(rate.replace('%', '').replace('+', ''));
161
+ normalized.speed = 1 + (percent / 100);
162
+ } else {
163
+ normalized.speed = parseFloat(rate);
164
+ }
165
+ delete normalized.rate;
166
+ }
167
+
168
+ return normalized;
169
+ }
170
+
171
+ async getVoices() {
172
+ this.validateConfig();
173
+
174
+ const token = await this._getAccessToken();
175
+
176
+ const response = await fetch('https://texttospeech.googleapis.com/v1/voices', {
177
+ headers: {
178
+ 'Authorization': `Bearer ${token}`,
179
+ 'Content-Type': 'application/json'
180
+ }
181
+ });
182
+
183
+ if (!response.ok) {
184
+ const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
185
+ throw new Error(`Failed to fetch voices (${response.status}): ${errorData.error?.message || JSON.stringify(errorData)}`);
186
+ }
187
+
188
+ const data = await response.json();
189
+ return data.voices
190
+ .filter(v => v.languageCodes.some(lc => lc.startsWith('en')))
191
+ .map(v => ({
192
+ id: v.name,
193
+ name: v.name.split('-').pop(),
194
+ description: `${v.ssmlGender} - ${v.languageCodes.join(', ')}`
195
+ }));
196
+ }
197
+
198
+ /**
199
+ * Extract language code from voice name if not specified
200
+ */
201
+ _getLanguageFromVoice(voice) {
202
+ // Voice names like "en-US-Chirp3-HD-Leda" start with language code
203
+ const match = voice.match(/^([a-z]{2}-[A-Z]{2})/);
204
+ return match ? match[1] : DEFAULT_LANGUAGE;
205
+ }
206
+
207
+ async generateAudio(text, settings = {}) {
208
+ this.validateConfig();
209
+
210
+ const normalizedSettings = this.normalizeSettings(settings);
211
+ const defaults = this.getDefaultSettings();
212
+
213
+ const voice = normalizedSettings.voice || defaults.voice;
214
+ const language = normalizedSettings.language || this._getLanguageFromVoice(voice);
215
+ const speed = parseFloat(normalizedSettings.speed) || defaults.speed;
216
+ const pitch = parseFloat(normalizedSettings.pitch) || defaults.pitch;
217
+
218
+ // Validate speed
219
+ if (speed < 0.25 || speed > 4.0) {
220
+ throw new Error(`Speed must be between 0.25 and 4.0, got ${speed}`);
221
+ }
222
+
223
+ // Validate pitch
224
+ if (pitch < -20 || pitch > 20) {
225
+ throw new Error(`Pitch must be between -20 and 20, got ${pitch}`);
226
+ }
227
+
228
+ const token = await this._getAccessToken();
229
+
230
+ const requestBody = {
231
+ input: { text },
232
+ voice: {
233
+ languageCode: language,
234
+ name: voice
235
+ },
236
+ audioConfig: {
237
+ audioEncoding: 'MP3',
238
+ speakingRate: speed,
239
+ pitch: pitch
240
+ }
241
+ };
242
+
243
+ let response;
244
+ try {
245
+ response = await fetch('https://texttospeech.googleapis.com/v1/text:synthesize', {
246
+ method: 'POST',
247
+ headers: {
248
+ 'Authorization': `Bearer ${token}`,
249
+ 'Content-Type': 'application/json'
250
+ },
251
+ body: JSON.stringify(requestBody)
252
+ });
253
+ } catch (fetchError) {
254
+ const cause = fetchError.cause ? `: ${fetchError.cause.message || fetchError.cause.code}` : '';
255
+ throw new Error(`Network error connecting to Google Cloud TTS API${cause}. Check your internet connection.`);
256
+ }
257
+
258
+ if (!response.ok) {
259
+ const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
260
+ throw new Error(`Google Cloud TTS API error (${response.status}): ${errorData.error?.message || JSON.stringify(errorData)}`);
261
+ }
262
+
263
+ const data = await response.json();
264
+
265
+ // Google returns base64-encoded audio
266
+ if (!data.audioContent) {
267
+ throw new Error('No audio content in response');
268
+ }
269
+
270
+ return Buffer.from(data.audioContent, 'base64');
271
+ }
272
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * TTS Provider Registry
3
+ *
4
+ * Manages available TTS providers and handles provider selection.
5
+ *
6
+ * Provider Selection Priority:
7
+ * 1. TTS_PROVIDER environment variable (explicit selection)
8
+ * 2. Auto-detect based on available API keys
9
+ * 3. Default to deepgram
10
+ *
11
+ * Environment Variables:
12
+ * TTS_PROVIDER - Explicit provider selection: elevenlabs, openai, azure
13
+ */
14
+
15
+ import { ElevenLabsProvider } from './elevenlabs-provider.js';
16
+ import { OpenAIProvider } from './openai-provider.js';
17
+ import { AzureProvider } from './azure-provider.js';
18
+ import { GoogleProvider } from './google-provider.js';
19
+ import { DeepgramProvider } from './deepgram-provider.js';
20
+
21
+ // Provider registry
22
+ const PROVIDERS = {
23
+ elevenlabs: ElevenLabsProvider,
24
+ openai: OpenAIProvider,
25
+ azure: AzureProvider,
26
+ google: GoogleProvider,
27
+ deepgram: DeepgramProvider
28
+ };
29
+
30
+ // Provider aliases for convenience
31
+ const PROVIDER_ALIASES = {
32
+ '11labs': 'elevenlabs',
33
+ 'eleven': 'elevenlabs',
34
+ 'gpt': 'openai',
35
+ 'chatgpt': 'openai',
36
+ 'microsoft': 'azure',
37
+ 'cognitive': 'azure',
38
+ 'chirp': 'google',
39
+ 'gcloud': 'google',
40
+ 'googlecloud': 'google',
41
+ 'aura': 'deepgram'
42
+ };
43
+
44
+ /**
45
+ * Get a provider instance by name
46
+ * @param {string} name - Provider name or alias
47
+ * @param {Object} config - Optional provider configuration
48
+ * @returns {BaseTTSProvider}
49
+ */
50
+ export function getProvider(name, config = {}) {
51
+ // Resolve aliases
52
+ const resolvedName = PROVIDER_ALIASES[name?.toLowerCase()] || name?.toLowerCase();
53
+
54
+ const ProviderClass = PROVIDERS[resolvedName];
55
+ if (!ProviderClass) {
56
+ const available = Object.keys(PROVIDERS).join(', ');
57
+ throw new Error(`Unknown TTS provider '${name}'. Available providers: ${available}`);
58
+ }
59
+
60
+ return new ProviderClass(config);
61
+ }
62
+
63
+ /**
64
+ * Auto-detect the best available provider based on configured API keys
65
+ * @returns {string|null} Provider name or null if none configured
66
+ */
67
+ export function detectProvider() {
68
+ // Check each provider's required env vars
69
+ if (process.env.ELEVENLABS_API_KEY) {
70
+ return 'elevenlabs';
71
+ }
72
+ if (process.env.OPENAI_API_KEY) {
73
+ return 'openai';
74
+ }
75
+ if (process.env.AZURE_SPEECH_KEY && process.env.AZURE_SPEECH_REGION) {
76
+ return 'azure';
77
+ }
78
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
79
+ return 'google';
80
+ }
81
+ if (process.env.DEEPGRAM_API_KEY) {
82
+ return 'deepgram';
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Get the active provider based on environment configuration
89
+ * @param {Object} config - Optional provider configuration
90
+ * @returns {BaseTTSProvider}
91
+ */
92
+ export function getActiveProvider(config = {}) {
93
+ // 1. Explicit selection via TTS_PROVIDER
94
+ let providerName = process.env.TTS_PROVIDER;
95
+
96
+ // 2. Auto-detect from available API keys
97
+ if (!providerName) {
98
+ providerName = detectProvider();
99
+ }
100
+
101
+ // 3. Default to deepgram
102
+ if (!providerName) {
103
+ providerName = 'deepgram';
104
+ }
105
+
106
+ return getProvider(providerName, config);
107
+ }
108
+
109
+ /**
110
+ * List all available providers with their configuration status
111
+ * @returns {Array<{name: string, configured: boolean, envVars: Array}>}
112
+ */
113
+ export function listProviders() {
114
+ return Object.entries(PROVIDERS).map(([name, ProviderClass]) => {
115
+ const envVars = ProviderClass.getRequiredEnvVars();
116
+ const configured = envVars
117
+ .filter(v => v.required)
118
+ .every(v => !!process.env[v.name]);
119
+
120
+ return {
121
+ name,
122
+ configured,
123
+ envVars
124
+ };
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Print provider configuration help
130
+ * @param {boolean} verbose - Include all env vars, not just required
131
+ */
132
+ export function printProviderHelp(verbose = false) {
133
+ console.warn('\n📢 Available TTS Providers:\n');
134
+
135
+ for (const [name, ProviderClass] of Object.entries(PROVIDERS)) {
136
+ const envVars = ProviderClass.getRequiredEnvVars();
137
+ const requiredVars = envVars.filter(v => v.required);
138
+ const configured = requiredVars.every(v => !!process.env[v.name]);
139
+
140
+ const status = configured ? '✅' : '❌';
141
+ console.warn(` ${status} ${name}`);
142
+
143
+ if (verbose || !configured) {
144
+ for (const v of envVars) {
145
+ const isSet = !!process.env[v.name];
146
+ const required = v.required ? '(required)' : '(optional)';
147
+ const setStatus = isSet ? '✓' : '✗';
148
+ console.warn(` ${setStatus} ${v.name} ${required} - ${v.description}`);
149
+ }
150
+ console.warn('');
151
+ }
152
+ }
153
+
154
+ console.warn(' Set TTS_PROVIDER=<name> to explicitly select a provider.\n');
155
+ }
156
+
157
+ // Export provider classes for direct use
158
+ export { ElevenLabsProvider, OpenAIProvider, AzureProvider, GoogleProvider, DeepgramProvider };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * OpenAI TTS Provider
3
+ *
4
+ * Uses OpenAI's Text-to-Speech API with natural-sounding voices.
5
+ *
6
+ * Environment variables:
7
+ * OPENAI_API_KEY - Required: Your OpenAI API key
8
+ * OPENAI_VOICE - Optional: Default voice (alloy, echo, fable, onyx, nova, shimmer)
9
+ * OPENAI_MODEL - Optional: Model to use (tts-1, tts-1-hd)
10
+ *
11
+ * Voice settings (per-narration):
12
+ * voice - Voice name (alloy, echo, fable, onyx, nova, shimmer)
13
+ * model - Model to use (tts-1 for speed, tts-1-hd for quality)
14
+ * speed - Speed multiplier (0.25-4.0, default: 1.0)
15
+ */
16
+
17
+ import { BaseTTSProvider } from './base-provider.js';
18
+
19
+ // Default settings
20
+ const DEFAULT_VOICE = 'alloy';
21
+ const DEFAULT_MODEL = 'tts-1';
22
+ const DEFAULT_SPEED = 1.0;
23
+
24
+ // Available voices
25
+ const AVAILABLE_VOICES = [
26
+ { id: 'alloy', name: 'Alloy', description: 'Neutral and balanced' },
27
+ { id: 'echo', name: 'Echo', description: 'Warm and conversational' },
28
+ { id: 'fable', name: 'Fable', description: 'Expressive and British-accented' },
29
+ { id: 'onyx', name: 'Onyx', description: 'Deep and authoritative' },
30
+ { id: 'nova', name: 'Nova', description: 'Friendly and upbeat' },
31
+ { id: 'shimmer', name: 'Shimmer', description: 'Clear and pleasant' }
32
+ ];
33
+
34
+ export class OpenAIProvider extends BaseTTSProvider {
35
+ constructor(config = {}) {
36
+ super(config);
37
+ this.name = 'openai';
38
+ this.apiKey = config.apiKey || process.env.OPENAI_API_KEY;
39
+ }
40
+
41
+ static getRequiredEnvVars() {
42
+ return [
43
+ { name: 'OPENAI_API_KEY', required: true, description: 'OpenAI API key' },
44
+ { name: 'OPENAI_VOICE', required: false, description: 'Default voice (alloy, echo, fable, onyx, nova, shimmer)' },
45
+ { name: 'OPENAI_MODEL', required: false, description: 'Model (tts-1 or tts-1-hd)' }
46
+ ];
47
+ }
48
+
49
+ validateConfig() {
50
+ if (!this.apiKey) {
51
+ throw new Error('OPENAI_API_KEY not set in environment or .env file');
52
+ }
53
+
54
+ const trimmedKey = this.apiKey.trim();
55
+ if (trimmedKey !== this.apiKey || !trimmedKey) {
56
+ throw new Error('OPENAI_API_KEY contains invalid whitespace - check your .env file');
57
+ }
58
+ }
59
+
60
+ getDefaultVoiceId() {
61
+ return process.env.OPENAI_VOICE || DEFAULT_VOICE;
62
+ }
63
+
64
+ getDefaultSettings() {
65
+ return {
66
+ voice: this.getDefaultVoiceId(),
67
+ model: process.env.OPENAI_MODEL || DEFAULT_MODEL,
68
+ speed: DEFAULT_SPEED
69
+ };
70
+ }
71
+
72
+ normalizeSettings(settings) {
73
+ const normalized = { ...settings };
74
+
75
+ // Map 'voice_id' to 'voice' (from ElevenLabs format)
76
+ if (settings.voice_id && !settings.voice) {
77
+ normalized.voice = settings.voice_id;
78
+ delete normalized.voice_id;
79
+ }
80
+
81
+ // Map 'model_id' to 'model'
82
+ if (settings.model_id && !settings.model) {
83
+ normalized.model = settings.model_id;
84
+ delete normalized.model_id;
85
+ }
86
+
87
+ return normalized;
88
+ }
89
+
90
+ async getVoices() {
91
+ // OpenAI voices are fixed, no API call needed
92
+ return AVAILABLE_VOICES;
93
+ }
94
+
95
+ async generateAudio(text, settings = {}) {
96
+ this.validateConfig();
97
+
98
+ const normalizedSettings = this.normalizeSettings(settings);
99
+ const defaults = this.getDefaultSettings();
100
+
101
+ const voice = normalizedSettings.voice || defaults.voice;
102
+ const model = normalizedSettings.model || defaults.model;
103
+ const speed = parseFloat(normalizedSettings.speed) || defaults.speed;
104
+
105
+ // Validate voice
106
+ if (!AVAILABLE_VOICES.some(v => v.id === voice)) {
107
+ throw new Error(`Invalid voice '${voice}'. Available: ${AVAILABLE_VOICES.map(v => v.id).join(', ')}`);
108
+ }
109
+
110
+ // Validate speed
111
+ if (speed < 0.25 || speed > 4.0) {
112
+ throw new Error(`Speed must be between 0.25 and 4.0, got ${speed}`);
113
+ }
114
+
115
+ let response;
116
+ try {
117
+ response = await fetch('https://api.openai.com/v1/audio/speech', {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Authorization': `Bearer ${this.apiKey.trim()}`,
121
+ 'Content-Type': 'application/json'
122
+ },
123
+ body: JSON.stringify({
124
+ model,
125
+ input: text,
126
+ voice,
127
+ speed,
128
+ response_format: 'mp3'
129
+ })
130
+ });
131
+ } catch (fetchError) {
132
+ const cause = fetchError.cause ? `: ${fetchError.cause.message || fetchError.cause.code}` : '';
133
+ throw new Error(`Network error connecting to OpenAI API${cause}. Check your internet connection.`);
134
+ }
135
+
136
+ if (!response.ok) {
137
+ const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
138
+ throw new Error(`OpenAI API error (${response.status}): ${errorData.error?.message || JSON.stringify(errorData)}`);
139
+ }
140
+
141
+ return Buffer.from(await response.arrayBuffer());
142
+ }
143
+ }
@@ -0,0 +1,63 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "name": "SCORM 2004 4th Edition Framework",
4
+ "description": "Production-ready SCORM framework with modular architecture and advanced features",
5
+ "released": "2025-11-07",
6
+ "scorm_version": "SCORM 2004 4th Edition",
7
+ "compatibility": {
8
+ "minimum_course_version": "0.1.0",
9
+ "breaking_changes": [
10
+ "StateManager now owns all SCORM persistence; PersistenceManager removed"
11
+ ]
12
+ },
13
+ "components": {
14
+ "core": [
15
+ "pipwerks.js",
16
+ "runtime.js",
17
+ "event-bus.js",
18
+ "course-app.js",
19
+ "accessibility-enhancements.js"
20
+ ],
21
+ "managers": [
22
+ "state-manager.js",
23
+ "navigation-manager.js",
24
+ "objective-manager.js",
25
+ "interaction-manager.js",
26
+ "accessibility-manager.js",
27
+ "assessment-manager.js"
28
+ ],
29
+ "utilities": [
30
+ "utilities.js",
31
+ "feedback-system.js"
32
+ ],
33
+ "components": [
34
+ "ui-components/index.js",
35
+ "ui-components/modal.js",
36
+ "ui-components/dropdown.js",
37
+ "ui-components/notifications.js",
38
+ "ui-components/tabs.js",
39
+ "interactions/"
40
+ ]
41
+ },
42
+ "features": [
43
+ "SCORM 2004 4th Edition compliant",
44
+ "Sequential assessment navigator",
45
+ "Interactive question types (multiple-choice, fill-in, numeric, hotspot, drag-drop, true-false)",
46
+ "SCORM-based state persistence",
47
+ "Accessibility features (WCAG 2.1 AA)",
48
+ "Adaptive feedback system",
49
+ "Course gating and navigation rules",
50
+ "Time tracking and analytics",
51
+ "Offline mode support",
52
+ "Assessment security features"
53
+ ],
54
+ "upgrade": {
55
+ "instructions": "See UPGRADE_GUIDE.md for detailed upgrade instructions",
56
+ "backup_recommended": true,
57
+ "test_before_deployment": true
58
+ },
59
+ "support": {
60
+ "documentation": "https://github.com/svincent/scorm-template",
61
+ "issues": "https://github.com/svincent/scorm-template/issues"
62
+ }
63
+ }