Started work on persistent session data

This commit is contained in:
Davide Passoni 2024-12-01 12:40:07 +01:00
parent 42e62be0f5
commit 5e40d7abf1
20 changed files with 794 additions and 252 deletions

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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);
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,6 @@ export class MissionManager {
AppStateChangedEvent.on((state, subState) => {
if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null);
})
}
/** Update location of bullseyes

View File

@ -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);
}
}

View File

@ -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);
});
}

View File

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

View File

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

View File

@ -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 (
<Modal
open={props.open}
className={`
inline-flex h-fit max-h-[800px] w-[600px] overflow-y-auto scroll-smooth
bg-white p-10
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<div className="flex h-full w-full flex-col gap-12">
<div className={`flex flex-col items-start gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
Please, select the files for the following audio sources
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
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.
</span>
<span
className={`
text-gray-800 text-md
dark:text-gray-500
`}
>
If you don't want to reload your audio sources, press "Skip".
</span>
<div className="mt-4 w-full">
{files.map((fileData, idx) => {
return (
<div
className={`flex w-full content-center justify-between gap-4`}
>
<span className={`my-auto truncate text-white`}>{fileData.filename}</span>
<button
type="button"
disabled={loaded[idx] === true || (idx > 0 && loaded[idx - 1] == false)}
data-disabled={loaded[idx] === true || (idx > 0 && loaded[idx - 1] == false)}
onClick={() => {
var input = document.createElement("input");
input.type = "file";
input.click();
input.onchange = (e: Event) => {
let target = e.target as HTMLInputElement;
if (target && target.files) {
var file = target.files[0];
getApp().getAudioManager().addFileSource(file).setVolume(fileData.volume);
loaded[idx] = true;
setLoaded([...loaded]);
if (idx === loaded.length - 1) getApp().setState(OlympusState.IDLE);
}
};
}}
className={`
mb-2 me-2 ml-auto flex cursor-pointer content-center
items-center gap-2 rounded-sm bg-blue-600 px-5 py-2.5
text-sm font-medium text-white
data-[disabled="true"]:bg-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-800
hover:bg-blue-700
`}
>
{loaded[idx] ? "Loaded" : "Load"}
<FontAwesomeIcon className={`my-auto`} icon={loaded[idx] ? faCheck : faUpload} />
</button>
</div>
);
})}
</div>
</div>
<div className="flex">
<button
type="button"
onClick={() => {getApp().setState(OlympusState.IDLE)}}
className={`
mb-2 me-2 ml-auto flex content-center items-center gap-2
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5 text-sm
font-medium text-white
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Skip
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
</div>
</div>
</Modal>
);
}

View File

@ -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 (
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<div
@ -143,6 +87,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
</div>
</div>
<div className="mt-4 flex flex-col gap-2">
{/*}
{activeGroups.length == 0 ? (
<>No hotgroups</>
) : (
@ -154,6 +99,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
<button onClick={() => getApp().getAudioManager().playText(readout.reduce((acc, line) => acc += " " + line, ""))}>Play</button>
</>
)}
{*/}
</div>
</>
</div>

View File

