Kaynağa Gözat

enh: kokorojs call support

Timothy Jaeryang Baek 2 ay önce
ebeveyn
işleme
d95e5e0ba5

+ 13 - 1
src/lib/components/chat/MessageInput.svelte

@@ -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) {

+ 17 - 2
src/lib/components/chat/MessageInput/CallOverlay.svelte

@@ -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

+ 0 - 2
src/lib/components/chat/Messages/ResponseMessage.svelte

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

+ 59 - 23
src/lib/workers/KokoroWorker.ts

@@ -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,20 +76,22 @@ export class KokoroWorker {
 		}
 
 		return new Promise<string>((resolve, reject) => {
-			this.worker.postMessage({ type: 'generate', payload: { text, voice } });
+			this.requestQueue.push({ text, voice, resolve, reject });
+			if (!this.processing) {
+				this.processNextRequest();
+			}
+		});
+	}
 
-			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));
-				}
-			};
+	private processNextRequest() {
+		if (this.requestQueue.length === 0) {
+			this.processing = false;
+			return;
+		}
 
-			this.worker.addEventListener('message', handleMessage);
-		});
+		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() {
@@ -65,6 +99,8 @@ export class KokoroWorker {
 			this.worker.terminate();
 			this.worker = null;
 			this.initialized = false;
+			this.requestQueue = [];
+			this.processing = false;
 		}
 	}
 }