Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
e-adrien committed Dec 9, 2024
1 parent f007e60 commit a07fc00
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 24 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/quality-control.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,3 @@ jobs:

- name: TSC checks
run: npm run tsc-check

- name: Build JS
run: npm run build
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ node_modules/
!.yarn/sdks
!.yarn/versions

# Configuration & Tests folders
# Configuration folder
config
texts

# Test files
tests/*wav
tests/*m4a
tests/*weba
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extension": ["ts"],
"spec": "tests/**/*.spec.ts",
"require": ["ts-node/register", "tests/fixtures.ts"]
"require": ["ts-node/register"]
}
10 changes: 8 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
.vscode
.yarn
node_modules
tests
.editorconfig
eslint.config.mjs
.gitattributes
.gitignore
.mocharc.json
.prettierignore
.whitesource
.yarnrc.yml
eslint.config.mjs
prettier.config.mjs
tsconfig.build.json
tsconfig.json
.whitesource
update-voices.ts
5 changes: 5 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"name": "Tests",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/mocha",
"runtimeArgs": [],
"env": {
"TTS_GCP_CREDENTIALS": "${workspaceFolder}/config/tts-gcp-credentials.json",
"TTS_GCP_BUCKET": "tests-gcp-tts",
"DEBUG": "gcp-tts:*"
},
"outputCapture": "std",
"skipFiles": ["<node_internals>/**/*.js"]
}
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
],
"author": "",
"license": "MIT",
"repository": "lesjoursfr/gcp-tts",
"repository": {
"type": "git",
"url": "git+https://github.com/lesjoursfr/gcp-tts.git"
},
"homepage": "https://github.com/lesjoursfr/gcp-tts#readme",
"bugs": {
"url": "https://github.com/lesjoursfr/gcp-tts/issues"
Expand Down
58 changes: 55 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,62 @@ Generate audio files from text file using GCP.
## Presentation

This module convert text to sound using the Google Cloud Text-to-Speech service.
The generated audio is a WAVE file but you can ask for extra WEBA and/or M4A files (they will be generated using ffmpeg).

```javascript
import gcpTTS from "@lesjoursfr/gcp-tts";
import { Languages, synthesizeTextWithGCP, Voices } from "@lesjoursfr/gcp-tts";

let result = synthesizeTextWithGCP("Lorem ipsum...");
let result = await synthesizeTextWithGCP(
"Alice, assise auprès de sa sœur sur le gazon, …",
{
projectId: "gcp-project-id",
clientOptions: { credentials: credentials },
bucketId: "gcp-bucket-id",
},
{
language: Languages.fr_FR,
voice: Voices.fr_FR_Neural2_A,
audioEncoding: "LINEAR16",
},
{ folder: "/an/absolute/path", filename: "filename-without-extension" }
);
```

You can also convert the generated file to WEBA and/or M4A.
**You need to have ffmpeg installed on your system to do that.**

```javascript
import {
Codecs,
Languages,
synthesizeTextWithGCP,
Voices,
} from "@lesjoursfr/gcp-tts";

let result = await synthesizeTextWithGCP(
"Alice, assise auprès de sa sœur sur le gazon, …",
{
projectId: "gcp-project-id",
clientOptions: { credentials: credentials },
bucketId: "gcp-bucket-id",
},
{
language: Languages.fr_FR,
voice: Voices.fr_FR_Neural2_A,
audioEncoding: "LINEAR16",
},
{ folder: "/an/absolute/path", filename: "filename-without-extension" },
[
{ codec: Codecs.weba, options: { audioBitrate: 256 } },
{ codec: Codecs.m4a, options: { audioBitrate: 256 } },
]
);
```

The return object is:

```typescript
type SynthesizeResult = {
sourceFile: string; // File path of the original generated file
extraEncodes: Array<string>; // Array of file paths converted by ffmpeg
};
```
1 change: 1 addition & 0 deletions src/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export function encode(file: string, codec: Codecs, options: EncoderOptions): Pr
log(`${EOL}Conversion complete : ${pc.gray(destination)}`);
resolve(destination);
})
.renice(20)
.save(destination);
});
}
20 changes: 12 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,32 @@ export { Codecs, EncoderOptions } from "./encoder";
export { GCPConfig, SynthesizeDestination, SynthesizeOptions } from "./synthesizer";
export * from "./voices";

