Skip to content

Commit

Permalink
Merge branch 'openAI-TTS'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jérémy Soulary committed Feb 17, 2024
2 parents 6756895 + 3ef4bfe commit 6edc01b
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 93 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### v0.3.0 / 2024.02.27

- feat: add openAI TTS
- refactor: use Dax, update dependencies and to deno 1.40.5, clean

### v0.2.10 / 2023.12.27

- use deno 1.39.1
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,13 @@ Options:
-m, --skip-extract-image-from-mp3 skip extract item image from story mp3 [boolean] [default: false]
-i, --skip-image-item-gen skip image item generation [boolean] [default: false]
-s, --skip-not-rss skip all except download RSS files [boolean] [default: false]
--skip-rss-image-dl skip RSS image download of items [boolean] [default: false]
-r, --skip-rss-image-dl skip RSS image download of items [boolean] [default: false]
-w, --skip-wsl disable WSL usage [boolean] [default: false]
-z, --skip-zip-generation only process item generation, don't create zip [boolean] [default: false]
-e, --use-open-ai-tts generate missing audio item with Open AI TTS [boolean] [default: false]
-f, --open-ai-api-key the OpenAI API key [string]
-g, --open-ai-model OpenAi model : tts-1, tts-1-hd [string] [default: "tts-1"]
-h, --open-ai-voice OpenAi voice : alloy, echo, fable, onyx, nova, shimmer [string] [default: "onyx"]
```

Separate options by spaces, ex :
Expand Down Expand Up @@ -269,6 +273,14 @@ All key/value are optional, ex:
}
```

## OpenAI TTS

To use OpenAI TTS, use `--use-open-ai-tts` option, and you must set the API key:

- set OPENAI_API_KEY in the environnement variables
- or use --open-ai-api-key parameter
- or enter the key when the program prompt

## Development

Some dev command are listed in the deno.json file :
Expand Down
44 changes: 44 additions & 0 deletions deno.lock

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

2 changes: 2 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ export {
} from "https://deno.land/x/[email protected]/index.js";

export { default as $ } from "https://deno.land/x/[email protected]/mod.ts";

export { default as OpenAI } from "https://deno.land/x/[email protected]/mod.ts";
11 changes: 7 additions & 4 deletions gen_pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "./utils/utils.ts";
import { getLang, initI18n } from "./utils/i18n.ts";
import { convertImageOfFolder } from "./utils/convert_image.ts";
import { OPEN_AI_MODELS, OPEN_AI_VOICES } from "./generate/openai_tts.ts";

