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

Commit 5c8b136

Browse files
authored
Merge pull request #991 from SteffoSpieler/feat/add-gametts-service
feat(gametts): Add gametts service
2 parents 438172d + 70d0a46 commit 5c8b136

File tree

21 files changed

+515
-9
lines changed

21 files changed

+515
-9
lines changed

package-lock.json

Lines changed: 56 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/gametts/extension/index.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import NodeCG from "@nodecg/types";
2+
import { requireService } from "nodecg-io-core";
3+
import { GameTTSClient } from "nodecg-io-gametts";
4+
5+
/**
6+
* Plays a hello world tts message inside the graphic of this sample bundle.
7+
*/
8+
async function playTTSInGraphic(client: GameTTSClient, nodecg: NodeCG.ServerAPI) {
9+
const voices = client.getVoices();
10+
11+
// Get random voice
12+
const voiceId = Object.values(voices)[Math.floor(Math.random() * Object.keys(voices).length)];
13+
if (voiceId === undefined) throw new Error("no voice available");
14+
15+
const helloWorldUrl = client.generateWavUrl("Hallo aus Noud Zeh Geh: Ei Oh!", voiceId);
16+
nodecg.sendMessage("setSrc", helloWorldUrl);
17+
}
18+
19+
module.exports = function (nodecg: NodeCG.ServerAPI) {
20+
nodecg.log.info("Sample bundle for the GameTTS service started.");
21+
22+
const gametts = requireService<GameTTSClient>(nodecg, "gametts");
23+
24+
nodecg.listenFor("ready", () => {
25+
const client = gametts?.getClient();
26+
if (client !== undefined) {
27+
playTTSInGraphic(client, nodecg).catch((err) =>
28+
nodecg.log.error(`Error while trying to play tts message: ${err.messages}`),
29+
);
30+
}
31+
});
32+
33+
gametts?.onAvailable(async (client) => {
34+
nodecg.log.info("GameTTS service available.");
35+
await playTTSInGraphic(client, nodecg);
36+
});
37+
38+
gametts?.onUnavailable(() => {
39+
nodecg.log.info("GameTTS service unavailable.");
40+
});
41+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "nodecg-io-tsconfig",
3+
"references": [
4+
{
5+
"path": "../../../nodecg-io-core"
6+
},
7+
{
8+
"path": "../../../services/nodecg-io-gametts"
9+
}
10+
]
11+
}

samples/gametts/graphics/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>GameTTS Sample</title>
5+
</head>
6+
<body>
7+
<audio controls id="gametts-audio"></audio>
8+
<script type="module" src="index.js"></script>
9+
</body>
10+
</html>

samples/gametts/graphics/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NodeCGAPIClient } from "@nodecg/types/client/api/api.client";
2+
3+
declare global {
4+
const nodecg: NodeCGAPIClient;
5+
const NodeCG: typeof NodeCGAPIClient;
6+
}
7+
8+
// Listens for event from gametts sample and plays the audio by the provided url.
9+
10+
const audioElement = document.getElementById("gametts-audio") as HTMLAudioElement;
11+
12+
// Play audio when the graphic is newly opened
13+
nodecg.sendMessage("ready");
14+
15+
nodecg.listenFor("setSrc", (newSrc) => {
16+
audioElement.src = newSrc;
17+
audioElement.currentTime = 0;
18+
audioElement.play();
19+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "nodecg-io-tsconfig",
3+
"compilerOptions": {
4+
"lib": ["ES2015", "dom"],
5+
"module": "ES2015"
6+
}
7+
}

samples/gametts/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "gametts",
3+
"version": "0.3.0",
4+
"private": true,
5+
"nodecg": {
6+
"compatibleRange": ">=1.1.1 <3.0.0",
7+
"bundleDependencies": {
8+
"nodecg-io-gametts": "^0.3.0"
9+
},
10+
"graphics": [
11+
{
12+
"file": "index.html",
13+
"width": "1920",
14+
"height": "1080"
15+
}
16+
]
17+
},
18+
"license": "MIT",
19+
"dependencies": {
20+
"@types/node": "^20.4.7",
21+
"@nodecg/types": "^2.1.11",
22+
"nodecg-io-core": "^0.3.0",
23+
"nodecg-io-gametts": "^0.3.0",
24+
"typescript": "^5.1.6",
25+
"nodecg-io-tsconfig": "^1.0.0"
26+
}
27+
}

