diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 37284e8..c343a7d 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -40,10 +40,12 @@ jobs: ls -la ./ ls -laR ./build ls -laR ./demo + ls -laR ./json mkdir build-demo cp README.md ./build-demo/ cp -r ./build ./build-demo/ cp -r ./demo ./build-demo/ + cp -r ./json ./build-demo/ ls -laR ./build-demo diff --git a/.github/workflows/validate-json.yml b/.github/workflows/validate-json.yml new file mode 100644 index 0000000..b42dc8d --- /dev/null +++ b/.github/workflows/validate-json.yml @@ -0,0 +1,32 @@ +name: Validate JSON Schema + +on: + push: + branches: + - main + - develop + paths: + - "json/**/*.json" + pull_request: + branches: + - main + - develop + paths: + - "json/**/*.json" + workflow_dispatch: + +jobs: + validate-json: + name: Validate JSON Schema + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate JSON against schema + uses: dsanders11/json-schema-validate-action@v1.4.0 + with: + schema: "./voices.schema.json" + files: | + json/**/*.json + !json/localizedNames/** \ No newline at end of file diff --git a/README.md b/README.md index 2e5bc28..463d2be 100644 --- a/README.md +++ b/README.md @@ -17,165 +17,252 @@ Readium Speech was spun out as a separate project in order to facilitate its int ## Current focus -For our initial work on this project, we're focusing on voice selection based on [recommended voices](https://github.com/HadrienGardeur/web-speech-recommended-voices). +For our initial work on this project, we focused on voice selection based on [recommended voices](https://github.com/HadrienGardeur/web-speech-recommended-voices). The outline of this work has been explored in a [GitHub discussion](https://github.com/HadrienGardeur/web-speech-recommended-voices/discussions/9) and through a [best practices document](https://github.com/HadrienGardeur/read-aloud-best-practices/blob/main/voice-selection.md). -## Demo +In the second phase, we focused on implementing a WebSpeech API-based solution with an architecture designed for future extensibility: -[A live demo](https://readium.org/speech/demo/) of the voice selection API is available. +- **Engine Layer**: Core TTS functionality through `ReadiumSpeechPlaybackEngine` +- **Navigator Layer**: Content and playback management via (a temporary) `ReadiumSpeechNavigator` +- **Current Implementation**: WebSpeech API with cross-browser compatibility +- **Future-Proof Design**: Architecture prepared for additional TTS service adapters -It demonstrates the following features: +Key features include advanced voice selection, cross-browser playback control, flexible content loading, and comprehensive event handling for UI feedback. The architecture is designed to be extensible for different TTS backends while maintaining TypeScript-first development practices. + +## Demos + +Two live demos are available: + +1. [Voice selection with playback demo](https://readium.org/speech/demo) +2. [In-context demo](https://readium.org/speech/demo/article) + +The first demo showcases the following features: - fetching a list of all available languages, translating them to the user's locale and sorting them based on these translations - returning a list of voices for a given language, grouped by region and sorted based on quality - filtering languages and voices based on gender and offline availability - using embedded test utterances to demo voices +- using the current Navigator for playback control + +The second demo focuses on in-context reading with seamless voice selection (grouped by region and sorted based on quality), and playback control, providing an optional read-along experience that integrates naturally with the content. ## QuickStart -At the moment, the new alpha version of the library is not published on npm, so you need to clone the repository and build it yourself. +### Prerequisites -```sh -git clone https://github.com/readium/speech.git -``` +- Node.js +- npm -```sh -cd speech -npm install -npm run build -``` +### Installation -You can then link the library to your project, for example using `npm link`. +1. Clone the repository: + ```bash + git clone https://github.com/readium/speech.git + cd speech + ``` -```typescript -import { getVoices } from "readium-speech"; -console.log(getVoices); +2. Install dependencies: + ```bash + npm install + ``` -const voices = await getVoices(); -console.log(voices); +3. Build the package: + ```bash + npm run build + ``` -``` +4. Link the package locally (optional, for development): + ```bash + npm link + # Then in your project directory: + # npm link readium-speech + ``` ### Basic Usage -Here's how to get started with the Readium Speech library: - ```typescript -import { WebSpeechReadAloudNavigator } from "readium-speech"; +import { WebSpeechVoiceManager } from "readium-speech"; + +async function setupVoices() { + try { + // Initialize the voice manager + const voiceManager = await WebSpeechVoiceManager.initialize(); + + // Get all available voices + const allVoices = voiceManager.getVoices(); + console.log("Available voices:", allVoices); + + // Get voices with filters + const filteredVoices = voiceManager.getVoices({ + language: ["en", "fr"], + gender: "female", + quality: "high", + offlineOnly: true, + excludeNovelty: true, + excludeVeryLowQuality: true + }); + + // Get voices grouped by language + const voices = voiceManager.getVoices(); + const groupedByLanguage = voiceManager.groupVoices(voices, "language"); + + // Get a test utterance for a specific language + const testText = voiceManager.getTestUtterance("en"); + + } catch (error) { + console.error("Error initializing voice manager:", error); + } +} -// Initialize the navigator with default WebSpeech engine -const navigator = new WebSpeechReadAloudNavigator(); +await setupVoices(); +``` -// Load content to be read -navigator.loadContent([ - { text: "Hello, this is the first sentence.", language: "en-US" }, - { text: "And this is the second sentence.", language: "en-US" } -]); +## API Reference -// Set up event listeners -navigator.on("start", () => console.log("Playback started")); -navigator.on("end", () => console.log("Playback finished")); +### Class: WebSpeechVoiceManager -// Start playback -navigator.play(); +The main class for managing Web Speech API voices with enhanced functionality. -// Later, you can pause, resume, or stop -// navigator.pause(); -// navigator.stop(); +#### Initialize the Voice Manager -// Clean up when done -// navigator.destroy(); +```typescript +static initialize(maxTimeout?: number, interval?: number): Promise ``` -## Voices API +Creates and initializes a new WebSpeechVoiceManager instance. This static factory method must be called to create an instance. -### Interface +- `maxTimeout`: Maximum time in milliseconds to wait for voices to load (default: 10000ms) +- `interval`: Interval in milliseconds between voice loading checks (default: 100ms) +- Returns: Promise that resolves with a new WebSpeechVoiceManager instance -```typescript -export interface ReadiumSpeechVoices { - label: string; - voiceURI: string; - name: string; - language: string; - gender?: TGender | undefined; - age?: string | undefined; - offlineAvailability: boolean; - quality?: TQuality | undefined; - pitchControl: boolean; - recommendedPitch?: number | undefined; - recommendedRate?: number | undefined; -} +#### Get Available Voices -export interface ILanguages { - label: string; - code: string; - count: number; -} +```typescript +voiceManager.getVoices(options?: VoiceFilterOptions): ReadiumSpeechVoice[] ``` -#### Parse and Extract ReadiumSpeechVoices from speechSynthesis WebAPI +Fetches all available voices that match the specified filter criteria. ```typescript -function getVoices(preferredLanguage?: string[] | string, localization?: string): Promise +interface VoiceFilterOptions { + language?: string | string[]; // Filter by language code(s) (e.g., "en", "fr") + gender?: TGender; // "male" | "female" | "other" + quality?: TQuality | TQuality[]; // "high" | "medium" | "low" | "veryLow" + offlineOnly?: boolean; // Only return voices available offline + provider?: string; // Filter by voice provider + excludeNovelty?: boolean; // Exclude novelty voices + excludeVeryLowQuality?: boolean; // Exclude very low quality voices +} ``` -#### List languages from ReadiumSpeechVoices +#### Group Voices ```typescript -function getLanguages(voices: ReadiumSpeechVoices[], preferredLanguage?: string[] | string, localization?: string | undefined): ILanguages[] +voiceManager.groupVoices(voices: ReadiumSpeechVoice[], groupBy: "language" | "region" | "gender" | "quality" | "provider"): VoiceGroup ``` -#### helpers - -```typescript -function listLanguages(voices: ReadiumSpeechVoices[], localization?: string): ILanguages[] +Organizes voices into groups based on the specified criteria. The available grouping options are: -function ListRegions(voices: ReadiumSpeechVoices[], localization?: string): ILanguages[] +- `"language"`: Groups voices by their language code +- `"region"`: Groups voices by their region +- `"gender"`: Groups voices by gender +- `"quality"`: Groups voices by quality level +- `"provider"`: Groups voices by their provider -function parseSpeechSynthesisVoices(speechSynthesisVoices: SpeechSynthesisVoice[]): ReadiumSpeechVoices[] +#### Sort Voices -function getSpeechSynthesisVoices(): Promise +```typescript +voiceManager.sortVoices(voices: ReadiumSpeechVoice[], options: SortOptions): ReadiumSpeechVoice[] ``` -#### groupBy +Arranges voices according to the specified sorting criteria. The `SortOptions` interface allows you to sort by various properties and specify sort order. ```typescript -function groupByKindOfVoices(allVoices: ReadiumSpeechVoices[]): TGroupVoices - -function groupByRegions(voices: ReadiumSpeechVoices[], language: string, preferredRegions?: string[] | string, localization?: string): TGroupVoices - -function groupByLanguage(voices: ReadiumSpeechVoices[], preferredLanguage?: string[] | string, localization?: string): TGroupVoices +interface SortOptions { + by: "name" | "language" | "gender" | "quality" | "region"; + order?: "asc" | "desc"; +} ``` -#### sortBy +### Testing + +#### Get Test Utterance ```typescript -function sortByLanguage(voices: ReadiumSpeechVoices[], preferredLanguage?: string[] | string): ReadiumSpeechVoices[] +voiceManager.getTestUtterance(language: string): string +``` -function sortByRegion(voices: ReadiumSpeechVoices[], preferredRegions?: string[] | string, localization?: string | undefined): ReadiumSpeechVoices[] +Retrieves a sample text string suitable for testing text-to-speech functionality in the specified language. If no sample text is available for the specified language, it returns an empty string. -function sortByGender(voices: ReadiumSpeechVoices[], genderFirst: TGender): ReadiumSpeechVoices[] +### Interfaces -function sortByName(voices: ReadiumSpeechVoices[]): ReadiumSpeechVoices[] +#### `ReadiumSpeechVoice` -function sortByQuality(voices: ReadiumSpeechVoices[]): ReadiumSpeechVoices[] +```typescript +interface ReadiumSpeechVoice { + // Core identification (required) + label: string; // Human-friendly label for the voice + name: string; // System/technical name (matches Web Speech API voiceURI) + voiceURI?: string; // For Web Speech API compatibility + + // Localization + language: string; // BCP-47 language tag + localizedName?: TLocalizedName; // Localization pattern (android/apple) + altNames?: string[]; // Alternative names (mostly for Apple voices) + altLanguage?: string; // Alternative BCP-47 language tag + otherLanguages?: string[]; // Other languages this voice can speak + multiLingual?: boolean; // If voice can handle multiple languages + + // Voice characteristics + gender?: TGender; // Voice gender ("female" | "male" | "neutral") + children?: boolean; // If this is a children's voice + + // Quality and capabilities + quality?: TQuality[]; // Available quality levels for this voice ("veryLow" | "low" | "normal" | "high" | "veryHigh") + pitchControl?: boolean; // Whether pitch can be controlled + + // Performance settings + pitch?: number; // Current pitch (0-2, where 1 is normal) + rate?: number; // Speech rate (0.1-10, where 1 is normal) + + // Platform and compatibility + browser?: string[]; // Supported browsers + os?: string[]; // Supported operating systems + preloaded?: boolean; // If the voice is preloaded on the system + nativeID?: string | string[]; // Platform-specific voice ID(s) + + // Additional metadata + note?: string; // Additional notes about the voice + provider?: string; // Voice provider (e.g., "Microsoft", "Google") + + // Allow any additional properties that might be in the JSON + [key: string]: any; +} ``` -#### filterOn +#### `LanguageInfo` ```typescript -function filterOnRecommended(voices: ReadiumSpeechVoices[], _recommended?: IRecommended[]): TReturnFilterOnRecommended +interface LanguageInfo { + code: string; + label: string; + count: number; +} +``` -function filterOnVeryLowQuality(voices: ReadiumSpeechVoices[]): ReadiumSpeechVoices[] +### Enums -function filterOnNovelty(voices: ReadiumSpeechVoices[]): ReadiumSpeechVoices[] +#### `TQuality` -function filterOnQuality(voices: ReadiumSpeechVoices[], quality: TQuality | TQuality[]): ReadiumSpeechVoices[] +```typescript +type TQuality = "veryLow" | "low" | "normal" | "high" | "veryHigh"; +``` -function filterOnLanguage(voices: ReadiumSpeechVoices[], language: string | string[]): ReadiumSpeechVoices[] +#### `TGender` -function filterOnGender(voices: ReadiumSpeechVoices[], gender: TGender): ReadiumSpeechVoices[] +```typescript +type TGender = "female" | "male" | "neutral"; ``` ## Playback API diff --git a/ava.config.js b/ava.config.js index e84e800..0129c8a 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,11 +1,8 @@ export default { extensions: { - ts: 'module' - }, - environmentVariables: { - TS_NODE_COMPILER_OPTIONS: '{"module":"ES2022"}' + ts: "module" }, nodeArguments: [ - '--loader=ts-node/esm' + "--loader=ts-node/esm" ] } diff --git a/demo/article/index.html b/demo/article/index.html new file mode 100644 index 0000000..fb692b4 --- /dev/null +++ b/demo/article/index.html @@ -0,0 +1,51 @@ + + + + + + Speech Synthesis - Article Demo + + + +
+
+ + +
+
+ + + + +
+
+ + +
+
+ Utterance: 1/- +
+
+ +
+

Speech Synthesis

+ +

Speech synthesis is the artificial production of human speech. A computer system used for this purpose is called a speech synthesizer, and can be implemented in software or hardware products. A text-to-speech (TTS) system converts normal language text into speech; other systems render symbolic linguistic representations like phonetic transcriptions into speech.

+ +

Synthesized speech can be created by concatenating pieces of recorded speech that are stored in a database. Systems differ in the size of the stored speech units; a system that stores phones or diphones provides the largest output range, but may lack clarity. For specific usage domains, the storage of entire words or sentences allows for high-quality output. Alternatively, a synthesizer can incorporate a model of the vocal tract and other human voice characteristics to create a completely "synthetic" voice output.

+ +

The quality of a speech synthesizer is judged by its similarity to the human voice and by its ability to be understood clearly. An intelligible text-to-speech program allows people with visual impairments or reading disabilities to listen to written words on a home computer. The earliest computer operating system to have included a speech synthesizer was Unix in 1974, through the Unix speak utility. In 2000, Microsoft Sam was the default text-to-speech voice synthesizer used by the narrator accessibility feature, which shipped with all Windows 2000 operating systems, and subsequent Windows XP systems.

+ +

A text-to-speech system (or "engine") is composed of two parts: a front-end and a back-end. The front-end has two major tasks. First, it converts raw text containing symbols like numbers and abbreviations into the equivalent of written-out words. This process is often called text normalization, pre-processing, or tokenization. The front-end then assigns phonetic transcriptions to each word, and divides and marks the text into prosodic units, like phrases, clauses, and sentences. The process of assigning phonetic transcriptions to words is called text-to-phoneme or grapheme-to-phoneme conversion. Phonetic transcriptions and prosody information together make up the symbolic linguistic representation that is output by the front-end. The back-end—often referred to as the synthesizer—then converts the symbolic linguistic representation into sound. In certain systems, this part includes the computation of the target prosody (pitch contour, phoneme durations), which is then imposed on the output speech.

+
+ + + + + + diff --git a/demo/article/script.js b/demo/article/script.js new file mode 100644 index 0000000..6d8baea --- /dev/null +++ b/demo/article/script.js @@ -0,0 +1,491 @@ +import { WebSpeechVoiceManager, WebSpeechReadAloudNavigator } from "../../build/index.js"; + +// DOM Elements +const content = document.getElementById("content"); +const voiceSelect = document.getElementById("voiceSelect"); +const playPauseBtn = document.getElementById("playPauseBtn"); +const stopBtn = document.getElementById("stopBtn"); +const prevBtn = document.getElementById("prevBtn"); +const nextBtn = document.getElementById("nextBtn"); +const currentUtteranceSpan = document.getElementById("currentUtterance"); +const totalUtterancesSpan = document.getElementById("totalUtterances"); +const readAlongCheckbox = document.getElementById("readAlong"); + +// State +let voiceManager; +let navigator; +let allVoices = []; +let currentVoice = null; +let isPlaying = false; +let utterances = []; +let currentWordHighlight = null; +let readAlongEnabled = true; // Default to true to match default checkbox state + +// Initialize voice manager and navigator +async function initialize() { + try { + // Initialize the voice manager + voiceManager = await WebSpeechVoiceManager.initialize(); + + // Only get English voices + allVoices = voiceManager.getVoices({language: "en"}); + + // Initialize the navigator + navigator = new WebSpeechReadAloudNavigator(); + + // Set up event listeners + setupEventListeners(); + + // Initialize the UI + updateUI(); + + // Populate voice select + populateVoiceSelect(); + + // Get the default voice for English + currentVoice = voiceManager.getDefaultVoice("en-US"); + + if (currentVoice && navigator) { + navigator.setVoice(currentVoice); + // Update the select element to reflect the selected voice + if (voiceSelect) { + const option = voiceSelect.querySelector(`option[data-voice-uri="${currentVoice.voiceURI}"]`); + if (option) { + option.selected = true; + } + } + } + + // Initialize content + await initializeContent(); + + } catch (error) { + console.error("Initialization error:", error); + } +} + +// Set up event listeners +function setupEventListeners() { + + // Navigator events + navigator.on("start", () => { + isPlaying = true; + updateUI(); + }); + + navigator.on("pause", () => { + isPlaying = false; + updateUI(); + }); + + navigator.on("stop", () => { + isPlaying = false; + clearWordHighlighting(); + updateUI(); + }); + + navigator.on("end", () => { + isPlaying = false; + clearWordHighlighting(); + updateUI(); + }); + + navigator.on("error", (event) => { + console.error("Navigator error:", event.detail); + updateUI(); + }); + + navigator.on("boundary", (event) => { + if (event.detail && event.detail.name === "word") { + highlightCurrentWord(event.detail.charIndex, event.detail.charLength); + } + updateUI(); + }); + + // Button events + if (playPauseBtn) playPauseBtn.addEventListener("click", togglePlayback); + if (stopBtn) stopBtn.addEventListener("click", stopPlayback); + if (prevBtn) prevBtn.addEventListener("click", previousUtterance); + if (nextBtn) nextBtn.addEventListener("click", nextUtterance); + + // Checkbox events + if (readAlongCheckbox) { + readAlongCheckbox.checked = readAlongEnabled; + readAlongCheckbox.addEventListener("change", handleReadAlongChange); + } + + // Voice selection + if (voiceSelect) voiceSelect.addEventListener("change", handleVoiceChange); +} + +// Handle read along checkbox change +function handleReadAlongChange(e) { + readAlongEnabled = e.target.checked; + if (!readAlongEnabled) { + clearWordHighlighting(); + } else if (isPlaying) { + const currentIndex = navigator?.getCurrentUtteranceIndex(); + if (currentIndex !== undefined) { + const utterance = utterances[currentIndex]; + if (utterance) { + const charIndex = utterance.text.indexOf(utterance.word); + if (charIndex !== -1) { + highlightCurrentWord(charIndex, utterance.word?.length || 0); + } + } + } + } +} + +// Initialize content with proper segmentation +async function initializeContent() { + const paragraphs = Array.from(content.querySelectorAll("p, h1, h2, h3, h4, h5, h6")); + utterances = []; + + // Process each paragraph/heading + paragraphs.forEach((p) => { + const text = p.textContent; + if (!text.trim()) return; + + // Use Intl.Segmenter for sentence segmentation + const segmenter = new Intl.Segmenter("en", { granularity: "sentence" }); + const segments = Array.from(segmenter.segment(text)); + + // Process each sentence + segments.forEach(({ segment }) => { + const sentence = segment.trim(); + if (!sentence) return; + + // Add to utterances + utterances.push({ + id: `utterance-${utterances.length}`, + text: sentence, + language: "en" + }); + }); + }); + + // Load utterances into the navigator + await navigator.loadContent(utterances); + + // Update UI + updateUI(); +} + +// Populate voice select dropdown +function populateVoiceSelect() { + if (!voiceSelect) return; + + voiceSelect.innerHTML = ""; + + if (!allVoices || !allVoices.length) { + const option = document.createElement("option"); + option.disabled = true; + option.textContent = "No voices available. Please check your browser settings and internet connection."; + voiceSelect.appendChild(option); + return; + } + + try { + const sortedVoices = voiceManager.sortVoices(allVoices, { + by: "region", + order: "asc", + preferredLanguages: window.navigator.languages + }); + + let currentRegion = null; + let optgroup = null; + + for (const voice of sortedVoices) { + // Extract region from language code (e.g., "US" from "en-US") + const region = voice.language.split("-")[1] || "Other"; + + // Create new optgroup when region changes + if (region !== currentRegion) { + currentRegion = region; + optgroup = document.createElement("optgroup"); + // Add emoji flag before the region code using the existing helper function + const flag = getCountryFlag(region === "Other" ? null : region); + optgroup.label = `${flag} ${region}`; + voiceSelect.appendChild(optgroup); + } + + const option = document.createElement("option"); + option.value = voice.voiceURI; + option.textContent = `${voice.label || voice.name}`; + option.dataset.voiceUri = voice.voiceURI; + + if (currentVoice && voice.voiceURI === currentVoice.voiceURI) { + option.selected = true; + } + + optgroup?.appendChild(option); + } + + // Set the default voice selection + if (currentVoice) { + const option = voiceSelect.querySelector(`option[data-voice-uri="${currentVoice.voiceURI}"]`); + if (option) { + option.selected = true; + } + } + + } catch (error) { + console.error("Error populating voice dropdown:", error); + // Fallback to simple list if there's an error + allVoices.forEach(voice => { + const option = document.createElement("option"); + option.value = voice.name; + option.textContent = [ + voice.label || voice.name, + voice.gender ? `• ${voice.gender}` : "", + voice.offlineAvailability ? "• offline" : "• online" + ].filter(Boolean).join(" "); + option.dataset.voiceUri = voice.voiceURI; + voiceSelect.appendChild(option); + }); + } + + // Set up voice change event listener + voiceSelect.addEventListener("change", handleVoiceChange); + + // Helper function to get country flag emoji from country code + function getCountryFlag(countryCode) { + if (!countryCode) return "🌐"; + + try { + const codePoints = countryCode + .toUpperCase() + .split("") + .map(char => 127397 + char.charCodeAt(0)); + + return String.fromCodePoint(...codePoints); + } catch (e) { + console.warn("Could not generate flag for country code:", countryCode); + return "🌐"; + } + } +} + +// Toggle sample text playback +async function togglePlayback() { + if (!currentVoice) { + console.error("No voice selected"); + return; + } + + try { + const state = navigator.getState(); + if (state === "playing") { + await navigator.pause(); + } else if (state === "paused") { + // Use play() to resume from paused state + await navigator.play(); + } else { + // Start from beginning if stopped or in an unknown state + await navigator.jumpTo(0); + await navigator.play(); + } + } catch (error) { + console.error("Error toggling playback:", error); + } +} + +function stopPlayback() { + if (!navigator) return; + navigator.stop(); + clearWordHighlighting(); + updateUI(); +} + +function previousUtterance() { + if (!navigator) return; + navigator.previous(); + updateUI(); +} + +function nextUtterance() { + if (!navigator) return; + navigator.next(); + updateUI(); +} + +// Handle voice change +async function handleVoiceChange(e) { + const voiceName = e.target.value; + if (!voiceName) return; + + // Find the selected voice by name + currentVoice = allVoices.find(v => v.name === voiceName); + + if (!currentVoice) { + console.error("Voice not found:", voiceName); + return; + } + + // Stop any current playback + if (navigator) { + try { + // Stop the current speech + await navigator.stop(); + + // Set the new voice + navigator.setVoice(currentVoice); + + // Update UI to reflect the change + updateUI(); + + } catch (error) { + console.error("Error changing voice:", error); + } + } +} + +// Clear any previous highlighting +function clearWordHighlighting() { + if (window.CSS?.highlights) { + CSS.highlights.clear(); + } +} + +// Highlight current word in the content +function highlightCurrentWord(charIndex, charLength) { + // Check if read-along is enabled + if (!readAlongEnabled) return; + + // Clear previous highlighting + clearWordHighlighting(); + + // Get the current utterance + const currentIndex = navigator.getCurrentUtteranceIndex(); + const currentUtterance = utterances[currentIndex]; + if (!currentUtterance) return; + + // Get the content element + const contentElement = document.getElementById("content"); + if (!contentElement) return; + + // Create a range for the current word + const range = document.createRange(); + const walker = document.createTreeWalker( + contentElement, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let node; + let found = false; + + while ((node = walker.nextNode())) { + const nodeText = node.nodeValue; + const nodeLength = nodeText.length; + + // Check if this node contains the current utterance + const utteranceText = currentUtterance.text; + const nodeStart = nodeText.indexOf(utteranceText); + + if (nodeStart !== -1) { + // Calculate the position within this node + const startPos = nodeStart + charIndex; + const endPos = Math.min(startPos + charLength, nodeLength); + + // Ensure the range is valid + if (startPos >= 0 && endPos <= nodeLength) { + try { + range.setStart(node, startPos); + range.setEnd(node, endPos); + + // Use CSS Highlight API + const highlight = new Highlight(range); + if (window.CSS?.highlights) { + CSS.highlights.set("current-word", highlight); + } + + // Store current highlight info + currentWordHighlight = { + utteranceIndex: currentIndex, + charIndex: charIndex, + charLength: charLength, + range: range + }; + + // Scroll the highlighted word into view with smooth behavior + const rect = range.getBoundingClientRect(); + const isVisible = ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + + if (!isVisible) { + range.startContainer.parentElement.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest" + }); + } + + found = true; + } catch (e) { + console.error("Error setting highlight range:", e); + } + } + break; + } + } + + if (!found) { + console.warn("Could not find position for highlight"); + } +} + +// Update UI +function updateUI() { + if (!navigator) return; + + const currentIndex = navigator.getCurrentUtteranceIndex(); + const total = utterances.length; + const state = navigator.getState(); + const hasContent = total > 0; + + // Update play/pause button + if (playPauseBtn) { + playPauseBtn.disabled = !currentVoice || !hasContent; + if (state === "playing") { + playPauseBtn.innerHTML = `⏸️ Pause`; + playPauseBtn.classList.remove("play-state"); + playPauseBtn.classList.add("pause-state"); + } else { + playPauseBtn.innerHTML = `▶️ Play`; + playPauseBtn.classList.remove("pause-state"); + playPauseBtn.classList.add("play-state"); + } + } + + // Update other buttons + if (stopBtn) { + stopBtn.disabled = !currentVoice || !hasContent || (state !== "playing" && state !== "paused"); + } + + if (prevBtn) { + prevBtn.disabled = !currentVoice || !hasContent || currentIndex <= 0; + } + + if (nextBtn) { + nextBtn.disabled = !currentVoice || !hasContent || currentIndex >= total - 1; + } + + // Update utterance counter + if (currentUtteranceSpan) { + currentUtteranceSpan.textContent = currentIndex + 1; + } + + if (totalUtterancesSpan) { + totalUtterancesSpan.textContent = total; + } +} + +// Initialize the application +initialize().catch(console.error); \ No newline at end of file diff --git a/demo/article/styles.css b/demo/article/styles.css new file mode 100644 index 0000000..cdf486b --- /dev/null +++ b/demo/article/styles.css @@ -0,0 +1,192 @@ +/* Base styles */ +body, +html { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: 1.6; + color: #333; + max-width: 800px; + margin: 0 auto; + padding: 20px; + box-sizing: border-box; +} + +/* Control panel */ +.controls { + position: sticky; + top: 0; + background: #f5f5f5; + padding: 20px; + border-radius: 8px; + margin: 0 0 20px 0; + width: 100%; + z-index: 100; + box-sizing: border-box; +} + +/* Form elements */ +.control-group { + margin-bottom: 15px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} + +label { + font-weight: 500; + margin: 0; + cursor: pointer; +} + +input[type="checkbox"] { + margin: 0; + width: auto; +} + +select { + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + min-width: 200px; +} + +/* Base button styles */ +button { + padding: 8px 16px; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + cursor: pointer; + transition: opacity 0.2s; + font-size: 14px; + margin: 0; + display: inline-flex; + align-items: center; + gap: 6px; +} + +/* Button icons and text */ +.btn-icon { + display: inline-block; + width: 20px; + text-align: center; +} + +.btn-text { + display: inline-block; +} + +button:disabled { + background-color: #cccccc !important; + cursor: not-allowed; + opacity: 0.7; +} + +button:hover:not(:disabled) { + opacity: 0.9; +} + +/* Play/Pause button */ +#playPauseBtn { + background-color: #4CAF50; /* Green for play */ +} + +#playPauseBtn.paused { + background-color: #ff9800; /* Orange for pause */ +} + +#playPauseBtn:hover:not(:disabled) { + background-color: #45a049; /* Darker green on hover */ +} + +#playPauseBtn.paused:hover:not(:disabled) { + background-color: #e68900; /* Darker orange on hover */ +} + +/* Stop button */ +#stopBtn { + background-color: #f44336; /* Red for stop */ +} + +#stopBtn:hover:not(:disabled) { + background-color: #d32f2f; /* Darker red on hover */ +} + +/* Navigation buttons */ +#prevBtn, +#nextBtn { + background-color: #2196F3; /* Blue for navigation */ +} + +#prevBtn:hover:not(:disabled), +#nextBtn:hover:not(:disabled) { + background-color: #1976D2; /* Darker blue on hover */ +} + +/* Utterance counter */ +#currentUtterance { + font-weight: bold; + color: #2196F3; +} + +/* Content area */ +#content { + line-height: 1.8; +} + +#content p { + margin-bottom: 1.2em; +} + +/* Highlight for current word */ +::highlight(current-word) { + background-color: #ffeb3b; + color: black; +} + +/* Footer */ +footer { + margin: 2em 0 1em; + padding: 1em 0 0; + border-top: 1px solid #e0e0e0; + font-size: 0.8em; + color: #666; + line-height: 1.5; + max-width: 800px; + margin-left: auto; + margin-right: auto; + padding-left: 1em; + padding-right: 1em; +} + +footer p { + margin: 0.3em 0; +} + +footer a { + color: #1a73e8; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .control-group { + flex-direction: column; + align-items: flex-start; + } + + select { + width: 100%; + } + + button { + padding: 8px 12px; + font-size: 13px; + } +} \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 58f63ca..169d5a4 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,15 +1,89 @@ - Readium Speech Demo - - + - - +

Readium Speech Demo

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
- \ No newline at end of file +
+
+
+ + +
+
+ +
+
+
+ +
+
+ Voice Details +
+

Select a voice to see its properties

+
+
+
+
+ +
+
+
+ + + + +
+ +
+ + of - + +
+
+ +
+
+ Select a language to load sample text... +
+
+
+ + + + diff --git a/demo/lit-html_3-2-0_esm.js b/demo/lit-html_3-2-0_esm.js deleted file mode 100644 index 93b0419..0000000 --- a/demo/lit-html_3-2-0_esm.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2. - * Original file: /npm/lit-html@3.2.0/lit-html.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -const t=globalThis,e=t.trustedTypes,s=e?e.createPolicy("lit-html",{createHTML:t=>t}):void 0,i="$lit$",n=`lit$${Math.random().toFixed(9).slice(2)}$`,o="?"+n,r=`<${o}>`,h=document,l=()=>h.createComment(""),$=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,A=t=>a(t)||"function"==typeof t?.[Symbol.iterator],c="[ \t\n\f\r]",_=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,d=/-->/g,p=/>/g,u=RegExp(`>|${c}(?:([^\\s"'>=/]+)(${c}*=${c}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,v=/"/g,f=/^(?:script|style|textarea|title)$/i,m=t=>(e,...s)=>({_$litType$:t,strings:e,values:s}),y=m(1),H=m(2),x=m(3),N=Symbol.for("lit-noChange"),T=Symbol.for("lit-nothing"),b=new WeakMap,M=h.createTreeWalker(h,129);function w(t,e){if(!a(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s?s.createHTML(e):e}const S=(t,e)=>{const s=t.length-1,o=[];let h,l=2===e?"":3===e?"":"",$=_;for(let e=0;e"===A[0]?($=h??_,c=-1):void 0===A[1]?c=-2:(c=$.lastIndex-A[2].length,a=A[1],$=void 0===A[3]?u:'"'===A[3]?v:g):$===v||$===g?$=u:$===d||$===p?$=_:($=u,h=void 0);const y=$===u&&t[e+1].startsWith("/>")?" ":"";l+=$===_?s+r:c>=0?(o.push(a),s.slice(0,c)+i+s.slice(c)+n+y):s+n+(-2===c?e:y)}return[w(t,l+(t[s]||"")+(2===e?"":3===e?"":"")),o]};class I{constructor({strings:t,_$litType$:s},r){let h;this.parts=[];let $=0,a=0;const A=t.length-1,c=this.parts,[_,d]=S(t,s);if(this.el=I.createElement(_,r),M.currentNode=this.el.content,2===s||3===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(h=M.nextNode())&&c.length0){h.textContent=e?e.emptyScript:"";for(let e=0;e2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=T}_$AI(t,e=this,s,i){const n=this.strings;let o=!1;if(void 0===n)t=C(this,t,e,0),o=!$(t)||t!==this._$AH&&t!==N,o&&(this._$AH=t);else{const i=t;let r,h;for(t=n[0],r=0;r{const i=s?.renderBefore??e;let n=i._$litPart$;if(void 0===n){const t=s?.renderBefore??null;i._$litPart$=n=new B(e.insertBefore(l(),t),t,void 0,s??{})}return n._$AI(t),n};export{W as _$LH,y as html,x as mathml,N as noChange,T as nothing,D as render,H as svg};export default null; \ No newline at end of file diff --git a/demo/navigator/index.html b/demo/navigator/index.html deleted file mode 100644 index 4830a35..0000000 --- a/demo/navigator/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Readium Speech Navigator Demo - - - - - - - - diff --git a/demo/navigator/navigator-demo-script.js b/demo/navigator/navigator-demo-script.js deleted file mode 100644 index 1959d61..0000000 --- a/demo/navigator/navigator-demo-script.js +++ /dev/null @@ -1,684 +0,0 @@ -import { WebSpeechReadAloudNavigator } from "../../build/index.js"; - -import * as lit from "../lit-html_3-2-0_esm.js" -const { html, render } = lit; - -// Sample text from Moby Dick by Herman Melville -const sampleText = ` -Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. - -Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people's hats off—then, I account it high time to get to sea as soon as I can. - -This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me. - -There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf. Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there.`; - -// Create navigator instance -const navigator = new WebSpeechReadAloudNavigator(); - -// Main render function -const viewRender = () => { - const state = { - isPlaying: navigator.getState() === "playing", - currentUtteranceIndex: navigator.getCurrentUtteranceIndex() || 0, - totalUtterances: navigator.getContentQueue().length, - currentVoice: navigator.getCurrentVoice() - }; - - render(content(state), document.body); - - // Update input field only if user hasn't manually changed it - updateJumpInputIfNeeded(state.currentUtteranceIndex + 1); - - // Initialize position tracking on first render - if (lastNavigatorPosition === 0) { - lastNavigatorPosition = state.currentUtteranceIndex + 1; - } -}; - -// Split text into sentences for utterances -function createUtterancesFromText(text) { - // Split by sentences (basic implementation) - const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); - return sentences.map((sentence, index) => ({ - id: `utterance-${index}`, - text: sentence.trim() + (sentence.endsWith(".") || sentence.endsWith("!") || sentence.endsWith("?") ? "" : "."), - language: "en-US" - })); -} - -const utterances = createUtterancesFromText(sampleText); -console.log(`Created ${utterances.length} utterances`); - -let voices = []; -let currentVoice = null; -let currentWordHighlight = null; // Track current word being highlighted - -// Initialize voices -async function initVoices() { - try { - // Get all voices - voices = await navigator.getVoices(); - // Filter for English voices - const englishVoices = voices.filter(v => v.language.startsWith("en")); - // Set the first English voice as default, or fallback to first available - currentVoice = englishVoices.length > 0 ? englishVoices[0] : voices[0]; - - if (currentVoice) { - await navigator.setVoice(currentVoice); - } - - // Re-render to show the voice selector - viewRender(); - } catch (error) { - console.error("Error initializing voices:", error); - } -} - -// Handle voice selection -async function handleVoiceChange(event) { - const voiceName = event.target.value; - const selectedVoice = voices.find(v => v.name === voiceName); - if (selectedVoice) { - // Stop any ongoing speech before changing the voice - navigator.stop(); - currentVoice = selectedVoice; - await navigator.setVoice(selectedVoice); - // The view will be updated automatically through the state change events - } -} - -// Load utterances into navigator and initialize voices -navigator.loadContent(utterances); -initVoices(); - -// Input value management -let jumpInputUserChanged = false; -let lastNavigatorPosition = 0; - -function updateJumpInputIfNeeded(navigatorPosition) { - const input = document.getElementById("utterance-index"); - if (!input) return; - - // If user has changed the input, don't update it - if (jumpInputUserChanged) { - return; - } - - // Only update if position actually changed - if (navigatorPosition !== lastNavigatorPosition) { - input.value = navigatorPosition; - lastNavigatorPosition = navigatorPosition; - } -} - -// Track when user manually changes the input -const jumpInput = document.getElementById("utterance-index"); -if (jumpInput) { - jumpInput.addEventListener("input", () => { - jumpInputUserChanged = true; - }); - - // Set initial value once input is ready - jumpInput.addEventListener("focus", () => { - if (jumpInput.value === "" && !jumpInputUserChanged) { - const currentPos = navigator.getCurrentUtteranceIndex() + 1; - jumpInput.value = currentPos; - lastNavigatorPosition = currentPos; - } - }, { once: true }); -} - -// Event listeners for navigator -navigator.on("start", () => { - clearWordHighlighting(); // Clear any previous highlighting - viewRender(); -}); - -navigator.on("pause", () => { - viewRender(); -}); - -navigator.on("resume", () => { - viewRender(); -}); - -navigator.on("stop", () => { - clearWordHighlighting(); - viewRender(); -}); - -navigator.on("end", () => { - viewRender(); -}); - -navigator.on("error", (event) => { - console.error("Navigator error:", event.detail); - viewRender(); -}); - -navigator.on("skip", (event) => { - // Update the UI when the position changes programmatically - const newPosition = (event.detail?.position ?? 0) + 1; // Convert to 1-based index for display - lastNavigatorPosition = newPosition; - const input = document.getElementById("utterance-index"); - if (input && input !== document.activeElement) { - input.value = newPosition; - } - viewRender(); -}); - -navigator.on("boundary", (event) => { - // Handle word boundaries for highlighting - if (event.detail.name === "word") { - highlightCurrentWord(event.detail.charIndex, event.detail.charLength); - } - viewRender(); -}); - -// Playback control functions -const playPause = async () => { - const state = navigator.getState(); - if (state === "playing") { - navigator.pause(); - } else { - navigator.play(); - } -}; - -const stop = () => { - clearWordHighlighting(); - navigator.stop(); -}; - -const next = async () => { - clearWordHighlighting(); - navigator.next(); -}; - -const previous = async () => { - clearWordHighlighting(); - navigator.previous(); -}; - -const jumpToUtterance = () => { - const input = document.getElementById("utterance-index"); - const index = parseInt(input.value) - 1; // Convert to 0-based index - if (index >= 0 && index < navigator.getContentQueue().length) { - clearWordHighlighting(); - navigator.jumpTo(index); - // Clear user changed flag and update position tracking - jumpInputUserChanged = false; - lastNavigatorPosition = index + 1; - // Update input to reflect the new position - input.value = lastNavigatorPosition; - } else { - alert(`Please enter a number between 1 and ${navigator.getContentQueue().length}`); - // Reset input to current position and clear user changed flag - const currentPos = navigator.getCurrentUtteranceIndex() + 1; - input.value = currentPos; - jumpInputUserChanged = false; - lastNavigatorPosition = currentPos; - } -}; - -function highlightCurrentWord(charIndex, charLength) { - // Clear previous highlighting - clearWordHighlighting(); - - // Find the current utterance being spoken - const currentUtterance = navigator.getCurrentContent(); - if (!currentUtterance) return; - - // Extract the word based on character index and length - const text = currentUtterance.text; - if (charIndex >= 0 && charIndex < text.length) { - const wordEnd = Math.min(charIndex + charLength, text.length); - const word = text.substring(charIndex, wordEnd); - - // Find the specific occurrence of this word at this position - highlightSpecificWord(text, word, charIndex); - - currentWordHighlight = { - utteranceIndex: navigator.getCurrentUtteranceIndex(), - charIndex: charIndex, - charLength: charLength, - word: word - }; - } -} - -function highlightSpecificWord(fullText, targetWord, startIndex) { - const utteranceElements = document.querySelectorAll('.utterance-text'); - const currentUtteranceIndex = navigator.getCurrentUtteranceIndex(); - - if (utteranceElements.length > currentUtteranceIndex) { - const currentElement = utteranceElements[currentUtteranceIndex]; - if (currentElement) { - // Find the specific occurrence of the word at the given character position - const beforeText = fullText.substring(0, startIndex); - const afterText = fullText.substring(startIndex + targetWord.length); - - // Reconstruct the HTML with only the specific word highlighted - currentElement.innerHTML = - beforeText + - '' + targetWord + '' + - afterText; - } - } -} - -function clearWordHighlighting() { - // Remove all word highlighting - const highlightedWords = document.querySelectorAll('.highlighted-word'); - highlightedWords.forEach(el => { - el.outerHTML = el.textContent; - }); - - currentWordHighlight = null; -} - -// UI Components -const content = (state) => { - // Show loading state if navigator isn't ready - if (navigator.getState() === "loading" || !state) { - return html` -
-

Readium Speech Navigator Demo

-
-

Loading speech engine...

-
-
`; - } - - return html` -
-

Readium Speech Navigator Demo

- -
-
-

Voice Settings

- ${voices.length > 0 ? html` -
- - -
-
-
- Voice Details -
- ${(() => { - const voice = navigator.getCurrentVoice(); - if (!voice) return html`

No voice selected

`; - - // Get all properties from the voice object - const voiceProps = []; - - // Add all properties from the voice object - for (const [key, value] of Object.entries(voice)) { - if (key.startsWith("_")) continue; - - let displayValue = value; - if (value === undefined) displayValue = "undefined"; - else if (value === null) displayValue = "null"; - else if (typeof value === "boolean") displayValue = value ? "Yes" : "No"; - else if (typeof value === "object") displayValue = JSON.stringify(value); - - voiceProps.push({ key, value: displayValue }); - } - - return html` -
- ${voiceProps.map(({key, value}) => html` -
-
${key}:
-
${value}
-
- `)} -
- `; - })()} -
-
- - -
- ` : html`
Loading voices...
`} -
-
- -
- - - - -
- -
- - of ${state.totalUtterances} - -
- -
-

State: ${navigator.getState()}

-
- -
-

Content Preview

-
- ${navigator.getContentQueue().map((utterance, index) => html` -
- ${index + 1}. - ${utterance.text} -
- `)} -
-
- - -`; -}; - -// Initial render with loading state -viewRender(); - -// Re-render once voices are loaded -initVoices().then(() => viewRender()); diff --git a/demo/sampleText.json b/demo/sampleText.json new file mode 100644 index 0000000..bc84135 --- /dev/null +++ b/demo/sampleText.json @@ -0,0 +1,182 @@ +{ + "ar": { + "language": "Arabic", + "text": "كانت أليس تبدأ بالشعور بالتعب الشديد من الجلوس بجانب أختها على الضفة. مرة أو مرتين كانت قد ألقت نظرة خاطفة على الكتاب الذي كانت أختها تقرأه، لكنه لم يكن يحتوي على صور أو محادثات. 'وما فائدة الكتاب،' فكرت أليس، 'بدون صور أو محادثات؟' لذا كانت تفكر في نفسها ما إذا كانت متعة صنع سلسلة أزهار الأقحوان تستحق عناء النهوض وقطف الأقحوان. وفجأة ركض أرنب أبيض بعينين ورديتين بالقرب منها. لم يكن هناك ما يستحق الذكر كثيرًا في ذلك؛ ولم تظن أليس أنه غريب جدًا أن تسمع الأرنب يقول لنفسه، 'يا إلهي! يا إلهي! سأكون متأخرة!' لكن عندما أخرج الأرنب ساعة من جيب صدر سترته ونظر إليها، قفزت أليس على قدميها. لم ترَ من قبل أرنبًا لديه جيب صدر أو ساعة ليخرجها." + }, + "bg": { + "language": "Bulgarian", + "text": "Алиса започваше да се уморява много от това да седи до сестра си на брега. Веднъж или два пъти тя поглеждаше в книгата, която сестра ѝ четеше, но тя нямаше нито картинки, нито разговори. „И каква е ползата от книга,“ мислеше Алиса, „без картинки или разговори?“ Така че тя обмисляше в ума си дали удоволствието да направи гирлянда от маргаритки ще си заслужава усилието да се изправи и да бере маргаритките. И изведнъж бял заек с розови очи пробяга покрай нея. Нямаше нищо толкова забележително в това; нито Алиса смяташе, че е толкова необичайно да чуе заека да казва на себе си: „О, Боже! О, Боже! Ще закъснея!“ Но когато заекът всъщност извади часовник от джоба на жилетката си и го погледна, Алиса скочи на крака си." + }, + "bho": { + "language": "Bhojpuri", + "text": "एलिस आपन बहिन का पास किनारे पर बइठल-बइठल बहुत थाक गइल रही। एक-दू बार उ बहिन जे किताब पढ़त रहली ओकरा में झांकली, बाकिर ओह में कवनो चित्र या बातचीत ना रहे। 'और किताब के फायदा का बा,' एलिस सोचली, 'बिना चित्र या बातचीत के?' त ओह सोच में पड़ गइल की फूल के माला बनावे के मज़ा उठावे खातिर उठ के फूल तोड़े के मेहनत करल जाय कि ना। अचानक एगो गोरो रंग के खरगोश गुलाबी आंख के पास से दौड़ल। एकर कवनो खास बात ना रहल; न एलिस ई सोचली की खरगोश के अपने से कहत सुनल अजीब बा, 'हे भगवान! हे भगवान! हम देर हो जाएब!' बाकिर जब खरगोश वाकई में अपनी जेब से घड़ी निकाललस आ ओकरा देखलस, एलिस तुरंते खड़ा हो गइल।" + }, + "bn": { + "language": "Bengali", + "text": "অ্যালিস তার বোনের পাশে নদীর ধারে বসে বসে খুব ক্লান্ত হয়ে যাচ্ছিল। এক-দুইবার সে তার বোন যে বই পড়ছিল তাতে চেয়ে দেখেছিল, কিন্তু এতে কোনো ছবি বা কথোপকথন ছিল না। 'একটি বইয়ের ব্যবহার কী,' অ্যালিস ভাবল, 'ছবি বা কথোপকথন ছাড়া?' তাই সে নিজের মনে বিবেচনা করছিল যে দাইজি-চেইন বানানো আনন্দের মূল্য কি বসে উঠে দাইজি তোলা ঝামেলার সমান। হঠাৎ করেই একটি সাদা খরগোশ গোলাপি চোখ নিয়ে তার পাশ দিয়ে ছুটে গেল। এতে তেমন কিছু আশ্চর্যজনক কিছু ছিল না; অ্যালিসও মনে করল না এটি অস্বাভাবিক যে খরগোশ নিজের সঙ্গে বলল, 'ওহ দ্যাখ! ওহ দ্যাখ! আমি দেরি হয়ে যাব!' কিন্তু যখন খরগোশ সত্যিই তার কোমরের পকেট থেকে একটি ঘড়ি বের করল এবং তাকাল, অ্যালিস লাফিয়ে দাঁড়াল।" + }, + "ca": { + "language": "Catalan", + "text": "A l'Alice li començava a cansar molt seure al costat de la seva germana a la riba. Un parell de vegades havia mirat el llibre que la seva germana llegia, però no tenia ni imatges ni converses. 'I de què serveix un llibre,' pensava l'Alice, 'sense imatges ni converses?' Així que es preguntava si el plaer de fer una cadena de margarides valia la pena de llevar-se i recollir les margarides. De sobte, un Conill Blanc amb els ulls roses va passar corrent a prop seu. No hi havia res de tan extraordinari en això; ni l'Alice trobava tan fora del normal sentir el conill dir-se a si mateix: 'Ai, Déu! Ai, Déu! Arribaré tard!' Però quan el Conill va treure realment un rellotge del seu butxaca de jupó i el va mirar, l'Alice es va aixecar de cop." + }, + "cmn": { + "language": "Mandarin Chinese", + "text": "爱丽丝开始感到坐在河岸上陪着她姐姐非常无聊。有一两次,她偷看了她姐姐正在读的书,但书中没有图片或对话。‘一本没有图片或对话的书有什么用呢,’爱丽丝想。于是她在心里考虑制作一串雏菊花链的乐趣是否值得起身去采摘雏菊。突然,一只粉眼睛的白兔跑到她身边。这没什么特别的;爱丽丝也不觉得听到兔子自言自语‘天哪!天哪!我要迟到了!’有什么奇怪。但当兔子真的从马甲口袋里掏出一块表看时,爱丽丝立刻跳了起来。" + }, + "cs": { + "language": "Czech", + "text": "Alice začínala být velmi unavená z toho, že seděla u své sestry na břehu. Jednou nebo dvakrát nahlédla do knihy, kterou její sestra četla, ale neměla v ní žádné obrázky ani rozhovory. 'A k čemu je kniha,' přemýšlela Alice, 'bez obrázků nebo rozhovorů?' Tak přemýšlela, zda potěšení z výroby řetízku z kopretin stojí za to vstát a sbírat kopretiny. Najednou kolem ní běžel Bílý králík s růžovýma očima. Na tom nebylo nic tak zvláštního; ani Alice nepovažovala za tak divné slyšet, jak králík říká sám sobě: 'Ach ne! Ach ne! Budu pozdě!' Ale když králík skutečně vytáhl hodinky z kapsy na vestě a podíval se na ně, Alice vyskočila na nohy." + }, + "da": { + "language": "Danish", + "text": "Alice begyndte at blive meget træt af at sidde ved siden af sin søster på bredden. En eller to gange havde hun kigget i den bog, hendes søster læste, men den havde ingen billeder eller samtaler i sig. 'Og hvad er nytten af en bog,' tænkte Alice, 'uden billeder eller samtaler?' Så hun overvejede for sig selv, om glæden ved at lave en margeritkæde ville være værd besværet ved at rejse sig og plukke margeritter. Pludselig løb en Hvid Kanin med lyserøde øjne forbi hende. Der var ikke noget særligt bemærkelsesværdigt ved det; Alice syntes heller ikke, det var så mærkeligt at høre kaninen sige til sig selv: 'Åh kære! Åh kære! Jeg kommer for sent!' Men da kaninen faktisk tog et ur op af sin vestlomme og kiggede på det, sprang Alice op på sine fødder." + }, + "de": { + "language": "German", + "text": "Alice begann sehr müde zu werden, neben ihrer Schwester am Ufer zu sitzen. Ein- oder zweimal hatte sie in das Buch ihrer Schwester hineingeschaut, aber es hatte keine Bilder oder Gespräche. ‚Und was nützt ein Buch,‘ dachte Alice, ‚ohne Bilder oder Gespräche?‘ Also überlegte sie in ihrem Kopf, ob der Spaß daran, eine Gänseblümchenkette zu machen, den Aufwand wert sei, aufzustehen und die Gänseblümchen zu pflücken. Plötzlich rannte ein Weißes Kaninchen mit rosa Augen dicht an ihr vorbei. Daran war nichts besonders Bemerkenswertes; auch hielt Alice es nicht für sonderlich ungewöhnlich, das Kaninchen zu sich selbst sagen zu hören: ‚Oje! Oje! Ich werde zu spät kommen!‘ Aber als das Kaninchen tatsächlich eine Uhr aus seiner Westentasche zog und darauf sah, sprang Alice auf die Füße." + }, + "el": { + "language": "Greek", + "text": "Η Αλίκη άρχισε να κουράζεται πολύ από το να κάθεται δίπλα στην αδελφή της στην όχθη. Μία ή δύο φορές είχε ρίξει μια ματιά στο βιβλίο που διάβαζε η αδελφή της, αλλά δεν είχε εικόνες ή διάλογους. «Και ποιο το όφελος ενός βιβλίου,» σκέφτηκε η Αλίκη, «χωρίς εικόνες ή διάλογους;» Έτσι σκεφτόταν μέσα της αν η ευχαρίστηση να φτιάξει μια αλυσίδα από μαργαρίτες άξιζε τον κόπο να σηκωθεί και να μαζέψει τα λουλούδια. Ξαφνικά, ένα Λευκό Κουνέλι με ροζ μάτια πέρασε κοντά της. Δεν υπήρχε κάτι τόσο αξιοσημείωτο σε αυτό· ούτε η Αλίκη το βρήκε παράξενο να ακούει το κουνέλι να λέει στον εαυτό του: «Ωχ! Ωχ! Θα αργήσω!» Αλλά όταν το Κουνέλι έβγαλε πραγματικά ένα ρολόι από την τσέπη του γιλέκου του και το κοίταξε, η Αλίκη πετάχτηκε όρθια." + }, + "en": { + "language": "English", + "text": "Alice was beginning to get very tired of sitting by her sister on the bank. Once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it. ‘And what is the use of a book,’ thought Alice, ‘without pictures or conversations?’ So she was considering in her own mind whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies. When suddenly a White Rabbit with pink eyes ran close by her. There was nothing so very remarkable in that; nor did Alice think it so very much out of the way to hear the rabbit say to itself, ‘Oh dear! Oh dear! I shall be late!’ But when the Rabbit actually took a watch out of its waistcoat-pocket and looked at it, Alice jumped up on her feet. She had never before seen a rabbit with either a waistcoat-pocket, or a watch to take out of it." + }, + "es": { + "language": "Spanish", + "text": "A Alicia le empezaba a cansar mucho estar sentada junto a su hermana en la orilla. Una o dos veces había echado un vistazo al libro que su hermana estaba leyendo, pero no tenía imágenes ni conversaciones. '¿Y de qué sirve un libro,' pensaba Alicia, 'sin imágenes ni conversaciones?' Así que estaba considerando en su mente si el placer de hacer una guirnalda de margaritas valdría el esfuerzo de levantarse y recoger las margaritas. De repente, un Conejo Blanco con ojos rosas corrió cerca de ella. No había nada tan notable en eso; ni Alicia pensó que fuera tan extraño oír al conejo decirse a sí mismo: '¡Oh cielos! ¡Oh cielos! ¡Llegaré tarde!' Pero cuando el Conejo realmente sacó un reloj de su bolsillo del chaleco y lo miró, Alicia se levantó de un salto." + }, + "eu": { + "language": "Basque", + "text": "Alisek oso nekatuta hasi zen ibiltzen bere arrebaren ondoan bankuan esertzeaz. Behin edo bi aldiz begiratu zion bere arreba irakurtzen ari zen liburuari, baina ez zuen argazkirik edo elkarrizketarik. 'Eta zertarako balio du liburu batek,' pentsatu zuen Alisek, 'argazkirik edo elkarrizketarik gabe?' Beraz, bere buruan pentsatzen ari zen margarita-kate bat egiteak altxatzeko eta margarita jasotzeko emango lukeen ahalegina merezi ote zuen. Bat-batean, begi arrosadun Errabo Zuria bere ondoan korrika ibili zen. Ez zegoen horren bitxia; ez Alisek uste zuen hain arraroa zenik erraboa bere buruarentzat 'Aizu! Aizu! Berandu iritsiko naiz!' esaten entzutea. Baina Erraboak benetan bere kamiseta-poltsikotik erloju bat atera eta begiratu zuenean, Alisek jauzi egin zuen oinetan." + }, + "fa": { + "language": "Persian", + "text": "آلیس داشت از نشستن کنار خواهرش روی کرانه بسیار خسته می‌شد. یک یا دو بار به کتابی که خواهرش می‌خواند نگاه کرده بود، اما هیچ تصویر یا گفت‌وگویی در آن نبود. «یک کتاب چه فایده‌ای دارد،» آلیس فکر کرد، «بدون تصویر یا گفت‌وگو؟» بنابراین در ذهن خود بررسی می‌کرد که آیا لذت ساختن زنجیره‌ای از گل‌های بابونه ارزش زحمت برخاستن و چیدن گل‌ها را دارد یا نه. ناگهان یک خرگوش سفید با چشم‌های صورتی از کنار او دوید. در این کار چیز بسیار عجیبی نبود؛ آلیس هم فکر نمی‌کرد عجیب باشد که خرگوش به خودش بگوید: «وای! وای! دیرم خواهد شد!» اما وقتی خرگوش واقعاً ساعتی از جیب جلیقه‌اش بیرون آورد و به آن نگاه کرد، آلیس روی پاهایش پرید." + }, + "fi": { + "language": "Finnish", + "text": "Alice alkoi väsyä istuessaan sisarensa vieressä joen rannalla. Kerran tai pari hän oli kurkistanut sisarensa lukemaan kirjaan, mutta siinä ei ollut kuvia tai keskusteluja. 'Mihin hyötyyn kirja on,' Alice ajatteli, 'jos siinä ei ole kuvia tai keskusteluja?' Niinpä hän pohti mielessään, olisiko daisy-ketjun tekemisen ilo vaivan arvoista nousta ylös ja poimia päivänkakkarat. Yhtäkkiä valkoinen kani, jolla oli vaaleanpunaiset silmät, juoksi hänen ohi. Siinä ei ollut mitään erityisen merkittävää; Alice ei myöskään pitänyt kovin outona kuulla kanin sanovan itselleen: 'Oi voi! Oi voi! Myöhästyn!' Mutta kun kani oikeasti otti kellon liivintaskustaan ja katsoi sitä, Alice hyppäsi jaloilleen." + }, + "fr": { + "language": "French", + "text": "Alice commençait à se lasser de rester assise près de sa sœur sur la berge. Une ou deux fois, elle avait jeté un coup d’œil dans le livre que sa sœur lisait, mais il n’y avait ni images ni conversations. « Et à quoi sert un livre, » pensait Alice, « sans images ni conversations ? » Elle réfléchissait donc en elle-même pour savoir si le plaisir de faire une guirlande de marguerites valait la peine de se lever et de cueillir les marguerites. Soudain, un Lapin Blanc aux yeux roses passa près d’elle. Il n’y avait rien de très remarquable là-dedans ; Alice ne pensait pas non plus qu’il soit si étrange d’entendre le lapin se dire à lui-même : « Oh là là ! Oh là là ! Je vais être en retard ! » Mais lorsque le Lapin sortit effectivement une montre de sa poche de gilet et la regarda, Alice bondit sur ses pieds." + }, + "gl": { + "language": "Galician", + "text": "A Alicia comezaba a cansarse moito de estar sentada xunto á súa irmá na beira. Un par de veces espiou o libro que a súa irmá estaba lendo, pero non tiña imaxes nin conversacións. 'E de que serve un libro,' pensaba Alicia, 'sen imaxes nin conversacións?' Así que estaba a considerar na súa mente se o pracer de facer unha cadea de margaridas valería a pena levantarse e recoller as margaridas. De súpeto, un Coello Branco con ollos rosas pasou correndo preto dela. Non había nada tan notable en iso; tampouco Alicia pensou que fose tan raro escoitar ao coello dicir para si mesmo: '¡Ai Deus! ¡Ai Deus! Chegarei tarde!' Pero cando o Coello realmente sacou un reloxo do peto do chaleco e mirouno, Alicia deu un salto sobre os pés." + }, + "he": { + "language": "Hebrew", + "text": "אליס התחילה להתעייף מאוד מלהיות יושבת ליד אחותה על הגדה. פעם או פעמיים היא הציצה בספר שאחותה קראה, אך לא היו בו תמונות או שיחות. 'ומה תועלת ספר,' חשבה אליס, 'ללא תמונות או שיחות?' כך היא שקלה בליבה האם ההנאה של עשיית שרשרת פרחי דייזי שווה את המאמץ לקום ולאסוף את הפרחים. פתאום ארנב לבן עם עיניים ורודות רץ לידה. לא היה בכך משהו מיוחד; ולא חשבה אליס שזה מוזר מדי לשמוע את הארנב אומר לעצמו: 'או, אלוהים! או, אלוהים! אני אגיע באיחור!' אבל כאשר הארנב באמת הוציא שעון מכיס אפודו והביט בו, אליס קפצה על רגליה." + }, + "hi": { + "language": "Hindi", + "text": "ऐलिस अपनी बहन के पास किनारे पर बैठकर बहुत थकने लगी थी। एक-दो बार उसने उस किताब में झांक लिया जो उसकी बहन पढ़ रही थी, लेकिन उसमें न तो चित्र थे और न ही संवाद। 'और किताब का क्या फायदा,' ऐलिस ने सोचा, 'बिना चित्र या संवाद के?' इसलिए वह अपने मन में सोच रही थी कि डेज़ी-चेन बनाने का आनंद उठाना खड़ा होकर फूल तोड़ने के कष्ट के लायक है या नहीं। अचानक गुलाबी आंखों वाला एक सफेद खरगोश उसके पास से दौड़ता हुआ गुजरा। इसमें कुछ विशेष आश्चर्यजनक नहीं था; न ही ऐलिस को यह अजीब लगा कि खरगोश अपने आप से कहे, 'हे भगवान! हे भगवान! मैं देर हो जाऊंगी!' लेकिन जब खरगोश ने वास्तव में अपनी वेस्टकोट की जेब से घड़ी निकाली और उसे देखा, ऐलिस तुरंत अपने पैरों पर कूद पड़ी।" + }, + "hr": { + "language": "Croatian", + "text": "Alice je počinjala biti vrlo umorna od sjedenja kraj svoje sestre na obali. Jednom ili dvaput je zavirila u knjigu koju je njezina sestra čitala, ali nije imala slike ili dijaloge. 'I čemu služi knjiga,' mislila je Alice, 'bez slika ili dijaloga?' Tako je razmišljala u svom umu je li zadovoljstvo izrade lančića od tratinčica vrijedno truda ustati i brati tratinčice. Odjednom je bijeli Zec s ružičastim očima protrčao pokraj nje. U tome nije bilo ništa posebno zapanjujuće; Alice također nije mislila da je čudno čuti zeca kako sam sebi kaže: 'O, dragi! O, dragi! Kasnit ću!' Ali kada je Zec zapravo izvadio sat iz džepa svog prsluka i pogledao ga, Alice je skočila na noge." + }, + "hu": { + "language": "Hungarian", + "text": "Alice kezdett nagyon elfáradni attól, hogy a parton ült a nővére mellett. Egyszer-kétszer belesett a könyvbe, amit a nővére olvasott, de abban nem voltak képek vagy párbeszédek. 'És mi haszna van egy könyvnek,' gondolta Alice, 'képek vagy párbeszéd nélkül?' Így hát azon töprengett a fejében, vajon megéri-e a fáradságot felállni és margarétaláncot készíteni. Hirtelen egy fehér Nyúl rózsaszín szemekkel futott el mellette. Ebben nem volt semmi különösen figyelemre méltó; Alice sem találta túl szokatlannak hallani a nyulat, ahogy magának mondja: 'Ó jaj! Ó jaj! Elkésem!' De amikor a Nyúl tényleg elővett egy órát a mellényzsebéből és rápillantott, Alice felugrott a lábára." + }, + "id": { + "language": "Indonesian", + "text": "Alice mulai sangat lelah duduk di samping saudara perempuannya di tepi sungai. Sekali atau dua kali ia mengintip buku yang dibaca saudaranya, tetapi tidak ada gambar atau percakapan di dalamnya. 'Dan apa gunanya sebuah buku,' pikir Alice, 'tanpa gambar atau percakapan?' Jadi ia sedang mempertimbangkan dalam pikirannya apakah kesenangan membuat rantai bunga daisy sepadan dengan repot bangun dan memetik bunga-bunga itu. Tiba-tiba seekor Kelinci Putih bermata merah muda berlari dekat dengannya. Tidak ada yang begitu luar biasa; Alice juga tidak menganggapnya aneh mendengar kelinci berkata pada dirinya sendiri: 'Astaga! Astaga! Aku akan terlambat!' Tetapi ketika Kelinci benar-benar mengeluarkan jam dari saku rompinya dan melihatnya, Alice meloncat berdiri." + }, + "it": { + "language": "Italian", + "text": "Alice stava cominciando a stancarsi molto di stare seduta accanto a sua sorella sulla riva. Una o due volte aveva dato un’occhiata al libro che sua sorella stava leggendo, ma non aveva né immagini né conversazioni. 'E a cosa serve un libro,' pensava Alice, 'senza immagini o conversazioni?' Così stava considerando nella sua mente se il piacere di fare una collana di margherite valesse la fatica di alzarsi e raccogliere i fiori. Improvvisamente un Coniglio Bianco dagli occhi rosa corse vicino a lei. Non c’era nulla di particolarmente notevole in ciò; né Alice trovava così strano sentire il coniglio dire a se stesso: 'Oh cielo! Oh cielo! Farò tardi!' Ma quando il Coniglio tirò davvero fuori un orologio dalla tasca del panciotto e lo guardò, Alice saltò in piedi." + }, + "ja": { + "language": "Japanese", + "text": "アリスは川岸で姉のそばに座っているのにとても疲れ始めていた。何度か姉が読んでいる本を覗き込んだが、そこには絵も会話もなかった。『絵も会話もない本なんて、何の役に立つのだろう』とアリスは思った。そこで彼女は、デイジーチェーンを作る楽しみが立ち上がって花を摘む手間に見合うかどうか、自分の心の中で考えていた。突然、ピンクの目をした白ウサギが彼女のそばを走り抜けた。それに特に驚くことはなかった;アリスもウサギが自分自身に『ああ、しまった!ああ、しまった!遅れる!』と言うのを聞いても特に変だとは思わなかった。しかし、ウサギが実際にチョッキのポケットから時計を取り出して見たとき、アリスは飛び上がった。" + }, + "kn": { + "language": "Kannada", + "text": "ಆಲಿಸ್ ತನ್ನ ಸಹೋದರಿಯ ಬಳಿಯಲ್ಲಿ ತೀರದಲ್ಲಿ ಕೂತಿರುವುದರಿಂದ ತುಂಬಾ ಥಕತಿಹೋಗುತ್ತಿದ್ದುದು ಪ್ರಾರಂಭಿಸಿತು. ಒಮ್ಮೆ ಅಥವಾ ಎರಡು ಬಾರಿ ಅವಳು ತನ್ನ ಸಹೋದರಿ ಓದುತ್ತಿದ್ದ ಪುಸ್ತಕವನ್ನು ನೋಡಿದಳು, ಆದರೆ ಅದರಲ್ಲಿ ಚಿತ್ರಗಳು ಅಥವಾ ಸಂಭಾಷಣೆಗಳೇ ಇರಲಿಲ್ಲ. 'ಚಿತ್ರಗಳು ಅಥವಾ ಸಂಭಾಷಣೆ ಇಲ್ಲದ ಪುಸ್ತಕದ ಉಪಯೋಗವೇನು,' ಎಂದು ಆಲಿಸ್ ಚಿಂತನೆ ಮಾಡಿದರು. ಆದ್ದರಿಂದ ದೇಸಿ-ಚೈನ್ ಮಾಡುವ ಸಂತೋಷವು ಎದ್ದುಕೊಂಡು ಹೂವುಗಳನ್ನು ತೂಗುವ ಕಷ್ಟಕ್ಕೆ ಸಮರ್ಥವಾಗುವುದೇ ಎಂದು ಅವಳು ತನ್ನ ಮನಸ್ಸಿನಲ್ಲಿ ಪರಿಗಣಿಸುತ್ತಿದ್ದಳು. ಹಠಾತ್ತಾಗಿ, ಪಿಂಕ್ ಕಣ್ಣುಳ್ಳ ಬಿಳಿ ಮೊಲ ಅವಳ ಹತ್ತಿರ ಓಡಿತು. ಅದರಲ್ಲಿ ಯಾವುದೇ ವಿಶೇಷವಾದ ಅಚ್ಚರಿ ಏನೂ ಇರಲಿಲ್ಲ; ಆಲಿಸಿಗೂ ಮೊಲವು ತನ್ನನ್ನೇ 'ಅಯ್ಯೋ! ಅಯ್ಯೋ! ನಾನು ತಡವಾಗಿ ಆಗುತ್ತೇನೆ!' ಎಂದು ಹೇಳುತ್ತಿರುವುದು ಅನ್ಯಾಯವೆಂದು ಕಂಡಿಲ್ಲ. ಆದರೆ ಮೊಲವು ವಾಸ್ಟ್ಕೋಟ್ ಜೇಬಿನಿಂದ ಘಡಿಯನ್ನು ತೆಗೆದು ಅದನ್ನು ನೋಡಿದಾಗ, ಆಲಿಸ್ ತನ್ನ ಪಾದಗಳ ಮೇಲೆ ಜಿಗಿಯಿತು." + }, + "ko": { + "language": "Korean", + "text": "앨리스는 강둑에서 그녀의 언니 옆에 앉아 있는 것에 매우 지쳐가기 시작했다. 한두 번 그녀는 언니가 읽고 있는 책을 살짝 들여다보았지만, 거기에는 그림이나 대화가 없었다. '그림이나 대화 없는 책이 무슨 소용이지,' 앨리스는 생각했다. 그래서 그녀는 데이지 체인을 만드는 즐거움이 일어나서 꽃을 따는 수고를 할 가치가 있는지 마음속으로 고민하고 있었다. 갑자기 분홍색 눈을 가진 흰 토끼가 그녀 옆을 달려 지나갔다. 그것에 특별히 놀랄 일은 없었다; 앨리스도 토끼가 혼잣말로 '오 이런! 오 이런! 늦겠어!'라고 말하는 것을 듣고 그렇게 이상하다고 생각하지 않았다. 그러나 토끼가 실제로 조끼 주머니에서 시계를 꺼내 들여다보았을 때, 앨리스는 벌떡 일어섰다." + }, + "mr": { + "language": "Marathi", + "text": "ऍलिस तिच्या बहिणीच्या बाजूला तटावर बसून खूप थकायला लागली होती. एक-दोन वेळा तिने पाहिले की तिची बहिण वाचत असलेली पुस्तक पाहिली, पण त्यात चित्रे किंवा संवाद नव्हते. 'आणि पुस्तकाचा काय उपयोग,' असे ऍलिसला वाटले, 'चित्रे किंवा संवादांशिवाय?' त्यामुळे ती स्वतःच्या मनात विचार करत होती की डेझी चेन बनवण्याचा आनंद उभे राहून फुले तोडण्याच्या त्रासाच्या लायक आहे की नाही. अचानक गुलाबी डोळ्यांचा पांढरा ससा तिच्या जवळून धावून गेला. यात काहीतरी फारच विशेष नव्हते; ऍलिसला असे वाटले नाही की ससाने स्वतःशी सांगताना ऐकणे एवढे विचित्र आहे: 'अरे देवा! अरे देवा! मी उशिरा पोहचणार!' पण जेव्हा ससाने खरोखरच आपला वेस्टकोट पॉकेटमधून घड्याळ काढले आणि पाहिले, तेव्हा ऍलिस उभी राहिली." + }, + "ms": { + "language": "Malay", + "text": "Alice mula merasa sangat letih duduk di sebelah kakaknya di tebing. Sekali atau dua kali dia mengintip buku yang dibaca oleh kakaknya, tetapi tiada gambar atau perbualan di dalamnya. 'Dan apa guna sebuah buku,' fikir Alice, 'tanpa gambar atau perbualan?' Jadi dia sedang memikirkan dalam fikirannya sama ada keseronokan membuat rantai daisy berbaloi dengan kesukaran untuk bangun dan memetik bunga-bunga itu. Tiba-tiba seekor Arnab Putih dengan mata merah jambu berlari di dekatnya. Tiada apa yang sangat luar biasa dalam itu; Alice juga tidak menganggapnya pelik mendengar arnab itu berkata pada dirinya sendiri: 'Ya ampun! Ya ampun! Aku akan lambat!' Tetapi apabila Arnab itu benar-benar mengeluarkan jam dari poket rompinya dan melihatnya, Alice melompat bangun." + }, + "nb": { + "language": "Norwegian Bokmål", + "text": "Alice begynte å bli veldig sliten av å sitte ved siden av søsteren sin på bredden. En eller to ganger hadde hun tittet inn i boken søsteren hennes leste, men den hadde ingen bilder eller samtaler. 'Og hva er nytten av en bok,' tenkte Alice, 'uten bilder eller samtaler?' Så hun vurderte i sitt eget sinn om gleden ved å lage en prestekragekjede var verdt bryet med å reise seg og plukke prestekragene. Plutselig løp en Hvit Kanin med rosa øyne nær henne. Det var ikke noe veldig bemerkelsesverdig med det; heller ikke syntes Alice at det var så merkelig å høre kaninen si til seg selv: 'Å nei! Å nei! Jeg kommer til å bli sen!' Men da kaninen faktisk tok ut et ur fra vestlommen og kikket på det, hoppet Alice opp på beina." + }, + "nl": { + "language": "Dutch", + "text": "Alice begon erg moe te worden van het zitten naast haar zus aan de oever. Een of twee keer had ze in het boek gekeken dat haar zus aan het lezen was, maar het bevatte geen plaatjes of gesprekken. 'En wat heb je aan een boek,' dacht Alice, 'zonder plaatjes of gesprekken?' Dus dacht ze in zichzelf na of het plezier van het maken van een madeliefjesketting de moeite waard was om op te staan en de madeliefjes te plukken. Plotseling rende een Wit Konijn met roze ogen dicht langs haar heen. Er was niets zo merkwaardigs aan; ook vond Alice het niet zo vreemd om het konijn tegen zichzelf te horen zeggen: 'O je! O je! Ik zal te laat komen!' Maar toen het Konijn daadwerkelijk een horloge uit zijn vestzak haalde en erop keek, sprong Alice op." + }, + "pl": { + "language": "Polish", + "text": "Alicja zaczynała być bardzo zmęczona siedzeniem obok swojej siostry na brzegu. Raz czy dwa zajrzała do książki, którą czytała jej siostra, ale nie było w niej ani obrazków, ani dialogów. 'I cóż za pożytek z książki,' pomyślała Alicja, 'bez obrazków i rozmów?' Tak więc rozważała w duchu, czy przyjemność robienia girlandy z stokrotek jest warta wysiłku wstania i zerwania kwiatów. Nagle obok niej przebiegł Biały Królik o różowych oczach. Nie było w tym nic nadzwyczajnego; Alicja też nie uważała, że to dziwne, gdy słyszy, jak królik mówi do siebie: 'Ojej! Ojej! Będę spóźniona!' Ale kiedy Królik naprawdę wyciągnął zegarek z kieszeni kamizelki i na niego spojrzał, Alicja podskoczyła na nogi." + }, + "pt": { + "language": "Portuguese", + "text": "Alice estava começando a ficar muito cansada de sentar-se ao lado de sua irmã na margem. Uma ou duas vezes ela espiou o livro que sua irmã estava lendo, mas não havia imagens nem conversas nele. 'E de que serve um livro,' pensou Alice, 'sem imagens ou conversas?' Assim, ela considerava em sua mente se o prazer de fazer uma corrente de margaridas valeria o esforço de se levantar e colher as flores. De repente, um Coelho Branco com olhos rosas correu perto dela. Não havia nada de muito notável nisso; Alice também não achou tão estranho ouvir o coelho dizendo a si mesmo: 'Oh, céus! Oh, céus! Vou me atrasar!' Mas quando o Coelho realmente tirou um relógio do bolso do colete e olhou para ele, Alice pulou de pé." + }, + "ro": { + "language": "Romanian", + "text": "Alice începea să se simtă foarte obosită stând lângă sora ei pe mal. O dată sau de două ori a aruncat o privire în cartea pe care o citea sora ei, dar nu avea imagini sau conversații. 'Și la ce folosește o carte,' se gândi Alice, 'fără imagini sau conversații?' Așa că se gândea în sinea ei dacă plăcerea de a face un lanț de margarete merită efortul de a se ridica și a culege florile. Deodată, un Iepure Alb cu ochi roz a alergat pe lângă ea. Nu era nimic atât de remarcabil în asta; nici Alice nu considera ciudat să audă iepurele spunându-și: 'Oh, Doamne! Oh, Doamne! O să întârzii!' Dar când Iepurele a scos cu adevărat un ceas din buzunarul vestei și s-a uitat la el, Alice a sărit în picioare." + }, + "ru": { + "language": "Russian", + "text": "Алиса начинала очень уставать от того, что сидела рядом с сестрой на берегу. Один или два раза она заглядывала в книгу, которую читала её сестра, но в ней не было ни картинок, ни диалогов. «И какая польза от книги, — думала Алиса, — без картинок и разговоров?» Поэтому она размышляла про себя, стоит ли удовольствие от создания цепочки из маргариток того труда, чтобы встать и собрать цветы. Вдруг мимо неё пробежал Белый Кролик с розовыми глазами. В этом не было ничего особо примечательного; Алиса также не считала странным слышать, как кролик говорит сам себе: «О, боже! О, боже! Я опоздаю!» Но когда Кролик действительно достал часы из кармана жилета и посмотрел на них, Алиса вскочила на ноги." + }, + "sk": { + "language": "Slovak", + "text": "Alice sa začala veľmi unavovať z toho, že sedela pri svojej sestre na brehu. Raz alebo dvakrát nakukla do knihy, ktorú čítala jej sestra, ale neobsahovala žiadne obrázky ani rozhovory. 'A na čo je kniha,' pomyslela si Alice, 'bez obrázkov alebo rozhovorov?' Takže si v duchu rozvažovala, či radosť z vytvorenia reťazca z margarét stojí za námahu vstať a nazbierať margaréty. Zrazu okolo nej prebehol Biely Králik s ružovými očami. V tom nebolo nič zvlášť pozoruhodného; Alice si tiež nemyslela, že by bolo zvláštne počuť králika hovoriť si sám pre seba: 'Och, drahý! Och, drahý! Meškám!' Ale keď králik naozaj vytiahol hodinky z vrecka vesty a pozrel sa na ne, Alice vyskočila na nohy." + }, + "sl": { + "language": "Slovenian", + "text": "Alice je začela biti zelo utrujena od sedenja ob svoji sestri na bregu. Enkrat ali dvakrat je pokukala v knjigo, ki jo je brala njena sestra, vendar v njej ni bilo nobenih slik ali pogovorov. 'In kakšen namen ima knjiga,' je razmišljala Alice, 'brez slik ali pogovorov?' Tako je v svojem notranjem svetu razmišljala, ali bi bil užitek izdelovanja verige iz marjetic vreden truda vstati in pobrati cvetlice. Nenadoma je mimo nje pritekel Bel Zajec z roza očmi. V tem ni bilo nič posebej nenavadnega; Alice se tudi ni zdelo čudno slišati, kako zajec sam sebi reče: 'O, dragi! O, dragi! Zamudila bom!' Ko pa je zajec res izvlekel uro iz žepka telovnika in pogledal nanjo, je Alice poskočila na noge." + }, + "sv": { + "language": "Swedish", + "text": "Alice började bli mycket trött på att sitta bredvid sin syster vid flodbanken. En eller två gånger hade hon tittat i boken som hennes syster läste, men den hade inga bilder eller samtal. 'Och vad är nyttan med en bok,' tänkte Alice, 'utan bilder eller samtal?' Så hon funderade i sitt eget sinne om nöjet att göra en prästkragakedja var värt besväret att resa sig och plocka prästkragar. Plötsligt sprang en Vit Kanin med rosa ögon nära henne. Det var inget särskilt anmärkningsvärt med det; Alice tyckte inte heller att det var särskilt märkligt att höra kaninen säga till sig själv: 'Åh nej! Åh nej! Jag kommer att bli sen!' Men när Kaninen faktiskt tog fram en klocka ur sin västficka och tittade på den, hoppade Alice upp på fötterna." + }, + "ta": { + "language": "Tamil", + "text": "அலிஸ் தனது சகோதரியின் அருகே நதிக்கரை மீது உட்கார்ந்து மிகவும் சோர்வடைந்துவிட்டாள். ஒருமுறை அல்லது இருமுறை அவள் தனது சகோதரியின் படிக்கிற புத்தகத்தை சிறிது பார்வையிட்டாள், ஆனால் அதில் படங்கள் அல்லது உரையாடல்கள் எதுவும் இல்லை. 'படங்களோ உரையாடல்களோ இல்லாத புத்தகம் எந்த பயனுடையது?' என்று அலிஸ் நினைத்தாள். எனவே அவள் மனதில் சிந்தித்தாள், டெய்சி சங்கிலியை உருவாக்கும் மகிழ்ச்சி எழுந்து பூக்களை எடுக்கக்கூடிய முயற்சிக்குரியதா என்று. திடீரென ஒரு வெள்ளை முயல் ரோஜா கண்களுடன் அவளின் அருகே ஓடிச் சென்றது. அதில் மிக விசித்திரமானதல்ல; அலிஸ் அந்த முயல் தனக்கே சொல்லிக்கொண்டதை கேட்டு அதிர்ச்சியடைந்தாள் என்று எண்ணவில்லை: 'ஓ அன்பே! ஓ அன்பே! நான் தாமதமாகி விடுவேன்!' ஆனால் முயல் உண்மையில் வஸ்கோட் பாக்கெட் இருந்து கடிகாரத்தை எடுத்துச் பார்த்தபோது, அலிஸ் நின்றாள்." + }, + "te": { + "language": "Telugu", + "text": "అలిస్ తన చెల్లెదరి పక్కన తీరానికి కూర్చొని చాలా అలసిపోతుండేది. ఒకసారి లేదా రెండు సార్లు ఆమె తన చెల్లెదరి చదువుతున్న పుస్తకంలో చుడుతుందివి, కానీ అందులో చిత్రాలు లేదా సంభాషణలు లేవు. 'చిత్రాలు లేదా సంభాషణలు లేకుండా పుస్తకానికి ఏమి ఉపయోగం,' అని అలిస్ ఆలోచించింది. కాబట్టి డైసీ చైన్ తయారు చేసే ఆనందం లేపి పూలను కోసుకునే కష్టం విలువైనదా అనే విషయాన్ని ఆమె తన మనసులో ఆలోచిస్తోంది. అకస్మాత్తుగా గులాబీ కళ్లతో ఉన్న తెల్ల శూక్రగొంగ ఆమె పక్కన పరిగెత్తింది. అందులో ఏ ప్రత్యేకంగా ఆశ్చర్యకరమైనది లేదు; అలిస్ కప్పు తనకే చెప్పుకుంటూ ఉంది అని విన్నా సాధారణంగా ఆశ్చర్యంగా అనిపించలేదు: 'ఓ దేవా! ఓ దేవా! నేను ఆలస్యమవుతాను!' కానీ శూక్రగొంగ నిజంగా తన వెస్ట్‌కోట్ పొకెట్ నుండి గడియారం తీసుకుని దానిని చూసినప్పుడు, అలిస్ కదిలింది." + }, + "th": { + "language": "Thai", + "text": "อลิซเริ่มรู้สึกเหนื่อยมากที่นั่งอยู่ข้างๆ พี่สาวของเธอที่ริมฝั่ง แม้เธอจะชำเลืองดูหนังสือที่พี่สาวกำลังอ่านอยู่หนึ่งหรือสองครั้ง แต่ก็ไม่มีภาพหรือบทสนทนาใดๆ 'แล้วหนังสือมีประโยชน์อะไร,' อลิซคิด, 'ถ้าไม่มีภาพหรือบทสนทนา?' ดังนั้นเธอจึงคิดอยู่ในใจเองว่าความเพลิดเพลินจากการทำพวงมาลัยดอกเดซี่นั้นคุ้มค่ากับความเหนื่อยที่จะลุกขึ้นไปเก็บดอกไม้หรือไม่ อยู่ดีๆ กระต่ายขาวที่มีดวงตาสีชมพูวิ่งผ่านมาใกล้เธอ ไม่มีอะไรน่าประหลาดใจมากนัก; อลิซก็ไม่คิดว่าการได้ยินกระต่ายพูดกับตัวเองว่า 'โอ้ พระเจ้า! โอ้ พระเจ้า! ฉันจะสาย!' เป็นเรื่องแปลก แต่เมื่อกระต่ายเอานาฬิกาออกจากกระเป๋าเสื้อกั๊กและดูเวลา อลิซก็ลุกขึ้นทันที" + }, + "tr": { + "language": "Turkish", + "text": "Alice, kız kardeşinin yanında kıyıda oturmaktan çok yorulmaya başlamıştı. Bir veya iki kez, kız kardeşinin okuduğu kitaba göz atmıştı, ama içinde ne resim ne de konuşma vardı. 'Resimsiz veya konuşmasız bir kitabın ne faydası var,' diye düşündü Alice. Bu yüzden, papatya zinciri yapmak gibi bir zevkin, kalkıp papatyaları toplamanın zahmetine değip değmeyeceğini kafasında tartıyordu. Aniden, pembe gözlü Beyaz Bir Tavşan yanından koşarak geçti. Bu o kadar da dikkate değer bir şey değildi; Alice de tavşanın kendi kendine 'Ah hayır! Ah hayır! Geç kalacağım!' dediğini duymanın garip olduğunu düşünmedi. Ama Tavşan gerçekten yelek cebinden bir saat çıkardığında ve ona baktığında, Alice ayağa fırladı." + }, + "uk": { + "language": "Ukrainian", + "text": "Аліса почала дуже втомлюватися, сидячи біля своєї сестри на березі. Один або два рази вона зазирала в книгу, яку читала її сестра, але там не було ні зображень, ні діалогів. «А яка користь від книги,» подумала Аліса, «якщо в ній немає зображень або діалогів?» Тож вона розмірковувала про себе, чи варто задоволення від створення гірлянди з маргариток клопоту встати і зірвати квіти. Раптом повз неї пробіг Білий Кролик з рожевими очима. У цьому не було нічого особливо примітного; Аліса також не вважала дивним почути, як кролик каже сам собі: «О Боже! О Боже! Я запізнюся!» Але коли Кролик справді витягнув годинник із кишені жилета і подивився на нього, Аліса вскочила на ноги." + }, + "vi": { + "language": "Vietnamese", + "text": "Alice bắt đầu cảm thấy rất mệt khi ngồi bên cạnh chị gái của mình trên bờ sông. Một hoặc hai lần cô liếc vào cuốn sách mà chị gái đang đọc, nhưng trong đó không có hình ảnh hay đối thoại. 'Một cuốn sách để làm gì,' Alice nghĩ, 'nếu không có hình ảnh hay đối thoại?' Vì vậy cô tự hỏi liệu niềm vui làm vòng hoa cúc có xứng đáng với công sức đứng dậy và hái những bông hoa hay không. Bất ngờ, một Con Thỏ Trắng với đôi mắt hồng chạy gần cô. Không có gì quá đáng chú ý trong đó; Alice cũng không thấy lạ khi nghe con thỏ tự nói với chính nó: 'Ôi trời! Ôi trời! Mình sẽ đến trễ!' Nhưng khi con thỏ thực sự lấy đồng hồ ra khỏi túi áo ghi lê và nhìn vào nó, Alice nhảy lên." + }, + "wuu": { + "language": "Wu Chinese", + "text": "阿丽思坐在河边,陪着她姐姐,越坐越困倦。她瞄了一眼姐姐在读的书,可书里没有图画,也没有对话。‘一本没有图画和对话的书有什么用呢,’阿丽思心想。于是她心里盘算着,做雏菊链子的乐趣是否值得起身去采花。突然,一只粉色眼睛的白兔跑过她身边。其实并不特别惊讶;阿丽思也没觉得听见兔子自言自语‘哎呀!哎呀!我要迟到了!’很奇怪。但当兔子真的从马甲口袋里掏出怀表看时,阿丽思跳了起来。" + }, + "yue": { + "language": "Cantonese", + "text": "愛麗絲開始覺得好累,因為她坐喺河邊陪住佢嘅姐姐。佢望咗一兩次佢姐姐睇緊嘅書,但入面冇圖畫同對話。『冇圖畫同對話嘅書有咩用呀,』愛麗絲諗。於是佢心入面諗住,做雛菊鏈嘅樂趣值唔值得企起身去摘花。突然,一隻粉紅色眼睛嘅白兔跑過佢身邊。其實冇乜特別驚訝;愛麗絲都唔覺得聽到隻兔自言自語『哎呀!哎呀!我要遲到啦!』咁奇怪。但當兔仔真係從背心袋攞出懷錶睇嘅時候,愛麗絲跳咗起身。" + } +} \ No newline at end of file diff --git a/demo/script.js b/demo/script.js index f796597..123af05 100644 --- a/demo/script.js +++ b/demo/script.js @@ -1,214 +1,887 @@ - -import { getSpeechSynthesisVoices, parseSpeechSynthesisVoices, filterOnNovelty, filterOnVeryLowQuality, - filterOnRecommended, sortByLanguage, sortByQuality, getVoices, groupByKindOfVoices, groupByRegions, - getLanguages, filterOnOfflineAvailability, listLanguages, filterOnGender, filterOnLanguage } from "../build/index.js"; - -import * as lit from './lit-html_3-2-0_esm.js' -const { html, render } = lit; - -async function loadJSONData(url) { - try { - const response = await fetch(url); - const jsonData = JSON.parse(await response.text()); - return jsonData; - } catch (error) { - console.error('Error loading JSON data:', error); - return null; - } +import { WebSpeechVoiceManager, WebSpeechReadAloudNavigator, chineseVariantMap } from "../build/index.js"; + +let samples = null; + +const highlight = new Highlight(); +CSS.highlights.set("readium-speech-highlight", highlight); +let currentWordHighlight = null; + +// DOM Elements +const languageSelect = document.getElementById("language-select"); +const genderSelect = document.getElementById("gender-select"); +const offlineOnlyCheckbox = document.getElementById("offline-only"); +const voiceSelect = document.getElementById("voice-select"); +const testUtteranceInput = document.getElementById("test-utterance"); +const playPauseBtn = document.getElementById("play-pause-btn"); +const stopBtn = document.getElementById("stop-btn"); +const testUtteranceBtn = document.getElementById("test-utterance-btn"); +const prevUtteranceBtn = document.getElementById("prev-utterance-btn"); +const nextUtteranceBtn = document.getElementById("next-utterance-btn"); +const jumpToBtn = document.getElementById("jump-to-btn"); +const utteranceIndexInput = document.getElementById("utterance-index"); +const totalUtterancesSpan = document.getElementById("total-utterances"); +const sampleTextDisplay = document.getElementById("sample-text"); + +// Track if user has manually changed the jump input +let jumpInputUserChanged = false; + +// State +let voiceManager; +let allVoices = []; +let filteredVoices = []; +let languages = []; +let currentVoice = null; +let testUtterance = ""; +let lastNavigatorPosition = 1; + +const navigator = new WebSpeechReadAloudNavigator(); + +// Set up event listeners for the navigator +navigator.on("boundary", (event) => { + if (event.detail && event.detail.name === "word") { + highlightCurrentWord(event.detail.charIndex, event.detail.charLength); + } +}); + +navigator.on("start", () => { + clearWordHighlighting(); + updateUI(); +}); + +navigator.on("pause", updateUI); +navigator.on("resume", updateUI); +navigator.on("stop", () => { + clearWordHighlighting(); + updateUI(); +}); + +navigator.on("end", updateUI); +navigator.on("error", (event) => { + console.error("Navigator error:", event.detail); + updateUI(); +}); + +// Initialize the application +async function init() { + try { + // Initialize the voice manager + voiceManager = await WebSpeechVoiceManager.initialize(); + + const initOptions = { + excludeNovelty: true, + excludeVeryLowQuality: true + }; + + // Load all available voices + allVoices = voiceManager.getVoices(initOptions); + + // Get languages, excluding novelty and very low quality voices + const allLanguages = voiceManager.getLanguages(window.navigator.language, initOptions); + + // Sort languages with browser's preferred languages first + languages = voiceManager.sortVoices( + allLanguages.map(lang => ({ + ...lang, + language: lang.code, + name: lang.label + })), + { + by: "language", + order: "asc", + preferredLanguages: window.navigator.languages + } + ).map(voice => ({ + code: voice.language, + label: voice.name, + count: voice.count + })); + + // Populate language dropdown + populateLanguageDropdown(); + + // Set up event listeners + setupEventListeners(); + + // Update UI + updateUI(); + + } catch (error) { + console.error("Error initializing application:", error); + const errorDiv = document.createElement("div"); + errorDiv.style.color = "red"; + errorDiv.textContent = "Error loading voices. Please check console for details."; + document.body.prepend(errorDiv); + } } -function downloadJSON(obj, filename) { - // Convert the JSON object to a string - const data = JSON.stringify(obj, null, 2); - - // Create a blob from the string - const blob = new Blob([data], { type: "application/json" }); - - // Generate an object URL - const jsonObjectUrl = URL.createObjectURL(blob); - - // Create an anchor element - const anchorEl = document.createElement("a"); - anchorEl.href = jsonObjectUrl; - anchorEl.download = `${filename}.json`; - - // Simulate a click on the anchor element - anchorEl.click(); - - // Revoke the object URL - URL.revokeObjectURL(jsonObjectUrl); +// Populate the language dropdown +function populateLanguageDropdown() { + languageSelect.innerHTML = ""; + + languages.forEach(lang => { + const option = document.createElement("option"); + option.value = lang.code; + option.textContent = `${lang.label} (${lang.count})`; + languageSelect.appendChild(option); + }); } -const viewRender = () => render(content(), document.body); - -const voices = await getVoices(); -console.log(voices); - -const languages = getLanguages(voices); - -let voicesFiltered = voices; -let languagesFiltered = languages; +// Filter voices based on current filters +function filterVoices() { + const language = languageSelect.value; + const gender = genderSelect.value; + const offlineOnly = offlineOnlyCheckbox.checked; -let textToRead = ""; -let textToReadFormated = ""; - -let selectedLanguage = undefined; - -let voicesSelectElem = []; + const filterOptions = {}; + + if (language) { + filterOptions.language = language; + } + + if (gender !== "all") { + filterOptions.gender = gender; + } + + if (offlineOnly) { + filterOptions.offlineOnly = true; + } + + // Apply filters + filteredVoices = voiceManager.filterVoices(allVoices, filterOptions); + + // Sort voices by quality (highest first) + filteredVoices = voiceManager.sortVoices(filteredVoices, { + by: "quality", + order: "desc" + }); + populateVoiceDropdown(language); + updateUI(); +} -let selectedVoice = ""; +// Populate the voice dropdown with filtered voices +function populateVoiceDropdown(language = "") { + voiceSelect.innerHTML = ""; + + try { + if (!filteredVoices.length) { + const option = document.createElement("option"); + option.disabled = true; + option.textContent = "No voices match the current filters"; + voiceSelect.appendChild(option); + return; + } -let selectedGender = "all"; + // Sort voices with browser's preferred languages first + const sortedVoices = voiceManager.sortVoices([...filteredVoices], { + by: "language", + order: "asc", + preferredLanguages: window.navigator.languages + }); + + // Group the sorted voices by region + const voiceGroups = voiceManager.groupVoices(sortedVoices, "region"); + + // Add optgroups for each region + for (const [region, voices] of Object.entries(voiceGroups)) { + if (!voices.length) continue; + + const countryCode = region.split("-").pop() || region; + const optgroup = document.createElement("optgroup"); + optgroup.label = `${getCountryFlag(countryCode)} ${region}`; + + // Sort voices by quality within each region + const sortedVoicesInRegion = voiceManager.sortVoices(voices, { + by: "quality", + order: "desc" + }); + + for (const voice of sortedVoicesInRegion) { + const option = document.createElement("option"); + option.value = voice.name; + option.textContent = [ + voice.label || voice.name, + voice.gender ? `• ${voice.gender}` : "", + voice.offlineAvailability ? "• offline" : "• online" + ].filter(Boolean).join(" "); + option.dataset.voiceUri = voice.voiceURI; + optgroup.appendChild(option); + } + + voiceSelect.appendChild(optgroup); + } + + // If we have a current voice, try to select it + if (currentVoice) { + const option = voiceSelect.querySelector(`option[data-voice-uri="${currentVoice.voiceURI}"]`); + if (option) { + option.selected = true; + } + } + + // Only show error message if we don't have any valid voice options + const hasValidOptions = Array.from(voiceSelect.options).some(opt => !opt.disabled); + if (!hasValidOptions) { + const option = document.createElement("option"); + option.disabled = true; + option.textContent = "No voices available. Please check your browser settings and internet connection."; + voiceSelect.appendChild(option); + } + } catch (error) { + console.error("Error populating voice dropdown:", error); + // Log the error but don't add any error message to the dropdown + } + + // Helper function to get country flag emoji from country code + function getCountryFlag(countryCode) { + if (!countryCode) return "🌐"; + + // Convert country code to flag emoji + try { + const codePoints = countryCode + .toUpperCase() + .split("") + .map(char => 127397 + char.charCodeAt(0)); + + return String.fromCodePoint(...codePoints); + } catch (e) { + console.warn("Could not generate flag for country code:", countryCode); + return "🌐"; + } + } +} -let checkboxOfflineChecked = false; +// Load sample text for the selected language +async function loadSampleText(languageCode) { + try { + // Show loading state + sampleTextDisplay.innerHTML = "
Loading text...
"; + + // Load sample texts if not already loaded + if (!samples) { + const response = await fetch("sampleText.json"); + if (!response.ok) { + throw new Error("Failed to load sample texts"); + } + samples = await response.json(); + } + + // Normalize the language code to lowercase for case-insensitive comparison + const langLower = languageCode.toLowerCase(); + + // Function to find a case-insensitive match in the samples + const findCaseInsensitiveMatch = (lang) => { + const normalizedLang = lang.toLowerCase(); + const matchingKey = Object.keys(samples).find(key => key.toLowerCase() === normalizedLang); + return matchingKey ? samples[matchingKey]?.text : null; + }; + + // Try direct case-insensitive match first + let sampleText = findCaseInsensitiveMatch(languageCode); + + // Try with Chinese variant mapping if no direct match + if (!sampleText) { + const mappedLang = chineseVariantMap[langLower]; + if (mappedLang) { + sampleText = samples[mappedLang]?.text; + } + } + + // If still no match, try with base language + if (!sampleText) { + const [baseLang] = WebSpeechVoiceManager.extractLangRegionFromBCP47(languageCode); + const baseLangLower = baseLang.toLowerCase(); + + // Try case-insensitive match with base language + sampleText = findCaseInsensitiveMatch(baseLang); + + // Try Chinese variant mapping for base language if still no match + if (!sampleText && chineseVariantMap[baseLangLower]) { + sampleText = samples[chineseVariantMap[baseLangLower]]?.text; + } + } + + // If no match found, return a message + if (!sampleText) { + return `No sample text available for language: ${languageCode}`; + } + + // Create utterances from the sample text + const utterances = createUtterancesFromText(sampleText); + + // Clear any existing content + sampleTextDisplay.innerHTML = ""; + + // Create a demo section container + const demoSection = document.createElement("div"); + demoSection.className = "demo-section"; + + // Add a heading + const heading = document.createElement("h2"); + heading.textContent = "Content Preview"; + demoSection.appendChild(heading); + + // Create a container for the utterances list + const utterancesList = document.createElement("div"); + utterancesList.className = "utterances-list"; + + // Add each utterance with number indicator + utterances.forEach((utterance, index) => { + const utteranceElement = document.createElement("div"); + utteranceElement.className = `utterance ${index === 0 ? "current" : ""}`; + utteranceElement.dataset.utteranceIndex = index; + + // Add utterance number + const numberSpan = document.createElement("span"); + numberSpan.className = "utterance-number"; + numberSpan.textContent = `${index + 1}.`; + + // Add text content + const textSpan = document.createElement("span"); + textSpan.className = "utterance-text"; + textSpan.dataset.utteranceId = utterance.id; + textSpan.textContent = utterance.text; + + // Assemble the elements + utteranceElement.appendChild(numberSpan); + utteranceElement.appendChild(textSpan); + + utterancesList.appendChild(utteranceElement); + }); + + // Assemble the section + demoSection.appendChild(utterancesList); + sampleTextDisplay.appendChild(demoSection); + + // Load utterances into the navigator + await navigator.loadContent(utterances); + + // Update total utterances display + const totalUtterancesSpan = document.getElementById("total-utterances"); + if (totalUtterancesSpan) { + totalUtterancesSpan.textContent = utterances.length; + } + + // Update UI to enable playback controls + updateUI(); + + // Update utterance input + if (utteranceIndexInput) { + utteranceIndexInput.max = utterances.length; + utteranceIndexInput.value = "1"; + } + } catch (error) { + console.error("Error loading sample text:", error); + sampleTextDisplay.textContent = "Error loading sample text"; + } +} -const readTextWithSelectedVoice = () => { - const voices = window.speechSynthesis.getVoices(); +// Update the test utterance based on the current voice and language +function updateTestUtterance(voice, languageCode) { + if (!voice) { + testUtterance = ""; + testUtteranceInput.value = ""; + testUtteranceBtn.disabled = true; + return; + } + + // Use the voice's language as the primary source, fall back to the language selector, then default to "en" + const language = voice.language || languageCode || "en"; + const baseUtterance = voiceManager.getTestUtterance(language) || + `This is a test of the {name} voice.`; + testUtterance = baseUtterance.replace(/\{\s*name\s*\}/g, voice.label || voice.name || "this voice"); + testUtteranceInput.value = testUtterance; + testUtteranceBtn.disabled = false; +} - const utterance = new SpeechSynthesisUtterance(); - utterance.text = textToReadFormated; +// Create utterances from text with better sentence splitting +function createUtterancesFromText(text) { + // Use Intl.Segmenter for proper sentence segmentation + const segmenter = new Intl.Segmenter(languageSelect.value || "en", { + granularity: "sentence" + }); + + // Convert segments to array and extract text + const sentences = Array.from(segmenter.segment(text), + ({ segment }) => segment.trim() + ).filter(Boolean); // Remove any empty strings + + // Create utterances from sentences + return sentences.map((sentence, index) => ({ + id: `utterance-${index}`, + text: sentence, + language: languageSelect.value || "en" + })); +} - for (const voice of voices) { - if (voice.name === selectedVoice) { - utterance.voice = voice; - utterance.lang = voice.lang; - break; +// Set up event listeners +function setupEventListeners() { + // Language selection + languageSelect.addEventListener("change", async () => { + const languageCode = languageSelect.value; + + // Reset voice selection and clear test utterance + voiceSelect.disabled = false; + currentVoice = null; + testUtterance = ""; + testUtteranceInput.value = ""; + testUtteranceBtn.disabled = true; + + // Clear voice properties + displayVoiceProperties(null); + + // Filter voices for the selected language + filterVoices(); + + // Get the default voice for the selected language using pre-filtered voices + if (languageCode) { + // Use the already filtered voices if available, otherwise fall back to the old behavior + currentVoice = voiceManager.getDefaultVoice(languageCode, filteredVoices.length ? filteredVoices : undefined); + + if (currentVoice) { + try { + // Set the voice for the navigator + navigator.setVoice(currentVoice); + + // Update the voice dropdown to reflect the selected voice + const voiceOption = voiceSelect.querySelector(`option[value="${currentVoice.name}"]`); + if (voiceOption) { + voiceOption.selected = true; + } + + // Display voice properties + displayVoiceProperties(currentVoice); + + // Update the test utterance with the new voice + updateTestUtterance(currentVoice, languageCode); + + } catch (error) { + console.error("Error setting default voice:", error); } + } } - - if (!utterance.voice) { - console.error("Speech : Voice NOT FOUND"); - alert("voice not found"); - } - - console.log("Speech", utterance); - - speechSynthesis.speak(utterance); + // Load sample text using the voice's language code if available, otherwise use the selector's value + const languageToUse = currentVoice?.language || languageCode; + loadSampleText(languageToUse); + + updateUI(); + }); + + /** + * Format a value for display in the voice properties + */ +function formatValue(value) { + if (value === undefined || value === null) { + return { display: "undefined", className: "undefined" }; + } + + if (typeof value === "boolean") { + return { + display: value ? "true" : "false", + className: `boolean-${value}` + }; + } + + if (Array.isArray(value)) { + return { + display: value.length > 0 ? value.join(", ") : "[]", + className: "" + }; + } + + if (typeof value === "object") { + return { + display: JSON.stringify(value, null, 2).replace(/"/g, ""), + className: "object-value" + }; + } + + return { display: String(value), className: "" }; } -const filterVoices = () => { - - voicesFiltered = voices; +/** + * Display voice properties in the UI + */ +function displayVoiceProperties(voice) { + const propertiesContainer = document.getElementById("voice-properties"); + + if (!voice) { + propertiesContainer.innerHTML = "

No voice selected

"; + return; + } + + // Sort properties alphabetically + const sortedProps = Object.keys(voice).sort(); + + // Create HTML for each property + const propertiesHtml = sortedProps.map(prop => { + // Skip internal/private properties that start with underscore + if (prop.startsWith("_")) return ""; - if (selectedGender !== "all") { - voicesFiltered = filterOnGender(voicesFiltered, selectedGender); - } + const value = voice[prop]; + const { display, className } = formatValue(value); + + return ` +
+
${prop}
+
${display}
+
+ `; + }).join(""); + + propertiesContainer.innerHTML = propertiesHtml || "

No properties available

"; +} - if (checkboxOfflineChecked) { - voicesFiltered = filterOnOfflineAvailability(voicesFiltered, true); + // Voice selection + voiceSelect.addEventListener("change", async () => { + const selectedVoiceName = voiceSelect.value; + currentVoice = filteredVoices.find(v => v.name === selectedVoiceName) || null; + + if (currentVoice) { + try { + // Set the voice for the navigator + navigator.setVoice(currentVoice); + + // Display voice properties + displayVoiceProperties(currentVoice); + + // Update the test utterance with the new voice + updateTestUtterance(currentVoice, currentVoice.language || languageSelect.value); + + // Load sample text using the voice's language code (fire and forget) + loadSampleText(currentVoice.language || languageSelect.value); + } catch (error) { + console.error("Error setting voice:", error); + } + } else { + if (testUtteranceBtn) { + testUtteranceBtn.disabled = true; + } } - - languagesFiltered = getLanguages(voicesFiltered); - - const voicesFilteredOnLanguage = filterOnLanguage(voicesFiltered, selectedLanguage); - const voicesGroupedByRegions = groupByRegions(voicesFilteredOnLanguage); - voicesSelectElem = listVoicesWithLanguageSelected(voicesGroupedByRegions); + updateUI(); + }); - viewRender(); -} + // Test utterance button + testUtteranceBtn.addEventListener("click", playTestUtterance); -const setSelectVoice = (name) => { + // Play/Pause button (for sample text) + playPauseBtn.addEventListener("click", togglePlayback); + playPauseBtn.disabled = !currentVoice; - selectedVoice = name; - textToReadFormated = textToRead.replace("{name}", selectedVoice); -} + // Stop button (for sample text) + stopBtn.addEventListener("click", stopPlayback); + stopBtn.disabled = !currentVoice; -const languageSelectOnChange = async (ev) => { + // Previous utterance button + prevUtteranceBtn.addEventListener("click", previousUtterance); - selectedLanguage = ev.target.value; + // Next utterance button + nextUtteranceBtn.addEventListener("click", nextUtterance); - const jsonData = await loadJSONData("https://raw.githubusercontent.com/HadrienGardeur/web-speech-recommended-voices/main/json/" + selectedLanguage + ".json"); + // Jump to utterance button + jumpToBtn.addEventListener("click", jumpToUtterance); + + // Handle Enter key in jump input + utteranceIndexInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + jumpToUtterance(); + } + }); + + // Track manual changes to jump input + utteranceIndexInput.addEventListener("input", () => { + jumpInputUserChanged = true; + }); - textToRead = jsonData?.testUtterance || ""; + // Update voices when gender filter changes + genderSelect.addEventListener("change", () => { + filterVoices(); + }); + // Update voices when offline filter changes + offlineOnlyCheckbox.addEventListener("change", () => { filterVoices(); -} + }); -const listVoicesWithLanguageSelected = (voiceMap) => { + // Update test utterance when language changes + languageSelect.addEventListener("change", () => { + if (languageSelect.value) { + updateTestUtterance(currentVoice, languageSelect.value); + } + }); +} - const elem = []; - selectedVoice = ""; +// Play test utterance - independent of the navigator +async function playTestUtterance() { + if (!currentVoice) { + console.error("No voice selected"); + return; + } + + try { + // Reset playback controls first + if (navigator) { + navigator.stop(); + } + + // Get test utterance for the selected language + let testText = testUtteranceInput.value; + if (!testText) { + updateTestUtterance(currentVoice, languageSelect.value); + testText = testUtteranceInput.value; + } + + // Create a new SpeechSynthesisUtterance + const utterance = new SpeechSynthesisUtterance(testText); + + // Convert the ReadiumSpeechVoice to a native SpeechSynthesisVoice + const nativeVoice = voiceManager.convertToSpeechSynthesisVoice(currentVoice); + if (nativeVoice) { + utterance.voice = nativeVoice; + utterance.lang = nativeVoice.lang; + } + + // Update UI state + testUtteranceBtn.disabled = true; + testUtteranceBtn.textContent = "Playing..."; + + // Handle when speech ends + utterance.onend = () => { + testUtteranceBtn.disabled = false; + testUtteranceBtn.textContent = "Play Test Utterance"; + }; + + // Handle errors + utterance.onerror = (event) => { + console.error("SpeechSynthesis error:", event); + testUtteranceBtn.disabled = false; + testUtteranceBtn.textContent = "Play Test Utterance"; + }; + + // Speak the utterance directly + speechSynthesis.speak(utterance); + + } catch (error) { + console.error("Error playing test utterance:", error); + testUtteranceBtn.textContent = "Play Test Utterance"; + testUtteranceBtn.disabled = false; + } +} - for (const [region, voice] of voiceMap) { - const option = []; +// Toggle sample text playback +async function togglePlayback() { + if (!currentVoice) { + console.error("No voice selected"); + return; + } + + try { + const state = navigator.getState(); + if (state === "playing") { + await navigator.pause(); + } else if (state === "paused") { + // Use play() to resume from paused state + await navigator.play(); + } else { + // Start from beginning if stopped or in an unknown state + await navigator.jumpTo(0); + await navigator.play(); + } + } catch (error) { + console.error("Error toggling playback:", error); + } + + // Update the UI to reflect the new state + updateUI(); +} - for (const {name, label} of voice) { - option.push(html``); - if (!selectedVoice) setSelectVoice(name); - } - elem.push(html` - - ${option} - - `) - } +// Stop sample playback +async function stopPlayback() { + try { + await navigator.stop(); + clearWordHighlighting(); + playPauseBtn.textContent = "Play Sample"; + updateUI(); + } catch (error) { + console.error("Error stopping playback:", error); + } +} - return elem; +// Go to previous utterance +async function previousUtterance() { + await navigator.previous(); + updateUI(); } -const aboutVoice = () => { - return html` -
-
${JSON.stringify(voicesFiltered.filter(({name}) => name === selectedVoice), null, 4)}
-
- `; +// Go to next utterance +async function nextUtterance() { + await navigator.next(); + updateUI(); } -const getVoicesInputForDebug = () => { - const a = window.speechSynthesis.getVoices() || []; - return a.map(({ default: def, lang, localService, name, voiceURI}) => ({default: def, lang, localService, name, voiceURI})); +// Jump to a specific utterance +function jumpToUtterance() { + const totalUtterances = navigator.getContentQueue()?.length || 0; + + // Ensure we have a valid input value + const index = Math.max(0, Math.min(parseInt(utteranceIndexInput.value) - 1, totalUtterances - 1)); + + if (!isNaN(index) && index >= 0 && index < totalUtterances) { + clearWordHighlighting(); + navigator.jumpTo(index); + + // Update UI to reflect the new position + if (utteranceIndexInput) { + utteranceIndexInput.value = index + 1; + } + + // Update total utterances display if needed + if (totalUtterancesSpan) { + totalUtterancesSpan.textContent = totalUtterances; + } + + // Clear user changed flag and update position tracking + jumpInputUserChanged = false; + lastNavigatorPosition = index + 1; + + // Update input to reflect the new position + utteranceIndexInput.value = lastNavigatorPosition; + } else { + // Invalid input, reset to current position + const currentPos = (navigator.getCurrentUtteranceIndex() || 0) + 1; + utteranceIndexInput.value = currentPos; + jumpInputUserChanged = false; + lastNavigatorPosition = currentPos; + + // Ensure total is displayed + if (totalUtterancesSpan && totalUtterances > 0) { + totalUtterancesSpan.textContent = totalUtterances; + } + } } -const content = () => html` -

ReadiumSpeech

+// Clear any previous highlighting +function clearWordHighlighting() { + if (window.CSS?.highlights) { + CSS.highlights.clear(); + } +} -

Language :

- +// Highlight current word in the sample text +function highlightCurrentWord(charIndex, charLength) { + // Clear previous highlighting + clearWordHighlighting(); + + // Get the current utterance element + const currentIndex = navigator.getCurrentUtteranceIndex(); + const utteranceElement = document.querySelector(`.utterance[data-utterance-index="${currentIndex}"] .utterance-text`); + if (!utteranceElement) return; + + const text = utteranceElement.textContent; + if (charIndex < 0 || charIndex >= text.length) return; + + // Create a range for the current word + const range = document.createRange(); + const textNode = utteranceElement.firstChild || utteranceElement; + + try { + range.setStart(textNode, charIndex); + range.setEnd(textNode, charIndex + charLength); + + // Use CSS Highlight API + const highlight = new Highlight(range); + CSS.highlights.set("current-word", highlight); + + // Update current word highlight + currentWordHighlight = { + utteranceIndex: currentIndex, + charIndex: charIndex, + charLength: charLength, + range: range + }; + } catch (e) { + console.error("Error highlighting word:", e); + } +} -

Voices :

- +// Update UI based on current state +function updateUI() { + try { + const state = navigator.getState(); + const currentIndex = navigator.getCurrentUtteranceIndex() || 0; + const totalUtterances = navigator.getContentQueue()?.length || 0; + const hasContent = totalUtterances > 0; + + // Update playback controls + if (playPauseBtn) { + playPauseBtn.disabled = !currentVoice || !hasContent; + if (state === "playing") { + playPauseBtn.innerHTML = "⏸️ Pause"; + playPauseBtn.classList.remove("play-state"); + playPauseBtn.classList.add("pause-state"); + } else { + playPauseBtn.innerHTML = "▶️ Play"; + playPauseBtn.classList.remove("pause-state"); + playPauseBtn.classList.add("play-state"); + } + } + + // Update stop button + if (stopBtn) { + stopBtn.disabled = !currentVoice || !hasContent || (state !== "playing" && state !== "paused"); + } + + // Update navigation controls + if (prevUtteranceBtn) { + prevUtteranceBtn.disabled = !currentVoice || !hasContent || currentIndex <= 0; + } + + if (nextUtteranceBtn) { + nextUtteranceBtn.disabled = !currentVoice || !hasContent || currentIndex >= totalUtterances - 1; + } + + // Update jump controls + if (utteranceIndexInput) { + utteranceIndexInput.disabled = !currentVoice || !hasContent; + if (!jumpInputUserChanged && hasContent) { + utteranceIndexInput.value = currentIndex + 1; + } + } + + if (jumpToBtn) { + jumpToBtn.disabled = !currentVoice || !hasContent; + } + + // Update test utterance button + if (testUtteranceBtn) { + testUtteranceBtn.disabled = !currentVoice; + } + + // Update utterance highlighting and scroll to current position + if (hasContent) { + const utteranceElements = document.querySelectorAll(".utterance"); + utteranceElements.forEach((el, i) => { + if (i === currentIndex) { + el.classList.add("current"); + el.classList.remove("played"); + } else if (i < currentIndex) { + el.classList.add("played"); + el.classList.remove("current"); + } else { + el.classList.remove("current", "played"); + } + }); + } + } catch (error) { + console.error("Error updating UI:", error); + } +} -

Gender :

- - -

Filter :

-
- { - checkboxOfflineChecked = e.target.checked; - filterVoices(); - }}> - -
- -

Text :

- textToReadFormated = e.target.value ? e.target.value : textToReadFormated}> - -
- -
- -
- ${selectedVoice ? aboutVoice() : undefined} -
- -
- -
- -`; -viewRender(); +// Initialize the application +init().then(() => { + // If there's a default voice selected after initialization, display its properties + if (currentVoice) { + displayVoiceProperties(currentVoice); + } +}); \ No newline at end of file diff --git a/demo/styles.css b/demo/styles.css index 55577b6..9efe1df 100644 --- a/demo/styles.css +++ b/demo/styles.css @@ -1,6 +1,10 @@ body, html { margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: 1.6; + color: #333; } html { @@ -8,9 +12,213 @@ html { } body { - height: 90%; + min-height: 100%; max-width: 800px; margin: 0 auto; + padding: 20px; + box-sizing: border-box; +} + +/* Control panel */ +.control-panel { + background: #f5f5f5; + padding: 20px; + border-radius: 8px; + margin: 0 0 20px 0; + width: 100%; + box-sizing: border-box; + text-align: left; +} + +.content-container { + background: #f5f5f5; + padding: 20px; + border-radius: 8px; + margin: 20px 0; + width: 100%; + box-sizing: border-box; +} + +/* Form elements */ +.form-group { + margin-bottom: 15px; + width: 100%; + text-align: left; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +select, +input[type="text"], +input[type="number"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + text-align: left; +} + +.checkbox-group { + margin: 10px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.checkbox-group input[type="checkbox"] { + margin: 0; + width: auto; +} + +/* Test utterance section */ +.test-utterance-container { + display: flex; + align-items: center; + gap: 10px; + width: 100%; +} + +.test-utterance-input { + flex: 1; +} + +.test-utterance-button { + margin: 0; + align-self: flex-end; + margin-bottom: 1px; /* Small adjustment for visual alignment */ +} + +.test-utterance-button button { + background-color: #4CAF50; /* Green color for the test button */ + color: white; + white-space: nowrap; + padding: 8px 16px; + height: 38px; /* Match input field height */ + box-sizing: border-box; +} + +.test-utterance-button button:hover:not(:disabled) { + background-color: #45a049; /* Darker green on hover */ + opacity: 1; +} + +/* Base button styles */ +button { + padding: 8px 16px; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + cursor: pointer; + transition: opacity 0.2s; + font-size: 14px; + margin-right: 10px; +} + +button:disabled { + background-color: #cccccc !important; + cursor: not-allowed; + opacity: 0.7; +} + +button:hover:not(:disabled) { + opacity: 0.9; +} + +/* Playback controls */ +.playback-controls { + display: flex; + gap: 10px; + margin: 20px 0; + justify-content: center; + flex-wrap: wrap; + align-items: center; +} + +/* Play state (green) */ +.play-state { + background-color: #4CAF50; /* Green for play */ +} + +.play-state:hover:not(:disabled) { + background-color: #45a049; /* Darker green on hover */ +} + +/* Pause state (orange) */ +.pause-state { + background-color: #ff9800; /* Orange for pause */ +} + +.pause-state:hover:not(:disabled) { + background-color: #e68900; /* Darker orange on hover */ +} + +/* Stop button (red) */ +.stop { + background-color: #f44336; /* Red for stop */ +} + +.stop:hover:not(:disabled) { + background-color: #d32f2f; /* Darker red on hover */ +} + +/* Navigation buttons (blue) */ +.nav { + background-color: #2196F3; /* Blue for navigation */ +} + +.nav:hover:not(:disabled) { + background-color: #1976D2; /* Darker blue on hover */ +} + +/* Jump Controls */ +.jump-controls { + display: flex; + gap: 10px; + margin: 20px 0; + justify-content: center; + align-items: center; +} + +.jump-controls input[type="number"] { + width: 60px; + padding: 5px; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* Jump To button (purple) */ +.jump-btn { + background-color: #9c27b0; /* Purple for jump */ +} + +.jump-btn:hover:not(:disabled) { + background-color: #7b1fa2; /* Darker purple on hover */ +} + +/* Highlight for current utterance */ +.highlight { + background-color: #ffeb3b; + padding: 0 2px; + border-radius: 3px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); + transition: background-color 0.2s ease; +} + +/* Text display */ +.text-display { + line-height: 2; + min-height: 100px; + border: 1px solid #eee; + padding: 15px; + border-radius: 4px; + white-space: pre-wrap; } h1, @@ -20,7 +228,6 @@ p { padding: 10px; } -.txt, select, form > div { display: block; @@ -30,52 +237,300 @@ form > div { padding: 5px; } -.txt { - width: 82%; +form > div { + margin-bottom: 10px; + overflow: auto; } -select { - width: 83%; +/* Demo section styles */ +.demo-section { + margin: 30px 0; + padding: 20px; + border: 1px solid #ddd; + border-radius: 8px; + background: #f9f9f9; } -form > div { - width: 81%; +.demo-section h2 { + margin-top: 0; + color: #333; + font-size: 1.5em; + padding-bottom: 10px; + border-bottom: 1px solid #eee; } -.txt, -form > div { - margin-bottom: 10px; - overflow: auto; +/* Utterances list */ +.utterances-list { + margin-top: 15px; } -.clearfix { - clear: both; +.utterance { + margin: 10px 0; + padding: 8px 12px; + border-radius: 4px; + transition: all 0.2s ease; + display: flex; + align-items: flex-start; + line-height: 1.5; + position: relative; + background: white; + border: 1px solid #e0e0e0; } -.controls { - text-align: center; - margin-top: 50px; +.utterance.current { + background-color: #e6f3ff; + border-left: 3px solid #1a73e8; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } -.controls > * { - margin-bottom: 10px; - text-align: center; +.utterance.played { + opacity: 0.7; + background-color: #f8f9fa; } -.controls fieldset { + +.utterance-number { display: inline-block; - text-align: left; + min-width: 24px; + color: #666; + font-weight: 600; + margin-right: 8px; + user-select: none; + font-family: monospace; + text-align: right; + flex-shrink: 0; + padding-top: 2px; } -.controls button { - padding: 10px; - width: 100px; +.utterance-text { + flex: 1; + white-space: pre-wrap; + word-break: break-word; + padding: 2px 0; } -.checkbox { - text-align: center; +/* Playback Controls */ +.playback-controls { + display: flex; + gap: 10px; + margin: 20px 0; + justify-content: center; } -.debug { - margin-top: 100px; - text-align: center; +/* Playback control buttons - base styles */ +.playback-controls button { + padding: 8px 16px; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + cursor: pointer; + transition: opacity 0.2s; +} + +.playback-controls button:hover { + opacity: 0.9; +} + +/* Play state (green) */ +.play-state, +.playback-controls .play-state { + background-color: #4CAF50 !important; /* Green for play */ +} + +.play-state:hover, +.playback-controls .play-state:hover { + background-color: #45a049 !important; /* Darker green on hover */ +} + +/* Pause state (orange) */ +.pause-state, +.playback-controls .pause-state { + background-color: #ff9800 !important; /* Orange for pause */ +} + +.pause-state:hover, +.playback-controls .pause-state:hover { + background-color: #e68900 !important; /* Darker orange on hover */ +} + +/* Stop button (red) */ +.stop, +.playback-controls .stop { + background-color: #f44336 !important; /* Red for stop */ +} + +.stop:hover, +.playback-controls .stop:hover { + background-color: #d32f2f !important; /* Darker red on hover */ +} + +/* Navigation buttons (blue) */ +.nav, +.playback-controls .nav { + background-color: #2196F3 !important; /* Blue for navigation */ +} + +.nav:hover, +.playback-controls .nav:hover { + background-color: #1976D2 !important; /* Darker blue on hover */ +} + +/* Jump Controls */ +.jump-controls { + display: flex; + gap: 10px; + margin: 20px 0; + justify-content: center; + align-items: center; +} + +.jump-controls input[type="number"] { + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + width: 120px; +} + +/* Jump To button (purple) */ +.jump-btn { + padding: 8px 16px; + background-color: #9c27b0; /* Purple for jump */ + color: white; + border: none; + border-radius: 4px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.2s; +} + +.jump-btn:hover { + background-color: #7b1fa2; /* Darker purple on hover */ +} + +/* Simple highlight style for the current word */ +::highlight(current-word) { + background-color: #ffeb3b; + color: black; +} + +/* Current utterance styling */ +.utterance.current { + background-color: #e3f2fd; + border-left: 3px solid #2196f3; + padding-left: 8px; +} + +/* Played utterances styling */ +.utterance.played { + background-color: #e8f5e9; + opacity: 0.8; +} + +.utterance.played .utterance-number { + color: #4caf50; + font-weight: bold; +} + +.utterance.played .utterance-text { + color: #2e7d32; +} + +/* Add some spacing between utterances */ +.utterance + .utterance { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #eee; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .demo-section { + padding: 15px; + } + + .utterance { + padding: 6px 8px; + } + + .utterance-number { + min-width: 20px; + margin-right: 6px; + } +} + +/* Voice Details Section */ +.voice-details { + background: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 8px; + margin: 20px 0; + overflow: hidden; +} + +.voice-details summary { + padding: 12px 16px; + background: #f0f0f0; + cursor: pointer; + font-weight: 600; + color: #333; + outline: none; + user-select: none; + transition: background-color 0.2s ease; +} + +.voice-details summary:hover { + background: #e8e8e8; +} + +.voice-details[open] summary { + background: #e0e0e0; +} + +.voice-properties { + padding: 16px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 14px; + line-height: 1.5; + color: #333; + background: white; +} + +.voice-property { + display: flex; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.voice-property:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.voice-property-name { + font-weight: 600; + color: #555; + min-width: 180px; + flex-shrink: 0; +} + +.voice-property-value { + flex-grow: 1; + word-break: break-word; +} + +.voice-property-value.boolean-true { + color: #388e3c; + font-weight: 500; +} + +.voice-property-value.boolean-false { + color: #d32f2f; + font-weight: 500; +} + +.voice-property-value.undefined { + color: #9e9e9e; + font-style: italic; } \ No newline at end of file diff --git a/docs/VoicesAndFiltering.md b/docs/VoicesAndFiltering.md new file mode 100644 index 0000000..2317e4b --- /dev/null +++ b/docs/VoicesAndFiltering.md @@ -0,0 +1,351 @@ +# Voices and Filtering + +With hundreds of voices available by default across various browsers and OS, it can be tricky for developers to provide sensible defaults and a curated list of voices. + +One of the goals of this project is to document higher quality voices available on various platforms and provide an easy way to implement these recommendations using JSON configuration files. + +## Use cases + +* Providing the best possible default voice per language +* Displaying an ordered list of voices, based on quality +* Displaying user-friendly voice names +* Filtering recommended voices per gender and age (adult vs children) +* Filtering out novelty and low quality voices +* Previewing a voice with a test utterance + +## List of supported languages + +The goal of this project is to support all 43 languages available on Windows and macOS. + +In its current state, it covers 43 languages: + +* [Arabic](json/ar.json) (Algeria, Bahrain, Egypt, Iraq, Jordan, Kuwait, Lebanon, Libya, Morocco, Oman, Qatar, Saudi Arabia, Syria, Tunisia, United Arab Emirates, Yemen) +* [Basque](json/eu.json) +* [Bengali](json/bn.json) (India and Bangladesh) +* [Bhojpuri](json/bho.json) +* [Bulgarian](json/bg.json) +* [Catalan](json/ca.json) +* Chinese: + * [Mandarin Chinese](json/cmn.json) (Mainland China, Taiwan) + * [Wu Chinese](json/wuu.json) (aka "Shanghainese") + * [Yue Chinese](json/yue.json) (aka "Cantonese") +* [Croatian](json/hr.json) +* [Czech](json/cs.json) +* [Danish](json/da.json) +* [Dutch](json/nl.json) (Netherlands and Belgium) +* [English](json/en.json) (United States, United Kingdom, Australia, Canada, Hong Kong, India, Ireland, Kenya, New Zealand, Nigeria, Scotland, Singapore, South Africa and Tanzania) +* [Finnish](json/fi.json) +* [French](json/fr.json) (France, Canada, Belgium and Switzerland) +* [Galician](json/gl.json) +* [German](json/de.json) (Germany, Austria and Switzerland) +* [Greek](json/el.json) +* [Hebrew](json/he.json) +* [Hindi](json/hi.json) +* [Hungarian](json/hu.json) +* [Indonesian](json/id.json) +* [Italian](json/it.json) +* [Japanese](json/ja.json) +* [Kannada](json/kn.json) +* [Korean](json/ko.json) +* [Malay](json/ms.json) +* [Marathi](json/mr.json) +* [Norwegian](json/nb.json) +* [Persian](json/fa.json) +* [Polish](json/pl.json) +* [Portuguese](json/pt.json) (Portugal and Brazil) +* [Romanian](json/ro.json) +* [Russian](json/ru.json) +* [Slovak](json/sk.json) +* [Slovenian](json/sl.json) +* [Spanish](json/es.json) (Spain, Argentina, Bolivia, Chile, Colombia, Costa Rica, Cuba, Dominican Republic, Ecuador, El Salvador, Equatorial Guinea, Guatemala, Honduras, Mexico, Nicaragua, Panama, Paraguay, Peru, Puerto Rico, United States, Uruguay and Venezuela) +* [Swedish](json/sv.json) +* [Tamil](json/ta.json) (India, Sri Lanka, Malaysia and Singapore) +* [Telugu](json/te.json) +* [Thai](json/th.json) +* [Turkish](json/tr.json) +* [Ukrainian](json/uk.json) +* [Vietnamese](json/vi.json) + +## List of voices to filter out + +At the other end up the spectrum, this project also identifies a number of voices that should be filtered out from a voice selector component. + +Some of them are harmful to the overall reading experience, while others have a very low quality on platforms where better preloaded options are available. + +* [Novelty voices](json/filters/novelty.json) (Apple devices) +* [Very low quality voices](json/filters/veryLowQuality.json) (Apple devices and Chrome OS) + + +## Guiding principles + +* Each voice list is ordered and meant to provide an optimal listening experience on all browsers/OS/languages covered by this project. +* But each list also includes default options, to make sure that there's always something reliable to lean on. +* With these two goals in mind, higher quality voices are listed on top of the list, while lower quality voices or specialized ones are listed at the bottom. +* The number of voices can look overwhelming (110+ voices in English alone) but in practice, just a few of them will be available to users on each of their device. +* The voice names returned by the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API) are hardly user-friendly, which is the reason why this list provides alternate ones that usually include a first name (or a gender) along with the region associated to the voice. +* Whenever possible, I will always try to include a good mix of high quality and default options for both genders. +* But the list has to be prioritized somehow, female voices are currently listed above their male counterparts. Since the gender associated to each voice is documented, this allows implementers to re-prioritize/filter the list based on this criteria. +* Regional variants are also grouped together in a single list rather than separated in their own files on purpose. On some devices, only two or three voices might be available and separating regional variants wouldn't make much sense. +* But regional variants have to be prioritized somehow in the list. For now, the regions with the best selections of voices are listed above, but it is highly recommended to implementers [to consider the user's regional preferences](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/languages). + + +## Syntax + +[A JSON Schema](voices.schema.json) is available for validation or potential contributors interested in opening a PR for new languages or voice additions. + +### Label + +`label` is required for each recommended voice and provides a human-friendly label for each voice. + +This string is localized for the target language and usually contains the following information: + +* First name (if available) +* Gender (when the first name is missing) +* Country/region + +**Example 1: Microsoft Natural voices** + +While the names documented by Microsoft for their natural voices are easily understandable, they tend to be very long and they're all localized in English. + +```json +{ + "label": "Isabella (Italia)", + "name": "Microsoft Isabella Online (Natural) - Italian (Italy)", + "language": "it-IT" +} +``` + +**Example 2: Chrome OS voices** + +Chrome OS provides a number of high quality voices through its Android subsystems, but they come with some of the worst names possibles for an end-user. + +```json +{ + "label": "Female voice 1 (US)", + "name": "Android Speech Recognition and Synthesis from Google en-us-x-tpc-network", + "language": "en-US" +} +``` + +### Names + +`name` is required for each recommended voice and it's used as the main identifier for voices in this project. + +Names are mostly stable across browsers, which means that for most voices, a single string is sufficient. + +But there are unfortunately some outliers: Android, iOS, iPadOS and macOS voices. + +For those voices, at least a portion of the string is often localized, naming can be inconsistent across browsers and they can change depending on the number of variants installed. + +Because of this, each list can also contain the following properties: + +- `altNames` with an array of alternate strings for a given voice +- and `localizedName` that identifies the string pattern used for localizing these voices + +**Example 3: Alternate version of an Apple preloaded voice** + +```json +{ + "label": "Samantha (US)", + "name": "Samantha", + "localizedName": "apple", + "altNames": [ + "Samantha (Enhanced)", + "Samantha (English (United States))" + ], + "language": "en-US" +} +``` + +### Languages + +`language` is required for each recommended voice. + +It contains a BCP 47 language tag where a downcased two-letter language code is followed by an uppercased two-letter country code. + +The language and country codes are separated using a hyphen (-). + +Somes voices are also capable of handling another language, for example a Spanish voice for the United States might also be capable of handling English. + +For this reason, an `additionalLanguages` property is also available although it is fairly rarely used right now. + +It contains a list of languages using only two-letter codes, without a sub-tag. + +Some brand new voices from Microsoft are also capable of a multilingual output. The language switch isn't supported in the middle of a sentence, but the output seems capable of auto-detecting the language of each sentence and adopt itself accordingly. + +In order to support this, the output might automatically switch to a different voice in the process. + +These voices are identified using the `multiLingual` boolean. + +**Example 4: Voice with a multilingual output** + +```json +{ + "label": "Emma (US)", + "name": "Microsoft EmmaMultilingual Online (Natural) - English (United States)", + "language": "en-US", + "multiLingual": true +} +``` + +**Example 5: Voice capable of handling a secondary language** + +```json +{ + "label": "Sylvie (Canada)", + "name": "Microsoft Sylvie Online (Natural) - French (Canada)", + "language": "fr-CA", + "otherLanguages": [ + "en" + ] +} +``` + +### Gender and children voices + +`gender` is an optional property for each voice, that documents the gender associated to each voice. + +The following values are supported: `female`, `male` or `neutral`. + +`children` is also optional and identifies children voices using a boolean. + +**Example 6: Female children voice** + +```json +{ + "label": "Ana (US)", + "name": "Microsoft Ana Online (Natural) - English (United States)", + "language": "en-US", + "gender": "female", + "children": true +} +``` + +### Quality + +`quality` is an optional property for each voice, that documents the quality of the various variants of a voice. + +The following values are supported: +
+
veryHigh
+
Very high, almost human-indistinguishable quality of speech synthesis
+
high
+
High, human-like quality of speech synthesis
+
normal
+
Normal quality of speech synthesis
+
low
+
Low, not human-like quality of speech synthesis
+
veryLow
+
Very low, but still intelligible quality of speech synthesis
+
+ +**Example 7: An Apple voice available in three quality variants** + +```json +{ + "label": "Ava (US)", + "name": "Ava", + "note": "This voice can be installed on all Apple devices and offers three variants. Like all voices that can be installed on Apple devices, it suffers from inconsistent naming due to localization.", + "altNames": [ + "Ava (Premium)", + "Ava (Enhanced)", + "Ava (English (United States))", + ], + "language": "en-US", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] +} +``` + + +### OS and browser + +Both `os` and `browser` are optional properties. They're used to indicate in which operating systems and browsers a voice is available. + +These two properties are meant to be interpreted separately and not as a combination. + +**Example 8: A Microsoft voice available in both Edge and Windows** + +```json +{ + "label": "Denise (France)", + "name": "Microsoft Denise Online (Natural) - French (France)", + "note": "This voice is preloaded in Edge on desktop. In other browsers, it requires the user to run Windows 11 and install the voice pack.", + "language": "fr-FR", + "gender": "female", + "os": [ + "Windows" + ], + "browser": [ + "Edge" + ] +} +``` + +In addition, `preloaded` indicates if the voice is preloaded in all the OS and browsers that have been identified. + +With the current approach, it's not possible to indicate that a voice is available on Chrome and Windows, but requires a download on Windows for example. + +**Example 9: A Google voice preloaded in Chrome Desktop** + +```json +{ + "label": "Google female voice (UK)", + "name": "Google UK English Female", + "language": "en-GB", + "gender": "female", + "browser": [ + "ChromeDesktop" + ], + "preloaded": true +} +``` + +### Speech rate and pitch + +When using the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API), `SpeechSynthesisUtterance` supports optional values for: + +- [`rate`](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/rate) to control the speech rate +- and [`pitch`](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/pitch) to control the pitch + +Each voice documented in this repo supports the following optional properties: + +- `pitchControl` is a boolean that defaults to `true` and indicates if a voice can be pitch controlled +- `rate` is an integer between 0.1 and 10 that defaults to 1 and provides a recommended default speech rate for each voice +- `pitch` is an integer between 0 and 2 that defaults to 1 and provides a recommended default pitch for each voice + +**Example 10: Microsoft voice where the pitch cannot be adjusted** + +```json +{ + "label": "Ana (US)", + "name": "Microsoft Ana Online (Natural) - English (United States)", + "language": "en-US", + "gender": "female", + "pitchControl": false +} +``` + +**Example 11: Google voice with recommended pitch and speed rates** + +```json +{ + "label": "Voix Google féminine (France)", + "name": "Google français", + "language": "fr-FR", + "gender": "female", + "rate": 1, + "pitch": 0.8 +} +``` \ No newline at end of file diff --git a/docs/WebSpeech.md b/docs/WebSpeech.md new file mode 100644 index 0000000..554d0f2 --- /dev/null +++ b/docs/WebSpeech.md @@ -0,0 +1,99 @@ +# SpeechSynthesis in browsers and OSes + +Through the work done to document a list of recommended voices, various browsers/OS have been tested to see how they behave. This section is meant to summarize some of this information. + +## General + +* The Web Speech API returns the following fields through the `getVoices()` method: `name`, `voiceURI`, `lang`, `localService` and `default`. +* While `voiceURI` should be the most consistent way of identifying a voice in theory, in practice this couldn't be further from the truth. Most browsers use the same value than `name` for `voiceURI` and do not enforce uniqueness. +* As we'll see in notes for specific browsers/OS, `name` is also inconsistently implemented and can return different values for the same voice on the same device. +* `localService` indicates if a voice is available for offline use and it seems to be working as expected, which is why the current list of recommended voices doesn't contain that information. +* `lang` seems to be mostly reliable across implementations, returning a language using BCP 47 language tags, with the main language in downcase and the subtag in uppercase (`pt-BR`). +* There are unfortunately a few outliers: + * On Android, Samsung and Chrome use an underscore as the separator instead: `en_us` ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/13)) + * While Firefox on Android gets even more creative, using three letter codes for languages and adding an extra string at the end: `eng-US-f000` ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/17)) +* `default` is meant to indicate if a voice is the default voice for the current app language. In theory this should be extremely useful, but in practice it's really hard to use due to inconsistencies across implementations, limited context (system default vs user default) and the lack of capability for setting a default voice per language. +* In addition to the use of `default`, implementers should always consider using the `Accept-Language` HTTP header as well, as it contains an ordered list of preferred language/region for a given user. + +## Android + +* For now, we've only covered testing and documentation on vanilla versions of Android, as available on Google Pixel devices. The list of voices available may vary greatly based on OEM, device and Android version. +* Due to the nature of Android, documenting all these variations will be very difficult. Further attempts will be made in future version of this project through the use of device farms ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/8)). +* In recent versions of vanilla Android, there's an excellent selection of high quality voices which cover a wide range of languages/regions (67 as of April 2024). +* To use these voices, the user needs to go fairly deep in system settings either to download them (only your system language and some of the most popular languages are preloaded by default) or select their preferred voice per language/region. +* Unfortunately, Chrome on Android doesn't return the list of voices available to the users, instead it returns an unfiltered list of languages/regions ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/12)). +* To make things worse, these voices and regions are all localized with the system locale. +* Among other things, this means that even languages and regions which require a voice pack to be installed will show up in the list returned by the Web Speech API ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/14)). +* If the user selects a language/region for which the voice pack needs to be downloaded, Chrome will default to an English voice instead ([related issue](https://github.com/HadrienGardeur/read-aloud-best-practices/issues/6)). +* Even when a voice pack has been installed, the user may need to select a default voice for each region before a language/region can be used at all. +* With this poor approach to voice selection, Chrome on Android doesn't indicate the user's preferred language/region either using `default` ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/16)). + +## Chrome Desktop + +* On desktop, Chrome comes preloaded with a limited selection of 19 high quality voices across 15 languages. +* All of these voices require online access to use them, without any fallback to a lower quality offline variant. +* Unfortunately, these voices are also plagued by a bug if any utterance read by the Web Speech API takes longer than 14 seconds ([related issue](https://github.com/HadrienGardeur/read-aloud-best-practices/issues/3)) and do not return boundary events ([related issue](https://github.com/HadrienGardeur/read-aloud-best-practices/issues/4)). +* Under the current circumstances, these Google voices have been prioritized lower than their Microsoft/Apple counterparts in the list of recommended voices. +* Overall, it's unfortunate that Chrome Desktop is lagging far behind Android and Chrome OS when it comes to the range of voices and languages supported by default ([related issue](https://github.com/HadrienGardeur/read-aloud-best-practices/issues/21)). + +## Chrome OS + +* Chrome OS comes with four sets of voices: Chrome OS voices, Android voices (50+ languages), Natural voices and eSpeak voices (38 languages). +* By default, Chrome OS downloads Chrome OS voices for your system language, while Android and eSpeak voices are available for all languages. +* Google is also gradually adding support for Natural voices, which are basically the higher quality variants of their Android voices with the added benefit of working offline. Natural voices require the user to go to their system settings to install them. +* Chrome OS has an unfortunate tendency of uninstalling voice packs whenever a new Chrome OS update is installed, which happens very often. +* Most Android voices offer offline and online variants and they're on par quality-wise with what Apple offers in terms of downloadable voices. +* These Android voices have some of the worst names on any platform/browser, making them hardly usable without the kind of re-labeling offered by this project. +* Android voices also suffer from issues with latency and/or availability. In some cases, it might take up to a minute for the first utterance to be read aloud. +* Chrome voices are one step below Android voices, but they offer a decent selection for the most common languages. +* eSpeak voices should be avoided at all cost due to their extremely low quality and have been documented separately in order to filter them out. + +## Edge + +* On desktop, Edge provides the best selection of high quality voices with over 250 preloaded voices across 75 languages (as of April 2024). +* All of these so-called "natural" voices rely on Machine Learning (ML) and therefore require online access to use them. +* A small number of those voices are also multilingual and seem to be able to detect the language of a sentence and adapt accordingly. Unfortunately, this doesn't work as well when there's a language switch in the middle of a sentence. +* On macOS at least, there's a weird bug where Edge only displays 18 natural voices initially, but this extends to 250+ once Web Speech API has been used to output an utterance. +* There are also additional issues that implementers should be aware of when using these voices: they don't support pitch adjustment ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/35)) and a number of characters need to be escaped to avoid playback issues ([related issue](https://github.com/HadrienGardeur/read-aloud-best-practices/issues/8)). +* On mobile, Edge isn't nearly as interesting: + * It's completely unusable on Android since it returns an empty list of voices, which makes it impossible to use with Web Speech API ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/20)). + * On iOS/iPadOS, all browsers are currently forced to use Safari as their engine, which means that Edge behaves exactly like Safari Mobile. + +## Firefox + +* On desktop, Firefox seems fairly straightforward when it comes to voice selection. +* Unlike Chrome and Edge, Firefox doesn't come with any preloaded voice of its own. +* Firefox has a different approach for `voiceURI` where each voice is truly identified by a unique URN. +* Since this is unique to Firefox, the current JSON files do not document these URI yet, but this could be a future addition. +* On macOS, Firefox requires a full system reboot for new voices to show up in the list. + +## iOS and iPadOS + +* Both OS come with the same set of preloaded voices and downloadable voices than macOS. [Read the macOS section](#macOS) below for additional information about the voices available. +* For an unknown reason, some preloaded voices are also listed twice but provide the same audio output. +* All browsers need to run on the system webview which means that they're just a shell on top of Safari Mobile rather than truly different browsers. +* This situation could change due to the Digital Market Act in Europe, forcing Apple to change its policy on third-party browsers and webviews. + +## macOS + +* macOS provides an extensive list of voices across 45 languages, both preloaded or downloadable. +* These voices can have up to three different variants, based on the quality of the output (and download size). +* The highest quality voices are probably the ones available for Siri, but they're unfortunately unavailable through the Web Speech API ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/22)). +* At the other end of the spectrum, Apple had the unfortunate idea of preloading a large range of low quality and weird voices such as the Eloquence (8 voices) and Effects (15 voices) voice packs. +* The existence of these voices alone is a good reason to filter voices available to macOS users and highlight the ones recommended on this repo. +* Unlike other platforms/OS, macOS decided to localize voice names. This wouldn't be an issue if `voiceURI` could be used as a reliable identifier for voices, but that's not the case ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/23)). +* In its current state, this repo only documents localizations for the languages supported officially and not the 45 languages supported by the macOS TTS engine. + +## Safari + +* For better or for worse, Safari's behaviour is mostly consistent between its desktop and mobile versions. +* Downloadable voices do not show up in the list returned by the Web Speech API ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/19)). +* Even worse than that, when installing higher quality variants of preloaded voices, these voices disappear in Safari, which means that entire languages could disappear completely. +* All voices return `true` for `default` in Safari, which makes it impossible to detect and select the system/user default ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/16)). + +## Windows + +* [Microsoft provides a very helpful page](https://support.microsoft.com/en-us/windows/appendix-a-supported-languages-and-voices-4486e345-7730-53da-fcfe-55cc64300f01), listing all voices available across Windows 10 and 11 for a total of 98 voices across 36 languages. +* Natural voices provide a far better experience but they require an up-to-date version of Windows 11 and need to be downloaded (with the added benefit that they also work offline). +* Microsoft has been slow to add these natural voices to Windows 11 overall. Until fairly recently, only US voices (3 voices) were available. The list is now a little longer (23 voices across 8 languages) but remains far behind what they offer through Edge (250+ voices across 75 languages). +* Unfortunately, these higher quality voices are not properly listed in Chrome or Firefox currently ([related issue](https://github.com/HadrienGardeur/web-speech-recommended-voices/issues/15)). They only show up in Edge, where they're preloaded anyway but strictly for an online use. diff --git a/json/ar.json b/json/ar.json new file mode 100644 index 0000000..3085259 --- /dev/null +++ b/json/ar.json @@ -0,0 +1,684 @@ +{ + "language": "ar", + "defaultRegion": "ar-SA", + "testUtterance": "مرحبًا، اسمي {name} وأنا صوت عربي.", + "voices": [ + { + "label": "Amina", + "name": "Microsoft Amina Online (Natural) - Arabic (Algeria)", + "language": "ar-DZ", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ismael", + "name": "Microsoft Ismael Online (Natural) - Arabic (Algeria)", + "language": "ar-DZ", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Laila", + "name": "Microsoft Laila Online (Natural) - Arabic (Bahrain)", + "language": "ar-BH", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ali", + "name": "Microsoft Ali Online (Natural) - Arabic (Bahrain)", + "language": "ar-BH", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Salma", + "name": "Microsoft Salma Online (Natural) - Arabic (Egypt)", + "language": "ar-EG", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Shakir", + "name": "Microsoft Shakir Online (Natural) - Arabic (Egypt)", + "language": "ar-EG", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Rana", + "name": "Microsoft Rana Online (Natural) - Arabic (Iraq)", + "language": "ar-IQ", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Bassel", + "name": "Microsoft Bassel Online (Natural) - Arabic (Iraq)", + "language": "ar-IQ", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sana", + "name": "Microsoft Sana Online (Natural) - Arabic (Jordan)", + "language": "ar-JO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Taim", + "name": "Microsoft Taim Online (Natural) - Arabic (Jordan)", + "language": "ar-JO", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Noura", + "name": "Microsoft Noura Online (Natural) - Arabic (Kuwait)", + "language": "ar-KW", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Fahed", + "name": "Microsoft Fahed Online (Natural) - Arabic (Kuwait)", + "language": "ar-KW", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Layla", + "name": "Microsoft Layla Online (Natural) - Arabic (Lebanon)", + "language": "ar-LB", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Rami", + "name": "Microsoft Rami Online (Natural) - Arabic (Lebanon)", + "language": "ar-LB", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Iman", + "name": "Microsoft Iman Online (Natural) - Arabic (Libya)", + "language": "ar-LY", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Omar", + "name": "Microsoft Omar Online (Natural) - Arabic (Libya)", + "language": "ar-LY", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mouna", + "name": "Microsoft Mouna Online (Natural) - Arabic (Morocco)", + "language": "ar-MA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Jamal", + "name": "Microsoft Jamal Online (Natural) - Arabic (Morocco)", + "language": "ar-MA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Aysha", + "name": "Microsoft Aysha Online (Natural) - Arabic (Oman)", + "language": "ar-OM", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Abdullah", + "name": "Microsoft Abdullah Online (Natural) - Arabic (Oman)", + "language": "ar-OM", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Amal", + "name": "Microsoft Amal Online (Natural) - Arabic (Qatar)", + "language": "ar-QA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Moaz", + "name": "Microsoft Moaz Online (Natural) - Arabic (Qatar)", + "language": "ar-QA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Zariyah", + "name": "Microsoft Zariyah Online (Natural) - Arabic (Saudi Arabia)", + "language": "ar-SA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Hamed", + "name": "Microsoft Hamed Online (Natural) - Arabic (Saudi Arabia)", + "language": "ar-SA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Amany", + "name": "Microsoft Amany Online (Natural) - Arabic (Syria)", + "language": "ar-SY", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Laith", + "name": "Microsoft Laith Online (Natural) - Arabic (Syria)", + "language": "ar-SY", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Reem", + "name": "Microsoft Reem Online (Natural) - Arabic (Tunisia)", + "language": "ar-TN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Hedi", + "name": "Microsoft Hedi Online (Natural) - Arabic (Tunisia)", + "language": "ar-TN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Fatima", + "name": "Microsoft Fatima Online (Natural) - Arabic (United Arab Emirates)", + "language": "ar-AE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Hamdan", + "name": "Microsoft Hamdan Online (Natural) - Arabic (United Arab Emirates)", + "language": "ar-AE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Maryam", + "name": "Microsoft Maryam Online (Natural) - Arabic (Yemen)", + "language": "ar-YE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Saleh", + "name": "Microsoft Saleh Online (Natural) - Arabic (Yemen)", + "language": "ar-YE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mariam", + "name": "Mariam", + "localizedName": "apple", + "language": "ar-001", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Apple Laila", + "name": "Laila", + "localizedName": "apple", + "language": "ar-001", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Tarik", + "name": "Tarik", + "localizedName": "apple", + "language": "ar-001", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Majed", + "name": "Majed", + "localizedName": "apple", + "language": "ar-001", + "gender": "male", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Hoda", + "name": "Microsoft Hoda - Arabic (Arabic )", + "language": "ar-EG", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Naayf", + "name": "Microsoft Naayf - Arabic (Saudi Arabia)", + "language": "ar-AS", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "صوت انثوي 1", + "name": "Android Speech Recognition and Synthesis from Google ar-xa-x-arc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ar-xa-x-arc-local", + "Android Speech Recognition and Synthesis from Google ar-language" + ], + "nativeID": [ + "ar-xa-x-arc-network", + "ar-xa-x-arc-local" + ], + "language": "ar", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "صوت انثوي 2", + "name": "Android Speech Recognition and Synthesis from Google ar-xa-x-arz-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ar-xa-x-arz-local" + ], + "nativeID": [ + "ar-xa-x-arz-network", + "ar-xa-x-arz-local" + ], + "language": "ar", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "صوت ذكر 1", + "name": "Android Speech Recognition and Synthesis from Google ar-xa-x-ard-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ar-xa-x-ard-local" + ], + "nativeID": [ + "ar-xa-x-ard-network", + "ar-xa-x-ard-local" + ], + "language": "ar", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "صوت ذكر 2", + "name": "Android Speech Recognition and Synthesis from Google ar-xa-x-are-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ar-xa-x-are-local" + ], + "nativeID": [ + "ar-xa-x-are-network", + "ar-xa-x-are-local" + ], + "language": "ar", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/bg.json b/json/bg.json new file mode 100644 index 0000000..5e6a138 --- /dev/null +++ b/json/bg.json @@ -0,0 +1,95 @@ +{ + "language": "bg", + "defaultRegion": "bg-BG", + "testUtterance": "Здравейте, казвам се {name} и съм български глас.", + "voices": [ + { + "label": "Kalina", + "name": "Microsoft Kalina Online (Natural) - Bulgarian (Bulgaria)", + "language": "bg-BG", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Borislav", + "name": "Microsoft Borislav Online (Natural) - Bulgarian (Bulgaria)", + "language": "bg-BG", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Daria", + "name": "Daria", + "localizedName": "apple", + "language": "bg-BG", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Ivan", + "name": "Microsoft Ivan - Bulgarian (Bulgaria)", + "language": "bg-BG", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Женски глас", + "name": "Android Speech Recognition and Synthesis from Google bg-bg-x-ifk-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google bg-bg-x-ifk-local", + "Android Speech Recognition and Synthesis from Google bg-bg-language" + ], + "nativeID": [ + "bg-bg-x-ifk-network", + "bg-bg-x-ifk-local" + ], + "language": "bg-BG", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/bho.json b/json/bho.json new file mode 100644 index 0000000..da3f456 --- /dev/null +++ b/json/bho.json @@ -0,0 +1,26 @@ +{ + "language": "bho", + "defaultRegion": "bho-IN", + "testUtterance": "नमस्कार, हमार नाम {name} ह आ हम भोजपुरी आवाज हईं", + "voices": [ + { + "label": "Jaya", + "name": "Jaya", + "localizedName": "apple", + "language": "bho-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/bn.json b/json/bn.json new file mode 100644 index 0000000..3c52da6 --- /dev/null +++ b/json/bn.json @@ -0,0 +1,205 @@ +{ + "language": "bn", + "defaultRegion": "bn-IN", + "testUtterance": "হ্যালো, আমার নাম {name} এবং আমি একজন বাংলা ভয়েস।", + "voices": [ + { + "label": "Tanishaa", + "name": "Microsoft Tanishaa Online (Natural) - Bengali (India)", + "language": "bn-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Bashkar", + "name": "Microsoft Bashkar Online (Natural) - Bangla (India)", + "language": "bn-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Nabanita", + "name": "Microsoft Nabanita Online (Natural) - Bangla (Bangladesh)", + "language": "bn-BD", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Pradeep", + "name": "Microsoft Pradeep Online (Natural) - Bangla (Bangladesh)", + "language": "bn-BD", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Piya", + "name": "Piya", + "localizedName": "apple", + "language": "bn-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "মহিলা কণ্ঠস্বর 1", + "name": "Android Speech Recognition and Synthesis from Google bn-in-x-bnf-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google bn-in-x-bnf-local", + "Android Speech Recognition and Synthesis from Google bn-IN-language" + ], + "nativeID": [ + "bn-in-x-bnf-network", + "bn-in-x-bnf-local" + ], + "language": "bn-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "মহিলা কণ্ঠস্বর 2", + "name": "Android Speech Recognition and Synthesis from Google bn-in-x-bnx-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google bn-in-x-bnx-local" + ], + "nativeID": [ + "bn-in-x-bnx-network", + "bn-in-x-bnx-local" + ], + "language": "bn-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "পুরুষ কন্ঠ 1", + "name": "Android Speech Recognition and Synthesis from Google bn-in-x-bin-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google bn-in-x-bin-local" + ], + "nativeID": [ + "bn-in-x-bin-network", + "bn-in-x-bin-local" + ], + "language": "bn-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "পুরুষ কন্ঠ 2", + "name": "Android Speech Recognition and Synthesis from Google bn-in-x-bnm-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google bn-in-x-bnm-local" + ], + "nativeID": [ + "bn-in-x-bnm-network", + "bn-in-x-bnm-local" + ], + "language": "bn-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "পুরুষ কন্ঠ", + "name": "Google বাংলা (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google bn-bd-x-ban-network", + "Chrome OS বাংলা", + "Android Speech Recognition and Synthesis from Google bn-bd-x-ban-local", + "Android Speech Recognition and Synthesis from Google bn-BD-language" + ], + "nativeID": [ + "bn-bd-x-ban-network", + "bn-bd-x-ban-local" + ], + "language": "bn-BD", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/ca.json b/json/ca.json new file mode 100644 index 0000000..411d500 --- /dev/null +++ b/json/ca.json @@ -0,0 +1,140 @@ +{ + "language": "ca", + "defaultRegion": "ca-ES", + "testUtterance": "Hola, em dic {name} i sóc una veu catalana", + "voices": [ + { + "label": "Joana (Català)", + "name": "Microsoft Joana Online (Natural) - Catalan", + "language": "ca-ES", + "otherLanguages": [ + "es" + ], + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Enric (Català)", + "name": "Microsoft Enric Online (Natural) - Catalan", + "language": "ca-ES", + "otherLanguages": [ + "es" + ], + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Montse (Català)", + "name": "Montse", + "localizedName": "apple", + "language": "ca-ES", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Pau (Valencià)", + "name": "Pau", + "localizedName": "apple", + "language": "ca-ES-u-sd-esvc", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Jordi (Català)", + "name": "Jordi", + "localizedName": "apple", + "language": "ca-ES", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Herena (Català)", + "name": "Microsoft Herena - Catalan (Spain)", + "language": "ca-ES", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Veu femenina catalana", + "name": "Android Speech Recognition and Synthesis from Google ca-es-x-caf-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ca-es-x-caf-local", + "Android Speech Recognition and Synthesis from Google ca-ES-language" + ], + "nativeID": [ + "ca-es-x-caf-network", + "ca-es-x-caf-local" + ], + "language": "ca-ES", + "otherLanguages": [ + "es" + ], + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/cmn.json b/json/cmn.json new file mode 100644 index 0000000..82a329c --- /dev/null +++ b/json/cmn.json @@ -0,0 +1,844 @@ +{ + "language": "cmn", + "defaultRegion": "cmn-CN", + "testUtterance": "你好,我的名字是 {name},我是普通话配音。", + "voices": [ + { + "label": "Xiaoxiao", + "name": "Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Xiaoyi", + "name": "Microsoft Xiaoyi Online (Natural) - Chinese (Mainland)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yunxi", + "name": "Microsoft Yunxi Online (Natural) - Chinese (Mainland)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yunxia", + "name": "Microsoft Yunxia Online (Natural) - Chinese (Mainland)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Xiaobei", + "name": "Microsoft Xiaobei Online (Natural) - Chinese (Northeastern Mandarin)", + "language": "cmn-CN-liaoning", + "altLanguage": "zh-CN-liaoning", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Xiaoni", + "name": "Microsoft Xiaoni Online (Natural) - Chinese (Zhongyuan Mandarin Shaanxi)", + "language": "cmn-CN-shaanxi", + "altLanguage": "zh-CN-shaanxi", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yunjian", + "name": "Microsoft Yunjian Online (Natural) - Chinese (Mainland)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yunyang", + "name": "Microsoft Yunyang Online (Natural) - Chinese (Mainland)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "HsiaoChen", + "name": "Microsoft HsiaoChen Online (Natural) - Chinese (Taiwan)", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "HsiaoYu", + "name": "Microsoft HsiaoYu Online (Natural) - Chinese (Taiwanese Mandarin)", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "YunJhe", + "name": "Microsoft YunJhe Online (Natural) - Chinese (Taiwan)", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Lilian", + "name": "Lilian", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Tiantian", + "name": "Tiantian", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Shasha", + "name": "Shasha", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Lili", + "name": "Lili", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Lisheng", + "name": "Lisheng", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Lanlan", + "name": "Lanlan", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Shanshan", + "name": "Shanshan", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Yue", + "name": "Yue", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Tingting", + "name": "Tingting", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Yu-shu", + "name": "Yu-shu", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Dongmei", + "name": "Dongmei", + "localizedName": "apple", + "language": "cmn-CN-liaoning", + "altLanguage": "zh-CN-liaoning", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Panpan", + "name": "Panpan", + "localizedName": "apple", + "language": "cmn-CN-sichuan", + "altLanguage": "zh-CN-sichuan", + "gender": "female", + "quality": [ + "low", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Meijia", + "name": "Meijia", + "localizedName": "apple", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "low", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Han", + "name": "Han", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Bobo", + "name": "Bobo", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Taotao", + "name": "Taotao", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Binbin", + "name": "Binbin", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Li-Mu", + "name": "Li-Mu", + "localizedName": "apple", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Haohao", + "name": "Haohao", + "localizedName": "apple", + "language": "cmn-CN-shaanxi", + "altLanguage": "zh-CN-shaanxi", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Google 女声", + "name": "Google 普通话(中国大陆)", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Google 女聲", + "name": "Google 國語(臺灣)", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Huihui", + "name": "Microsoft Huihui - Chinese (Simplified, PRC)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Yaoyao", + "name": "Microsoft Yaoyao - Chinese (Simplified, PRC)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Kangkang", + "name": "Microsoft Kangkang - Chinese (Simplified, PRC)", + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Yating", + "name": "Microsoft Yating - Chinese (Traditional, Taiwan)", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Hanhan", + "name": "Microsoft Hanhan - Chinese (Traditional, Taiwan)", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Zhiwei", + "name": "Microsoft Zhiwei - Chinese (Traditional, Taiwan)", + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "女声1", + "name": "Android Speech Recognition and Synthesis from Google cmn-CN-x-ccc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google cmn-CN-x-ccc-local", + "Android Speech Recognition and Synthesis from Google zh-CN-language" + ], + "nativeID": [ + "cmn-CN-x-ccc-network", + "cmn-CN-x-ccc-local" + ], + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "女声2", + "name": "Android Speech Recognition and Synthesis from Google cmn-CN-x-ssa-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google cmn-CN-x-ssa-local" + ], + "nativeID": [ + "cmn-CN-x-ssa-network", + "cmn-CN-x-ssa-local" + ], + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男声1", + "name": "Android Speech Recognition and Synthesis from Google cmn-CN-x-ccd-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google cmn-CN-x-ccd-local" + ], + "nativeID": [ + "cmn-CN-x-ccd-network", + "cmn-CN-x-ccd-local" + ], + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男声2", + "name": "Android Speech Recognition and Synthesis from Google cmn-CN-x-cce-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google cmn-CN-x-cce-local" + ], + "nativeID": [ + "cmn-CN-x-cce-network", + "cmn-CN-x-cce-local" + ], + "language": "cmn-CN", + "altLanguage": "zh-CN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "女聲", + "name": "Android Speech Recognition and Synthesis from Google cmn-TW-x-ctc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google cmn-TW-x-ctc-local", + "Android Speech Recognition and Synthesis from Google zh-TW-language" + ], + "nativeID": [ + "cmn-TW-x-ctc-network", + "cmn-TW-x-ctc-local" + ], + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男聲1", + "name": "Android Speech Recognition and Synthesis from Google cmn-TW-x-ctd-network", + "altNames": [ + "Chrome OS 粵語 1", + "Android Speech Recognition and Synthesis from Google cmn-TW-x-ctd-local" + ], + "nativeID": [ + "cmn-TW-x-ctd-network", + "cmn-TW-x-ctd-local" + ], + "language": "cmn-TW", + "altLanguage": "zh-TW", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男聲2", + "name": "Android Speech Recognition and Synthesis from Google cmn-TW-x-cte-network", + "altNames": [ + "Chrome OS 粵語 1", + "Android Speech Recognition and Synthesis from Google cmn-TW-x-cte-local" + ], + "nativeID": [ + "cmn-TW-x-cte-network", + "cmn-TW-x-cte-local" + ], + "language": "cmn-CTW", + "altLanguage": "zh-TW", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/cs.json b/json/cs.json new file mode 100644 index 0000000..dcb7c19 --- /dev/null +++ b/json/cs.json @@ -0,0 +1,116 @@ +{ + "language": "cs", + "defaultRegion": "cs-CZ", + "testUtterance": "Dobrý den, jmenuji se {name} a jsem český hlas.", + "voices": [ + { + "label": "Vlasta", + "name": "Microsoft Vlasta Online (Natural) - Czech (Czech)", + "language": "cs-CZ", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Antonin", + "name": "Microsoft Antonin Online (Natural) - Czech (Czech)", + "language": "cs-CZ", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Zuzana", + "name": "Zuzana", + "localizedName": "apple", + "language": "cs-CZ", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Iveta", + "name": "Iveta", + "localizedName": "apple", + "language": "cs-CZ", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Jakub", + "name": "Microsoft Jakub - Czech (Czech)", + "language": "cs-CZ", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Ženský hlas", + "name": "Google čeština (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google cs-cz-x-jfs-network", + "Chrome OS čeština", + "Android Speech Recognition and Synthesis from Google cs-cz-x-jfs-local", + "Android Speech Recognition and Synthesis from Google cs-CZ-language" + ], + "nativeID": [ + "cs-cz-x-jfs-network", + "cs-cz-x-jfs-local" + ], + "language": "cs-CZ", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} diff --git a/json/da.json b/json/da.json new file mode 100644 index 0000000..0f19cf3 --- /dev/null +++ b/json/da.json @@ -0,0 +1,190 @@ +{ + "language": "da", + "defaultRegion": "da-DK", + "testUtterance": "Hej, mit navn er {name} og jeg er en dansk stemme.", + "voices": [ + { + "label": "Christel", + "name": "Microsoft Christel Online (Natural) - Danish (Denmark)", + "language": "da-DK", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Jeppe", + "name": "Microsoft Jeppe Online (Natural) - Danish (Denmark)", + "language": "da-DK", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sara", + "name": "Sara", + "localizedName": "apple", + "language": "da-DK", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Magnus", + "name": "Magnus", + "localizedName": "apple", + "language": "da-DK", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Helle", + "name": "Microsoft Helle - Danish (Denmark)", + "language": "da-DK", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Kvindestemme 1", + "name": "Google Dansk 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google da-dk-x-kfm-network", + "Chrome OS Dansk 1", + "Android Speech Recognition and Synthesis from Google da-dk-x-kfm-local", + "Android Speech Recognition and Synthesis from Google da-DK-language" + ], + "nativeID": [ + "da-dk-x-kfm-network", + "da-dk-x-kfm-local" + ], + "language": "da-DK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kvindestemme 2", + "name": "Google Dansk 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google da-dk-x-sfp-network", + "Chrome OS Dansk 3", + "Android Speech Recognition and Synthesis from Google da-dk-x-sfp-local" + ], + "nativeID": [ + "da-dk-x-sfp-network", + "da-dk-x-sfp-local" + ], + "language": "da-DK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kvindestemme 3", + "name": "Google Dansk 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google da-dk-x-vfb-network", + "Chrome OS Dansk 4", + "Android Speech Recognition and Synthesis from Google da-dk-x-vfb-local" + ], + "nativeID": [ + "da-dk-x-vfb-network", + "da-dk-x-vfb-local" + ], + "language": "da-DK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mandsstemme", + "name": "Google Dansk 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google da-dk-x-nmm-network", + "Chrome OS Dansk 2", + "Android Speech Recognition and Synthesis from Google da-dk-x-nmm-local" + ], + "nativeID": [ + "da-dk-x-nmm-network", + "da-dk-x-nmm-local" + ], + "language": "da-DK", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/de.json b/json/de.json new file mode 100644 index 0000000..e998345 --- /dev/null +++ b/json/de.json @@ -0,0 +1,481 @@ +{ + "language": "de", + "defaultRegion": "de-DE", + "testUtterance": "Hallo, mein Name ist {name} und ich bin eine deutsche Stimme.", + "voices": [ + { + "label": "Seraphina", + "name": "Microsoft SeraphinaMultilingual Online (Natural) - German (Germany)", + "language": "de-DE", + "multiLingual": true, + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Amala", + "name": "Microsoft Amala Online (Natural) - German (Germany)", + "language": "de-DE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Katja", + "name": "Microsoft Katja Online (Natural) - German (Germany)", + "language": "de-DE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Florian", + "name": "Microsoft FlorianMultilingual Online (Natural) - German (Germany)", + "language": "de-DE", + "multiLingual": true, + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Conrad", + "name": "Microsoft Conrad Online (Natural) - German (Germany)", + "language": "de-DE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Killian", + "name": "Microsoft Killian Online (Natural) - German (Germany)", + "language": "de-DE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ingrid", + "name": "Microsoft Ingrid Online (Natural) - German (Austria)", + "language": "de-AT", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Jonas", + "name": "Microsoft Jonas Online (Natural) - German (Austria)", + "language": "de-AT", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Leni", + "name": "Microsoft Leni Online (Natural) - German (Switzerland)", + "language": "de-CH", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Jan", + "name": "Microsoft Jan Online (Natural) - German (Switzerland)", + "language": "de-CH", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Petra", + "name": "Petra", + "localizedName": "apple", + "language": "de-DE", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Anna", + "name": "Anna", + "localizedName": "apple", + "language": "de-DE", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Helena", + "name": "Helena", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "de-DE", + "gender": "female", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Markus", + "name": "Markus", + "localizedName": "apple", + "language": "de-DE", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Viktor", + "name": "Viktor", + "localizedName": "apple", + "language": "de-DE", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Yannick", + "name": "Yannick", + "localizedName": "apple", + "language": "de-DE", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Martin", + "name": "Martin", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "de-DE", + "gender": "male", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Google Deutsch", + "name": "Weibliche Google-Stimme (Deutschland)", + "language": "de-DE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Hedda", + "name": "Microsoft Hedda - German (Germany)", + "language": "de-DE", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Katja", + "name": "Microsoft Katja - German (Germany)", + "language": "de-DE", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Stefan", + "name": "Microsoft Stefan - German (Germany)", + "language": "de-DE", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Michael", + "name": "Microsoft Michael - German (Austria)", + "language": "de-AT", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Karsten", + "name": "Microsoft Karsten - German (Switzerland)", + "language": "de-CH", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Weibliche Stimme 1 (Deutschland)", + "name": "Google Deutsch 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google de-de-x-dea-network", + "Chrome OS Deutsch 2", + "Android Speech Recognition and Synthesis from Google de-de-x-dea-local", + "Android Speech Recognition and Synthesis from Google de-DE-language" + ], + "nativeID": [ + "de-de-x-dea-network", + "de-de-x-dea-local" + ], + "language": "de-DE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Weibliche Stimme 2 (Deutschland)", + "name": "Google Deutsch 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google de-de-x-nfh-network", + "Chrome OS Deutsch 1", + "Android Speech Recognition and Synthesis from Google de-de-x-nfh-local" + ], + "nativeID": [ + "de-de-x-nfh-network", + "de-de-x-nfh-local" + ], + "language": "de-DE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Männliche Stimme 1 (Deutschland)", + "name": "Google Deutsch 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google de-de-x-deb-network", + "Chrome OS Deutsch 3", + "Android Speech Recognition and Synthesis from Google de-de-x-deb-local" + ], + "nativeID": [ + "de-de-x-deb-network", + "de-de-x-deb-local" + ], + "language": "de-DE", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Männliche Stimme 2 (Deutschland)", + "name": "Google Deutsch 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google de-de-x-deg-network", + "Chrome OS Deutsch 4", + "Android Speech Recognition and Synthesis from Google de-de-x-deg-local" + ], + "nativeID": [ + "de-de-x-deg-network", + "de-de-x-deg-local" + ], + "language": "de-DE", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/el.json b/json/el.json new file mode 100644 index 0000000..78b89f5 --- /dev/null +++ b/json/el.json @@ -0,0 +1,115 @@ +{ + "language": "el", + "defaultRegion": "el-GR", + "testUtterance": "Γεια σας, με λένε {name} και είμαι ελληνική φωνή.", + "voices": [ + { + "label": "Athina", + "name": "Microsoft Athina Online (Natural) - Greek (Greece)", + "language": "el-GR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Nestoras", + "name": "Microsoft Nestoras Online (Natural) - Greek (Greece)", + "language": "el-GR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Melina", + "name": "Melina", + "localizedName": "apple", + "language": "el-GR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Nikos", + "name": "Nikos", + "localizedName": "apple", + "language": "el-GR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Stefanos", + "name": "Microsoft Stefanos - Greek (Greece)", + "language": "el-GR", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Γυναικεία φωνή", + "name": "Google Ελληνικά (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google el-gr-x-vfz-network", + "Chrome OS Ελληνικά", + "Android Speech Recognition and Synthesis from Google el-gr-x-vfz-local", + "Android Speech Recognition and Synthesis from Google el-GR-language" + ], + "nativeID": [ + "el-gr-x-vfz-network", + "el-gr-x-vfz-local" + ], + "language": "el-GR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/en.json b/json/en.json new file mode 100644 index 0000000..7ca8768 --- /dev/null +++ b/json/en.json @@ -0,0 +1,2082 @@ +{ + "language": "en", + "defaultRegion": "en-US", + "testUtterance": "Hello, my name is {name} and I am an English voice.", + "voices": [ + { + "label": "Emma", + "name": "Microsoft EmmaMultilingual Online (Natural) - English (United States)", + "altNames": [ + "Microsoft Emma Online (Natural) - English (United States)" + ], + "language": "en-US", + "multiLingual": true, + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Microsoft Ava", + "name": "Microsoft AvaMultilingual Online (Natural) - English (United States)", + "altNames": [ + "Microsoft Ava Online (Natural) - English (United States)" + ], + "language": "en-US", + "multiLingual": true, + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Jenny", + "name": "Microsoft Jenny Online (Natural) - English (United States)", + "language": "en-US", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Aria", + "name": "Microsoft Aria Online (Natural) - English (United States)", + "language": "en-US", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Michelle", + "name": "Microsoft Michelle Online (Natural) - English (United States)", + "language": "en-US", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ana", + "name": "Microsoft Ana Online (Natural) - English (United States)", + "language": "en-US", + "gender": "female", + "children": true, + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Andrew", + "name": "Microsoft AndrewMultilingual Online (Natural) - English (United States)", + "altNames": [ + "Microsoft Andrew Online (Natural) - English (United States)" + ], + "language": "en-US", + "multiLingual": true, + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Brian", + "name": "Microsoft BrianMultilingual Online (Natural) - English (United States)", + "altNames": [ + "Microsoft Brian Online (Natural) - English (United States)" + ], + "language": "en-US", + "multiLingual": true, + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Guy", + "name": "Microsoft Guy Online (Natural) - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Eric", + "name": "Microsoft Eric Online (Natural) - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Steffan", + "name": "Microsoft Steffan Online (Natural) - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Christopher", + "name": "Microsoft Christopher Online (Natural) - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Roger", + "name": "Microsoft Roger Online (Natural) - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sonia", + "name": "Microsoft Sonia Online (Natural) - English (United Kingdom)", + "language": "en-GB", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Libby", + "name": "Microsoft Libby Online (Natural) - English (United Kingdom)", + "language": "en-GB", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Maisie", + "name": "Microsoft Maisie Online (Natural) - English (United Kingdom)", + "language": "en-GB", + "gender": "female", + "children": true, + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ryan", + "name": "Microsoft Ryan Online (Natural) - English (United Kingdom)", + "language": "en-GB", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Thomas", + "name": "Microsoft Thomas Online (Natural) - English (United Kingdom)", + "language": "en-GB", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Natasha", + "name": "Microsoft Natasha Online (Natural) - English (Australia)", + "language": "en-AU", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Hayley", + "name": "Microsoft Hayley Online - English (Australia)", + "language": "en-AU", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "William", + "name": "Microsoft William Online (Natural) - English (Australia)", + "altNames": [ + "Microsoft WilliamMultilingual Online (Natural) - English (Australia)" + ], + "language": "en-AU", + "multiLingual": true, + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Clara", + "name": "Microsoft Clara Online (Natural) - English (Canada)", + "language": "en-CA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Heather", + "name": "Microsoft Heather Online - English (Canada)", + "language": "en-CA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Liam", + "name": "Microsoft Liam Online (Natural) - English (Canada)", + "language": "en-CA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Neerja", + "name": "Microsoft Neerja Online (Natural) - English (India)", + "altNames": [ + "Microsoft Neerja Online (Natural) - English (India) (Preview)" + ], + "language": "en-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Prabhat", + "name": "Microsoft Prabhat Online (Natural) - English (India)", + "language": "en-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Emily", + "name": "Microsoft Emily Online (Natural) - English (Ireland)", + "language": "en-IE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Connor", + "name": "Microsoft Connor Online (Natural) - English (Ireland)", + "language": "en-IE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Leah", + "name": "Microsoft Leah Online (Natural) - English (South Africa)", + "language": "en-ZA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Luke", + "name": "Microsoft Luke Online (Natural) - English (South Africa)", + "language": "en-ZA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yan", + "name": "Microsoft Yan Online (Natural) - English (Hong Kong SAR)", + "language": "en-HK", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sam", + "name": "Microsoft Sam Online (Natural) - English (Hongkong)", + "language": "en-HK", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Asilia", + "name": "Microsoft Asilia Online (Natural) - English (Kenya)", + "language": "en-KE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Chilemba", + "name": "Microsoft Chilemba Online (Natural) - English (Kenya)", + "language": "en-KE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Molly", + "name": "Microsoft Molly Online (Natural) - English (New Zealand)", + "language": "en-NZ", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mitchell", + "name": "Microsoft Mitchell Online (Natural) - English (New Zealand)", + "language": "en-NZ", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ezinne", + "name": "Microsoft Ezinne Online (Natural) - English (Nigeria)", + "language": "en-NG", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Abeo", + "name": "Microsoft Abeo Online (Natural) - English (Nigeria)", + "language": "en-NG", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Rosa", + "name": "Microsoft Rosa Online (Natural) - English (Philippines)", + "language": "en-PH", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "James", + "name": "Microsoft James Online (Natural) - English (Philippines)", + "language": "en-PH", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Luna", + "name": "Microsoft Luna Online (Natural) - English (Singapore)", + "language": "en-SG", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Wayne", + "name": "Microsoft Wayne Online (Natural) - English (Singapore)", + "language": "en-SG", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Imani", + "name": "Microsoft Imani Online (Natural) - English (Tanzania)", + "language": "en-TZ", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Elimu", + "name": "Microsoft Elimu Online (Natural) - English (Tanzania)", + "language": "en-TZ", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Apple Ava", + "name": "Ava", + "localizedName": "apple", + "language": "en-US", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Zoe", + "name": "Zoe", + "localizedName": "apple", + "language": "en-US", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Allison", + "name": "Allison", + "localizedName": "apple", + "language": "en-US", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Nicky", + "name": "Nicky", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS. Unlike other Siri voices, a higher quality version can be installed and used.", + "language": "en-US", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Samantha", + "name": "Samantha", + "localizedName": "apple", + "language": "en-US", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Joelle", + "name": "Joelle", + "localizedName": "apple", + "language": "en-US", + "gender": "female", + "children": true, + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Evan", + "name": "Evan", + "localizedName": "apple", + "language": "en-US", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Nathan", + "name": "Nathan", + "localizedName": "apple", + "language": "en-US", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Tom", + "name": "Tom", + "localizedName": "apple", + "language": "en-US", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Alex", + "name": "Alex", + "localizedName": "apple", + "language": "en-US", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Aaron", + "name": "Aaron", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "en-US", + "gender": "male", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Kate", + "name": "Kate", + "localizedName": "apple", + "language": "en-GB", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Stephanie", + "name": "Stephanie", + "localizedName": "apple", + "language": "en-GB", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Serena", + "name": "Serena", + "localizedName": "apple", + "language": "en-GB", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Martha", + "name": "Martha", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "en-GB", + "gender": "female", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Jamie", + "name": "Jamie", + "localizedName": "apple", + "language": "en-GB", + "gender": "male", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Oliver", + "name": "Oliver", + "localizedName": "apple", + "language": "en-GB", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Daniel", + "name": "Daniel", + "localizedName": "apple", + "language": "en-GB", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Arthur", + "name": "Arthur", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "en-GB", + "gender": "male", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Matilda", + "name": "Matilda", + "localizedName": "apple", + "language": "en-AU", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Karen", + "name": "Karen", + "localizedName": "apple", + "language": "en-AU", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Catherine", + "name": "Catherine", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "en-AU", + "gender": "female", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Lee", + "name": "Lee", + "localizedName": "apple", + "language": "en-AU", + "gender": "male", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Gordon", + "name": "Gordon", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "en-AU", + "gender": "male", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Isha", + "name": "Isha", + "localizedName": "apple", + "language": "en-IN", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Sangeeta", + "name": "Sangeeta", + "localizedName": "apple", + "language": "en-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Rishi", + "name": "Rishi", + "localizedName": "apple", + "language": "en-IN", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Moira", + "name": "Moira", + "localizedName": "apple", + "language": "en-IE", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Tessa", + "name": "Tessa", + "localizedName": "apple", + "language": "en-ZA", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Fiona", + "name": "Fiona", + "localizedName": "apple", + "language": "en-GB-u-sd-gbsct", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Female Google voice (US)", + "name": "Google US English", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "en-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Female Google voice (UK)", + "name": "Google UK English Female", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "en-GB", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Male Google voice (UK)", + "name": "Google UK English Male", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "en-GB", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Zira", + "name": "Microsoft Zira - English (United States)", + "language": "en-US", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "David", + "name": "Microsoft David - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Mark", + "name": "Microsoft Mark - English (United States)", + "language": "en-US", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Hazel", + "name": "Microsoft Hazel - English (Great Britain)", + "language": "en-GB", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Susan", + "name": "Microsoft Susan - English (Great Britain)", + "language": "en-GB", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "George", + "name": "Microsoft George - English (Great Britain)", + "language": "en-GB", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Catherine", + "name": "Microsoft Catherine - English (Austalia)", + "language": "en-AU", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "James", + "name": "Microsoft Richard - English (Australia)", + "language": "en-AU", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Linda", + "name": "Microsoft Linda - English (Canada)", + "language": "en-CA", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Richard", + "name": "Microsoft Richard - English (Canada)", + "language": "en-CA", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Heera", + "name": "Microsoft Heera - English (India)", + "language": "en-IN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Ravi", + "name": "Microsoft Ravi - English (India)", + "language": "en-IN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Sean", + "name": "Microsoft Sean - English (Ireland)", + "language": "en-IE", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Female voice 1 (US)", + "name": "Google US English 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-tpc-network", + "Chrome OS US English 5", + "Android Speech Recognition and Synthesis from Google en-us-x-tpc-local", + "Android Speech Recognition and Synthesis from Google en-US-language" + ], + "nativeID": [ + "en-us-x-tpc-network", + "en-us-x-tpc-local" + ], + "language": "en-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 2 (US)", + "name": "Google US English 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-iob-network", + "Chrome OS US English 1", + "Android Speech Recognition and Synthesis from Google en-us-x-iob-local" + ], + "nativeID": [ + "en-us-x-iob-network", + "en-us-x-iob-local" + ], + "language": "en-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 3 (US)", + "name": "Google US English 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-iog-network", + "Chrome OS US English 2", + "Android Speech Recognition and Synthesis from Google en-us-x-iog-local" + ], + "nativeID": [ + "en-us-x-iog-network", + "en-us-x-iog-local" + ], + "language": "en-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 4 (US)", + "name": "Google US English 7 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-tpf-network", + "Chrome OS US English 7", + "Android Speech Recognition and Synthesis from Google en-us-x-tpf-local" + ], + "nativeID": [ + "en-us-x-tpf-network", + "en-us-x-tpf-local" + ], + "language": "en-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 5 (US)", + "name": "Android Speech Recognition and Synthesis from Google en-us-x-sfg-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-sfg-local" + ], + "nativeID": [ + "en-us-x-sfg-network", + "en-us-x-sfg-local" + ], + "language": "en-US", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 6 (US)", + "name": "Chrome OS US English 8", + "language": "en-US", + "gender": "female", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 1 (US)", + "name": "Google US English 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-iom-network", + "Chrome OS US English 4", + "Android Speech Recognition and Synthesis from Google en-us-x-iom-local" + ], + "nativeID": [ + "en-us-x-iom-network", + "en-us-x-iom-local" + ], + "language": "en-US", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 2 (US)", + "name": "Google US English 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-iol-network", + "Chrome OS US English 3", + "Android Speech Recognition and Synthesis from Google en-us-x-iol-local" + ], + "nativeID": [ + "en-us-x-iol-network", + "en-us-x-iol-local" + ], + "language": "en-US", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 3 (US)", + "name": "Google US English 6 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-us-x-tpd-network", + "Chrome OS US English 6", + "Android Speech Recognition and Synthesis from Google en-us-x-tpd-local" + ], + "nativeID": [ + "en-us-x-tpd-network", + "en-us-x-tpd-local" + ], + "language": "en-US", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 1 (UK)", + "name": "Google UK English 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-gb-x-gba-network", + "Chrome OS UK English 2", + "Android Speech Recognition and Synthesis from Google en-gb-x-gba-local", + "Android Speech Recognition and Synthesis from Google en-GB-language" + ], + "nativeID": [ + "en-gb-x-gba-network", + "en-gb-x-gba-local" + ], + "language": "en-GB", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 2 (UK)", + "name": "Google UK English 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-gb-x-gbc-network", + "Chrome OS UK English 4", + "Android Speech Recognition and Synthesis from Google en-gb-x-gbc-local" + ], + "nativeID": [ + "en-gb-x-gbc-network", + "en-gb-x-gbc-local" + ], + "language": "en-GB", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 3 (UK)", + "name": "Google UK English 6 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-gb-x-gbg-network", + "Chrome OS UK English 6", + "Android Speech Recognition and Synthesis from Google en-gb-x-gbg-local" + ], + "nativeID": [ + "en-gb-x-gbg-network", + "en-gb-x-gbg-local" + ], + "language": "en-GB", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 4 (UK)", + "name": "Chrome OS UK English 7", + "language": "en-GB", + "gender": "female", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 1 (UK)", + "name": "Google UK English 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-gb-x-rjs-network", + "Chrome OS UK English 1", + "Android Speech Recognition and Synthesis from Google en-gb-x-rjs-local" + ], + "nativeID": [ + "en-gb-x-rjs-network", + "en-gb-x-rjs-local" + ], + "language": "en-GB", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 2 (UK)", + "name": "Google UK English 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-gb-x-gbb-network", + "Chrome OS UK English 3", + "Android Speech Recognition and Synthesis from Google en-gb-x-gbb-local" + ], + "nativeID": [ + "en-gb-x-gbb-network", + "en-gb-x-gbb-local" + ], + "language": "en-GB", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 3 (UK)", + "name": "Google UK English 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-gb-x-gbd-network", + "Chrome OS UK English 5", + "Android Speech Recognition and Synthesis from Google en-gb-x-gbd-local" + ], + "nativeID": [ + "en-gb-x-gbd-network", + "en-gb-x-gbd-local" + ], + "language": "en-GB", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 1 (Australia)", + "name": "Google Australian English 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-au-x-aua-network", + "Chrome OS Australian English 1", + "Android Speech Recognition and Synthesis from Google en-au-x-aua-local", + "Android Speech Recognition and Synthesis from Google en-AU-language" + ], + "nativeID": [ + "en-au-x-aua-network", + "en-au-x-aua-local" + ], + "language": "en-AU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 2 (Australia)", + "name": "Google Australian English 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-au-x-auc-network", + "Chrome OS Australian English 3", + "Android Speech Recognition and Synthesis from Google en-au-x-auc-local" + ], + "nativeID": [ + "en-au-x-auc-network", + "en-au-x-auc-local" + ], + "language": "en-AU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 1 (Australia)", + "name": "Google Australian English 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-au-x-aub-network", + "Chrome OS Australian English 2", + "Android Speech Recognition and Synthesis from Google en-au-x-aub-local" + ], + "nativeID": [ + "en-au-x-aub-network", + "en-au-x-aub-local" + ], + "language": "en-AU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 2 (Australia)", + "name": "Google Australian English 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-au-x-aud-network", + "Chrome OS Australian English 4", + "Android Speech Recognition and Synthesis from Google en-au-x-aud-local" + ], + "nativeID": [ + "en-au-x-aud-network", + "en-au-x-aud-local" + ], + "language": "en-AU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 3 (Australia)", + "name": "Chrome OS Australian English 5", + "language": "en-AU", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 1 (India)", + "name": "Android Speech Recognition and Synthesis from Google en-in-x-ena-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-in-x-ena-local", + "Android Speech Recognition and Synthesis from Google en-IN-language" + ], + "nativeID": [ + "en-in-x-ena-network", + "en-in-x-ena-local" + ], + "language": "en-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Female voice 2 (India)", + "name": "Android Speech Recognition and Synthesis from Google en-in-x-enc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-in-x-enc-local" + ], + "nativeID": [ + "en-in-x-enc-network", + "en-in-x-enc-local" + ], + "language": "en-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 1 (India)", + "name": "Android Speech Recognition and Synthesis from Google en-in-x-end-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-in-x-end-local" + ], + "nativeID": [ + "en-in-x-end-network", + "en-in-x-end-local" + ], + "language": "en-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Male voice 2 (India)", + "name": "Android Speech Recognition and Synthesis from Google en-in-x-ene-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google en-in-x-ene-local" + ], + "nativeID": [ + "en-in-x-ene-network", + "en-in-x-ene-local" + ], + "language": "en-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} diff --git a/json/es.json b/json/es.json new file mode 100644 index 0000000..71bd485 --- /dev/null +++ b/json/es.json @@ -0,0 +1,1233 @@ +{ + "language": "es", + "defaultRegion": "es-ES", + "testUtterance": "Hola, mi nombre es {name} y soy una voz española.", + "voices": [ + { + "label": "Elvira", + "name": "Microsoft Elvira Online (Natural) - Spanish (Spain)", + "language": "es-ES", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Alvaro", + "name": "Microsoft Alvaro Online (Natural) - Spanish (Spain)", + "language": "es-ES", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Dalia", + "name": "Microsoft Dalia Online (Natural) - Spanish (Mexico)", + "language": "es-MX", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Microsoft Jorge", + "name": "Microsoft Jorge Online (Natural) - Spanish (Mexico)", + "language": "es-MX", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Elena", + "name": "Microsoft Elena Online (Natural) - Spanish (Argentina)", + "language": "es-AR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Tomas", + "name": "Microsoft Tomas Online (Natural) - Spanish (Argentina)", + "language": "es-AR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sofia", + "name": "Microsoft Sofia Online (Natural) - Spanish (Bolivia)", + "language": "es-BO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Marcelo", + "name": "Microsoft Marcelo Online (Natural) - Spanish (Bolivia)", + "language": "es-BO", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Catalina", + "name": "Microsoft Catalina Online (Natural) - Spanish (Chile)", + "language": "es-CL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Lorenzo", + "name": "Microsoft Lorenzo Online (Natural) - Spanish (Chile)", + "language": "es-CL", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ximena", + "name": "Microsoft Ximena Online (Natural) - Spanish (Colombia)", + "language": "es-CO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Salome", + "name": "Microsoft Salome Online (Natural) - Spanish (Colombia)", + "language": "es-CO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Gonzalo", + "name": "Microsoft Gonzalo Online (Natural) - Spanish (Colombia)", + "language": "es-CO", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Maria", + "name": "Microsoft Maria Online (Natural) - Spanish (Costa Rica)", + "language": "es-CR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Juan", + "name": "Microsoft Juan Online (Natural) - Spanish (Costa Rica)", + "language": "es-CR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Belkys", + "name": "Microsoft Belkys Online (Natural) - Spanish (Cuba)", + "language": "es-CU", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Manuel", + "name": "Microsoft Manuel Online (Natural) - Spanish (Cuba)", + "language": "es-CU", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Andrea", + "name": "Microsoft Andrea Online (Natural) - Spanish (Ecuador)", + "language": "es-EC", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Luis", + "name": "Microsoft Luis Online (Natural) - Spanish (Ecuador)", + "language": "es-EC", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Lorena", + "name": "Microsoft Lorena Online (Natural) - Spanish (El Salvador)", + "language": "es-SV", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Rodrigo", + "name": "Microsoft Rodrigo Online (Natural) - Spanish (El Salvador)", + "language": "es-SV", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Paloma", + "name": "Microsoft Paloma Online (Natural) - Spanish (United States)", + "language": "es-US", + "otherLanguages": [ + "en" + ], + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Alonso", + "name": "Microsoft Alonso Online (Natural) - Spanish (United States)", + "language": "es-US", + "otherLanguages": [ + "en" + ], + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Marta", + "name": "Microsoft Marta Online (Natural) - Spanish (Guatemala)", + "language": "es-GT", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Andres", + "name": "Microsoft Andres Online (Natural) - Spanish (Guatemala)", + "language": "es-GT", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Teresa", + "name": "Microsoft Teresa Online (Natural) - Spanish (Equatorial Guinea)", + "language": "es-GQ", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Javier", + "name": "Microsoft Javier Online (Natural) - Spanish (Equatorial Guinea)", + "language": "es-GQ", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Karla", + "name": "Microsoft Karla Online (Natural) - Spanish (Honduras)", + "language": "es-HN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Carlos", + "name": "Microsoft Carlos Online (Natural) - Spanish (Honduras)", + "language": "es-HN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yolanda", + "name": "Microsoft Yolanda Online (Natural) - Spanish (Nicaragua)", + "language": "es-NI", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Federico", + "name": "Microsoft Federico Online (Natural) - Spanish (Nicaragua)", + "language": "es-NI", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Margarita", + "name": "Microsoft Margarita Online (Natural) - Spanish (Panama)", + "language": "es-PA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Roberto", + "name": "Microsoft Roberto Online (Natural) - Spanish (Panama)", + "language": "es-PA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Tania", + "name": "Microsoft Tania Online (Natural) - Spanish (Paraguay)", + "language": "es-PY", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mario", + "name": "Microsoft Mario Online (Natural) - Spanish (Paraguay)", + "language": "es-PY", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Camila", + "name": "Microsoft Camila Online (Natural) - Spanish (Peru)", + "language": "es-PE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Alex", + "name": "Microsoft Alex Online (Natural) - Spanish (Peru)", + "language": "es-PE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Karina", + "name": "Microsoft Karina Online (Natural) - Spanish (Puerto Rico)", + "language": "es-PR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Victor", + "name": "Microsoft Victor Online (Natural) - Spanish (Puerto Rico)", + "language": "es-PR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ramona", + "name": "Microsoft Ramona Online (Natural) - Spanish (Dominican Republic)", + "language": "es-DO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Emilio", + "name": "Microsoft Emilio Online (Natural) - Spanish (Dominican Republic)", + "language": "es-DO", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Valentina", + "name": "Microsoft Valentina Online (Natural) - Spanish (Uruguay)", + "language": "es-UY", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mateo", + "name": "Microsoft Mateo Online (Natural) - Spanish (Uruguay)", + "language": "es-UY", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Paola", + "name": "Microsoft Paola Online (Natural) - Spanish (Venezuela)", + "language": "es-VE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sebastian", + "name": "Microsoft Sebastian Online (Natural) - Spanish (Venezuela)", + "language": "es-VE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Marisol", + "name": "Marisol", + "localizedName": "apple", + "language": "es-ES", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Mónica", + "name": "Mónica", + "localizedName": "apple", + "language": "es-ES", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Apple Jorge", + "name": "Jorge", + "localizedName": "apple", + "language": "es-ES", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Angelica", + "name": "Angelica", + "localizedName": "apple", + "language": "es-MX", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Paulina", + "name": "Paulina", + "localizedName": "apple", + "language": "es-MX", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Juan", + "name": "Juan", + "localizedName": "apple", + "language": "es-MX", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Isabela", + "name": "Isabela", + "localizedName": "apple", + "language": "es-AR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Diego", + "name": "Diego", + "localizedName": "apple", + "language": "es-AR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Francisca", + "name": "Francisca", + "localizedName": "apple", + "language": "es-CL", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Soledad", + "name": "Soledad", + "localizedName": "apple", + "language": "es-CO", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Jimena", + "name": "Jimena", + "localizedName": "apple", + "language": "es-CO", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Carlos", + "name": "Carlos", + "localizedName": "apple", + "language": "es-CO", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Voz Google masculina (España)", + "name": "Google español", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "es-ES", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Voz Google femenina (Estados Unidos)", + "name": "Google español de Estados Unidos", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "es-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Helena", + "name": "Microsoft Helena - Spanish (Spain)", + "language": "es-ES", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Laura", + "name": "Microsoft Laura - Spanish (Spain)", + "language": "es-ES", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Pablo", + "name": "Microsoft Pablo - Spanish (Spain)", + "language": "es-ES", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Sabina", + "name": "Microsoft Sabina - Spanish (Mexico)", + "language": "es-MX", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Raul", + "name": "Microsoft Raul - Spanish (Mexico)", + "language": "es-MX", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Voz femenina 1 (España)", + "name": "Google español 4 (Natural)", + "altNames": [ + "Chrome OS español 4", + "Android Speech Recognition and Synthesis from Google es-es-x-eee-local", + "Android Speech Recognition and Synthesis from Google es-ES-language" + ], + "nativeID": [ + "es-es-x-eee-local" + ], + "language": "es-ES", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz femenina 2 (España)", + "name": "Google español 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-es-x-eea-network", + "Chrome OS español 1", + "Android Speech Recognition and Synthesis from Google es-es-x-eea-local" + ], + "nativeID": [ + "es-es-x-eea-network", + "es-es-x-eea-local" + ], + "language": "es-ES", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz femenina 3 (España)", + "name": "Google español 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-es-x-eec-network", + "Chrome OS español 2", + "Android Speech Recognition and Synthesis from Google es-es-x-eec-local" + ], + "nativeID": [ + "es-es-x-eec-network", + "es-es-x-eec-local" + ], + "language": "es-ES", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina 1 (España)", + "name": "Google español 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-es-x-eed-network", + "Chrome OS español 3", + "Android Speech Recognition and Synthesis from Google es-es-x-eed-local" + ], + "nativeID": [ + "es-es-x-eed-network", + "es-es-x-eed-local" + ], + "language": "es-ES", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina 2 (España)", + "name": "Google español 5 (Natural)", + "altNames": [ + "Chrome OS español 5", + "Android Speech Recognition and Synthesis from Google es-es-x-eef-local" + ], + "nativeID": [ + "es-es-x-eef-local" + ], + "language": "es-ES", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz femenina 1 (Estados Unidos)", + "name": "Google español de Estados Unidos 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-us-x-esc-network", + "Chrome OS español de Estados Unidos", + "Android Speech Recognition and Synthesis from Google es-us-x-esc-local", + "Android Speech Recognition and Synthesis from Google es-US-language" + ], + "nativeID": [ + "es-us-x-esc-network", + "es-us-x-esc-local" + ], + "language": "es-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz femenina 2 (Estados Unidos)", + "name": "Google español de Estados Unidos 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-us-x-sfb-network", + "Android Speech Recognition and Synthesis from Google es-us-x-sfb-local" + ], + "nativeID": [ + "es-us-x-sfb-network", + "es-us-x-sfb-local" + ], + "language": "es-US", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina 1 (Estados Unidos)", + "name": "Google español de Estados Unidos 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-us-x-esd-network", + "Android Speech Recognition and Synthesis from Google es-us-x-esd-local" + ], + "nativeID": [ + "es-us-x-esd-network", + "es-us-x-esd-local" + ], + "language": "es-US", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina 2 (Estados Unidos)", + "name": "Google español de Estados Unidos 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google es-us-x-esf-network", + "Android Speech Recognition and Synthesis from Google es-us-x-esf-local" + ], + "nativeID": [ + "es-us-x-esf-network", + "es-us-x-esf-local" + ], + "language": "es-US", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/eu.json b/json/eu.json new file mode 100644 index 0000000..c51d67c --- /dev/null +++ b/json/eu.json @@ -0,0 +1,25 @@ +{ + "language": "eu", + "defaultRegion": "eu-ES", + "testUtterance": "Kaixo, nire izena {name} da eta euskal ahotsa naiz.", + "voices": [ + { + "label": "Miren", + "name": "Miren", + "localizedName": "apple", + "language": "eu-ES", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + } + ] +} \ No newline at end of file diff --git a/json/fa.json b/json/fa.json new file mode 100644 index 0000000..c088bb9 --- /dev/null +++ b/json/fa.json @@ -0,0 +1,56 @@ +{ + "language": "fa", + "defaultRegion": "fa-IR", + "testUtterance": "سلام اسم من {name} و صدای فارسی هستم", + "voices": [ + { + "label": "Dilara", + "name": "Microsoft Dilara Online (Natural) - Persian (Iran)", + "language": "fa-IR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Farid", + "name": "Microsoft Farid Online (Natural) - Persian (Iran)", + "language": "fa-IR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Dariush", + "name": "Dariush", + "localizedName": "apple", + "language": "fa-IR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/fi.json b/json/fi.json new file mode 100644 index 0000000..2fcca35 --- /dev/null +++ b/json/fi.json @@ -0,0 +1,115 @@ +{ + "language": "fi", + "defaultRegion": "fi-FI", + "testUtterance": "Hei, nimeni on {name} ja olen suomalainen ääni.", + "voices": [ + { + "label": "Noora", + "name": "Microsoft Noora Online (Natural) - Finnish (Finland)", + "language": "fi-FI", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Harri", + "name": "Microsoft Harri Online (Natural) - Finnish (Finland)", + "language": "fi-FI", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Satu", + "name": "Satu", + "localizedName": "apple", + "language": "fi-FI", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Onni", + "name": "Onni", + "localizedName": "apple", + "language": "fi-FI", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Heidi", + "name": "Microsoft Heidi - Finnish (Finland)", + "language": "fi-FI", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Suomalainen naisääni", + "name": "Google Suomi (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fi-fi-x-afi-network", + "Chrome OS Suomi", + "Android Speech Recognition and Synthesis from Google fi-fi-x-afi-local", + "Android Speech Recognition and Synthesis from Google fi-FI-language" + ], + "nativeID": [ + "fi-fi-x-afi-network", + "fi-fi-x-afi-local" + ], + "language": "fi-FI", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/filters/novelty.json b/json/filters/novelty.json new file mode 100644 index 0000000..b66163c --- /dev/null +++ b/json/filters/novelty.json @@ -0,0 +1,242 @@ +{ + "voices": [ + { + "name": "Albert", + "nativeID": [ + "com.apple.speech.synthesis.voice.Albert" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Bad News", + "nativeID": [ + "com.apple.speech.synthesis.voice.BadNews" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Mauvaises nouvelles", + "Malas noticias", + "Brutte notizie" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Bahh", + "nativeID": [ + "com.apple.speech.synthesis.voice.Bahh" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Bells", + "nativeID": [ + "com.apple.speech.synthesis.voice.Bells" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Cloches", + "Campanas", + "Campane" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Boing", + "nativeID": [ + "com.apple.speech.synthesis.voice.Boing" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Bubbles", + "nativeID": [ + "com.apple.speech.synthesis.voice.Bubbles" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Bulles", + "Burbujas", + "Bollicine" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Cellos", + "nativeID": [ + "com.apple.speech.synthesis.voice.Cellos" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Violoncelles", + "Violonchelos", + "Violoncelli" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Good News", + "nativeID": [ + "com.apple.speech.synthesis.voice.GoodNews" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Bonnes nouvelles", + "Buenas noticias", + "Buone notizie" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Jester", + "nativeID": [ + "com.apple.speech.synthesis.voice.Hysterical" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Bouffon", + "Bufón", + "Giullare" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Organ", + "nativeID": [ + "com.apple.speech.synthesis.voice.Organ" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Orgue", + "Órgano", + "Organo" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Superstar", + "nativeID": [ + "com.apple.speech.synthesis.voice.Princess" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Superestrella" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Trinoids", + "nativeID": [ + "com.apple.speech.synthesis.voice.Trinoids" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Whisper", + "nativeID": [ + "com.apple.speech.synthesis.voice.Whisper" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "altNames": [ + "Murmure", + "Susurro", + "Sussurro" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Wobble", + "nativeID": [ + "com.apple.speech.synthesis.voice.Deranged" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Zarvox", + "nativeID": [ + "com.apple.speech.synthesis.voice.Zarvox" + ], + "note": "This novelty voice is part of a pack preloaded by Apple.", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/filters/veryLowQuality.json b/json/filters/veryLowQuality.json new file mode 100644 index 0000000..4a0c3af --- /dev/null +++ b/json/filters/veryLowQuality.json @@ -0,0 +1,629 @@ +{ + "voices": [ + { + "name": "Eddy", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Flo", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Grandma", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Grandpa", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Jacques", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Reed", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Rocko", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Sandy", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Shelley", + "localizedName": "apple", + "note": "Eloquence voices are preloaded by default on Apple devices.", + "language": "en-US", + "otherLanguages": [ + "en-GB", + "de-DE", + "fr-FR", + "fr-CA", + "es-ES", + "es-MX", + "fi-FI", + "it-IT", + "ja-JP", + "ko-KR", + "pt-BR", + "zh-CN", + "zh-HK" + ], + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Fred", + "language": "en-US", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Junior", + "language": "en-US", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Kathy", + "language": "en-US", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "Ralph", + "language": "en-US", + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Arabic", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ar", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Bulgarian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "bg", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Bengali", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "bn", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Catalan", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ca", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Chinese (Mandarin, latin as English)", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "cmn", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Czech", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "cs", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Danish", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "da", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak German", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "de", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Greek", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "el", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Spanish (Spain)", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "es", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Estonian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "et", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Finnish", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "fi", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Gujarati", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "gu", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Croatian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "hr", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Hungarian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "hu", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Indonesian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "id", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Italian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "it", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Kannada", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "kn", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Korean", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ko", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Lithuanian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "lt", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Latvian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "lv", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Malayalm", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ml", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Marathi", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "mr", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Malay", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ms", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Norwegian Bokmål", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "nb", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Polish", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "pl", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Portuguese (Brazil)", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "pt-br", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Romanian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ro", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Russian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ru", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Slovak", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "sk", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Slovenian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "sl", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Serbian", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "sv", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Swedish", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "sv", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Swahili", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "sw", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Tamil", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "ta", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Telugu", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "te", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Turkish", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "tr", + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "name": "eSpeak Vietnamese (Northern)", + "note": "eSpeak voices are preloaded by default on Chrome OS.", + "language": "vi", + "os": [ + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/fr.json b/json/fr.json new file mode 100644 index 0000000..763811a --- /dev/null +++ b/json/fr.json @@ -0,0 +1,684 @@ +{ + "language": "fr", + "defaultRegion": "fr-FR", + "testUtterance": "Bonjour, mon nom est {name} et je suis une voix française.", + "voices": [ + { + "label": "Vivienne", + "name": "Microsoft VivienneMultilingual Online (Natural) - French (France)", + "language": "fr-FR", + "multiLingual": true, + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Denise", + "name": "Microsoft Denise Online (Natural) - French (France)", + "language": "fr-FR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Charline", + "name": "Microsoft Charline Online (Natural) - French (Belgium)", + "language": "fr-BE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ariane", + "name": "Microsoft Ariane Online (Natural) - French (Switzerland)", + "language": "fr-CH", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Eloise", + "name": "Microsoft Eloise Online (Natural) - French (France)", + "language": "fr-FR", + "gender": "female", + "children": true, + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Remy", + "name": "Microsoft RemyMultilingual Online (Natural) - French (France)", + "language": "fr-FR", + "multiLingual": true, + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Henri", + "name": "Microsoft Henri Online (Natural) - French (France)", + "language": "fr-FR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Gerard", + "name": "Microsoft Gerard Online (Natural) - French (Belgium)", + "language": "fr-BE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Fabrice", + "name": "Microsoft Fabrice Online (Natural) - French (Switzerland)", + "language": "fr-CH", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sylvie", + "name": "Microsoft Sylvie Online (Natural) - French (Canada)", + "language": "fr-CA", + "otherLanguages": [ + "en" + ], + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Antoine", + "name": "Microsoft Antoine Online (Natural) - French (Canada)", + "language": "fr-CA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Jean", + "name": "Microsoft Jean Online (Natural) - French (Canada)", + "language": "fr-CA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Thierry", + "name": "Microsoft Thierry Online (Natural) - French (Canada)", + "language": "fr-CA", + "otherLanguages": [ + "en" + ], + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Audrey", + "name": "Audrey", + "localizedName": "apple", + "language": "fr-FR", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Aurélie", + "name": "Aurélie", + "localizedName": "apple", + "language": "fr-FR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 0.9, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Marie", + "name": "Marie", + "localizedName": "apple", + "note": "This is a compact version of a preloaded Siri voice on macOS.", + "language": "fr-FR", + "gender": "female", + "quality": [ + "low" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Thomas", + "name": "Thomas", + "localizedName": "apple", + "language": "fr-FR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Aude", + "name": "Aude", + "localizedName": "apple", + "language": "fr-BE", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Chantal", + "name": "Chantal", + "localizedName": "apple", + "language": "fr-CA", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Amélie", + "name": "Amélie", + "localizedName": "apple", + "language": "fr-CA", + "gender": "female", + "quality": [ + "low", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Nicolas", + "name": "Nicolas", + "localizedName": "apple", + "language": "fr-CA", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Voix Google féminine (France)", + "name": "Google français", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "fr-FR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Julie", + "name": "Microsoft Julie - French (France)", + "language": "fr-FR", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Hortence", + "name": "Microsoft Hortence - French (France)", + "language": "fr-FR", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Paul", + "name": "Microsoft Paul - French (France)", + "language": "fr-FR", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Caroline", + "name": "Microsoft Caroline - French (Canada)", + "language": "fr-CA", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Claude", + "name": "Microsoft Claude - French (Canada)", + "language": "fr-CA", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Guillaume", + "name": "Microsoft Claude - French (Switzerland)", + "language": "fr-CH", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Voix féminine 1 (France)", + "name": "Google français 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-fr-x-frc-network", + "Chrome OS français 4", + "Android Speech Recognition and Synthesis from Google fr-fr-x-frc-local", + "Android Speech Recognition and Synthesis from Google fr-FR-language" + ], + "nativeID": [ + "fr-fr-x-frc-network", + "fr-fr-x-frc-local" + ], + "language": "fr-FR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix féminine 2 (France)", + "name": "Google français 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-fr-x-fra-network", + "Chrome OS français 2", + "Android Speech Recognition and Synthesis from Google fr-fr-x-fra-local" + ], + "nativeID": [ + "fr-fr-x-fra-network", + "fr-fr-x-fra-local" + ], + "language": "fr-FR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix féminine 3 (France)", + "name": "Google français 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-fr-x-vlf-network", + "Chrome OS français 1", + "Android Speech Recognition and Synthesis from Google fr-fr-x-vlf-local" + ], + "nativeID": [ + "fr-fr-x-vlf-network", + "fr-fr-x-vlf-local" + ], + "language": "fr-FR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix masculine 1 (France)", + "name": "Google français 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-fr-x-frd-network", + "Chrome OS français 5", + "Android Speech Recognition and Synthesis from Google fr-fr-x-frd-local" + ], + "nativeID": [ + "fr-fr-x-frd-network", + "fr-fr-x-frd-local" + ], + "language": "fr-FR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix masculine 2 (France)", + "name": "Google français 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-fr-x-frb-network", + "Chrome OS français 3", + "Android Speech Recognition and Synthesis from Google fr-fr-x-frb-local" + ], + "nativeID": [ + "fr-fr-x-frb-network", + "fr-fr-x-frb-local" + ], + "language": "fr-FR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix féminine 1 (Canada)", + "name": "Android Speech Recognition and Synthesis from Google fr-ca-x-caa-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-ca-x-caa-local", + "Android Speech Recognition and Synthesis from Google fr-CA-language" + ], + "nativeID": [ + "fr-ca-x-caa-network", + "fr-ca-x-caa-local" + ], + "language": "fr-CA", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix féminine 2 (Canada)", + "name": "Android Speech Recognition and Synthesis from Google fr-ca-x-cac-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-ca-x-cac-local" + ], + "nativeID": [ + "fr-ca-x-cac-network", + "fr-ca-x-cac-local" + ], + "language": "fr-CA", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix masculine 1 (Canada)", + "name": "Android Speech Recognition and Synthesis from Google fr-ca-x-cab-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-ca-x-cab-local" + ], + "nativeID": [ + "fr-ca-x-cab-network", + "fr-ca-x-cab-local" + ], + "language": "fr-CA", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voix masculine 2 (Canada)", + "name": "Android Speech Recognition and Synthesis from Google fr-ca-x-cad-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google fr-ca-x-cad-local" + ], + "nativeID": [ + "fr-ca-x-cad-network", + "fr-ca-x-cad-local" + ], + "language": "fr-CA", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/gl.json b/json/gl.json new file mode 100644 index 0000000..605d5ff --- /dev/null +++ b/json/gl.json @@ -0,0 +1,55 @@ +{ + "language": "gl", + "defaultRegion": "gl-ES", + "testUtterance": "Ola, chámome {name} e son unha voz galega.", + "voices": [ + { + "label": "Sabela", + "name": "Microsoft Sabela Online (Natural) - Galician", + "language": "gl-ES", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Roi", + "name": "Microsoft Roi Online (Natural) - Galician", + "language": "gl-ES", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Carmela", + "name": "Carmela", + "localizedName": "apple", + "language": "gl-ES", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + } + ] +} \ No newline at end of file diff --git a/json/he.json b/json/he.json new file mode 100644 index 0000000..11b5ff3 --- /dev/null +++ b/json/he.json @@ -0,0 +1,172 @@ +{ + "language": "he", + "defaultRegion": "he-IL", + "testUtterance": "שלום, שמי {name} ואני קול עברי.", + "voices": [ + { + "label": "Hila", + "name": "Microsoft Hila Online (Natural) - Hebrew (Israel)", + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Avri", + "name": "Microsoft Avri Online (Natural) - Hebrew (Israel)", + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Carmit", + "name": "Carmit", + "localizedName": "apple", + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Asaf", + "name": "Microsoft Asaf - Hebrew (Israel)", + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "קול גברי 1", + "name": "Android Speech Recognition and Synthesis from Google he-il-x-heb-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google he-il-x-heb-local", + "Android Speech Recognition and Synthesis from Google he-IL-language" + ], + "nativeID": [ + "he-il-x-heb-network", + "he-il-x-heb-local" + ], + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "קול נשי 1", + "name": "Android Speech Recognition and Synthesis from Google he-il-x-hec-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google he-il-x-hec-local" + ], + "nativeID": [ + "he-il-x-hec-network", + "he-il-x-hec-local" + ], + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "קול גברי 2", + "name": "Android Speech Recognition and Synthesis from Google he-il-x-hed-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google he-il-x-hed-local" + ], + "nativeID": [ + "he-il-x-hed-network", + "he-il-x-hed-local" + ], + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "קול נשי 2", + "name": "Android Speech Recognition and Synthesis from Google he-il-x-hee-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google he-il-x-hee-local" + ], + "nativeID": [ + "he-il-x-hee-network", + "he-il-x-hee-local" + ], + "language": "he-IL", + "altLanguage": "iw-IL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} diff --git a/json/hi.json b/json/hi.json new file mode 100644 index 0000000..6c9e369 --- /dev/null +++ b/json/hi.json @@ -0,0 +1,255 @@ +{ + "language": "hi", + "defaultRegion": "hi-IN", + "testUtterance": "नमस्कार, मेरा नाम {name} है और मैं एक हिंदी आवाज़ हूँ।", + "voices": [ + { + "label": "Swara", + "name": "Microsoft Swara Online (Natural) - Hindi (India)", + "language": "hi-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Madhur", + "name": "Microsoft Madhur Online (Natural) - Hindi (India)", + "language": "hi-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Kiyara", + "name": "Kiyara", + "localizedName": "apple", + "language": "hi-IN", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Lekha", + "name": "Lekha", + "localizedName": "apple", + "language": "hi-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Neel", + "name": "Neel", + "localizedName": "apple", + "language": "hi-IN", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "महिला Google आवाज़", + "name": "Google हिन्दी", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "hi-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Kalpana", + "name": "Microsoft Kalpana - Hindi (India)", + "language": "hi-IN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Hemant", + "name": "Microsoft Hemant - Hindi (India)", + "language": "hi-IN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "महिला आवाज़ 1", + "name": "Google हिन्दी 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hi-in-x-hia-network", + "Chrome OS हिन्दी 2", + "Android Speech Recognition and Synthesis from Google hi-in-x-hia-local", + "Android Speech Recognition and Synthesis from Google hi-IN-language" + ], + "nativeID": [ + "hi-in-x-hia-network", + "hi-in-x-hia-local" + ], + "language": "hi-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "महिला आवाज़ 2", + "name": "Google हिन्दी 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hi-in-x-hic-network", + "Chrome OS हिन्दी 3", + "Android Speech Recognition and Synthesis from Google hi-in-x-hic-local" + ], + "nativeID": [ + "hi-in-x-hic-network", + "hi-in-x-hic-local" + ], + "language": "hi-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "महिला आवाज़ 3", + "name": "Chrome OS हिन्दी 1", + "language": "hi-IN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "पुरुष आवाज 1", + "name": "Google हिन्दी 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hi-in-x-hid-network", + "Chrome OS हिन्दी 4", + "Android Speech Recognition and Synthesis from Google hi-in-x-hid-local" + ], + "nativeID": [ + "hi-in-x-hid-network", + "hi-in-x-hid-local" + ], + "language": "hi-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "पुरुष आवाज 2", + "name": "Google हिन्दी 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hi-in-x-hie-network", + "Chrome OS हिन्दी 5", + "Android Speech Recognition and Synthesis from Google hi-in-x-hie-local" + ], + "nativeID": [ + "hi-in-x-hie-network", + "hi-in-x-hie-local" + ], + "language": "hi-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/hr.json b/json/hr.json new file mode 100644 index 0000000..d4a2091 --- /dev/null +++ b/json/hr.json @@ -0,0 +1,122 @@ +{ + "language": "hr", + "defaultRegion": "hr-HR", + "testUtterance": "Pozdrav, ja sam {name} i hrvatski sam glas.", + "voices": [ + { + "label": "Gabrijela", + "name": "Microsoft Gabrijela Online (Natural) - Croatian (Croatia)", + "language": "hr-HR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Srecko", + "name": "Microsoft Srecko Online (Natural) - Croatian (Croatia)", + "language": "hr-HR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Lana", + "name": "Lana", + "localizedName": "apple", + "altNames": [ + "Lana (poboljšani)", + "Lana (hrvatski (Hrvatska))" + ], + "language": "hr-HR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Matej", + "name": "Microsoft Matej - Croatian (Croatia)", + "language": "hr-HR", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Ženski glas", + "name": "Android Speech Recognition and Synthesis from Google hr-hr-x-hra-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hr-hr-x-hra-local" + ], + "nativeID": [ + "hr-hr-x-hra-network", + "hr-hr-x-hra-local" + ], + "language": "hr-HR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Muški glas", + "name": "Android Speech Recognition and Synthesis from Google hr-hr-x-hrb-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hr-hr-x-hrb-local", + "Android Speech Recognition and Synthesis from Google hr-HR-language" + ], + "nativeID": [ + "hr-hr-x-hrb-network", + "hr-hr-x-hrb-local" + ], + "language": "hr-HR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/hu.json b/json/hu.json new file mode 100644 index 0000000..5ee6b41 --- /dev/null +++ b/json/hu.json @@ -0,0 +1,98 @@ +{ + "language": "hu", + "defaultRegion": "hu-HU", + "testUtterance": "Helló, a nevem {name} és magyar hangú vagyok.", + "voices": [ + { + "label": "Noemi", + "name": "Microsoft Noemi Online (Natural) - Hungarian (Hungary)", + "language": "hu-HU", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Tamas", + "name": "Microsoft Tamas Online (Natural) - Hungarian (Hungary)", + "language": "hu-HU", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Tünde", + "name": "Tünde", + "localizedName": "apple", + "language": "hu-HU", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Szabolcs", + "name": "Microsoft Szabolcs - Hungarian (Hungary)", + "language": "hu-HU", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Női hang", + "name": "Google Magyar (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google hu-hu-x-kfl-network", + "Chrome OS Magyar", + "Android Speech Recognition and Synthesis from Google hu-hu-x-kfl-local", + "Android Speech Recognition and Synthesis from Google hu-HU-language" + ], + "nativeID": [ + "hu-hu-x-kfl-network", + "hu-hu-x-kfl-local" + ], + "language": "hu-HU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/id.json b/json/id.json new file mode 100644 index 0000000..23a49eb --- /dev/null +++ b/json/id.json @@ -0,0 +1,188 @@ +{ + "language": "id", + "defaultRegion": "id-ID", + "testUtterance": "Halo, nama saya {name} dan saya suara Indonesia.", + "voices": [ + { + "label": "Gadis", + "name": "Microsoft Gadis Online (Natural) - Indonesian (Indonesia)", + "language": "id-ID", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ardi", + "name": "Microsoft Ardi Online (Natural) - Indonesian (Indonesia)", + "language": "id-ID", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Damayanti", + "name": "Damayanti", + "localizedName": "apple", + "language": "id-ID", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Suara Google wanita", + "name": "Google Bahasa Indonesia", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "id-ID", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Andika", + "name": "Microsoft Andika - Indonesian (Indonesia)", + "language": "id-ID", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Suara wanita 1", + "name": "Google Bahasa Indonesia 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google id-id-x-idc-network", + "Chrome OS Bahasa Indonesia 1", + "Android Speech Recognition and Synthesis from Google id-id-x-idc-local", + "Android Speech Recognition and Synthesis from Google id-ID-language" + ], + "nativeID": [ + "id-id-x-idc-network", + "id-id-x-idc-local" + ], + "language": "id-ID", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Suara wanita 2", + "name": "Google Bahasa Indonesia 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google id-id-x-idd-network", + "Chrome OS Bahasa Indonesia 2", + "Android Speech Recognition and Synthesis from Google id-id-x-idd-local" + ], + "nativeID": [ + "id-id-x-idd-network", + "id-id-x-idd-local" + ], + "language": "id-ID", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Suara laki-laki 1", + "name": "Google Bahasa Indonesia 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google id-id-x-ide-network", + "Chrome OS Bahasa Indonesia 3", + "Android Speech Recognition and Synthesis from Google id-id-x-ide-local" + ], + "nativeID": [ + "id-id-x-ide-network", + "id-id-x-ide-local" + ], + "language": "id-ID", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Suara laki-laki 2", + "name": "Google Bahasa Indonesia 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google id-id-x-dfz-network", + "Chrome OS Bahasa Indonesia 4", + "Android Speech Recognition and Synthesis from Google id-id-x-dfz-local" + ], + "nativeID": [ + "id-id-x-dfz-network", + "id-id-x-dfz-local" + ], + "language": "id-ID", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/it.json b/json/it.json new file mode 100644 index 0000000..8026afd --- /dev/null +++ b/json/it.json @@ -0,0 +1,307 @@ +{ + "language": "it", + "defaultRegion": "it-IT", + "testUtterance": "Ciao, mi chiamo {name} e sono una voce italiana.", + "voices": [ + { + "label": "Elsa (Alta qualita)", + "name": "Microsoft Elsa Online (Natural) - Italian (Italy)", + "language": "it-IT", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Isabella", + "name": "Microsoft Isabella Online (Natural) - Italian (Italy)", + "language": "it-IT", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Giuseppe", + "name": "Microsoft GiuseppeMultilingual Online (Natural) - Italian (Italy)", + "language": "it-IT", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Diego", + "name": "Microsoft Diego Online (Natural) - Italian (Italy)", + "language": "it-IT", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Federica", + "name": "Federica", + "localizedName": "apple", + "language": "it-IT", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Emma", + "name": "Emma", + "localizedName": "apple", + "language": "it-IT", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Alice", + "name": "Alice", + "localizedName": "apple", + "language": "it-IT", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Paola", + "name": "Paola", + "localizedName": "apple", + "language": "it-IT", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Luca", + "name": "Luca", + "localizedName": "apple", + "language": "it-IT", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Voce Google femminile", + "name": "Google italiano", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "it-IT", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Elsa", + "name": "Microsoft Elsa - Italian (Italy)", + "language": "it-IT", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Cosimo", + "name": "Microsoft Cosimo - Italian (Italy)", + "language": "it-IT", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Voce femminile 1", + "name": "Google italiano 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google it-it-x-itb-network", + "Chrome OS italiano 2", + "Android Speech Recognition and Synthesis from Google it-it-x-itb-local", + "Android Speech Recognition and Synthesis from Google it-IT-language" + ], + "nativeID": [ + "it-it-x-itb-network", + "it-it-x-itb-local" + ], + "language": "it-IT", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voce femminile 2", + "name": "Google italiano 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google it-it-x-kda-network", + "Chrome OS italiano 1", + "Android Speech Recognition and Synthesis from Google it-it-x-kda-local" + ], + "nativeID": [ + "it-it-x-kda-network", + "it-it-x-kda-local" + ], + "language": "it-IT", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voce maschile 1", + "name": "Google italiano 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google it-it-x-itc-network", + "Chrome OS italiano 3", + "Android Speech Recognition and Synthesis from Google it-it-x-itc-local" + ], + "nativeID": [ + "it-it-x-itc-network", + "it-it-x-itc-local" + ], + "language": "it-IT", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voce maschile 2", + "name": "Google italiano 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google it-it-x-itd-network", + "Chrome OS italiano 4", + "Android Speech Recognition and Synthesis from Google it-it-x-itd-local" + ], + "nativeID": [ + "it-it-x-itd-network", + "it-it-x-itd-local" + ], + "language": "it-IT", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/ja.json b/json/ja.json new file mode 100644 index 0000000..8ef8ac3 --- /dev/null +++ b/json/ja.json @@ -0,0 +1,271 @@ +{ + "language": "ja", + "defaultRegion": "ja-JP", + "testUtterance": "こんにちは。私の名前は{name}で、日本語の声を担当しています。", + "voices": [ + { + "label": "Nanami", + "name": "Microsoft Nanami Online (Natural) - Japanese (Japan)", + "language": "ja-JP", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Keita", + "name": "Microsoft Keita Online (Natural) - Japanese (Japan)", + "language": "ja-JP", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "O-Ren", + "name": "O-Ren", + "localizedName": "apple", + "language": "ja-JP", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Kyoko", + "name": "Kyoko", + "localizedName": "apple", + "language": "ja-JP", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Otoya", + "name": "Otoya", + "localizedName": "apple", + "language": "ja-JP", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Hattori", + "name": "Hattori", + "localizedName": "apple", + "language": "ja-JP", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Google の女性の声", + "name": "Google 日本語", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "ja-JP", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Ayumi", + "name": "Microsoft Ayumi - Japanese (Japan)", + "language": "ja-JP", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Haruka", + "name": "Microsoft Haruka - Japanese (Japan)", + "language": "ja-JP", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Ichiro", + "name": "Microsoft Ichiro - Japanese (Japan)", + "language": "ja-JP", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "女性の声1", + "name": "Google 日本語 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ja-jp-x-htm-network", + "Chrome OS 日本語 1", + "Android Speech Recognition and Synthesis from Google ja-jp-x-htm-local", + "Android Speech Recognition and Synthesis from Google ja-JP-language" + ], + "nativeID": [ + "ja-jp-x-htm-network", + "ja-jp-x-htm-local" + ], + "language": "ja-JP", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "女性の声2", + "name": "Chrome OS 日本語 2", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ja-jp-x-jab-network", + "Android Speech Recognition and Synthesis from Google ja-jp-x-jab-local" + ], + "nativeID": [ + "ja-jp-x-jab-network", + "ja-jp-x-jab-local" + ], + "language": "ja-JP", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男性の声1", + "name": "Google 日本語 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ja-jp-x-jac-network", + "Chrome OS 日本語 3", + "Android Speech Recognition and Synthesis from Google ja-jp-x-jac-local" + ], + "nativeID": [ + "ja-jp-x-jac-network", + "ja-jp-x-jac-local" + ], + "language": "ja-JP", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男性の声2", + "name": "Google 日本語 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ja-jp-x-jad-network", + "Chrome OS 日本語 4", + "Android Speech Recognition and Synthesis from Google ja-jp-x-jad-local" + ], + "nativeID": [ + "ja-jp-x-jad-network", + "ja-jp-x-jad-local" + ], + "language": "ja-JP", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/kn.json b/json/kn.json new file mode 100644 index 0000000..6f25fdf --- /dev/null +++ b/json/kn.json @@ -0,0 +1,103 @@ +{ + "language": "kn", + "defaultRegion": "kn-IN", + "testUtterance": "ಹಲೋ, ನನ್ನ ಹೆಸರು {name} ಮತ್ತು ನಾನು ಕನ್ನಡ ಧ್ವನಿ.", + "voices": [ + { + "label": "Sapna", + "name": "Microsoft Sapna Online (Natural) - Kannada (India)", + "language": "kn-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Gagan", + "name": "Microsoft Gagan Online (Natural) - Kannada (India)", + "language": "kn-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Soumya", + "name": "Soumya", + "localizedName": "apple", + "language": "kn-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "ಸ್ತ್ರೀ ಧ್ವನಿ", + "name": "Android Speech Recognition and Synthesis from Google kn-in-x-knf-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google kn-in-x-knf-local", + "Android Speech Recognition and Synthesis from Google kn-IN-language" + ], + "nativeID": [ + "kn-in-x-knf-network", + "kn-in-x-knf-local" + ], + "language": "kn-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "ಪುರುಷ ಧ್ವನಿ", + "name": "Android Speech Recognition and Synthesis from Google kn-in-x-knm-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google kn-in-x-knm-local" + ], + "nativeID": [ + "kn-in-x-knm-network", + "kn-in-x-knm-local" + ], + "language": "kn-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} diff --git a/json/ko.json b/json/ko.json new file mode 100644 index 0000000..ffb5e21 --- /dev/null +++ b/json/ko.json @@ -0,0 +1,280 @@ +{ + "language": "ko", + "defaultRegion": "ko-KR", + "testUtterance": "안녕하세요, 저는 {name}이고 한국어 음성입니다.", + "voices": [ + { + "label": "SunHi", + "name": "Microsoft SunHi Online (Natural) - Korean (Korea)", + "language": "ko-KR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Hyunsu", + "name": "Microsoft HyunsuMultilingual Online (Natural) - Korean (Korea)", + "altNames": [ + "Microsoft Hyunsu Online (Natural) - Korean (Korea)" + ], + "language": "ko-KR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "InJoon", + "name": "Microsoft InJoon Online (Natural) - Korean (Korea)", + "language": "ko-KR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yuna", + "name": "Yuna", + "localizedName": "apple", + "language": "ko-KR", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Jian", + "name": "Jian", + "localizedName": "apple", + "language": "ko-KR", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Suhyun", + "name": "Suhyun", + "localizedName": "apple", + "language": "ko-KR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Sora", + "name": "Sora", + "localizedName": "apple", + "language": "ko-KR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Minsu", + "name": "Minsu", + "localizedName": "apple", + "language": "ko-KR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Google 여성 음성", + "name": "Google 한국의", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "ko-KR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Heami", + "name": "Microsoft Heami - Korean (Korea)", + "language": "ko-KR", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "여성 목소리 1", + "name": "Google 한국어 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ko-kr-x-kob-network", + "Chrome OS 한국어 2", + "Android Speech Recognition and Synthesis from Google ko-kr-x-kob-local", + "Android Speech Recognition and Synthesis from Google ko-KR-language" + ], + "nativeID": [ + "ko-kr-x-kob-network", + "ko-kr-x-kob-local" + ], + "language": "ko-KR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "여성 목소리 2", + "name": "Google 한국어 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ko-kr-x-ism-network", + "Chrome OS 한국어 1", + "Android Speech Recognition and Synthesis from Google ko-kr-x-ism-local" + ], + "nativeID": [ + "ko-kr-x-ism-network", + "ko-kr-x-ism-local" + ], + "language": "ko-KR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "남성 1", + "name": "Google 한국어 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ko-kr-x-koc-network", + "Chrome OS 한국어 3", + "Android Speech Recognition and Synthesis from Google ko-kr-x-koc-local" + ], + "nativeID": [ + "ko-kr-x-koc-network", + "ko-kr-x-koc-local" + ], + "language": "ko-KR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "남성 2", + "name": "Google 한국어 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ko-kr-x-kod-network", + "Chrome OS 한국어 4", + "Android Speech Recognition and Synthesis from Google ko-kr-x-kod-local" + ], + "nativeID": [ + "ko-kr-x-kod-network", + "ko-kr-x-kod-local" + ], + "language": "ko-KR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/localizedNames/apple.json b/json/localizedNames/apple.json new file mode 100644 index 0000000..544a262 --- /dev/null +++ b/json/localizedNames/apple.json @@ -0,0 +1,140 @@ +{ + "quality": { + "ar": { + "normal": "محسن", + "high": "استثنائي" + }, + "ca": { + "normal": "millorada", + "high": "prèmium" + }, + "cmn-CN": { + "normal": "优化音质", + "high": "高音质" + }, + "cmn-TW": { + "normal": "增強音質", + "high": "高音質" + }, + "cs": { + "normal": "vylepšená verze", + "high": "prémiový" + }, + "da": { + "normal": "forbedret", + "high": "høj kvalitet" + }, + "de": { + "normal": "erweitert", + "high": "premium" + }, + "el": { + "normal": "βελτιωμένη", + "high": "υψηλής ποιότητας" + }, + "en": { + "normal": "Enhanced", + "high": "Premium" + }, + "es": { + "normal": "mejorada", + "high": "premium" + }, + "fi": { + "normal": "parannettu", + "high": "korkealaatuinen" + }, + "fr": { + "normal": "premium", + "high": "de qualité" + }, + "he": { + "normal": "משופר", + "high": "פרימיום" + }, + "hi": { + "normal": "बेहतर", + "high": "प्रीमियम" + }, + "hr": { + "normal": "poboljšani", + "high": "vrhunski" + }, + "hu": { + "normal": "továbbfejlesztett", + "high": "prémium" + }, + "id": { + "normal": "Ditingkatkan", + "high": "Premium" + }, + "it": { + "normal": "ottimizzata", + "high": "premium" + }, + "ja": { + "normal": "拡張", + "high": "プレミアム" + }, + "ko": { + "normal": "고품질", + "high": "프리미엄" + }, + "ms": { + "normal": "Dipertingkat", + "high": "Premium" + }, + "nb": { + "normal": "forbedret", + "high": "premium" + }, + "nl": { + "normal": "verbeterd", + "high": "premium" + }, + "pl": { + "normal": "rozszerzony", + "high": "premium" + }, + "pt": { + "normal": "melhorada", + "high": "premium" + }, + "ro": { + "normal": "îmbunătățită", + "high": "premium" + }, + "ru": { + "normal": "улучшенный", + "high": "высшее качество" + }, + "sk": { + "normal": "vylepšený", + "high": "prémiový" + }, + "sl": { + "normal": "izboljšano", + "high": "prvovrsten" + }, + "sv": { + "normal": "förbättrad", + "high": "premium" + }, + "th": { + "normal": "คุณภาพสูง", + "high": "คุณภาพสูง" + }, + "tr": { + "normal": "Geliştirilmiş", + "high": "Yüksek Kaliteli" + }, + "uk": { + "normal": "вдосконалений", + "high": "високої якості" + }, + "vi": { + "normal": "Nâng cao", + "high": "Cao cấp" + } + } +} \ No newline at end of file diff --git a/json/localizedNames/full/en.json b/json/localizedNames/full/en.json new file mode 100644 index 0000000..3f49124 --- /dev/null +++ b/json/localizedNames/full/en.json @@ -0,0 +1,147 @@ +{ + "quality": { + "medium": "Enhanced", + "high": "Premium" + }, + "languages": { + "ar": "Arabic", + "as": "Assamese", + "bg": "Bulgarian", + "bho": "Bhojpuri", + "bn": "Bangla", + "brx": "Bodo", + "bs": "Bosnian", + "ca": "Catalan", + "cmn": "Chinese", + "cs": "Czech", + "cy": "Welsh", + "da": "Danish", + "de": "German", + "doi": "Dogri", + "el": "Greek", + "en": "English", + "es": "Spanish", + "et": "Estonian", + "eu": "Basque", + "fa": "Persian", + "fi": "Finnish", + "fil": "Filipino", + "fr": "French", + "gl": "Galician", + "gu": "Gujarati", + "he": "Hebrew", + "hi": "Hindi", + "hr": "Croatian", + "hu": "Hungarian", + "id": "Indonesian", + "is": "Icelandic", + "it": "Italian", + "ja": "Japanese", + "jv": "Javanese", + "km": "khmer", + "kn": "Kannada", + "kok": "Konkani", + "ko": "Korean", + "lt": "Lithuanian", + "lv": "Latvia", + "mai": "Maithili", + "mal": "Malayalam", + "mni": "Manipuri", + "mr": "Marathi", + "ms": "Malay", + "nb": "Norwegian Bokmål", + "ne": "Nepali", + "nl": "Dutch", + "od": "Odia", + "pa": "Punjabi", + "pl": "Polish", + "pt": "Portuguese", + "ro": "Romanian", + "ru": "Russian", + "sa": "Sanskrit", + "sat": "Santali", + "sd": "Sindhi", + "si": "Sinhala", + "sk": "Slovak", + "sl": "Slovenian", + "sq": "Albanese", + "sr": "Serbian", + "su": "Sundanese", + "sv": "Swedish", + "sw": "Swahili", + "ta": "Tamil", + "te": "Telugu", + "th": "Thai", + "tr": "Turkish", + "uk": "Ukrainian", + "ur": "Urdu", + "vi": "Vietnamese", + "wuu": "Shanghainese" + }, + "regions": { + "0001": "World", + "al": "Albania", + "ar": "Argentina", + "at": "Austria", + "au": "Australia", + "ba": "Bosnia & Herzegovina", + "bd": "Bangladesh", + "be": "Belgium", + "bg": "Bulgaria", + "br": "Brazil", + "ca": "Canada", + "ch": "Switzerland", + "cl": "Chile", + "cn": "China Mainland", + "cz": "Czechia", + "co": "Colombia", + "da": "Denmark", + "de": "Germany", + "ee": "Estonia", + "es": "Spain", + "fi": "Finland", + "fr": "French", + "gb": "United Kingdom", + "gr": "Greece", + "hk": "Hong Kong", + "hr": "Croatia", + "hu": "Hungary", + "id": "Indonesia", + "ie": "Ireland", + "il": "Israel", + "in": "India", + "ir": "Iran", + "is": "Iceland", + "it": "Italy", + "jp": "Japan", + "ke": "Kenya", + "kh": "Cambodia", + "kr": "South Korea", + "lk": "Sri Lanka", + "lt": "Lithuania", + "lv": "Latvia", + "mx": "Mexico", + "my": "Malaysia", + "ng": "Nigeria", + "nl": "Netherlands", + "no": "Norway", + "np": "Nepal", + "pk": "Pakistan", + "pl": "Poland", + "pt": "Portugal", + "ro": "Romania", + "rs": "Serbia", + "ru": "Russia", + "se": "Sweden", + "si": "Slovenia", + "sk": "Slovakia", + "th": "Thailand", + "tr": "Türkiye", + "tw": "Taiwan", + "ua": "Ukraine", + "us": "United States", + "vn": "Viet Nam", + "yue": "Cantonese", + "za": "South Africa" + } +} \ No newline at end of file diff --git a/json/localizedNames/full/fr.json b/json/localizedNames/full/fr.json new file mode 100644 index 0000000..3822c05 --- /dev/null +++ b/json/localizedNames/full/fr.json @@ -0,0 +1,147 @@ +{ + "quality": { + "medium": "premium", + "high": "de qualité" + }, + "languages": { + "ar": "arabe", + "as": "assamais", + "bg": "bulgare", + "bho": "bhodjpouri", + "bn": "bengali", + "brx": "bodo", + "bs": "bosniaque", + "ca": "catalan", + "cs": "tchèque", + "cy": "gallois", + "da": "danois", + "de": "allemand", + "doi": "dogri", + "el": "grec", + "en": "anglais", + "es": "espagnol", + "et": "estonien", + "eu": "basque", + "fa": "persan", + "fi": "finnois", + "fil": "philippin", + "fr": "français", + "gl": "galicien", + "gu": "goudjarati", + "he": "hébreu", + "hi": "hindi", + "hr": "croate", + "hu": "hongrois", + "id": "indonésien", + "is": "islandais", + "it": "italien", + "ja": "japonais", + "jv": "javanais", + "km": "khmer", + "kn": "kannada", + "kok": "konkani", + "ko": "coréen", + "lt": "lituanien", + "lv": "letton", + "mai": "maïthili", + "mal": "malayalam", + "mni": "manipuri", + "mr": "marathi", + "ms": "malais", + "nb": "norvégien bokmål", + "ne": "népalais", + "nl": "néerlandais", + "od": "odia", + "pa": "pendjabi", + "pl": "polonais", + "pt": "portugais", + "ro": "roumain", + "ru": "russe", + "sa": "sanskrit", + "sat": "santali", + "sd": "sindhi", + "si": "singhalais", + "sk": "slovaque", + "sl": "slovène", + "sq": "albanais", + "sr": "serbe", + "su": "soudanais", + "sv": "suédois", + "sw": "swahili", + "ta": "tamoul", + "te": "télougou", + "th": "thaï", + "tr": "turc", + "uk": "ukrainien", + "ur": "ourdou", + "vi": "vietnamien", + "wuu": "shanghaïen", + "yue": "cantonais", + "zh": "chinois" + }, + "regions": { + "0001": "Monde", + "al": "Albanie", + "ar": "Argentine", + "at": "Autriche", + "au": "Australie", + "ba": "Bosnie-Herzégovine", + "bd": "Bangladesh", + "be": "Belgique", + "bg": "Bulgarie", + "br": "Brésil", + "ca": "Canada", + "ch": "Suisse", + "cl": "Chili", + "cn": "Chine continentale", + "cz": "Tchéquie", + "co": "Colombie", + "da": "Danemark", + "de": "Allemagne", + "ee": "Estonie", + "es": "Espagne", + "fi": "Finlande", + "fr": "France", + "gb": "Royaume-Uni", + "gr": "Grèce", + "hk": "Hong Kong", + "hr": "Croatie", + "hu": "Hongrie", + "id": "Indonésie", + "ie": "Irlande", + "il": "Israël", + "in": "Inde", + "ir": "Iran", + "is": "Islande", + "it": "Italie", + "jp": "Japon", + "ke": "Kenya", + "kh": "Cambodge", + "kr": "Corée du Sud", + "lk": "Sri Lanka", + "lt": "Lituanie", + "lv": "Lettonie", + "mx": "Mexique", + "my": "Malaisie", + "ng": "Nigéria", + "nl": "Pays-Bas", + "no": "Norvège", + "np": "Népal", + "pk": "Pakistan", + "pl": "Pologne", + "pt": "Portugal", + "ro": "Roumanie", + "rs": "Serbie", + "ru": "Russie", + "se": "Suède", + "si": "Slovénie", + "sk": "Slovaquie", + "th": "Thaïlande", + "tr": "Turquie", + "tw": "Taïwan", + "ua": "Ukraine", + "us": "États-Unis", + "vn": "Viêt Nam", + "za": "Afrique du Sud" + } +} \ No newline at end of file diff --git a/json/mr.json b/json/mr.json new file mode 100644 index 0000000..79ec849 --- /dev/null +++ b/json/mr.json @@ -0,0 +1,79 @@ +{ + "language": "mr", + "defaultRegion": "mr-IN", + "testUtterance": "नमस्कार, माझे नाव {name} आहे आणि मी एक मराठी आवाज आहे.", + "voices": [ + { + "label": "Aarohi", + "name": "Microsoft Aarohi Online (Natural) - Marathi (India)", + "language": "mr-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Manohar", + "name": "Microsoft Manohar Online (Natural) - Marathi (India)", + "language": "mr-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ananya", + "name": "Ananya", + "localizedName": "apple", + "language": "mr-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "स्त्री आवाज", + "name": "Android Speech Recognition and Synthesis from Google mr-in-x-mrf-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google mr-in-x-mrf-local", + "Android Speech Recognition and Synthesis from Google mr-IN-language" + ], + "nativeID": [ + "mr-in-x-mrf-network", + "mr-in-x-mrf-local" + ], + "language": "mr-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/ms.json b/json/ms.json new file mode 100644 index 0000000..b28a424 --- /dev/null +++ b/json/ms.json @@ -0,0 +1,165 @@ +{ + "language": "ms", + "defaultRegion": "ms-MY", + "testUtterance": "Hello, nama saya {name} dan saya suara Melayu.", + "voices": [ + { + "label": "Yasmin", + "name": "Microsoft Yasmin Online (Natural) - Malay (Malaysia)", + "language": "ms-MY", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Osman", + "name": "Microsoft Osman Online (Natural) - Malay (Malaysia)", + "language": "ms-MY", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Amira", + "name": "Amira", + "localizedName": "apple", + "language": "ms-MY", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Rizwan", + "name": "Microsoft Rizwan - Malay (Malaysia)", + "language": "ms-MY", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + + { + "label": "Suara perempuan 1", + "name": "Android Speech Recognition and Synthesis from Google ms-my-x-msc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ms-my-x-msc-local", + "Android Speech Recognition and Synthesis from Google ms-MY-language" + ], + "nativeID": [ + "ms-my-x-msc-network", + "ms-my-x-msc-local" + ], + "language": "ms-MY", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Suara perempuan 2", + "name": "Android Speech Recognition and Synthesis from Google ms-my-x-mse-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ms-my-x-mse-local" + ], + "nativeID": [ + "ms-my-x-mse-network", + "ms-my-x-mse-local" + ], + "language": "ms-MY", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Suara lelaki 1", + "name": "Android Speech Recognition and Synthesis from Google ms-my-x-msd-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ms-my-x-msd-local" + ], + "nativeID": [ + "ms-my-x-msd-network", + "ms-my-x-msd-local" + ], + "language": "ms-MY", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Suara lelaki 2", + "name": "Android Speech Recognition and Synthesis from Google ms-my-x-msg-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ms-my-x-msg-local" + ], + "nativeID": [ + "ms-my-x-msg-network", + "ms-my-x-msg-local" + ], + "language": "ms-MY", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/nb.json b/json/nb.json new file mode 100644 index 0000000..47f84cc --- /dev/null +++ b/json/nb.json @@ -0,0 +1,215 @@ +{ + "language": "nb", + "defaultRegion": "nb-NO", + "testUtterance": "Hei, jeg heter {name} og er en norsk stemme.", + "voices": [ + { + "label": "Pernille", + "name": "Microsoft Pernille Online (Natural) - Norwegian (Bokmål, Norway)", + "language": "nb-NO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Finn", + "name": "Microsoft Finn Online (Natural) - Norwegian (Bokmål Norway)", + "language": "nb-NO", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Nora", + "name": "Nora", + "localizedName": "apple", + "language": "nb-NO", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Henrik", + "name": "Henrik", + "localizedName": "apple", + "language": "nb-NO", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Jon", + "name": "Microsoft Jon - Norwegian (Bokmål Norway)", + "language": "nb-NO", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Kvinnestemme 1", + "name": "Google Norsk Bokmål 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nb-no-x-cfl-network", + "Chrome OS Norsk Bokmål 2", + "Android Speech Recognition and Synthesis from Google nb-no-x-cfl-local", + "Android Speech Recognition and Synthesis from Google nb-NO-language" + ], + "nativeID": [ + "nb-no-x-cfl-network", + "nb-no-x-cfl-local" + ], + "language": "nb-NO", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kvinnestemme 2", + "name": "Google Norsk Bokmål 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nb-no-x-rfj-network", + "Chrome OS Norsk Bokmål 1", + "Android Speech Recognition and Synthesis from Google nb-no-x-rfj-local" + ], + "nativeID": [ + "nb-no-x-rfj-network", + "nb-no-x-rfj-local" + ], + "language": "nb-NO", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kvinnestemme 3", + "name": "Google Norsk Bokmål 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nb-no-x-tfs-network", + "Chrome OS Norsk Bokmål 4", + "Android Speech Recognition and Synthesis from Google nb-no-x-tfs-local" + ], + "nativeID": [ + "nb-no-x-tfs-network", + "nb-no-x-tfs-local" + ], + "language": "nb-NO", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mannsstemme 1", + "name": "Google Norsk Bokmål 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nb-no-x-cmj-network", + "Chrome OS Norsk Bokmål 3", + "Android Speech Recognition and Synthesis from Google nb-no-x-cmj-local" + ], + "nativeID": [ + "nb-no-x-cmj-network", + "nb-no-x-cmj-local" + ], + "language": "nb-NO", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mannsstemme 2", + "name": "Google Norsk Bokmål 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nb-no-x-tmg-network", + "Chrome OS Norsk Bokmål 5", + "Android Speech Recognition and Synthesis from Google nb-no-x-tmg-local" + ], + "nativeID": [ + "nb-no-x-tmg-network", + "nb-no-x-tmg-local" + ], + "language": "nb-NO", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/nl.json b/json/nl.json new file mode 100644 index 0000000..55792af --- /dev/null +++ b/json/nl.json @@ -0,0 +1,357 @@ +{ + "language": "nl", + "defaultRegion": "nl-NL", + "testUtterance": "Hallo, mijn naam is {name} en ik ben een Nederlandse stem.", + "voices": [ + { + "label": "Colette", + "name": "Microsoft Colette Online (Natural) - Dutch (Netherlands)", + "language": "nl-NL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Fenna", + "name": "Microsoft Fenna Online (Natural) - Dutch (Netherlands)", + "language": "nl-NL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Hanna", + "name": "Microsoft Hanna Online - Dutch (Netherlands)", + "language": "nl-NL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Maarten", + "name": "Microsoft Maarten Online (Natural) - Dutch (Netherlands)", + "language": "nl-NL", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Dena", + "name": "Microsoft Dena Online (Natural) - Dutch (Belgium)", + "language": "nl-BE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Arnaud", + "name": "Microsoft Arnaud Online (Natural) - Dutch (Belgium)", + "language": "nl-BE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Claire", + "name": "Claire", + "localizedName": "apple", + "language": "nl-NL", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Xander", + "name": "Xander", + "localizedName": "apple", + "language": "nl-NL", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Ellen", + "name": "Ellen", + "localizedName": "apple", + "language": "nl-BE", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Google mannelijke stem", + "name": "Google Nederlands", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "nl-NL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Frank", + "name": "Microsoft Frank - Dutch (Netherlands)", + "language": "nl-NL", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Vrouwelijke stem 1 (Nederland)", + "name": "Google Nederlands 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-nl-x-lfc-network", + "Chrome OS Nederlands 4", + "Android Speech Recognition and Synthesis from Google nl-nl-x-lfc-local", + "Android Speech Recognition and Synthesis from Google nl-NL-language" + ], + "nativeID": [ + "nl-nl-x-lfc-network", + "nl-nl-x-lfc-local" + ], + "language": "nl-NL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Vrouwelijke stem 2 (Nederland)", + "name": "Google Nederlands 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-nl-x-tfb-network", + "Chrome OS Nederlands 1", + "Android Speech Recognition and Synthesis from Google nl-nl-x-tfb-local" + ], + "nativeID": [ + "nl-nl-x-tfb-network", + "nl-nl-x-tfb-local" + ], + "language": "nl-NL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Vrouwelijke stem 3 (Nederland)", + "name": "Google Nederlands 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-nl-x-yfr-network", + "Chrome OS Nederlands 5", + "Android Speech Recognition and Synthesis from Google nl-nl-x-yfr-local" + ], + "nativeID": [ + "nl-nl-x-yfr-network", + "nl-nl-x-yfr-local" + ], + "language": "nl-NL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mannelijke stem 1 (Nederland)", + "name": "Google Nederlands 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-nl-x-bmh-network", + "Chrome OS Nederlands 2", + "Android Speech Recognition and Synthesis from Google nl-nl-x-bmh-local" + ], + "nativeID": [ + "nl-nl-x-bmh-network", + "nl-nl-x-bmh-local" + ], + "language": "nl-NL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mannelijke stem 2 (Nederland)", + "name": "Google Nederlands 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-nl-x-dma-network", + "Chrome OS Nederlands 3", + "Android Speech Recognition and Synthesis from Google nl-nl-x-dma-local" + ], + "nativeID": [ + "nl-nl-x-dma-network", + "nl-nl-x-dma-local" + ], + "language": "nl-NL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Vrouwelijke stem (België)", + "name": "Android Speech Recognition and Synthesis from Google nl-be-x-bec-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-be-x-bec-local", + "Android Speech Recognition and Synthesis from Google nl-BE-language" + ], + "nativeID": [ + "nl-be-x-bec-network", + "nl-be-x-bec-local" + ], + "language": "nl-BE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mannelijke stem (België)", + "name": "Android Speech Recognition and Synthesis from Google nl-be-x-bed-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google nl-be-x-bed-local" + ], + "nativeID": [ + "nl-be-x-bed-network", + "nl-be-x-bed-local" + ], + "language": "nl-BE", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/pl.json b/json/pl.json new file mode 100644 index 0000000..9389925 --- /dev/null +++ b/json/pl.json @@ -0,0 +1,280 @@ +{ + "language": "pl", + "defaultRegion": "pl-PL", + "testUtterance": "Cześć, nazywam się {name} i mam polski głos.", + "voices": [ + { + "label": "Zofia", + "name": "Microsoft Zofia Online (Natural) - Polish (Poland)", + "language": "pl-PL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Paulina", + "name": "Microsoft Paulina Online - Polish (Poland)", + "language": "pl-PL", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Marek", + "name": "Microsoft Marek Online (Natural) - Polish (Poland)", + "language": "pl-PL", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ewa", + "name": "Ewa", + "localizedName": "apple", + "language": "pl-PL", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Zosia", + "name": "Zosia", + "localizedName": "apple", + "language": "pl-PL", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Krzysztof", + "name": "Krzysztof", + "localizedName": "apple", + "language": "pl-PL", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Żeński głos Google’a", + "name": "Google polski", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "pl-PL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Paulina", + "name": "Microsoft Paulina - Polish (Poland)", + "language": "pl-PL", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Adam", + "name": "Microsoft Adam - Polish (Poland)", + "language": "pl-PL", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Głos żeński 1", + "name": "Google Polski 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pl-pl-x-afb-network", + "Chrome OS Polski 2", + "Android Speech Recognition and Synthesis from Google pl-pl-x-afb-local", + "Android Speech Recognition and Synthesis from Google pl-PL-language" + ], + "nativeID": [ + "pl-pl-x-afb-network", + "pl-pl-x-afb-local" + ], + "language": "pl-PL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Głos żeński 2", + "name": "Google Polski 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pl-pl-x-oda-network", + "Chrome OS Polski 1", + "Android Speech Recognition and Synthesis from Google pl-pl-x-oda-local" + ], + "nativeID": [ + "pl-pl-x-oda-network", + "pl-pl-x-oda-local" + ], + "language": "pl-PL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Głos żeński 3", + "name": "Google Polski 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pl-pl-x-zfg-network", + "Chrome OS Polski 5", + "Android Speech Recognition and Synthesis from Google pl-pl-x-zfg-local" + ], + "nativeID": [ + "pl-pl-x-zfg-network", + "pl-pl-x-zfg-local" + ], + "language": "pl-PL", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Głos męski 1", + "name": "Google Polski 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pl-pl-x-bmg-network", + "Chrome OS Polski 3", + "Android Speech Recognition and Synthesis from Google pl-pl-x-bmg-local" + ], + "nativeID": [ + "pl-pl-x-bmg-network", + "pl-pl-x-bmg-local" + ], + "language": "pl-PL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Głos męski 2", + "name": "Google Polski 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pl-pl-x-jmk-network", + "Chrome OS Polski 4", + "Android Speech Recognition and Synthesis from Google pl-pl-x-jmk-local" + ], + "nativeID": [ + "pl-pl-x-jmk-network", + "pl-pl-x-jmk-local" + ], + "language": "pl-PL", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/pt.json b/json/pt.json new file mode 100644 index 0000000..46fd004 --- /dev/null +++ b/json/pt.json @@ -0,0 +1,425 @@ +{ + "language": "pt", + "defaultRegion": "pt-BR", + "testUtterance": "Olá, o meu nome é {name} e sou uma voz portuguesa.", + "voices": [ + { + "label": "Raquel", + "name": "Microsoft Raquel Online (Natural) - Portuguese (Portugal)", + "language": "pt-PT", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Duarte", + "name": "Microsoft Duarte Online (Natural) - Portuguese (Portugal)", + "language": "pt-PT", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Francisca", + "name": "Microsoft Francisca Online (Natural) - Portuguese (Brazil)", + "language": "pt-BR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Thalita", + "name": "Microsoft ThalitaMultilingual Online (Natural) - Portuguese (Brazil)", + "language": "pt-BR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Antonio", + "name": "Microsoft Antonio Online (Natural) - Portuguese (Brazil)", + "language": "pt-BR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Catarina", + "name": "Catarina", + "localizedName": "apple", + "language": "pt-PT", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Joana", + "name": "Joana", + "localizedName": "apple", + "language": "pt-PT", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Joaquim", + "name": "Joaquim", + "localizedName": "apple", + "language": "pt-PT", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Fernanda", + "name": "Fernanda", + "localizedName": "apple", + "language": "pt-BR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Luciana", + "name": "Luciana", + "localizedName": "apple", + "language": "pt-BR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Felipe", + "name": "Felipe", + "localizedName": "apple", + "language": "pt-BR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Voz feminina do Google", + "name": "Google português do Brasil", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "pt-BR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Helia", + "name": "Microsoft Helia - Portuguese (Portugal)", + "language": "pt-PT", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Maria", + "name": "Microsoft Maria - Portuguese (Brazil)", + "language": "pt-BR", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Daniel", + "name": "Microsoft Daniel - Portuguese (Brazil)", + "language": "pt-BR", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Voz feminina 1 (Portugal)", + "name": "Google português de Portugal 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-pt-x-jfb-network", + "Android Speech Recognition and Synthesis from Google pt-pt-x-jfb-local", + "Android Speech Recognition and Synthesis from Google pt-PT-language" + ], + "nativeID": [ + "pt-pt-x-jfb-network", + "pt-pt-x-jfb-local" + ], + "language": "pt-PT", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz feminina 2 (Portugal)", + "name": "Google português de Portugal 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-pt-x-sfs-network", + "Chrome OS português de Portugal", + "Android Speech Recognition and Synthesis from Google pt-pt-x-sfs-local" + ], + "nativeID": [ + "pt-pt-x-sfs-network", + "pt-pt-x-sfs-local" + ], + "language": "pt-PT", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina 1 (Portugal)", + "name": "Google português de Portugal 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-pt-x-jmn-network", + "Android Speech Recognition and Synthesis from Google pt-pt-x-jmn-local" + ], + "nativeID": [ + "pt-pt-x-jmn-network", + "pt-pt-x-jmn-local" + ], + "language": "pt-PT", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina 2 (Portugal)", + "name": "Google português de Portugal 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-pt-x-pmj-network", + "Android Speech Recognition and Synthesis from Google pt-pt-x-pmj-local" + ], + "nativeID": [ + "pt-pt-x-pmj-network", + "pt-pt-x-pmj-local" + ], + "language": "pt-PT", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz feminina 1 (Brasil)", + "name": "Google português do Brasil 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-br-x-afs-network", + "Chrome OS português do Brasil", + "Android Speech Recognition and Synthesis from Google pt-br-x-afs-local", + "Android Speech Recognition and Synthesis from Google pt-BR-language" + ], + "nativeID": [ + "pt-br-x-afs-network", + "pt-br-x-afs-local" + ], + "language": "pt-BR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz feminina 2 (Brasil)", + "name": "Google português do Brasil 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-br-x-pte-network", + "Android Speech Recognition and Synthesis from Google pt-br-x-pte-local" + ], + "nativeID": [ + "pt-br-x-pte-network", + "pt-br-x-pte-local" + ], + "language": "pt-BR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Voz masculina (Brasil)", + "name": "Google português do Brasil 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google pt-br-x-ptd-network", + "Android Speech Recognition and Synthesis from Google pt-br-x-ptd-local" + ], + "nativeID": [ + "pt-br-x-ptd-network", + "pt-br-x-ptd-local" + ], + "language": "pt-BR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/ro.json b/json/ro.json new file mode 100644 index 0000000..5a4dc71 --- /dev/null +++ b/json/ro.json @@ -0,0 +1,95 @@ +{ + "language": "ro", + "defaultRegion": "ro-RO", + "testUtterance": "Buna ziua, ma numesc {name} si sunt o voce romaneasca.", + "voices": [ + { + "label": "Alina", + "name": "Microsoft Alina Online (Natural) - Romanian (Romania)", + "language": "ro-RO", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Emil", + "name": "Microsoft Emil Online (Natural) - Romanian (Romania)", + "language": "ro-RO", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ioana", + "name": "Ioana", + "localizedName": "apple", + "language": "ro-RO", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Andrei", + "name": "Microsoft Andrei - Romanian (Romania)", + "language": "ro-RO", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Voce feminină", + "name": "Android Speech Recognition and Synthesis from Google ro-ro-x-vfv-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ro-ro-x-vfv-local", + "Android Speech Recognition and Synthesis from Google ro-RO-language" + ], + "nativeID": [ + "ro-ro-x-vfv-network", + "ro-ro-x-vfv-local" + ], + "language": "ro-RO", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/ru.json b/json/ru.json new file mode 100644 index 0000000..299470c --- /dev/null +++ b/json/ru.json @@ -0,0 +1,269 @@ +{ + "language": "ru", + "defaultRegion": "ru-RU", + "testUtterance": "Здравствуйте, меня зовут {name} и я русский голос.", + "voices": [ + { + "label": "Svetlana", + "name": "Microsoft Svetlana Online (Natural) - Russian (Russia)", + "language": "ru-RU", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ekaterina", + "name": "Microsoft Ekaterina Online - Russian (Russia)", + "language": "ru-RU", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Dmitry", + "name": "Microsoft Dmitry Online (Natural) - Russian (Russia)", + "language": "ru-RU", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Katya", + "name": "Katya", + "localizedName": "apple", + "language": "ru-RU", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Milena", + "name": "Milena", + "localizedName": "apple", + "language": "ru-RU", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Yuri", + "name": "Yuri", + "localizedName": "apple", + "language": "ru-RU", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Google женский голос", + "name": "Google русский", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "ru-RU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + + { + "label": "Irina", + "name": "Microsoft Irina - Russian (Russian)", + "language": "ru-RU", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Pavel", + "name": "Microsoft Pavel - Russian (Russian)", + "language": "ru-RU", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Женский голос 1", + "name": "Android Speech Recognition and Synthesis from Google ru-ru-x-dfc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ru-ru-x-dfc-local" + ], + "nativeID": [ + "ru-ru-x-dfc-network", + "ru-ru-x-dfc-local" + ], + "language": "ru-RU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Женский голос 2", + "name": "Android Speech Recognition and Synthesis from Google ru-ru-x-ruc-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ru-ru-x-ruc-local" + ], + "nativeID": [ + "ru-ru-x-ruc-network", + "ru-ru-x-ruc-local" + ], + "language": "ru-RU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Женский голос 3", + "name": "Android Speech Recognition and Synthesis from Google ru-ru-x-rue-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ru-ru-x-rue-local" + ], + "nativeID": [ + "ru-ru-x-rue-network", + "ru-ru-x-rue-local" + ], + "language": "ru-RU", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Мужской голос 1", + "name": "Android Speech Recognition and Synthesis from Google ru-ru-x-rud-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ru-ru-x-rud-local" + ], + "nativeID": [ + "ru-ru-x-rud-network", + "ru-ru-x-rud-local" + ], + "language": "ru-RU", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Мужской голос 2", + "name": "Android Speech Recognition and Synthesis from Google ru-ru-x-ruf-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ru-ru-x-ruf-local" + ], + "nativeID": [ + "ru-ru-x-ruf-network", + "ru-ru-x-ruf-local" + ], + "language": "ru-RU", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/sk.json b/json/sk.json new file mode 100644 index 0000000..56ad63e --- /dev/null +++ b/json/sk.json @@ -0,0 +1,97 @@ +{ + "language": "sk", + "defaultRegion": "sk-SK", + "testUtterance": "Dobrý deň, volám sa {name} a som slovenský hlas.", + "voices": [ + { + "label": "Viktoria", + "name": "Microsoft Viktoria Online (Natural) - Slovak (Slovakia)", + "language": "sk-SK", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Lukas", + "name": "Microsoft Lukas Online (Natural) - Slovak (Slovakia)", + "language": "sk-SK", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Laura", + "name": "Laura", + "localizedName": "apple", + "language": "sk-SK", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Filip", + "name": "Microsoft Filip - Slovak (Slovakia)", + "language": "sk-SK", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Ženský hlas", + "name": "Google Slovenčina (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sk-sk-x-sfk-network", + "Android Speech Recognition and Synthesis from Google sk-sk-x-sfk-local", + "Chrome OS Slovenčina", + "Android Speech Recognition and Synthesis from Google sk-SK-language" + ], + "nativeID": [ + "sk-sk-x-sfk-network", + "sk-sk-x-sfk-local" + ], + "language": "sk-SK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/sl.json b/json/sl.json new file mode 100644 index 0000000..1e5f6f9 --- /dev/null +++ b/json/sl.json @@ -0,0 +1,93 @@ +{ + "language": "sl", + "defaultRegion": "sl-SI", + "testUtterance": "Pozdravljeni, moje ime je {name} in sem slovenski glas.", + "voices": [ + { + "label": "Petra", + "name": "Microsoft Petra Online (Natural) - Slovenian (Slovenia)", + "language": "sl-SI", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Rok", + "name": "Microsoft Rok Online (Natural) - Slovenian (Slovenia)", + "language": "sl-SI", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Tina", + "name": "Tina", + "localizedName": "apple", + "language": "sl-SI", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Lado", + "name": "Microsoft Lado - Slovenian (Slovenia)", + "language": "sl-SI", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Ženski glas", + "name": "Android Speech Recognition and Synthesis from Google sl-si-x-frm-local", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sl-SI-language" + ], + "nativeID": [ + "sl-si-x-frm-local" + ], + "language": "sl-SI", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/sv.json b/json/sv.json new file mode 100644 index 0000000..d99e577 --- /dev/null +++ b/json/sv.json @@ -0,0 +1,231 @@ +{ + "language": "sv", + "defaultRegion": "sv-SE", + "testUtterance": "Hej, jag heter {name} och jag är en svensk röst.", + "voices": [ + { + "label": "Sofie", + "name": "Microsoft Sofie Online (Natural) - Swedish (Sweden)", + "language": "sv-SE", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mattias", + "name": "Microsoft Mattias Online (Natural) - Swedish (Sweden)", + "language": "sv-SE", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Klara", + "name": "Klara", + "localizedName": "apple", + "language": "sv-SE", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Alva", + "name": "Alva", + "localizedName": "apple", + "language": "sv-SE", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Oskar", + "name": "Oskar", + "note": "This voice can be installed on all Apple devices and offers two variants. Like all voices that can be installed on Apple devices, it suffers from inconsistent naming due to localization.", + "language": "sv-SE", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Bengt", + "name": "Microsoft Bengt - Swedish (Sweden)", + "language": "sv-SE", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Kvinnlig röst 1", + "name": "Google Svenska 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sv-se-x-lfs-network", + "Chrome OS Svenska", + "Android Speech Recognition and Synthesis from Google sv-se-x-lfs-local", + "Android Speech Recognition and Synthesis from Google sv-SE-language" + ], + "nativeID": [ + "sv-se-x-lfs-network", + "sv-se-x-lfs-local" + ], + "language": "sv-SE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kvinnlig röst 2", + "name": "Google Svenska 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sv-se-x-afp-network", + "Android Speech Recognition and Synthesis from Google sv-se-x-afp-local" + ], + "nativeID": [ + "sv-se-x-afp-network", + "sv-se-x-afp-local" + ], + "language": "sv-SE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kvinnlig röst 3", + "name": "Google Svenska 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sv-se-x-cfg-network", + "Android Speech Recognition and Synthesis from Google sv-se-x-cfg-local" + ], + "nativeID": [ + "sv-se-x-cfg-network", + "sv-se-x-cfg-local" + ], + "language": "sv-SE", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mansröst 1", + "name": "Google Svenska 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sv-se-x-cmh-network", + "Android Speech Recognition and Synthesis from Google sv-se-x-cmh-local" + ], + "nativeID": [ + "sv-se-x-cmh-network", + "sv-se-x-cmh-local" + ], + "language": "sv-SE", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Mansröst 2", + "name": "Google Svenska 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google sv-se-x-dmc-network", + "Android Speech Recognition and Synthesis from Google sv-se-x-dmc-local" + ], + "nativeID": [ + "sv-se-x-dmc-network", + "sv-se-x-dmc-local" + ], + "language": "sv-SE", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/ta.json b/json/ta.json new file mode 100644 index 0000000..ac844dd --- /dev/null +++ b/json/ta.json @@ -0,0 +1,209 @@ +{ + "language": "ta", + "defaultRegion": "ta-IN", + "testUtterance": "வணக்கம், என் பெயர் {name} மற்றும் நான் ஒரு தமிழ் குரல்", + "voices": [ + { + "label": "Pallavi", + "name": "Microsoft Pallavi Online (Natural) - Tamil (India)", + "language": "ta-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Valluvar", + "name": "Microsoft Valluvar Online (Natural) - Tamil (India)", + "language": "ta-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Saranya", + "name": "Microsoft Saranya Online (Natural) - Tamil (Sri Lanka)", + "language": "ta-LK", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Kumar", + "name": "Microsoft Kumar Online (Natural) - Tamil (Sri Lanka)", + "language": "ta-LK", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Kani", + "name": "Microsoft Kani Online (Natural) - Tamil (Malaysia)", + "language": "ta-MY", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Surya", + "name": "Microsoft Surya Online (Natural) - Tamil (Malaysia)", + "language": "ta-MY", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Venba", + "name": "Microsoft Venba Online (Natural) - Tamil (Singapore)", + "language": "ta-SG", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Anbu", + "name": "Microsoft Anbu Online (Natural) - Tamil (Singapore)", + "language": "ta-SG", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Vani", + "name": "Vani", + "localizedName": "apple", + "language": "ta-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Valluvar", + "name": "Microsoft Valluvar - Tamil (India)", + "language": "ta-IN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + + { + "label": "பெண் குரல்", + "name": "Android Speech Recognition and Synthesis from Google ta-in-x-tac-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ta-in-x-tac-local", + "Android Speech Recognition and Synthesis from Google ta-IN-language" + ], + "nativeID": [ + "ta-in-x-tac-network", + "ta-in-x-tac-local" + ], + "language": "ta-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "ஆண் குரல்", + "name": "Android Speech Recognition and Synthesis from Google ta-in-x-tad-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google ta-in-x-tad-local" + ], + "nativeID": [ + "ta-in-x-tad-network", + "ta-in-x-tad-local" + ], + "language": "ta-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/te.json b/json/te.json new file mode 100644 index 0000000..8f7b06b --- /dev/null +++ b/json/te.json @@ -0,0 +1,103 @@ +{ + "language": "te", + "defaultRegion": "te-IN", + "testUtterance": "హలో, నా పేరు {name} మరియు నేను తెలుగు వాణిని.", + "voices": [ + { + "label": "Shruti", + "name": "Microsoft Shruti Online (Natural) - Telugu (India)", + "language": "te-IN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Mohan", + "name": "Microsoft Mohan Online (Natural) - Telugu (India)", + "language": "te-IN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Geeta", + "name": "Geeta", + "localizedName": "apple", + "language": "te-IN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "స్త్రీ స్వరం", + "name": "Android Speech Recognition and Synthesis from Google te-in-x-tef-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google te-in-x-tef-local", + "Android Speech Recognition and Synthesis from Google te-IN-language" + ], + "nativeID": [ + "te-in-x-tef-network", + "te-in-x-tef-local" + ], + "language": "te-IN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "పురుష స్వరం", + "name": "Android Speech Recognition and Synthesis from Google te-in-x-tem-network", + "altNames": [ + "Android Speech Recognition and Synthesis from Google te-in-x-tem-local" + ], + "nativeID": [ + "te-in-x-tem-network", + "te-in-x-tem-local" + ], + "language": "te-IN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/th.json b/json/th.json new file mode 100644 index 0000000..399533a --- /dev/null +++ b/json/th.json @@ -0,0 +1,115 @@ +{ + "language": "th", + "defaultRegion": "th-TH", + "testUtterance": "สวัสดีค่ะ ฉันชื่อ {name} และฉันเป็นคนมีเสียงภาษาไทย", + "voices": [ + { + "label": "Premwadee", + "name": "Microsoft Premwadee Online (Natural) - Thai (Thailand)", + "language": "th-TH", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Niwat", + "name": "Microsoft Niwat Online (Natural) - Thai (Thailand)", + "language": "th-TH", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Narisa", + "name": "Narisa", + "localizedName": "apple", + "language": "th-TH", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Kanya", + "name": "Kanya", + "localizedName": "apple", + "language": "th-TH", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Pattara", + "name": "Microsoft Pattara - Thai (Thailand)", + "language": "th-TH", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "เสียงผู้หญิง", + "name": "Google ไทย (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google th-th-x-mol-network", + "Chrome OS ไทย", + "Android Speech Recognition and Synthesis from Google th-th-x-mol-local", + "Android Speech Recognition and Synthesis from Google th-TH-language" + ], + "nativeID": [ + "th-th-x-mol-network", + "th-th-x-mol-local" + ], + "language": "th-TH", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/tr.json b/json/tr.json new file mode 100644 index 0000000..c6fa47d --- /dev/null +++ b/json/tr.json @@ -0,0 +1,219 @@ +{ + "language": "tr", + "defaultRegion": "tr-TR", + "testUtterance": "Merhaba, adım {name} ve Türk sesiyim.", + "voices": [ + { + "label": "Emel", + "name": "Microsoft Emel Online (Natural) - Turkish (Turkey)", + "language": "tr-TR", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ahmet", + "name": "Microsoft Ahmet Online (Natural) - Turkish (Türkiye)", + "language": "tr-TR", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Yelda", + "name": "Yelda", + "localizedName": "apple", + "altNames": [ + "Yelda (Geliştirilmiş)", + "Yelda (Türkçe (Türkiye))" + ], + "language": "tr-TR", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Cem", + "name": "Cem", + "localizedName": "apple", + "language": "tr-TR", + "gender": "male", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ] + }, + { + "label": "Tolga", + "name": "Microsoft Tolga - Turkish (Turkey)", + "language": "tr-TR", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Kadın sesi 1", + "name": "Google Türkçe 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google tr-tr-x-cfs-network", + "Chrome OS Türkçe 3", + "Android Speech Recognition and Synthesis from Google tr-tr-x-cfs-local", + "Android Speech Recognition and Synthesis from Google tr-TR-language" + ], + "nativeID": [ + "tr-tr-x-cfs-network", + "tr-tr-x-cfs-local" + ], + "language": "tr-TR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kadın sesi 2", + "name": "Google Türkçe 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google tr-tr-x-efu-network", + "Chrome OS Türkçe 4", + "Android Speech Recognition and Synthesis from Google tr-tr-x-efu-local" + ], + "nativeID": [ + "tr-tr-x-efu-network", + "tr-tr-x-efu-local" + ], + "language": "tr-TR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Kadın sesi 3", + "name": "Google Türkçe 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google tr-tr-x-mfm-network", + "Chrome OS Türkçe 1", + "Android Speech Recognition and Synthesis from Google tr-tr-x-mfm-local" + ], + "nativeID": [ + "tr-tr-x-mfm-network", + "tr-tr-x-mfm-local" + ], + "language": "tr-TR", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Erkek sesi 1", + "name": "Google Türkçe 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google tr-tr-x-ama-network", + "Chrome OS Türkçe 2", + "Android Speech Recognition and Synthesis from Google tr-tr-x-ama-local" + ], + "nativeID": [ + "tr-tr-x-ama-network", + "tr-tr-x-ama-local" + ], + "language": "tr-TR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Erkek sesi 2", + "name": "Google Türkçe 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google tr-tr-x-tmc-network", + "Chrome OS Türkçe 5", + "Android Speech Recognition and Synthesis from Google tr-tr-x-tmc-local" + ], + "nativeID": [ + "tr-tr-x-tmc-network", + "tr-tr-x-tmc-local" + ], + "language": "tr-TR", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/uk.json b/json/uk.json new file mode 100644 index 0000000..f65fc23 --- /dev/null +++ b/json/uk.json @@ -0,0 +1,82 @@ +{ + "language": "uk", + "defaultRegion": "uk-UA", + "testUtterance": "Здравствуйте, меня зовут {name} и я украинский голос.", + "voices": [ + { + "label": "Polina", + "name": "Microsoft Polina Online (Natural) - Ukrainian (Ukraine)", + "language": "uk-UA", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Ostap", + "name": "Microsoft Ostap Online (Natural) - Ukrainian (Ukraine)", + "language": "uk-UA", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Lesya", + "name": "Lesya", + "localizedName": "apple", + "language": "uk-UA", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Жіночий голос", + "name": "Google українська (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google uk-ua-x-hfd-network", + "Chrome OS українська", + "Android Speech Recognition and Synthesis from Google uk-ua-x-hfd-local", + "Android Speech Recognition and Synthesis from Google uk-UA-language" + ], + "nativeID": [ + "uk-ua-x-hfd-network", + "uk-ua-x-hfd-local" + ], + "language": "uk-UA", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/vi.json b/json/vi.json new file mode 100644 index 0000000..638646c --- /dev/null +++ b/json/vi.json @@ -0,0 +1,197 @@ +{ + "language": "vi", + "defaultRegion": "vi-VN", + "testUtterance": "Xin chào, tôi tên là {name} và tôi là giọng nói tiếng Việt.", + "voices": [ + { + "label": "HoaiMy", + "name": "Microsoft HoaiMy Online (Natural) - Vietnamese (Vietnam)", + "language": "vi-VN", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "NamMinh", + "name": "Microsoft NamMinh Online (Natural) - Vietnamese (Vietnam)", + "language": "vi-VN", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Linh", + "name": "Linh", + "localizedName": "apple", + "language": "vi-VN", + "gender": "female", + "quality": [ + "low", + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "An", + "name": "Microsoft An - Vietnamese (Vietnam)", + "language": "vi-VN", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Giọng nữ 1", + "name": "Google Tiếng Việt 1 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google vi-vn-x-vic-network", + "Chrome OS Tiếng Việt 1", + "Android Speech Recognition and Synthesis from Google vi-vn-x-vic-local", + "Android Speech Recognition and Synthesis from Google vi-VN-language" + ], + "nativeID": [ + "vi-vn-x-vic-network", + "vi-vn-x-vic-local" + ], + "language": "vi-VN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Giọng nữ 2", + "name": "Google Tiếng Việt 2 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google vi-vn-x-vid-network", + "Chrome OS Tiếng Việt 2", + "Android Speech Recognition and Synthesis from Google vi-vn-x-vid-local" + ], + "nativeID": [ + "vi-vn-x-vid-network", + "vi-vn-x-vid-local" + ], + "language": "vi-VN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Giọng nữ 3", + "name": "Google Tiếng Việt 4 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google vi-vn-x-vif-network", + "Chrome OS Tiếng Việt 4", + "Android Speech Recognition and Synthesis from Google vi-vn-x-vif-local" + ], + "nativeID": [ + "vi-vn-x-vif-network", + "vi-vn-x-vif-local" + ], + "language": "vi-VN", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Giọng nam 1", + "name": "Google Tiếng Việt 3 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google vi-vn-x-vie-network", + "Chrome OS Tiếng Việt 3", + "Android Speech Recognition and Synthesis from Google vi-vn-x-vie-local" + ], + "nativeID": [ + "vi-vn-x-vie-network", + "vi-vn-x-vie-local" + ], + "language": "vi-VN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "Giọng nam 2", + "name": "Google Tiếng Việt 5 (Natural)", + "altNames": [ + "Android Speech Recognition and Synthesis from Google vi-vn-x-gft-network", + "Chrome OS Tiếng Việt 5", + "Android Speech Recognition and Synthesis from Google vi-vn-x-gft-local" + ], + "nativeID": [ + "vi-vn-x-gft-network", + "vi-vn-x-gft-local" + ], + "language": "vi-VN", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/wuu.json b/json/wuu.json new file mode 100644 index 0000000..564f0b1 --- /dev/null +++ b/json/wuu.json @@ -0,0 +1,25 @@ +{ + "language": "wuu", + "defaultRegion": "wuu-CN", + "testUtterance": "你好,我的名字是 {name},我是吴语配音。", + "voices": [ + { + "label": "Nannan", + "name": "Nannan", + "localizedName": "apple", + "language": "wuu-CN", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/json/yue.json b/json/yue.json new file mode 100644 index 0000000..4d85e63 --- /dev/null +++ b/json/yue.json @@ -0,0 +1,270 @@ +{ + "language": "yue", + "defaultRegion": "yue-HK", + "testUtterance": "你好,我叫 {name},係越中文聲。", + "voices": [ + { + "label": "HiuGaai", + "name": "Microsoft HiuGaai Online (Natural) - Chinese (Cantonese Traditional)", + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "HiuMaan", + "name": "Microsoft HiuMaan Online (Natural) - Chinese (Hong Kong SAR)", + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "WanLung", + "name": "Microsoft WanLung Online (Natural) - Chinese (Hong Kong SAR)", + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "male", + "quality": [ + "veryHigh" + ], + "rate": 1, + "pitchControl": false, + "browser": [ + "Edge" + ], + "preloaded": true + }, + { + "label": "Sinji", + "name": "Sinji", + "localizedName": "apple", + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "low", + "normal", + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Aasing", + "name": "Aasing", + "localizedName": "apple", + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "macOS", + "iOS", + "iPadOS" + ], + "preloaded": true + }, + { + "label": "Google 女聲", + "name": "Google 粤語(香港)", + "note": "This voice is pre-loaded in Chrome on desktop. Utterances that are longer than 14 seconds long can trigger a bug with this voice, check the notes in the project's README for more information.", + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "browser": [ + "ChromeDesktop" + ], + "preloaded": true + }, + { + "label": "Tracy", + "name": "Microsoft Tracy - Chinese (Traditional, Hong Kong S.A.R.)", + "language": "cmn-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "Danny", + "name": "Microsoft Danny - Chinese (Traditional, Hong Kong S.A.R.)", + "language": "cmn-HK", + "altLanguage": "zh-HK", + "gender": "male", + "quality": [ + "normal" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Windows" + ], + "preloaded": true + }, + { + "label": "女聲1", + "name": "Android Speech Recognition and Synthesis from Google yue-hk-x-jar-network", + "altNames": [ + "Chrome OS 粵語 1", + "Android Speech Recognition and Synthesis from Google yue-HK-x-jar-local", + "Android Speech Recognition and Synthesis from Google yue-HK-language" + ], + "nativeID": [ + "yue-hk-x-jar-network", + "yue-hk-x-jar-local" + ], + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "女聲2", + "name": "Android Speech Recognition and Synthesis from Google yue-hk-x-yuc-network", + "altNames": [ + "Chrome OS 粵語 2", + "Android Speech Recognition and Synthesis from Google yue-HK-x-yuc-local" + ], + "nativeID": [ + "yue-hk-x-yuc-network", + "yue-hk-x-yuc-local" + ], + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "female", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男聲1", + "name": "Android Speech Recognition and Synthesis from Google yue-hk-x-yud-network", + "altNames": [ + "Chrome OS 粵語 3", + "Android Speech Recognition and Synthesis from Google yue-HK-x-yud-local" + ], + "nativeID": [ + "yue-hk-x-yud-network", + "yue-hk-x-yud-local" + ], + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男聲2", + "name": "Android Speech Recognition and Synthesis from Google yue-hk-x-yue-network", + "altNames": [ + "Chrome OS 粵語 5", + "Android Speech Recognition and Synthesis from Google yue-HK-x-yue-local" + ], + "nativeID": [ + "yue-hk-x-yue-network", + "yue-hk-x-yue-local" + ], + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + }, + { + "label": "男聲3", + "name": "Android Speech Recognition and Synthesis from Google yue-hk-x-yuf-network", + "altNames": [ + "Chrome OS 粵語 5", + "Android Speech Recognition and Synthesis from Google yue-HK-x-yuf-local" + ], + "nativeID": [ + "yue-hk-x-yuf-network", + "yue-hk-x-yuf-local" + ], + "language": "yue-HK", + "altLanguage": "zh-HK", + "gender": "male", + "quality": [ + "high" + ], + "rate": 1, + "pitch": 1, + "os": [ + "Android", + "ChromeOS" + ], + "preloaded": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 3417967..a9e0a01 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,25 @@ "name": "@readium/speech", "version": "0.1.0-beta.1", "description": "Readium Speech is a TypeScript library for implementing a read aloud feature with Web technologies. It follows [best practices](https://github.com/HadrienGardeur/read-aloud-best-practices) gathered through interviews with members of the digital publishing industry.", - "main": "build/index.js", - "module": "build/index.js", + "main": "./build/index.cjs", + "module": "./build/index.js", + "types": "./build/index.d.ts", + "sideEffects": false, + "files": ["build", "json"], + "exports": { + ".": { + "types": "./build/index.d.ts", + "import": "./build/index.js", + "require": "./build/index.cjs" + } + }, "scripts": { - "test": "ava test/**/*.test.ts", + "test": "npm run build && ava test/WebSpeechVoiceManager.test.ts", "clean": "rimraf ./build", "types": "tsc -p tsconfig-types.json", "build": "vite build", "start": "node build/index.js", - "extract-json-data": "node script/extract-json.mjs", + "generate-voice-data": "node script/generate-voice-data.mjs", "serve": "http-server ./", "watch": "tsc -w" }, diff --git a/script/extract-json.mjs b/script/extract-json.mjs deleted file mode 100644 index 635681d..0000000 --- a/script/extract-json.mjs +++ /dev/null @@ -1,277 +0,0 @@ - -import { spawn } from 'node:child_process'; -import { rm } from 'node:fs/promises'; -import { join } from 'node:path'; -import { writeFileSync } from 'node:fs'; - -const repoUrl = 'https://github.com/HadrienGardeur/web-speech-recommended-voices.git'; -// const repoBranch = 'locales-for-voice-names'; -const repoBranch = 'main'; -const repoPath = 'script/web-speech-recommended-voices'; - -// Clone the repository -await new Promise((resolve, reject) => { - const cloneProcess = spawn('git', ['clone', '--depth=1', '--branch', repoBranch, repoUrl, repoPath]); - cloneProcess.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Git clone failed with code ${code}`)); - } - }); -}); - - -const jsonFiles = [ - 'ar.json', - 'bg.json', - 'bho.json', - 'bn.json', - 'ca.json', - 'cmn.json', - 'cs.json', - 'da.json', - 'de.json', - 'el.json', - 'en.json', - 'es.json', - 'eu.json', - 'fa.json', - 'fi.json', - 'fr.json', - 'gl.json', - 'he.json', - 'hi.json', - 'hr.json', - 'hu.json', - 'id.json', - 'it.json', - 'ja.json', - 'kn.json', - 'ko.json', - 'mr.json', - 'ms.json', - 'nb.json', - 'nl.json', - 'pl.json', - 'pt.json', - 'ro.json', - 'ru.json', - 'sk.json', - 'sl.json', - 'sv.json', - 'ta.json', - 'te.json', - 'th.json', - 'tr.json', - 'uk.json', - 'vi.json', - 'wuu.json', - 'yue.json', -]; - -const filters = [ - 'novelty.json', - 'veryLowQuality.json', -]; - -// const localizedNames = [ -// 'ca.json', -// 'da.json', -// 'de.json', -// 'en.json', -// 'es.json', -// 'fi.json', -// 'fr.json', -// 'it.json', -// 'nb.json', -// 'nl.json', -// 'pt.json', -// 'sv.json', -// ]; - -let novelty = []; -let veryLowQuality = []; - -// let localization = {}; - -let recommended = [] - -let quality = []; - -const defaultRegion = {}; - -// function generateLanguageRegionStrings(languages, regions) { - -// const result = {}; -// for (const languageCode in languages) { -// for (const regionCode in regions) { -// const bcp47Code = `${languageCode.toLowerCase()}-${regionCode.toLowerCase()}`; -// const translation = `(${languages[languageCode]} (${regions[regionCode]}))`; -// result[bcp47Code] = translation; -// } -// } - -// return result; -// } - -// function getAltName(languages) { - -// if (!languages.length) { -// return []; -// } - -// const result = []; -// for (const language of languages) { -// for (const langLocalization in localization) { - -// const v = localization[langLocalization][language.toLowerCase()]; -// if (v) { -// result.push(v); -// } -// } -// } - -// return result; -// } - -function filterBCP47(data) { - return data.filter((v) => /\w{2,3}-\w{2,3}/.test(v)); -} - -{ - const file = 'apple.json'; - const filePath = join(process.cwd(), repoPath, 'json', 'localizedNames', file); - try { - const { default: jsonData } = await import(filePath, { with: { type: 'json' } }); - console.log(`Imported localizedNames/${file}:` /*, jsonData*/); - - quality = jsonData.quality; - } catch (error) { - console.error(`Failed to import localizedNames/${file}: ${error.message}`); - } -} - -// for (const file of localizedNames) { -// const filePath = join(process.cwd(), repoPath, 'json', 'localizedNames', 'full', file); -// try { -// const { default: jsonData } = await import(filePath, { with: { type: 'json' } }); -// console.log(`Imported localizedNames/${file}:` /*, jsonData*/); - -// const lang = file.split(".")[0]; -// localization[lang] = generateLanguageRegionStrings(jsonData.languages, jsonData.regions); -// } catch (error) { -// console.error(`Failed to import localizedNames/${file}: ${error.message}`); -// } -// } -// // console.log(localization); - - -for (const file of jsonFiles) { - const filePath = join(process.cwd(), repoPath, 'json', file); - try { - const { default: jsonData } = await import(filePath, { with: { type: 'json' } }); - console.log(`Imported ${file}:` /*, jsonData*/); - - defaultRegion[jsonData.language] = jsonData.defaultRegion; - - const voices = jsonData.voices; - - for (const voice of voices) { - - recommended.push({ - label: voice.label, - name: voice.name || undefined, - altNames: voice.altNames || undefined, - language: voice.language || undefined, - gender: voice.gender || undefined, - age: voice.age || undefined, - quality: Array.isArray(voice.quality) ? voice.quality : [], - recommendedPitch: voice.pitchControl === false ? undefined : voice.pitch || 1, - recommendedRate: voice.pitchControl === false ? undefined : voice.rate || 1, - localizedName: voice.localizedName || "", - }); - } - - } catch (error) { - console.error(`Failed to import ${file}: ${error.message}`); - } -} - -for (const file of filters) { - const filePath = join(process.cwd(), repoPath, 'json', 'filters', file); - try { - const { default: jsonData } = await import(filePath, { with: { type: 'json' } }); - console.log(`Imported filters/${file}:` /*, jsonData*/); - - if (file.startsWith("novelty")) { - novelty = jsonData.voices.map(({ name, altNames }) => [name, ...(Array.isArray(altNames) ? altNames : [])]).flat(); - } - - if (file.startsWith("veryLow")) { - veryLowQuality = jsonData.voices.map(({ name, language, otherLanguages }) => { - // const languages = filterBCP47([language, otherLanguages].flat()); - // const altNamesGenerated = getAltName(languages); - // const altNames = altNamesGenerated.map((v) => name + " " + v); - - // return [name, altNames].flat(); - return name; - }).flat(); - } - } catch (error) { - console.error(`Failed to import filters/${file}: ${error.message}`); - } -} - - - -const content = ` -// https://github.com/readium/speech -// file script-generated by : npm run extract-json-data -// - -export const novelty = ${JSON.stringify(novelty)}; - -export const veryLowQuality = ${JSON.stringify(veryLowQuality)}; - -export type TGender = "female" | "male" | "nonbinary" -export type TQuality = "veryLow" | "low" | "normal" | "high" | "veryHigh"; - -export interface IRecommended { - label: string; - name: string; - altNames?: string[]; - language: string; - gender?: TGender | undefined; - age?: string | undefined; - quality: TQuality[]; - recommendedPitch?: number | undefined; - recommendedRate?: number | undefined; - localizedName: string; -}; - -export const recommended: Array = ${JSON.stringify(recommended)}; - -export const quality = ${JSON.stringify(quality)}; - -export const defaultRegion = ${JSON.stringify(defaultRegion)}; - -// EOF -`; - -const filePath = './src/data.gen.ts'; - -try { - writeFileSync(filePath, content); - console.log('File has been written successfully'); -} catch (err) { - console.error(err); -} - -// Delete the cloned repository -try { - await rm(repoPath, { recursive: true, force: true }); - console.log(`Deleted repository at ${repoPath}`); -} catch (error) { - console.error(`Failed to delete repository: ${error.message}`); -} diff --git a/src/WebSpeech/TmpNavigator.ts b/src/WebSpeech/TmpNavigator.ts index 1e58042..a2993a9 100644 --- a/src/WebSpeech/TmpNavigator.ts +++ b/src/WebSpeech/TmpNavigator.ts @@ -1,7 +1,7 @@ import { ReadiumSpeechPlaybackEngine } from "../engine"; import { ReadiumSpeechNavigator, ReadiumSpeechPlaybackEvent, ReadiumSpeechPlaybackState } from "../navigator"; import { ReadiumSpeechUtterance } from "../utterance"; -import { ReadiumSpeechVoice } from "../voices"; +import { ReadiumSpeechVoice } from "../voices/types"; import { WebSpeechEngine } from "./webSpeechEngine"; export class WebSpeechReadAloudNavigator implements ReadiumSpeechNavigator { @@ -99,7 +99,7 @@ export class WebSpeechReadAloudNavigator implements ReadiumSpeechNavigator { return this.engine.getAvailableVoices(); } - async setVoice(voice: ReadiumSpeechVoice | string): Promise { + setVoice(voice: ReadiumSpeechVoice | string): void { this.engine.setVoice(voice); } diff --git a/src/WebSpeech/WebSpeechVoiceManager.ts b/src/WebSpeech/WebSpeechVoiceManager.ts new file mode 100644 index 0000000..b078f27 --- /dev/null +++ b/src/WebSpeech/WebSpeechVoiceManager.ts @@ -0,0 +1,769 @@ +import { ReadiumSpeechVoice, TGender, TQuality } from "../voices/types"; +import { getTestUtterance, getVoices } from "../voices/languages"; +import { + isNoveltyVoice, + isVeryLowQualityVoice, + filterOutNoveltyVoices, + filterOutVeryLowQualityVoices +} from "../voices/filters"; + +import { extractLangRegionFromBCP47 } from "../utils/language"; + +/** + * Options for filtering voices + */ +interface VoiceFilterOptions { + language?: string | string[]; + gender?: TGender; + quality?: TQuality | TQuality[]; + offlineOnly?: boolean; + provider?: string; + excludeNovelty?: boolean; + excludeVeryLowQuality?: boolean; +} + +/** + * Language/Region information with voice count + */ +interface LanguageInfo { + code: string; + label: string; + count: number; +} + +/** + * Grouped voices + */ +interface VoiceGroup { + [key: string]: ReadiumSpeechVoice[]; +} + +/** + * Sort order for voices + */ +type SortOrder = "asc" | "desc"; + +/** + * Grouping criteria for voices + */ +type GroupBy = "language" | "gender" | "quality" | "region"; + +/** + * Sort options for voices + */ +interface SortOptions { + by: GroupBy | "name"; + order?: SortOrder; + preferredLanguages?: string[]; +} + +/** + * Manages Web Speech API voices with enhanced functionality + */ +export class WebSpeechVoiceManager { + private static instance: WebSpeechVoiceManager; + private static initializationPromise: Promise | null = null; + private voices: ReadiumSpeechVoice[] = []; + private browserVoices: SpeechSynthesisVoice[] = []; + private isInitialized = false; + + private constructor() { + if (typeof window === "undefined" || !window.speechSynthesis) { + throw new Error("Web Speech API is not available in this environment"); + } + } + + /** + * Initialize the voice manager + * @param options Configuration options for voice loading + * @param options.maxTime Maximum time in milliseconds to wait for voices to load (passed to getBrowserVoices) + * @param options.interval Interval in milliseconds between voice loading checks (passed to getBrowserVoices) + * @returns Promise that resolves with the WebSpeechVoiceManager instance + */ + static async initialize( + maxTimeout?: number, + interval?: number + ): Promise { + // If we already have an initialized instance, return it + if (WebSpeechVoiceManager.instance?.isInitialized) { + return WebSpeechVoiceManager.instance; + } + + // If initialization is in progress, return the existing promise + if (WebSpeechVoiceManager.initializationPromise) { + return WebSpeechVoiceManager.initializationPromise; + } + + // Create a new instance and store the initialization promise + WebSpeechVoiceManager.initializationPromise = (async () => { + try { + const instance = new WebSpeechVoiceManager(); + WebSpeechVoiceManager.instance = instance; + + instance.browserVoices = await instance.getBrowserVoices(maxTimeout, interval); + instance.voices = await instance.parseToReadiumSpeechVoices(instance.browserVoices); + instance.isInitialized = true; + + return instance; + } catch (error) { + // On error, clear the promise so initialization can be retried + WebSpeechVoiceManager.initializationPromise = null; + console.error("Failed to initialize WebSpeechVoiceManager:", error); + throw error; + } + })(); + + return WebSpeechVoiceManager.initializationPromise; + } + + /** + * Extract language and region from BCP47 language tag + * @param lang - The BCP47 language tag (e.g., "en-US", "zh-CN") + * @returns A tuple of [language, region] where language is lowercase and region is UPPERCASE + */ + static extractLangRegionFromBCP47(lang: string): [string, string | undefined] { + return extractLangRegionFromBCP47(lang); + } + + /** + * Get display name for a language code + * @private + */ + private static getLanguageDisplayName(code: string, localization?: string): string { + try { + // Use the code as-is, let Intl handle the display name + const displayName = new Intl.DisplayNames( + localization ? [localization] : [], + { type: "language", languageDisplay: "standard" } + ).of(code); + + return displayName || code.toUpperCase(); + } catch (e) { + return code.toUpperCase(); + } + } + + /** + * Remove duplicate voices, keeping the highest quality version of each voice + * @param voices Array of voices to remove duplicates from + * @returns Filtered array with duplicates removed, keeping only the highest quality versions + */ + private removeDuplicate(voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] { + const voiceMap = new Map(); + + for (const voice of voices) { + // Create a unique key based on voice identity (excluding quality) + const key = `${voice.voiceURI}_${voice.name}_${voice.language}`; + const existingVoice = voiceMap.get(key); + + // If we don't have this voice yet, or if the current voice is of higher quality + if (!existingVoice || this.getQualityValue(voice.quality) > this.getQualityValue(existingVoice.quality)) { + voiceMap.set(key, voice); + } + } + + return Array.from(voiceMap.values()); + } + + /** + * Get test utterance for a given language + * @param language - Language code (e.g., "en", "fr", "es") + * @returns Promise that resolves to the test utterance text + */ + getTestUtterance(language: string): string { + if (!language) return ""; + + // Try direct match + const utterance = getTestUtterance(language); + if (utterance) return utterance; + + // Try with base language as fallback + const [baseLang] = WebSpeechVoiceManager.extractLangRegionFromBCP47(language); + if (baseLang && baseLang !== language) { + const baseUtterance = getTestUtterance(baseLang); + if (baseUtterance) return baseUtterance; + } + + return ""; + } + + /** + * Get all voices matching the filter criteria + * @returns Promise that resolves to an array of filtered voices + */ + getVoices(options: VoiceFilterOptions = {}): ReadiumSpeechVoice[] { + if (!this.isInitialized) { + throw new Error("WebSpeechVoiceManager not initialized. Call initialize() first."); + } + + // Set default values for filter options + const filterOptions: VoiceFilterOptions = { + excludeNovelty: true, // Default to true to filter out novelty voices + excludeVeryLowQuality: true, // Default to true to filter out very low quality voices + ...options // Let explicit options override the defaults + }; + + return this.filterVoices([...this.voices], filterOptions); + } + + /** + * Get available languages with voice counts + * @param localization Optional BCP 47 language tag to use for language names + * @param filterOptions Optional filters to apply to voices before counting languages + */ + getLanguages(localization?: string, filterOptions?: VoiceFilterOptions): LanguageInfo[] { + if (!this.isInitialized) { + throw new Error("WebSpeechVoiceManager not initialized. Call initialize() first."); + } + + const languages = new Map(); + + // Apply filters if provided + const voicesToCount = filterOptions ? this.filterVoices([...this.voices], filterOptions) : this.voices; + + voicesToCount.forEach(voice => { + const langCode = voice.language; + const [baseLang] = WebSpeechVoiceManager.extractLangRegionFromBCP47(langCode); + + // Use the base language code for grouping (e.g., "en" for both "en-US" and "en-GB") + const key = baseLang; + const displayName = WebSpeechVoiceManager.getLanguageDisplayName(baseLang, localization); + + const entry = languages.get(key) || { count: 0, label: displayName, code: baseLang }; + languages.set(key, { ...entry, count: entry.count + 1 }); + }); + + // Convert to array and sort + return Array.from(languages.entries()) + .map(([_, { code, label, count }]) => ({ + code, + label, + count + })) + .sort((a, b) => a.label.localeCompare(b.label)); + } + + /** + * Get available regions with voice counts + */ + getRegions(localization?: string): LanguageInfo[] { + if (!this.isInitialized) { + throw new Error("WebSpeechVoiceManager not initialized. Call initialize() first."); + } + + const regions = new Map(); + + this.voices.forEach(voice => { + const [, region] = WebSpeechVoiceManager.extractLangRegionFromBCP47(voice.language); + if (region) { + const entry = regions.get(region) || { count: 0, label: voice.language }; + regions.set(region, { ...entry, count: entry.count + 1 }); + } + }); + + return Array.from(regions.entries()).map(([code, { count, label }]) => { + let displayName = label; + try { + const locale = localization || navigator.language; + displayName = new Intl.DisplayNames([locale], { type: "region" }).of(code) || label; + } catch (e) { + console.warn(`Failed to get display name for region ${code}`, e); + } + return { + code, + label: displayName, + count + }; + }); + } + + + /** + * Get the default voice for a language + * @param language The language code to get the default voice for (e.g., "en-US") + * @param voices Optional pre-filtered voices array to use instead of fetching voices + * @returns The default voice for the language, or null if no voices are available + */ + getDefaultVoice(language: string, voices?: ReadiumSpeechVoice[]): ReadiumSpeechVoice | null { + if (!language) return null; + + // Use provided voices or get filtered voices if not provided + const filteredVoices = voices || this.getVoices({ language }); + if (!filteredVoices.length) return null; + + // Try to find a default voice with high quality + const defaultVoice = filteredVoices.find(v => v.isDefault && this.getQualityValue(v.quality) >= 2); + if (defaultVoice) return defaultVoice; + + // Try to find any high quality voice + const highQualityVoice = filteredVoices.find(v => this.getQualityValue(v.quality) >= 2); + if (highQualityVoice) return highQualityVoice; + + // Fall back to the first available voice + return filteredVoices[0]; + } + + getBrowserVoices(maxTimeout = 10000, interval = 10): Promise { + const getVoices = () => window.speechSynthesis?.getVoices() || []; + + // Check if speechSynthesis is available + if (!window.speechSynthesis) { + return Promise.resolve([]); + } + + // Step 1: Try to load voices directly (best case scenario) + const voices = getVoices(); + if (Array.isArray(voices) && voices.length) return Promise.resolve(voices); + + return new Promise((resolve, reject) => { + // Calculate iterations from total timeout + let counter = Math.floor(maxTimeout / interval); + // Flag to ensure polling only starts once + let pollingStarted = false; + + // Polling function: Checks for voices periodically until counter expires + const startPolling = () => { + // Prevent multiple starts + if (pollingStarted) return; + pollingStarted = true; + + const tick = () => { + // Resolve with empty array if no voices found + if (counter < 1) return resolve([]); + --counter; + const voices = getVoices(); + // Resolve if voices loaded + if (Array.isArray(voices) && voices.length) return resolve(voices); + // Continue polling + setTimeout(tick, interval); + }; + // Initial start + setTimeout(tick, interval); + }; + + // Step 2: Use onvoiceschanged if available (prioritizes event over polling) + if (window.speechSynthesis.onvoiceschanged !== undefined) { + window.speechSynthesis.onvoiceschanged = () => { + const voices = getVoices(); + if (Array.isArray(voices) && voices.length) { + // Resolve immediately if voices are available + resolve(voices); + } else { + // Fallback to polling if event fires but no voices + startPolling(); + } + }; + } else { + // Step 3: No onvoiceschanged support, start polling directly + startPolling(); + } + + // Step 4: Overall safety timeout - resolve with empty array if nothing happens + setTimeout(() => resolve([]), maxTimeout); + }); + } + + /** + * Find a browser voice in the voice data + * @private + */ + private async findVoiceInData(browserVoice: SpeechSynthesisVoice): Promise { + if (!browserVoice?.lang) return undefined; + + try { + const browserLang = browserVoice.lang.toLowerCase(); + const [baseLang] = browserLang.split("-"); + + // Get voices for the base language + const voices = await getVoices(baseLang); + if (!voices || voices.length === 0) return undefined; + + // Try exact match first + const exactMatch = voices.find(voice => { + const voiceLang = voice.language?.toLowerCase(); + const voiceAltLang = voice.altLanguage?.toLowerCase(); + return voiceLang === browserLang || voiceAltLang === browserLang; + }); + + if (exactMatch) return exactMatch; + + // Then try base language match + return voices.find(voice => { + const voiceLang = voice.language?.toLowerCase(); + const voiceAltLang = voice.altLanguage?.toLowerCase(); + return (voiceLang?.startsWith(baseLang)) || + (voiceAltLang?.startsWith(baseLang)); + }); + } catch (error) { + console.error(`Error finding voice data for ${browserVoice.lang}:`, error); + return undefined; + } + } + + /** + * Convert SpeechSynthesisVoice array to ReadiumSpeechVoice array + * @private + */ + private parseToReadiumSpeechVoices(speechVoices: SpeechSynthesisVoice[]): ReadiumSpeechVoice[] { + const parseAndFormatBCP47 = (lang: string) => { + const speechVoiceLang = lang.replace("_", "-"); + if (/\w{2,3}-\w{2,3}/.test(speechVoiceLang)) { + return `${speechVoiceLang.split("-")[0].toLowerCase()}-${speechVoiceLang.split("-")[1].toUpperCase()}`; + } + return lang; + }; + + return speechVoices + .filter(voice => voice && voice.name && voice.lang) + .map(voice => { + const formattedLang = parseAndFormatBCP47(voice.lang); + const [baseLang] = formattedLang.split("-"); + + // Get voices for the specific language + const langVoices = getVoices(baseLang); + + // Try to find a matching voice by name + const jsonVoice = langVoices.find(v => + v.name === voice.name || + (v.altNames && v.altNames.some((name: string) => name === voice.name)) + ); + + if (jsonVoice) { + // Found a match in JSON data, merge with browser voice + return { + ...jsonVoice, + // Preserve browser-specific properties + voiceURI: voice.voiceURI, + isDefault: voice.default || false, + offlineAvailability: voice.localService || false, + // Use utility functions from filters.ts + isNovelty: isNoveltyVoice(voice.name, voice.voiceURI), + isLowQuality: isVeryLowQualityVoice(voice.name, jsonVoice.quality) + }; + } + + // No match found in JSON, create basic voice object + return { + label: voice.name, + name: voice.name, + voiceURI: voice.voiceURI, + language: formattedLang, + isDefault: voice.default || false, + offlineAvailability: voice.localService || false, + isNovelty: isNoveltyVoice(voice.name, voice.voiceURI), + isLowQuality: isVeryLowQualityVoice(voice.name) + }; + }); + } + + /** + * Convert an ReadiumSpeechVoice to a native SpeechSynthesisVoice + */ + convertToSpeechSynthesisVoice(voice: ReadiumSpeechVoice): SpeechSynthesisVoice | undefined { + if (!voice) return undefined; + + return this.browserVoices.find(v => + v.voiceURI === voice.voiceURI || + v.name === voice.name + ); + } + + /** + * Filter voices based on the provided options + */ + filterVoices(voices: ReadiumSpeechVoice[], options: VoiceFilterOptions): ReadiumSpeechVoice[] { + let result = [...voices]; + + if (options.language) { + const langs = Array.isArray(options.language) ? options.language : [options.language]; + + result = result.filter(voice => { + return langs.some(requestedLang => { + const reqLang = requestedLang.toLowerCase(); + const voiceLang = voice.language?.toLowerCase(); + const voiceAltLang = voice.altLanguage?.toLowerCase(); + + // Check direct matches first + if (voiceLang === reqLang || voiceAltLang === reqLang) { + return true; + } + + // Then check base language matches + const [reqBase] = reqLang.split("-"); + return (voiceLang && voiceLang.startsWith(reqBase)) || + (voiceAltLang && voiceAltLang.startsWith(reqBase)); + }); + }); + } + + if (options.gender) { + result = result.filter(v => v.gender === options.gender); + } + + if (options.quality) { + const qualities = Array.isArray(options.quality) ? options.quality : [options.quality]; + result = result.filter(v => + v.quality && v.quality.some(q => qualities.includes(q as any)) + ); + } + + if (options.offlineOnly) { + result = result.filter(v => v.offlineAvailability === true); + } + + if (options.provider) { + result = result.filter(v => + v.provider?.toLowerCase() === options.provider?.toLowerCase() + ); + } + + if (options.excludeNovelty) { + result = filterOutNoveltyVoices(result); + } + + if (options.excludeVeryLowQuality) { + result = filterOutVeryLowQualityVoices(result); + } + + return result; + } + + /** + * Filter out novelty voices + * @param voices Array of voices to filter + * @returns Filtered array with novelty voices removed + */ + filterOutNoveltyVoices(voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] { + return filterOutNoveltyVoices(voices); + } + + /** + * Filter out very low quality voices + * @param voices Array of voices to filter + * @returns Filtered array with very low quality voices removed + */ + filterOutVeryLowQualityVoices(voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] { + return filterOutVeryLowQualityVoices(voices); + } + + /** + * Get the numeric value for a quality level + * @private + */ + private getQualityValue(quality: TQuality | TQuality[] | undefined): number { + const qualityOrder: Record = { + "veryLow": 0, + "low": 1, + "normal": 2, + "high": 3, + "veryHigh": 4 + }; + + if (!quality) return 1; // "low" as fallback + + // Handle both single quality values and arrays + if (Array.isArray(quality)) { + return Math.max(...quality.map(q => qualityOrder[q] ?? 1)); + } + + // Fallback for single quality values + return qualityOrder[quality] ?? 1; + } + + /** + * Sort voices by the specified criteria + */ + sortVoices(voices: ReadiumSpeechVoice[], options: SortOptions): ReadiumSpeechVoice[] { + if (!voices?.length) return []; + + const result = [...voices]; + + switch (options.by) { + case "name": + result.sort((a, b) => + options.order === "desc" + ? b.name.localeCompare(a.name) + : a.name.localeCompare(b.name) + ); + break; + + case "language": + result.sort((a, b) => { + const [aLang, aRegion] = WebSpeechVoiceManager.extractLangRegionFromBCP47(a.language); + const [bLang, bRegion] = WebSpeechVoiceManager.extractLangRegionFromBCP47(b.language); + + // Get display names for both languages for comparison + const aDisplayName = WebSpeechVoiceManager.getLanguageDisplayName(aLang).toLowerCase(); + const bDisplayName = WebSpeechVoiceManager.getLanguageDisplayName(bLang).toLowerCase(); + + // If preferredLanguages is provided, prioritize them + if (options.preferredLanguages?.length) { + const aIndex = options.preferredLanguages.findIndex(prefLang => { + const [prefLangBase, prefRegion] = WebSpeechVoiceManager.extractLangRegionFromBCP47(prefLang); + // Match both language and region if specified in preferred language + return aLang === prefLangBase.toLowerCase() && + (!prefRegion || !aRegion || prefRegion === aRegion); + }); + + const bIndex = options.preferredLanguages.findIndex(prefLang => { + const [prefLangBase, prefRegion] = WebSpeechVoiceManager.extractLangRegionFromBCP47(prefLang); + return bLang === prefLangBase.toLowerCase() && + (!prefRegion || !bRegion || prefRegion === bRegion); + }); + + // If both languages are in preferred list, sort by their position + if (aIndex !== -1 && bIndex !== -1) { + // If same preferred language but different regions, sort by region if specified + if (aIndex === bIndex && aRegion && bRegion) { + return options.order === "desc" + ? bRegion.localeCompare(aRegion) + : aRegion.localeCompare(bRegion); + } + return options.order === "desc" ? bIndex - aIndex : aIndex - bIndex; + } + // If only one language is in preferred list, it comes first + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + } + + // Sort by display name for all languages + const compare = aDisplayName.localeCompare(bDisplayName); + + // If same display name, sort by region if available + if (compare === 0 && aRegion && bRegion) { + return options.order === "desc" + ? bRegion.localeCompare(aRegion) + : aRegion.localeCompare(bRegion); + } + + return options.order === "desc" ? -compare : compare; + }); + break; + + case "gender": + result.sort((a, b) => { + const aGender = a.gender || ""; + const bGender = b.gender || ""; + return options.order === "desc" + ? bGender.localeCompare(aGender) + : aGender.localeCompare(bGender); + }); + break; + + case "quality": + result.sort((a, b) => { + const aQuality = this.getQualityValue(a.quality); + const bQuality = this.getQualityValue(b.quality); + + return options.order === "desc" + ? bQuality - aQuality // desc: high quality first + : aQuality - bQuality; // asc: low quality first + }); + break; + + case "region": + result.sort((a, b) => { + const [aLang, aRegion = ""] = WebSpeechVoiceManager.extractLangRegionFromBCP47(a.language); + const [bLang, bRegion = ""] = WebSpeechVoiceManager.extractLangRegionFromBCP47(b.language); + + // If preferredLanguages is provided, prioritize exact matches first + if (options.preferredLanguages?.length) { + // Check for exact language-region matches first (e.g., "en-US" matches "en-US") + const aExactMatchIndex = options.preferredLanguages.findIndex(prefLang => { + const [prefLangBase, prefRegion] = WebSpeechVoiceManager.extractLangRegionFromBCP47(prefLang); + return aLang === prefLangBase.toLowerCase() && + aRegion === prefRegion?.toUpperCase(); + }); + + const bExactMatchIndex = options.preferredLanguages.findIndex(prefLang => { + const [prefLangBase, prefRegion] = WebSpeechVoiceManager.extractLangRegionFromBCP47(prefLang); + return bLang === prefLangBase.toLowerCase() && + bRegion === prefRegion?.toUpperCase(); + }); + + // If one has an exact match and the other doesn't, the exact match comes first + if (aExactMatchIndex !== -1 && bExactMatchIndex === -1) return -1; + if (aExactMatchIndex === -1 && bExactMatchIndex !== -1) return 1; + + // If both have exact matches, sort by their position in preferredLanguages + if (aExactMatchIndex !== -1 && bExactMatchIndex !== -1 && aExactMatchIndex !== bExactMatchIndex) { + return aExactMatchIndex - bExactMatchIndex; + } + + // Then check for language-only matches (e.g., "en" matches "en-US") + const aLangMatchIndex = options.preferredLanguages.findIndex(prefLang => { + const [prefLangBase] = WebSpeechVoiceManager.extractLangRegionFromBCP47(prefLang); + return aLang === prefLangBase.toLowerCase(); + }); + + const bLangMatchIndex = options.preferredLanguages.findIndex(prefLang => { + const [prefLangBase] = WebSpeechVoiceManager.extractLangRegionFromBCP47(prefLang); + return bLang === prefLangBase.toLowerCase(); + }); + + // If one has a language match and the other doesn't, the language match comes first + if (aLangMatchIndex !== -1 && bLangMatchIndex === -1) return -1; + if (aLangMatchIndex === -1 && bLangMatchIndex !== -1) return 1; + + // If both have language matches, sort by their position in preferredLanguages + if (aLangMatchIndex !== -1 && bLangMatchIndex !== -1 && aLangMatchIndex !== bLangMatchIndex) { + return aLangMatchIndex - bLangMatchIndex; + } + } + + // If no preferred language matches, sort alphabetically by region + const regionCompare = options.order === "desc" + ? bRegion.localeCompare(aRegion) + : aRegion.localeCompare(bRegion); + + // If regions are the same, sort by language + return regionCompare === 0 + ? aLang.localeCompare(bLang) + : regionCompare; + }); + break; + } + + return result; + } + + /** + * Group voices by the specified criteria + * @param voices Array of voices to group + * @param options Grouping options + * @returns Object with voice groups keyed by the grouping criteria + */ + groupVoices(voices: ReadiumSpeechVoice[], by: GroupBy): VoiceGroup { + const groups: VoiceGroup = {}; + + for (const voice of voices) { + let key = "Unknown"; + + switch (by) { + case "language": + key = WebSpeechVoiceManager.extractLangRegionFromBCP47(voice.language)[0]; + break; + + case "gender": + key = voice.gender || "unknown"; + break; + + case "quality": + key = voice.quality?.[0] || "unknown"; + break; + + case "region": + const [, region] = WebSpeechVoiceManager.extractLangRegionFromBCP47(voice.language); + key = region || "unknown"; + break; + } + + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(voice); + } + + return groups; + } +} diff --git a/src/WebSpeech/index.ts b/src/WebSpeech/index.ts new file mode 100644 index 0000000..4fa73db --- /dev/null +++ b/src/WebSpeech/index.ts @@ -0,0 +1,5 @@ +export * from "./WebSpeechVoiceManager"; +export * from "./webSpeechEngine"; +export * from "./webSpeechEngineProvider"; + +export * from "./TmpNavigator"; \ No newline at end of file diff --git a/src/WebSpeech/webSpeechEngine.ts b/src/WebSpeech/webSpeechEngine.ts index 45b629e..5fd6d21 100644 --- a/src/WebSpeech/webSpeechEngine.ts +++ b/src/WebSpeech/webSpeechEngine.ts @@ -1,8 +1,8 @@ import { ReadiumSpeechPlaybackEngine } from "../engine"; import { ReadiumSpeechPlaybackEvent, ReadiumSpeechPlaybackState } from "../navigator"; import { ReadiumSpeechUtterance } from "../utterance"; -import { ReadiumSpeechVoice } from "../voices"; -import { getSpeechSynthesisVoices, parseSpeechSynthesisVoices, filterOnLanguage } from "../voices"; +import { ReadiumSpeechVoice } from "../voices/types"; +import { WebSpeechVoiceManager } from "./WebSpeechVoiceManager"; import { detectFeatures, WebSpeechFeatures } from "../utils/features"; import { detectPlatformFeatures, WebSpeechPlatformPatches } from "../utils/patches"; @@ -18,8 +18,8 @@ export class WebSpeechEngine implements ReadiumSpeechPlaybackEngine { private playbackState: ReadiumSpeechPlaybackState = "idle"; private eventListeners: Map void)[]> = new Map(); + private voiceManager: WebSpeechVoiceManager | null = null; private voices: ReadiumSpeechVoice[] = []; - private browserVoices: SpeechSynthesisVoice[] = []; private defaultVoice: ReadiumSpeechVoice | null = null; // Enhanced properties for cross-browser compatibility @@ -73,7 +73,7 @@ export class WebSpeechEngine implements ReadiumSpeechPlaybackEngine { interval?: number; maxLengthExceeded?: "error" | "none" | "warn"; } = {}): Promise { - const { maxTimeout = 10000, interval = 10, maxLengthExceeded = "warn" } = options; + const { maxTimeout, interval, maxLengthExceeded = "warn" } = options; if (this.initialized) { return false; @@ -82,14 +82,12 @@ export class WebSpeechEngine implements ReadiumSpeechPlaybackEngine { this.maxLengthExceeded = maxLengthExceeded; try { - // Get and cache the browser's native voices - this.browserVoices = await getSpeechSynthesisVoices(maxTimeout, interval); - // Parse them into our internal format - this.voices = parseSpeechSynthesisVoices(this.browserVoices); + // Initialize voice manager with provided options and get voices + this.voiceManager = await WebSpeechVoiceManager.initialize(maxTimeout, interval); + this.voices = this.voiceManager.getVoices(); - // Try to find voice matching user's language - const langVoices = filterOnLanguage(this.voices); - this.defaultVoice = langVoices.length > 0 ? langVoices[0] : this.voices[0] || null; + // Find the best matching voice for the user's language using the optimized method + this.defaultVoice = this.voiceManager.getDefaultVoice(navigator.language || "en", this.voices); this.initialized = true; return true; @@ -155,18 +153,16 @@ export class WebSpeechEngine implements ReadiumSpeechPlaybackEngine { if (typeof voice === "string") { // Find voice by name or language - this.getAvailableVoices().then(voices => { - const foundVoice = voices.find(v => v.name === voice || v.language === voice); - if (foundVoice) { - this.currentVoice = foundVoice; - // Reset position when voice changes for fresh start with new voice - if (previousVoice && previousVoice.name !== foundVoice.name) { - this.currentUtteranceIndex = 0; - } - } else { - console.warn(`Voice "${voice}" not found`); + const foundVoice = this.voices.find(v => v.name === voice || v.language === voice); + if (foundVoice) { + this.currentVoice = foundVoice; + // Reset position when voice changes for fresh start with new voice + if (previousVoice && previousVoice.name !== foundVoice.name) { + this.currentUtteranceIndex = 0; } - }); + } else { + console.warn(`Voice "${voice}" not found`); + } } else { this.currentVoice = voice; // Reset position when voice changes for fresh start with new voice @@ -266,13 +262,9 @@ export class WebSpeechEngine implements ReadiumSpeechPlaybackEngine { // Enhanced voice selection with MSNatural detection const selectedVoice = this.getCurrentVoiceForUtterance(this.currentVoice); - if (selectedVoice) { - // Find the matching voice in our cached browser voices - // as converting ReadiumSpeechVoice to SpeechSynthesisVoice is not possible - const nativeVoice = this.browserVoices.find(v => - v.name === selectedVoice.name && - v.lang === (selectedVoice.__lang || selectedVoice.language) - ); + if (selectedVoice && this.voiceManager) { + // Convert ReadiumSpeechVoice to SpeechSynthesisVoice using the initialized voiceManager + const nativeVoice = this.voiceManager.convertToSpeechSynthesisVoice(selectedVoice); if (nativeVoice) { utterance.voice = nativeVoice; // Use the real native voice from cache diff --git a/src/WebSpeech/webSpeechEngineProvider.ts b/src/WebSpeech/webSpeechEngineProvider.ts index ad82fae..c5db3eb 100644 --- a/src/WebSpeech/webSpeechEngineProvider.ts +++ b/src/WebSpeech/webSpeechEngineProvider.ts @@ -1,6 +1,6 @@ import { ReadiumSpeechEngineProvider } from "../provider"; import { ReadiumSpeechPlaybackEngine } from "../engine"; -import { ReadiumSpeechVoice } from "../voices"; +import { ReadiumSpeechVoice } from "../voices/types"; import { WebSpeechEngine } from "./webSpeechEngine"; export class WebSpeechEngineProvider implements ReadiumSpeechEngineProvider { diff --git a/src/data.gen.ts b/src/data.gen.ts deleted file mode 100644 index e747d4b..0000000 --- a/src/data.gen.ts +++ /dev/null @@ -1,32 +0,0 @@ - -// https://github.com/readium/speech -// file script-generated by : npm run extract-json-data -// - -export const novelty = ["Albert","Bad News","Bahh","Bells","Boing","Bubbles","Cellos","Good News","Jester","Organ","Superstar","Trinoids","Whisper","Wobble","Zarvox"]; - -export const veryLowQuality = ["Eddy","Flo","Grandma","Grandpa","Jacques","Reed","Rocko","Sandy","Shelley","Fred","Junior","Kathy","Ralph","eSpeak Arabic","eSpeak Bulgarian","eSpeak Bengali","eSpeak Catalan","eSpeak Chinese (Mandarin, latin as English)","eSpeak Czech","eSpeak Danish","eSpeak German","eSpeak Greek","eSpeak Spanish (Spain)","eSpeak Estonian","eSpeak Finnish","eSpeak Gujarati","eSpeak Croatian","eSpeak Hungarian","eSpeak Indonesian","eSpeak Italian","eSpeak Kannada","eSpeak Korean","eSpeak Lithuanian","eSpeak Latvian","eSpeak Malayalm","eSpeak Marathi","eSpeak Malay","eSpeak Norwegian Bokmål","eSpeak Polish","eSpeak Portuguese (Brazil)","eSpeak Romanian","eSpeak Russian","eSpeak Slovak","eSpeak Slovenian","eSpeak Serbian","eSpeak Swedish","eSpeak Swahili","eSpeak Tamil","eSpeak Telugu","eSpeak Turkish","eSpeak Vietnamese (Northern)"]; - -export type TGender = "female" | "male" | "nonbinary" -export type TQuality = "veryLow" | "low" | "normal" | "high" | "veryHigh"; - -export interface IRecommended { - label: string; - name: string; - altNames?: string[]; - language: string; - gender?: TGender | undefined; - age?: string | undefined; - quality: TQuality[]; - recommendedPitch?: number | undefined; - recommendedRate?: number | undefined; - localizedName: string; -}; - -export const recommended: Array = [{"label":"Amina","name":"Microsoft Amina Online (Natural) - Arabic (Algeria)","language":"ar-DZ","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ismael","name":"Microsoft Ismael Online (Natural) - Arabic (Algeria)","language":"ar-DZ","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Laila","name":"Microsoft Laila Online (Natural) - Arabic (Bahrain)","language":"ar-BH","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ali","name":"Microsoft Ali Online (Natural) - Arabic (Bahrain)","language":"ar-BH","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Salma","name":"Microsoft Salma Online (Natural) - Arabic (Egypt)","language":"ar-EG","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Shakir","name":"Microsoft Shakir Online (Natural) - Arabic (Egypt)","language":"ar-EG","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Rana","name":"Microsoft Rana Online (Natural) - Arabic (Iraq)","language":"ar-IQ","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Bassel","name":"Microsoft Bassel Online (Natural) - Arabic (Iraq)","language":"ar-IQ","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Sana","name":"Microsoft Sana Online (Natural) - Arabic (Jordan)","language":"ar-JO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Taim","name":"Microsoft Taim Online (Natural) - Arabic (Jordan)","language":"ar-JO","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Noura","name":"Microsoft Noura Online (Natural) - Arabic (Kuwait)","language":"ar-KW","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Fahed","name":"Microsoft Fahed Online (Natural) - Arabic (Kuwait)","language":"ar-KW","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Layla","name":"Microsoft Layla Online (Natural) - Arabic (Lebanon)","language":"ar-LB","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Rami","name":"Microsoft Rami Online (Natural) - Arabic (Lebanon)","language":"ar-LB","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Iman","name":"Microsoft Iman Online (Natural) - Arabic (Libya)","language":"ar-LY","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Omar","name":"Microsoft Omar Online (Natural) - Arabic (Libya)","language":"ar-LY","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Mouna","name":"Microsoft Mouna Online (Natural) - Arabic (Morocco)","language":"ar-MA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Jamal","name":"Microsoft Jamal Online (Natural) - Arabic (Morocco)","language":"ar-MA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Aysha","name":"Microsoft Aysha Online (Natural) - Arabic (Oman)","language":"ar-OM","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Abdullah","name":"Microsoft Abdullah Online (Natural) - Arabic (Oman)","language":"ar-OM","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Amal","name":"Microsoft Amal Online (Natural) - Arabic (Qatar)","language":"ar-QA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Moaz","name":"Microsoft Moaz Online (Natural) - Arabic (Qatar)","language":"ar-QA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Zariyah","name":"Microsoft Zariyah Online (Natural) - Arabic (Saudi Arabia)","language":"ar-SA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Hamed","name":"Microsoft Hamed Online (Natural) - Arabic (Saudi Arabia)","language":"ar-SA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Amany","name":"Microsoft Amany Online (Natural) - Arabic (Syria)","language":"ar-SY","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Laith","name":"Microsoft Laith Online (Natural) - Arabic (Syria)","language":"ar-SY","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Reem","name":"Microsoft Reem Online (Natural) - Arabic (Tunisia)","language":"ar-TN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Hedi","name":"Microsoft Hedi Online (Natural) - Arabic (Tunisia)","language":"ar-TN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Fatima","name":"Microsoft Fatima Online (Natural) - Arabic (United Arab Emirates)","language":"ar-AE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Hamdan","name":"Microsoft Hamdan Online (Natural) - Arabic (United Arab Emirates)","language":"ar-AE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Maryam","name":"Microsoft Maryam Online (Natural) - Arabic (Yemen)","language":"ar-YE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Saleh","name":"Microsoft Saleh Online (Natural) - Arabic (Yemen)","language":"ar-YE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Mariam","name":"Mariam","language":"ar-001","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Apple Laila","name":"Laila","language":"ar-001","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tarik","name":"Tarik","language":"ar-001","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Majed","name":"Majed","language":"ar-001","gender":"male","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Hoda","name":"Microsoft Hoda - Arabic (Arabic )","language":"ar-EG","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Naayf","name":"Microsoft Naayf - Arabic (Saudi Arabia)","language":"ar-AS","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"صوت انثوي 1","name":"Android Speech Recognition and Synthesis from Google ar-xa-x-arc-network","altNames":["Android Speech Recognition and Synthesis from Google ar-xa-x-arc-local","Android Speech Recognition and Synthesis from Google ar-language"],"language":"ar","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"صوت انثوي 2","name":"Android Speech Recognition and Synthesis from Google ar-xa-x-arz-network","altNames":["Android Speech Recognition and Synthesis from Google ar-xa-x-arz-local"],"language":"ar","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"صوت ذكر 1","name":"Android Speech Recognition and Synthesis from Google ar-xa-x-ard-network","altNames":["Android Speech Recognition and Synthesis from Google ar-xa-x-ard-local"],"language":"ar","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"صوت ذكر 2","name":"Android Speech Recognition and Synthesis from Google ar-xa-x-are-network","altNames":["Android Speech Recognition and Synthesis from Google ar-xa-x-are-local"],"language":"ar","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kalina","name":"Microsoft Kalina Online (Natural) - Bulgarian (Bulgaria)","language":"bg-BG","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Borislav","name":"Microsoft Borislav Online (Natural) - Bulgarian (Bulgaria)","language":"bg-BG","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Daria","name":"Daria","language":"bg-BG","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Ivan","name":"Microsoft Ivan - Bulgarian (Bulgaria)","language":"bg-BG","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Женски глас","name":"Android Speech Recognition and Synthesis from Google bg-bg-x-ifk-network","altNames":["Android Speech Recognition and Synthesis from Google bg-bg-x-ifk-local","Android Speech Recognition and Synthesis from Google bg-bg-language"],"language":"bg-BG","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Jaya","name":"Jaya","language":"bho-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tanishaa","name":"Microsoft Tanishaa Online (Natural) - Bengali (India)","language":"bn-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Bashkar","name":"Microsoft Bashkar Online (Natural) - Bangla (India)","language":"bn-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Nabanita","name":"Microsoft Nabanita Online (Natural) - Bangla (Bangladesh)","language":"bn-BD","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Pradeep","name":"Microsoft Pradeep Online (Natural) - Bangla (Bangladesh)","language":"bn-BD","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Piya","name":"Piya","language":"bn-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"মহিলা কণ্ঠস্বর 1","name":"Android Speech Recognition and Synthesis from Google bn-in-x-bnf-network","altNames":["Android Speech Recognition and Synthesis from Google bn-in-x-bnf-local","Android Speech Recognition and Synthesis from Google bn-IN-language"],"language":"bn-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"মহিলা কণ্ঠস্বর 2","name":"Android Speech Recognition and Synthesis from Google bn-in-x-bnx-network","altNames":["Android Speech Recognition and Synthesis from Google bn-in-x-bnx-local"],"language":"bn-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"পুরুষ কন্ঠ 1","name":"Android Speech Recognition and Synthesis from Google bn-in-x-bin-network","altNames":["Android Speech Recognition and Synthesis from Google bn-in-x-bin-local"],"language":"bn-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"পুরুষ কন্ঠ 2","name":"Android Speech Recognition and Synthesis from Google bn-in-x-bnm-network","altNames":["Android Speech Recognition and Synthesis from Google bn-in-x-bnm-local"],"language":"bn-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"পুরুষ কন্ঠ","name":"Google বাংলা (Natural)","altNames":["Android Speech Recognition and Synthesis from Google bn-bd-x-ban-network","Chrome OS বাংলা","Android Speech Recognition and Synthesis from Google bn-bd-x-ban-local","Android Speech Recognition and Synthesis from Google bn-BD-language"],"language":"bn-BD","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Joana (Català)","name":"Microsoft Joana Online (Natural) - Catalan (Spain)","language":"ca-ES","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Enric (Català)","name":"Microsoft Enric Online (Natural) - Catalan (Spain)","language":"ca-ES","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Montse (Català)","name":"Montse","language":"ca-ES","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Pau (Valencià)","name":"Pau","language":"ca-ES-u-sd-esvc","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Jordi (Català)","name":"Jordi","language":"ca-ES","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Herena (Català)","name":"Microsoft Herena - Catalan (Spain)","language":"ca-ES","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Veu femenina catalana","name":"Android Speech Recognition and Synthesis from Google ca-es-x-caf-network","altNames":["Android Speech Recognition and Synthesis from Google ca-es-x-caf-local","Android Speech Recognition and Synthesis from Google ca-ES-language"],"language":"ca-ES","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Xiaoxiao","name":"Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)","language":"cmn-CN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Xiaoyi","name":"Microsoft Xiaoyi Online (Natural) - Chinese (Mainland)","language":"cmn-CN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Yunxi","name":"Microsoft Yunxi Online (Natural) - Chinese (Mainland)","language":"cmn-CN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Yunxia","name":"Microsoft Yunxia Online (Natural) - Chinese (Mainland)","language":"cmn-CN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Xiaobei","name":"Microsoft Xiaobei Online (Natural) - Chinese (Northeastern Mandarin)","language":"cmn-CN-liaoning","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Xiaoni","name":"Microsoft Xiaoni Online (Natural) - Chinese (Zhongyuan Mandarin Shaanxi)","language":"cmn-CN-shaanxi","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Yunjian","name":"Microsoft Yunjian Online (Natural) - Chinese (Mainland)","language":"cmn-CN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Yunyang","name":"Microsoft Yunyang Online (Natural) - Chinese (Mainland)","language":"cmn-CN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"HsiaoChen","name":"Microsoft HsiaoChen Online (Natural) - Chinese (Taiwan)","language":"cmn-TW","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"HsiaoYu","name":"Microsoft HsiaoYu Online (Natural) - Chinese (Taiwanese Mandarin)","language":"cmn-TW","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"YunJhe","name":"Microsoft YunJhe Online (Natural) - Chinese (Taiwan)","language":"cmn-TW","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Lilian","name":"Lilian","language":"cmn-CN","gender":"female","quality":["normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tiantian","name":"Tiantian","language":"cmn-CN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Shasha","name":"Shasha","language":"cmn-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Lili","name":"Lili","language":"cmn-CN","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Lisheng","name":"Lisheng","language":"cmn-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Lanlan","name":"Lanlan","language":"cmn-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Shanshan","name":"Shanshan","language":"cmn-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Yue","name":"Yue","language":"cmn-CN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tingting","name":"Tingting","language":"cmn-CN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Yu-shu","name":"Yu-shu","language":"cmn-CN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Dongmei","name":"Dongmei","language":"cmn-CN-liaoning","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Panpan","name":"Panpan","language":"cmn-CN-sichuan","gender":"female","quality":["low","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Meijia","name":"Meijia","language":"cmn-TW","gender":"female","quality":["low","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Han","name":"Han","language":"cmn-CN","gender":"male","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Bobo","name":"Bobo","language":"cmn-CN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Taotao","name":"Taotao","language":"cmn-CN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Binbin","name":"Binbin","language":"cmn-CN","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Li-Mu","name":"Li-Mu","language":"cmn-CN","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Haohao","name":"Haohao","language":"cmn-CN-shaanxi","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google 女声","name":"Google 普通话(中国大陆)","language":"cmn-CN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Google 女聲","name":"Google 國語(臺灣)","language":"cmn-TW","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Huihui","name":"Microsoft Huihui - Chinese (Simplified, PRC)","language":"cmn-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Yaoyao","name":"Microsoft Yaoyao - Chinese (Simplified, PRC)","language":"cmn-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kangkang","name":"Microsoft Kangkang - Chinese (Simplified, PRC)","language":"cmn-CN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Yating","name":"Microsoft Yating - Chinese (Traditional, Taiwan)","language":"cmn-TW","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Hanhan","name":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","language":"cmn-TW","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Zhiwei","name":"Microsoft Zhiwei - Chinese (Traditional, Taiwan)","language":"cmn-TW","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女声1","name":"Android Speech Recognition and Synthesis from Google cmn-CN-x-ccc-network","altNames":["Android Speech Recognition and Synthesis from Google cmn-CN-x-ccc-local","Android Speech Recognition and Synthesis from Google zh-CN-language"],"language":"cmn-CN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女声2","name":"Android Speech Recognition and Synthesis from Google cmn-CN-x-ssa-network","altNames":["Android Speech Recognition and Synthesis from Google cmn-CN-x-ssa-local"],"language":"cmn-CN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男声1","name":"Android Speech Recognition and Synthesis from Google cmn-CN-x-ccd-network","altNames":["Android Speech Recognition and Synthesis from Google cmn-CN-x-ccd-local"],"language":"cmn-CN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男声2","name":"Android Speech Recognition and Synthesis from Google cmn-CN-x-cce-network","altNames":["Android Speech Recognition and Synthesis from Google cmn-CN-x-cce-local"],"language":"cmn-CN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女聲","name":"Android Speech Recognition and Synthesis from Google cmn-TW-x-ctc-network","altNames":["Android Speech Recognition and Synthesis from Google cmn-TW-x-ctc-local","Android Speech Recognition and Synthesis from Google zh-TW-language"],"language":"cmn-TW","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男聲1","name":"Android Speech Recognition and Synthesis from Google cmn-TW-x-ctd-network","altNames":["Chrome OS 粵語 1","Android Speech Recognition and Synthesis from Google cmn-TW-x-ctd-local"],"language":"cmn-TW","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男聲2","name":"Android Speech Recognition and Synthesis from Google cmn-TW-x-cte-network","altNames":["Chrome OS 粵語 1","Android Speech Recognition and Synthesis from Google cmn-TW-x-cte-local"],"language":"cmn-CTW","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Vlasta","name":"Microsoft Vlasta Online (Natural) - Czech (Czech)","language":"cs-CZ","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Antonin","name":"Microsoft Antonin Online (Natural) - Czech (Czech)","language":"cs-CZ","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Zuzana","name":"Zuzana","language":"cs-CZ","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Iveta","name":"Iveta","language":"cs-CZ","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Jakub","name":"Microsoft Jakub - Czech (Czech)","language":"cs-CZ","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ženský hlas","name":"Google čeština (Natural)","altNames":["Android Speech Recognition and Synthesis from Google cs-cz-x-jfs-network","Chrome OS čeština","Android Speech Recognition and Synthesis from Google cs-cz-x-jfs-local","Android Speech Recognition and Synthesis from Google cs-CZ-language"],"language":"cs-CZ","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Christel","name":"Microsoft Christel Online (Natural) - Danish (Denmark)","language":"da-DK","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Jeppe","name":"Microsoft Jeppe Online (Natural) - Danish (Denmark)","language":"da-DK","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Sara","name":"Sara","language":"da-DK","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Magnus","name":"Magnus","language":"da-DK","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Helle","name":"Microsoft Helle - Danish (Denmark)","language":"da-DK","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvindestemme 1","name":"Google Dansk 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google da-dk-x-kfm-network","Chrome OS Dansk 1","Android Speech Recognition and Synthesis from Google da-dk-x-kfm-local","Android Speech Recognition and Synthesis from Google da-DK-language"],"language":"da-DK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvindestemme 2","name":"Google Dansk 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google da-dk-x-sfp-network","Chrome OS Dansk 3","Android Speech Recognition and Synthesis from Google da-dk-x-sfp-local"],"language":"da-DK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvindestemme 3","name":"Google Dansk 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google da-dk-x-vfb-network","Chrome OS Dansk 4","Android Speech Recognition and Synthesis from Google da-dk-x-vfb-local"],"language":"da-DK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mandsstemme","name":"Google Dansk 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google da-dk-x-nmm-network","Chrome OS Dansk 2","Android Speech Recognition and Synthesis from Google da-dk-x-nmm-local"],"language":"da-DK","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Seraphina","name":"Microsoft SeraphinaMultilingual Online (Natural) - German (Germany)","language":"de-DE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Amala","name":"Microsoft Amala Online (Natural) - German (Germany)","language":"de-DE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Katja","name":"Microsoft Katja Online (Natural) - German (Germany)","language":"de-DE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Florian","name":"Microsoft FlorianMultilingual Online (Natural) - German (Germany)","language":"de-DE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Conrad","name":"Microsoft Conrad Online (Natural) - German (Germany)","language":"de-DE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Killian","name":"Microsoft Killian Online (Natural) - German (Germany)","language":"de-DE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ingrid","name":"Microsoft Ingrid Online (Natural) - German (Austria)","language":"de-AT","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Jonas","name":"Microsoft Jonas Online (Natural) - German (Austria)","language":"de-AT","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Leni","name":"Microsoft Leni Online (Natural) - German (Switzerland)","language":"de-CH","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Jan","name":"Microsoft Jan Online (Natural) - German (Switzerland)","language":"de-CH","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Petra","name":"Petra","language":"de-DE","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Anna","name":"Anna","language":"de-DE","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Helena","name":"Helena","language":"de-DE","gender":"female","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Markus","name":"Markus","language":"de-DE","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Viktor","name":"Viktor","language":"de-DE","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Yannick","name":"Yannick","language":"de-DE","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Martin","name":"Martin","language":"de-DE","gender":"male","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google Deutsch","name":"Weibliche Google-Stimme (Deutschland)","language":"de-DE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Hedda","name":"Microsoft Hedda - German (Germany)","language":"de-DE","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Katja","name":"Microsoft Katja - German (Germany)","language":"de-DE","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Stefan","name":"Microsoft Stefan - German (Germany)","language":"de-DE","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Michael","name":"Microsoft Michael - German (Austria)","language":"de-AT","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Karsten","name":"Microsoft Karsten - German (Switzerland)","language":"de-CH","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Weibliche Stimme 1 (Deutschland)","name":"Google Deutsch 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google de-de-x-dea-network","Chrome OS Deutsch 2","Android Speech Recognition and Synthesis from Google de-de-x-dea-local","Android Speech Recognition and Synthesis from Google de-DE-language"],"language":"de-DE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Weibliche Stimme 2 (Deutschland)","name":"Google Deutsch 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google de-de-x-nfh-network","Chrome OS Deutsch 1","Android Speech Recognition and Synthesis from Google de-de-x-nfh-local"],"language":"de-DE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Männliche Stimme 1 (Deutschland)","name":"Google Deutsch 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google de-de-x-deb-network","Chrome OS Deutsch 3","Android Speech Recognition and Synthesis from Google de-de-x-deb-local"],"language":"de-DE","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Männliche Stimme 2 (Deutschland)","name":"Google Deutsch 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google de-de-x-deg-network","Chrome OS Deutsch 4","Android Speech Recognition and Synthesis from Google de-de-x-deg-local"],"language":"de-DE","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Athina","name":"Microsoft Athina Online (Natural) - Greek (Greece)","language":"el-GR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Nestoras","name":"Microsoft Nestoras Online (Natural) - Greek (Greece)","language":"el-GR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Melina","name":"Melina","language":"el-GR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Nikos","name":"Nikos","language":"el-GR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Stefanos","name":"Microsoft Stefanos - Greek (Greece)","language":"el-GR","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Γυναικεία φωνή","name":"Google Ελληνικά (Natural)","altNames":["Android Speech Recognition and Synthesis from Google el-gr-x-vfz-network","Chrome OS Ελληνικά","Android Speech Recognition and Synthesis from Google el-gr-x-vfz-local","Android Speech Recognition and Synthesis from Google el-GR-language"],"language":"el-GR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Emma","name":"Microsoft EmmaMultilingual Online (Natural) - English (United States)","altNames":["Microsoft Emma Online (Natural) - English (United States)"],"language":"en-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Microsoft Ava","name":"Microsoft AvaMultilingual Online (Natural) - English (United States)","altNames":["Microsoft Ava Online (Natural) - English (United States)"],"language":"en-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Jenny","name":"Microsoft Jenny Online (Natural) - English (United States)","language":"en-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Aria","name":"Microsoft Aria Online (Natural) - English (United States)","language":"en-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Michelle","name":"Microsoft Michelle Online (Natural) - English (United States)","language":"en-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ana","name":"Microsoft Ana Online (Natural) - English (United States)","language":"en-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Andrew","name":"Microsoft AndrewMultilingual Online (Natural) - English (United States)","altNames":["Microsoft Andrew Online (Natural) - English (United States)"],"language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Brian","name":"Microsoft BrianMultilingual Online (Natural) - English (United States)","altNames":["Microsoft Brian Online (Natural) - English (United States)"],"language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Guy","name":"Microsoft Guy Online (Natural) - English (United States)","language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Eric","name":"Microsoft Eric Online (Natural) - English (United States)","language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Steffan","name":"Microsoft Steffan Online (Natural) - English (United States)","language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Christopher","name":"Microsoft Christopher Online (Natural) - English (United States)","language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Roger","name":"Microsoft Roger Online (Natural) - English (United States)","language":"en-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Sonia","name":"Microsoft Sonia Online (Natural) - English (United Kingdom)","language":"en-GB","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Libby","name":"Microsoft Libby Online (Natural) - English (United Kingdom)","language":"en-GB","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Maisie","name":"Microsoft Maisie Online (Natural) - English (United Kingdom)","language":"en-GB","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ryan","name":"Microsoft Ryan Online (Natural) - English (United Kingdom)","language":"en-GB","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Thomas","name":"Microsoft Thomas Online (Natural) - English (United Kingdom)","language":"en-GB","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Natasha","name":"Microsoft Natasha Online (Natural) - English (Australia)","language":"en-AU","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Hayley","name":"Microsoft Hayley Online - English (Australia)","language":"en-AU","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"William","name":"Microsoft William Online (Natural) - English (Australia)","language":"en-AU","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Clara","name":"Microsoft Clara Online (Natural) - English (Canada)","language":"en-CA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Heather","name":"Microsoft Heather Online - English (Canada)","language":"en-CA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Liam","name":"Microsoft Liam Online (Natural) - English (Canada)","language":"en-CA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Neerja","name":"Microsoft Neerja Online (Natural) - English (India)","altNames":["Microsoft Neerja Online (Natural) - English (India) (Preview)"],"language":"en-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Prabhat","name":"Microsoft Prabhat Online (Natural) - English (India)","language":"en-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Emily","name":"Microsoft Emily Online (Natural) - English (Ireland)","language":"en-IE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Connor","name":"Microsoft Connor Online (Natural) - English (Ireland)","language":"en-IE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Leah","name":"Microsoft Leah Online (Natural) - English (South Africa)","language":"en-ZA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Luke","name":"Microsoft Luke Online (Natural) - English (South Africa)","language":"en-ZA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Yan","name":"Microsoft Yan Online (Natural) - English (Hongkong)","language":"en-HK","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Sam","name":"Microsoft Sam Online (Natural) - English (Hongkong)","language":"en-HK","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Asilia","name":"Microsoft Asilia Online (Natural) - English (Kenya)","language":"en-KE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Chilemba","name":"Microsoft Chilemba Online (Natural) - English (Kenya)","language":"en-KE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Molly","name":"Microsoft Molly Online (Natural) - English (New Zealand)","language":"en-NZ","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Mitchell","name":"Microsoft Mitchell Online (Natural) - English (New Zealand)","language":"en-NZ","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ezinne","name":"Microsoft Ezinne Online (Natural) - English (Nigeria)","language":"en-NG","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Abeo","name":"Microsoft Abeo Online (Natural) - English (Nigeria)","language":"en-NG","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Luna","name":"Microsoft Luna Online (Natural) - English (Singapore)","language":"en-SG","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Wayne","name":"Microsoft Wayne Online (Natural) - English (Singapore)","language":"en-SG","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Imani","name":"Microsoft Imani Online (Natural) - English (Tanzania)","language":"en-TZ","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Elimu","name":"Microsoft Elimu Online (Natural) - English (Tanzania)","language":"en-TZ","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Apple Ava","name":"Ava","language":"en-US","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Zoe","name":"Zoe","language":"en-US","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Allison","name":"Allison","language":"en-US","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Nicky","name":"Nicky","language":"en-US","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Samantha","name":"Samantha","language":"en-US","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Joelle","name":"Joelle","language":"en-US","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Evan","name":"Evan","language":"en-US","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Nathan","name":"Nathan","language":"en-US","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tom","name":"Tom","language":"en-US","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Alex","name":"Alex","language":"en-US","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Aaron","name":"Aaron","language":"en-US","gender":"male","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Kate","name":"Kate","language":"en-GB","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Stephanie","name":"Stephanie","language":"en-GB","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Serena","name":"Serena","language":"en-GB","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Martha","name":"Martha","language":"en-GB","gender":"female","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Jamie","name":"Jamie","language":"en-GB","gender":"male","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Oliver","name":"Oliver","language":"en-GB","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Daniel","name":"Daniel","language":"en-GB","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Arthur","name":"Arthur","language":"en-GB","gender":"male","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Matilda","name":"Matilda","language":"en-AU","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Karen","name":"Karen","language":"en-AU","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Catherine","name":"Catherine","language":"en-AU","gender":"female","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Lee","name":"Lee","language":"en-AU","gender":"male","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Gordon","name":"Gordon","language":"en-AU","gender":"male","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Isha","name":"Isha","language":"en-IN","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Sangeeta","name":"Sangeeta","language":"en-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Rishi","name":"Rishi","language":"en-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Moira","name":"Moira","language":"en-IE","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tessa","name":"Tessa","language":"en-ZA","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Fiona","name":"Fiona","language":"en-GB-u-sd-gbsct","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Female Google voice (US)","name":"Google US English","language":"en-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female Google voice (UK)","name":"Google UK English Female","language":"en-GB","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male Google voice (UK)","name":"Google UK English Male","language":"en-GB","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Zira","name":"Microsoft Zira - English (United States)","language":"en-US","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"David","name":"Microsoft David - English (United States)","language":"en-US","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mark","name":"Microsoft Mark - English (United States)","language":"en-US","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Hazel","name":"Microsoft Hazel - English (Great Britain)","language":"en-GB","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Susan","name":"Microsoft Susan - English (Great Britain)","language":"en-GB","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"George","name":"Microsoft George - English (Great Britain)","language":"en-GB","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Catherine","name":"Microsoft Catherine - English (Austalia)","language":"en-AU","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"James","name":"Microsoft Richard - English (Australia)","language":"en-AU","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Linda","name":"Microsoft Linda - English (Canada)","language":"en-CA","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Richard","name":"Microsoft Richard - English (Canada)","language":"en-CA","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Heera","name":"Microsoft Heera - English (India)","language":"en-IN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ravi","name":"Microsoft Ravi - English (India)","language":"en-IN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Sean","name":"Microsoft Sean - English (Ireland)","language":"en-IE","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 1 (US)","name":"Google US English 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-tpc-network","Chrome OS US English 5","Android Speech Recognition and Synthesis from Google en-us-x-tpc-local","Android Speech Recognition and Synthesis from Google en-US-language"],"language":"en-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 2 (US)","name":"Google US English 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-iob-network","Chrome OS US English 1","Android Speech Recognition and Synthesis from Google en-us-x-iob-local"],"language":"en-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 3 (US)","name":"Google US English 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-iog-network","Chrome OS US English 2","Android Speech Recognition and Synthesis from Google en-us-x-iog-local"],"language":"en-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 4 (US)","name":"Google US English 7 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-tpf-network","Chrome OS US English 7","Android Speech Recognition and Synthesis from Google en-us-x-tpf-local"],"language":"en-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 5 (US)","name":"Android Speech Recognition and Synthesis from Google en-us-x-sfg-network","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-sfg-local"],"language":"en-US","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 6 (US)","name":"Chrome OS US English 8","language":"en-US","gender":"female","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 1 (US)","name":"Google US English 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-iom-network","Chrome OS US English 4","Android Speech Recognition and Synthesis from Google en-us-x-iom-local"],"language":"en-US","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 2 (US)","name":"Google US English 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-iol-network","Chrome OS US English 3","Android Speech Recognition and Synthesis from Google en-us-x-iol-local"],"language":"en-US","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 3 (US)","name":"Google US English 6 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-us-x-tpd-network","Chrome OS US English 6","Android Speech Recognition and Synthesis from Google en-us-x-tpd-local"],"language":"en-US","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 1 (UK)","name":"Google UK English 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-gb-x-gba-network","Chrome OS UK English 2","Android Speech Recognition and Synthesis from Google en-gb-x-gba-local","Android Speech Recognition and Synthesis from Google en-GB-language"],"language":"en-GB","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 2 (UK)","name":"Google UK English 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-gb-x-gbc-network","Chrome OS UK English 4","Android Speech Recognition and Synthesis from Google en-gb-x-gbc-local"],"language":"en-GB","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 3 (UK)","name":"Google UK English 6 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-gb-x-gbg-network","Chrome OS UK English 6","Android Speech Recognition and Synthesis from Google en-gb-x-gbg-local"],"language":"en-GB","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 4 (UK)","name":"Chrome OS UK English 7","language":"en-GB","gender":"female","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 1 (UK)","name":"Google UK English 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-gb-x-rjs-network","Chrome OS UK English 1","Android Speech Recognition and Synthesis from Google en-gb-x-rjs-local"],"language":"en-GB","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 2 (UK)","name":"Google UK English 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-gb-x-gbb-network","Chrome OS UK English 3","Android Speech Recognition and Synthesis from Google en-gb-x-gbb-local"],"language":"en-GB","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 3 (UK)","name":"Google UK English 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-gb-x-gbd-network","Chrome OS UK English 5","Android Speech Recognition and Synthesis from Google en-gb-x-gbd-local"],"language":"en-GB","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 1 (Australia)","name":"Google Australian English 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-au-x-aua-network","Chrome OS Australian English 1","Android Speech Recognition and Synthesis from Google en-au-x-aua-local","Android Speech Recognition and Synthesis from Google en-AU-language"],"language":"en-AU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 2 (Australia)","name":"Google Australian English 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-au-x-auc-network","Chrome OS Australian English 3","Android Speech Recognition and Synthesis from Google en-au-x-auc-local"],"language":"en-AU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 1 (Australia)","name":"Google Australian English 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-au-x-aub-network","Chrome OS Australian English 2","Android Speech Recognition and Synthesis from Google en-au-x-aub-local"],"language":"en-AU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 2 (Australia)","name":"Google Australian English 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google en-au-x-aud-network","Chrome OS Australian English 4","Android Speech Recognition and Synthesis from Google en-au-x-aud-local"],"language":"en-AU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 3 (Australia)","name":"Chrome OS Australian English 5","language":"en-AU","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 1 (India)","name":"Android Speech Recognition and Synthesis from Google en-in-x-ena-network","altNames":["Android Speech Recognition and Synthesis from Google en-in-x-ena-local","Android Speech Recognition and Synthesis from Google en-IN-language"],"language":"en-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Female voice 2 (India)","name":"Android Speech Recognition and Synthesis from Google en-in-x-enc-network","altNames":["Android Speech Recognition and Synthesis from Google en-in-x-enc-local"],"language":"en-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 1 (India)","name":"Android Speech Recognition and Synthesis from Google en-in-x-end-network","altNames":["Android Speech Recognition and Synthesis from Google en-in-x-end-local"],"language":"en-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Male voice 2 (India)","name":"Android Speech Recognition and Synthesis from Google en-in-x-ene-network","altNames":["Android Speech Recognition and Synthesis from Google en-in-x-ene-local"],"language":"en-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Elvira","name":"Microsoft Elvira Online (Natural) - Spanish (Spain)","language":"es-ES","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Alvaro","name":"Microsoft Alvaro Online (Natural) - Spanish (Spain)","language":"es-ES","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Dalia","name":"Microsoft Dalia Online (Natural) - Spanish (Mexico)","language":"es-MX","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Microsoft Jorge","name":"Microsoft Jorge Online (Natural) - Spanish (Mexico)","language":"es-MX","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Elena","name":"Microsoft Elena Online (Natural) - Spanish (Argentina)","language":"es-AR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Tomas","name":"Microsoft Tomas Online (Natural) - Spanish (Argentina)","language":"es-AR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Sofia","name":"Microsoft Sofia Online (Natural) - Spanish (Bolivia)","language":"es-BO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Marcelo","name":"Microsoft Marcelo Online (Natural) - Spanish (Bolivia)","language":"es-BO","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Catalina","name":"Microsoft Catalina Online (Natural) - Spanish (Chile)","language":"es-CL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Lorenzo","name":"Microsoft Lorenzo Online (Natural) - Spanish (Chile)","language":"es-CL","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ximena","name":"Microsoft Ximena Online (Natural) - Spanish (Colombia)","language":"es-CO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Salome","name":"Microsoft Salome Online (Natural) - Spanish (Colombia)","language":"es-CO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Gonzalo","name":"Microsoft Gonzalo Online (Natural) - Spanish (Colombia)","language":"es-CO","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Maria","name":"Microsoft Maria Online (Natural) - Spanish (Costa Rica)","language":"es-CR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Juan","name":"Microsoft Juan Online (Natural) - Spanish (Costa Rica)","language":"es-CR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Belkys","name":"Microsoft Belkys Online (Natural) - Spanish (Cuba)","language":"es-CU","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Manuel","name":"Microsoft Manuel Online (Natural) - Spanish (Cuba)","language":"es-CU","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Andrea","name":"Microsoft Andrea Online (Natural) - Spanish (Ecuador)","language":"es-EC","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Luis","name":"Microsoft Luis Online (Natural) - Spanish (Ecuador)","language":"es-EC","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Lorena","name":"Microsoft Lorena Online (Natural) - Spanish (El Salvador)","language":"es-SV","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Rodrigo","name":"Microsoft Rodrigo Online (Natural) - Spanish (El Salvador)","language":"es-SV","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Paloma","name":"Microsoft Paloma Online (Natural) - Spanish (United States)","language":"es-US","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Alonso","name":"Microsoft Alonso Online (Natural) - Spanish (United States)","language":"es-US","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Marta","name":"Microsoft Marta Online (Natural) - Spanish (Guatemala)","language":"es-GT","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Andres","name":"Microsoft Andres Online (Natural) - Spanish (Guatemala)","language":"es-GT","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Teresa","name":"Microsoft Teresa Online (Natural) - Spanish (Equatorial Guinea)","language":"es-GQ","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Javier","name":"Microsoft Javier Online (Natural) - Spanish (Equatorial Guinea)","language":"es-GQ","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Karla","name":"Microsoft Karla Online (Natural) - Spanish (Honduras)","language":"es-HN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Carlos","name":"Microsoft Carlos Online (Natural) - Spanish (Honduras)","language":"es-HN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Yolanda","name":"Microsoft Yolanda Online (Natural) - Spanish (Nicaragua)","language":"es-NI","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Federico","name":"Microsoft Federico Online (Natural) - Spanish (Nicaragua)","language":"es-NI","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Margarita","name":"Microsoft Margarita Online (Natural) - Spanish (Panama)","language":"es-PA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Roberto","name":"Microsoft Roberto Online (Natural) - Spanish (Panama)","language":"es-PA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Tania","name":"Microsoft Tania Online (Natural) - Spanish (Paraguay)","language":"es-PY","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Mario","name":"Microsoft Mario Online (Natural) - Spanish (Paraguay)","language":"es-PY","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Camila","name":"Microsoft Camila Online (Natural) - Spanish (Peru)","language":"es-PE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Alex","name":"Microsoft Alex Online (Natural) - Spanish (Peru)","language":"es-PE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Karina","name":"Microsoft Karina Online (Natural) - Spanish (Puerto Rico)","language":"es-PR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Victor","name":"Microsoft Victor Online (Natural) - Spanish (Puerto Rico)","language":"es-PR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ramona","name":"Microsoft Ramona Online (Natural) - Spanish (Dominican Republic)","language":"es-DO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Emilio","name":"Microsoft Emilio Online (Natural) - Spanish (Dominican Republic)","language":"es-DO","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Valentina","name":"Microsoft Valentina Online (Natural) - Spanish (Uruguay)","language":"es-UY","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Mateo","name":"Microsoft Mateo Online (Natural) - Spanish (Uruguay)","language":"es-UY","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Paola","name":"Microsoft Paola Online (Natural) - Spanish (Venezuela)","language":"es-VE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Sebastian","name":"Microsoft Sebastian Online (Natural) - Spanish (Venezuela)","language":"es-VE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Marisol","name":"Marisol","language":"es-ES","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Mónica","name":"Mónica","language":"es-ES","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Apple Jorge","name":"Jorge","language":"es-ES","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Angelica","name":"Angelica","language":"es-MX","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Paulina","name":"Paulina","language":"es-MX","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Juan","name":"Juan","language":"es-MX","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Isabela","name":"Isabela","language":"es-AR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Diego","name":"Diego","language":"es-AR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Francisca","name":"Francisca","language":"es-CL","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Soledad","name":"Soledad","language":"es-CO","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Jimena","name":"Jimena","language":"es-CO","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Carlos","name":"Carlos","language":"es-CO","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Voz Google masculina (España)","name":"Google español","language":"es-ES","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz Google femenina (Estados Unidos)","name":"Google español de Estados Unidos","language":"es-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Helena","name":"Microsoft Helena - Spanish (Spain)","language":"es-ES","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Laura","name":"Microsoft Laura - Spanish (Spain)","language":"es-ES","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Pablo","name":"Microsoft Pablo - Spanish (Spain)","language":"es-ES","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Sabina","name":"Microsoft Sabina - Spanish (Mexico)","language":"es-MX","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Raul","name":"Microsoft Raul - Spanish (Mexico)","language":"es-MX","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz femenina 1 (España)","name":"Google español 4 (Natural)","altNames":["Chrome OS español 4","Android Speech Recognition and Synthesis from Google es-es-x-eee-local","Android Speech Recognition and Synthesis from Google es-ES-language"],"language":"es-ES","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz femenina 2 (España)","name":"Google español 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-es-x-eea-network","Chrome OS español 1","Android Speech Recognition and Synthesis from Google es-es-x-eea-local"],"language":"es-ES","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz femenina 3 (España)","name":"Google español 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-es-x-eec-network","Chrome OS español 2","Android Speech Recognition and Synthesis from Google es-es-x-eec-local"],"language":"es-ES","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina 1 (España)","name":"Google español 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-es-x-eed-network","Chrome OS español 3","Android Speech Recognition and Synthesis from Google es-es-x-eed-local"],"language":"es-ES","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina 2 (España)","name":"Google español 5 (Natural)","altNames":["Chrome OS español 5","Android Speech Recognition and Synthesis from Google es-es-x-eef-local"],"language":"es-ES","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz femenina 1 (Estados Unidos)","name":"Google español de Estados Unidos 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-us-x-esc-network","Chrome OS español de Estados Unidos","Android Speech Recognition and Synthesis from Google es-us-x-esc-local","Android Speech Recognition and Synthesis from Google es-US-language"],"language":"es-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz femenina 2 (Estados Unidos)","name":"Google español de Estados Unidos 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-us-x-sfb-network","Android Speech Recognition and Synthesis from Google es-us-x-sfb-local"],"language":"es-US","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina 1 (Estados Unidos)","name":"Google español de Estados Unidos 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-us-x-esd-network","Android Speech Recognition and Synthesis from Google es-us-x-esd-local"],"language":"es-US","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina 2 (Estados Unidos)","name":"Google español de Estados Unidos 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google es-us-x-esf-network","Android Speech Recognition and Synthesis from Google es-us-x-esf-local"],"language":"es-US","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Miren","name":"Miren","language":"eu-ES","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Dilara","name":"Microsoft Dilara Online (Natural) - Persian (Iran)","language":"fa-IR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Farid","name":"Microsoft Farid Online (Natural) - Persian (Iran)","language":"fa-IR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Dariush","name":"Dariush","language":"fa-IR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Noora","name":"Microsoft Noora Online (Natural) - Finnish (Finland)","language":"fi-FI","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Harri","name":"Microsoft Harri Online (Natural) - Finnish (Finland)","language":"fi-FI","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Satu","name":"Satu","language":"fi-FI","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Onni","name":"Onni","language":"fi-FI","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Heidi","name":"Microsoft Heidi - Finnish (Finland)","language":"fi-FI","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suomalainen naisääni","name":"Google Suomi (Natural)","altNames":["Android Speech Recognition and Synthesis from Google fi-fi-x-afi-network","Chrome OS Suomi","Android Speech Recognition and Synthesis from Google fi-fi-x-afi-local","Android Speech Recognition and Synthesis from Google fi-FI-language"],"language":"fi-FI","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Vivienne","name":"Microsoft VivienneMultilingual Online (Natural) - French (France)","language":"fr-FR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Denise","name":"Microsoft Denise Online (Natural) - French (France)","language":"fr-FR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Charline","name":"Microsoft Charline Online (Natural) - French (Belgium)","language":"fr-BE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ariane","name":"Microsoft Ariane Online (Natural) - French (Switzerland)","language":"fr-CH","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Eloise","name":"Microsoft Eloise Online (Natural) - French (France)","language":"fr-FR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Remy","name":"Microsoft RemyMultilingual Online (Natural) - French (France)","language":"fr-FR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Henri","name":"Microsoft Henri Online (Natural) - French (France)","language":"fr-FR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Gerard","name":"Microsoft Gerard Online (Natural) - French (Belgium)","language":"fr-BE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Fabrice","name":"Microsoft Fabrice Online (Natural) - French (Switzerland)","language":"fr-CH","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Sylvie","name":"Microsoft Sylvie Online (Natural) - French (Canada)","language":"fr-CA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Antoine","name":"Microsoft Antoine Online (Natural) - French (Canada)","language":"fr-CA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Jean","name":"Microsoft Jean Online (Natural) - French (Canada)","language":"fr-CA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Thierry","name":"Microsoft Thierry Online (Natural) - French (Canada)","language":"fr-CA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Audrey","name":"Audrey","language":"fr-FR","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Aurélie","name":"Aurélie","language":"fr-FR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":0.9,"localizedName":"apple"},{"label":"Marie","name":"Marie","language":"fr-FR","gender":"female","quality":["low"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Thomas","name":"Thomas","language":"fr-FR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Aude","name":"Aude","language":"fr-BE","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Chantal","name":"Chantal","language":"fr-CA","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Amélie","name":"Amélie","language":"fr-CA","gender":"female","quality":["low","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Nicolas","name":"Nicolas","language":"fr-CA","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Voix Google féminine (France)","name":"Google français","language":"fr-FR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Julie","name":"Microsoft Julie - French (France)","language":"fr-FR","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Hortence","name":"Microsoft Hortence - French (France)","language":"fr-FR","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Paul","name":"Microsoft Paul - French (France)","language":"fr-FR","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Caroline","name":"Microsoft Caroline - French (Canada)","language":"fr-CA","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Claude","name":"Microsoft Claude - French (Canada)","language":"fr-CA","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Guillaume","name":"Microsoft Claude - French (Switzerland)","language":"fr-CH","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix féminine 1 (France)","name":"Google français 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google fr-fr-x-frc-network","Chrome OS français 4","Android Speech Recognition and Synthesis from Google fr-fr-x-frc-local","Android Speech Recognition and Synthesis from Google fr-FR-language"],"language":"fr-FR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix féminine 2 (France)","name":"Google français 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google fr-fr-x-fra-network","Chrome OS français 2","Android Speech Recognition and Synthesis from Google fr-fr-x-fra-local"],"language":"fr-FR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix féminine 3 (France)","name":"Google français 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google fr-fr-x-vlf-network","Chrome OS français 1","Android Speech Recognition and Synthesis from Google fr-fr-x-vlf-local"],"language":"fr-FR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix masculine 1 (France)","name":"Google français 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google fr-fr-x-frd-network","Chrome OS français 5","Android Speech Recognition and Synthesis from Google fr-fr-x-frd-local"],"language":"fr-FR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix masculine 2 (France)","name":"Google français 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google fr-fr-x-frb-network","Chrome OS français 3","Android Speech Recognition and Synthesis from Google fr-fr-x-frb-local"],"language":"fr-FR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix féminine 1 (Canada)","name":"Android Speech Recognition and Synthesis from Google fr-ca-x-caa-network","altNames":["Android Speech Recognition and Synthesis from Google fr-ca-x-caa-local","Android Speech Recognition and Synthesis from Google fr-CA-language"],"language":"fr-CA","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix féminine 2 (Canada)","name":"Android Speech Recognition and Synthesis from Google fr-ca-x-cac-network","altNames":["Android Speech Recognition and Synthesis from Google fr-ca-x-cac-local"],"language":"fr-CA","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix masculine 1 (Canada)","name":"Android Speech Recognition and Synthesis from Google fr-ca-x-cab-network","altNames":["Android Speech Recognition and Synthesis from Google fr-ca-x-cab-local"],"language":"fr-CA","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voix masculine 2 (Canada)","name":"Android Speech Recognition and Synthesis from Google fr-ca-x-cad-network","altNames":["Android Speech Recognition and Synthesis from Google fr-ca-x-cad-local"],"language":"fr-CA","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Sabela","name":"Microsoft Sabela Online (Natural) - Galician (Spain)","language":"gl-ES","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Roi","name":"Microsoft Roi Online (Natural) - Galician (Spain)","language":"gl-ES","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Carmela","name":"Carmela","language":"gl-ES","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Hila","name":"Microsoft Hila Online (Natural) - Hebrew (Israel)","language":"he-IL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Avri","name":"Microsoft Avri Online (Natural) - Hebrew (Israel)","language":"he-IL","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Carmit","name":"Carmit","language":"he-IL","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Asaf","name":"Microsoft Asaf - Hebrew (Israel)","language":"he-IL","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"קול גברי 1","name":"Android Speech Recognition and Synthesis from Google he-il-x-heb-network","altNames":["Android Speech Recognition and Synthesis from Google he-il-x-heb-local","Android Speech Recognition and Synthesis from Google he-IL-language"],"language":"he-IL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"קול גברי 2","name":"Android Speech Recognition and Synthesis from Google he-il-x-hec-network","altNames":["Android Speech Recognition and Synthesis from Google he-il-x-hec-local"],"language":"he-IL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"קול נשי 1","name":"Android Speech Recognition and Synthesis from Google he-il-x-hed-network","altNames":["Android Speech Recognition and Synthesis from Google he-il-x-hed-local"],"language":"he-IL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"קול נשי 2","name":"Android Speech Recognition and Synthesis from Google he-il-x-hee-network","altNames":["Android Speech Recognition and Synthesis from Google he-il-x-hee-local"],"language":"he-IL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Swara","name":"Microsoft Swara Online (Natural) - Hindi (India)","language":"hi-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Madhur","name":"Microsoft Madhur Online (Natural) - Hindi (India)","language":"hi-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Kiyara","name":"Kiyara","language":"hi-IN","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Lekha","name":"Lekha","language":"hi-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Neel","name":"Neel","language":"hi-IN","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"महिला Google आवाज़","name":"Google हिन्दी","language":"hi-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kalpana","name":"Microsoft Kalpana - Hindi (India)","language":"hi-IN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Hemant","name":"Microsoft Hemant - Hindi (India)","language":"hi-IN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"महिला आवाज़ 1","name":"Google हिन्दी 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google hi-in-x-hia-network","Chrome OS हिन्दी 2","Android Speech Recognition and Synthesis from Google hi-in-x-hia-local","Android Speech Recognition and Synthesis from Google hi-IN-language"],"language":"hi-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"महिला आवाज़ 2","name":"Google हिन्दी 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google hi-in-x-hic-network","Chrome OS हिन्दी 3","Android Speech Recognition and Synthesis from Google hi-in-x-hic-local"],"language":"hi-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"महिला आवाज़ 3","name":"Chrome OS हिन्दी 1","language":"hi-IN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"पुरुष आवाज 1","name":"Google हिन्दी 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google hi-in-x-hid-network","Chrome OS हिन्दी 4","Android Speech Recognition and Synthesis from Google hi-in-x-hid-local"],"language":"hi-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"पुरुष आवाज 2","name":"Google हिन्दी 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google hi-in-x-hie-network","Chrome OS हिन्दी 5","Android Speech Recognition and Synthesis from Google hi-in-x-hie-local"],"language":"hi-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Gabrijela","name":"Microsoft Gabrijela Online (Natural) - Croatian (Croatia)","language":"hr-HR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Srecko","name":"Microsoft Srecko Online (Natural) - Croatian (Croatia)","language":"hr-HR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Lana","name":"Lana","altNames":["Lana (poboljšani)","Lana (hrvatski (Hrvatska))"],"language":"hr-HR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Matej","name":"Microsoft Matej - Croatian (Croatia)","language":"hr-HR","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ženski glas","name":"Android Speech Recognition and Synthesis from Google hr-hr-x-hra-network","altNames":["Android Speech Recognition and Synthesis from Google hr-hr-x-hra-local"],"language":"hr-HR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Muški glas","name":"Android Speech Recognition and Synthesis from Google hr-hr-x-hrb-network","altNames":["Android Speech Recognition and Synthesis from Google hr-hr-x-hrb-local","Android Speech Recognition and Synthesis from Google hr-HR-language"],"language":"hr-HR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Noemi","name":"Microsoft Noemi Online (Natural) - Hungarian (Hungary)","language":"hu-HU","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Tamas","name":"Microsoft Tamas Online (Natural) - Hungarian (Hungary)","language":"hu-HU","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Tünde","name":"Tünde","language":"hu-HU","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Szabolcs","name":"Microsoft Szabolcs - Hungarian (Hungary)","language":"hu-HU","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Női hang","name":"Google Magyar (Natural)","altNames":["Android Speech Recognition and Synthesis from Google hu-hu-x-kfl-network","Chrome OS Magyar","Android Speech Recognition and Synthesis from Google hu-hu-x-kfl-local","Android Speech Recognition and Synthesis from Google hu-HU-language"],"language":"hu-HU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Gadis","name":"Microsoft Gadis Online (Natural) - Indonesian (Indonesia)","language":"id-ID","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ardi","name":"Microsoft Ardi Online (Natural) - Indonesian (Indonesia)","language":"id-ID","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Damayanti","name":"Damayanti","language":"id-ID","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Suara Google wanita","name":"Google Bahasa Indonesia","language":"id-ID","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Andika","name":"Microsoft Andika - Indonesian (Indonesia)","language":"id-ID","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara wanita 1","name":"Google Bahasa Indonesia 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google id-id-x-idc-network","Chrome OS Bahasa Indonesia 1","Android Speech Recognition and Synthesis from Google id-id-x-idc-local","Android Speech Recognition and Synthesis from Google id-ID-language"],"language":"id-ID","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara wanita 2","name":"Google Bahasa Indonesia 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google id-id-x-idd-network","Chrome OS Bahasa Indonesia 2","Android Speech Recognition and Synthesis from Google id-id-x-idd-local"],"language":"id-ID","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara laki-laki 1","name":"Google Bahasa Indonesia 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google id-id-x-ide-network","Chrome OS Bahasa Indonesia 3","Android Speech Recognition and Synthesis from Google id-id-x-ide-local"],"language":"id-ID","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara laki-laki 2","name":"Google Bahasa Indonesia 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google id-id-x-dfz-network","Chrome OS Bahasa Indonesia 4","Android Speech Recognition and Synthesis from Google id-id-x-dfz-local"],"language":"id-ID","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Elsa (Alta qualita)","name":"Microsoft Elsa Online (Natural) - Italian (Italy)","language":"it-IT","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Isabella","name":"Microsoft Isabella Online (Natural) - Italian (Italy)","language":"it-IT","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Giuseppe","name":"Microsoft Giuseppe Online (Natural) - Italian (Italy)","language":"it-IT","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Diego","name":"Microsoft Diego Online (Natural) - Italian (Italy)","language":"it-IT","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Federica","name":"Federica","language":"it-IT","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Emma","name":"Emma","language":"it-IT","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Alice","name":"Alice","language":"it-IT","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Paola","name":"Paola","language":"it-IT","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Luca","name":"Luca","language":"it-IT","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Voce Google femminile","name":"Google italiano","language":"it-IT","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Elsa","name":"Microsoft Elsa - Italian (Italy)","language":"it-IT","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Cosimo","name":"Microsoft Cosimo - Italian (Italy)","language":"it-IT","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voce femminile 1","name":"Google italiano 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google it-it-x-itb-network","Chrome OS italiano 2","Android Speech Recognition and Synthesis from Google it-it-x-itb-local","Android Speech Recognition and Synthesis from Google it-IT-language"],"language":"it-IT","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voce femminile 2","name":"Google italiano 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google it-it-x-kda-network","Chrome OS italiano 1","Android Speech Recognition and Synthesis from Google it-it-x-kda-local"],"language":"it-IT","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voce maschile 1","name":"Google italiano 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google it-it-x-itc-network","Chrome OS italiano 3","Android Speech Recognition and Synthesis from Google it-it-x-itc-local"],"language":"it-IT","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voce maschile 2","name":"Google italiano 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google it-it-x-itd-network","Chrome OS italiano 4","Android Speech Recognition and Synthesis from Google it-it-x-itd-local"],"language":"it-IT","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Nanami","name":"Microsoft Nanami Online (Natural) - Japanese (Japan)","language":"ja-JP","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Keita","name":"Microsoft Keita Online (Natural) - Japanese (Japan)","language":"ja-JP","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"O-Ren","name":"O-Ren","language":"ja-JP","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Kyoko","name":"Kyoko","language":"ja-JP","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Otoya","name":"Otoya","language":"ja-JP","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Hattori","name":"Hattori","language":"ja-JP","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google の女性の声","name":"Google 日本語","language":"ja-JP","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ayumi","name":"Microsoft Ayumi - Japanese (Japan)","language":"ja-JP","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Haruka","name":"Microsoft Haruka - Japanese (Japan)","language":"ja-JP","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ichiro","name":"Microsoft Ichiro - Japanese (Japan)","language":"ja-JP","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女性の声1","name":"Google 日本語 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ja-jp-x-htm-network","Chrome OS 日本語 1","Android Speech Recognition and Synthesis from Google ja-jp-x-htm-local","Android Speech Recognition and Synthesis from Google ja-JP-language"],"language":"ja-JP","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女性の声2","name":"Chrome OS 日本語 2","altNames":["Android Speech Recognition and Synthesis from Google ja-jp-x-jab-network","Android Speech Recognition and Synthesis from Google ja-jp-x-jab-local"],"language":"ja-JP","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男性の声1","name":"Google 日本語 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ja-jp-x-jac-network","Chrome OS 日本語 3","Android Speech Recognition and Synthesis from Google ja-jp-x-jac-local"],"language":"ja-JP","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男性の声2","name":"Google 日本語 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ja-jp-x-jad-network","Chrome OS 日本語 4","Android Speech Recognition and Synthesis from Google ja-jp-x-jad-local"],"language":"ja-JP","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Sapna","name":"Microsoft Sapna Online (Natural) - Kannada (India)","language":"kn-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Gagan","name":"Microsoft Gagan Online (Natural) - Kannada (India)","language":"kn-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Soumya","name":"Soumya","language":"kn-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"ಸ್ತ್ರೀ ಧ್ವನಿ","name":"Android Speech Recognition and Synthesis from Google kn-in-x-knf-network","altNames":["Android Speech Recognition and Synthesis from Google kn-in-x-knf-local","Android Speech Recognition and Synthesis from Google kn-IN-language"],"language":"kn-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"ಪುರುಷ ಧ್ವನಿ","name":"Android Speech Recognition and Synthesis from Google kn-in-x-knm-network","altNames":["Android Speech Recognition and Synthesis from Google kn-in-x-knm-local"],"language":"kn-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"SunHi","name":"Microsoft SunHi Online (Natural) - Korean (Korea)","language":"ko-KR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Hyunsu","name":"Microsoft Hyunsu Online (Natural) - Korean (Korea)","language":"ko-KR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"InJoon","name":"Microsoft InJoon Online (Natural) - Korean (Korea)","language":"ko-KR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Yuna","name":"Yuna","language":"ko-KR","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Jian","name":"Jian","language":"ko-KR","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Suhyun","name":"Suhyun","language":"ko-KR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Sora","name":"Sora","language":"ko-KR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Minsu","name":"Minsu","language":"ko-KR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google 여성 음성","name":"Google 한국의","language":"ko-KR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Heami","name":"Microsoft Heami - Korean (Korea)","language":"ko-KR","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"여성 목소리 1","name":"Google 한국어 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ko-kr-x-kob-network","Chrome OS 한국어 2","Android Speech Recognition and Synthesis from Google ko-kr-x-kob-local","Android Speech Recognition and Synthesis from Google ko-KR-language"],"language":"ko-KR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"여성 목소리 2","name":"Google 한국어 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ko-kr-x-ism-network","Chrome OS 한국어 1","Android Speech Recognition and Synthesis from Google ko-kr-x-ism-local"],"language":"ko-KR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"남성 1","name":"Google 한국어 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ko-kr-x-koc-network","Chrome OS 한국어 3","Android Speech Recognition and Synthesis from Google ko-kr-x-koc-local"],"language":"ko-KR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"남성 2","name":"Google 한국어 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google ko-kr-x-kod-network","Chrome OS 한국어 4","Android Speech Recognition and Synthesis from Google ko-kr-x-kod-local"],"language":"ko-KR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Aarohi","name":"Microsoft Aarohi Online (Natural) - Marathi (India)","language":"mr-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Manohar","name":"Microsoft Manohar Online (Natural) - Marathi (India)","language":"mr-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ananya","name":"Ananya","language":"mr-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"स्त्री आवाज","name":"Android Speech Recognition and Synthesis from Google mr-in-x-mrf-network","altNames":["Android Speech Recognition and Synthesis from Google mr-in-x-mrf-local","Android Speech Recognition and Synthesis from Google mr-IN-language"],"language":"mr-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Yasmin","name":"Microsoft Yasmin Online (Natural) - Malay (Malaysia)","language":"ms-MY","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Osman","name":"Microsoft Osman Online (Natural) - Malay (Malaysia)","language":"ms-MY","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Amira","name":"Amira","language":"ms-MY","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Rizwan","name":"Microsoft Rizwan - Malay (Malaysia)","language":"ms-MY","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara perempuan 1","name":"Android Speech Recognition and Synthesis from Google ms-my-x-msc-network","altNames":["Android Speech Recognition and Synthesis from Google ms-my-x-msc-local","Android Speech Recognition and Synthesis from Google ms-MY-language"],"language":"ms-MY","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara perempuan 2","name":"Android Speech Recognition and Synthesis from Google ms-my-x-mse-network","altNames":["Android Speech Recognition and Synthesis from Google ms-my-x-mse-local"],"language":"ms-MY","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara lelaki 1","name":"Android Speech Recognition and Synthesis from Google ms-my-x-msd-network","altNames":["Android Speech Recognition and Synthesis from Google ms-my-x-msd-local"],"language":"ms-MY","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Suara lelaki 2","name":"Android Speech Recognition and Synthesis from Google ms-my-x-msg-network","altNames":["Android Speech Recognition and Synthesis from Google ms-my-x-msg-local"],"language":"ms-MY","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Pernille","name":"Microsoft Pernille Online (Natural) - Norwegian (Bokmål, Norway)","language":"nb-NO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Finn","name":"Microsoft Finn Online (Natural) - Norwegian (Bokmål Norway)","language":"nb-NO","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Nora","name":"Nora","language":"nb-NO","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Henrik","name":"Henrik","language":"nb-NO","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Jon","name":"Microsoft Jon - Norwegian (Bokmål Norway)","language":"nb-NO","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvinnestemme 1","name":"Google Norsk Bokmål 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nb-no-x-cfl-network","Chrome OS Norsk Bokmål 2","Android Speech Recognition and Synthesis from Google nb-no-x-cfl-local","Android Speech Recognition and Synthesis from Google nb-NO-language"],"language":"nb-NO","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvinnestemme 2","name":"Google Norsk Bokmål 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nb-no-x-rfj-network","Chrome OS Norsk Bokmål 1","Android Speech Recognition and Synthesis from Google nb-no-x-rfj-local"],"language":"nb-NO","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvinnestemme 3","name":"Google Norsk Bokmål 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nb-no-x-tfs-network","Chrome OS Norsk Bokmål 4","Android Speech Recognition and Synthesis from Google nb-no-x-tfs-local"],"language":"nb-NO","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mannsstemme 1","name":"Google Norsk Bokmål 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nb-no-x-cmj-network","Chrome OS Norsk Bokmål 3","Android Speech Recognition and Synthesis from Google nb-no-x-cmj-local"],"language":"nb-NO","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mannsstemme 2","name":"Google Norsk Bokmål 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nb-no-x-tmg-network","Chrome OS Norsk Bokmål 5","Android Speech Recognition and Synthesis from Google nb-no-x-tmg-local"],"language":"nb-NO","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Colette","name":"Microsoft Colette Online (Natural) - Dutch (Netherlands)","language":"nl-NL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Fenna","name":"Microsoft Fenna Online (Natural) - Dutch (Netherlands)","language":"nl-NL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Hanna","name":"Microsoft Hanna Online - Dutch (Netherlands)","language":"nl-NL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Maarten","name":"Microsoft Maarten Online (Natural) - Dutch (Netherlands)","language":"nl-NL","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Dena","name":"Microsoft Dena Online (Natural) - Dutch (Belgium)","language":"nl-BE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Arnaud","name":"Microsoft Arnaud Online (Natural) - Dutch (Belgium)","language":"nl-BE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Claire","name":"Claire","language":"nl-NL","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Xander","name":"Xander","language":"nl-NL","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Ellen","name":"Ellen","language":"nl-BE","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google mannelijke stem","name":"Google Nederlands","language":"nl-NL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Frank","name":"Microsoft Frank - Dutch (Netherlands)","language":"nl-NL","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Vrouwelijke stem 1 (Nederland)","name":"Google Nederlands 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nl-nl-x-lfc-network","Chrome OS Nederlands 4","Android Speech Recognition and Synthesis from Google nl-nl-x-lfc-local","Android Speech Recognition and Synthesis from Google nl-NL-language"],"language":"nl-NL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Vrouwelijke stem 2 (Nederland)","name":"Google Nederlands 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nl-nl-x-tfb-network","Chrome OS Nederlands 1","Android Speech Recognition and Synthesis from Google nl-nl-x-tfb-local"],"language":"nl-NL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Vrouwelijke stem 3 (Nederland)","name":"Google Nederlands 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nl-nl-x-yfr-network","Chrome OS Nederlands 5","Android Speech Recognition and Synthesis from Google nl-nl-x-yfr-local"],"language":"nl-NL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mannelijke stem 1 (Nederland)","name":"Google Nederlands 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nl-nl-x-bmh-network","Chrome OS Nederlands 2","Android Speech Recognition and Synthesis from Google nl-nl-x-bmh-local"],"language":"nl-NL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mannelijke stem 2 (Nederland)","name":"Google Nederlands 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google nl-nl-x-dma-network","Chrome OS Nederlands 3","Android Speech Recognition and Synthesis from Google nl-nl-x-dma-local"],"language":"nl-NL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Vrouwelijke stem (België)","name":"Android Speech Recognition and Synthesis from Google nl-be-x-bec-network","altNames":["Android Speech Recognition and Synthesis from Google nl-be-x-bec-local","Android Speech Recognition and Synthesis from Google nl-BE-language"],"language":"nl-BE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mannelijke stem (België)","name":"Android Speech Recognition and Synthesis from Google nl-be-x-bed-network","altNames":["Android Speech Recognition and Synthesis from Google nl-be-x-bed-local"],"language":"nl-BE","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Zofia","name":"Microsoft Zofia Online (Natural) - Polish (Poland)","language":"pl-PL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Paulina","name":"Microsoft Paulina Online - Polish (Poland)","language":"pl-PL","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Marek","name":"Microsoft Marek Online (Natural) - Polish (Poland)","language":"pl-PL","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ewa","name":"Ewa","language":"pl-PL","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Zosia","name":"Zosia","language":"pl-PL","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Krzysztof","name":"Krzysztof","language":"pl-PL","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Żeński głos Google’a","name":"Google polski","language":"pl-PL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Paulina","name":"Microsoft Paulina - Polish (Poland)","language":"pl-PL","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Adam","name":"Microsoft Adam - Polish (Poland)","language":"pl-PL","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Głos żeński 1","name":"Google Polski 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pl-pl-x-afb-network","Chrome OS Polski 2","Android Speech Recognition and Synthesis from Google pl-pl-x-afb-local","Android Speech Recognition and Synthesis from Google pl-PL-language"],"language":"pl-PL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Głos żeński 2","name":"Google Polski 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pl-pl-x-oda-network","Chrome OS Polski 1","Android Speech Recognition and Synthesis from Google pl-pl-x-oda-local"],"language":"pl-PL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Głos żeński 3","name":"Google Polski 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pl-pl-x-zfg-network","Chrome OS Polski 5","Android Speech Recognition and Synthesis from Google pl-pl-x-zfg-local"],"language":"pl-PL","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Głos męski 1","name":"Google Polski 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pl-pl-x-bmg-network","Chrome OS Polski 3","Android Speech Recognition and Synthesis from Google pl-pl-x-bmg-local"],"language":"pl-PL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Głos męski 2","name":"Google Polski 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pl-pl-x-jmk-network","Chrome OS Polski 4","Android Speech Recognition and Synthesis from Google pl-pl-x-jmk-local"],"language":"pl-PL","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Raquel","name":"Microsoft Raquel Online (Natural) - Portuguese (Portugal)","language":"pt-PT","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Duarte","name":"Microsoft Duarte Online (Natural) - Portuguese (Portugal)","language":"pt-PT","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Francisca","name":"Microsoft Francisca Online (Natural) - Portuguese (Brazil)","language":"pt-BR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Thalita","name":"Microsoft Thalita Online (Natural) - Portuguese (Brazil)","language":"pt-BR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Antonio","name":"Microsoft Antonio Online (Natural) - Portuguese (Brazil)","language":"pt-BR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Catarina","name":"Catarina","language":"pt-PT","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Joana","name":"Joana","language":"pt-PT","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Joaquim","name":"Joaquim","language":"pt-PT","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Fernanda","name":"Fernanda","language":"pt-BR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Luciana","name":"Luciana","language":"pt-BR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Felipe","name":"Felipe","language":"pt-BR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Voz feminina do Google","name":"Google português do Brasil","language":"pt-BR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Helia","name":"Microsoft Helia - Portuguese (Portugal)","language":"pt-PT","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Maria","name":"Microsoft Maria - Portuguese (Brazil)","language":"pt-BR","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Daniel","name":"Microsoft Daniel - Portuguese (Brazil)","language":"pt-BR","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz feminina 1 (Portugal)","name":"Google português de Portugal 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-pt-x-jfb-network","Android Speech Recognition and Synthesis from Google pt-pt-x-jfb-local","Android Speech Recognition and Synthesis from Google pt-PT-language"],"language":"pt-PT","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz feminina 2 (Portugal)","name":"Google português de Portugal 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-pt-x-sfs-network","Chrome OS português de Portugal","Android Speech Recognition and Synthesis from Google pt-pt-x-sfs-local"],"language":"pt-PT","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina 1 (Portugal)","name":"Google português de Portugal 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-pt-x-jmn-network","Android Speech Recognition and Synthesis from Google pt-pt-x-jmn-local"],"language":"pt-PT","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina 2 (Portugal)","name":"Google português de Portugal 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-pt-x-pmj-network","Android Speech Recognition and Synthesis from Google pt-pt-x-pmj-local"],"language":"pt-PT","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz feminina 1 (Brasil)","name":"Google português do Brasil 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-br-x-afs-network","Chrome OS português do Brasil","Android Speech Recognition and Synthesis from Google pt-br-x-afs-local","Android Speech Recognition and Synthesis from Google pt-BR-language"],"language":"pt-BR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz feminina 2 (Brasil)","name":"Google português do Brasil 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-br-x-pte-network","Android Speech Recognition and Synthesis from Google pt-br-x-pte-local"],"language":"pt-BR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voz masculina (Brasil)","name":"Google português do Brasil 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google pt-br-x-ptd-network","Android Speech Recognition and Synthesis from Google pt-br-x-ptd-local"],"language":"pt-BR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Alina","name":"Microsoft Alina Online (Natural) - Romanian (Romania)","language":"ro-RO","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Emil","name":"Microsoft Emil Online (Natural) - Romanian (Romania)","language":"ro-RO","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Ioana","name":"Ioana","language":"ro-RO","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Andrei","name":"Microsoft Andrei - Romanian (Romania)","language":"ro-RO","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Voce feminină","name":"Android Speech Recognition and Synthesis from Google ro-ro-x-vfv-network","altNames":["Android Speech Recognition and Synthesis from Google ro-ro-x-vfv-local","Android Speech Recognition and Synthesis from Google ro-RO-language"],"language":"ro-RO","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Svetlana","name":"Microsoft Svetlana Online (Natural) - Russian (Russia)","language":"ru-RU","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ekaterina","name":"Microsoft Ekaterina Online - Russian (Russia)","language":"ru-RU","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Dmitry","name":"Microsoft Dmitry Online (Natural) - Russian (Russia)","language":"ru-RU","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Katya","name":"Katya","language":"ru-RU","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Milena","name":"Milena","language":"ru-RU","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Yuri","name":"Yuri","language":"ru-RU","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google женский голос","name":"Google русский","language":"ru-RU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Irina","name":"Microsoft Irina - Russian (Russian)","language":"ru-RU","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Pavel","name":"Microsoft Pavel - Russian (Russian)","language":"ru-RU","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Женский голос 1","name":"Android Speech Recognition and Synthesis from Google ru-ru-x-dfc-network","altNames":["Android Speech Recognition and Synthesis from Google ru-ru-x-dfc-local"],"language":"ru-RU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Женский голос 2","name":"Android Speech Recognition and Synthesis from Google ru-ru-x-ruc-network","altNames":["Android Speech Recognition and Synthesis from Google ru-ru-x-ruc-local"],"language":"ru-RU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Женский голос 3","name":"Android Speech Recognition and Synthesis from Google ru-ru-x-rue-network","altNames":["Android Speech Recognition and Synthesis from Google ru-ru-x-rue-local"],"language":"ru-RU","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Мужской голос 1","name":"Android Speech Recognition and Synthesis from Google ru-ru-x-rud-network","altNames":["Android Speech Recognition and Synthesis from Google ru-ru-x-rud-local"],"language":"ru-RU","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Мужской голос 2","name":"Android Speech Recognition and Synthesis from Google ru-ru-x-ruf-network","altNames":["Android Speech Recognition and Synthesis from Google ru-ru-x-ruf-local"],"language":"ru-RU","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Viktoria","name":"Microsoft Viktoria Online (Natural) - Slovak (Slovakia)","language":"sk-SK","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Lukas","name":"Microsoft Lukas Online (Natural) - Slovak (Slovakia)","language":"sk-SK","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Laura","name":"Laura","language":"sk-SK","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Filip","name":"Microsoft Filip - Slovak (Slovakia)","language":"sk-SK","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ženský hlas","name":"Google Slovenčina (Natural)","altNames":["Android Speech Recognition and Synthesis from Google sk-sk-x-sfk-network","Android Speech Recognition and Synthesis from Google sk-sk-x-sfk-local","Chrome OS Slovenčina","Android Speech Recognition and Synthesis from Google sk-SK-language"],"language":"sk-SK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Petra","name":"Microsoft Petra Online (Natural) - Slovenian (Slovenia)","language":"sl-SI","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Rok","name":"Microsoft Rok Online (Natural) - Slovenian (Slovenia)","language":"sl-SI","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Tina","name":"Tina","language":"sl-SI","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Lado","name":"Microsoft Lado - Slovenian (Slovenia)","language":"sl-SI","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Ženski glas","name":"Android Speech Recognition and Synthesis from Google sl-si-x-frm-local","altNames":["Android Speech Recognition and Synthesis from Google sl-SI-language"],"language":"sl-SI","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Sofie","name":"Microsoft Sofie Online (Natural) - Swedish (Sweden)","language":"sv-SE","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Mattias","name":"Microsoft Mattias Online (Natural) - Swedish (Sweden)","language":"sv-SE","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Klara","name":"Klara","language":"sv-SE","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Alva","name":"Alva","language":"sv-SE","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Oskar","name":"Oskar","language":"sv-SE","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Bengt","name":"Microsoft Bengt - Swedish (Sweden)","language":"sv-SE","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvinnlig röst 1","name":"Google Svenska 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google sv-se-x-lfs-network","Chrome OS Svenska","Android Speech Recognition and Synthesis from Google sv-se-x-lfs-local","Android Speech Recognition and Synthesis from Google sv-SE-language"],"language":"sv-SE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvinnlig röst 2","name":"Google Svenska 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google sv-se-x-afp-network","Android Speech Recognition and Synthesis from Google sv-se-x-afp-local"],"language":"sv-SE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kvinnlig röst 3","name":"Google Svenska 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google sv-se-x-cfg-network","Android Speech Recognition and Synthesis from Google sv-se-x-cfg-local"],"language":"sv-SE","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mansröst 1","name":"Google Svenska 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google sv-se-x-cmh-network","Android Speech Recognition and Synthesis from Google sv-se-x-cmh-local"],"language":"sv-SE","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Mansröst 2","name":"Google Svenska 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google sv-se-x-dmc-network","Android Speech Recognition and Synthesis from Google sv-se-x-dmc-local"],"language":"sv-SE","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Pallavi","name":"Microsoft Pallavi Online (Natural) - Tamil (India)","language":"ta-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Valluvar","name":"Microsoft Valluvar Online (Natural) - Tamil (India)","language":"ta-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Saranya","name":"Microsoft Saranya Online (Natural) - Tamil (Sri Lanka)","language":"ta-LK","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Kumar","name":"Microsoft Kumar Online (Natural) - Tamil (Sri Lanka)","language":"ta-LK","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Kani","name":"Microsoft Kani Online (Natural) - Tamil (Malaysia)","language":"ta-MY","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Surya","name":"Microsoft Surya Online (Natural) - Tamil (Malaysia)","language":"ta-MY","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Venba","name":"Microsoft Venba Online (Natural) - Tamil (Singapore)","language":"ta-SG","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Anbu","name":"Microsoft Anbu Online (Natural) - Tamil (Singapore)","language":"ta-SG","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Vani","name":"Vani","language":"ta-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Valluvar","name":"Microsoft Valluvar - Tamil (India)","language":"ta-IN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"பெண் குரல்","name":"Android Speech Recognition and Synthesis from Google ta-in-x-tac-network","altNames":["Android Speech Recognition and Synthesis from Google ta-in-x-tac-local","Android Speech Recognition and Synthesis from Google ta-IN-language"],"language":"ta-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"ஆண் குரல்","name":"Android Speech Recognition and Synthesis from Google ta-in-x-tad-network","altNames":["Android Speech Recognition and Synthesis from Google ta-in-x-tad-local"],"language":"ta-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Shruti","name":"Microsoft Shruti Online (Natural) - Telugu (India)","language":"te-IN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Mohan","name":"Microsoft Mohan Online (Natural) - Telugu (India)","language":"te-IN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Geeta","name":"Geeta","language":"te-IN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"స్త్రీ స్వరం","name":"Android Speech Recognition and Synthesis from Google te-in-x-tef-network","altNames":["Android Speech Recognition and Synthesis from Google te-in-x-tef-local","Android Speech Recognition and Synthesis from Google te-IN-language"],"language":"te-IN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"పురుష స్వరం","name":"Android Speech Recognition and Synthesis from Google te-in-x-tem-network","altNames":["Android Speech Recognition and Synthesis from Google te-in-x-tem-local"],"language":"te-IN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Premwadee","name":"Microsoft Premwadee Online (Natural) - Thai (Thailand)","language":"th-TH","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Niwat","name":"Microsoft Niwat Online (Natural) - Thai (Thailand)","language":"th-TH","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Narisa","name":"Narisa","language":"th-TH","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Kanya","name":"Kanya","language":"th-TH","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Pattara","name":"Microsoft Pattara - Thai (Thailand)","language":"th-TH","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"เสียงผู้หญิง","name":"Google ไทย (Natural)","altNames":["Android Speech Recognition and Synthesis from Google th-th-x-mol-network","Chrome OS ไทย","Android Speech Recognition and Synthesis from Google th-th-x-mol-local","Android Speech Recognition and Synthesis from Google th-TH-language"],"language":"th-TH","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Emel","name":"Microsoft Emel Online (Natural) - Turkish (Turkey)","language":"tr-TR","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ahmet","name":"Microsoft Ahmet Online (Natural) - Turkish (Turkey)","language":"tr-TR","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Yelda","name":"Yelda","altNames":["Yelda (Geliştirilmiş)","Yelda (Türkçe (Türkiye))"],"language":"tr-TR","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Cem","name":"Cem","language":"tr-TR","gender":"male","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Tolga","name":"Microsoft Tolga - Turkish (Turkey)","language":"tr-TR","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kadın sesi 1","name":"Google Türkçe 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google tr-tr-x-cfs-network","Chrome OS Türkçe 3","Android Speech Recognition and Synthesis from Google tr-tr-x-cfs-local","Android Speech Recognition and Synthesis from Google tr-TR-language"],"language":"tr-TR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kadın sesi 2","name":"Google Türkçe 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google tr-tr-x-efu-network","Chrome OS Türkçe 4","Android Speech Recognition and Synthesis from Google tr-tr-x-efu-local"],"language":"tr-TR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Kadın sesi 3","name":"Google Türkçe 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google tr-tr-x-mfm-network","Chrome OS Türkçe 1","Android Speech Recognition and Synthesis from Google tr-tr-x-mfm-local"],"language":"tr-TR","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Erkek sesi 1","name":"Google Türkçe 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google tr-tr-x-ama-network","Chrome OS Türkçe 2","Android Speech Recognition and Synthesis from Google tr-tr-x-ama-local"],"language":"tr-TR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Erkek sesi 2","name":"Google Türkçe 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google tr-tr-x-tmc-network","Chrome OS Türkçe 5","Android Speech Recognition and Synthesis from Google tr-tr-x-tmc-local"],"language":"tr-TR","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Polina","name":"Microsoft Polina Online (Natural) - Ukrainian (Ukraine)","language":"uk-UA","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"Ostap","name":"Microsoft Ostap Online (Natural) - Ukrainian (Ukraine)","language":"uk-UA","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Lesya","name":"Lesya","language":"uk-UA","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Жіночий голос","name":"Google українська (Natural)","altNames":["Android Speech Recognition and Synthesis from Google uk-ua-x-hfd-network","Chrome OS українська","Android Speech Recognition and Synthesis from Google uk-ua-x-hfd-local","Android Speech Recognition and Synthesis from Google uk-UA-language"],"language":"uk-UA","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"HoaiMy","name":"Microsoft HoaiMy Online (Natural) - Vietnamese (Vietnam)","language":"vi-VN","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"NamMinh","name":"Microsoft NamMinh Online (Natural) - Vietnamese (Vietnam)","language":"vi-VN","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Linh","name":"Linh","language":"vi-VN","gender":"female","quality":["low","normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"An","name":"Microsoft An - Vietnamese (Vietnam)","language":"vi-VN","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Giọng nữ 1","name":"Google Tiếng Việt 1 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google vi-vn-x-vic-network","Chrome OS Tiếng Việt 1","Android Speech Recognition and Synthesis from Google vi-vn-x-vic-local","Android Speech Recognition and Synthesis from Google vi-VN-language"],"language":"vi-VN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Giọng nữ 2","name":"Google Tiếng Việt 2 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google vi-vn-x-vid-network","Chrome OS Tiếng Việt 2","Android Speech Recognition and Synthesis from Google vi-vn-x-vid-local"],"language":"vi-VN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Giọng nữ 3","name":"Google Tiếng Việt 4 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google vi-vn-x-vif-network","Chrome OS Tiếng Việt 4","Android Speech Recognition and Synthesis from Google vi-vn-x-vif-local"],"language":"vi-VN","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Giọng nam 1","name":"Google Tiếng Việt 3 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google vi-vn-x-vie-network","Chrome OS Tiếng Việt 3","Android Speech Recognition and Synthesis from Google vi-vn-x-vie-local"],"language":"vi-VN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Giọng nam 2","name":"Google Tiếng Việt 5 (Natural)","altNames":["Android Speech Recognition and Synthesis from Google vi-vn-x-gft-network","Chrome OS Tiếng Việt 5","Android Speech Recognition and Synthesis from Google vi-vn-x-gft-local"],"language":"vi-VN","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Nannan","name":"Nannan","language":"wuu-CN","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"HiuGaai","name":"Microsoft HiuGaai Online (Natural) - Chinese (Cantonese Traditional)","language":"yue-HK","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"HiuMaan","name":"Microsoft HiuMaan Online (Natural) - Chinese (Hong Kong)","language":"yue-HK","gender":"female","quality":["veryHigh"],"localizedName":""},{"label":"WanLung","name":"Microsoft WanLung Online (Natural) - Chinese (Hong Kong)","language":"yue-HK","gender":"male","quality":["veryHigh"],"localizedName":""},{"label":"Sinji","name":"Sinji","language":"yue-HK","gender":"female","quality":["low","normal","high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Aasing","name":"Aasing","language":"yue-HK","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":"apple"},{"label":"Google 女聲","name":"Google 粤語(香港)","language":"yue-HK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Tracy","name":"Microsoft Tracy - Chinese (Traditional, Hong Kong S.A.R.)","language":"cmn-HK","gender":"female","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"Danny","name":"Microsoft Danny - Chinese (Traditional, Hong Kong S.A.R.)","language":"cmn-HK","gender":"male","quality":["normal"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女聲1","name":"Android Speech Recognition and Synthesis from Google yue-hk-x-jar-network","altNames":["Chrome OS 粵語 1","Android Speech Recognition and Synthesis from Google yue-HK-x-jar-local","Android Speech Recognition and Synthesis from Google yue-HK-language"],"language":"yue-HK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"女聲2","name":"Android Speech Recognition and Synthesis from Google yue-hk-x-yuc-network","altNames":["Chrome OS 粵語 2","Android Speech Recognition and Synthesis from Google yue-HK-x-yuc-local"],"language":"yue-HK","gender":"female","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男聲1","name":"Android Speech Recognition and Synthesis from Google yue-hk-x-yud-network","altNames":["Chrome OS 粵語 3","Android Speech Recognition and Synthesis from Google yue-HK-x-yud-local"],"language":"yue-HK","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男聲2","name":"Android Speech Recognition and Synthesis from Google yue-hk-x-yue-network","altNames":["Chrome OS 粵語 5","Android Speech Recognition and Synthesis from Google yue-HK-x-yue-local"],"language":"yue-HK","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""},{"label":"男聲3","name":"Android Speech Recognition and Synthesis from Google yue-hk-x-yuf-network","altNames":["Chrome OS 粵語 5","Android Speech Recognition and Synthesis from Google yue-HK-x-yuf-local"],"language":"yue-HK","gender":"male","quality":["high"],"recommendedPitch":1,"recommendedRate":1,"localizedName":""}]; - -export const quality = {"ar":{"normal":"محسن","high":"استثنائي"},"ca":{"normal":"millorada","high":"prèmium"},"cmn-CN":{"normal":"优化音质","high":"高音质"},"cmn-TW":{"normal":"增強音質","high":"高音質"},"cs":{"normal":"vylepšená verze","high":"prémiový"},"da":{"normal":"forbedret","high":"høj kvalitet"},"de":{"normal":"erweitert","high":"premium"},"el":{"normal":"βελτιωμένη","high":"υψηλής ποιότητας"},"en":{"normal":"Enhanced","high":"Premium"},"es":{"normal":"mejorada","high":"premium"},"fi":{"normal":"parannettu","high":"korkealaatuinen"},"fr":{"normal":"premium","high":"de qualité"},"he":{"normal":"משופר","high":"פרימיום"},"hi":{"normal":"बेहतर","high":"प्रीमियम"},"hr":{"normal":"poboljšani","high":"vrhunski"},"hu":{"normal":"továbbfejlesztett","high":"prémium"},"id":{"normal":"Ditingkatkan","high":"Premium"},"it":{"normal":"ottimizzata","high":"premium"},"ja":{"normal":"拡張","high":"プレミアム"},"ko":{"normal":"고품질","high":"프리미엄"},"ms":{"normal":"Dipertingkat","high":"Premium"},"nb":{"normal":"forbedret","high":"premium"},"nl":{"normal":"verbeterd","high":"premium"},"pl":{"normal":"rozszerzony","high":"premium"},"pt":{"normal":"melhorada","high":"premium"},"ro":{"normal":"îmbunătățită","high":"premium"},"ru":{"normal":"улучшенный","high":"высшее качество"},"sk":{"normal":"vylepšený","high":"prémiový"},"sl":{"normal":"izboljšano","high":"prvovrsten"},"sv":{"normal":"förbättrad","high":"premium"},"th":{"normal":"คุณภาพสูง","high":"คุณภาพสูง"},"tr":{"normal":"Geliştirilmiş","high":"Yüksek Kaliteli"},"uk":{"normal":"вдосконалений","high":"високої якості"},"vi":{"normal":"Nâng cao","high":"Cao cấp"}}; - -export const defaultRegion = {"ar":"ar-SA","bg":"bg-BG","bho":"bho-IN","bn":"bn-IN","ca":"ca-ES","cmn":"cmn-CN","cs":"cs-CZ","da":"da-DK","de":"de-DE","el":"el-GR","en":"en-US","es":"es-ES","eu":"eu-ES","fa":"fa-IR","fi":"fi-FI","fr":"fr-FR","gl":"gl-ES","he":"he-IL","hi":"hi-IN","hr":"hr-HR","hu":"hu-HU","id":"id-ID","it":"it-IT","ja":"ja-JP","kn":"kn-IN","ko":"ko-KR","mr":"mr-IN","ms":"ms-MY","nb":"nb-NO","nl":"nl-NL","pl":"pl-PL","pt":"pt-BR","ro":"ro-RO","ru":"ru-RU","sk":"sk-SK","sl":"sl-SI","sv":"sv-SE","ta":"ta-IN","te":"te-IN","th":"th-TH","tr":"tr-TR","uk":"uk-UA","vi":"vi-VN","wuu":"wuu-CN","yue":"yue-HK"}; - -// EOF diff --git a/src/engine.ts b/src/engine.ts index 79ff6b7..9d742e8 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,6 +1,6 @@ import { ReadiumSpeechPlaybackEvent, ReadiumSpeechPlaybackState } from "./navigator"; import { ReadiumSpeechUtterance } from "./utterance"; -import { ReadiumSpeechVoice } from "./voices"; +import { ReadiumSpeechVoice } from "./voices/types"; export interface ReadiumSpeechPlaybackEngine { // Queue Management diff --git a/src/index.ts b/src/index.ts index 7096f9b..8e342c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ -export * from "./voices"; +// Core exports +export * from "./WebSpeech"; + +// Data exports +export * from "./voices/languages"; + +// Other exports export * from "./engine"; export * from "./navigator"; export * from "./provider"; -export * from "./utterance"; -export * from "./WebSpeech/webSpeechEngine"; -export * from "./WebSpeech/webSpeechEngineProvider"; -export * from "./WebSpeech/TmpNavigator"; \ No newline at end of file +export * from "./utterance"; \ No newline at end of file diff --git a/src/navigator.ts b/src/navigator.ts index b8e96a1..1ab3163 100644 --- a/src/navigator.ts +++ b/src/navigator.ts @@ -1,4 +1,4 @@ -import { ReadiumSpeechVoice } from "./voices"; +import { ReadiumSpeechVoice } from "./voices/types"; import { ReadiumSpeechUtterance } from "./utterance"; export type ReadiumSpeechPlaybackState = "playing" | "paused" | "idle" | "loading" | "ready"; @@ -26,7 +26,7 @@ export interface ReadiumSpeechPlaybackEvent { export interface ReadiumSpeechNavigator { // Voice Management getVoices(): Promise; - setVoice(voice: ReadiumSpeechVoice | string): Promise; + setVoice(voice: ReadiumSpeechVoice | string): void; getCurrentVoice(): ReadiumSpeechVoice | null; // Content Management diff --git a/src/provider.ts b/src/provider.ts index 5a4defb..4d9dcd4 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -1,5 +1,5 @@ import { ReadiumSpeechPlaybackEngine } from "./engine"; -import { ReadiumSpeechVoice } from "./voices"; +import { ReadiumSpeechVoice } from "./voices/types"; export interface ReadiumSpeechEngineProvider { readonly id: string; diff --git a/src/types/json.d.ts b/src/types/json.d.ts new file mode 100644 index 0000000..a3932b0 --- /dev/null +++ b/src/types/json.d.ts @@ -0,0 +1,5 @@ +declare module "@json/*.json" { + import { VoiceData } from "../voices/types"; + const value: VoiceData; + export default value; +} diff --git a/src/utils/language.ts b/src/utils/language.ts new file mode 100644 index 0000000..6d8aa6c --- /dev/null +++ b/src/utils/language.ts @@ -0,0 +1,26 @@ +/** + * Extracts language and region from a BCP 47 language tag. + * @param lang - The BCP 47 language tag (e.g., "en-US", "fr-CA") + * @returns A tuple containing [language, region] where region is optional + */ +export const extractLangRegionFromBCP47 = (lang: string): [string, string | undefined] => { + if (!lang) return ["", undefined]; + + // Normalize language code by replacing underscores with dashes + const normalizedLang = lang.replace(/_/g, "-"); + + try { + const locale = new Intl.Locale(normalizedLang); + return [ + locale.language.toLowerCase(), + locale.region?.toUpperCase() + ]; + } catch { + // Fallback to simple parsing if Intl.Locale fails + const parts = normalizedLang.split("-"); + return [ + parts[0].toLowerCase(), + parts[1]?.toUpperCase() + ]; + } +} diff --git a/src/voices.ts b/src/voices.ts deleted file mode 100644 index faa96e9..0000000 --- a/src/voices.ts +++ /dev/null @@ -1,571 +0,0 @@ - -import { novelty, quality, recommended, veryLowQuality, TGender, TQuality, IRecommended, defaultRegion } from "./data.gen.js"; - -// export type TOS = 'Android' | 'ChromeOS' | 'iOS' | 'iPadOS' | 'macOS' | 'Windows'; -// export type TBrowser = 'ChromeDesktop' | 'Edge' | 'Firefox' | 'Safari'; - -const navigatorLanguages = () => window?.navigator?.languages || []; -const navigatorLang = () => (navigator?.language || "").split("-")[0].toLowerCase(); - -export interface ReadiumSpeechVoice { - label: string; - voiceURI: string; - name: string; - __lang?: string | undefined; - language: string; - gender?: TGender | undefined; - age?: string | undefined; - offlineAvailability: boolean; - quality?: TQuality | undefined; - pitchControl: boolean; - recommendedPitch?: number | undefined; - recommendedRate?: number | undefined; -} - -const normalQuality = Object.values(quality).map(({ normal }) => normal); -const highQuality = Object.values(quality).map(({ high }) => high); - -function compareQuality(a?: TQuality, b?: TQuality): number { - const qualityToNumber = (quality: TQuality) => { - switch (quality) { - case "veryLow": {return 0;} - case "low": {return 1;} - case "normal": {return 2;} - case "high": {return 3;} - case "veryHigh": {return 4;} - default: {return -1}; - } - } - - return qualityToNumber(b || "low") - qualityToNumber(a || "low"); -}; - -export async function getSpeechSynthesisVoices(maxTimeout = 10000, interval = 10): Promise { - const a = () => speechSynthesis.getVoices(); - - // Step 1: Try to load voices directly (best case scenario) - const voices = a(); - if (Array.isArray(voices) && voices.length) return voices; - - return new Promise((resolve, reject) => { - // Calculate iterations from total timeout - let counter = Math.floor(maxTimeout / interval); - // Flag to ensure polling only starts once - let pollingStarted = false; - - // Polling function: Checks for voices periodically until counter expires - const startPolling = () => { - // Prevent multiple starts - if (pollingStarted) return; - pollingStarted = true; - - const tick = () => { - // Resolve with empty array if no voices found - if (counter < 1) return resolve([]); - --counter; - const voices = a(); - // Resolve if voices loaded - if (Array.isArray(voices) && voices.length) return resolve(voices); - // Continue polling - setTimeout(tick, interval); - }; - // Initial start - setTimeout(tick, interval); - }; - - // Step 2: Use onvoiceschanged if available (prioritizes event over polling) - if (speechSynthesis.onvoiceschanged) { - speechSynthesis.onvoiceschanged = () => { - const voices = a(); - if (Array.isArray(voices) && voices.length) { - // Resolve immediately if voices are available - resolve(voices); - } else { - // Fallback to polling if event fires but no voices - startPolling(); - } - }; - } else { - // Step 3: No onvoiceschanged support, start polling directly - startPolling(); - } - - // Step 4: Overall safety timeout - fail if nothing happens after maxTimeout - setTimeout(() => reject(new Error("No voices available after timeout")), maxTimeout); - }); -} - -const _strHash = ({voiceURI, name, language, offlineAvailability}: ReadiumSpeechVoice) => `${voiceURI}_${name}_${language}_${offlineAvailability}`; - -function removeDuplicate(voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] { - - const voicesStrMap = [...new Set(voices.map((v) => _strHash(v)))]; - - const voicesFiltered = voicesStrMap - .map((s) => voices.find((v) => _strHash(v) === s)) - .filter((v) => !!v); - - return voicesFiltered; -} - -export function parseSpeechSynthesisVoices(speechSynthesisVoices: SpeechSynthesisVoice[]): ReadiumSpeechVoice[] { - - const parseAndFormatBCP47 = (lang: string) => { - const speechVoiceLang = lang.replace("_", "-"); - if (/\w{2,3}-\w{2,3}/.test(speechVoiceLang)) { - return `${speechVoiceLang.split("-")[0].toLowerCase()}-${speechVoiceLang.split("-")[1].toUpperCase()}`; - } - - // bad formated !? - return lang; - }; - return speechSynthesisVoices.map((speechVoice) => ({ - label: speechVoice.name, - voiceURI: speechVoice.voiceURI , - name: speechVoice.name, - __lang: speechVoice.lang, - language: parseAndFormatBCP47(speechVoice.lang) , - gender: undefined, - age: undefined, - offlineAvailability: speechVoice.localService, - quality: undefined, - pitchControl: true, - recommendedPitch: undefined, - recomendedRate: undefined, - })); -} - -// Note: This does not work as browsers expect an actual SpeechSynthesisVoice -// Here it is just an object with the same-ish properties -export function convertToSpeechSynthesisVoices(voices: ReadiumSpeechVoice[]): SpeechSynthesisVoice[] { - return voices.map((voice) => ({ - default: false, - lang: voice.__lang || voice.language, - localService: voice.offlineAvailability, - name: voice.name, - voiceURI: voice.voiceURI, - })); -} - -export function filterOnOfflineAvailability(voices: ReadiumSpeechVoice[], offline = true): ReadiumSpeechVoice[] { - return voices.filter(({offlineAvailability}) => { - return offlineAvailability === offline; - }); -} - -export function filterOnGender(voices: ReadiumSpeechVoice[], gender: TGender): ReadiumSpeechVoice[] { - return voices.filter(({gender: voiceGender}) => { - return voiceGender === gender; - }) -} - -export function filterOnLanguage(voices: ReadiumSpeechVoice[], language: string | string[] = navigatorLang()): ReadiumSpeechVoice[] { - language = Array.isArray(language) ? language : [language]; - language = language.map((l) => extractLangRegionFromBCP47(l)[0]); - return voices.filter(({language: voiceLanguage}) => { - const [lang] = extractLangRegionFromBCP47(voiceLanguage); - return language.includes(lang); - }) -} - -export function filterOnQuality(voices: ReadiumSpeechVoice[], quality: TQuality | TQuality[]): ReadiumSpeechVoice[] { - quality = Array.isArray(quality) ? quality : [quality]; - return voices.filter(({quality: voiceQuality}) => { - return quality.some((qual) => qual === voiceQuality); - }); -} - -export function filterOnNovelty(voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] { - return voices.filter(({ name }) => { - return !novelty.includes(name); - }); -} - -export function filterOnVeryLowQuality(voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] { - return voices.filter(({ name }) => { - return !veryLowQuality.find((v) => name.startsWith(v)); - }); -} - -function updateVoiceInfo(recommendedVoice: IRecommended, voice: ReadiumSpeechVoice) { - voice.label = recommendedVoice.label; - voice.gender = recommendedVoice.gender; - voice.recommendedPitch = recommendedVoice.recommendedPitch; - voice.recommendedRate = recommendedVoice.recommendedRate; - - return voice; -} -export type TReturnFilterOnRecommended = [voicesRecommended: ReadiumSpeechVoice[], voicesLowerQuality: ReadiumSpeechVoice[]]; -export function filterOnRecommended(voices: ReadiumSpeechVoice[], _recommended: IRecommended[] = recommended): TReturnFilterOnRecommended { - - const voicesRecommended: ReadiumSpeechVoice[] = []; - const voicesLowerQuality: ReadiumSpeechVoice[] = []; - - recommendedVoiceLoop: - for (const recommendedVoice of _recommended) { - if (Array.isArray(recommendedVoice.quality) && recommendedVoice.quality.length > 1) { - - const voicesFound = voices.filter(({ name }) => name.startsWith(recommendedVoice.name)); - if (voicesFound.length) { - - for (const qualityTested of ["high", "normal"] as TQuality[]) { - for (let i = 0; i < voicesFound.length; i++) { - const voice = voicesFound[i]; - - const rxp = /^.*\((.*)\)$/; - if (rxp.test(voice.name)) { - const res = rxp.exec(voice.name); - const maybeQualityString = res ? res[1] || "" : ""; - const qualityDataArray = qualityTested === "high" ? highQuality : normalQuality; - - if (recommendedVoice.quality.includes(qualityTested) && qualityDataArray.includes(maybeQualityString)) { - voice.quality = qualityTested; - voicesRecommended.push(updateVoiceInfo(recommendedVoice, voice)); - - voicesFound.splice(i, 1); - voicesLowerQuality.push(...(voicesFound.map((v) => { - v.quality = "low"; // Todo need to be more precise for 'normal' quality voices - return updateVoiceInfo(recommendedVoice, v); - }))); - - continue recommendedVoiceLoop; - } - } - } - } - const voice = voicesFound[0]; - for (let i = 1; i < voicesFound.length; i++) { - voicesLowerQuality.push(voicesFound[i]); - } - - voice.quality = voicesFound.length > 3 ? "veryHigh" : voicesFound.length > 2 ? "high" : "normal"; - voicesRecommended.push(updateVoiceInfo(recommendedVoice, voice)); - - } - } else if (Array.isArray(recommendedVoice.altNames) && recommendedVoice.altNames.length) { - - const voiceFound = voices.find(({ name }) => name === recommendedVoice.name); - if (voiceFound) { - const voice = voiceFound; - - voice.quality = Array.isArray(recommendedVoice.quality) ? recommendedVoice.quality[0] : undefined; - voicesRecommended.push(updateVoiceInfo(recommendedVoice, voice)); - - // voice Name found so altNames array must be filter and push to voicesLowerQuality - const altNamesVoicesFound = voices.filter(({name}) => recommendedVoice.altNames!.includes(name)); - // TODO: Typescript bug type assertion doesn't work, need to force the compiler with the Non-null Assertion Operator - - voicesLowerQuality.push(...(altNamesVoicesFound.map((v) => { - v.quality = recommendedVoice.quality[0]; - return updateVoiceInfo(recommendedVoice, v); - }))); - } else { - - // filter voices on altNames, keep the first and push the remaining to voicesLowerQuality - const altNamesVoicesFound = voices.filter(({name}) => recommendedVoice.altNames!.includes(name)); - if (altNamesVoicesFound.length) { - - const voice = altNamesVoicesFound.shift() as ReadiumSpeechVoice; - - voice.quality = Array.isArray(recommendedVoice.quality) ? recommendedVoice.quality[0] : undefined; - voicesRecommended.push(updateVoiceInfo(recommendedVoice, voice)); - - - voicesLowerQuality.push(...(altNamesVoicesFound.map((v) => { - v.quality = recommendedVoice.quality[0]; - return updateVoiceInfo(recommendedVoice, v); - }))); - } - } - } else { - - const voiceFound = voices.find(({ name }) => name === recommendedVoice.name); - if (voiceFound) { - - const voice = voiceFound; - - voice.quality = Array.isArray(recommendedVoice.quality) ? recommendedVoice.quality[0] : undefined; - voicesRecommended.push(updateVoiceInfo(recommendedVoice, voice)); - - } - } - } - - return [removeDuplicate(voicesRecommended), removeDuplicate(voicesLowerQuality)]; -} - -const extractLangRegionFromBCP47 = (l: string) => [l.split("-")[0].toLowerCase(), l.split("-")[1]?.toUpperCase()]; - -export function sortByQuality(voices: ReadiumSpeechVoice[]) { - return voices.sort(({quality: qa}, {quality: qb}) => { - return compareQuality(qa, qb); - }); -} - -export function sortByName(voices: ReadiumSpeechVoice[]) { - return voices.sort(({name: na}, {name: nb}) => { - return na.localeCompare(nb); - }) -} - -export function sortByGender(voices: ReadiumSpeechVoice[], genderFirst: TGender) { - return voices.sort(({gender: ga}, {gender: gb}) => { - return ga === gb ? 0 : ga === genderFirst ? -1 : gb === genderFirst ? -1 : 1; - }) -} - -function orderByPreferredLanguage(preferredLanguage?: string[] | string): string[] { - preferredLanguage = Array.isArray(preferredLanguage) ? preferredLanguage : - preferredLanguage ? [preferredLanguage] : []; - - return [...(new Set([...preferredLanguage, ...navigatorLanguages()]))]; -} -function orderByPreferredRegion(preferredLanguage?: string[] | string): string[] { - preferredLanguage = Array.isArray(preferredLanguage) ? preferredLanguage : - preferredLanguage ? [preferredLanguage] : []; - - const regionByDefaultArray = Object.values(defaultRegion); - - return [...(new Set([...preferredLanguage, ...navigatorLanguages(), ...regionByDefaultArray]))]; -} - -const getLangFromBCP47Array = (a: string[]) => { - return [...(new Set(a.map((v) => extractLangRegionFromBCP47(v)[0]).filter((v) => !!v)))]; -} -const getRegionFromBCP47Array = (a: string[]) => { - return [...(new Set(a.map((v) => (extractLangRegionFromBCP47(v)[1] || "").toUpperCase()).filter((v) => !!v)))]; -} - -export function sortByLanguage(voices: ReadiumSpeechVoice[], preferredLanguage: string[] | string = [], localization: string | undefined = navigatorLang()): ReadiumSpeechVoice[] { - - const languages = getLangFromBCP47Array(orderByPreferredLanguage(preferredLanguage)); - - const voicesSorted: ReadiumSpeechVoice[] = []; - for (const lang of languages) { - voicesSorted.push(...voices.filter(({language: voiceLanguage}) => lang === extractLangRegionFromBCP47(voiceLanguage)[0])); - } - - let langueName: Intl.DisplayNames | undefined = undefined; - if (localization) { - try { - langueName = new Intl.DisplayNames([localization], { type: "language" }); - } catch (e) { - console.error("Intl.DisplayNames throw an exception with ", localization, e); - } - } - - const remainingVoices = voices.filter((v) => !voicesSorted.includes(v)); - remainingVoices.sort(({ language: a }, { language: b }) => { - - let nameA = a, nameB = b; - try { - if (langueName) { - nameA = langueName.of(extractLangRegionFromBCP47(a)[0]) || a; - nameB = langueName.of(extractLangRegionFromBCP47(b)[0]) || b; - } - } catch (e) { - // ignore - } - return nameA.localeCompare(nameB); - }); - - return [...voicesSorted, ...remainingVoices]; -} - -export function sortByRegion(voices: ReadiumSpeechVoice[], preferredRegions: string[] | string = [], localization: string | undefined = navigatorLang()): ReadiumSpeechVoice[] { - - const regions = getRegionFromBCP47Array(orderByPreferredRegion(preferredRegions)); - - const voicesSorted: ReadiumSpeechVoice[] = []; - for (const reg of regions) { - voicesSorted.push(...voices.filter(({language: voiceLanguage}) => reg === extractLangRegionFromBCP47(voiceLanguage)[1])); - } - - let regionName: Intl.DisplayNames | undefined = undefined; - if (localization) { - try { - regionName = new Intl.DisplayNames([localization], { type: "region" }); - } catch (e) { - console.error("Intl.DisplayNames throw an exception with ", localization, e); - } - } - - const remainingVoices = voices.filter((v) => !voicesSorted.includes(v)); - remainingVoices.sort(({ language: a }, { language: b }) => { - - let nameA = a, nameB = b; - try { - if (regionName) { - nameA = regionName.of(extractLangRegionFromBCP47(a)[1]) || a; - nameB = regionName.of(extractLangRegionFromBCP47(b)[1]) || b; - } - } catch (e) { - // ignore - } - return nameA.localeCompare(nameB); - }); - - return [...voicesSorted, ...remainingVoices]; -} - -export interface ILanguages { - label: string; - code: string; - count: number; -} -export function listLanguages(voices: ReadiumSpeechVoice[], localization: string | undefined = navigatorLang()): ILanguages[] { - let langueName: Intl.DisplayNames | undefined = undefined; - if (localization) { - try { - langueName = new Intl.DisplayNames([localization], { type: "language" }); - } catch (e) { - console.error("Intl.DisplayNames throw an exception with ", localization, e); - } - } - return voices.reduce((acc, cv) => { - const [lang] = extractLangRegionFromBCP47(cv.language); - let name = lang; - try { - if (langueName) { - name = langueName.of(lang) || lang; - } - } catch (e) { - console.error("langueName.of throw an error with ", lang, e); - } - const found = acc.find(({code}) => code === lang) - if (found) { - found.count++; - } else { - acc.push({code: lang, count: 1, label: name}); - } - return acc; - }, []); -} -export function listRegions(voices: ReadiumSpeechVoice[], localization: string | undefined = navigatorLang()): ILanguages[] { - let regionName: Intl.DisplayNames | undefined = undefined; - if (localization) { - try { - regionName = new Intl.DisplayNames([localization], { type: "region" }); - } catch (e) { - console.error("Intl.DisplayNames throw an exception with ", localization, e); - } - } - return voices.reduce((acc, cv) => { - const [,region] = extractLangRegionFromBCP47(cv.language); - let name = region; - try { - if (regionName) { - name = regionName.of(region) || region; - } - } catch (e) { - console.error("regionName.of throw an error with ", region, e); - } - const found = acc.find(({code}) => code === region); - if (found) { - found.count++; - } else { - acc.push({code: region, count: 1, label: name}); - } - return acc; - }, []); -} - -export type TGroupVoices = Map; -export function groupByLanguages(voices: ReadiumSpeechVoice[], preferredLanguage: string[] | string = [], localization: string | undefined = navigatorLang()): TGroupVoices { - - const voicesSorted = sortByLanguage(voices, preferredLanguage, localization); - - const languagesStructure = listLanguages(voicesSorted, localization); - const res: TGroupVoices = new Map(); - for (const { code, label } of languagesStructure) { - res.set(label, voicesSorted - .filter(({ language: voiceLang }) => { - const [l] = extractLangRegionFromBCP47(voiceLang); - return l === code; - })); - } - return res; -} - -export function groupByRegions(voices: ReadiumSpeechVoice[], preferredRegions: string[] | string = [], localization: string | undefined = navigatorLang()): TGroupVoices { - - const voicesSorted = sortByRegion(voices, preferredRegions, localization); - - const languagesStructure = listRegions(voicesSorted, localization); - const res: TGroupVoices = new Map(); - for (const { code, label } of languagesStructure) { - res.set(label, voicesSorted - .filter(({ language: voiceLang }) => { - const [, r] = extractLangRegionFromBCP47(voiceLang); - return r === code; - })); - } - return res; -} - -export function groupByKindOfVoices(allVoices: ReadiumSpeechVoice[]): TGroupVoices { - - const [recommendedVoices, lowQualityVoices] = filterOnRecommended(allVoices); - const remainingVoice = allVoices.filter((v) => !recommendedVoices.includes(v) && !lowQualityVoices.includes(v)); - const noveltyFiltered = filterOnNovelty(remainingVoice); - const noveltyVoices = remainingVoice.filter((v) => !noveltyFiltered.includes(v)); - const veryLowQualityFiltered = filterOnVeryLowQuality(remainingVoice); - const veryLowQualityVoices = remainingVoice.filter((v) => !veryLowQualityFiltered.includes(v)); - const remainingVoiceFiltered = filterOnNovelty(filterOnVeryLowQuality(remainingVoice)); - - const res: TGroupVoices = new Map(); - res.set("recommendedVoices", recommendedVoices); - res.set("lowerQuality", lowQualityVoices); - res.set("novelty", noveltyVoices); - res.set("veryLowQuality", veryLowQualityVoices); - res.set("remaining", remainingVoiceFiltered); - - return res; -} - -export function getLanguages(voices: ReadiumSpeechVoice[], preferredLanguage: string[] | string = [], localization: string | undefined = navigatorLang()): ILanguages[] { - - const group = groupByLanguages(voices, preferredLanguage, localization); - - return Array.from(group.entries()).map(([label, _voices]) => { - return {label, count: _voices.length, code: extractLangRegionFromBCP47(_voices[0]?.language || "")[0]} - }); -} - -/** - * Parse and extract SpeechSynthesisVoices, - * @returns ReadiumSpeechVoice[] - */ -export async function getVoices(preferredLanguage?: string[] | string, localization?: string) { - - const speechVoices = await getSpeechSynthesisVoices(); - const allVoices = removeDuplicate(parseSpeechSynthesisVoices(speechVoices)); - const recommendedTuple = filterOnRecommended(allVoices); - const [recommendedVoices, lowQualityVoices] = recommendedTuple; - const recommendedTupleFlatten = recommendedTuple.flat(); - const remainingVoices = allVoices - .map((allVoicesItem) => _strHash(allVoicesItem)) - .filter((str) => !recommendedTupleFlatten.find((recommendedVoicesPtr) => _strHash(recommendedVoicesPtr) === str)) - .map((str) => allVoices.find((allVoicesPtr) => _strHash(allVoicesPtr) === str)) - .filter((v) => !!v); - const remainingVoiceFiltered = filterOnNovelty(filterOnVeryLowQuality(remainingVoices)); - - - // console.log("PRE_recommendedVoices_GET_VOICES", recommendedVoices.filter(({label}) => label === "Paulina"), recommendedVoices.length); - - // console.log("PRE_lowQualityVoices_GET_VOICES", lowQualityVoices.filter(({label}) => label === "Paulina"), lowQualityVoices.length); - - // console.log("PRE_remainingVoiceFiltered_GET_VOICES", remainingVoiceFiltered.filter(({label}) => label === "Paulina"), remainingVoiceFiltered.length); - - // console.log("PRE_allVoices_GET_VOICES", allVoices.filter(({label}) => label === "Paulina"), allVoices.length); - - const voices = [recommendedVoices, lowQualityVoices, remainingVoiceFiltered].flat(); - - // console.log("MID_GET_VOICES", voices.filter(({label}) => label === "Paulina"), voices.length); - - const voicesSorted = sortByLanguage(sortByQuality(voices), preferredLanguage, localization || navigatorLang()); - - // console.log("POST_GET_VOICES", voicesSorted.filter(({ label }) => label === "Paulina"), voicesSorted.length); - - return voicesSorted; -} \ No newline at end of file diff --git a/src/voices/filters.ts b/src/voices/filters.ts new file mode 100644 index 0000000..93a2e0c --- /dev/null +++ b/src/voices/filters.ts @@ -0,0 +1,36 @@ +import type { ReadiumSpeechVoice } from "./types"; +import noveltyFilterData from "@json/filters/novelty.json"; +import veryLowQualityFilterData from "@json/filters/veryLowQuality.json"; + +interface FilterVoice { + name: string; + nativeID?: string[]; + altNames?: string[]; +} + +const noveltyFilter = noveltyFilterData as { voices: FilterVoice[] }; +const veryLowQualityFilter = veryLowQualityFilterData as { voices: FilterVoice[] }; + +export const isNoveltyVoice = (voiceName: string, voiceId?: string): boolean => { + return noveltyFilter.voices.some(filter => + voiceName.includes(filter.name) || + (voiceId && filter.nativeID?.some(id => voiceId.includes(id))) || + (filter.altNames?.some(name => voiceName.includes(name))) + ); +} + +export const isVeryLowQualityVoice = (voiceName: string, quality?: string[]): boolean => { + return veryLowQualityFilter.voices.some(filter => + voiceName.includes(filter.name) + ) || (Array.isArray(quality) && quality.includes("veryLow")); +} + +export const filterOutNoveltyVoices = (voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] => { + if (!voices?.length) return []; + return voices.filter(voice => !(voice.isNovelty || isNoveltyVoice(voice.name, voice.voiceURI))); +} + +export const filterOutVeryLowQualityVoices = (voices: ReadiumSpeechVoice[]): ReadiumSpeechVoice[] => { + if (!voices?.length) return []; + return voices.filter(voice => !isVeryLowQualityVoice(voice.name, voice.quality)); +} \ No newline at end of file diff --git a/src/voices/languages.ts b/src/voices/languages.ts new file mode 100644 index 0000000..f747b83 --- /dev/null +++ b/src/voices/languages.ts @@ -0,0 +1,199 @@ +import { extractLangRegionFromBCP47 } from "../utils/language"; +import type { ReadiumSpeechVoice, VoiceData, TGender, TQuality, TLocalizedName } from "./types"; + +// Import all language JSON files statically +import ar from "@json/ar.json"; +import bg from "@json/bg.json"; +import bho from "@json/bho.json"; +import bn from "@json/bn.json"; +import ca from "@json/ca.json"; +import cmn from "@json/cmn.json"; +import cs from "@json/cs.json"; +import da from "@json/da.json"; +import de from "@json/de.json"; +import el from "@json/el.json"; +import en from "@json/en.json"; +import es from "@json/es.json"; +import eu from "@json/eu.json"; +import fa from "@json/fa.json"; +import fi from "@json/fi.json"; +import fr from "@json/fr.json"; +import gl from "@json/gl.json"; +import he from "@json/he.json"; +import hi from "@json/hi.json"; +import hr from "@json/hr.json"; +import hu from "@json/hu.json"; +import id from "@json/id.json"; +import it from "@json/it.json"; +import ja from "@json/ja.json"; +import kn from "@json/kn.json"; +import ko from "@json/ko.json"; +import mr from "@json/mr.json"; +import ms from "@json/ms.json"; +import nb from "@json/nb.json"; +import nl from "@json/nl.json"; +import pl from "@json/pl.json"; +import pt from "@json/pt.json"; +import ro from "@json/ro.json"; +import ru from "@json/ru.json"; +import sk from "@json/sk.json"; +import sl from "@json/sl.json"; +import sv from "@json/sv.json"; +import ta from "@json/ta.json"; +import te from "@json/te.json"; +import th from "@json/th.json"; +import tr from "@json/tr.json"; +import uk from "@json/uk.json"; +import vi from "@json/vi.json"; +import wuu from "@json/wuu.json"; +import yue from "@json/yue.json"; + +// Helper function to cast voice data to the correct type +const castVoice = (voice: any): ReadiumSpeechVoice => ({ + ...voice, + gender: voice.gender as TGender | undefined, + quality: voice.quality ? (Array.isArray(voice.quality) + ? voice.quality.filter((q: any) => + ["veryLow", "low", "normal", "high", "veryHigh"].includes(q) + ) as TQuality[] + : [voice.quality].filter((q: any) => + ["veryLow", "low", "normal", "high", "veryHigh"].includes(q) + ) as TQuality[] + ) : undefined, + localizedName: voice.localizedName && ["android", "apple"].includes(voice.localizedName) + ? voice.localizedName as TLocalizedName + : undefined +}); + +// Map of language codes to their respective voice data with proper casting +const voiceDataMap: Record = Object.fromEntries( + Object.entries({ + ar, bg, bho, bn, ca, cmn, cs, da, de, el, en, es, eu, fa, fi, fr, gl, he, hi, + hr, hu, id, it, ja, kn, ko, mr, ms, nb, nl, pl, pt, ro, ru, sk, sl, sv, ta, + te, th, tr, uk, vi, wuu, yue + }).map(([lang, data]) => [ + lang, + { + ...data, + voices: data.voices.map(castVoice) + } + ]) +); + +// Helper function to get voice data synchronously +const getVoiceData = (lang: string): VoiceData | undefined => voiceDataMap[lang]; + +// Chinese variant mapping for special handling +export const chineseVariantMap: {[key: string]: string} = { + "cmn": "cmn", + "cmn-cn": "cmn", + "cmn-tw": "cmn", + "zh": "cmn", + "zh-cn": "cmn", + "zh-tw": "cmn", + "yue": "yue", + "yue-hk": "yue", + "zh-hk": "yue", + "wuu": "wuu", + "wuu-cn": "wuu" +}; + +/** + * Normalizes language code with special handling for Chinese variants + * @param lang - Input language code + * @returns Normalized language code + */ +const normalizeLanguageCode = (lang: string): string => { + if (!lang) return ""; + + const normalized = lang.toLowerCase().replace(/_/g, "-"); + return chineseVariantMap[normalized] || normalized; +}; + +/** + * Get all voices for a specific language + * @param {string} lang - Language code (e.g., "en", "fr", "zh-CN") + * @returns {ReadiumSpeechVoice[]} Array of voices for the specified language + */ +export const getVoices = (lang: string): ReadiumSpeechVoice[] => { + if (!lang) return []; + + try { + // Normalize the language code first + const normalizedLang = normalizeLanguageCode(lang); + + // Try with the normalized language code + let voiceData = getVoiceData(normalizedLang); + + // If no voices found and it's a Chinese variant, try with the base Chinese code + if ((!voiceData || !voiceData.voices?.length) && normalizedLang in chineseVariantMap) { + voiceData = getVoiceData("zh"); + } + + // If still no voices, try with the base language code + if (!voiceData || !voiceData.voices?.length) { + const [baseLang] = extractLangRegionFromBCP47(normalizedLang); + if (baseLang !== normalizedLang) { + voiceData = getVoiceData(baseLang); + } + } + + return voiceData?.voices || []; + } catch (error) { + console.error(`Failed to load voices for ${lang}:`, error); + return []; + } +}; + +/** + * Get all available language codes + * @returns {string[]} Array of available language codes + */ +export const getAvailableLanguages = (): string[] => Object.keys(voiceDataMap); + +/** + * Get the test utterance for a language + * @param {string} lang - Language code (e.g., "en", "fr", "zh-CN") + * @returns {string} The test utterance or empty string if not found + */ +export const getTestUtterance = (lang: string): string => { + if (!lang) return ""; + + try { + // Normalize the language code first + const normalizedLang = normalizeLanguageCode(lang); + + // Try with the normalized language code + let voiceData = getVoiceData(normalizedLang); + + // If no test utterance found and it's a Chinese variant, try with the mapped variant code + if ((!voiceData?.testUtterance) && normalizedLang in chineseVariantMap) { + const variantCode = chineseVariantMap[normalizedLang]; + if (variantCode) { + const variantData = getVoiceData(variantCode); + if (variantData?.testUtterance) { + return variantData.testUtterance; + } + } + } + + // If still no test utterance, try with the base language code + if (!voiceData?.testUtterance) { + const [baseLang] = extractLangRegionFromBCP47(normalizedLang); + if (baseLang !== normalizedLang) { + const baseLangData = getVoiceData(baseLang); + if (baseLangData?.testUtterance) { + return baseLangData.testUtterance; + } + } + } + + return voiceData?.testUtterance ?? ""; + } catch (error) { + console.error(`Failed to get test utterance for ${lang}:`, error); + return ""; + } +}; + +// Re-export types for backward compatibility +export * from "./types"; diff --git a/src/voices/types.ts b/src/voices/types.ts new file mode 100644 index 0000000..8fb0fcb --- /dev/null +++ b/src/voices/types.ts @@ -0,0 +1,71 @@ +// Auto-generated file - DO NOT EDIT + +/** + * Voice gender as defined in the schema + */ +export type TGender = "neutral" | "female" | "male"; + +/** + * Voice quality levels as defined in the schema + */ +export type TQuality = "veryLow" | "low" | "normal" | "high" | "veryHigh"; + +/** + * Localization type for voice names + */ +export type TLocalizedName = "android" | "apple"; + +export interface VoiceFilterData { + voices: Array<{ + name: string; + altNames?: string[]; + [key: string]: any; + }>; +} + +export interface ReadiumSpeechVoice { + // Core identification (required) + label: string; // Human-friendly label for the voice + name: string; // System/technical name (matches Web Speech API voiceURI) + voiceURI?: string; // For Web Speech API compatibility + + // Localization + language: string; // BCP-47 language tag + localizedName?: TLocalizedName; // Localization pattern (android/apple) + altNames?: string[]; // Alternative names (mostly for Apple voices) + altLanguage?: string; // Alternative BCP-47 language tag + otherLanguages?: string[]; // Other languages this voice can speak + multiLingual?: boolean; // If voice can handle multiple languages + + // Voice characteristics + gender?: TGender; // Voice gender + children?: boolean; // If this is a children's voice + + // Quality and capabilities + quality?: TQuality[]; // Available quality levels for this voice + pitchControl?: boolean; // Whether pitch can be controlled + + // Performance settings + pitch?: number; // Current pitch (0-2, where 1 is normal) + rate?: number; // Speech rate (0.1-10, where 1 is normal) + + // Platform and compatibility + browser?: string[]; // Supported browsers + os?: string[]; // Supported operating systems + preloaded?: boolean; // If the voice is preloaded on the system + nativeID?: string | string[]; // Platform-specific voice ID(s) + + // Additional metadata + note?: string; // Additional notes about the voice + provider?: string; // Voice provider (e.g., "Microsoft", "Google") + + // Allow any additional properties that might be in the JSON + [key: string]: any; +} + +export interface VoiceData { + language: string; // BCP-47 language tag + defaultRegion: string; // Default region for this language + testUtterance: string; // Sample text for testing the voice + voices: ReadiumSpeechVoice[]; // Array of available voices +} \ No newline at end of file diff --git a/test/WebSpeechVoiceManager.test.ts b/test/WebSpeechVoiceManager.test.ts new file mode 100644 index 0000000..24acf7c --- /dev/null +++ b/test/WebSpeechVoiceManager.test.ts @@ -0,0 +1,1303 @@ +import test, { type ExecutionContext } from "ava"; +import { WebSpeechVoiceManager, ReadiumSpeechVoice } from "../build/index.js"; + +// ============================================= +// Mock Data and Helpers +// ============================================= + +// Mock DisplayNames for testing +class MockDisplayNames { + options: any; + constructor(_: any, options: any) { + this.options = options; + } + + of(code: string): string { + if (this.options.type === "language") { + return `${code.toUpperCase()}_LANG`; + } + if (this.options.type === "region") { + return `${code.toUpperCase()}_REGION`; + } + return code; + } + + static supportedLocalesOf(locales: string[]): string[] { + return locales; + } +} + +// Mock Intl.DisplayNames +if (typeof (globalThis as any).Intl === "undefined") { + (globalThis as any).Intl = {}; +} +(globalThis as any).Intl.DisplayNames = MockDisplayNames as any; + +interface TestContext { + manager: WebSpeechVoiceManager; +} + + +// ============================================= +// Test Data +// ============================================= + +// Mock voices for testing +const mockVoices = [ + { + voiceURI: "voice1", + name: "Voice 1", + lang: "en-US", + localService: true, + default: true + }, + { + voiceURI: "voice2", + name: "Voice 2", + lang: "fr-FR", + localService: true, + default: false + }, + { + voiceURI: "voice3", + name: "Voice 3", + lang: "es-ES", + localService: true, + default: false + }, + { + voiceURI: "voice4", + name: "Voice 4", + lang: "de-DE", + localService: true, + default: false + }, + { + voiceURI: "voice5", + name: "Voice 5", + lang: "it-IT", + localService: true, + default: false + } +]; + +// Store original globals +const originalNavigator = globalThis.navigator; +const originalSpeechSynthesis = globalThis.speechSynthesis; + +// ============================================= +// Test Setup +// ============================================= + +// Test context type and setup +type TestFn = (t: ExecutionContext) => void | Promise; +const testWithContext = test as unknown as { + (name: string, fn: TestFn): void; + afterEach: { + always: (fn: (t: ExecutionContext) => void | Promise) => void; + }; + beforeEach: (fn: (t: ExecutionContext) => void | Promise) => void; +}; + +// Helper function to create test voice objects that match ReadiumSpeechVoice interface +function createTestVoice(overrides: Partial = {}): ReadiumSpeechVoice { + return { + label: overrides.name || "Test Voice", + name: overrides.name || "Test Voice", + voiceURI: `voice-${overrides.name || "test"}`, + language: "en-US", + ...overrides + }; +} + +// Set up global mocks before any tests run +if (typeof globalThis.window === "undefined") { + (globalThis as any).window = globalThis; +} + +// Mock the global objects +Object.defineProperty(globalThis, "navigator", { + value: { + ...originalNavigator, + languages: ["en-US", "fr-FR"] + }, + configurable: true, + writable: true +}); + +// Create a mock speechSynthesis object that matches the browser's API +const mockSpeechSynthesis = { + getVoices: () => mockVoices, + onvoiceschanged: null as (() => void) | null, + addEventListener: function(event: string, callback: () => void) { + if (event === "voiceschanged") { + this.onvoiceschanged = callback; + } + }, + removeEventListener: function(event: string) { + }, + _triggerVoicesChanged: function() { + if (this.onvoiceschanged) { + this.onvoiceschanged(); + } + } +}; + +// Mock the window.speechSynthesis to return our mock voices +Object.defineProperty(globalThis.window, "speechSynthesis", { + value: mockSpeechSynthesis, + configurable: true, + writable: true +}); + +// ============================================= +// Test Hooks +// ============================================= + +testWithContext.beforeEach(async (t) => { + // Reset singleton instance + (WebSpeechVoiceManager as any).instance = undefined; + (WebSpeechVoiceManager as any).initializationPromise = null; + + // Initialize and store the manager + t.context.manager = await WebSpeechVoiceManager.initialize(); +}); + +testWithContext.afterEach.always((t: ExecutionContext) => { + // Clean up + (WebSpeechVoiceManager as any).instance = undefined; + + // Restore original globals + Object.defineProperty(globalThis, "navigator", { + value: originalNavigator, + configurable: true, + writable: true + }); + + Object.defineProperty(globalThis, "speechSynthesis", { + value: originalSpeechSynthesis, + configurable: true, + writable: true + }); +}); + +// ============================================= +// 1. Initialization Tests +// ============================================= + +testWithContext("initialize: returns singleton instance", async (t) => { + const instance1 = await WebSpeechVoiceManager.initialize(); + const instance2 = await WebSpeechVoiceManager.initialize(); + t.is(instance1, instance2); +}); + +testWithContext("initialize: loads voices and gets voices successfully", (t) => { + const manager = t.context.manager; + const voices = manager.getVoices(); + t.true(Array.isArray(voices)); + t.true(voices.length > 0); +}); + +// ============================================= +// 2. Voice Retrieval Tests +// ============================================= + +testWithContext("getVoices: returns all voices when no filters are provided", (t) => { + const voices = t.context.manager.getVoices(); + t.is(voices.length, mockVoices.length); +}); + +testWithContext("getVoices: throws if not initialized", (t) => { + // Create a new instance without initializing + const manager = new (WebSpeechVoiceManager as any)(); + t.throws(() => manager.getVoices(), { + message: 'WebSpeechVoiceManager not initialized. Call initialize() first.' + }); +}); + +testWithContext("getVoices: combines all filters", async (t: ExecutionContext) => { + const manager = t.context.manager; + + (manager as any).voices = [ + createTestVoice({ name: "English Male High", language: "en-US", gender: "male", quality: ["high"], provider: "Google", offlineAvailability: true }), + createTestVoice({ name: "English Female Normal", language: "en-US", gender: "female", quality: ["normal"], provider: "Microsoft", offlineAvailability: false }), + createTestVoice({ name: "French Male Low", language: "fr-FR", gender: "male", quality: ["low"], provider: "Google", offlineAvailability: true }), + createTestVoice({ name: "French Female High", language: "fr-FR", gender: "female", quality: ["high"], provider: "Amazon", offlineAvailability: false }), + createTestVoice({ name: "Spanish Male Normal", language: "es-ES", gender: "male", quality: ["normal"], provider: "Microsoft", offlineAvailability: true }) + ]; + + // Test with all filters combined + const filtered = await manager.getVoices({ + language: ["en", "fr"], + gender: "male", + quality: ["high", "normal"], + provider: "Google", + offlineOnly: true, + excludeNovelty: true, + excludeVeryLowQuality: true + }); + + t.is(filtered.length, 1); + t.true(filtered.every(v => + (v.language.startsWith("en") || v.language.startsWith("fr")) && + v.gender === "male" && + (v.quality?.includes("high") || v.quality?.includes("normal")) && + v.provider === "Google" && + v.offlineAvailability === true + )); +}); + +testWithContext("getVoices: handles empty navigator.languages", async (t) => { + const manager = t.context.manager; + + // Create test voices + const testVoices = [ + { voiceURI: "voice1", name: "Voice 1", language: "en-US" }, + { voiceURI: "voice2", name: "Voice 2", language: "fr-FR" } + ]; + + // Replace the voices in the manager + (manager as any).voices = testVoices; + + // Mock empty navigator.languages + const originalLanguages = [...(globalThis.navigator as any).languages]; + (globalThis.navigator as any).languages = []; + + try { + const voices = await manager.getVoices(); + + // Should still return all voices even with empty languages + t.is(voices.length, 2); + } finally { + // Restore original languages + (globalThis.navigator as any).languages = originalLanguages; + } +}); + +testWithContext("getVoices: handles undefined navigator.languages", async (t) => { + const manager = t.context.manager; + + // Create test voices + const testVoices = [ + { voiceURI: "voice1", name: "Voice 1", language: "en-US" }, + { voiceURI: "voice2", name: "Voice 2", language: "fr-FR" } + ]; + + // Replace the voices in the manager + (manager as any).voices = testVoices; + + // Mock undefined navigator.languages + const originalLanguages = (globalThis.navigator as any).languages; + delete (globalThis.navigator as any).languages; + + try { + const voices = await manager.getVoices(); + + // Should still return all voices even with undefined languages + t.is(voices.length, 2); + } finally { + // Restore original languages + (globalThis.navigator as any).languages = originalLanguages; + } +}); + + +testWithContext("getVoices: returns empty array when no voices are available", async (t) => { + // Save the original getVoices implementation + const originalGetVoices = mockSpeechSynthesis.getVoices; + + try { + // Override getVoices to return empty array + mockSpeechSynthesis.getVoices = () => []; + + // Create a fresh instance to avoid interference from other tests + (WebSpeechVoiceManager as any).instance = undefined; + const manager = await WebSpeechVoiceManager.initialize(); + + // Reset initialization to force re-initialization with empty voices + (manager as any).initializationPromise = null; + (manager as any).voices = []; + (manager as any).browserVoices = []; + + // Should return empty array when no voices are available + const voices = manager.getVoices(); + t.deepEqual(voices, []); + } finally { + // Restore original getVoices implementation + mockSpeechSynthesis.getVoices = originalGetVoices; + } +}); + +testWithContext("getVoices: filters by language", async (t: ExecutionContext) => { + const manager = t.context.manager; + + // Single language + let voices = await manager.getVoices({ language: "en" }); + t.true(voices.length > 0); + t.true(voices.every((v: ReadiumSpeechVoice) => v.language.startsWith("en"))); + + // Multiple languages + voices = await manager.getVoices({ language: ["en", "fr"] }); + t.true(voices.length > 1); + t.true(voices.some((v: ReadiumSpeechVoice) => v.language.startsWith("en"))); + t.true(voices.some((v: ReadiumSpeechVoice) => v.language.startsWith("fr"))); +}); + +testWithContext("getVoices: filters by quality", async (t: ExecutionContext) => { + const manager = t.context.manager; + + // Mock quality property on voices + const voices = await manager.getVoices(); + const voicesWithQuality = voices.map((v: ReadiumSpeechVoice, i: number) => ({ + ...v, + quality: i % 2 === 0 ? ["high"] : ["low"] + })); + + // Replace the voices in the manager + (manager as any).voices = voicesWithQuality; + + const highQualityVoices = await manager.getVoices({ quality: "high" }); + t.true(highQualityVoices.length > 0); + t.true(highQualityVoices.every((v: ReadiumSpeechVoice) => v.quality?.includes("high") ?? false)); +}); + +testWithContext("getVoices: returns empty array when speechSynthesis is not available", async (t) => { + // Save original + const originalSpeechSynthesis = globalThis.speechSynthesis; + + try { + // Mock speechSynthesis to be undefined + Object.defineProperty(globalThis, "speechSynthesis", { + value: undefined, + configurable: true, + writable: true + }); + + // Create a new instance + (WebSpeechVoiceManager as any).instance = undefined; + const manager = await WebSpeechVoiceManager.initialize(); + + // Should return empty array when speechSynthesis is not available + const voices = manager.getVoices(); + t.deepEqual(voices, []); + } finally { + // Restore + Object.defineProperty(globalThis, "speechSynthesis", { + value: originalSpeechSynthesis, + configurable: true, + writable: true + }); + } +}); + +// ============================================= +// 3. Language Retrieval Tests +// ============================================= + +testWithContext("getLanguages: returns available languages with counts", async (t: ExecutionContext) => { + const languages = await t.context.manager.getLanguages(); + t.true(Array.isArray(languages)); + + // Check that we have at least one language + t.true(languages.length > 0); + + // Check structure of language entries + for (const lang of languages) { + t.truthy(lang.code); + t.truthy(lang.label); + t.true(typeof lang.count === "number"); + } +}); + +testWithContext("getLanguages: handles empty voices array", async (t: ExecutionContext) => { + // Save the original getVoices implementation + const originalGetVoices = mockSpeechSynthesis.getVoices; + + try { + // Override getVoices to return empty array + mockSpeechSynthesis.getVoices = () => []; + + // Create a fresh instance to avoid interference + (WebSpeechVoiceManager as any).instance = undefined; + const manager = await WebSpeechVoiceManager.initialize(); + + // Reset initialization to force re-initialization with empty voices + (manager as any).initializationPromise = null; + (manager as any).voices = []; + (manager as any).browserVoices = []; + + const languages = manager.getLanguages(); + t.deepEqual(languages, []); + } finally { + // Restore original getVoices implementation + mockSpeechSynthesis.getVoices = originalGetVoices; + } +}); + +// ============================================= +// 4. Region Retrieval Tests +// ============================================= + +testWithContext("getRegions: returns available regions with counts", async (t: ExecutionContext) => { + const regions = await t.context.manager.getRegions(); + t.true(Array.isArray(regions)); + + // Check that we have at least one region + t.true(regions.length > 0); + + // Check structure of region entries + for (const region of regions) { + t.truthy(region.code); + t.truthy(region.label); + t.true(typeof region.count === "number"); + } +}); + +testWithContext("getRegions: handles empty voices array", async (t: ExecutionContext) => { + // Create a fresh instance to avoid interference + (WebSpeechVoiceManager as any).instance = undefined; + const manager = await WebSpeechVoiceManager.initialize(); + + // Mock empty voices array + const emptyMockVoices: any[] = []; + const mockSpeechSynthesis = { + getVoices: () => emptyMockVoices, + onvoiceschanged: null as (() => void) | null, + addEventListener: function(event: string, callback: () => void) { + if (event === "voiceschanged") { + this.onvoiceschanged = callback; + } + }, + removeEventListener: function(event: string) { + }, + _triggerVoicesChanged: function() { + if (this.onvoiceschanged) { + this.onvoiceschanged(); + } + } + }; + + Object.defineProperty(globalThis.window, "speechSynthesis", { + value: mockSpeechSynthesis, + configurable: true, + writable: true + }); + + try { + // Reset initialization + (manager as any).initializationPromise = null; + (manager as any).voices = []; + (manager as any).browserVoices = []; + + const regions = manager.getRegions(); + t.deepEqual(regions, []); + } finally { + // Restore for other tests + Object.defineProperty(globalThis.window, "speechSynthesis", { + value: { + getVoices: () => mockVoices, + onvoiceschanged: null as (() => void) | null, + addEventListener: function(event: string, callback: () => void) { + if (event === "voiceschanged") { + this.onvoiceschanged = callback; + } + }, + removeEventListener: function(event: string) { + }, + _triggerVoicesChanged: function() { + if (this.onvoiceschanged) { + this.onvoiceschanged(); + } + } + }, + configurable: true, + writable: true + }); + } +}); + +// ============================================= +// 5. Default Voice Retrieval Tests +// ============================================= + +testWithContext("getDefaultVoice: returns default voice for language", async (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with one default voice for en-US + const testVoices = [ + { + voiceURI: "voice1", + name: "Voice 1", + language: "en-US", + isDefault: true, + quality: ["high"] + }, + { + voiceURI: "voice2", + name: "Voice 2", + language: "en-US", + isDefault: false, + quality: ["normal"] + } + ]; + + // Replace the voices in the manager + (manager as any).voices = testVoices; + + const defaultVoice = await manager.getDefaultVoice("en-US"); + t.truthy(defaultVoice); + t.is(defaultVoice?.voiceURI, "voice1"); +}); + +testWithContext("getDefaultVoice: returns undefined when no voices available", async (t) => { + // Create a fresh instance to avoid interference + (WebSpeechVoiceManager as any).instance = undefined; + const manager = await WebSpeechVoiceManager.initialize(); + + // Mock empty voices array + const emptyMockVoices: any[] = []; + const mockSpeechSynthesis = { + getVoices: () => emptyMockVoices, + onvoiceschanged: null as (() => void) | null, + addEventListener: function(event: string, callback: () => void) { + if (event === "voiceschanged") { + this.onvoiceschanged = callback; + } + }, + removeEventListener: function(event: string) { + }, + _triggerVoicesChanged: function() { + if (this.onvoiceschanged) { + this.onvoiceschanged(); + } + } + }; + + Object.defineProperty(globalThis.window, "speechSynthesis", { + value: mockSpeechSynthesis, + configurable: true, + writable: true + }); + + try { + // Reset initialization + (manager as any).initializationPromise = null; + (manager as any).voices = []; + (manager as any).browserVoices = []; + + const defaultVoice = manager.getDefaultVoice("en-US"); + t.is(defaultVoice, null); + } finally { + // Restore for other tests + Object.defineProperty(globalThis.window, "speechSynthesis", { + value: { + getVoices: () => mockVoices, + onvoiceschanged: null as (() => void) | null, + addEventListener: function(event: string, callback: () => void) { + if (event === "voiceschanged") { + this.onvoiceschanged = callback; + } + }, + removeEventListener: function(event: string) { + }, + _triggerVoicesChanged: function() { + if (this.onvoiceschanged) { + this.onvoiceschanged(); + } + } + }, + configurable: true, + writable: true + }); + } +}); + +testWithContext("getDefaultVoice: returns null when no matching language", async (t: ExecutionContext) => { + const manager = t.context.manager; + + // Test with language that has no voices + const result = await manager.getDefaultVoice("xx-XX"); + t.is(result, null); +}); + +// ============================================= +// 6. Test Utterance Retrieval Tests +// ============================================= + +testWithContext("getTestUtterance: returns test utterance for supported language", (t) => { + const manager = t.context.manager; + + // Test with a base language + const utterance1 = manager.getTestUtterance("en"); + t.is(typeof utterance1, "string"); + t.true(utterance1 && utterance1.length > 0); + + // Test with a locale variant (should fall back to base language) + const utterance2 = manager.getTestUtterance("en-US"); + t.is(typeof utterance2, "string"); + t.true(utterance2 && utterance2.length > 0); + t.is(utterance1, utterance2); // Should be the same +}); + +testWithContext("getTestUtterance: returns empty string for unsupported language", (t) => { + const manager = t.context.manager; + + // Test with an unsupported language + const utterance = manager.getTestUtterance("xx-XX"); + t.is(utterance, ""); +}); + +// ============================================= +// 7. Voice Filtering Tests +// ============================================= + +testWithContext("filterVoices: filters by language", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices + const testVoices = [ + createTestVoice({ name: "English Voice 1", language: "en-US" }), + createTestVoice({ name: "English Voice 2", language: "en-GB" }), + createTestVoice({ name: "French Voice", language: "fr-FR" }), + createTestVoice({ name: "Spanish Voice", language: "es-ES" }) + ]; + + const englishVoices = manager.filterVoices(testVoices, { language: "en" }); + t.is(englishVoices.length, 2); + t.true(englishVoices.every(v => v.language.startsWith("en"))); + + const multiLangVoices = manager.filterVoices(testVoices, { language: ["en", "fr"] }); + t.is(multiLangVoices.length, 3); + t.true(multiLangVoices.every(v => v.language.startsWith("en") || v.language.startsWith("fr"))); +}); + +testWithContext("filterVoices: filters by gender", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different genders + const testVoices = [ + createTestVoice({ name: "Male Voice 1", language: "en-US", gender: "male" }), + createTestVoice({ name: "Female Voice 1", language: "en-US", gender: "female" }), + createTestVoice({ name: "Male Voice 2", language: "en-US", gender: "male" }), + createTestVoice({ name: "Unknown Gender Voice", language: "en-US" }) + ]; + + const maleVoices = manager.filterVoices(testVoices, { gender: "male" }); + t.is(maleVoices.length, 2); + t.true(maleVoices.every(v => v.gender === "male")); + + const femaleVoices = manager.filterVoices(testVoices, { gender: "female" }); + t.is(femaleVoices.length, 1); + t.is(femaleVoices[0].gender, "female"); +}); + +testWithContext("filterVoices: filters by quality array", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different quality levels + const testVoices = [ + createTestVoice({ name: "High Quality Voice", language: "en-US", quality: ["high"] }), + createTestVoice({ name: "Low Quality Voice", language: "en-US", quality: ["low"] }), + createTestVoice({ name: "Normal Quality Voice", language: "en-US", quality: ["normal"] }), + createTestVoice({ name: "Very High Quality Voice", language: "en-US", quality: ["veryHigh"] }), + createTestVoice({ name: "Multi Quality Voice", language: "en-US", quality: ["high", "normal"] }), + createTestVoice({ name: "No Quality Voice", language: "en-US", quality: undefined }) + ]; + + // Test single quality filter + const highQualityVoices = manager.filterVoices(testVoices, { quality: "high" }); + t.is(highQualityVoices.length, 2); // high and multi quality voices + + // Test multiple quality filter + const multiQualityVoices = manager.filterVoices(testVoices, { quality: ["high", "normal"] }); + t.is(multiQualityVoices.length, 3); // high, normal, and multi quality voices + + // Test that undefined quality voices are filtered out + const filteredVoices = manager.filterVoices(testVoices, { quality: "high" }); + t.false(filteredVoices.some(v => v.quality === undefined)); +}); + +testWithContext("filterVoices: filters out novelty and low quality voices", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices using the createTestVoice helper + const testVoices = [ + createTestVoice({ + voiceURI: "com.apple.speech.synthesis.voice.Albert", + name: "Albert", + language: "en-US", + isNovelty: true + }), + createTestVoice({ + voiceURI: "com.appk.it.speech.synthesis.voice.Eddy", + name: "Eddy", + language: "en-US", + quality: ["veryLow"] + }) + ]; + + // Test filtering with default options (should filter out both voices) + const filteredVoices = manager.filterVoices(testVoices, { + excludeNovelty: true, + excludeVeryLowQuality: true + }); + t.is(filteredVoices.length, 0, "Should filter out all test voices by default"); + + // Test including them by disabling the filters + const allVoices = manager.filterVoices(testVoices, { + excludeNovelty: false, + excludeVeryLowQuality: false + }); + t.is(allVoices.length, 2, "Should include all voices when not filtered"); +}); + +testWithContext("filterVoices: filters by offline availability", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different offline availability + const testVoices = [ + createTestVoice({ name: "Offline Voice 1", language: "en-US", offlineAvailability: true }), + createTestVoice({ name: "Online Voice 1", language: "en-US", offlineAvailability: false }), + createTestVoice({ name: "Offline Voice 2", language: "en-US", offlineAvailability: true }), + createTestVoice({ name: "Undefined Availability Voice", language: "en-US" }) + ]; + + const offlineVoices = manager.filterVoices(testVoices, { offlineOnly: true }); + t.is(offlineVoices.length, 2); + t.true(offlineVoices.every(v => v.offlineAvailability === true)); + + // Test that undefined and false values are filtered out + t.false(offlineVoices.some(v => v.offlineAvailability === false)); + t.false(offlineVoices.some(v => v.offlineAvailability === undefined)); +}); + +testWithContext("filterVoices: filters by provider", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different providers + const testVoices = [ + createTestVoice({ name: "Google Voice", language: "en-US", provider: "Google" }), + createTestVoice({ name: "Microsoft Voice", language: "en-US", provider: "Microsoft" }), + createTestVoice({ name: "Amazon Voice", language: "en-US", provider: "Amazon" }), + createTestVoice({ name: "Another Google Voice", language: "en-US", provider: "Google" }) + ]; + + const googleVoices = manager.filterVoices(testVoices, { provider: "Google" }); + t.is(googleVoices.length, 2); + t.true(googleVoices.every(v => v.provider === "Google")); + + // Test case insensitive matching + const caseInsensitiveVoices = manager.filterVoices(testVoices, { provider: "google" }); + t.is(caseInsensitiveVoices.length, 2); +}); + +testWithContext("filterVoices: combines multiple filters", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with various properties + const testVoices = [ + createTestVoice({ name: "Male High Quality English", language: "en-US", gender: "male", quality: ["high"], provider: "Google" }), + createTestVoice({ name: "Female Low Quality English", language: "en-US", gender: "female", quality: ["low"], provider: "Google" }), + createTestVoice({ name: "Male High Quality French", language: "fr-FR", gender: "male", quality: ["high"], provider: "Microsoft" }), + createTestVoice({ name: "Female Normal Quality English", language: "en-US", gender: "female", quality: ["normal"], provider: "Google" }) + ]; + + // Filter by language and gender + const englishFemaleVoices = manager.filterVoices(testVoices, { + language: "en", + gender: "female" + }); + t.is(englishFemaleVoices.length, 2); + t.true(englishFemaleVoices.every(v => + v.language.startsWith("en") && v.gender === "female" + )); + + // Filter by quality and provider + const highQualityGoogleVoices = manager.filterVoices(testVoices, { + quality: "high", + provider: "Google" + }); + t.is(highQualityGoogleVoices.length, 1); + t.is(highQualityGoogleVoices[0].name, "Male High Quality English"); +}); + +testWithContext("filterVoices: handles edge cases", (t: ExecutionContext) => { + const manager = t.context.manager; + + const testVoices = [ + createTestVoice({ name: "Voice 1", language: "en-US", gender: "male", quality: ["high"] }), + createTestVoice({ name: "Voice 2", language: "fr-FR", gender: "female", quality: ["low"] }), + createTestVoice({ name: "Voice 3", language: "de-DE", gender: "male", quality: ["normal"] }) + ]; + + // Test empty filter arrays + const emptyLanguageFilter = manager.filterVoices(testVoices, { language: [] }); + t.is(emptyLanguageFilter.length, 0); + + const emptyQualityFilter = manager.filterVoices(testVoices, { quality: [] }); + t.is(emptyQualityFilter.length, 0); + + // Test case sensitivity for language + const caseSensitiveLanguage = manager.filterVoices(testVoices, { language: "EN-us" }); + t.is(caseSensitiveLanguage.length, 1); // Should match due to toLowerCase() + + // Test invalid quality values - cast to any for testing invalid input + const invalidQualityFilter = manager.filterVoices(testVoices, { quality: "invalid" as any }); + t.is(invalidQualityFilter.length, 0); +}); + +testWithContext("filterVoices: uses array values for multiple filters", (t: ExecutionContext) => { + const manager = t.context.manager; + + const testVoices = [ + createTestVoice({ name: "English Male", language: "en-US", gender: "male", quality: ["high"] }), + createTestVoice({ name: "English Female", language: "en-US", gender: "female", quality: ["normal"] }), + createTestVoice({ name: "French Male", language: "fr-FR", gender: "male", quality: ["low"] }), + createTestVoice({ name: "French Female", language: "fr-FR", gender: "female", quality: ["high"] }), + createTestVoice({ name: "Spanish Male", language: "es-ES", gender: "male", quality: ["normal"] }) + ]; + + // Test with array of languages and array of qualities + const filtered = manager.filterVoices(testVoices, { + language: ["en", "fr"], + quality: ["high", "normal"] + }); + t.is(filtered.length, 3); + t.true(filtered.every(v => + (v.language.startsWith("en") || v.language.startsWith("fr")) && + (v.quality?.includes("high") || v.quality?.includes("normal")) + )); +}); + +testWithContext("filterOutNoveltyVoices: removes novelty voices", (t: ExecutionContext) => { + const manager = t.context.manager; + + const testVoices = [ + createTestVoice({ name: "Regular Voice 1", language: "en-US" }), + createTestVoice({ name: "Novelty Voice 1", language: "en-US", isNovelty: true }), + createTestVoice({ name: "Regular Voice 2", language: "en-US" }), + createTestVoice({ name: "Novelty Voice 2", language: "en-US", isNovelty: true }) + ]; + + const filtered = manager.filterOutNoveltyVoices(testVoices); + t.is(filtered.length, 2); + t.false(filtered.some((v: ReadiumSpeechVoice) => v.isNovelty)); +}); + +testWithContext("filterOutVeryLowQualityVoices: removes very low quality voices", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with one very low quality voice + const testVoices = [ + createTestVoice({ name: "Voice 1", language: "en-US", quality: ["normal"] }), + createTestVoice({ name: "Low Quality Voice", language: "en-US", quality: ["veryLow"] }), + createTestVoice({ name: "Voice 2", language: "fr-FR", quality: ["normal"] }) + ]; + + const filtered = manager.filterOutVeryLowQualityVoices(testVoices); + t.is(filtered.length, testVoices.length - 1); + t.false(filtered.some((v: ReadiumSpeechVoice) => v.quality?.includes("veryLow"))); +}); + +// ============================================= +// 8. Voice Sorting Tests +// ============================================= + +testWithContext("sortVoices: sorts by name", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices + const testVoices = [ + createTestVoice({ name: "Zeta Voice", language: "en-US" }), + createTestVoice({ name: "Alpha Voice", language: "en-US" }), + createTestVoice({ name: "Beta Voice", language: "en-US" }) + ]; + + // Test ascending order + const sortedAsc = manager.sortVoices(testVoices, { by: "name", order: "asc" }); + t.is(sortedAsc[0].name, "Alpha Voice"); + t.is(sortedAsc[1].name, "Beta Voice"); + t.is(sortedAsc[2].name, "Zeta Voice"); + + // Test descending order + const sortedDesc = manager.sortVoices(testVoices, { by: "name", order: "desc" }); + t.is(sortedDesc[0].name, "Zeta Voice"); + t.is(sortedDesc[1].name, "Beta Voice"); + t.is(sortedDesc[2].name, "Alpha Voice"); +}); + +testWithContext("sortVoices: sorts by quality with proper direction", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different quality levels + const testVoices = [ + createTestVoice({ name: "High Quality Voice", language: "en-US", quality: ["high"] }), + createTestVoice({ name: "Low Quality Voice", language: "en-US", quality: ["low"] }), + createTestVoice({ name: "Normal Quality Voice", language: "en-US", quality: ["normal"] }), + createTestVoice({ name: "Very High Quality Voice", language: "en-US", quality: ["veryHigh"] }), + createTestVoice({ name: "Very Low Quality Voice", language: "en-US", quality: ["veryLow"] }) + ]; + + // Test ascending order (low to high quality) + const sortedAsc = manager.sortVoices(testVoices, { by: "quality", order: "asc" }); + t.is(sortedAsc[0].quality?.[0], "veryLow"); + t.is(sortedAsc[1].quality?.[0], "low"); + t.is(sortedAsc[2].quality?.[0], "normal"); + t.is(sortedAsc[3].quality?.[0], "high"); + t.is(sortedAsc[4].quality?.[0], "veryHigh"); + + // Test descending order (high to low quality) + const sortedDesc = manager.sortVoices(testVoices, { by: "quality", order: "desc" }); + t.is(sortedDesc[0].quality?.[0], "veryHigh"); + t.is(sortedDesc[1].quality?.[0], "high"); + t.is(sortedDesc[2].quality?.[0], "normal"); + t.is(sortedDesc[3].quality?.[0], "low"); + t.is(sortedDesc[4].quality?.[0], "veryLow"); +}); + +testWithContext("sortVoices: sorts by language", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different languages + const testVoices = [ + createTestVoice({ name: "French Voice", language: "fr-FR" }), + createTestVoice({ name: "English Voice", language: "en-US" }), + createTestVoice({ name: "Spanish Voice", language: "es-ES" }), + createTestVoice({ name: "German Voice", language: "de-DE" }) + ]; + + // Test ascending order + const sortedAsc = manager.sortVoices(testVoices, { by: "language", order: "asc" }); + t.is(sortedAsc[0].language, "de-DE"); + t.is(sortedAsc[1].language, "en-US"); + t.is(sortedAsc[2].language, "es-ES"); + t.is(sortedAsc[3].language, "fr-FR"); + + // Test descending order + const sortedDesc = manager.sortVoices(testVoices, { by: "language", order: "desc" }); + t.is(sortedDesc[0].language, "fr-FR"); + t.is(sortedDesc[1].language, "es-ES"); + t.is(sortedDesc[2].language, "en-US"); + t.is(sortedDesc[3].language, "de-DE"); +}); + +testWithContext("sortVoices: sorts by gender", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different genders + const testVoices = [ + createTestVoice({ name: "Female Voice 1", language: "en-US", gender: "female" }), + createTestVoice({ name: "Male Voice 1", language: "en-US", gender: "male" }), + createTestVoice({ name: "Unknown Voice", language: "en-US" }), + createTestVoice({ name: "Female Voice 2", language: "en-US", gender: "female" }) + ]; + + // Test ascending order (undefined should come first, then female, then male) + const sortedAsc = manager.sortVoices(testVoices, { by: "gender", order: "asc" }); + t.is(sortedAsc[0].gender, undefined); + t.is(sortedAsc[1].gender, "female"); + t.is(sortedAsc[2].gender, "female"); + t.is(sortedAsc[3].gender, "male"); + + // Test descending order (male should come first, then female, then undefined) + const sortedDesc = manager.sortVoices(testVoices, { by: "gender", order: "desc" }); + t.is(sortedDesc[0].gender, "male"); + t.is(sortedDesc[1].gender, "female"); + t.is(sortedDesc[2].gender, "female"); + t.is(sortedDesc[3].gender, undefined); +}); + +testWithContext("sortVoices: sorts by region", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different regions + const testVoices = [ + createTestVoice({ name: "US Voice", language: "en-US" }), + createTestVoice({ name: "UK Voice", language: "en-GB" }), + createTestVoice({ name: "Canada Voice", language: "en-CA" }), + createTestVoice({ name: "Australia Voice", language: "en-AU" }) + ]; + + // Test ascending order + const sortedAsc = manager.sortVoices(testVoices, { by: "region", order: "asc" }); + t.is(sortedAsc[0].language, "en-AU"); + t.is(sortedAsc[1].language, "en-CA"); + t.is(sortedAsc[2].language, "en-GB"); + t.is(sortedAsc[3].language, "en-US"); + + // Test descending order + const sortedDesc = manager.sortVoices(testVoices, { by: "region", order: "desc" }); + t.is(sortedDesc[0].language, "en-US"); + t.is(sortedDesc[1].language, "en-GB"); + t.is(sortedDesc[2].language, "en-CA"); + t.is(sortedDesc[3].language, "en-AU"); +}); + +testWithContext("sortVoices: sorts by preferred languages", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different languages and regions + const testVoices = [ + createTestVoice({ name: "French Voice", language: "fr-FR" }), + createTestVoice({ name: "US English Voice", language: "en-US" }), + createTestVoice({ name: "UK English Voice", language: "en-GB" }), + createTestVoice({ name: "German Voice", language: "de-DE" }), + createTestVoice({ name: "Spanish Voice", language: "es-ES" }), + createTestVoice({ name: "French Canadian Voice", language: "fr-CA" }) + ]; + + // Test with preferred languages (exact matches first, then partial matches) + const preferredLangs = ["en-US", "fr", "es-ES"]; + const sorted = manager.sortVoices(testVoices, { + by: "language", + preferredLanguages: preferredLangs + }); + + // Exact matches should come first in the order of preferredLanguages + t.is(sorted[0].language, "en-US"); // Exact match + t.is(sorted[1].language, "fr-CA"); // Partial match for "fr" - sorts by region code + t.is(sorted[2].language, "fr-FR"); // Also partial match for "fr" - sorts by region code + t.is(sorted[3].language, "es-ES"); // Exact match + + // Non-preferred languages should come after, sorted alphabetically + t.is(sorted[4].language, "de-DE"); + t.is(sorted[5].language, "en-GB"); + + // Test with region-specific preferences + const regionSpecific = manager.sortVoices(testVoices, { + by: "language", + preferredLanguages: ["fr-CA", "en-GB"] + }); + + t.is(regionSpecific[0].language, "fr-CA"); // Exact match + t.is(regionSpecific[1].language, "en-GB"); // Exact match + // Others should be sorted alphabetically + t.is(regionSpecific[2].language, "de-DE"); + t.is(regionSpecific[3].language, "en-US"); + t.is(regionSpecific[4].language, "es-ES"); + t.is(regionSpecific[5].language, "fr-FR"); + + // Test with empty preferred languages (should sort alphabetically) + const emptyPreferred = manager.sortVoices(testVoices, { + by: "language", + preferredLanguages: [] + }); + t.is(emptyPreferred[0].language, "de-DE"); + t.is(emptyPreferred[1].language, "en-GB"); + t.is(emptyPreferred[2].language, "en-US"); + t.is(emptyPreferred[3].language, "es-ES"); + t.is(emptyPreferred[4].language, "fr-CA"); + t.is(emptyPreferred[5].language, "fr-FR"); + + // Test with undefined preferred languages (should sort alphabetically) + const undefinedPreferred = manager.sortVoices(testVoices, { + by: "language" + }); + t.is(undefinedPreferred[0].language, "de-DE"); + t.is(undefinedPreferred[1].language, "en-GB"); + t.is(undefinedPreferred[2].language, "en-US"); + t.is(undefinedPreferred[3].language, "es-ES"); + t.is(undefinedPreferred[4].language, "fr-CA"); + t.is(undefinedPreferred[5].language, "fr-FR"); + + // Test with case-insensitive matching + const caseInsensitive = manager.sortVoices(testVoices, { + by: "language", + preferredLanguages: ["EN-us", "FR"] // Mixed case and partial + }); + t.is(caseInsensitive[0].language, "en-US"); // Matches despite case difference + t.is(caseInsensitive[1].language, "fr-CA"); // Partial match, sorted by region + t.is(caseInsensitive[2].language, "fr-FR"); // Also partial match +}); + +testWithContext("sortVoices: sorts by region with preferred languages", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different regions + const testVoices = [ + createTestVoice({ name: "US English", language: "en-US" }), + createTestVoice({ name: "UK English", language: "en-GB" }), + createTestVoice({ name: "Australian English", language: "en-AU" }), + createTestVoice({ name: "Canadian French", language: "fr-CA" }), + createTestVoice({ name: "French", language: "fr-FR" }), + createTestVoice({ name: "Canadian English", language: "en-CA" }) + ]; + + // Test with preferred languages that include regions + const sorted = manager.sortVoices(testVoices, { + by: "region", + preferredLanguages: ["en-CA", "fr-CA", "en"] // Prefer Canadian English, then Canadian French, then any English + }); + + // Verify order: + // 1. en-CA (exact match for first preferred) + // 2. fr-CA (exact match for second preferred) + // 3. en-US (language match for third preferred) + // 4. en-GB (language match for third preferred) + // 5. en-AU (language match for third preferred) + // 6. fr-FR (no match, should come last) + t.is(sorted[0].language, "en-CA", "en-CA should be first (exact match)"); + t.is(sorted[1].language, "fr-CA", "fr-CA should be second (exact match)"); + + // The remaining English variants should be in their natural order + const remainingEnglish = sorted.slice(2, 5).map(v => v.language); + t.true( + ["en-US", "en-GB", "en-AU"].every(lang => remainingEnglish.includes(lang)), + "Should include all English variants after exact matches" + ); + + t.is(sorted[5].language, "fr-FR", "fr-FR should be last (no match)"); + + // Test with preferred languages that don't match any regions + const noMatches = manager.sortVoices(testVoices, { + by: "region", + preferredLanguages: ["es-ES", "de-DE"] // No matches in test data + }); + + // Should sort alphabetically by region + const regions = noMatches.map(v => v.language.split("-")[1]); + const sortedRegions = [...regions].sort(); + t.deepEqual(regions, sortedRegions, "Should sort alphabetically by region when no preferred matches"); +}); + +// ============================================= +// 9. Voice Grouping Tests +// ============================================= + +testWithContext("groupVoices: groups by language", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different languages + const testVoices = [ + { voiceURI: "voice1", name: "Voice 1", language: "en-US" }, + { voiceURI: "voice2", name: "Voice 2", language: "fr-FR" }, + { voiceURI: "voice3", name: "Voice 3", language: "en-US" } + ]; + + const groups = (manager as any).groupVoices(testVoices, "language"); + + // Check that groups were created for each language + t.truthy(groups["en"]); + t.truthy(groups["fr"]); + + // Check the number of voices in each group + t.is(groups["en"].length, 2); + t.is(groups["fr"].length, 1); +}); + +testWithContext("groupVoices: groups by gender", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different genders + const testVoices = [ + createTestVoice({ name: "Male Voice 1", language: "en-US", gender: "male" }), + createTestVoice({ name: "Female Voice 1", language: "en-US", gender: "female" }), + createTestVoice({ name: "Male Voice 2", language: "fr-FR", gender: "male" }), + createTestVoice({ name: "Unknown Voice", language: "es-ES" }) + ]; + + const groups = manager.groupVoices(testVoices, "gender"); + t.true(groups.hasOwnProperty("male")); + t.true(groups.hasOwnProperty("female")); + t.true(groups.hasOwnProperty("unknown")); + t.is(groups.male.length, 2); + t.is(groups.female.length, 1); + t.is(groups.unknown.length, 1); +}); + +testWithContext("groupVoices: groups by quality", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different qualities + const testVoices = [ + createTestVoice({ name: "High Quality 1", language: "en-US", quality: ["high"] }), + createTestVoice({ name: "Low Quality Voice", language: "en-US", quality: ["low"] }), + createTestVoice({ name: "High Quality 2", language: "fr-FR", quality: ["high"] }) + ]; + + const groups = manager.groupVoices(testVoices, "quality"); + t.is(Object.keys(groups).length, 2); + t.is(groups.high.length, 2); + t.is(groups.low.length, 1); +}); + +testWithContext("groupVoices: groups by region", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Create test voices with different regions + const testVoices = [ + createTestVoice({ name: "US Voice", language: "en-US" }), + createTestVoice({ name: "UK Voice", language: "en-GB" }), + createTestVoice({ name: "Canada Voice", language: "en-CA" }), + createTestVoice({ name: "Australia Voice", language: "en-AU" }) + ]; + + const groups = manager.groupVoices(testVoices, "region"); + t.is(Object.keys(groups).length, 4); + t.is(groups.US.length, 1); + t.is(groups.GB.length, 1); + t.is(groups.CA.length, 1); + t.is(groups.AU.length, 1); +}); + +testWithContext("groupVoices: handles empty voices array", (t: ExecutionContext) => { + const manager = t.context.manager; + + const groups = manager.groupVoices([], "language"); + t.deepEqual(groups, {}); +}); + +testWithContext("groupVoices: handles voices with missing properties", (t: ExecutionContext) => { + const manager = t.context.manager; + + const testVoices = [ + createTestVoice({ name: "Voice 1", language: "en-US" }), + createTestVoice({ name: "Voice 2", language: undefined as any }), + createTestVoice({ name: "Voice 3", language: "fr-FR", gender: undefined as any }), + createTestVoice({ name: "Voice 4", language: "es-ES", quality: undefined as any }) + ]; + + // Should handle missing properties gracefully + const groupsByLanguage = manager.groupVoices(testVoices, "language"); + t.true(groupsByLanguage.hasOwnProperty("en")); + t.true(groupsByLanguage.hasOwnProperty("fr")); + t.true(groupsByLanguage.hasOwnProperty("es")); + + const groupsByGender = manager.groupVoices(testVoices, "gender"); + // Should have an "undefined" group for voices without gender + + const groupsByQuality = manager.groupVoices(testVoices, "quality"); + // Should have an "undefined" group for voices without quality +}); + +// ============================================= +// 10. Conversion Tests +// ============================================= + +testWithContext("convertToSpeechSynthesisVoice: converts ReadiumSpeechVoice to SpeechSynthesisVoice", async (t: ExecutionContext) => { + const manager = t.context.manager; + const voices = await manager.getVoices(); + t.plan(3); + + if (voices.length > 0) { + const speechVoice = manager.convertToSpeechSynthesisVoice(voices[0]); + t.truthy(speechVoice); + t.is(speechVoice?.name, voices[0].name); + t.is(speechVoice?.voiceURI, voices[0].voiceURI); + } else { + t.pass("No voices available to test"); + } +}); + +testWithContext("convertToSpeechSynthesisVoice: handles invalid voice", (t: ExecutionContext) => { + const manager = t.context.manager; + + // Test with undefined voice + const result1 = manager.convertToSpeechSynthesisVoice(undefined as any); + t.is(result1, undefined); + + // Test with voice that doesn't match any browser voice + const invalidVoice = createTestVoice({ name: "Non-existent Voice", language: "xx-XX" }); + const result2 = manager.convertToSpeechSynthesisVoice(invalidVoice); + t.is(result2, undefined); +}); \ No newline at end of file diff --git a/test/voices.test.ts b/test/voices.test.ts deleted file mode 100644 index c61b0c3..0000000 --- a/test/voices.test.ts +++ /dev/null @@ -1,553 +0,0 @@ -import test from "ava"; -import { filterOnRecommended, groupByLanguages, ReadiumSpeechVoice, sortByLanguage, groupByRegions } from "../src/voices.js"; -import { IRecommended } from "../src/data.gen.js"; - -test('dumb test', t => { - t.deepEqual([], []); -}); - -test.before(t => { - // This runs before all tests - globalThis.window = { navigator: { languages: [] } as any } as any; -}); - -test('sortByLanguage: Empty preferred language list', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, [], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'en-US'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'fr-FR'); -}); - -test('sortByLanguage: Preferred language list with one language', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, ['fr-FR'], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'fr-FR'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'en-US'); -}); - -test('sortByLanguage: Preferred language list with multiple languages', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 4', voiceURI: 'uri4', name: 'Name 4', language: 'es-ES', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, ['fr-FR', 'es-ES'], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'fr-FR'); - t.true(result[1].language === 'es-ES'); - t.true(result[2].language === 'en-US'); - t.true(result[3].language === 'en-US'); -}); - -test('sortByLanguage: No matching languages', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, ['de-DE'], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'en-US'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'fr-FR'); -}); - -test('sortByLanguage: Preferred language list is not an array', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, 'en-US', ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'en-US'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'fr-FR'); -}); - -test('sortByLanguage: Preferred language undefined and navigator langua', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, 'en-US', ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'en-US'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'fr-FR'); -}); - -test('sortByLanguage: Preferred language list with one language and navigator.languages', t => { - (globalThis.window.navigator as any).languages = ['fr-FR', 'en-US']; - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, ['fr-FR'], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'fr-FR'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'en-US'); -}); - -test('sortByLanguage: Preferred language list with multiple languages and navigator.languages', t => { - (globalThis.window.navigator as any).languages = ['fr-FR', 'en-US']; - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 4', voiceURI: 'uri4', name: 'Name 4', language: 'es-ES', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, ['fr-FR', 'es-ES'], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'fr-FR'); - t.true(result[1].language === 'es-ES'); - t.true(result[2].language === 'en-US'); - t.true(result[3].language === 'en-US'); -}); - -test('sortByLanguage: No matching languages and navigator.languages', t => { - (globalThis.window.navigator as any).languages = ['de-DE', 'en-US']; - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, ['de-DE'], ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'en-US'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'fr-FR'); -}); - -test('sortByLanguage: Preferred language list is not an array and navigator.languages', t => { - (globalThis.window.navigator as any).languages = ['fr-FR', 'en-US']; - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - - const result = sortByLanguage(voices, 'en-US', ""); - t.true(result.length === voices.length); - t.true(result[0].language === 'en-US'); - t.true(result[1].language === 'en-US'); - t.true(result[2].language === 'fr-FR'); -}); - -test('filterOnRecommended: Empty input', t => { - const voices: ReadiumSpeechVoice[] = []; - const result = filterOnRecommended(voices); - t.deepEqual(result, [[], []]); -}); - -test('filterOnRecommended: No recommended voices', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - ]; - const result = filterOnRecommended(voices, []); - t.deepEqual(result, [[], []]); -}); - -test('filterOnRecommended: Single recommended voice with single quality', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high'], language: 'en-US', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [], - ]); -}); - -test('filterOnRecommended: Single recommended voice with multiple qualities', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high', 'normal'], language: 'en-US', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'normal', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [], - ]); -}); - -test('filterOnRecommended: Single recommended voice with multiple qualities and remaining lowQuality', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 1 (Premium)', language: 'en-US', offlineAvailability: true, pitchControl: true }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high', 'normal'], language: 'en-US', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri3', name: 'Name 1 (Premium)', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'low', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - ]); -}); - -test('filterOnRecommended: Multiple recommended voices', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high'], language: 'en-US', localizedName: "" }, - { name: 'Name 2', label: 'Voice 2', quality: ['normal'], language: 'es-ES', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false, quality: 'normal', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [], - ]); -}); -test('filterOnRecommended: Recommended voices with altNames', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 1-1', voiceURI: 'uri1-1', name: 'Name 1 with an altNames', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high'], altNames: ['Name 1 with an altNames'], language: 'en-US', localizedName: "" }, - { name: 'Name 2', label: 'Voice 2', quality: ['normal'], language: 'es-ES', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false, quality: 'normal', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [ - { label: 'Voice 1', voiceURI: 'uri1-1', name: 'Name 1 with an altNames', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - ]); -}); -test('filterOnRecommended: Recommended voices with altNames only and voices not in name', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1 with an altNames', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high'], altNames: ['Name 1 with an altNames'], language: 'en-US', localizedName: "" }, - { name: 'Name 2', label: 'Voice 2', quality: ['normal'], language: 'es-ES', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1 with an altNames', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false, quality: 'normal', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [ - ], - ]); -}); -test('filterOnRecommended: Recommended voices with multiple altNames and voices not in name', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1 with an altNames', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 1-1', voiceURI: 'uri1-1', name: 'Name 1 with a second altNames', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - ]; - const recommended: IRecommended[] = [ - { name: 'Name 1', label: 'Voice 1', quality: ['high'], altNames: ['Name 1 with an altNames', 'Name 1 with a second altNames'], language: 'en-US', localizedName: "" }, - { name: 'Name 2', label: 'Voice 2', quality: ['normal'], language: 'es-ES', localizedName: "" }, - ]; - const result = filterOnRecommended(voices, recommended); - t.deepEqual(result, [ - [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1 with an altNames', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'es-ES', offlineAvailability: false, pitchControl: false, quality: 'normal', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - [ - { label: 'Voice 1', voiceURI: 'uri1-1', name: 'Name 1 with a second altNames', language: 'en-US', offlineAvailability: true, pitchControl: true, quality: 'high', recommendedRate: undefined, recommendedPitch: undefined, gender: undefined }, - ], - ]); -}); -test('groupByLanguage: ', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 4', voiceURI: 'uri4', name: 'Name 4', language: 'es-ES', offlineAvailability: true, pitchControl: true }, - ]; - const result = groupByLanguages(voices, ['fr-FR', 'es-ES'], ""); - t.deepEqual(result, new Map([ - ['fr', [ - { - label: 'Voice 2', - language: 'fr-FR', - name: 'Name 2', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri2', - }, - ]], - ['es', [ - { - label: 'Voice 4', - language: 'es-ES', - name: 'Name 4', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri4', - }, - ]], - ['en', [ - { - label: 'Voice 1', - language: 'en-US', - name: 'Name 1', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri1', - }, - { - label: 'Voice 3', - language: 'en-US', - name: 'Name 3', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri3', - }, - ]], - ])); -}); -test('groupByLanguage: localized en', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 4', voiceURI: 'uri4', name: 'Name 4', language: 'es-ES', offlineAvailability: true, pitchControl: true }, - ]; - const result = groupByLanguages(voices, ['fr-FR', 'es-ES'], "en"); - t.deepEqual(result, new Map([ - ['French', [ - { - label: 'Voice 2', - language: 'fr-FR', - name: 'Name 2', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri2', - }, - ]], - ['Spanish', [ - { - label: 'Voice 4', - language: 'es-ES', - name: 'Name 4', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri4', - }, - ]], - ['English', [ - { - label: 'Voice 1', - language: 'en-US', - name: 'Name 1', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri1', - }, - { - label: 'Voice 3', - language: 'en-US', - name: 'Name 3', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri3', - }, - ]], - ])); -}); -test('groupByRegion: ', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-GB', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 4', voiceURI: 'uri4', name: 'Name 4', language: 'es-ES', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 5', voiceURI: 'uri5', name: 'Name 5', language: 'en-CA', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 6', voiceURI: 'uri6', name: 'Name 6', language: 'fr-CA', offlineAvailability: true, pitchControl: true }, - ]; - const result = groupByRegions(voices, ['fr-FR', 'es-ES'], ""); - t.deepEqual(result, new Map([ - ['FR', [ - { - label: 'Voice 2', - language: 'fr-FR', - name: 'Name 2', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri2', - }, - ]], - ['ES', [ - { - label: 'Voice 4', - language: 'es-ES', - name: 'Name 4', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri4', - }, - ]], - ['US', [ - { - label: 'Voice 1', - language: 'en-US', - name: 'Name 1', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri1', - }, - ]], - ['CA', [ - { - label: 'Voice 5', - language: 'en-CA', - name: 'Name 5', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri5', - }, - { - label: 'Voice 6', - language: 'fr-CA', - name: 'Name 6', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri6', - }, - ]], - ['GB', [ - { - label: 'Voice 3', - language: 'en-GB', - name: 'Name 3', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri3', - }, - ]], - ])); -}); -test('groupByRegion: localized fr', t => { - const voices = [ - { label: 'Voice 1', voiceURI: 'uri1', name: 'Name 1', language: 'en-US', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 2', voiceURI: 'uri2', name: 'Name 2', language: 'fr-FR', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 3', voiceURI: 'uri3', name: 'Name 3', language: 'en-GB', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 4', voiceURI: 'uri4', name: 'Name 4', language: 'es-ES', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 5', voiceURI: 'uri5', name: 'Name 5', language: 'en-CA', offlineAvailability: true, pitchControl: true }, - { label: 'Voice 6', voiceURI: 'uri6', name: 'Name 6', language: 'fr-CA', offlineAvailability: true, pitchControl: true }, - ]; - const result = groupByRegions(voices, ['fr-FR', 'es-ES'], "fr"); - t.deepEqual(result, new Map([ - ['France', [ - { - label: 'Voice 2', - language: 'fr-FR', - name: 'Name 2', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri2', - }, - ]], - ['Espagne', [ - { - label: 'Voice 4', - language: 'es-ES', - name: 'Name 4', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri4', - }, - ]], - ['États-Unis', [ - { - label: 'Voice 1', - language: 'en-US', - name: 'Name 1', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri1', - }, - ]], - ['Canada', [ - { - label: 'Voice 5', - language: 'en-CA', - name: 'Name 5', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri5', - }, - { - label: 'Voice 6', - language: 'fr-CA', - name: 'Name 6', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri6', - }, - ]], - ['Royaume-Uni', [ - { - label: 'Voice 3', - language: 'en-GB', - name: 'Name 3', - offlineAvailability: true, - pitchControl: true, - voiceURI: 'uri3', - }, - ]], - ])); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2604862..165e1bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,16 @@ { "extends": "./tsconfig-base.json", - "include": ["src/**/*"], + "compilerOptions": { + "resolveJsonModule": true, + "esModuleInterop": true, + "outDir": "build", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@json/*": ["json/*"] + } + }, + "include": ["src/**/*", "json"], "exclude": ["node_modules", "dist", "test", "demo", "build"] } diff --git a/vite.config.js b/vite.config.js index ff4cac7..9b63d1b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,6 @@ -import { defineConfig } from "vite" -import dts from "vite-plugin-dts" +import { defineConfig } from "vite"; +import dts from "vite-plugin-dts"; +import { resolve } from "path"; export default defineConfig({ build: { @@ -7,22 +8,29 @@ export default defineConfig({ lib: { entry: "src/index.ts", name: "ReadiumSpeech", - fileName: "index", - formats: ["es"] + fileName: (format) => format === "es" ? "index.js" : "index.cjs", + formats: ["es", "cjs"] }, rollupOptions: { external: [], output: { - format: "es" + inlineDynamicImports: true, + exports: "named", + preserveModules: false } } }, define: { - global: 'globalThis', - 'process.env': {}, - 'process.version': '""', - 'process.platform': '"browser"', - 'process.browser': true, + global: "globalThis", + "process.env": {}, + "process.version": '""', + "process.platform": '"browser"', + "process.browser": true, + }, + resolve: { + alias: { + "@json": resolve(__dirname, "./json") + } }, plugins: [ dts({ @@ -31,4 +39,4 @@ export default defineConfig({ include: ["src/**/*"] }) ] -}) +}) \ No newline at end of file diff --git a/voices.schema.json b/voices.schema.json new file mode 100644 index 0000000..5e5b5e7 --- /dev/null +++ b/voices.schema.json @@ -0,0 +1,168 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://readium.org/speech/voices.schema.json", + "title": "Recommended voices for TTS", + "type": "object", + "additionalProperties": false, + "properties": { + "language": { + "type": "string" + }, + "defaultRegion": { + "type": "string" + }, + "testUtterance": { + "type": "string" + }, + "voices": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "description": "Provides a human-friendly label for each voice." + }, + "name": { + "type": "string", + "description": "Identifies voices, as returned by the Web Speech API." + }, + "localizedName": { + "type": "string", + "description": "Identifies the string pattern used to localized a given voice.", + "enum": [ + "android", + "apple" + ] + }, + "note": { + "type": "string" + }, + "altNames": { + "type": "array", + "items": { + "type": "string", + "description": "Alternate names for a given voice. Only useful for Apple voices." + } + }, + "nativeID": { + "type": "array", + "items": { + "type": "string", + "description": "Identifiers used by the native API of the platform for a specific voice." + } + }, + "language": { + "type": "string", + "description": "BCP-47 language tag that identifies the language of a voice." + }, + "altLanguage": { + "type": "string", + "description": "Alternative BCP-47 language tag, mostly used for deprecated values." + }, + "otherLanguages": { + "type": "array", + "items": { + "type": "string" + } + }, + "multiLingual": { + "type": "boolean", + "description": "Identifies voices that are capable of handling multiple languages, even if it means that the voice itself will change. Only available on Microsoft Natural voices for now.", + "default": false + }, + "gender": { + "type": "string", + "description": "Identifies the gender of a voice.", + "enum": [ + "neutral", + "female", + "male" + ] + }, + "children": { + "type": "boolean", + "description": "Indicates if the voice is a children voice.", + "default": false + }, + "quality": { + "type": "array", + "description": "Quality available for the variants of a given voice", + "items": { + "type": "string", + "enum": [ + "veryLow", + "low", + "normal", + "high", + "veryHigh" + ] + } + }, + "rate": { + "type": "number", + "description": "Default recommended speed rate for a voice.", + "minimum": 0.1, + "maximum": 10, + "default": 1 + }, + "pitch": { + "type": "number", + "description": "Default recommended pitch rate for a voice.", + "minimum": 0, + "maximum": 2, + "default": 1 + }, + "pitchControl": { + "type": "boolean", + "description": "Indicates if the pitch of a voice can be controlled.", + "default": true + }, + "os": { + "type": "array", + "description": "List of operating systems in which a voice is available.", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "Android", + "ChromeOS", + "iOS", + "iPadOS", + "macOS", + "Windows" + ] + } + }, + "browser": { + "type": "array", + "description": "List of Web browsers in which a voice is available.", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "ChromeDesktop", + "Edge", + "Firefox", + "Safari" + ] + } + }, + "preloaded": { + "type": "boolean", + "description": "Indicates that a voice is preloaded in all OS and browsers that have been identified.", + "default": false + } + }, + "required": [ + "name" + ] + } + } + }, + "required": [ + "voices" + ] +} \ No newline at end of file