Skip to content

Commit a1117ab

Browse files
authored
Code splitting (#42)
1 parent 7d72c9a commit a1117ab

17 files changed

Lines changed: 843 additions & 312 deletions

README.md

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,14 @@ async function setupVoices() {
103103
excludeVeryLowQuality: true
104104
});
105105

106-
// Get voices grouped by language
107-
const voices = voiceManager.getVoices();
108-
const groupedByLanguage = voiceManager.groupVoices(voices, "languages");
106+
// Sort by quality
107+
const sortedByQuality = await voiceManager.sortVoicesByQuality(filteredVoices);
108+
109+
// Sort by preferred languages
110+
const sortedByLanguage = await voiceManager.sortVoicesByLanguages(["en", "fr"], filteredVoices);
111+
112+
// Sort by preferred languages and regions
113+
const sortedByRegion = await voiceManager.sortVoicesByRegions(["en-US", "en-GB"], filteredVoices);
109114

110115
// Get a test utterance for a specific language
111116
const testText = voiceManager.getTestUtterance("en");
@@ -134,11 +139,16 @@ The main class for managing Web Speech API voices with enhanced functionality.
134139
#### Initialize the Voice Manager
135140

136141
```typescript
137-
static initialize(maxTimeout?: number, interval?: number): Promise<WebSpeechVoiceManager>
142+
static initialize(options?: {
143+
languages?: string[];
144+
maxTimeout?: number;
145+
interval?: number;
146+
}): Promise<WebSpeechVoiceManager>
138147
```
139148

140149
Creates and initializes a new WebSpeechVoiceManager instance. This static factory method must be called to create an instance.
141150

151+
- `languages`: Optional array of preferred language codes to filter voices during initialization
142152
- `maxTimeout`: Maximum time in milliseconds to wait for voices to load (default: 10000ms)
143153
- `interval`: Interval in milliseconds between voice loading checks (default: 100ms)
144154
- Returns: Promise that resolves with a new WebSpeechVoiceManager instance
@@ -179,20 +189,20 @@ Returns arrays of languages and regions with their display names and voice count
179189
#### Get Default Voice
180190

181191
```typescript
182-
voiceManager.getDefaultVoice(languages: string | string[], voices?: ReadiumSpeechVoice[]): ReadiumSpeechVoice | null
192+
async voiceManager.getDefaultVoice(languages: string | string[], voices?: ReadiumSpeechVoice[]): Promise<ReadiumSpeechVoice | null>
183193
```
184194

185195
Automatically selects the best available voice based on quality and language preferences. This is the recommended method for getting a suitable voice without manual selection.
186196

187197
```typescript
188198
// Get the best voice for user's browser language
189-
const defaultVoice = voiceManager.getDefaultVoice(navigator.languages || ["en"]);
199+
const defaultVoice = await voiceManager.getDefaultVoice(navigator.languages || ["en"]);
190200

191201
// Get the best voice for specific preferred languages
192-
const frenchVoice = voiceManager.getDefaultVoice(["fr-FR", "fr-CA"]);
202+
const frenchVoice = await voiceManager.getDefaultVoice(["fr-FR", "fr-CA"]);
193203

194204
// Get the best voice from a pre-filtered voice list
195-
const customVoice = voiceManager.getDefaultVoice(["en-US", "en-GB"], customVoiceList);
205+
const customVoice = await voiceManager.getDefaultVoice(["en-US", "en-GB"], customVoiceList);
196206
```
197207

198208
The selection algorithm:
@@ -236,7 +246,7 @@ If you need more control over the sorting process, you can implement and apply y
236246
Sort voices from highest to lowest quality:
237247

238248
```typescript
239-
voiceManager.sortVoicesByQuality(voices?: ReadiumSpeechVoice[]);
249+
async voiceManager.sortVoicesByQuality(voices?: ReadiumSpeechVoice[]): Promise<ReadiumSpeechVoice[]>;
240250
// Returns: [veryHigh, high, normal, low, veryLow, null]
241251
```
242252

