From 5e40d7abf1ac5f24d93e23f2f4eb812da145518b Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Sun, 1 Dec 2024 12:40:07 +0100 Subject: [PATCH] Started work on persistent session data --- frontend/react/src/audio/audiomanager.ts | 87 ++++--- frontend/react/src/audio/filesource.ts | 6 + frontend/react/src/audio/radiosink.ts | 29 ++- frontend/react/src/audio/recorder.ts | 77 ++++++ frontend/react/src/audio/speechcontroller.ts | 45 ++++ frontend/react/src/constants/constants.ts | 1 + frontend/react/src/controllers/awacs.ts | 107 ++++++++ frontend/react/src/events.ts | 15 +- frontend/react/src/interfaces.ts | 12 +- frontend/react/src/mission/missionmanager.ts | 1 - frontend/react/src/olympusapp.ts | 28 ++- frontend/react/src/other/utils.ts | 8 + frontend/react/src/server/servermanager.ts | 5 +- frontend/react/src/sessiondata.ts | 112 +++++++++ .../src/ui/modals/filesourceloadprompt.tsx | 129 ++++++++++ frontend/react/src/ui/panels/awacsmenu.tsx | 62 +---- .../react/src/ui/panels/unitcontrolmenu.tsx | 233 +++++++++--------- frontend/react/src/ui/ui.tsx | 2 + frontend/server/src/routes/api/speech.ts | 60 ++--- frontend/server/src/routes/resources.ts | 27 ++ 20 files changed, 794 insertions(+), 252 deletions(-) create mode 100644 frontend/react/src/audio/recorder.ts create mode 100644 frontend/react/src/audio/speechcontroller.ts create mode 100644 frontend/react/src/controllers/awacs.ts create mode 100644 frontend/react/src/sessiondata.ts create mode 100644 frontend/react/src/ui/modals/filesourceloadprompt.tsx diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 9bc8b25a..d0ecff6b 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -1,4 +1,4 @@ -import { AudioMessageType } from "../constants/constants"; +import { AudioMessageType, OlympusState } from "../constants/constants"; import { MicrophoneSource } from "./microphonesource"; import { RadioSink } from "./radiosink"; import { getApp } from "../olympusapp"; @@ -23,13 +23,14 @@ import { } from "../events"; import { OlympusConfig } from "../interfaces"; import { TextToSpeechSource } from "./texttospeechsource"; +import { SpeechController } from "./speechcontroller"; export class AudioManager { #audioContext: AudioContext; - #synth = window.speechSynthesis; #devices: MediaDeviceInfo[] = []; #input: MediaDeviceInfo; #output: MediaDeviceInfo; + #speechController: SpeechController; /* The playback pipeline enables audio playback on the speakers/headphones */ #playbackPipeline: PlaybackPipeline; @@ -50,6 +51,7 @@ export class AudioManager { #guid: string = makeID(22); #SRSClientUnitIDs: number[] = []; #syncInterval: number; + #speechRecognition: boolean = true; constructor() { ConfigLoadedEvent.on((config: OlympusConfig) => { @@ -70,6 +72,8 @@ export class AudioManager { altKey: false, }); }); + + this.#speechController = new SpeechController(); } start() { @@ -77,7 +81,6 @@ export class AudioManager { this.#syncRadioSettings(); }, 1000); - this.#running = true; this.#audioContext = new AudioContext({ sampleRate: 16000 }); //@ts-ignore @@ -124,7 +127,12 @@ export class AudioManager { audioPacket.getFrequencies().forEach((frequencyInfo) => { if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation && sink.getTuned()) { sink.setReceiving(true); - this.#playbackPipeline.playBuffer(audioPacket.getAudioData().buffer); + + /* Make a copy of the array buffer for the playback pipeline to use */ + var dst = new ArrayBuffer(audioPacket.getAudioData().buffer.byteLength); + new Uint8Array(dst).set(new Uint8Array(audioPacket.getAudioData().buffer)); + sink.recordArrayBuffer(audioPacket.getAudioData().buffer); + this.#playbackPipeline.playBuffer(dst); } }); } else { @@ -144,16 +152,40 @@ export class AudioManager { this.#sources.push(microphoneSource); AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); - /* Add two default radios */ - this.addRadio(); - this.addRadio(); + let sessionRadios = getApp().getSessionDataManager().getSessionData().radios; + if (sessionRadios) { + /* Load session radios */ + sessionRadios.forEach((options) => { + let newRadio = this.addRadio(); + newRadio?.setFrequency(options.frequency); + newRadio?.setModulation(options.modulation); + }); + } else { + /* Add two default radios */ + this.addRadio(); + this.addRadio(); + } + + let sessionUnitSinks = getApp().getSessionDataManager().getSessionData().unitSinks; + if (sessionUnitSinks) { + /* Load session radios */ + sessionUnitSinks.forEach((options) => { + let unit = getApp().getUnitsManager().getUnitByID(options.ID); + if (unit) { + let newSink = this.addUnitSink(unit); + } + }); + } + let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources; + if (sessionFileSources && sessionFileSources.length > 0) getApp().setState(OlympusState.LOAD_FILES); + + this.#running = true; + AudioManagerStateChangedEvent.dispatch(this.#running); }); const textToSpeechSource = new TextToSpeechSource(); this.#sources.push(textToSpeechSource); - AudioManagerStateChangedEvent.dispatch(this.#running); - navigator.mediaDevices.enumerateDevices().then((devices) => { this.#devices = devices; AudioManagerDevicesChangedEvent.dispatch(devices); @@ -190,13 +222,10 @@ export class AudioManager { addFileSource(file) { console.log(`Adding file source from ${file.name}`); - if (!this.#running) { - console.log("Audio manager not started, aborting..."); - return; - } const newSource = new FileSource(file); this.#sources.push(newSource); AudioSourcesChangedEvent.dispatch(getApp().getAudioManager().getSources()); + return newSource; } getSources() { @@ -205,10 +234,6 @@ export class AudioManager { removeSource(source: AudioSource) { console.log(`Removing source ${source.getName()}`); - if (!this.#running) { - console.log("Audio manager not started, aborting..."); - return; - } source.disconnect(); this.#sources = this.#sources.filter((v) => v != source); AudioSourcesChangedEvent.dispatch(this.#sources); @@ -216,26 +241,22 @@ export class AudioManager { addUnitSink(unit: Unit) { console.log(`Adding unit sink for unit with ID ${unit.ID}`); - if (!this.#running) { - console.log("Audio manager not started, aborting..."); - return; - } - this.#sinks.push(new UnitSink(unit)); + const newSink = new UnitSink(unit); + this.#sinks.push(newSink); AudioSinksChangedEvent.dispatch(this.#sinks); + return newSink; } addRadio() { console.log("Adding new radio"); - if (!this.#running || this.#sources[0] === undefined) { - console.log("Audio manager not started, aborting..."); - return; - } const newRadio = new RadioSink(); + newRadio.speechDataAvailable = (blob) => this.#speechController.analyzeData(blob); this.#sinks.push(newRadio); /* Set radio name by default to be incremental number */ newRadio.setName(`Radio ${this.#sinks.length}`); this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio); AudioSinksChangedEvent.dispatch(this.#sinks); + return newRadio; } getSinks() { @@ -244,10 +265,6 @@ export class AudioManager { removeSink(sink) { console.log(`Removing sink ${sink.getName()}`); - if (!this.#running) { - console.log("Audio manager not started, aborting..."); - return; - } sink.disconnect(); this.#sinks = this.#sinks.filter((v) => v != sink); let idx = 1; @@ -309,12 +326,20 @@ export class AudioManager { this.#sources.find((source) => source instanceof TextToSpeechSource)?.playText(text); } + setSpeechRecognition(speechRecognition: boolean) { + this.#speechRecognition = this.#speechRecognition; + } + + getSpeechRecognition() { + return this.#speechRecognition; + } + #syncRadioSettings() { /* Send the radio settings of each radio to the SRS backend */ let message = { type: "Settings update", guid: this.#guid, - coalition: 2, + coalition: 2, // TODO settings: this.#sinks .filter((sink) => sink instanceof RadioSink) .map((radio) => { diff --git a/frontend/react/src/audio/filesource.ts b/frontend/react/src/audio/filesource.ts index 29647c6b..229c699c 100644 --- a/frontend/react/src/audio/filesource.ts +++ b/frontend/react/src/audio/filesource.ts @@ -4,6 +4,7 @@ import { AudioSourcesChangedEvent } from "../events"; export class FileSource extends AudioSource { #file: File; + #filename: string; #source: AudioBufferSourceNode; #duration: number = 0; #currentPosition: number = 0; @@ -17,6 +18,7 @@ export class FileSource extends AudioSource { constructor(file) { super(); this.#file = file; + this.#filename = this.#file?.name; this.setName(this.#file?.name ?? "N/A"); @@ -117,4 +119,8 @@ export class FileSource extends AudioSource { getLooping() { return this.#looping; } + + getFilename() { + return this.#filename; + } } diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index cf75253d..02bdc8eb 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -2,8 +2,8 @@ import { AudioSink } from "./audiosink"; import { AudioPacket } from "./audiopacket"; import { getApp } from "../olympusapp"; import { AudioSinksChangedEvent } from "../events"; -import { timeStamp } from "console"; import { makeID } from "../other/utils"; +import { Recorder } from "./recorder"; /* Radio sink, basically implements a simple SRS Client in Olympus. Does not support encryption at this moment */ export class RadioSink extends AudioSink { @@ -19,10 +19,15 @@ export class RadioSink extends AudioSink { #clearReceivingTimeout: number; #packetID = 0; #guid = makeID(22); + #recorder: Recorder; + speechDataAvailable: (blob: Blob) => void; constructor() { super(); + this.#recorder = new Recorder(); + this.#recorder.onRecordingCompleted = (blob) => this.speechDataAvailable(blob); + this.#encoder = new AudioEncoder({ output: (data) => this.handleEncodedData(data), error: (e) => { @@ -104,11 +109,19 @@ export class RadioSink extends AudioSink { setReceiving(receiving) { // Only do it if actually changed - if (receiving !== this.#receiving) AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); + if (receiving !== this.#receiving) { + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); + + if (getApp().getAudioManager().getSpeechRecognition()) { + if (receiving) this.#recorder.start(); + else this.#recorder.stop(); + } + } if (receiving) { window.clearTimeout(this.#clearReceivingTimeout); this.#clearReceivingTimeout = window.setTimeout(() => this.setReceiving(false), 500); } + this.#receiving = receiving; } @@ -124,11 +137,13 @@ export class RadioSink extends AudioSink { let audioPacket = new AudioPacket(); audioPacket.setAudioData(new Uint8Array(arrayBuffer)); audioPacket.setPacketID(this.#packetID++); - audioPacket.setFrequencies([{ + audioPacket.setFrequencies([ + { frequency: this.#frequency, modulation: this.#modulation, - encryption: 0 - }]) + encryption: 0, + }, + ]); audioPacket.setClientGUID(getApp().getAudioManager().getGuid()); audioPacket.setTransmissionGUID(this.#guid); getApp().getAudioManager().send(audioPacket.toByteArray()); @@ -139,4 +154,8 @@ export class RadioSink extends AudioSink { this.#encoder.encode(audioData); audioData.close(); } + + recordArrayBuffer(arrayBuffer: ArrayBuffer) { + this.#recorder.recordBuffer(arrayBuffer); + } } diff --git a/frontend/react/src/audio/recorder.ts b/frontend/react/src/audio/recorder.ts new file mode 100644 index 00000000..6b30d008 --- /dev/null +++ b/frontend/react/src/audio/recorder.ts @@ -0,0 +1,77 @@ +export class Recorder { + #decoder = new AudioDecoder({ + output: (chunk) => this.#handleDecodedData(chunk), + error: (e) => console.log(e), + }); + #trackGenerator: any; // TODO can we have typings? + #writer: any; + #gainNode: GainNode; + #mediaRecorder: MediaRecorder; + #recording = false; + #chunks: any[] = []; + onRecordingCompleted: (blob: Blob) => void + + constructor() { + this.#decoder.configure({ + codec: "opus", + numberOfChannels: 1, + sampleRate: 16000, + //@ts-ignore // TODO why is this giving an error? + opus: { + frameDuration: 40000, + }, + bitrateMode: "constant", + }); + + //@ts-ignore + this.#trackGenerator = new MediaStreamTrackGenerator({ kind: "audio" }); + this.#writer = this.#trackGenerator.writable.getWriter(); + + const stream = new MediaStream([this.#trackGenerator]); + this.#mediaRecorder = new MediaRecorder(stream, { + audioBitsPerSecond: 256000, + mimeType: `audio/webm;codecs="opus"`, + }); + } + + recordBuffer(arrayBuffer) { + if (!this.#recording) return; + + const init = { + type: "key", + data: arrayBuffer, + timestamp: 0, + duration: 2000000, + transfer: [arrayBuffer], + }; + //@ts-ignore //TODO Typings? + let encodedAudioChunk = new EncodedAudioChunk(init); + + this.#decoder.decode(encodedAudioChunk); + } + + start() { + this.#mediaRecorder.start(); + this.#recording = true; + this.#chunks = []; + + this.#mediaRecorder.onstop = (e) => { + if (this.#chunks.length > 0) this.onRecordingCompleted(this.#chunks[0]); + }; + + this.#mediaRecorder.ondataavailable = (e) => { + this.#chunks.push(e.data); + }; + } + + stop() { + this.#mediaRecorder.stop(); + this.#recording = false; + } + + #handleDecodedData(audioData) { + this.#writer.ready.then(() => { + this.#writer.write(audioData); + }); + } +} diff --git a/frontend/react/src/audio/speechcontroller.ts b/frontend/react/src/audio/speechcontroller.ts new file mode 100644 index 00000000..da2a9d84 --- /dev/null +++ b/frontend/react/src/audio/speechcontroller.ts @@ -0,0 +1,45 @@ +import { getApp } from "../olympusapp"; +import { blobToBase64 } from "../other/utils"; + +export class SpeechController { + constructor() {} + + analyzeData(blob: Blob) { + blobToBase64(blob) + .then((base64) => { + 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 + }; + + fetch(getApp().getExpressAddress() + `/api/speech/recognize`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`Speech recognized correctly`); + return response.text(); + } else { + getApp().addInfoMessage("Error recognizing speech"); + throw new Error("Error saving profile"); + } + }) + .then((text) => this.#executeCommand(text)) + .catch((error) => console.error(error)); // Handle errors + }) + .catch((error) => console.error(error)); + } + + #executeCommand(text) { + console.log(`Received speech command: ${text}`); + + if (text.indexOf("olympus") === 0 ) { + this.#olympusCommand(text); + } else if (text.indexOf(getApp().getAWACSController()?.getCallsign().toLowerCase()) === 0) { + getApp().getAWACSController()?.executeCommand(text); + } + } + + #olympusCommand(text) { + + } +} diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 81649e71..86de3a97 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -287,6 +287,7 @@ export enum OlympusState { AUDIO = "Audio", AIRBASE = "Airbase", GAME_MASTER = "Game master", + LOAD_FILES = "Load files" } export const NO_SUBSTATE = "No substate"; diff --git a/frontend/react/src/controllers/awacs.ts b/frontend/react/src/controllers/awacs.ts new file mode 100644 index 00000000..da053f23 --- /dev/null +++ b/frontend/react/src/controllers/awacs.ts @@ -0,0 +1,107 @@ +import { getApp } from "../olympusapp"; +import { Coalition } from "../types/types"; +import { Unit } from "../unit/unit"; +import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../other/utils"; + +const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West", "North"]; +const relTrackStrings = ["hot", "flank right", "beam right", "cold", "cold", "cold", "beam left", "flank left", "hot"]; + +export class AWACSController { + #coalition: Coalition = "blue"; + #callsign: string = "Magic"; + #referenceUnit: Unit; + + constructor() {} + + executeCommand(text) { + if (text.indexOf("request picture") > 0) { + console.log("Requested AWACS picture"); + const readout = this.createPicture(true); + getApp() + .getAudioManager() + .playText(readout.reduce((acc, line) => (acc += " " + line), "")); + } + } + + createPicture(forTextToSpeech: boolean = false, unitName?: string) { + let readout: string[] = []; + + const mapOptions = getApp().getMap().getOptions(); + const activeGroups = Object.values( + getApp() + .getUnitsManager() + .computeClusters((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition, 6) ?? {} + ); + const bullseyes = getApp().getMissionManager().getBullseyes(); + const referenceUnit: Unit | undefined = unitName ? undefined : this.#referenceUnit; //TODO + + if (bullseyes) { + if (referenceUnit !== undefined) { + readout.push(`${this.#callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s" : ""}`); + readout.push( + ...activeGroups.map((group, idx) => { + let order = "th"; + if (idx == 0) order = "st"; + else if (idx == 1) order = "nd"; + else if (idx == 2) order = "rd"; + + let trackDegs = + bearing(group[0].getPosition().lat, group[0].getPosition().lng, referenceUnit.getPosition().lat, referenceUnit.getPosition().lng) - + rad2deg(group[0].getTrack()); + if (trackDegs < 0) trackDegs += 360; + if (trackDegs > 360) trackDegs -= 360; + let trackIndex = Math.round(trackDegs / 45); + + let groupLine = `${activeGroups.length > 1 ? idx + 1 + "" + order + " group" : "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`; + + if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"; + else groupLine += ", hostile"; + + return groupLine; + }) + ); + } else { + readout.push(`${this.#callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s" : ""}`); + readout.push( + ...activeGroups.map((group, idx) => { + let order = "th"; + if (idx == 0) order = "st"; + else if (idx == 1) order = "nd"; + else if (idx == 2) order = "rd"; + + let trackDegs = rad2deg(group[0].getTrack()); + if (trackDegs < 0) trackDegs += 360; + let trackIndex = Math.round(trackDegs / 45); + + let groupLine = `${activeGroups.length > 1 ? idx + 1 + "" + order + " group" : "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${(mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`; + + if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey"; + else groupLine += ", hostile"; + + return groupLine; + }) + ); + } + } + + return readout; + } + + createBogeyDope(forTextToSpeech: boolean = false, unitName: string) {} + + setCallsign(callsign: string) { + this.#callsign = callsign; + } + + getCallsign() { + return this.#callsign; + } + + setCoalition(coalition: Coalition) { + this.#coalition = coalition; + } + + getCoalition() { + return this.#coalition; + } +} diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts index c2a56123..651b06f3 100644 --- a/frontend/react/src/events.ts +++ b/frontend/react/src/events.ts @@ -1,7 +1,7 @@ import { AudioSink } from "./audio/audiosink"; import { AudioSource } from "./audio/audiosource"; import { OlympusState, OlympusSubState } from "./constants/constants"; -import { CommandModeOptions, OlympusConfig, ServerStatus, SpawnRequestTable } from "./interfaces"; +import { CommandModeOptions, OlympusConfig, ServerStatus, SessionData, SpawnRequestTable } from "./interfaces"; import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle"; import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon"; import { Airbase } from "./mission/airbase"; @@ -150,6 +150,19 @@ export class ModalEvent { } } +export class SessionDataLoadedEvent { + static on(callback: (sessionData: SessionData) => void) { + document.addEventListener(this.name, (ev: CustomEventInit) => { + callback(ev.detail.sessionData); + }); + } + + static dispatch(sessionData: SessionData) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { sessionData } })); + console.log(`Event ${this.name} dispatched`); + } +} + /************** Map events ***************/ export class MouseMovedEvent { static on(callback: (latlng: LatLng, elevation: number) => void) { diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 34758ceb..90904d1f 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -29,9 +29,15 @@ export interface OlympusConfig { profiles?: ProfileOptions; } +export interface SessionData { + radios?: { frequency: number; modulation: number }[]; + fileSources?: { filename: string; volume: number }[]; + unitSinks?: {ID: number}[]; +} + export interface ProfileOptions { - mapOptions: MapOptions, - shortcuts: {[key: string]: ShortcutOptions} + mapOptions: MapOptions; + shortcuts: { [key: string]: ShortcutOptions }; } export interface ContextMenuOption { @@ -100,7 +106,7 @@ export interface SpawnRequestTable { coalition: string; unit: UnitSpawnTable; amount: number; - quickAccessName?: string + quickAccessName?: string; } export interface EffectRequestTable { diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 82b2b646..f95ff5de 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -37,7 +37,6 @@ export class MissionManager { AppStateChangedEvent.on((state, subState) => { if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null); }) - } /** Update location of bullseyes diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index c84e9008..d7cc97c7 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -23,6 +23,8 @@ import { AudioManager } from "./audio/audiomanager"; import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events"; import { OlympusConfig, ProfileOptions } from "./interfaces"; +import { AWACSController } from "./controllers/awacs"; +import { SessionDataManager } from "./sessiondata"; export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}"; export var IP = window.location.toString(); @@ -46,8 +48,12 @@ export class OlympusApp { #unitsManager: UnitsManager | null = null; #weaponsManager: WeaponsManager | null = null; #audioManager: AudioManager | null = null; + #sessionDataManager: SessionDataManager | null = null; //#pluginsManager: // TODO + /* Controllers */ + #AWACSController: AWACSController | null = null; + constructor() { SelectedUnitsChangedEvent.on((selectedUnits) => { if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL); @@ -86,12 +92,20 @@ export class OlympusApp { return this.#audioManager as AudioManager; } + getSessionDataManager() { + return this.#sessionDataManager as SessionDataManager; + } + /* TODO getPluginsManager() { return null // this.#pluginsManager as PluginsManager; } */ + getAWACSController() { + return this.#AWACSController; + } + getExpressAddress() { return `${window.location.href.split("?")[0].replace("vite/", "").replace("vite", "")}express`; } @@ -103,6 +117,7 @@ export class OlympusApp { start() { /* Initialize base functionalitites */ this.#shortcutManager = new ShortcutManager(); /* Keep first */ + this.#sessionDataManager = new SessionDataManager(); this.#map = new Map("map-container"); @@ -112,6 +127,9 @@ export class OlympusApp { this.#weaponsManager = new WeaponsManager(); this.#audioManager = new AudioManager(); + /* Controllers */ + this.#AWACSController = new AWACSController(); + /* Set the address of the server */ this.getServerManager().setAddress(this.getBackendAddress()); this.getAudioManager().setAddress(this.getExpressAddress()); @@ -207,7 +225,7 @@ export class OlympusApp { .then((response) => { if (response.status === 200) { console.log(`Profile ${this.#profileName} reset correctly`); - location.reload() + location.reload(); } else { this.addInfoMessage("Error resetting profile"); throw new Error("Error resetting profile"); @@ -228,7 +246,7 @@ export class OlympusApp { .then((response) => { if (response.status === 200) { console.log(`All profiles reset correctly`); - location.reload() + location.reload(); } else { this.addInfoMessage("Error resetting profiles"); throw new Error("Error resetting profiles"); @@ -243,6 +261,10 @@ export class OlympusApp { else return null; } + getProfileName() { + return this.#profileName; + } + loadProfile() { const profile = this.getProfile(); if (profile) { @@ -280,4 +302,6 @@ export class OlympusApp { InfoPopupEvent.dispatch(this.#infoMessages); }, 5000); } + + } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 307c9d4f..b9ca3b9b 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -397,3 +397,11 @@ export function wait(time) { export function computeBearingRangeString(latlng1, latlng2) { return `${bearing(latlng1.lat, latlng1.lng, latlng2.lat, latlng2.lng).toFixed()}/${(latlng1.distanceTo(latlng2) / 1852).toFixed(0)}`; } + +export function blobToBase64(blob) { + return new Promise((resolve: (value: string) => void, _) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.readAsDataURL(blob); + }); +} \ No newline at end of file diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 18fa6122..8e2f00e6 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -668,7 +668,10 @@ export class ServerManager { checkSessionHash(newSessionHash: string) { if (this.#sessionHash != null) { if (newSessionHash !== this.#sessionHash) location.reload(); - } else this.#sessionHash = newSessionHash; + } else { + this.#sessionHash = newSessionHash; + getApp().getSessionDataManager().loadSessionData(newSessionHash); + } } setConnected(newConnected: boolean) { diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts new file mode 100644 index 00000000..b257a12b --- /dev/null +++ b/frontend/react/src/sessiondata.ts @@ -0,0 +1,112 @@ +import { FileSource } from "./audio/filesource"; +import { RadioSink } from "./audio/radiosink"; +import { UnitSink } from "./audio/unitsink"; +import { AudioSinksChangedEvent, AudioSourcesChangedEvent, SessionDataLoadedEvent as SessionDataChangedEvent } from "./events"; +import { SessionData } from "./interfaces"; +import { getApp } from "./olympusapp"; + +export class SessionDataManager { + #sessionData: SessionData = {}; + #sessionHash: string = ""; + #saveSessionDataTimeout: number | null = null; + + constructor() { + AudioSinksChangedEvent.on((audioSinks) => { + if (getApp().getAudioManager().isRunning()) { + this.#sessionData.radios = audioSinks + .filter((sink) => sink instanceof RadioSink) + .map((radioSink) => { + return { + frequency: radioSink.getFrequency(), + modulation: radioSink.getModulation(), + }; + }); + + this.#sessionData.unitSinks = audioSinks + .filter((sink) => sink instanceof UnitSink) + .map((unitSink) => { + return { + ID: unitSink.getUnit().ID + }; + }); + + this.#saveSessionData(); + } + }); + + AudioSourcesChangedEvent.on((audioSources) => { + if (getApp().getAudioManager().isRunning()) { + this.#sessionData.fileSources = audioSources + .filter((sink) => sink instanceof FileSource) + .map((fileSource) => { + return { filename: fileSource.getFilename(), volume: fileSource.getVolume() }; + }); + this.#saveSessionData(); + } + }); + } + + loadSessionData(sessionHash?: string) { + if (sessionHash) this.#sessionHash = sessionHash; + if (this.#sessionHash === undefined) { + console.error("Trying to load session data but no session hash provided"); + return; + } + + const requestOptions = { + method: "PUT", // Specify the request method + headers: { "Content-Type": "application/json" }, // Specify the content type + body: JSON.stringify({ sessionHash }), // Send the data in JSON format + }; + + fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getProfileName()}`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${sessionHash} loaded correctly`); + return response.json(); + } else { + getApp().addInfoMessage("No session data found for this profile"); + throw new Error("No session data found for this profile"); + } + }) // Parse the response as JSON + .then((sessionData) => { + this.#sessionData = sessionData; + this.#applySessionData(); + SessionDataChangedEvent.dispatch(this.#sessionData); + }) + .catch((error) => console.error(error)); // Handle errors + } + + getSessionData() { + return this.#sessionData; + } + + #saveSessionData() { + if (this.#saveSessionDataTimeout) window.clearTimeout(this.#saveSessionDataTimeout); + this.#saveSessionDataTimeout = window.setTimeout(() => { + const requestOptions = { + method: "PUT", // Specify the request method + headers: { "Content-Type": "application/json" }, // Specify the content type + body: JSON.stringify({ sessionHash: this.#sessionHash, sessionData: this.#sessionData }), // Send the data in JSON format + }; + + fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getProfileName()}`, requestOptions) + .then((response) => { + if (response.status === 200) { + console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${this.#sessionHash} saved correctly`); + console.log(this.#sessionData); + SessionDataChangedEvent.dispatch(this.#sessionData); + } else { + getApp().addInfoMessage("Error loading session data"); + throw new Error("Error loading session data"); + } + }) // Parse the response as JSON + .catch((error) => console.error(error)); // Handle errors + this.#saveSessionDataTimeout = null; + }, 1000); + } + + #applySessionData() { + let asd = 1; + } +} diff --git a/frontend/react/src/ui/modals/filesourceloadprompt.tsx b/frontend/react/src/ui/modals/filesourceloadprompt.tsx new file mode 100644 index 00000000..c9d62afc --- /dev/null +++ b/frontend/react/src/ui/modals/filesourceloadprompt.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from "react"; +import { Modal } from "./components/modal"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight, faCheck, faUpload } from "@fortawesome/free-solid-svg-icons"; +import { getApp } from "../../olympusapp"; +import { OlympusState } from "../../constants/constants"; +import { SessionDataLoadedEvent } from "../../events"; + +export function FileSourceLoadPrompt(props: { open: boolean }) { + const [files, setFiles] = useState([] as { filename: string; volume: number }[]); + const [loaded, setLoaded] = useState([] as boolean[]); + + useEffect(() => { + SessionDataLoadedEvent.on((sessionData) => { + if (getApp().getState() === OlympusState.LOAD_FILES) return; // TODO don't like this, is hacky Should avoid reading state directly + if (sessionData.fileSources) { + setFiles([...sessionData.fileSources]); + setLoaded( + sessionData.fileSources.map((file) => { + return false; + }) + ); + } + }); + }, []); + + return ( + +
+
+ + Please, select the files for the following audio sources + + + Browsers can't automatically load files from your computer, therefore you must click on the following buttons to select the original files for each + audio file source. + + + If you don't want to reload your audio sources, press "Skip". + +
+ {files.map((fileData, idx) => { + return ( +
+ {fileData.filename} + +
+ ); + })} +
+
+
+ +
+
+
+ ); +} diff --git a/frontend/react/src/ui/panels/awacsmenu.tsx b/frontend/react/src/ui/panels/awacsmenu.tsx index af0355d8..789ced1d 100644 --- a/frontend/react/src/ui/panels/awacsmenu.tsx +++ b/frontend/react/src/ui/panels/awacsmenu.tsx @@ -14,10 +14,6 @@ import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { FaQuestionCircle } from "react-icons/fa"; import { Unit } from "../../unit/unit"; import { Bullseye } from "../../mission/bullseye"; -import { bearing, coalitionToEnum, computeBearingRangeString, mToFt, rad2deg } from "../../other/utils"; - -const trackStrings = ["North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West", "North"] -const relTrackStrings = ["hot", "flank right", "beam right", "cold", "cold", "cold", "beam left", "flank left", "hot"] export function AWACSMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { const [callsign, setCallsign] = useState("Magic"); @@ -35,61 +31,9 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children? UnitUpdatedEvent.on((unit) => setRefreshTime(Date.now())); }, []); - const activeGroups = Object.values(getApp()?.getUnitsManager().computeClusters((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition, 6) ?? {}); + + - /*Object.values(hotgroups).filter((hotgroup) => { - return hotgroup.every((unit) => unit.getCoalition() !== mapOptions.AWACSCoalition); - });*/ - - let readout: string[] = []; - - if (bullseyes) { - if (referenceUnit) { - readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`); - readout.push( - ...activeGroups.map((group, idx) => { - let order = "th"; - if (idx == 0) order = "st"; - else if (idx == 1) order = "nd"; - else if (idx == 2) order = "rd"; - - let trackDegs = bearing(group[0].getPosition().lat, group[0].getPosition().lng, referenceUnit.getPosition().lat, referenceUnit.getPosition().lng) - rad2deg(group[0].getTrack()) - if (trackDegs < 0) trackDegs += 360 - if (trackDegs > 360) trackDegs -= 360 - let trackIndex = Math.round(trackDegs / 45) - - let groupLine = `${activeGroups.length > 1? (idx + 1 + "" + order + " group"): "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, ${relTrackStrings[trackIndex]}`; - - if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey" - else groupLine += ", hostile" - - return groupLine; - }) - ); - } else { - readout.push(`${callsign}, ${activeGroups.length} group${activeGroups.length > 1 ? "s": ""}`); - readout.push( - ...activeGroups.map((group, idx) => { - let order = "th"; - if (idx == 0) order = "st"; - else if (idx == 1) order = "nd"; - else if (idx == 2) order = "rd"; - - let trackDegs = rad2deg(group[0].getTrack()) - if (trackDegs < 0) trackDegs += 360 - let trackIndex = Math.round(trackDegs / 45) - - let groupLine = `${activeGroups.length > 1? (idx + 1 + "" + order + " group"): "Single group"} bullseye ${computeBearingRangeString(bullseyes[coalitionToEnum(mapOptions.AWACSCoalition)].getLatLng(), group[0].getPosition()).replace("/", " ")}, ${ (mToFt(group[0].getPosition().alt ?? 0) / 1000).toFixed()} thousand, track ${trackStrings[trackIndex]}`; - - if (group.find((unit) => unit.getCoalition() === "neutral")) groupLine += ", bogey" - else groupLine += ", hostile" - - return groupLine; - }) - ); - } - } - return (
void; children?
+ {/*} {activeGroups.length == 0 ? ( <>No hotgroups ) : ( @@ -154,6 +99,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children? )} + {*/}
diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 905b83da..85640c00 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -110,7 +110,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { navyunit: true, }, }); - const [selectionBlueprint, setSelectionBlueprint] = useState(null as null | UnitBlueprint); + const [selectionID, setSelectionID] = useState(null as null | number); const [searchBarRefState, setSearchBarRefState] = useState(null as MutableRefObject | null); const [filterString, setFilterString] = useState(""); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); @@ -126,7 +126,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { useEffect(() => { if (!searchBarRefState) setSearchBarRefState(searchBarRef); - if (!props.open && selectionBlueprint !== null) setSelectionBlueprint(null); + if (!props.open && selectionID !== null) setSelectionID(null); if (!props.open && filterString !== "") setFilterString(""); }); @@ -194,11 +194,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? []; - const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = [{}, {}, {}, {}, {}]; // TODOgetUnitsByLabel(filterString); - - const mergedFilteredUnits = Object.assign({}, filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits) as { - [key: string]: UnitBlueprint; - }; + const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter( + (unit) => unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 + ); const everyUnitIsGround = selectedCategories.every((category) => { return category === "GroundUnit"; @@ -249,51 +247,58 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { their specific type by using the search input.
-
- Control mode -
-
- {Object.entries({ - human: ["Human", olButtonsVisibilityHuman], - olympus: ["Olympus controlled", olButtonsVisibilityOlympus], - dcs: ["From DCS mission", olButtonsVisibilityDcs], - }).map((entry, idx) => { - return ( -
- {entry[1][0] as string} - { - selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; - setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); - }} - toggled={selectionFilter["control"][entry[0]]} - /> -
- ); - })} -
+ {selectionID === null && ( + <> +
+ Control mode +
-
- Types and coalitions -
+
+ {Object.entries({ + human: ["Human", olButtonsVisibilityHuman], + olympus: ["Olympus controlled", olButtonsVisibilityOlympus], + dcs: ["From DCS mission", olButtonsVisibilityDcs], + }).map((entry, idx) => { + return ( +
+ {entry[1][0] as string} + { + selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]]; + setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); + }} + toggled={selectionFilter["control"][entry[0]]} + /> +
+ ); + })} +
+ +
+ Types and coalitions +
+ + )} - - - - - - - {selectionBlueprint === null && + {selectionID === null && ( + + + + + + + )} + {selectionID === null && Object.entries({ aircraft: olButtonsVisibilityAircraft, helicopter: olButtonsVisibilityHelicopter, @@ -311,7 +316,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { ); })} - - - - - - + {selectionID === null && ( + + + + + + + )}
BLUENEUTRALRED
BLUENEUTRALRED
{ selectionFilter[coalition][entry[0]] = !selectionFilter[coalition][entry[0]]; setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); @@ -323,45 +328,47 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
- value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); - Object.keys(selectionFilter["blue"]).forEach((key) => { - selectionFilter["blue"][key] = newValue; - }); - setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); - }} - /> - - value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); - Object.keys(selectionFilter["neutral"]).forEach((key) => { - selectionFilter["neutral"][key] = newValue; - }); - setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); - }} - /> - - value)} - onChange={() => { - const newValue = !Object.values(selectionFilter["red"]).some((value) => value); - Object.keys(selectionFilter["red"]).forEach((key) => { - selectionFilter["red"][key] = newValue; - }); - setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); - }} - /> -
+ value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["blue"]).some((value) => value); + Object.keys(selectionFilter["blue"]).forEach((key) => { + selectionFilter["blue"][key] = newValue; + }); + setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["neutral"]).some((value) => value); + Object.keys(selectionFilter["neutral"]).forEach((key) => { + selectionFilter["neutral"][key] = newValue; + }); + setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); + }} + /> + + value)} + onChange={() => { + const newValue = !Object.values(selectionFilter["red"]).some((value) => value); + Object.keys(selectionFilter["red"]).forEach((key) => { + selectionFilter["red"][key] = newValue; + }); + setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter))); + }} + /> +
@@ -369,29 +376,28 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { setFilterString(value); - selectionBlueprint && setSelectionBlueprint(null); + selectionID && setSelectionID(null); }} - text={selectionBlueprint ? selectionBlueprint.label : filterString} + text={selectionID ? (getApp().getUnitsManager().getUnitByID(selectionID)?.getUnitName() ?? "") : filterString} />
- +
{filterString !== "" && - Object.keys(mergedFilteredUnits).length > 0 && - Object.entries(mergedFilteredUnits).map((entry) => { - const blueprint = entry[1]; + filteredUnits.length > 0 && + filteredUnits.map((unit) => { return ( { - setSelectionBlueprint(blueprint); + setSelectionID(unit.ID); }} > - {blueprint.label} + {unit.getUnitName()} ); })} - {Object.keys(mergedFilteredUnits).length == 0 && No results} + {filteredUnits.length == 0 && No results}
@@ -409,19 +415,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { Object.values(getApp().getUnitsManager().getUnits()).forEach((unit) => { /* Check if the control type is respected, return if it is not */ if (unit.getHuman() && !selectionFilter["control"]["human"]) return; - if (unit.isControlledByOlympus() && !selectionFilter["control"]["olympus"]) return; - if (!unit.isControlledByDCS() && !selectionFilter["control"]["dcs"]) return; - /* If a specific unit is being selected check that the label is correct, otherwise check if the unit type is active for the coalition */ - if (selectionBlueprint) { - if (unit.getBlueprint()?.label === undefined || unit.getBlueprint()?.label !== selectionBlueprint.label) return; - - /* This is a trick to easily reuse the same checkboxes used to globally enable unit types for a coalition, - since those checkboxes are checked if at least one type is selected for a specific coalition. - */ - if (!Object.values(selectionFilter[unit.getCoalition()]).some((value) => value)) return; + /* If a specific unit is being selected select the unit */ + if (selectionID) { + if (unit.ID !== selectionID) return; } else { if (!selectionFilter[unit.getCoalition()][unit.getMarkerCategory()]) return; } @@ -644,7 +643,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { { - selectedUnits.forEach((unit) => { unit.setROE(ROEs[convertROE(idx)]); setSelectedUnitsData({ @@ -1180,9 +1178,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1} > - + { @@ -1301,9 +1300,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { className={` flex content-center gap-2 rounded-full ${selectedUnits[0].getFuel() > 40 && `bg-green-700`} - ${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && ` - bg-yellow-700 - `} + ${ + selectedUnits[0].getFuel() > 10 && + selectedUnits[0].getFuel() <= 40 && + `bg-yellow-700` + } ${selectedUnits[0].getFuel() <= 10 && `bg-red-700`} px-2 py-1 text-sm font-bold text-white `} diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index 22950104..81eeb3e2 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -11,6 +11,7 @@ import { MapHiddenTypes, MapOptions } from "../types/types"; import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants"; import { getApp, setupApp } from "../olympusapp"; import { LoginModal } from "./modals/loginmodal"; +import { FileSourceLoadPrompt } from "./modals/filesourceloadprompt"; import { MiniMapPanel } from "./panels/minimappanel"; import { UnitControlBar } from "./panels/unitcontrolbar"; @@ -69,6 +70,7 @@ export function UI() { +
diff --git a/frontend/server/src/routes/api/speech.ts b/frontend/server/src/routes/api/speech.ts index af4a445e..0b512bc4 100644 --- a/frontend/server/src/routes/api/speech.ts +++ b/frontend/server/src/routes/api/speech.ts @@ -1,7 +1,6 @@ import express = require("express"); -import fs = require("fs"); -var gtts = require("node-gtts")("en"); - +const gtts = require("node-gtts")("en"); +const speech = require("@google-cloud/speech"); const router = express.Router(); module.exports = function () { @@ -10,37 +9,30 @@ module.exports = function () { gtts.stream(req.body.text).pipe(res); }); - router.get("/recognize", (req, res, next) => { - //// Imports the Google Cloud client library - //const speech = require("@google-cloud/speech"); -// - //// Creates a client - //const client = new speech.SpeechClient(); -// - //// The path to the remote LINEAR16 file - //const gcsUri = "gs://cloud-samples-data/speech/brooklyn_bridge.raw"; -// - //// The audio file's encoding, sample rate in hertz, and BCP-47 language code - //const audio = { - // uri: gcsUri, - //}; - //const config = { - // encoding: "LINEAR16", - // sampleRateHertz: 16000, - // languageCode: "en-US", - //}; - //const request = { - // audio: audio, - // config: config, - //}; -// - //// Detects speech in the audio file - //client.recognize(request).then((response) => { - // const transcription = response.results - // .map((result) => result.alternatives[0].transcript) - // .join("\n"); - // console.log(`Transcription: ${transcription}`); - //}); + router.put("/recognize", (req, res, next) => { + // Creates a client + const client = new speech.SpeechClient(); + + // The audio file's encoding, sample rate in hertz, and BCP-47 language code + const audio = { + content: req.body.data.substring(req.body.data.indexOf("base64,") + 7), + }; + const config = { + encoding: "WEBM_OPUS", + languageCode: "en-US" + }; + const request = { + audio: audio, + config: config, + }; + + // Detects speech in the audio file + client.recognize(request).then((response) => { + const transcription = response[0].results + .map((result) => result.alternatives[0].transcript) + .join("\n"); + res.send(transcription) + }).catch((error) => res.sendStatus(400)); }); return router; diff --git a/frontend/server/src/routes/resources.ts b/frontend/server/src/routes/resources.ts index 33ae8d27..8c7ab160 100644 --- a/frontend/server/src/routes/resources.ts +++ b/frontend/server/src/routes/resources.ts @@ -2,6 +2,9 @@ import express = require("express"); import fs = require("fs"); const router = express.Router(); +let sessionHash = ""; +let sessionData = {} + module.exports = function (configLocation) { router.get("/config", function (req, res, next) { if (fs.existsSync(configLocation)) { @@ -70,5 +73,29 @@ module.exports = function (configLocation) { } }); + router.put("/sessiondata/save/:profileName", function (req, res, next) { + if (req.body.sessionHash === undefined || req.body.sessionData === undefined) res.sendStatus(400); + let thisSessionHash = req.body.sessionHash; + if (thisSessionHash !== sessionHash) { + sessionHash = thisSessionHash; + sessionData = {}; + } + sessionData[req.params.profileName] = req.body.sessionData; + res.end() + }) + + router.put("/sessiondata/load/:profileName", function (req, res, next) { + if (req.body.sessionHash === undefined) res.sendStatus(400); + let thisSessionHash = req.body.sessionHash; + if (thisSessionHash !== sessionHash) { + sessionHash = thisSessionHash; + sessionData = {}; + res.sendStatus(404); + } else { + res.send(sessionData[req.params.profileName]); + res.end(); + } + }) + return router; };