Skip to content

Commit d0f0eaf

Browse files
committed
feat: create initial library
0 parents  commit d0f0eaf

File tree

7 files changed

+193
-0
lines changed

7 files changed

+193
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.vscode
2+
*.wav
3+
.DS_Store

README.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# deno-gtts
2+
3+
A simple text-to-speech library using the google translate api. It is designed
4+
to be used either as a deno library or as a cli tool.
5+
6+
## Usage
7+
8+
```typescript
9+
import { save } from "https://deno.land/x/gtts/mod.ts";
10+
11+
await save("./demo.wav", "This sentence is being read by a machine");
12+
```
13+
14+
OR
15+
16+
```bash
17+
deno install --allow-write --allow-net -n gtts https://deno.land/x/gtts/cli.ts
18+
gtts "some text to speak"
19+
gtts "some text to speak to a destination" --path="./test.wav"
20+
gtts "text but in a french accent" --lang=fr
21+
gtts "text at a destination but in a french accent" --path="./french.wav" --lang=fr
22+
```
23+
24+
Very loosely inspired by
25+
[this nightmare of a library](https://github.com/lino-levan/better-node-gtts)

cli.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { parse } from "./deps.ts";
2+
import { save } from "./mod.ts";
3+
import { LANGUAGES } from "./src/languages.ts";
4+
5+
const args = parse(Deno.args, {
6+
string: ["lang", "path"],
7+
alias: {
8+
"L": "lang",
9+
"P": "path",
10+
},
11+
});
12+
13+
const text = args._.join(" ");
14+
const language = args.lang as keyof typeof LANGUAGES ?? "en-us";
15+
const path = args.path ??
16+
"./" + text.slice(0, 10).replace(/([^a-z0-9]+)/gi, "-") + ".wav";
17+
18+
if (!Object.hasOwn(LANGUAGES, language)) {
19+
throw `Invalid language: ${language}`;
20+
}
21+
22+
await save(path, text, {
23+
language,
24+
});

deps.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { writeAll } from "https://deno.land/[email protected]/streams/write_all.ts";
2+
export { assert } from "https://deno.land/[email protected]/testing/asserts.ts";
3+
export { parse } from "https://deno.land/[email protected]/flags/mod.ts";

mod.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { writeAll } from "./deps.ts";
2+
import { LANGUAGES } from "./src/languages.ts";
3+
4+
const GOOGLE_TTS_URL = "http://translate.google.com/translate_tts";
5+
const headers = {
6+
"User-Agent":
7+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.26.17 (KHTML like Gecko) Version/6.0.2 Safari/536.26.17",
8+
};
9+
10+
function tokenize(text: string) {
11+
return text.split(/¡|!|\(|\)|\[|\]|\¿|\?|\.|\,|\;|\:|\—|||\n/).filter(
12+
(p) => p,
13+
);
14+
}
15+
16+
export interface SaveOptions {
17+
language: keyof typeof LANGUAGES;
18+
}
19+
20+
export async function save(
21+
path: string,
22+
text: string,
23+
options?: Partial<SaveOptions>,
24+
) {
25+
const config: SaveOptions = {
26+
...{
27+
language: "en-us",
28+
},
29+
...options,
30+
};
31+
const textParts = tokenize(text);
32+
33+
try {
34+
await Deno.remove(path);
35+
} catch {
36+
// swallow error
37+
}
38+
39+
const file = await Deno.open(path, {
40+
create: true,
41+
append: true,
42+
write: true,
43+
});
44+
45+
for (const [i, part] of Object.entries(textParts)) {
46+
const encodedText = encodeURIComponent(part);
47+
const args =
48+
`?ie=UTF-8&tl=${config.language}&q=${encodedText}&total=${textParts.length}&idx=${i}&client=tw-ob&textlen=${encodedText.length}`;
49+
const url = GOOGLE_TTS_URL + args;
50+
51+
const req = await fetch(url, {
52+
headers,
53+
});
54+
const buffer = await req.arrayBuffer();
55+
const data = new Uint8Array(buffer);
56+
57+
await writeAll(file, data);
58+
}
59+
60+
file.close();
61+
}

mod_test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { assert } from "./deps.ts";
2+
import { save } from "./mod.ts";
3+
4+
async function exists(path: string) {
5+
try {
6+
await Deno.lstat(path);
7+
return true;
8+
} catch {
9+
return false;
10+
}
11+
}
12+
13+
Deno.test("saving short text works", async () => {
14+
await save("./test.wav", "Testing a basic string");
15+
assert(await exists("./test.wav"));
16+
});
17+
18+
Deno.test("saving long text also works", async () => {
19+
await save(
20+
"./long.wav",
21+
"Testing a very long string. One with multiple periods and, maybe even some commas. The grammar is completely off, but that is part of the fun of the string experience. They will never know how this is set up. No one will read the tests for this module. I hope not at least.",
22+
);
23+
assert(await exists("./long.wav"));
24+
});

src/languages.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
export const LANGUAGES = {
2+
af: "Afrikaans",
3+
sq: "Albanian",
4+
ar: "Arabic",
5+
hy: "Armenian",
6+
ca: "Catalan",
7+
zh: "Chinese",
8+
"zh-cn": "Chinese (Mandarin/China)",
9+
"zh-tw": "Chinese (Mandarin/Taiwan)",
10+
"zh-yue": "Chinese (Cantonese)",
11+
hr: "Croatian",
12+
cs: "Czech",
13+
da: "Danish",
14+
nl: "Dutch",
15+
en: "English",
16+
"en-au": "English (Australia)",
17+
"en-uk": "English (United Kingdom)",
18+
"en-us": "English (United States)",
19+
eo: "Esperanto",
20+
fi: "Finnish",
21+
fr: "French",
22+
de: "German",
23+
el: "Greek",
24+
ht: "Haitian Creole",
25+
hi: "Hindi",
26+
hu: "Hungarian",
27+
is: "Icelandic",
28+
id: "Indonesian",
29+
it: "Italian",
30+
ja: "Japanese",
31+
ko: "Korean",
32+
la: "Latin",
33+
lv: "Latvian",
34+
mk: "Macedonian",
35+
no: "Norwegian",
36+
pl: "Polish",
37+
pt: "Portuguese",
38+
"pt-br": "Portuguese (Brazil)",
39+
ro: "Romanian",
40+
ru: "Russian",
41+
sr: "Serbian",
42+
sk: "Slovak",
43+
es: "Spanish",
44+
"es-es": "Spanish (Spain)",
45+
"es-us": "Spanish (United States)",
46+
sw: "Swahili",
47+
sv: "Swedish",
48+
ta: "Tamil",
49+
th: "Thai",
50+
tr: "Turkish",
51+
vi: "Vietnamese",
52+
cy: "Welsh",
53+
};

0 commit comments

Comments
 (0)