@ -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> | 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.
</div>
<div className="flex flex-col gap-4 rounded-lg bg-olympus-600 p-4">
<div
className={`
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Control mode
</div>
<div className="flex flex-col justify-start gap-2">
{Object.entries({
human: ["Human", olButtonsVisibilityHuman],
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
dcs: ["From DCS mission", olButtonsVisibilityDcs],
}).map((entry, idx) => {
return (
<div className="flex justify-between" key={idx}>
<span className="font-light text-white">{entry[1][0] as string}</span>
<OlToggle
key={entry[0]}
onClick={() => {
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
toggled={selectionFilter["control"][entry[0]]}
/>
</div>
);
})}
</div>
{selectionID === null && (
<>
<div
className={`
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Control mode
</div>
<div
className={`
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Types and coalitions
</div>
<div className="flex flex-col justify-start gap-2">
{Object.entries({
human: ["Human", olButtonsVisibilityHuman],
olympus: ["Olympus controlled", olButtonsVisibilityOlympus],
dcs: ["From DCS mission", olButtonsVisibilityDcs],
}).map((entry, idx) => {
return (
<div className="flex justify-between" key={idx}>
<span className="font-light text-white">{entry[1][0] as string}</span>
<OlToggle
key={entry[0]}
onClick={() => {
selectionFilter["control"][entry[0]] = !selectionFilter["control"][entry[0]];
setSelectionFilter(JSON.parse(JSON.stringify(selectionFilter)));
}}
toggled={selectionFilter["control"][entry[0]]}
/>
</div>
);
})}
</div>
<div
className={`
text-bold border-b-2 border-b-white/10 pb-2 text-gray-400
`}
>
Types and coalitions
</div>
</>
)}
<table>
<tbody>
<tr>
<td></td>
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
<td className="pb-4 text-center font-bold text-red-500">RED</td>
</tr>
{selectionBlueprint === null &&
{selectionID === null && (
<tr>
<td></td>
<td className="pb-4 text-center font-bold text-blue-500">BLUE</td>
<td className="pb-4 text-center font-bold text-gray-500">NEUTRAL</td>
<td className="pb-4 text-center font-bold text-red-500">RED</td>
</tr>
)}
{selectionID === null &&
Object.entries({
aircraft: olButtonsVisibilityAircraft,
helicopter: olButtonsVisibilityHelicopter,
@ -311,7 +316,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<td className="text-center" key={coalition}>
<OlCheckbox
checked={selectionFilter[coalition][entry[0]]}
disabled={selectionBlueprint !== null}
disabled={selectionID !== null}
onChange={() => {
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 }) {
</tr>
);
})}
<tr>
<td className="text-gray-200"></td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["blue"]).some((value) => 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)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["neutral"]).some((value) => 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)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["red"]).some((value) => 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)));
}}
/>
</td>
</tr>
{selectionID === null && (
<tr>
<td className="text-gray-200"></td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["blue"]).some((value) => 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)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["neutral"]).some((value) => 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)));
}}
/>
</td>
<td className="text-center">
<OlCheckbox
checked={Object.values(selectionFilter["red"]).some((value) => 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)));
}}
/>
</td>
</tr>
)}
</tbody>
</table>
<div>
@ -369,29 +376,28 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
<OlSearchBar
onChange={(value) => {
setFilterString(value);
selectionBlueprint && setSelectionBlueprint(null);
selectionID && setSelectionID(null);
}}
text={selectionBlueprint ? selectionBlueprint.label : filterString}
text={selectionID ? (getApp().getUnitsManager().getUnitByID(selectionID)?.getUnitName() ?? "") : filterString}
/>
</div>
<OlDropdown buttonRef={searchBarRefState} open={filterString !== "" && selectionBlueprint === null}>
<OlDropdown buttonRef={searchBarRefState} open={filterString !== "" && selectionID === null}>
<div className="max-h-48">
{filterString !== "" &&
Object.keys(mergedFilteredUnits).length > 0 &&
Object.entries(mergedFilteredUnits).map((entry) => {
const blueprint = entry[1];
filteredUnits.length > 0 &&
filteredUnits.map((unit) => {
return (
<OlDropdownItem
key={entry[0]}
key={unit[0]}
onClick={() => {
setSelectionBlueprint(blueprint);
setSelectionID(unit.ID);
}}
>
{blueprint.label}
{unit.getUnitName()}
</OlDropdownItem>
);
})}
{Object.keys(mergedFilteredUnits).length == 0 && <span>No results</span>}
{filteredUnits.length == 0 && <span>No results</span>}
</div>
</OlDropdown>
</div>
@ -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 }) {
<OlButtonGroupItem
key={idx}
onClick={() => {
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}
></OlNumberInput>
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
my-auto w-20
`}>
<OlDropdown
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
className={`my-auto w-20`}
>
<OlDropdownItem
key={"X"}
onClick={() => {
@ -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
`}

View File

@ -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() {
<LoginModal open={appState === OlympusState.LOGIN} />
<ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
<FileSourceLoadPrompt open={appState === OlympusState.LOAD_FILES}/>
<div id="map-container" className="z-0 h-full w-screen" />

View File

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

View File

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