js-tts-wrapper
Version:
A JavaScript/TypeScript library that provides a unified API for working with multiple cloud-based Text-to-Speech (TTS) services
227 lines (226 loc) • 8.92 kB
JavaScript
import { isNode } from "../utils/environment.js";
const runtimeConfig = {};
const TRUE_PATTERN = /^(1|true|yes|on)$/i;
const FALSE_PATTERN = /^(0|false|no|off)$/i;
function parseBooleanFlag(value) {
if (value === undefined || value === null)
return undefined;
const text = String(value).trim();
if (!text)
return undefined;
if (TRUE_PATTERN.test(text))
return true;
if (FALSE_PATTERN.test(text))
return false;
return undefined;
}
function getEnvEnabledOverride() {
if (!isNode)
return undefined;
try {
const env = process?.env ?? {};
const disableFlag = parseBooleanFlag(env.SPEECHMARKDOWN_DISABLE);
if (disableFlag === true) {
return false;
}
if (disableFlag === false) {
return true;
}
const enableFlag = parseBooleanFlag(env.SPEECHMARKDOWN_ENABLE);
if (enableFlag !== undefined) {
return enableFlag;
}
}
catch {
// Ignore env parsing errors and fall back to defaults
}
return undefined;
}
function isSpeechMarkdownEnabled() {
if (typeof runtimeConfig.enabled === "boolean") {
return runtimeConfig.enabled;
}
const envOverride = getEnvEnabledOverride();
if (typeof envOverride === "boolean") {
return envOverride;
}
// Default: enabled everywhere (Node + browser)
return true;
}
export function configureSpeechMarkdown(options = {}) {
if (typeof options.enabled === "boolean") {
runtimeConfig.enabled = options.enabled;
}
}
/**
* Speech Markdown converter using the official speechmarkdown-js library
*
* This module provides functions to convert Speech Markdown to SSML
* using the speechmarkdown-js library (https://github.com/speechmarkdown/speechmarkdown-js)
*/
// Dynamic import for speechmarkdown-js
let SpeechMarkdown = null;
let speechMarkdownLoaded = false;
async function loadSpeechMarkdown() {
if (speechMarkdownLoaded)
return SpeechMarkdown;
try {
if (!isSpeechMarkdownEnabled()) {
console.warn("speechmarkdown-js disabled (set SPEECHMARKDOWN_DISABLE=false or configureSpeechMarkdown({ enabled: true }) to re-enable). Using built-in fallback.");
return null;
}
// Attempt dynamic import in both Node and browser without triggering bundlers to hard-require it
const dynamicImport = new Function("m", "return import(m)");
const module = await dynamicImport("speechmarkdown-js");
// Prefer named export, but tolerate default exports
SpeechMarkdown = module?.SpeechMarkdown ?? module?.default?.SpeechMarkdown ?? module?.default;
if (!SpeechMarkdown) {
throw new Error("speechmarkdown-js module did not expose SpeechMarkdown class");
}
speechMarkdownLoaded = true;
return SpeechMarkdown;
}
catch (_error) {
console.warn("speechmarkdown-js not available. Using built-in fallback. To enable full Speech Markdown in browsers, add 'speechmarkdown-js' to your app and it will be loaded at runtime.");
return null;
}
}
// Lightweight fallback converter for a minimal subset used in tests
function convertSpeechMarkdownFallback(markdown) {
let out = markdown;
// [break:"500ms"] -> <break time="500ms"/>
out = out.replace(/\[break:\"([^\"]+)\"\]/g, '<break time="$1"/>');
// [500ms] or [500s] -> <break time="500ms"/>
out = out.replace(/\[(\d+)m?s\]/g, '<break time="$1ms"/>');
return out;
}
/**
* SpeechMarkdownConverter class for converting Speech Markdown to SSML
*/
export class SpeechMarkdownConverter {
constructor() {
Object.defineProperty(this, "speechMarkdownInstance", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
}
async ensureInitialized() {
if (!isSpeechMarkdownEnabled()) {
this.speechMarkdownInstance = null;
return null;
}
if (!this.speechMarkdownInstance) {
const SpeechMarkdownClass = await loadSpeechMarkdown();
if (SpeechMarkdownClass) {
this.speechMarkdownInstance = new SpeechMarkdownClass();
}
}
return this.speechMarkdownInstance;
}
/**
* Convert Speech Markdown to SSML
*
* @param markdown Speech Markdown text
* @param platform Target platform (amazon-alexa, google-assistant, microsoft-azure, etc.)
* @returns SSML text
*/
async toSSML(markdown, platform = "amazon-alexa") {
if (!isSpeechMarkdownEnabled()) {
this.speechMarkdownInstance = null;
const converted = convertSpeechMarkdownFallback(markdown);
return `<speak>${converted}</speak>`;
}
// Attempt to initialize the full converter (no-op if disabled/unavailable)
await this.ensureInitialized();
if (this.speechMarkdownInstance) {
return this.speechMarkdownInstance.toSSML(markdown, { platform });
}
// Fallback: minimal conversion
const converted = convertSpeechMarkdownFallback(markdown);
return `<speak>${converted}</speak>`;
}
/**
* Check if text is Speech Markdown
*
* @param text Text to check
* @returns True if the text contains Speech Markdown syntax
*/
isSpeechMarkdown(text) {
return isSpeechMarkdown(text);
}
/**
* Get the available platforms supported by the Speech Markdown library
*
* @returns Array of platform names
*/
getAvailablePlatforms() {
return getAvailablePlatforms();
}
}
// Create a default converter instance
const defaultConverter = new SpeechMarkdownConverter();
/**
* Convert Speech Markdown to SSML
*
* This function uses the speechmarkdown-js library to convert Speech Markdown syntax to SSML.
* The library supports various Speech Markdown features including:
* - Breaks: [500ms] or [break:"500ms"]
* - Emphasis: ++emphasized++ or +emphasized+
* - Rate, pitch, volume: (text)[rate:"slow"], (text)[pitch:"high"], (text)[volume:"loud"]
* - And many more (see the speechmarkdown-js documentation)
*
* @param markdown Speech Markdown text
* @param platform Target platform (amazon-alexa, google-assistant, microsoft-azure, etc.)
* @returns SSML text
*/
export async function toSSML(markdown, platform = "amazon-alexa") {
return await defaultConverter.toSSML(markdown, platform);
}
/**
* Check if text is Speech Markdown
*
* This function checks if the text contains Speech Markdown syntax patterns.
* It uses regular expressions to detect common Speech Markdown patterns such as:
* - Breaks: [500ms] or [break:"500ms"]
* - Emphasis: ++text++ or +text+
* - Rate, pitch, volume: (text)[rate:"slow"], (text)[pitch:"high"], (text)[volume:"loud"]
*
* @param text Text to check
* @returns True if the text contains Speech Markdown syntax
*/
export function isSpeechMarkdown(text) {
// Use a simple heuristic to check for common Speech Markdown patterns
// This is a simplified version as the library doesn't provide a direct way to check
const patterns = [
/\[\d+m?s\]/, // Breaks: [500ms]
/\[break:"[^"\]]+"\]/, // Breaks with quotes: [break:"weak"] or [break:"500ms"]
/\+\+.*?\+\+/, // Strong emphasis: ++text++
/\+.*?\+/, // Moderate emphasis: +text+
/~.*?~/, // No emphasis: ~text~
/-.*?-/, // Reduced emphasis: -text-
/\(.*?\)\[emphasis(:"(strong|moderate|reduced|none)")?\]/, // Standard emphasis: (text)[emphasis:"strong"]
/\(.*?\)\[rate:"(x-slow|slow|medium|fast|x-fast)"\]/, // Rate: (text)[rate:"slow"]
/\(.*?\)\[pitch:"(x-low|low|medium|high|x-high)"\]/, // Pitch: (text)[pitch:"high"]
/\(.*?\)\[volume:"(silent|x-soft|soft|medium|loud|x-loud)"\]/, // Volume: (text)[volume:"loud"]
/\(.*?\)\[voice:".*?"\]/, // Voice: (text)[voice:"Brian"]
/\(.*?\)\[lang:".*?"\]/, // Language: (text)[lang:"en-US"]
/\(.*?\)\[\w+:"?.*?"?\]/, // Any other Speech Markdown modifier: (text)[modifier:"value"]
];
return patterns.some((pattern) => pattern.test(text));
}
/**
* Get the available platforms supported by the Speech Markdown library
*
* This function returns the list of platforms supported by the speechmarkdown-js library.
* These platforms have different SSML dialects, and the library will generate
* SSML appropriate for the specified platform.
*
* @returns Array of platform names (amazon-alexa, google-assistant, microsoft-azure)
*/
export function getAvailablePlatforms() {
// The library doesn't expose a direct way to get platforms, so we hardcode them
// These are the platforms supported by speechmarkdown-js as of version 1.x
return ["amazon-alexa", "google-assistant", "microsoft-azure"];
}