refac
This commit is contained in:
@@ -185,6 +185,7 @@
|
|||||||
let speakingIdx: number | undefined;
|
let speakingIdx: number | undefined;
|
||||||
|
|
||||||
let loadingSpeech = false;
|
let loadingSpeech = false;
|
||||||
|
let speakAbort: AbortController | null = null;
|
||||||
|
|
||||||
let showRateComment = false;
|
let showRateComment = false;
|
||||||
|
|
||||||
@@ -202,39 +203,39 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stopAudio = () => {
|
const stopAudio = () => {
|
||||||
|
speakAbort?.abort();
|
||||||
|
speakAbort = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
speechSynthesis.cancel();
|
speechSynthesis.cancel();
|
||||||
$audioQueue?.stop();
|
$audioQueue?.stop();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
if (speaking) {
|
speaking = false;
|
||||||
speaking = false;
|
speakingIdx = undefined;
|
||||||
speakingIdx = undefined;
|
loadingSpeech = false;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve voice: model-specific > user settings > config default
|
||||||
|
const getVoiceId = () =>
|
||||||
|
model?.info?.meta?.tts?.voice ??
|
||||||
|
($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
|
||||||
|
? $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
|
||||||
|
: $config?.audio?.tts?.voice);
|
||||||
|
|
||||||
const speak = async () => {
|
const speak = async () => {
|
||||||
if (!(message?.content ?? '').trim().length) {
|
if (!(message?.content ?? '').trim().length) {
|
||||||
toast.info($i18n.t('No content to speak'));
|
toast.info($i18n.t('No content to speak'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopAudio();
|
||||||
|
speakAbort = new AbortController();
|
||||||
|
const { signal } = speakAbort;
|
||||||
|
|
||||||
speaking = true;
|
speaking = true;
|
||||||
const content = removeAllDetails(message.content);
|
const content = removeAllDetails(message.content);
|
||||||
|
|
||||||
// Get voice: model-specific > user settings > config default
|
|
||||||
const getVoiceId = () => {
|
|
||||||
// Check for model-specific TTS voice first
|
|
||||||
if (model?.info?.meta?.tts?.voice) {
|
|
||||||
return model.info.meta.tts.voice;
|
|
||||||
}
|
|
||||||
// Fall back to user settings or config default
|
|
||||||
if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
|
|
||||||
return $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice;
|
|
||||||
}
|
|
||||||
return $config?.audio?.tts?.voice;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($config.audio.tts.engine === '') {
|
if ($config.audio.tts.engine === '') {
|
||||||
let voices = [];
|
let voices = [];
|
||||||
const getVoicesLoop = setInterval(() => {
|
const getVoicesLoop = setInterval(() => {
|
||||||
@@ -242,16 +243,10 @@
|
|||||||
if (voices.length > 0) {
|
if (voices.length > 0) {
|
||||||
clearInterval(getVoicesLoop);
|
clearInterval(getVoicesLoop);
|
||||||
|
|
||||||
const voiceId = getVoiceId();
|
const voice = voices.find((v) => v.voiceURI === getVoiceId());
|
||||||
const voice = voices?.filter((v) => v.voiceURI === voiceId)?.at(0) ?? undefined;
|
|
||||||
|
|
||||||
console.log(voice);
|
|
||||||
|
|
||||||
const speech = new SpeechSynthesisUtterance(content);
|
const speech = new SpeechSynthesisUtterance(content);
|
||||||
speech.rate = $settings.audio?.tts?.playbackRate ?? 1;
|
speech.rate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||||
|
|
||||||
console.log(speech);
|
|
||||||
|
|
||||||
speech.onend = () => {
|
speech.onend = () => {
|
||||||
speaking = false;
|
speaking = false;
|
||||||
if ($settings.conversationMode) {
|
if ($settings.conversationMode) {
|
||||||
@@ -281,9 +276,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!messageContentParts.length) {
|
if (!messageContentParts.length) {
|
||||||
console.log('No content to speak');
|
|
||||||
toast.info($i18n.t('No content to speak'));
|
toast.info($i18n.t('No content to speak'));
|
||||||
|
|
||||||
speaking = false;
|
speaking = false;
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
return;
|
return;
|
||||||
@@ -303,41 +296,43 @@
|
|||||||
await $TTSWorker.init();
|
await $TTSWorker.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [idx, sentence] of messageContentParts.entries()) {
|
for (const [, sentence] of messageContentParts.entries()) {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
const url = await $TTSWorker
|
const url = await $TTSWorker
|
||||||
.generate({
|
.generate({ text: sentence, voice: voiceId })
|
||||||
text: sentence,
|
|
||||||
voice: voiceId
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
|
|
||||||
speaking = false;
|
speaking = false;
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
if (url && speaking) {
|
if (url && speaking) {
|
||||||
$audioQueue.enqueue(url);
|
$audioQueue.enqueue(url);
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [idx, sentence] of messageContentParts.entries()) {
|
for (const [, sentence] of messageContentParts.entries()) {
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
const res = await synthesizeOpenAISpeech(localStorage.token, voiceId, sentence).catch(
|
const res = await synthesizeOpenAISpeech(localStorage.token, voiceId, sentence).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
|
|
||||||
speaking = false;
|
speaking = false;
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
if (res && speaking) {
|
if (res && speaking) {
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
$audioQueue.enqueue(url);
|
$audioQueue.enqueue(url);
|
||||||
loadingSpeech = false;
|
loadingSpeech = false;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-31
@@ -1,32 +1,39 @@
|
|||||||
|
type AudioQueueEvent = 'stop' | 'empty-queue' | 'id-change';
|
||||||
|
|
||||||
|
interface AudioQueueStopDetail {
|
||||||
|
event: AudioQueueEvent;
|
||||||
|
id: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnStoppedCallback = (detail: AudioQueueStopDetail) => void;
|
||||||
|
|
||||||
export class AudioQueue {
|
export class AudioQueue {
|
||||||
constructor(audioElement) {
|
private audio: HTMLAudioElement;
|
||||||
|
private queue: string[] = [];
|
||||||
|
private current: string | null = null;
|
||||||
|
private readonly _onEnded = () => this.next();
|
||||||
|
|
||||||
|
id: string | null = null;
|
||||||
|
onStopped: OnStoppedCallback | null = null;
|
||||||
|
|
||||||
|
constructor(audioElement: HTMLAudioElement) {
|
||||||
this.audio = audioElement;
|
this.audio = audioElement;
|
||||||
this.queue = [];
|
|
||||||
this.current = null;
|
|
||||||
this.id = null;
|
|
||||||
|
|
||||||
this._onEnded = () => this.next();
|
|
||||||
this.audio.addEventListener('ended', this._onEnded);
|
this.audio.addEventListener('ended', this._onEnded);
|
||||||
|
|
||||||
this.onStopped = null; // optional callback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setId(newId) {
|
setId(newId: string) {
|
||||||
console.log('Setting audio queue ID to:', newId);
|
if (this.id === newId) return;
|
||||||
if (this.id !== newId) {
|
|
||||||
this.stop();
|
this.#halt();
|
||||||
this.id = newId;
|
this.id = newId;
|
||||||
if (this.onStopped) this.onStopped({ event: 'id-change', id: newId });
|
this.onStopped?.({ event: 'id-change', id: newId });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlaybackRate(rate) {
|
setPlaybackRate(rate: number) {
|
||||||
console.log('Setting audio playback rate to:', rate);
|
|
||||||
this.audio.playbackRate = rate;
|
this.audio.playbackRate = rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue(url) {
|
enqueue(url: string) {
|
||||||
console.log('Enqueuing audio URL:', url);
|
|
||||||
this.queue.push(url);
|
this.queue.push(url);
|
||||||
|
|
||||||
// Auto-play if nothing is currently playing or loaded
|
// Auto-play if nothing is currently playing or loaded
|
||||||
@@ -44,30 +51,37 @@ export class AudioQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next() {
|
||||||
this.current = this.queue.shift();
|
this.current = this.queue.shift() ?? null;
|
||||||
|
|
||||||
if (this.current) {
|
if (this.current) {
|
||||||
this.audio.src = this.current;
|
this.audio.src = this.current;
|
||||||
this.audio.play();
|
this.audio.play();
|
||||||
console.log('Playing audio URL:', this.current);
|
|
||||||
} else {
|
} else {
|
||||||
this.stop();
|
this.#halt();
|
||||||
if (this.onStopped) this.onStopped({ event: 'empty-queue', id: this.id });
|
this.onStopped?.({ event: 'empty-queue', id: this.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
this.#halt();
|
||||||
|
this.onStopped?.({ event: 'stop', id: this.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.audio.removeEventListener('ended', this._onEnded);
|
||||||
|
this.#halt();
|
||||||
|
this.onStopped = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause audio and clear queue without firing onStopped.
|
||||||
|
* Callers that need the callback should invoke it themselves.
|
||||||
|
*/
|
||||||
|
#halt() {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
this.audio.currentTime = 0;
|
this.audio.currentTime = 0;
|
||||||
this.audio.src = '';
|
this.audio.src = '';
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.current = null;
|
this.current = null;
|
||||||
if (this.onStopped) this.onStopped({ event: 'stop', id: this.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.audio.removeEventListener('ended', this._onEnded);
|
|
||||||
this.stop();
|
|
||||||
this.onStopped = null;
|
|
||||||
this.audio = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user