use-voice-control 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/deepgram.d.ts +5 -0
- package/dist/core/kokoro.d.ts +5 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/types/types.d.ts +17 -0
- package/package.json +7 -11
- package/speech/core/KokoroTTS.js +91 -0
- package/speech/core/deepgram.ts +56 -0
- package/speech/core/kokoro.js +93 -0
- package/speech/core/kokoro.ts +81 -0
- package/speech/docs/ARCHITECTURE.md +349 -0
- package/speech/docs/HUGGINGFACE_MIGRATION.md +172 -0
- package/speech/docs/INTEGRATION.md +276 -0
- package/speech/docs/MIGRATION.md +220 -0
- package/speech/docs/QUICKSTART.md +159 -0
- package/speech/docs/README.md +167 -0
- package/speech/index.ts +54 -0
- package/speech/legacy/conversation.js +150 -0
- package/speech/legacy/main.js +56 -0
- package/speech/legacy/stt.js +161 -0
- package/speech/legacy/tts.js +56 -0
- package/speech/legacy/worker.js +71 -0
- package/speech/types/types.ts +34 -0
- package/speech/ui/AudioPlayer.js +84 -0
- package/speech/ui/ui.js +109 -0
- package/speech/ui/voice-selector.js +77 -0
- package/speech/ui/voices.js +259 -0
- package/speech/utils/audio-utils.js +82 -0
- package/speech/utils/phonemize.js +198 -0
- package/speech/utils/semantic-split.js +107 -0
- package/speech/utils/sentence-detector.js +89 -0
- package/readme.md +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { KokoroTTS } from "./kokoro.js";
|
|
2
|
+
import { splitTextSmart } from "./semantic-split.js";
|
|
3
|
+
|
|
4
|
+
async function detectWebGPU() {
|
|
5
|
+
try {
|
|
6
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
7
|
+
return !!adapter;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const device = (await detectWebGPU()) ? "webgpu" : "wasm";
|
|
14
|
+
self.postMessage({ status: "device", device });
|
|
15
|
+
|
|
16
|
+
let model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
|
|
17
|
+
|
|
18
|
+
const chunkQueue = [];
|
|
19
|
+
let isProcessing = false;
|
|
20
|
+
|
|
21
|
+
async function processQueue() {
|
|
22
|
+
if (isProcessing || chunkQueue.length === 0) return;
|
|
23
|
+
|
|
24
|
+
isProcessing = true;
|
|
25
|
+
const { chunk, voice } = chunkQueue.shift();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
console.log("Processing chunk", chunk);
|
|
29
|
+
const audio = await tts.generate(chunk, { voice });
|
|
30
|
+
let ab = audio.audio.buffer;
|
|
31
|
+
console.log("generate done");
|
|
32
|
+
self.postMessage({ status: "stream", audio: ab, text: chunk }, [ab]);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Error processing chunk:", error);
|
|
35
|
+
self.postMessage({ status: "error", error: error.message });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
isProcessing = false;
|
|
39
|
+
|
|
40
|
+
if (chunkQueue.length > 0) {
|
|
41
|
+
processQueue();
|
|
42
|
+
} else {
|
|
43
|
+
self.postMessage({ status: "complete" });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tts = await KokoroTTS.from_pretrained(model_id, {
|
|
48
|
+
dtype: device === "wasm" ? "q8" : "fp32",
|
|
49
|
+
device,
|
|
50
|
+
progress_callback: (progress) => {
|
|
51
|
+
// Report progress to main thread
|
|
52
|
+
self.postMessage({ status: "progress", progress });
|
|
53
|
+
}
|
|
54
|
+
}).catch((e) => {
|
|
55
|
+
self.postMessage({ status: "error", error: e.message });
|
|
56
|
+
throw e;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
self.postMessage({ status: "ready", voices: tts.voices, device });
|
|
60
|
+
|
|
61
|
+
self.addEventListener("message", async (e) => {
|
|
62
|
+
const { text, voice } = e.data;
|
|
63
|
+
let chunks = splitTextSmart(text, 600);
|
|
64
|
+
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
chunkQueue.push({ chunk, voice });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
processQueue();
|
|
70
|
+
});
|
|
71
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type definitions for text-to-speech providers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type TTSProvider = "kokoro" | "deepgram";
|
|
6
|
+
|
|
7
|
+
export interface TTSOptions {
|
|
8
|
+
text: string;
|
|
9
|
+
provider?: TTSProvider;
|
|
10
|
+
voice?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TTSResult {
|
|
14
|
+
audio: ArrayBuffer;
|
|
15
|
+
contentType: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Kokoro voices from the model
|
|
19
|
+
export const KOKORO_VOICES = [
|
|
20
|
+
"af_heart", "af_alloy", "af_aoede", "af_bella",
|
|
21
|
+
"af_jessica", "af_nicole", "af_river", "af_sarah", "af_sky",
|
|
22
|
+
"am_adam", "am_echo", "am_fable", "am_fenrir",
|
|
23
|
+
"am_liam", "am_michael", "am_onyx"
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type KokoroVoice = (typeof KOKORO_VOICES)[number];
|
|
27
|
+
|
|
28
|
+
// Deepgram Aura speakers
|
|
29
|
+
export const DEEPGRAM_SPEAKERS = [
|
|
30
|
+
"angus", "asteria", "arcas", "orion", "orpheus", "athena",
|
|
31
|
+
"luna", "zeus", "perseus", "helios", "hera", "stella",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type DeepgramSpeaker = (typeof DEEPGRAM_SPEAKERS)[number];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const SAMPLE_RATE = 24000;
|
|
2
|
+
|
|
3
|
+
export class AudioPlayer {
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this.audioContext = new AudioContext();
|
|
7
|
+
this.audioQueue = [];
|
|
8
|
+
this.isPlaying = false;
|
|
9
|
+
this.currentSource = null; // Track current audio source for stopping
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
queueAudio(audioData) {
|
|
13
|
+
const audioData2 = new Float32Array(audioData);
|
|
14
|
+
const audioBuffer = this.audioContext.createBuffer(1, audioData2.length, SAMPLE_RATE);
|
|
15
|
+
audioBuffer.getChannelData(0).set(audioData2);
|
|
16
|
+
this.audioQueue.push(audioBuffer);
|
|
17
|
+
this.playAudioQueue();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async playAudioQueue() {
|
|
21
|
+
if (this.isPlaying || this.audioQueue.length === 0) return;
|
|
22
|
+
|
|
23
|
+
this.isPlaying = true;
|
|
24
|
+
try {
|
|
25
|
+
while (this.audioQueue.length > 0) {
|
|
26
|
+
const source = this.audioContext.createBufferSource();
|
|
27
|
+
this.currentSource = source; // Store current source for stopping
|
|
28
|
+
source.buffer = this.audioQueue.shift();
|
|
29
|
+
source.connect(this.audioContext.destination);
|
|
30
|
+
|
|
31
|
+
if (this.audioContext.state === "suspended") {
|
|
32
|
+
await this.audioContext.resume();
|
|
33
|
+
console.log("AudioContext resumed.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log("Playing audio buffer");
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
source.onended = () => {
|
|
39
|
+
this.currentSource = null; // Clear reference when playback ends
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
source.start();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log("Audio playback finished.");
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Error during audio playback:", error);
|
|
51
|
+
} finally {
|
|
52
|
+
this.isPlaying = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Stop audio playback and clear the queue
|
|
57
|
+
stop() {
|
|
58
|
+
console.log("Stopping audio playback");
|
|
59
|
+
|
|
60
|
+
// Stop the currently playing source if any
|
|
61
|
+
if (this.currentSource) {
|
|
62
|
+
try {
|
|
63
|
+
this.currentSource.stop();
|
|
64
|
+
this.currentSource = null;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Error stopping current source:", error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.audioQueue = [];
|
|
71
|
+
this.isPlaying = false;
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
close() {
|
|
76
|
+
if (this.audioContext && this.audioContext.state !== "closed") {
|
|
77
|
+
this.audioContext.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getAudioContext() {
|
|
82
|
+
return this.audioContext;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/speech/ui/ui.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Conversation } from './conversation.js';
|
|
2
|
+
import { initVoiceSelector } from './voice-selector.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function updateTimer(recordingStartTime, recordingTimer) {
|
|
6
|
+
const elapsed = new Date() - recordingStartTime;
|
|
7
|
+
const seconds = Math.floor((elapsed / 1000) % 60).toString().padStart(2, '0');
|
|
8
|
+
const minutes = Math.floor((elapsed / 1000 / 60) % 60).toString().padStart(2, '0');
|
|
9
|
+
recordingTimer.textContent = `${minutes}:${seconds}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function displayConversation(conversationHistory) {
|
|
13
|
+
let transcriptionResult = document.getElementById('transcriptionResult');
|
|
14
|
+
let conversationHTML = '';
|
|
15
|
+
// Skip the system message at index 0
|
|
16
|
+
for (let i = 1; i < conversationHistory.length; i++) {
|
|
17
|
+
const message = conversationHistory[i];
|
|
18
|
+
const roleClass = message.role === 'user' ? 'user-message' : 'assistant-message';
|
|
19
|
+
const roleLabel = message.role === 'user' ? 'You' : 'Assistant';
|
|
20
|
+
conversationHTML += `<div class="${roleClass}">
|
|
21
|
+
<strong>${roleLabel}:</strong> ${message.content}
|
|
22
|
+
</div>`;
|
|
23
|
+
}
|
|
24
|
+
transcriptionResult.innerHTML = conversationHTML;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function displayTranscriptionError(transcriptionStatus, transcriptionResult, error) {
|
|
28
|
+
transcriptionStatus.textContent = 'Error during transcription:';
|
|
29
|
+
transcriptionResult.innerHTML = `<p class="error">${error.message}</p>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function setupTabNavigation() {
|
|
33
|
+
const tabButtons = document.querySelectorAll('.tab-button');
|
|
34
|
+
const tabContents = document.querySelectorAll('.tab-content');
|
|
35
|
+
|
|
36
|
+
tabButtons.forEach(button => {
|
|
37
|
+
button.addEventListener('click', () => {
|
|
38
|
+
const tabId = button.getAttribute('data-tab');
|
|
39
|
+
|
|
40
|
+
// Update active button
|
|
41
|
+
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
42
|
+
button.classList.add('active');
|
|
43
|
+
|
|
44
|
+
// Show selected tab content
|
|
45
|
+
tabContents.forEach(content => {
|
|
46
|
+
content.classList.remove('active');
|
|
47
|
+
if (content.id === tabId) {
|
|
48
|
+
content.classList.add('active');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
document.addEventListener('DOMContentLoaded', async function () {
|
|
56
|
+
const toggleButton = document.getElementById('toggleRecording');
|
|
57
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
58
|
+
const recordingIndicator = document.getElementById('recordingIndicator');
|
|
59
|
+
const recordingTimer = document.getElementById('recordingTimer');
|
|
60
|
+
const transcriptionStatus = document.getElementById('transcriptionStatus');
|
|
61
|
+
const transcriptionResult = document.getElementById('transcriptionResult');
|
|
62
|
+
|
|
63
|
+
let recordingStartTime;
|
|
64
|
+
let timerInterval;
|
|
65
|
+
let isRecording = false;
|
|
66
|
+
|
|
67
|
+
// Disable the button initially and show loading state
|
|
68
|
+
toggleButton.disabled = true;
|
|
69
|
+
toggleButton.textContent = 'Loading Models...';
|
|
70
|
+
recordingStatus.textContent = 'Loading speech recognition and synthesis models...';
|
|
71
|
+
|
|
72
|
+
setupTabNavigation();
|
|
73
|
+
initVoiceSelector();
|
|
74
|
+
|
|
75
|
+
let conversation = new Conversation();
|
|
76
|
+
|
|
77
|
+
document.addEventListener('keydown', function (event) {
|
|
78
|
+
if (event.key === 'F8') {
|
|
79
|
+
conversation.conversationHistory.push({
|
|
80
|
+
role: "user",
|
|
81
|
+
content: "please continue."
|
|
82
|
+
});
|
|
83
|
+
conversation.sendConversationHistory();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
toggleButton.addEventListener('click', function () {
|
|
88
|
+
if (!isRecording) {
|
|
89
|
+
isRecording = true;
|
|
90
|
+
toggleButton.textContent = 'Stop Recording';
|
|
91
|
+
recordingStatus.textContent = 'Recording...';
|
|
92
|
+
recordingIndicator.style.display = 'block';
|
|
93
|
+
transcriptionStatus.textContent = 'Recording in progress...';
|
|
94
|
+
transcriptionResult.textContent = '';
|
|
95
|
+
recordingStartTime = new Date();
|
|
96
|
+
timerInterval = setInterval(() => updateTimer(recordingStartTime, recordingTimer), 1000);
|
|
97
|
+
updateTimer(recordingStartTime, recordingTimer);
|
|
98
|
+
conversation.startRecording();
|
|
99
|
+
} else {
|
|
100
|
+
isRecording = false;
|
|
101
|
+
toggleButton.textContent = 'Start Recording';
|
|
102
|
+
recordingStatus.textContent = 'Recording stopped. Processing...';
|
|
103
|
+
transcriptionStatus.textContent = 'Transcribing audio...';
|
|
104
|
+
conversation.stopRecording();
|
|
105
|
+
clearInterval(timerInterval);
|
|
106
|
+
recordingIndicator.style.display = 'none';
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { VOICES } from './voices.js';
|
|
2
|
+
|
|
3
|
+
export function initVoiceSelector() {
|
|
4
|
+
const voiceSelect = document.getElementById('voiceSelect');
|
|
5
|
+
|
|
6
|
+
voiceSelect.innerHTML = '';
|
|
7
|
+
|
|
8
|
+
const femaleVoices = [];
|
|
9
|
+
const maleVoices = [];
|
|
10
|
+
const otherVoices = [];
|
|
11
|
+
|
|
12
|
+
Object.entries(VOICES).forEach(([id, voice]) => {
|
|
13
|
+
const option = {
|
|
14
|
+
id,
|
|
15
|
+
name: voice.name,
|
|
16
|
+
gender: voice.gender,
|
|
17
|
+
traits: voice.traits || '',
|
|
18
|
+
grade: voice.overallGrade || 'N/A'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (voice.gender === 'Female') {
|
|
22
|
+
femaleVoices.push(option);
|
|
23
|
+
} else if (voice.gender === 'Male') {
|
|
24
|
+
maleVoices.push(option);
|
|
25
|
+
} else {
|
|
26
|
+
otherVoices.push(option);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const sortByGrade = (a, b) => {
|
|
31
|
+
const gradeA = a.grade.charAt(0);
|
|
32
|
+
const gradeB = b.grade.charAt(0);
|
|
33
|
+
return gradeA.localeCompare(gradeB);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
femaleVoices.sort(sortByGrade);
|
|
37
|
+
maleVoices.sort(sortByGrade);
|
|
38
|
+
otherVoices.sort(sortByGrade);
|
|
39
|
+
|
|
40
|
+
const femaleGroup = document.createElement('optgroup');
|
|
41
|
+
femaleGroup.label = 'Female Voices';
|
|
42
|
+
|
|
43
|
+
const maleGroup = document.createElement('optgroup');
|
|
44
|
+
maleGroup.label = 'Male Voices';
|
|
45
|
+
|
|
46
|
+
const otherGroup = document.createElement('optgroup');
|
|
47
|
+
otherGroup.label = 'Other Voices';
|
|
48
|
+
|
|
49
|
+
femaleVoices.forEach(voice => {
|
|
50
|
+
const option = document.createElement('option');
|
|
51
|
+
option.value = voice.id;
|
|
52
|
+
option.textContent = `${voice.name} (${voice.grade})`;
|
|
53
|
+
femaleGroup.appendChild(option);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
maleVoices.forEach(voice => {
|
|
57
|
+
const option = document.createElement('option');
|
|
58
|
+
option.value = voice.id;
|
|
59
|
+
option.textContent = `${voice.name} (${voice.grade})`;
|
|
60
|
+
maleGroup.appendChild(option);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
otherVoices.forEach(voice => {
|
|
64
|
+
const option = document.createElement('option');
|
|
65
|
+
option.value = voice.id;
|
|
66
|
+
option.textContent = `${voice.name} (${voice.grade})`;
|
|
67
|
+
otherGroup.appendChild(option);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (femaleVoices.length > 0) voiceSelect.appendChild(femaleGroup);
|
|
71
|
+
if (maleVoices.length > 0) voiceSelect.appendChild(maleGroup);
|
|
72
|
+
if (otherVoices.length > 0) voiceSelect.appendChild(otherGroup);
|
|
73
|
+
|
|
74
|
+
if (VOICES.af_heart) {
|
|
75
|
+
voiceSelect.value = 'af_heart';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
export const VOICES = Object.freeze({
|
|
2
|
+
af_heart: {
|
|
3
|
+
name: "Heart",
|
|
4
|
+
language: "en-us",
|
|
5
|
+
gender: "Female",
|
|
6
|
+
traits: "❤️",
|
|
7
|
+
targetQuality: "A",
|
|
8
|
+
overallGrade: "A",
|
|
9
|
+
},
|
|
10
|
+
af_alloy: {
|
|
11
|
+
name: "Alloy",
|
|
12
|
+
language: "en-us",
|
|
13
|
+
gender: "Female",
|
|
14
|
+
targetQuality: "B",
|
|
15
|
+
overallGrade: "C",
|
|
16
|
+
},
|
|
17
|
+
af_aoede: {
|
|
18
|
+
name: "Aoede",
|
|
19
|
+
language: "en-us",
|
|
20
|
+
gender: "Female",
|
|
21
|
+
targetQuality: "B",
|
|
22
|
+
overallGrade: "C+",
|
|
23
|
+
},
|
|
24
|
+
af_bella: {
|
|
25
|
+
name: "Bella",
|
|
26
|
+
language: "en-us",
|
|
27
|
+
gender: "Female",
|
|
28
|
+
traits: "🔥",
|
|
29
|
+
targetQuality: "A",
|
|
30
|
+
overallGrade: "A-",
|
|
31
|
+
},
|
|
32
|
+
af_jessica: {
|
|
33
|
+
name: "Jessica",
|
|
34
|
+
language: "en-us",
|
|
35
|
+
gender: "Female",
|
|
36
|
+
targetQuality: "C",
|
|
37
|
+
overallGrade: "D",
|
|
38
|
+
},
|
|
39
|
+
af_kore: {
|
|
40
|
+
name: "Kore",
|
|
41
|
+
language: "en-us",
|
|
42
|
+
gender: "Female",
|
|
43
|
+
targetQuality: "B",
|
|
44
|
+
overallGrade: "C+",
|
|
45
|
+
},
|
|
46
|
+
af_nicole: {
|
|
47
|
+
name: "Nicole",
|
|
48
|
+
language: "en-us",
|
|
49
|
+
gender: "Female",
|
|
50
|
+
traits: "🎧",
|
|
51
|
+
targetQuality: "B",
|
|
52
|
+
overallGrade: "B-",
|
|
53
|
+
},
|
|
54
|
+
af_nova: {
|
|
55
|
+
name: "Nova",
|
|
56
|
+
language: "en-us",
|
|
57
|
+
gender: "Female",
|
|
58
|
+
targetQuality: "B",
|
|
59
|
+
overallGrade: "C",
|
|
60
|
+
},
|
|
61
|
+
af_river: {
|
|
62
|
+
name: "River",
|
|
63
|
+
language: "en-us",
|
|
64
|
+
gender: "Female",
|
|
65
|
+
targetQuality: "C",
|
|
66
|
+
overallGrade: "D",
|
|
67
|
+
},
|
|
68
|
+
af_sarah: {
|
|
69
|
+
name: "Sarah",
|
|
70
|
+
language: "en-us",
|
|
71
|
+
gender: "Female",
|
|
72
|
+
targetQuality: "B",
|
|
73
|
+
overallGrade: "C+",
|
|
74
|
+
},
|
|
75
|
+
af_sky: {
|
|
76
|
+
name: "Sky",
|
|
77
|
+
language: "en-us",
|
|
78
|
+
gender: "Female",
|
|
79
|
+
targetQuality: "B",
|
|
80
|
+
overallGrade: "C-",
|
|
81
|
+
},
|
|
82
|
+
am_adam: {
|
|
83
|
+
name: "Adam",
|
|
84
|
+
language: "en-us",
|
|
85
|
+
gender: "Male",
|
|
86
|
+
targetQuality: "D",
|
|
87
|
+
overallGrade: "F+",
|
|
88
|
+
},
|
|
89
|
+
am_echo: {
|
|
90
|
+
name: "Echo",
|
|
91
|
+
language: "en-us",
|
|
92
|
+
gender: "Male",
|
|
93
|
+
targetQuality: "C",
|
|
94
|
+
overallGrade: "D",
|
|
95
|
+
},
|
|
96
|
+
am_eric: {
|
|
97
|
+
name: "Eric",
|
|
98
|
+
language: "en-us",
|
|
99
|
+
gender: "Male",
|
|
100
|
+
targetQuality: "C",
|
|
101
|
+
overallGrade: "D",
|
|
102
|
+
},
|
|
103
|
+
am_fenrir: {
|
|
104
|
+
name: "Fenrir",
|
|
105
|
+
language: "en-us",
|
|
106
|
+
gender: "Male",
|
|
107
|
+
targetQuality: "B",
|
|
108
|
+
overallGrade: "C+",
|
|
109
|
+
},
|
|
110
|
+
am_liam: {
|
|
111
|
+
name: "Liam",
|
|
112
|
+
language: "en-us",
|
|
113
|
+
gender: "Male",
|
|
114
|
+
targetQuality: "C",
|
|
115
|
+
overallGrade: "D",
|
|
116
|
+
},
|
|
117
|
+
am_michael: {
|
|
118
|
+
name: "Michael",
|
|
119
|
+
language: "en-us",
|
|
120
|
+
gender: "Male",
|
|
121
|
+
targetQuality: "B",
|
|
122
|
+
overallGrade: "C+",
|
|
123
|
+
},
|
|
124
|
+
am_onyx: {
|
|
125
|
+
name: "Onyx",
|
|
126
|
+
language: "en-us",
|
|
127
|
+
gender: "Male",
|
|
128
|
+
targetQuality: "C",
|
|
129
|
+
overallGrade: "D",
|
|
130
|
+
},
|
|
131
|
+
am_puck: {
|
|
132
|
+
name: "Puck",
|
|
133
|
+
language: "en-us",
|
|
134
|
+
gender: "Male",
|
|
135
|
+
targetQuality: "B",
|
|
136
|
+
overallGrade: "C+",
|
|
137
|
+
},
|
|
138
|
+
am_santa: {
|
|
139
|
+
name: "Santa",
|
|
140
|
+
language: "en-us",
|
|
141
|
+
gender: "Male",
|
|
142
|
+
targetQuality: "C",
|
|
143
|
+
overallGrade: "D-",
|
|
144
|
+
},
|
|
145
|
+
bf_emma: {
|
|
146
|
+
name: "Emma",
|
|
147
|
+
language: "en-gb",
|
|
148
|
+
gender: "Female",
|
|
149
|
+
traits: "🚺",
|
|
150
|
+
targetQuality: "B",
|
|
151
|
+
overallGrade: "B-",
|
|
152
|
+
},
|
|
153
|
+
bf_isabella: {
|
|
154
|
+
name: "Isabella",
|
|
155
|
+
language: "en-gb",
|
|
156
|
+
gender: "Female",
|
|
157
|
+
targetQuality: "B",
|
|
158
|
+
overallGrade: "C",
|
|
159
|
+
},
|
|
160
|
+
bm_george: {
|
|
161
|
+
name: "George",
|
|
162
|
+
language: "en-gb",
|
|
163
|
+
gender: "Male",
|
|
164
|
+
targetQuality: "B",
|
|
165
|
+
overallGrade: "C",
|
|
166
|
+
},
|
|
167
|
+
bm_lewis: {
|
|
168
|
+
name: "Lewis",
|
|
169
|
+
language: "en-gb",
|
|
170
|
+
gender: "Male",
|
|
171
|
+
targetQuality: "C",
|
|
172
|
+
overallGrade: "D+",
|
|
173
|
+
},
|
|
174
|
+
bf_alice: {
|
|
175
|
+
name: "Alice",
|
|
176
|
+
language: "en-gb",
|
|
177
|
+
gender: "Female",
|
|
178
|
+
traits: "🚺",
|
|
179
|
+
targetQuality: "C",
|
|
180
|
+
overallGrade: "D",
|
|
181
|
+
},
|
|
182
|
+
bf_lily: {
|
|
183
|
+
name: "Lily",
|
|
184
|
+
language: "en-gb",
|
|
185
|
+
gender: "Female",
|
|
186
|
+
traits: "🚺",
|
|
187
|
+
targetQuality: "C",
|
|
188
|
+
overallGrade: "D",
|
|
189
|
+
},
|
|
190
|
+
bm_daniel: {
|
|
191
|
+
name: "Daniel",
|
|
192
|
+
language: "en-gb",
|
|
193
|
+
gender: "Male",
|
|
194
|
+
traits: "🚹",
|
|
195
|
+
targetQuality: "C",
|
|
196
|
+
overallGrade: "D",
|
|
197
|
+
},
|
|
198
|
+
bm_fable: {
|
|
199
|
+
name: "Fable",
|
|
200
|
+
language: "en-gb",
|
|
201
|
+
gender: "Male",
|
|
202
|
+
traits: "🚹",
|
|
203
|
+
targetQuality: "B",
|
|
204
|
+
overallGrade: "C",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const VOICE_DATA_URL = "https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/resolve/main/voices";
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
*
|
|
212
|
+
* @param {keyof typeof VOICES} id
|
|
213
|
+
* @returns {Promise<ArrayBufferLike>}
|
|
214
|
+
*/
|
|
215
|
+
async function getVoiceFile(id) {
|
|
216
|
+
const url = `${VOICE_DATA_URL}/${id}.bin`;
|
|
217
|
+
|
|
218
|
+
let cache;
|
|
219
|
+
try {
|
|
220
|
+
cache = await caches.open("kokoro-voices");
|
|
221
|
+
const cachedResponse = await cache.match(url);
|
|
222
|
+
if (cachedResponse) {
|
|
223
|
+
return await cachedResponse.arrayBuffer();
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.warn("Unable to open cache", e);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// No cache, or cache failed to open. Fetch the file.
|
|
230
|
+
const response = await fetch(url);
|
|
231
|
+
const buffer = await response.arrayBuffer();
|
|
232
|
+
|
|
233
|
+
if (cache) {
|
|
234
|
+
try {
|
|
235
|
+
// NOTE: We use `new Response(buffer, ...)` instead of `response.clone()` to handle LFS files
|
|
236
|
+
await cache.put(
|
|
237
|
+
url,
|
|
238
|
+
new Response(buffer, {
|
|
239
|
+
headers: response.headers,
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.warn("Unable to cache file", e);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return buffer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const VOICE_CACHE = new Map();
|
|
251
|
+
export async function getVoiceData(voice) {
|
|
252
|
+
if (VOICE_CACHE.has(voice)) {
|
|
253
|
+
return VOICE_CACHE.get(voice);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const buffer = new Float32Array(await getVoiceFile(voice));
|
|
257
|
+
VOICE_CACHE.set(voice, buffer);
|
|
258
|
+
return buffer;
|
|
259
|
+
}
|