diff --git a/frontend/react/src/audio/filesource.ts b/frontend/react/src/audio/filesource.ts index f324b119..12492cfc 100644 --- a/frontend/react/src/audio/filesource.ts +++ b/frontend/react/src/audio/filesource.ts @@ -8,7 +8,7 @@ export class FileSource extends AudioSource { #source: AudioBufferSourceNode; #duration: number = 0; #currentPosition: number = 0; - #updateInterval: number | null; + #updateInterval: number | null = null; #lastUpdateTime: number = 0; #playing = false; #audioBuffer: AudioBuffer; diff --git a/frontend/react/src/audio/speechcontroller.ts b/frontend/react/src/audio/speechcontroller.ts index 1370bfdf..8f181226 100644 --- a/frontend/react/src/audio/speechcontroller.ts +++ b/frontend/react/src/audio/speechcontroller.ts @@ -1,9 +1,9 @@ import { getApp } from "../olympusapp"; import { blobToBase64 } from "../other/utils"; -import { AudioSource } from "./audiosource"; import { RadioSink } from "./radiosink"; export class SpeechController { + #playingText: boolean = false; constructor() {} analyzeData(blob: Blob, radio: RadioSink) { @@ -12,7 +12,7 @@ export class SpeechController { const requestOptions = { method: "PUT", // Specify the request method headers: { "Content-Type": "application/json" }, // Specify the content type - body: JSON.stringify({data: base64}), // Send the data in blob format + body: JSON.stringify({ data: base64 }), // Send the data in blob format }; fetch(getApp().getExpressAddress() + `/api/speech/recognize`, requestOptions) @@ -32,23 +32,26 @@ export class SpeechController { } playText(text, radio: RadioSink) { - const textToSpeechSource = getApp() - .getAudioManager() - .getInternalTextToSpeechSource(); - - textToSpeechSource.connect(radio); - textToSpeechSource.playText(text); - radio.setPtt(true); - textToSpeechSource.onMessageCompleted = () => { - radio.setPtt(false); - textToSpeechSource.disconnect(radio); - } + if (this.#playingText) return; + this.#playingText = true; + const textToSpeechSource = getApp().getAudioManager().getInternalTextToSpeechSource(); + textToSpeechSource.connect(radio); + textToSpeechSource.playText(text); + radio.setPtt(true); + textToSpeechSource.onMessageCompleted = () => { + this.#playingText = false; + radio.setPtt(false); + textToSpeechSource.disconnect(radio); + }; + window.setTimeout(() => { + this.#playingText = false; + }, 30000); // Reset to false as failsafe } #executeCommand(text, radio) { console.log(`Received speech command: ${text}`); - if (text.indexOf("olympus") === 0 ) { + if (text.indexOf("olympus") === 0) { this.#olympusCommand(text, radio); } else if (text.indexOf(getApp().getAWACSController()?.getCallsign().toLowerCase()) === 0) { getApp().getAWACSController()?.executeCommand(text, radio); @@ -58,11 +61,9 @@ export class SpeechController { #olympusCommand(text, radio) { if (text.indexOf("request straight") > 0 || text.indexOf("request straightin") > 0) { this.playText("Confirm you are on step 13, being a pussy?", radio); - } - else if (text.indexOf("bolter") > 0) { + } else if (text.indexOf("bolter") > 0) { this.playText("What an idiot, I never boltered, 100% boarding rate", radio); - } - else if (text.indexOf("read back") > 0) { + } else if (text.indexOf("read back") > 0) { this.playText(text.replace("olympus", ""), radio); } } diff --git a/frontend/react/src/audio/texttospeechsource.ts b/frontend/react/src/audio/texttospeechsource.ts index 0d69e8c8..de0ceb83 100644 --- a/frontend/react/src/audio/texttospeechsource.ts +++ b/frontend/react/src/audio/texttospeechsource.ts @@ -6,12 +6,13 @@ export class TextToSpeechSource extends AudioSource { #source: AudioBufferSourceNode; #duration: number = 0; #currentPosition: number = 0; - #updateInterval: number | null; + #updateInterval: number | null = null; #lastUpdateTime: number = 0; #playing = false; #audioBuffer: AudioBuffer; #restartTimeout: any; #looping = false; + #loading = false; onMessageCompleted: () => void = () => {}; constructor() { @@ -27,6 +28,8 @@ export class TextToSpeechSource extends AudioSource { body: JSON.stringify({ text }), // Send the data in JSON format }; + this.#loading = true; + fetch(getApp().getExpressAddress() + `/api/speech/generate`, requestOptions) .then((response) => { if (response.status === 200) { @@ -48,10 +51,15 @@ export class TextToSpeechSource extends AudioSource { this.#audioBuffer = audioBuffer; this.#duration = audioBuffer.duration; + this.#loading = false; this.play(); }); }) - .catch((error) => console.error(error)); // Handle errors + .catch((error) => { + console.error(error); + this.#loading = false; + } + ); // Handle errors } play() { @@ -107,6 +115,10 @@ export class TextToSpeechSource extends AudioSource { return this.#playing; } + getLoading() { + return this.#loading; + } + getCurrentPosition() { return this.#currentPosition; } diff --git a/frontend/react/src/ui/panels/components/sourcepanel.tsx b/frontend/react/src/ui/panels/components/sourcepanel.tsx index 11495087..d01faad6 100644 --- a/frontend/react/src/ui/panels/components/sourcepanel.tsx +++ b/frontend/react/src/ui/panels/components/sourcepanel.tsx @@ -1,6 +1,6 @@ import React, { ForwardedRef, forwardRef, useEffect, useState } from "react"; import { OlStateButton } from "../../components/olstatebutton"; -import { faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons"; +import { faHourglass, faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons"; import { getApp } from "../../../olympusapp"; import { AudioSource } from "../../../audio/audiosource"; import { FaChevronUp, FaVolumeHigh, FaXmark } from "react-icons/fa6"; @@ -52,9 +52,7 @@ export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpa