gtts plugin, sounds better than opentts:ko
This commit is contained in:
1 parent
7a4c71675b
commit
7e8c38a80f
6 files changed
+162
-59
No files matched your search
+12
-58
@@ -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 { 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;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
@@ -69,7 +67,12 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
vite: {
|
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(),
|
envDir: process.cwd(),
|
||||||
ssr: {
|
ssr: {
|
||||||
noExternal: [],
|
noExternal: [],
|
||||||
@@ -98,6 +101,10 @@ export default defineConfig({
|
|||||||
port: 5174,
|
port: 5174,
|
||||||
}
|
}
|
||||||
: undefined,
|
: 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`.
|
// Env variables starting with the item of `envPrefix` will be exposed in tauri's source code through `import.meta.env`.
|
||||||
envPrefix: ["VITE_", "TAURI_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);
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,7 @@ async function playAudio() {
|
|||||||
if (navigator.platform === "Win32" || !(await playAudioTTSPlugin())) {
|
if (navigator.platform === "Win32" || !(await playAudioTTSPlugin())) {
|
||||||
if (!audioEl.value) return;
|
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("voice", "glow-speak:ko_kss");
|
||||||
url.searchParams.set("text", getMainText());
|
url.searchParams.set("text", getMainText());
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@types/node": "^24.13.3",
|
"@types/node": "^24.13.3",
|
||||||
"markdown-it": "^14.3.0",
|
"markdown-it": "^14.3.0",
|
||||||
"pdfjs-dist": "^5.7.284",
|
"pdfjs-dist": "^5.7.284",
|
||||||
|
"shell-quote": "^1.10.0",
|
||||||
"tauri-plugin-tts-api": "^0.1.11",
|
"tauri-plugin-tts-api": "^0.1.11",
|
||||||
"vite": "^8.1.5",
|
"vite": "^8.1.5",
|
||||||
"yaml": "^2.9.0",
|
"yaml": "^2.9.0",
|
||||||
|
|||||||
Generated
+9
@@ -26,6 +26,9 @@ importers:
|
|||||||
pdfjs-dist:
|
pdfjs-dist:
|
||||||
specifier: ^5.7.284
|
specifier: ^5.7.284
|
||||||
version: 5.7.284
|
version: 5.7.284
|
||||||
|
shell-quote:
|
||||||
|
specifier: ^1.10.0
|
||||||
|
version: 1.10.0
|
||||||
tauri-plugin-tts-api:
|
tauri-plugin-tts-api:
|
||||||
specifier: ^0.1.11
|
specifier: ^0.1.11
|
||||||
version: 0.1.11
|
version: 0.1.11
|
||||||
@@ -1088,6 +1091,10 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
shell-quote@1.10.0:
|
||||||
|
resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
shiki@3.23.0:
|
shiki@3.23.0:
|
||||||
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
|
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
|
||||||
|
|
||||||
@@ -2109,6 +2116,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.62.2
|
'@rollup/rollup-win32-x64-msvc': 4.62.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
shell-quote@1.10.0: {}
|
||||||
|
|
||||||
shiki@3.23.0:
|
shiki@3.23.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/core': 3.23.0
|
'@shikijs/core': 3.23.0
|
||||||
|
|||||||
Reference in new issue
Block a user