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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- 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
|
+
}
|