Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

feat(gametts): Add gametts service #991

Merged
merged 1 commit into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions samples/gametts/extension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import NodeCG from "@nodecg/types";
import { requireService } from "nodecg-io-core";
import { GameTTSClient } from "nodecg-io-gametts";

/**
* Plays a hello world tts message inside the graphic of this sample bundle.
*/
async function playTTSInGraphic(client: GameTTSClient, nodecg: NodeCG.ServerAPI) {
const voices = client.getVoices();

// Get random voice
const voiceId = Object.values(voices)[Math.floor(Math.random() * Object.keys(voices).length)];
if (voiceId === undefined) throw new Error("no voice available");

const helloWorldUrl = client.generateWavUrl("Hallo aus Noud Zeh Geh: Ei Oh!", voiceId);
nodecg.sendMessage("setSrc", helloWorldUrl);
}

module.exports = function (nodecg: NodeCG.ServerAPI) {
nodecg.log.info("Sample bundle for the GameTTS service started.");

const gametts = requireService<GameTTSClient>(nodecg, "gametts");

nodecg.listenFor("ready", () => {
const client = gametts?.getClient();
if (client !== undefined) {
playTTSInGraphic(client, nodecg).catch((err) =>
nodecg.log.error(`Error while trying to play tts message: ${err.messages}`),
);
}
});

gametts?.onAvailable(async (client) => {
nodecg.log.info("GameTTS service available.");
await playTTSInGraphic(client, nodecg);
});

gametts?.onUnavailable(() => {
nodecg.log.info("GameTTS service unavailable.");
});
};
11 changes: 11 additions & 0 deletions samples/gametts/extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "nodecg-io-tsconfig",
"references": [
{
"path": "../../../nodecg-io-core"
},
{
"path": "../../../services/nodecg-io-gametts"
}
]
}
10 changes: 10 additions & 0 deletions samples/gametts/graphics/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>GameTTS Sample</title>
</head>
<body>
<audio controls id="gametts-audio"></audio>
<script type="module" src="index.js"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions samples/gametts/graphics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NodeCGAPIClient } from "@nodecg/types/client/api/api.client";

declare global {
const nodecg: NodeCGAPIClient;
const NodeCG: typeof NodeCGAPIClient;
}

// Listens for event from gametts sample and plays the audio by the provided url.

const audioElement = document.getElementById("gametts-audio") as HTMLAudioElement;

// Play audio when the graphic is newly opened
nodecg.sendMessage("ready");

nodecg.listenFor("setSrc", (newSrc) => {
audioElement.src = newSrc;
audioElement.currentTime = 0;
audioElement.play();
});
7 changes: 7 additions & 0 deletions samples/gametts/graphics/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "nodecg-io-tsconfig",
"compilerOptions": {
"lib": ["ES2015", "dom"],
"module": "ES2015"
}
}
27 changes: 27 additions & 0 deletions samples/gametts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "gametts",
"version": "0.3.0",
"private": true,
"nodecg": {
"compatibleRange": ">=1.1.1 <3.0.0",
"bundleDependencies": {
"nodecg-io-gametts": "^0.3.0"
},
"graphics": [
{
"file": "index.html",
"width": "1920",
"height": "1080"
}
]
},
"license": "MIT",
"dependencies": {
"@types/node": "^20.4.7",
"@nodecg/types": "^2.1.11",
"nodecg-io-core": "^0.3.0",
"nodecg-io-gametts": "^0.3.0",
"typescript": "^5.1.6",
"nodecg-io-tsconfig": "^1.0.0"
}
}
14 changes: 14 additions & 0 deletions samples/gametts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"files": [],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./extension"
},
{
"path": "./graphics"
}
]
}
11 changes: 11 additions & 0 deletions samples/opentts/extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "nodecg-io-tsconfig",
"references": [
{
"path": "../../../nodecg-io-core"
},
{
"path": "../../../services/nodecg-io-opentts"
}
]
}
2 changes: 1 addition & 1 deletion samples/opentts/graphics/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
</head>
<body>
<audio controls id="opentts-audio"></audio>
<script src="index.js"></script>
<script type="module" src="index.js"></script>
</body>
</html>
7 changes: 7 additions & 0 deletions samples/opentts/graphics/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "nodecg-io-tsconfig",
"compilerOptions": {
"lib": ["ES2015", "dom"],
"module": "ES2015"
}
}
2 changes: 1 addition & 1 deletion samples/opentts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"nodecg": {
"compatibleRange": ">=1.1.1 <3.0.0",
"bundleDependencies": {
"nodecg-io-template": "^0.3.0"
"nodecg-io-opentts": "^0.3.0"
},
"graphics": [
{
Expand Down
8 changes: 4 additions & 4 deletions samples/opentts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"extends": "nodecg-io-tsconfig",
"files": [],
"compilerOptions": {
"lib": ["dom"]
"composite": true
},
"references": [
{
"path": "../../nodecg-io-core"
"path": "./extension"
},
{
"path": "../../services/nodecg-io-opentts"
"path": "./graphics"
}
]
}
61 changes: 61 additions & 0 deletions services/nodecg-io-gametts/extension/gameTtsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { GameTTSConfig } from "./index";
import fetch, { Response } from "node-fetch";
import { emptySuccess, error, ObjectMap, Result } from "nodecg-io-core";
import { voiceMap } from "./voiceMap";

export class GameTTSClient {
constructor(private readonly config: GameTTSConfig) {}

private buildBaseURL(): string {
const protocol = this.config.useHttps ? "https" : "http";
return `${protocol}://${this.config.host}`;
}

private async executeRequest(path: string): Promise<Response> {
const response = await fetch(this.buildBaseURL() + path);
if (!response.ok) {
throw new Error(`Failed to execute gametts request: ${await response.text()}`);
}

return response;
}

/**
* Get all the ids of all voices that gametts supports with a voice name as the object key.
*/
getVoices(): ObjectMap<number> {
const voiceEntries = Object.entries(voiceMap).map(([key, value]) => [key, parseInt(value)]);
return Object.fromEntries(voiceEntries);
}

/**
* Generates a URL to a .wav file with the spoken text that is generated using GameTTS:
*/
generateWavUrl(text: string, voiceId: number): string {
const params = new URLSearchParams({ text, speaker_id: voiceId.toString() });
return `${this.buildBaseURL()}/?${params}`;
}

/**
* Downloads the .wav file from the given URL and returns it as a Buffer.
* @param url the url generated by {@link generateWavUrl}
* @returns the wav file
*/
async getWavData(url: string): Promise<ArrayBuffer> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch wav audio data: ${await response.text()}`);
}

return await response.arrayBuffer();
}

async isGameTTSAvailable(): Promise<Result<void>> {
try {
await this.executeRequest("");
return emptySuccess();
} catch (err) {
return error(`Failed to connect to gametts instance at ${this.buildBaseURL()}: ${err}`);
}
}
}
Loading