export type SynthesizeResult = {
sourceFile: string;
extraEncodes: Array<string>;
};

export async function synthesizeTextWithGCP(
textToRead: string,
gcpConfig: GCPConfig,
options: SynthesizeOptions,
destination: SynthesizeDestination,
extraEncodes: Array<{ codec: Codecs; options: EncoderOptions }>
): Promise<{
sourceFile: string;
exextraEncodes: Array<string>;
}> {
extraEncodes?: Array<{ codec: Codecs; options: EncoderOptions }>
): Promise<SynthesizeResult> {
// Create the source version on the audio file
const sourceFilePath = await synthesize(textToRead, gcpConfig, options, destination);

// Generate extra version with different codecs
const extras = [] as Array<string>;
for (const { codec, options } of extraEncodes) {
extras.push(await encode(sourceFilePath, codec, options));
if (extraEncodes !== undefined) {
for (const { codec, options } of extraEncodes) {
extras.push(await encode(sourceFilePath, codec, options));
}
}

// Return the result
return {
sourceFile: sourceFilePath,
exextraEncodes: extras,
extraEncodes: extras,
};
}
6 changes: 3 additions & 3 deletions src/synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ export async function synthesize(
log(`Use TextToSpeechClient with the destination ${destFilePath}`);
const [response] = await client.synthesizeSpeech(request);
if (response.audioContent !== null && response.audioContent !== undefined) {
console.log("Speech synthesis finished.");
log("Speech synthesis finished.");
await writeFile(destFilePath, response.audioContent);
} else {
console.log("Speech synthesis failed.");
log("Speech synthesis failed.");
}
} else {
// The limit is 1M bytes
Expand Down Expand Up @@ -100,6 +100,6 @@ export async function synthesize(
// Return the destination file
return destFilePath;
} catch (err) {
throw new Error(`Can't synthesize the gievn text!`, { cause: err });
throw new Error(`Can't synthesize the given text!`, { cause: err });
}
}
25 changes: 25 additions & 0 deletions tests/alice-in-wonderland-long.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Alice, assise auprès de sa sœur sur le gazon, commençait à s’ennuyer de rester là à ne rien faire ; une ou deux fois elle avait jeté les yeux sur le livre que lisait sa sœur ; mais quoi ! pas d’images, pas de dialogues ! « La belle avance, » pensait Alice, « qu’un livre sans images, sans causeries ! »

Elle s’était mise à réfléchir, (tant bien que mal, car la chaleur du jour l’endormait et la rendait lourde,) se demandant si le plaisir de faire une couronne de marguerites valait bien la peine de se lever et de cueillir les fleurs, quand tout à coup un lapin blanc aux yeux roses passa près d’elle.

Il n’y avait rien là de bien étonnant, et Alice ne trouva même pas très-extraordinaire d’entendre parler le Lapin qui se disait : « Ah ! j’arriverai trop tard ! » (En y songeant après, il lui sembla bien qu’elle aurait dû s’en étonner, mais sur le moment cela lui avait paru tout naturel.) Cependant, quand le Lapin vint à tirer une montre de son gousset, la regarda, puis se prit à courir de plus belle, Alice sauta sur ses pieds, frappée de cette idée que jamais elle n’avait vu de lapin avec un gousset et une montre. Entraînée par la curiosité elle s’élança sur ses traces à travers le champ, et arriva tout juste à temps pour le voir disparaître dans un large trou au pied d’une haie.

Un instant après, Alice était à la poursuite du Lapin dans le terrier, sans songer comment elle en sortirait.

Pendant un bout de chemin le trou allait tout droit comme un tunnel, puis tout à coup il plongeait perpendiculairement d’une façon si brusque qu’Alice se sentit tomber comme dans un puits d’une grande profondeur, avant même d’avoir pensé à se retenir.

De deux choses l’une, ou le puits était vraiment bien profond, ou elle tombait bien doucement ; car elle eut tout le loisir, dans sa chute, de regarder autour d’elle et de se demander avec étonnement ce qu’elle allait devenir. D’abord elle regarda dans le fond du trou pour savoir où elle allait ; mais il y faisait bien trop sombre pour y rien voir. Ensuite elle porta les yeux sur les parois du puits, et s’aperçut qu’elles étaient garnies d’armoires et d’étagères ; çà et là, elle vit pendues à des clous des cartes géographiques et des images. En passant elle prit sur un rayon un pot de confiture portant cette étiquette, « MARMELADE D’ORANGES. » Mais, à son grand regret, le pot était vide : elle n’osait le laisser tomber dans la crainte de tuer quelqu’un ; aussi s’arrangea-t-elle de manière à le déposer en passant dans une des armoires.

« Certes, » dit Alice, « après une chute pareille je ne me moquerai pas mal de dégringoler l’escalier ! Comme ils vont me trouver brave chez nous ! Je tomberais du haut des toits que je ne ferais pas entendre une plainte. » (Ce qui était bien probable.)

Tombe, tombe, tombe ! « Cette chute n’en finira donc pas ! Je suis curieuse de savoir combien de milles j’ai déjà faits, » dit-elle tout haut. « Je dois être bien près du centre de la terre. Voyons donc, cela serait à quatre mille milles de profondeur, il me semble. » (Comme vous voyez, Alice avait appris pas mal de choses dans ses leçons ; et bien que ce ne fût pas là une très-bonne occasion de faire parade de son savoir, vu qu’il n’y avait point d’auditeur, cependant c’était un bon exercice que de répéter sa leçon.) « Oui, c’est bien à peu près cela ; mais alors à quel degré de latitude ou de longitude est-ce que je me trouve ? » (Alice n’avait pas la moindre idée de ce que voulait dire latitude ou longitude, mais ces grands mots lui paraissaient beaux et sonores.)

Bientôt elle reprit : « Si j’allais traverser complétement la terre ? Comme ça serait drôle de se trouver au milieu de gens qui marchent la tête en bas. Aux Antipathies, je crois. » (Elle n’était pas fâchée cette fois qu’il n’y eût personne là pour l’entendre, car ce mot ne lui faisait pas l’effet d’être bien juste.) « Eh mais, j’aurai à leur demander le nom du pays. — Pardon, Madame, est-ce ici la Nouvelle-Zemble ou l’Australie ? » — En même temps elle essaya de faire la révérence. (Quelle idée ! Faire la révérence en l’air ! Dites-moi un peu, comment vous y prendriez-vous ?) « Quelle petite ignorante ! pensera la dame quand je lui ferai cette question. Non, il ne faut pas demander cela ; peut-être le verrai-je écrit quelque part. »

Tombe, tombe, tombe ! — Donc Alice, faute d’avoir rien de mieux à faire, se remit à se parler : « Dinah remarquera mon absence ce soir, bien sûr. » (Dinah c’était son chat.) « Pourvu qu’on n’oublie pas de lui donner sa jatte de lait à l’heure du thé. Dinah, ma minette, que n’es-tu ici avec moi ? Il n’y a pas de souris dans les airs, j’en ai bien peur ; mais tu pourrais attraper une chauve-souris, et cela ressemble beaucoup à une souris, tu sais. Mais les chats mangent-ils les chauves-souris ? » Ici le sommeil commença à gagner Alice. Elle répétait, à moitié endormie : « Les chats mangent-ils les chauves-souris ? Les chats mangent-ils les chauves-souris ? » Et quelquefois : « Les chauves-souris mangent-elles les chats ? » Car vous comprenez bien que, puisqu’elle ne pouvait répondre ni à l’une ni à l’autre de ces questions, peu importait la manière de les poser. Elle s’assoupissait et commençait à rêver qu’elle se promenait tenant Dinah par la main, lui disant très-sérieusement : « Voyons, Dinah, dis-moi la vérité, as-tu jamais mangé des chauves-souris ? » Quand tout à coup, pouf ! la voilà étendue sur un tas de fagots et de feuilles sèches, — et elle a fini de tomber.

Alice ne s’était pas fait le moindre mal. Vite elle se remet sur ses pieds et regarde en l’air ; mais tout est noir là-haut. Elle voit devant elle un long passage et le Lapin Blanc qui court à toutes jambes. Il n’y a pas un instant à perdre ; Alice part comme le vent et arrive tout juste à temps pour entendre le Lapin dire, tandis qu’il tourne le coin : « Par ma moustache et mes oreilles, comme il se fait tard ! » Elle n’en était plus qu’à deux pas : mais le coin tourné, le Lapin avait disparu. Elle se trouva alors dans une salle longue et basse, éclairée par une rangée de lampes pendues au plafond.

Il y avait des portes tout autour de la salle : ces portes étaient toutes fermées, et, après avoir vainement tenté d’ouvrir celles du côté droit, puis celles du côté gauche, Alice se promena tristement au beau milieu de cette salle, se demandant comment elle en sortirait.

Tout à coup elle rencontra sur son passage une petite table à trois pieds, en verre massif, et rien dessus qu’une toute petite clef d’or. Alice pensa aussitôt que ce pouvait être celle d’une des portes ; mais hélas ! soit que les serrures fussent trop grandes, soit que la clef fût trop petite, elle ne put toujours en ouvrir aucune. Cependant, ayant fait un second tour, elle aperçut un rideau placé très-bas et qu’elle n’avait pas vu d’abord ; par derrière se trouvait encore une petite porte à peu près quinze pouces de haut ; elle essaya la petite clef d’or à la serrure, et, à sa grande joie, il se trouva qu’elle y allait à merveille. Alice ouvrit la porte, et vit qu’elle conduisait dans un étroit passage à peine plus large qu’un trou à rat. Elle s’agenouilla, et, jetant les yeux le long du passage, découvrit le plus ravissant jardin du monde. Oh ! Qu’il lui tardait de sortir de cette salle ténébreuse et d’errer au milieu de ces carrés de fleurs brillantes, de ces fraîches fontaines ! Mais sa tête ne pouvait même pas passer par la porte. « Et quand même ma tête y passerait, » pensait Alice, « à quoi cela servirait-il sans mes épaules ? Oh ! que je voudrais donc avoir la faculté de me fermer comme un télescope ! Ça se pourrait peut-être, si je savais comment m’y prendre. » Il lui était déjà arrivé tant de choses extraordinaires, qu’Alice commençait à croire qu’il n’y en avait guère d’impossibles.
5 changes: 5 additions & 0 deletions tests/alice-in-wonderland-short.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Alice, assise auprès de sa sœur sur le gazon, commençait à s’ennuyer de rester là à ne rien faire ; une ou deux fois elle avait jeté les yeux sur le livre que lisait sa sœur ; mais quoi ! pas d’images, pas de dialogues ! « La belle avance, » pensait Alice, « qu’un livre sans images, sans causeries ! »

Elle s’était mise à réfléchir, (tant bien que mal, car la chaleur du jour l’endormait et la rendait lourde,) se demandant si le plaisir de faire une couronne de marguerites valait bien la peine de se lever et de cueillir les fleurs, quand tout à coup un lapin blanc aux yeux roses passa près d’elle.

Il n’y avait rien là de bien étonnant, et Alice ne trouva même pas très-extraordinaire d’entendre parler le Lapin qui se disait : « Ah ! j’arriverai trop tard ! » (En y songeant après, il lui sembla bien qu’elle aurait dû s’en étonner, mais sur le moment cela lui avait paru tout naturel.) Cependant, quand le Lapin vint à tirer une montre de son gousset, la regarda, puis se prit à courir de plus belle, Alice sauta sur ses pieds, frappée de cette idée que jamais elle n’avait vu de lapin avec un gousset et une montre. Entraînée par la curiosité elle s’élança sur ses traces à travers le champ, et arriva tout juste à temps pour le voir disparaître dans un large trou au pied d’une haie.
61 changes: 61 additions & 0 deletions tests/gcp-tts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import assert from "assert";
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { Codecs, Languages, synthesizeTextWithGCP, Voices } from "../src/index";

type GcpCredentials = {
type: string;
project_id: string;
private_key_id: string;
private_key: string;
client_email: string;
client_id: string;
auth_uri: string;
token_uri: string;
auth_provider_x509_cert_url: string;
client_x509_cert_url: string;
universe_domain: string;
};

const { TTS_GCP_CREDENTIALS, TTS_GCP_BUCKET } = process.env;
if (typeof TTS_GCP_CREDENTIALS !== "string" || typeof TTS_GCP_BUCKET !== "string") {
throw new Error("Missing TTS_GCP_CREDENTIALS or TTS_GCP_BUCKET environment variable!");
}
if (!existsSync(TTS_GCP_CREDENTIALS)) {
throw new Error(`${TTS_GCP_CREDENTIALS} doesn't exist!`);
}
const credentials = Object.freeze(
JSON.parse(readFileSync(TTS_GCP_CREDENTIALS, { encoding: "utf-8" })) as GcpCredentials
);

it("Generate WAVE, WEBA & M4A for a small text", async () => {
const text = readFileSync(resolve(__dirname, "alice-in-wonderland-short.txt"), { encoding: "utf8" });
const results = await synthesizeTextWithGCP(
text,
{ projectId: credentials.project_id, clientOptions: { credentials: credentials }, bucketId: TTS_GCP_BUCKET },
{ language: Languages.fr_FR, voice: Voices.fr_FR_Neural2_A, audioEncoding: "LINEAR16" },
{ folder: resolve(__dirname), filename: "alice-in-wonderland-short" },
[
{ codec: Codecs.weba, options: { audioBitrate: 256 } },
{ codec: Codecs.m4a, options: { audioBitrate: 256 } },
]
);

assert.strictEqual(existsSync(results.sourceFile), true);
assert.strictEqual(results.extraEncodes.length, 2);
assert.strictEqual(existsSync(results.extraEncodes[0]), true);
assert.strictEqual(existsSync(results.extraEncodes[1]), true);
}).timeout(60000);

it("Generate WAVE for a long text", async () => {
const text = readFileSync(resolve(__dirname, "alice-in-wonderland-long.txt"), { encoding: "utf8" });
const results = await synthesizeTextWithGCP(
text,
{ projectId: credentials.project_id, clientOptions: { credentials: credentials }, bucketId: TTS_GCP_BUCKET },
{ language: Languages.fr_FR, voice: Voices.fr_FR_Neural2_A, audioEncoding: "LINEAR16" },
{ folder: resolve(__dirname), filename: "alice-in-wonderland-long" }
);

assert.strictEqual(existsSync(results.sourceFile), true);
assert.strictEqual(results.extraEncodes.length, 0);
}).timeout(60000);
2 changes: 1 addition & 1 deletion update-voices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
// Check if we have the required environment variables
const { TTS_GCP_CREDENTIALS } = process.env;
if (typeof TTS_GCP_CREDENTIALS !== "string") {
throw new Error("Missing GOOGLE_APPLICATION_CREDENTIALS or GOOGLE_APPLICATION_PROJECT_ID environment variable!");
throw new Error("Missing TTS_GCP_CREDENTIALS environment variable!");
}
if (!existsSync(TTS_GCP_CREDENTIALS)) {
throw new Error(`${TTS_GCP_CREDENTIALS} doesn't exist!`);
Expand Down

0 comments on commit a07fc00

Please sign in to comment.