@@ -247,7 +257,7 @@ If no voices are provided, it sorts the instance's internal voice list.
247257
Prioritize specific languages while maintaining JSON data’s quality order within each language group:
248258

249259
```typescript
250-
voiceManager.sortVoicesByLanguages(preferredLanguages?: string[], voices?: ReadiumSpeechVoice[]);
260+
async voiceManager.sortVoicesByLanguages(preferredLanguages?: string[], voices?: ReadiumSpeechVoice[]): Promise<ReadiumSpeechVoice[]>;
251261
// Returns: [preferred languages voices, other languages voices...]
252262
```
253263

@@ -258,7 +268,7 @@ If no voices are provided, it sorts the instance's internal voice list.
258268
Sort voices by preferred languages and regions, while maintaining JSON data’s quality order within each region group:
259269

260270
```typescript
261-
voiceManager.sortVoicesByRegions(preferredLanguages?: string[], voices?: ReadiumSpeechVoice[]);
271+
async voiceManager.sortVoicesByRegions(preferredLanguages?: string[], voices?: ReadiumSpeechVoice[]): Promise<ReadiumSpeechVoice[]>;
262272
// Returns: [languages in preferred then alphabetical order → regions: preferred regions → default region → alphabetical regions → voice quality within each region]
263273
```
264274

