enh: kokorojs call support

This commit is contained in:
Timothy Jaeryang Baek 2025-02-09 23:54:24 -08:00
parent 205ce635f6
commit d95e5e0ba5
4 changed files with 90 additions and 29 deletions

View File

@ -16,7 +16,8 @@
showCallOverlay,
tools,
user as _user,
showControls
showControls,
TTSWorker
} from '$lib/stores';
import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils';
@ -43,6 +44,7 @@
import PhotoSolid from '../icons/PhotoSolid.svelte';
import Photo from '../icons/Photo.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n');
@ -1281,6 +1283,16 @@
stream = null;
if (!$TTSWorker) {
await TTSWorker.set(
new KokoroWorker({
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
})
);
await $TTSWorker.init();
}
showCallOverlay.set(true);
showControls.set(true);
} catch (err) {

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { config, models, settings, showCallOverlay } from '$lib/stores';
import { config, models, settings, showCallOverlay, TTSWorker } from '$lib/stores';
import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@ -12,6 +12,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n');
@ -459,7 +460,21 @@
}
}
if ($config.audio.tts.engine !== '') {
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
const blob = await $TTSWorker
.generate({
text: content,
voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
})
.catch((error) => {
console.error(error);
toast.error(`${error}`);
});
if (blob) {
audioCache.set(content, new Audio(blob));
}
} else if ($config.audio.tts.engine !== '') {
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice

View File

@ -269,8 +269,6 @@
await $TTSWorker.init();
}
console.log($TTSWorker);
for (const [idx, sentence] of messageContentParts.entries()) {
const blob = await $TTSWorker
.generate({

View File

@ -4,6 +4,13 @@ export class KokoroWorker {
private worker: Worker | null = null;
private initialized: boolean = false;
private dtype: string;
private requestQueue: Array<{
text: string;
voice: string;
resolve: (value: string) => void;
reject: (reason: any) => void;
}> = [];
private processing = false; // To track if a request is being processed
constructor(dtype: string = 'fp32') {
this.dtype = dtype;
@ -17,24 +24,49 @@ export class KokoroWorker {
this.worker = new WorkerInstance();
return new Promise<void>((resolve, reject) => {
this.worker!.onmessage = (event) => {
const { status, error } = event.data;
// Handle worker messages
this.worker.onmessage = (event) => {
const { status, error, audioUrl } = event.data;
if (status === 'init:complete') {
this.initialized = true;
resolve();
} else if (status === 'init:error') {
console.error(error);
this.initialized = false;
reject(new Error(error));
if (status === 'init:complete') {
this.initialized = true;
} else if (status === 'init:error') {
console.error(error);
this.initialized = false;
} else if (status === 'generate:complete') {
// Resolve promise from queue
const request = this.requestQueue.shift();
if (request) {
request.resolve(audioUrl);
this.processNextRequest(); // Process next request in queue
}
};
} else if (status === 'generate:error') {
const request = this.requestQueue.shift();
if (request) {
request.reject(new Error(error));
this.processNextRequest(); // Continue processing next in queue
}
}
};
return new Promise<void>((resolve, reject) => {
this.worker!.postMessage({
type: 'init',
payload: { dtype: this.dtype }
});
const handleMessage = (event: MessageEvent) => {
if (event.data.status === 'init:complete') {
this.worker!.removeEventListener('message', handleMessage);
this.initialized = true;
resolve();
} else if (event.data.status === 'init:error') {
this.worker!.removeEventListener('message', handleMessage);
reject(new Error(event.data.error));
}
};
this.worker!.addEventListener('message', handleMessage);
});
}
@ -44,27 +76,31 @@ export class KokoroWorker {
}
return new Promise<string>((resolve, reject) => {
this.worker.postMessage({ type: 'generate', payload: { text, voice } });
const handleMessage = (event: MessageEvent) => {
if (event.data.status === 'generate:complete') {
this.worker!.removeEventListener('message', handleMessage);
resolve(event.data.audioUrl);
} else if (event.data.status === 'generate:error') {
this.worker!.removeEventListener('message', handleMessage);
reject(new Error(event.data.error));
}
};
this.worker.addEventListener('message', handleMessage);
this.requestQueue.push({ text, voice, resolve, reject });
if (!this.processing) {
this.processNextRequest();
}
});
}
private processNextRequest() {
if (this.requestQueue.length === 0) {
this.processing = false;
return;
}
this.processing = true;
const { text, voice } = this.requestQueue[0]; // Get first request but don't remove yet
this.worker!.postMessage({ type: 'generate', payload: { text, voice } });
}
public terminate() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
this.initialized = false;
this.requestQueue = [];
this.processing = false;
}
}
}