export type ModOptions = {
storyPath: string;
Expand All @@ -36,6 +37,10 @@ export type ModOptions = {
skipWsl?: boolean;
skipRssImageDl?: boolean;
outputFolder?: string;
useOpenAiTts?: boolean;
openAiApiKey?: string;
openAiModel?: typeof OPEN_AI_MODELS[number];
openAiVoice?: typeof OPEN_AI_VOICES[number];
};

async function genThumbnail(folder: Folder, storyPath: string) {
Expand Down Expand Up @@ -69,13 +74,11 @@ export async function generatePack(opt: ModOptions) {
}
if (!opt.skipImageItemGen || !opt.skipAudioItemGen) {
await genMissingItems(
opt.storyPath,
folder,
!opt.skipImageItemGen,
!opt.skipAudioItemGen,
lang,
true,
!!opt.skipWsl,
opt.storyPath,
opt,
);
folder = await fsToFolder(opt.storyPath, false);
}
Expand Down
78 changes: 78 additions & 0 deletions generate/basic_tts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ModOptions } from "../gen_pack.ts";
import { bgBlue } from "https://deno.land/[email protected]/fmt/colors.ts";
import { $ } from "../deps.ts";
import { convertPath } from "../utils/utils.ts";
import {
checkCommand,
getPico2waveCommand,
} from "../utils/external_commands.ts";

let hasPico2waveWslCache: undefined | boolean;

export async function hasPico2waveWsl() {
if (hasPico2waveWslCache === undefined) {
hasPico2waveWslCache = await checkCommand(
["wsl", "pico2wave", "--version"],
1,
);
}
return hasPico2waveWslCache;
}

let hasPico2waveCache: undefined | boolean;

export async function hasPico2wave() {
if (hasPico2waveCache === undefined) {
hasPico2waveCache = await checkCommand(["pico2wave", "--version"], 1);
}
return hasPico2waveCache;
}

export async function generate_audio_basic_tts(
title: string,
outputPath: string,
lang: string,
opt: ModOptions,
) {
console.log(bgBlue(`Generate basic TTS to ${outputPath}`));

if (
Deno.build.os === "windows" && (opt.skipWsl || !(await hasPico2waveWsl()))
) {
const audioFormat = "[System.Speech.AudioFormat.SpeechAudioFormatInfo]::" +
"new(8000,[System.Speech.AudioFormat.AudioBitsPerSample]" +
"::Sixteen,[System.Speech.AudioFormat.AudioChannel]::Mono)";

const args = [
"-Command",
`Add-Type -AssemblyName System.Speech; ` +
`$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; ` +
`$speak.SetOutputToWaveFile("${outputPath}",${audioFormat}); ` +
`$speak.Speak(" . ${title.replace(/["' ]/g, " ")} . "); ` +
`$speak.Dispose();`,
];
await $`PowerShell ${args}`.noThrow();
} else if (Deno.build.os === "darwin" && !(await hasPico2wave())) {
const args = [
"-o",
convertPath(outputPath),
"--file-format",
"WAVE",
"--data-format",
"LEF32@22050",
];
await $`say ${args}`.noThrow();
} else {
const pico2waveCommand = await getPico2waveCommand();
const cmd = [
pico2waveCommand[0],
...(pico2waveCommand.splice(1)),
"-l",
lang,
"-w",
convertPath(outputPath),
` . ${title} . `,
];
await $`${cmd}`.noThrow();
}
}
77 changes: 11 additions & 66 deletions generate/gen_audio.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,20 @@
import { $, bgBlue } from "../deps.ts";
import { convertPath } from "../utils/utils.ts";
import {
checkCommand,
getPico2waveCommand,
} from "../utils/external_commands.ts";
import { ModOptions } from "../gen_pack.ts";
import { generate_audio_basic_tts } from "./basic_tts.ts";
import { generate_audio_with_openAI } from "./openai_tts.ts";

let hasPico2waveWslCache: undefined | boolean;

export async function hasPico2waveWsl() {
if (hasPico2waveWslCache === undefined) {
hasPico2waveWslCache = await checkCommand(
["wsl", "pico2wave", "--version"],
1,
);
}
return hasPico2waveWslCache;
}

let hasPico2waveCache: undefined | boolean;

export async function hasPico2wave() {
if (hasPico2waveCache === undefined) {
hasPico2waveCache = await checkCommand(["pico2wave", "--version"], 1);
}
return hasPico2waveCache;
}

// FIXME : use object args
export async function generateAudio(
title: string,
outputPath: string,
lang: string,
skipWsl: boolean,
opt: ModOptions,
) {
console.log(bgBlue(`Generate audio to ${outputPath}`));

if (Deno.build.os === "windows" && (skipWsl || !(await hasPico2waveWsl()))) {
const audioFormat = "[System.Speech.AudioFormat.SpeechAudioFormatInfo]::" +
"new(8000,[System.Speech.AudioFormat.AudioBitsPerSample]" +
"::Sixteen,[System.Speech.AudioFormat.AudioChannel]::Mono)";

const args = [
"-Command",
`Add-Type -AssemblyName System.Speech; ` +
`$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; ` +
`$speak.SetOutputToWaveFile("${outputPath}",${audioFormat}); ` +
`$speak.Speak(" . ${title.replace(/["' ]/g, " ")} . "); ` +
`$speak.Dispose();`,
];
await $`PowerShell ${args}`.noThrow();
} else if (Deno.build.os === "darwin" && !(await hasPico2wave())) {
const args = [
"-o",
convertPath(outputPath),
"--file-format",
"WAVE",
"--data-format",
"LEF32@22050",
];
await $`say ${args}`.noThrow();
if (opt.useOpenAiTts) {
await generate_audio_with_openAI(
title,
outputPath.replace(/\.wav/i, ".mp3"),
opt,
);
} else {
const pico2waveCommand = await getPico2waveCommand();
const cmd = [
pico2waveCommand[0],
...(pico2waveCommand.splice(1)),
"-l",
lang,
"-w",
convertPath(outputPath),
` . ${title} . `,
];
await $`${cmd}`.noThrow();
await generate_audio_basic_tts(title, outputPath, lang, opt);
}
}
38 changes: 17 additions & 21 deletions generate/gen_missing_items.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { File, Folder } from "../serialize/types.ts";
import { Folder } from "../serialize/types.ts";
import {
checkRunPermission,
getFileAudioItem,
Expand All @@ -13,6 +13,7 @@ import {
import { generateImage } from "./gen_image.ts";
import { generateAudio } from "./gen_audio.ts";
import { i18next, join } from "../deps.ts";
import { ModOptions } from "../gen_pack.ts";

function getTitle(name: string): string {
if (/^[0-9]* *-? *$/.test(name)) {
Expand All @@ -22,62 +23,57 @@ function getTitle(name: string): string {
}
}

// FIXME : use object args
export async function genMissingItems(
rootpath: string,
folder: Folder,
genImage: boolean,
genAudio: boolean,
lang: string,
isRoot: boolean,
skipWsl: boolean,
rootpath: string,
opt: ModOptions,
) {
if (genImage || genAudio) {
if (!opt.skipImageItemGen || !opt.skipAudioItemGen) {
await checkRunPermission();
if (genImage && !getFolderImageItem(folder)) {
if (!opt.skipImageItemGen && !getFolderImageItem(folder)) {
await generateImage(getTitle(folder.name), `${rootpath}/0-item.png`);
}
if (genAudio && !getFolderAudioItem(folder)) {
if (!opt.skipAudioItemGen && !getFolderAudioItem(folder)) {
await generateAudio(
getTitle(folder.name),
`${rootpath}/0-item.wav`,
lang,
skipWsl,
opt,
);
}
if (genAudio && isRoot && !getNightModeAudioItem(folder)) {
if (!opt.skipAudioItemGen && isRoot && !getNightModeAudioItem(folder)) {
await generateAudio(
i18next.t("NightModeTransition"),
`${rootpath}/0-night-mode.wav`,
lang,
skipWsl,
opt,
);
}

for (const file of folder.files) {
if (isFolder(file)) {
await genMissingItems(
join(rootpath, file.name),
file as Folder,
genImage,
genAudio,
file,
lang,
false,
skipWsl,
join(rootpath, file.name),
opt,
);
} else if (isStory(file as File)) {
if (genImage && !getFileImageItem(file as File, folder)) {
} else if (isStory(file)) {
if (!opt.skipImageItemGen && !getFileImageItem(file, folder)) {
await generateImage(
getTitle(getNameWithoutExt(file.name)),
`${rootpath}/${getNameWithoutExt(file.name)}.item.png`,
);
}
if (genAudio && !getFileAudioItem(file as File, folder)) {
if (!opt.skipAudioItemGen && !getFileAudioItem(file, folder)) {
await generateAudio(
getTitle(getNameWithoutExt(file.name)),
`${rootpath}/${getNameWithoutExt(file.name)}.item.wav`,
lang,
skipWsl,
opt,
);
}
}
Expand Down
Loading

0 comments on commit 6edc01b

Please sign in to comment.