samples/gametts/tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"files": [],
3+
"compilerOptions": {
4+
"composite": true
5+
},
6+
"references": [
7+
{
8+
"path": "./extension"
9+
},
10+
{
11+
"path": "./graphics"
12+
}
13+
]
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "nodecg-io-tsconfig",
3+
"references": [
4+
{
5+
"path": "../../../nodecg-io-core"
6+
},
7+
{
8+
"path": "../../../services/nodecg-io-opentts"
9+
}
10+
]
11+
}

samples/opentts/graphics/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
</head>
66
<body>
77
<audio controls id="opentts-audio"></audio>
8-
<script src="index.js"></script>
8+
<script type="module" src="index.js"></script>
99
</body>
1010
</html>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "nodecg-io-tsconfig",
3+
"compilerOptions": {
4+
"lib": ["ES2015", "dom"],
5+
"module": "ES2015"
6+
}
7+
}

samples/opentts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"nodecg": {
66
"compatibleRange": ">=1.1.1 <3.0.0",
77
"bundleDependencies": {
8-
"nodecg-io-template": "^0.3.0"
8+
"nodecg-io-opentts": "^0.3.0"
99
},
1010
"graphics": [
1111
{

samples/opentts/tsconfig.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
2-
"extends": "nodecg-io-tsconfig",
2+
"files": [],
33
"compilerOptions": {
4-
"lib": ["dom"]
4+
"composite": true
55
},
66
"references": [
77
{
8-
"path": "../../nodecg-io-core"
8+
"path": "./extension"
99
},
1010
{
11-
"path": "../../services/nodecg-io-opentts"
11+
"path": "./graphics"
1212
}
1313
]
1414
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { GameTTSConfig } from "./index";
2+
import fetch, { Response } from "node-fetch";
3+
import { emptySuccess, error, ObjectMap, Result } from "nodecg-io-core";
4+
import { voiceMap } from "./voiceMap";
5+
6+
export class GameTTSClient {
7+
constructor(private readonly config: GameTTSConfig) {}
8+
9+
private buildBaseURL(): string {
10+
const protocol = this.config.useHttps ? "https" : "http";
11+
return `${protocol}://${this.config.host}`;
12+
}
13+
14+
private async executeRequest(path: string): Promise<Response> {
15+
const response = await fetch(this.buildBaseURL() + path);
16+
if (!response.ok) {
17+
throw new Error(`Failed to execute gametts request: ${await response.text()}`);
18+
}
19+
20+
return response;
21+
}
22+
23+
/**
24+
* Get all the ids of all voices that gametts supports with a voice name as the object key.
25+
*/
26+
getVoices(): ObjectMap<number> {
27+
const voiceEntries = Object.entries(voiceMap).map(([key, value]) => [key, parseInt(value)]);
28+
return Object.fromEntries(voiceEntries);
29+
}
30+
31+
/**
32+
* Generates a URL to a .wav file with the spoken text that is generated using GameTTS:
33+
*/
34+
generateWavUrl(text: string, voiceId: number): string {
35+
const params = new URLSearchParams({ text, speaker_id: voiceId.toString() });
36+
return `${this.buildBaseURL()}/?${params}`;
37+
}
38+
39+
/**
40+
* Downloads the .wav file from the given URL and returns it as a Buffer.
41+
* @param url the url generated by {@link generateWavUrl}
42+
* @returns the wav file
43+
*/
44+
async getWavData(url: string): Promise<ArrayBuffer> {
45+
const response = await fetch(url);
46+
if (!response.ok) {
47+
throw new Error(`Failed to fetch wav audio data: ${await response.text()}`);
48+
}
49+
50+
return await response.arrayBuffer();
51+
}
52+
53+
async isGameTTSAvailable(): Promise<Result<void>> {
54+
try {
55+
await this.executeRequest("");
56+
return emptySuccess();
57+
} catch (err) {
58+
return error(`Failed to connect to gametts instance at ${this.buildBaseURL()}: ${err}`);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)