gtts plugin, sounds better than opentts:ko

This commit is contained in:
polv committed 2026-08-26 18:21:47 +07:00
1 parent 7a4c71675b
commit 7e8c38a80f
6 files changed
+162 -59

No files matched your search

+12 -58
View File
@@ -1,8 +1,6 @@
import fs from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import type { Connect, Plugin, ViteDevServer } from "vite";
import { defineConfig, type MarkdownOptions } from "vitepress";
import { mp3DownloaderPlugin } from "./vite-plugins/mp3-downloader";
import { gttsPlugin } from "./vite-plugins/gtts";
const host = process.env.TAURI_DEV_HOST;
@@ -69,7 +67,12 @@ export default defineConfig({
},
},
vite: {
plugins: [audioProxyPlugin() as any],
plugins: [
mp3DownloaderPlugin() as any,
// (Python) pip install gtts
// gtts will intercept /api/tts away from opentts:ko container
gttsPlugin(),
],
envDir: process.cwd(),
ssr: {
noExternal: [],
@@ -98,6 +101,10 @@ export default defineConfig({
port: 5174,
}
: undefined,
proxy: {
// docker run -it -p 5500:5500 synesthesiam/opentts:ko
"/api/tts": "http://localhost:5500",
},
},
// Env variables starting with the item of `envPrefix` will be exposed in tauri's source code through `import.meta.env`.
envPrefix: ["VITE_", "TAURI_ENV_*"],
@@ -112,56 +119,3 @@ export default defineConfig({
},
},
});
function audioProxyPlugin(): Plugin {
return {
name: "audio-proxy",
configureServer(server: ViteDevServer) {
// Vite dev server middlewares are connect-style: (req, res, next)
server.middlewares.use(async (req, res, next) => {
const reqUrl = req.url;
if (!reqUrl || !reqUrl.startsWith("/uploads/")) return next();
if (!reqUrl.endsWith(".mp3")) return next();
try {
await proxyAudio(req, res, next);
} catch (e) {
console.error(e, reqUrl);
next();
}
});
},
};
}
async function proxyAudio(
req: Connect.IncomingMessage,
res: ServerResponse<IncomingMessage>,
next: Connect.NextFunction,
) {
const url = new URL(`https://www.howtostudykorean.com/wp-content${req.url}`);
const segs = req.url!.slice(1).split("/");
const filename = segs.pop()!;
const saveroot = "docs/public";
const saveto = path.join(saveroot, ...segs, filename);
if (fs.existsSync(saveto)) {
return next();
}
fs.mkdirSync(path.join(saveroot, ...segs), { recursive: true });
const r = await fetch(url, { redirect: "follow" });
if (!r.ok) {
console.error(r);
return next();
}
fs.writeFileSync(saveto, Buffer.from(await r.arrayBuffer()));
// console.log(fs.statSync(saveto));
console.log("saved to", saveto);
res.setHeader("Content-Type", "audio/mpeg");
fs.createReadStream(saveto).pipe(res);
}
+1 -1
View File
@@ -46,7 +46,7 @@ async function playAudio() {
if (navigator.platform === "Win32" || !(await playAudioTTSPlugin())) {
if (!audioEl.value) return;
const url = new URL("http://localhost:5500/api/tts");
const url = new URL("/api/tts", location.origin);
url.searchParams.set("voice", "glow-speak:ko_kss");
url.searchParams.set("text", getMainText());
+82
View File
@@ -0,0 +1,82 @@
import fs from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import type { Connect, Plugin, ViteDevServer } from "vite";
import { spawn } from "child_process";
export function gttsPlugin(): Plugin {
return {
name: "gtts",
configureServer(server: ViteDevServer) {
// Vite dev server middlewares are connect-style: (req, res, next)
server.middlewares.use(async (req, res, next) => {
const reqUrl = req.url;
if (!reqUrl) return next();
const url = new URL(reqUrl, "http://localhost");
if (url.pathname !== "/api/tts") return next();
const text = url.searchParams.get("text");
if (!text) return next();
try {
await gtts(text, req, res, next);
} catch (e) {
console.error(e, reqUrl);
next();
}
});
},
};
}
let fileList: Record<string, number> = {};
async function gtts(
text: string,
req: Connect.IncomingMessage,
res: ServerResponse<IncomingMessage>,
next: Connect.NextFunction,
) {
text = text.replace(/["]/g, " ");
const saveroot = "docs/.vitepress/cache/gtts";
fs.mkdirSync(saveroot, { recursive: true });
const fileListJSON = path.join(saveroot, "gtts.json");
if (Object.keys(fileList).length === 0 && fs.existsSync(fileListJSON)) {
fileList = JSON.parse(fs.readFileSync(fileListJSON, "utf-8"));
}
const fileNo = fileList[text] || Object.keys(fileList).length;
const filename = `${fileNo}.mp3`;
const saveto = path.join(saveroot, filename);
if (!fs.existsSync(saveto)) {
await new Promise((resolve, reject) => {
const p = spawn(
"gtts-cli",
[
`"${text}"`,
...["--lang", "ko"],
...["--output", path.join(process.cwd(), saveto)],
],
{
stdio: "inherit",
shell: true,
},
);
p.on("error", reject).on("close", resolve);
});
fileList[text] = fileNo;
console.log(`gtts: ${text} saved to ${saveto}`);
fs.writeFileSync(fileListJSON, JSON.stringify(fileList, null, 2));
} else {
// console.log(`gtts: ${text} loaded from ${saveto}`);
}
res.setHeader("Content-Type", "audio/mpeg");
fs.createReadStream(saveto).pipe(res);
}
@@ -0,0 +1,57 @@
import fs from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import type { Connect, Plugin, ViteDevServer } from "vite";
export function mp3DownloaderPlugin(): Plugin {
return {
name: "mp3-downloader",
configureServer(server: ViteDevServer) {
// Vite dev server middlewares are connect-style: (req, res, next)
server.middlewares.use(async (req, res, next) => {
const reqUrl = req.url;
if (!reqUrl || !reqUrl.startsWith("/uploads/")) return next();
if (!reqUrl.endsWith(".mp3")) return next();
try {
await mp3Downloader(req, res, next);
} catch (e) {
console.error(e, reqUrl);
next();
}
});
},
};
}
async function mp3Downloader(
req: Connect.IncomingMessage,
res: ServerResponse<IncomingMessage>,
next: Connect.NextFunction,
) {
const url = new URL(`https://www.howtostudykorean.com/wp-content${req.url}`);
const segs = req.url!.slice(1).split("/");
const filename = segs.pop()!;
const saveroot = "docs/public";
const saveto = path.join(saveroot, ...segs, filename);
if (fs.existsSync(saveto)) {
return next();
}
fs.mkdirSync(path.join(saveroot, ...segs), { recursive: true });
const r = await fetch(url, { redirect: "follow" });
if (!r.ok) {
console.error(r);
return next();
}
fs.writeFileSync(saveto, Buffer.from(await r.arrayBuffer()));
// console.log(fs.statSync(saveto));
console.log("saved to", saveto);
res.setHeader("Content-Type", "audio/mpeg");
fs.createReadStream(saveto).pipe(res);
}