demo/article/script.js

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ let readAlongEnabled = true; // Default to true to match default checkbox state
2525
async function initialize() {
2626
try {
2727
// Initialize the voice manager
28-
voiceManager = await WebSpeechVoiceManager.initialize();
28+
voiceManager = await WebSpeechVoiceManager.initialize({languages: ["en"]});
2929

30-
// Only get English voices
31-
enVoices = voiceManager.getVoices({languages: "en", removeDuplicates: true});
30+
// Get English voices asynchronously
31+
enVoices = await voiceManager.getVoices({removeDuplicates: true});
3232

3333
// Initialize the navigator
3434
navigator = new WebSpeechReadAloudNavigator();
@@ -39,11 +39,11 @@ async function initialize() {
3939
// Initialize the UI
4040
updateUI();
4141

42-
// Populate voice select
43-
populateVoiceSelect();
42+
// Populate voice select asynchronously
43+
await populateVoiceSelect();
4444

45-
// Get the default voice for English
46-
currentVoice = voiceManager.getDefaultVoice("en");
45+
// Get the default voice for English asynchronously
46+
currentVoice = await voiceManager.getDefaultVoice("en", enVoices);
4747

4848
if (currentVoice && navigator) {
4949
navigator.setVoice(currentVoice);
@@ -173,24 +173,29 @@ async function initializeContent() {
173173
}
174174

175175
// Populate voice select dropdown
176-
function populateVoiceSelect() {
176+
async function populateVoiceSelect() {
177177
if (!voiceSelect) return;
178178

179-
voiceSelect.innerHTML = "<option value=\"\" disabled selected>Select a voice</option>";
179+
voiceSelect.innerHTML = "<option value=\"\" disabled selected>Loading voices...</option>";
180180

181-
if (!enVoices || !enVoices.length) {
182-
const option = document.createElement("option");
183-
option.disabled = true;
184-
option.textContent = "No voices available. Please check your browser settings and internet connection.";
185-
voiceSelect.appendChild(option);
186-
return;
187-
}
181+
try {
182+
if (!enVoices || !enVoices.length) {
183+
enVoices = await voiceManager.getVoices({languages: "en", removeDuplicates: true});
184+
}
185+
186+
voiceSelect.innerHTML = "<option value=\"\" disabled selected>Select a voice</option>";
187+
188+
if (!enVoices || !enVoices.length) {
189+
const option = document.createElement("option");
190+
option.disabled = true;
191+
option.textContent = "No voices available. Please check your browser settings and internet connection.";
192+
voiceSelect.appendChild(option);
193+
return;
194+
}
188195

189196
try {
190197
// Sort by region while preserving quality order within each region
191-
const sortedVoices = voiceManager.sortVoicesByRegions(["en"], enVoices);
192-
193-
console.log(sortedVoices);
198+
const sortedVoices = await voiceManager.sortVoicesByRegions(["en"], enVoices);
194199

195200
let currentRegion = null;
196201
let optgroup = null;
@@ -223,14 +228,22 @@ function populateVoiceSelect() {
223228
optgroup?.appendChild(option);
224229
}
225230

226-
// Set the default voice selection
231+
// If we have a current voice, select it
227232
if (currentVoice) {
228-
const option = voiceSelect.querySelector(`option[value="${currentVoice.name}"]`);
233+
const option = voiceSelect.querySelector(`option[data-voice-uri="${currentVoice.voiceURI}"]`);
229234
if (option) {
230235
option.selected = true;
231236
}
232237
}
233238

239+
// Update the UI to reflect the current state
240+
updateUI();
241+
242+
} catch (error) {
243+
console.error("Error populating voice select:", error);
244+
voiceSelect.innerHTML = "<option value=\"\" disabled selected>Error loading voices</option>";
245+
}
246+
234247
} catch (error) {
235248
console.error("Error populating voice dropdown:", error);
236249
// Fallback to simple list if there's an error

demo/script.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ speechNavigator.on("error", (event) => {
6666
// Initialize the application
6767
async function init() {
6868
try {
69-
// Initialize the voice manager
69+
// Initialize the voice manager with preferred languages
7070
voiceManager = await WebSpeechVoiceManager.initialize();
7171

7272
// Sort those voices by browser preference using sortVoicesByRegions
73-
const voices = voiceManager.sortVoicesByRegions(window.navigator.languages);
73+
const voices = await voiceManager.sortVoicesByRegions(window.navigator.languages);
7474

7575
// Get languages
7676
languages = voiceManager.getLanguages(window.navigator.languages[0], { removeDuplicates: true }, voices);
@@ -201,18 +201,18 @@ function displayVoiceProperties(voice) {
201201
}
202202

203203
// Replace current voice with a new default voice if it gets filtered out
204-
function replaceCurrentVoiceIfFilteredOut(language) {
204+
async function replaceCurrentVoiceIfFilteredOut(language) {
205205
const currentVoiceFilteredOut = currentVoice && !filteredVoices.some(voice => voice.voiceURI === currentVoice.voiceURI);
206206
const needNewVoice = !currentVoice && filteredVoices.length > 0;
207207

208208
if (currentVoiceFilteredOut || needNewVoice) {
209209
// Current voice was filtered out or no voice selected, pick a new default voice based on language
210210
if (filteredVoices.length > 0) {
211-
currentVoice = voiceManager.getDefaultVoice(language, filteredVoices);
211+
currentVoice = await voiceManager.getDefaultVoice(language, filteredVoices);
212212

213213
if (currentVoice) {
214214
try {
215-
speechNavigator.setVoice(currentVoice);
215+
await speechNavigator.setVoice(currentVoice);
216216
displayVoiceProperties(currentVoice);
217217
updateTestUtterance(currentVoice, language);
218218

@@ -235,7 +235,7 @@ function replaceCurrentVoiceIfFilteredOut(language) {
235235
}
236236

237237
// Filter voices based on current filters
238-
function filterVoices() {
238+
async function filterVoices() {
239239
const language = languageSelect.value;
240240
const gender = genderSelect.value;
241241
const source = sourceSelect.value;
@@ -269,16 +269,16 @@ function filterVoices() {
269269
filteredVoices = voicesFilteredExceptLanguage;
270270
}
271271

272-
populateVoiceDropdown(language);
272+
await populateVoiceDropdown(language);
273273

274274
// Replace current voice if it was filtered out
275-
replaceCurrentVoiceIfFilteredOut(language);
275+
await replaceCurrentVoiceIfFilteredOut(language);
276276

277277
updateUI();
278278
}
279279

280280
// Populate the voice dropdown with filtered voices
281-
function populateVoiceDropdown() {
281+
async function populateVoiceDropdown() {
282282
voiceSelect.innerHTML = "<option value='' disabled selected>Select a voice</option>";
283283

284284
try {
@@ -291,7 +291,7 @@ function populateVoiceDropdown() {
291291
}
292292

293293
// Sort voices with browser's preferred languages first
294-
const sortedVoices = voiceManager.sortVoicesByRegions(window.navigator.languages, [...filteredVoices]);
294+
const sortedVoices = await voiceManager.sortVoicesByRegions(window.navigator.languages, [...filteredVoices]);
295295

296296
// Group the sorted voices by region
297297
const voiceGroups = voiceManager.groupVoices("region", sortedVoices);
@@ -550,22 +550,22 @@ function setupEventListeners() {
550550
displayVoiceProperties(null);
551551

552552
// Filter voices for the selected language
553-
filterVoices();
553+
await filterVoices();
554554

555555
// Get the default voice for the selected language using pre-filtered voices
556556
if (baseLanguage) {
557557
// Use the full navigator.languages array for proper language preference handling
558558
const preferredLanguages = [...(window.navigator.languages || [window.navigator.language] || [baseLanguage])];
559559

560-
currentVoice = voiceManager.getDefaultVoice(
560+
currentVoice = await voiceManager.getDefaultVoice(
561561
preferredLanguages,
562562
filteredVoices.length ? filteredVoices : undefined
563563
);
564564

565565
if (currentVoice) {
566566
try {
567567
// Set the voice for the navigator
568-
speechNavigator.setVoice(currentVoice);
568+
await speechNavigator.setVoice(currentVoice);
569569

570570
// Update the voice dropdown to reflect the selected voice
571571
const voiceOption = voiceSelect.querySelector(`option[value="${currentVoice.name}"]`);
@@ -600,7 +600,7 @@ function setupEventListeners() {
600600
if (currentVoice) {
601601
try {
602602
// Set the voice for the navigator
603-
speechNavigator.setVoice(currentVoice);
603+
await speechNavigator.setVoice(currentVoice);
604604

605605
// Display voice properties
606606
displayVoiceProperties(currentVoice);
@@ -655,18 +655,18 @@ function setupEventListeners() {
655655
});
656656

657657
// Update voices when gender filter changes
658-
genderSelect.addEventListener("change", () => {
659-
filterVoices();
658+
genderSelect.addEventListener("change", async () => {
659+
await filterVoices();
660660
});
661661

662662
// Update voices when source filter changes
663-
sourceSelect.addEventListener("change", () => {
664-
filterVoices();
663+
sourceSelect.addEventListener("change", async () => {
664+
await filterVoices();
665665
});
666666

667667
// Update voices when offline filter changes
668-
offlineOnlyCheckbox.addEventListener("change", () => {
669-
filterVoices();
668+
offlineOnlyCheckbox.addEventListener("change", async () => {
669+
await filterVoices();
670670
});
671671

672672
// Update test utterance when language changes

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@readium/speech",
3-
"version": "0.1.0-beta.6",
3+
"version": "0.1.0-beta.7",
44
"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.",
55
"main": "./build/index.cjs",
66
"module": "./build/index.js",
@@ -19,6 +19,7 @@
1919
},
2020
"scripts": {
2121
"test": "npm run build && NODE_NO_WARNINGS=1 ava test/**/*.test.ts",
22+
"generate-metadata": "node scripts/generate-metadata.js",
2223
"clean": "rimraf ./build",
2324
"build": "vite build",
2425
"start": "node build/index.js",

0 commit comments

Comments
 (0)