├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── help_wanted.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── main-mac.yml
│ └── main-win.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── electron-builder.json5
├── electron
├── electron-env.d.ts
├── main
│ └── index.ts
├── preload
│ └── index.ts
└── utils
│ ├── api.ts
│ ├── azure-api.ts
│ ├── edge-api.ts
│ ├── gpt-api.ts
│ └── log.ts
├── index.html
├── package-lock.json
├── package.json
├── public
├── electron-vite-vue.gif
├── favicon.ico
└── node.png
├── src
├── App.vue
├── assets
│ ├── electron.png
│ ├── i18n
│ │ └── i18n.ts
│ ├── vite.svg
│ └── vue.png
├── components
│ ├── aside
│ │ ├── Aside.vue
│ │ └── Version.vue
│ ├── configpage
│ │ ├── BiliBtn.vue
│ │ ├── ConfigPage.vue
│ │ ├── Donate.vue
│ │ ├── GiteeBtn.vue
│ │ └── GithubBtn.vue
│ ├── footer
│ │ └── Footer.vue
│ ├── header
│ │ ├── Header.vue
│ │ └── Logo.vue
│ └── main
│ │ ├── Loading.vue
│ │ ├── Main.vue
│ │ ├── MainOptions.vue
│ │ ├── emoji-config.ts
│ │ └── options-config.ts
├── env.d.ts
├── global
│ ├── index.ts
│ ├── initLocalStore.ts
│ ├── registerElement.ts
│ └── voices.ts
├── main.ts
├── store
│ ├── play.ts
│ └── store.ts
└── types
│ └── prompGPT.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
├── vite.config.ts.js
└── yarn.lock
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: 🐞 Bug report
4 | about: Create a report to help us improve
5 | title: "[Bug] the title of bug report"
6 | labels: bug
7 | assignees: ''
8 |
9 | ---
10 |
11 | #### Describe the bug
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help_wanted.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🥺 Help wanted
3 | about: Confuse about the use of electron-vue-vite
4 | title: "[Help] the title of help wanted report"
5 | labels: help wanted
6 | assignees: ''
7 |
8 | ---
9 |
10 | #### Describe the problem you confuse
11 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Description
4 |
5 |
6 |
7 | ### What is the purpose of this pull request?
8 |
9 | - [ ] Bug fix
10 | - [ ] New Feature
11 | - [ ] Documentation update
12 | - [ ] Other
13 |
--------------------------------------------------------------------------------
/.github/workflows/main-mac.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths-ignore:
7 | - "**.md"
8 | - "**.spec.js"
9 | - ".idea"
10 | - ".vscode"
11 | - ".dockerignore"
12 | - "Dockerfile"
13 | - ".gitignore"
14 | - ".github/**"
15 | - "!.github/workflows/main.yml"
16 | workflow_dispatch:
17 |
18 | jobs:
19 | build:
20 | runs-on: ${{ matrix.os }}
21 |
22 | strategy:
23 | matrix:
24 | # [macos-latest, ubuntu-latest, windows-latest]
25 | os: [macos-latest]
26 |
27 | steps:
28 | - name: Checkout Code
29 | uses: actions/checkout@v2
30 | with:
31 | fetch-depth: 0
32 |
33 | - name: Setup Node.js
34 | uses: actions/setup-node@v2
35 | with:
36 | node-version: 14
37 |
38 | - name: Install Dependencies
39 | run: npm install
40 |
41 | - name: Build Release Files
42 | run: npm run build
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
46 | - name: Upload Artifact
47 | uses: actions/upload-artifact@v3
48 | with:
49 | name: tts-vue-${{ matrix. os }}
50 | path: release/*/*.dmg
51 | retention-days: 30
52 |
--------------------------------------------------------------------------------
/.github/workflows/main-win.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths-ignore:
7 | - "**.md"
8 | - "**.spec.js"
9 | - ".idea"
10 | - ".vscode"
11 | - ".dockerignore"
12 | - "Dockerfile"
13 | - ".gitignore"
14 | - ".github/**"
15 | - "!.github/workflows/main.yml"
16 | workflow_dispatch:
17 |
18 | jobs:
19 | build:
20 | runs-on: ${{ matrix.os }}
21 |
22 | strategy:
23 | matrix:
24 | # [macos-latest, ubuntu-latest, windows-latest]
25 | os: [windows-latest]
26 |
27 | steps:
28 | - name: Checkout Code
29 | uses: actions/checkout@v2
30 | with:
31 | fetch-depth: 0
32 |
33 | - name: Setup Node.js
34 | uses: actions/setup-node@v2
35 | with:
36 | node-version: 14
37 |
38 | - name: Install Dependencies
39 | run: npm install
40 |
41 | - name: Build Release Files
42 | run: npm run build
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
46 | - name: Upload Artifact
47 | uses: actions/upload-artifact@v3
48 | with:
49 | name: tts-vue-${{ matrix. os }}
50 | path: release/*/*.exe
51 | retention-days: 30
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
25 | release
26 |
27 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2022-06-04
2 |
3 | [v2.0.0](https://github.com/electron-vite/electron-vite-vue/pull/156)
4 |
5 | - 🖖 Based on the `vue-ts` template created by `npm create vite`, integrate `vite-plugin-electron`
6 | - ⚡️ More simplify, is in line with Vite project structure
7 |
8 | ## 2022-01-30
9 |
10 | [v1.0.0](https://github.com/electron-vite/electron-vite-vue/releases/tag/v1.0.0)
11 |
12 | - ⚡️ Main、Renderer、preload, all built with vite
13 |
14 | ## 2022-01-27
15 | - Refactor the scripts part.
16 | - Remove `configs` directory.
17 |
18 | ## 2021-11-11
19 | - Refactor the project. Use vite.config.ts build `Main-process`, `Preload-script` and `Renderer-process` alternative rollup.
20 | - Scenic `Vue>=3.2.13`, `@vue/compiler-sfc` is no longer necessary.
21 | - If you prefer Rollup, Use rollup branch.
22 |
23 | ```bash
24 | Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree.
25 | ```
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 草鞋没号
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TTS-Vue
2 |
3 | 🎤 微软语音合成工具,使用 `Electron` + `Vue` + `ElementPlus` + `Vite` 构建.
4 |
5 | ## 开始使用
6 |
7 | - [项目简介](https://loker-page.lgwawork.com/guide/intro.html)
8 |
9 | - [安装运行](https://loker-page.lgwawork.com/guide/install.html)
10 |
11 | - [功能介绍](https://loker-page.lgwawork.com/guide/features.html)
12 |
13 | - [常见问题](https://loker-page.lgwawork.com/guide/qa.html)
14 |
15 | - [更新日志](https://loker-page.lgwawork.com/guide/update.html)
16 |
17 | ## 注意
18 |
19 | 该软件以及代码仅为个人学习测试使用,请在下载后24小时内删除,不得用于商业用途,否则后果自负。任何违规使用造成的法律后果与本人无关。该软件也永远不会收费,如果您使用该软件前支付了额外费用,或付费获得源码或成品软件,那么你一定被骗了!
20 |
21 | **搬运请注明出处。禁止诱导他人以加群、私信等方式获取软件的仓库、下载地址和安装包。**
22 |
23 | ### 意见问题反馈,版本发布企鹅群:
24 |
25 | `【tts-vue问题反馈群⑤】439382846`
26 |
27 | `【tts-vue问题反馈群④】781659118(满)`
28 |
29 | `【tts-vue问题反馈群③】474128303(满)`
30 |
31 | `【tts-vue问题反馈群②】702034846(满)`
32 |
33 | `【tts-vue问题反馈群①】752801820(满)`
34 |
35 | ## Star History
36 |
37 | [](https://star-history.com/#LokerL/tts-vue&Date)
38 |
--------------------------------------------------------------------------------
/electron-builder.json5:
--------------------------------------------------------------------------------
1 | /**
2 | * @see https://www.electron.build/configuration/configuration
3 | */
4 | {
5 | "appId": "vip.loker.tts-vue",
6 | "asar": true,
7 | "directories": {
8 | "output": "release/${version}"
9 | },
10 | "files": [
11 | "dist"
12 | ],
13 | "mac": {
14 | "artifactName": "${productName}-${version}.${ext}",
15 | "target": [
16 | "dmg"
17 | ]
18 | },
19 | "win": {
20 | "target": [
21 | {
22 | "target": "nsis",
23 | "arch": [
24 | "x64"
25 | ]
26 | }
27 | ],
28 | "artifactName": "${productName}-${version}.${ext}"
29 | },
30 | "nsis": {
31 | "oneClick": false,
32 | "perMachine": false,
33 | "allowToChangeInstallationDirectory": true,
34 | "deleteAppDataOnUninstall": false
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/electron/electron-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/electron/main/index.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, shell, ipcMain, dialog } from "electron";
2 | import { release } from "os";
3 | import { join } from "path";
4 | import api from "../utils/api";
5 | import edgeApi from "../utils/edge-api";
6 | import azureApi from "../utils/azure-api";
7 | import logger from "../utils/log";
8 | import { gptApi } from "../utils/gpt-api";
9 |
10 | // Disable GPU Acceleration for Windows 7
11 | //if (release().startsWith("6.1")) app.disableHardwareAcceleration();
12 | app.disableHardwareAcceleration();
13 |
14 | // Set application name for Windows 10+ notifications
15 | if (process.platform === "win32") app.setAppUserModelId(app.getName());
16 |
17 | if (!app.requestSingleInstanceLock()) {
18 | app.quit();
19 | process.exit(0);
20 | }
21 |
22 | // Remove electron security warnings
23 | // This warning only shows in development mode
24 | // Read more on https://www.electronjs.org/docs/latest/tutorial/security
25 | // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
26 |
27 | export const ROOT_PATH = {
28 | // /dist
29 | dist: join(__dirname, "../.."),
30 | // /dist or /public
31 | public: join(__dirname, app.isPackaged ? "../.." : "../../../public"),
32 | };
33 |
34 | let win: BrowserWindow | null = null;
35 | // Here, you can also use other preload
36 | const preload = join(__dirname, "../preload/index.js");
37 | // 🚧 Use ['ENV_NAME'] avoid vite:define plugin
38 | const url = `http://${process.env["VITE_DEV_SERVER_HOST"]}:${process.env["VITE_DEV_SERVER_PORT"]}`;
39 | const indexHtml = join(ROOT_PATH.dist, "index.html");
40 |
41 | async function createWindow() {
42 | win = new BrowserWindow({
43 | width: 1200,
44 | minWidth: 900,
45 | minHeight: 650,
46 | height: 650,
47 |
48 | title: "Main window",
49 | icon: join(ROOT_PATH.public, "favicon.ico"),
50 | // useContentSize: true,
51 | frame: false,
52 | // maximizable: false,
53 | // minimizable: false,
54 | // fullscreenable: false,
55 | transparent: true,
56 | hasShadow: false,
57 | // resizable: false,
58 | webPreferences: {
59 | preload,
60 | webSecurity: false,
61 | // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
62 | // Consider using contextBridge.exposeInMainWorld
63 | // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
64 | nodeIntegration: true,
65 | contextIsolation: false,
66 | },
67 | });
68 | app.commandLine.appendSwitch("disable-features", "OutOfBlinkCors");
69 | if (app.isPackaged) {
70 | win.loadFile(indexHtml);
71 | } else {
72 | win.loadURL(url);
73 | win.webContents.openDevTools();
74 | }
75 |
76 | // Test actively push message to the Electron-Renderer
77 | win.webContents.on("did-finish-load", () => {
78 | win?.webContents.send("main-process-message", new Date().toLocaleString());
79 | });
80 |
81 | // Make all links open with the browser, not with the application
82 | win.webContents.setWindowOpenHandler(({ url }) => {
83 | if (url.startsWith("https:")) shell.openExternal(url);
84 | return { action: "deny" };
85 | });
86 | }
87 |
88 | app.whenReady().then(createWindow);
89 |
90 | app.on("window-all-closed", () => {
91 | win = null;
92 | if (process.platform !== "darwin") app.quit();
93 | });
94 |
95 | app.on("second-instance", () => {
96 | if (win) {
97 | // Focus on the main window if the user tried to open another
98 | if (win.isMinimized()) win.restore();
99 | win.focus();
100 | }
101 | });
102 |
103 | app.on("activate", () => {
104 | const allWindows = BrowserWindow.getAllWindows();
105 | if (allWindows.length) {
106 | allWindows[0].focus();
107 | } else {
108 | createWindow();
109 | }
110 | });
111 |
112 | ipcMain.on("min", (e) => win.minimize());
113 | ipcMain.on("window-maximize", function () {
114 | if (win.isFullScreen()) {
115 | win.setFullScreen(false);
116 | } else if (win.isMaximized()) {
117 | win.unmaximize();
118 | } else {
119 | win.maximize();
120 | }
121 | });
122 | ipcMain.on("close", (e) => win.close());
123 | ipcMain.on("reload", (e) => win.reload());
124 |
125 | // new window example arg: new windows url
126 | ipcMain.handle("open-win", (event, arg) => {
127 | const childWindow = new BrowserWindow({
128 | webPreferences: {
129 | preload,
130 | },
131 | });
132 |
133 | if (app.isPackaged) {
134 | childWindow.loadFile(indexHtml, { hash: arg });
135 | } else {
136 | childWindow.loadURL(`${url}/#${arg}`);
137 | childWindow.webContents.openDevTools({ mode: "undocked", activate: true });
138 | }
139 | });
140 | const ElectronStore = require("electron-store");
141 | ElectronStore.initRenderer();
142 |
143 | ipcMain.on("log.info", async (event, arg) => {
144 | logger.info(arg);
145 | });
146 | ipcMain.on("log.error", async (event, arg) => {
147 | logger.error(arg);
148 | });
149 |
150 | ipcMain.on("openLogs", async (event, arg) => {
151 | shell.openPath(logger.logger.transports.file.getFile().path);
152 | });
153 | ipcMain.on("openLogFolder", async (event, arg) => {
154 | shell.openPath(logger.logFolder);
155 | });
156 | ipcMain.on("showItemInFolder", async (event, arg) => {
157 | shell.showItemInFolder(arg);
158 | });
159 | ipcMain.on("openDevTools", async (event, arg) => {
160 | if (win.webContents.isDevToolsOpened()) {
161 | win.webContents.closeDevTools();
162 | } else {
163 | win.webContents.openDevTools({ mode: "undocked", activate: true });
164 | }
165 | });
166 |
167 | // Get desktop path
168 | ipcMain.on("getDesktopPath", async (event) => {
169 | event.returnValue = app.getPath("desktop");
170 | });
171 |
172 | ipcMain.handle("speech", async (event, ssml) => {
173 | const res = api.speechApi(ssml);
174 | return res;
175 | });
176 |
177 | ipcMain.handle("voices", async (event) => {
178 | const res = api.voicesApi();
179 | return res;
180 | });
181 |
182 | ipcMain.handle("edgeApi", async (event, ssml) => {
183 | const res = edgeApi(ssml)
184 | return res;
185 | });
186 |
187 | ipcMain.handle("azureApi", async (event, ssml, key, region) => {
188 | const res = azureApi(ssml, key, region)
189 | return res;
190 | });
191 | // const result = await ipcRenderer.invoke("promptGPT", promptGPT, model, key);
192 | ipcMain.handle("promptGPT", async (event, promptGPT, model, key) => {
193 | const res = gptApi(promptGPT, model, key);
194 | return res;
195 | });
196 |
197 | ipcMain.handle("openFolderSelector", async (event) => {
198 | const path = dialog.showOpenDialogSync(win, {
199 | defaultPath: app.getPath("desktop"),
200 | properties: ["openDirectory"],
201 | });
202 | return path;
203 | });
204 |
--------------------------------------------------------------------------------
/electron/preload/index.ts:
--------------------------------------------------------------------------------
1 | function domReady(
2 | condition: DocumentReadyState[] = ["complete", "interactive"]
3 | ) {
4 | return new Promise((resolve) => {
5 | if (condition.includes(document.readyState)) {
6 | resolve(true);
7 | } else {
8 | document.addEventListener("readystatechange", () => {
9 | if (condition.includes(document.readyState)) {
10 | resolve(true);
11 | }
12 | });
13 | }
14 | });
15 | }
16 |
17 | const safeDOM = {
18 | append(parent: HTMLElement, child: HTMLElement) {
19 | if (!Array.from(parent.children).find((e) => e === child)) {
20 | return parent.appendChild(child);
21 | }
22 | },
23 | remove(parent: HTMLElement, child: HTMLElement) {
24 | if (Array.from(parent.children).find((e) => e === child)) {
25 | return parent.removeChild(child);
26 | }
27 | },
28 | };
29 |
30 | /**
31 | * https://tobiasahlin.com/spinkit
32 | * https://connoratherton.com/loaders
33 | * https://projects.lukehaas.me/css-loaders
34 | * https://matejkustec.github.io/SpinThatShit
35 | */
36 | function useLoading() {
37 | const className = `loaders-css__square-spin`;
38 | const styleContent = `
39 | @keyframes square-spin {
40 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
41 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
42 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
43 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); }
44 | }
45 | .${className} > div {
46 | animation-fill-mode: both;
47 | width: 50px;
48 | height: 50px;
49 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
50 | }
51 | .app-loading-wrap {
52 | position: fixed;
53 | top: 0;
54 | left: 0;
55 | width: 100vw;
56 | height: 100vh;
57 | display: flex;
58 | align-items: center;
59 | justify-content: center;
60 | z-index: 9;
61 | }
62 | `;
63 | const oStyle = document.createElement("style");
64 | const oDiv = document.createElement("div");
65 |
66 | oStyle.id = "app-loading-style";
67 | oStyle.innerHTML = styleContent;
68 | oDiv.className = "app-loading-wrap";
69 | oDiv.innerHTML = `
`;
70 |
71 | return {
72 | appendLoading() {
73 | safeDOM.append(document.head, oStyle);
74 | safeDOM.append(document.body, oDiv);
75 | },
76 | removeLoading() {
77 | safeDOM.remove(document.head, oStyle);
78 | safeDOM.remove(document.body, oDiv);
79 | },
80 | };
81 | }
82 |
83 | // ----------------------------------------------------------------------
84 |
85 | const { appendLoading, removeLoading } = useLoading();
86 | domReady().then(appendLoading);
87 |
88 | window.onmessage = (ev) => {
89 | ev.data.payload === "removeLoading" && removeLoading();
90 | };
91 |
92 | setTimeout(removeLoading, 5000);
93 |
--------------------------------------------------------------------------------
/electron/utils/api.ts:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const fs = require("fs");
3 | const { v4: uuidv4 } = require("uuid");
4 |
5 | const speechApi = (ssml: string) => {
6 | var data = JSON.stringify({
7 | ssml,
8 | ttsAudioFormat: "audio-24khz-160kbitrate-mono-mp3",
9 | offsetInPlainText: 0,
10 | properties: {
11 | SpeakTriggerSource: "AccTuningPagePlayButton",
12 | },
13 | });
14 |
15 | var config = {
16 | method: "post",
17 | url: "https://southeastasia.api.speech.microsoft.com/accfreetrial/texttospeech/acc/v3.0-beta1/vcg/speak",
18 | responseType: "arraybuffer",
19 | headers: {
20 | authority: "southeastasia.api.speech.microsoft.com",
21 | accept: "*/*",
22 | "accept-language": "zh-CN,zh;q=0.9",
23 | customvoiceconnectionid: uuidv4(),
24 | origin: "https://speech.microsoft.com",
25 | "sec-ch-ua":
26 | '"Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
27 | "sec-ch-ua-mobile": "?0",
28 | "sec-ch-ua-platform": '"Windows"',
29 | "sec-fetch-dest": "empty",
30 | "sec-fetch-mode": "cors",
31 | "sec-fetch-site": "same-site",
32 | "user-agent":
33 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36",
34 | "content-type": "application/json",
35 | },
36 |
37 | data: data,
38 | };
39 |
40 | return new Promise((resolve, reject) => {
41 | axios(config)
42 | .then(function (response) {
43 | resolve(response.data);
44 | })
45 | .catch(function (error) {
46 | console.error(error);
47 | reject(error);
48 | });
49 | });
50 | };
51 |
52 | const voicesApi = () => {
53 |
54 | const data = JSON.stringify({
55 | "queryCondition": {
56 | "items": [
57 | {
58 | "name": "VoiceTypeList",
59 | "value": "StandardVoice",
60 | "operatorKind": "Contains"
61 | }
62 | ]
63 | }
64 | });
65 |
66 | const config = {
67 | method: 'post',
68 | url: 'https://southeastasia.api.speech.microsoft.com/accfreetrial/texttospeech/acc/v3.0-beta1/vcg/voices',
69 | headers: {
70 | 'authority': 'southeastasia.api.speech.microsoft.com',
71 | 'accept': 'application/json, text/plain, */*',
72 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
73 | 'customvoiceconnectionid': '97130be0-f304-11ed-b81e-274ad6e5de17',
74 | 'origin': 'https://speech.microsoft.com',
75 | 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"',
76 | 'sec-ch-ua-mobile': '?0',
77 | 'sec-ch-ua-platform': '"Windows"',
78 | 'sec-fetch-dest': 'empty',
79 | 'sec-fetch-mode': 'cors',
80 | 'sec-fetch-site': 'same-site',
81 | 'speechstudio-session-id': '951910a0-f304-11ed-b81e-274ad6e5de17',
82 | 'speechstudio-subscriptionsession-id': 'undefined',
83 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.42',
84 | 'x-ms-useragent': 'SpeechStudio/2021.05.001',
85 | 'content-type': 'application/json'
86 | },
87 | data : data,
88 | timeout: 1500,
89 | };
90 | return new Promise((resolve, reject) => {
91 | axios(config)
92 | .then(function (response) {
93 | resolve(response.data);
94 | })
95 | .catch(function (error) {
96 | console.error(error);
97 | reject(error);
98 | });
99 | });
100 | }
101 |
102 | export default {
103 | speechApi,
104 | voicesApi,
105 | };
106 |
--------------------------------------------------------------------------------
/electron/utils/azure-api.ts:
--------------------------------------------------------------------------------
1 | import logger from "../utils/log";
2 | import * as sdk from "microsoft-cognitiveservices-speech-sdk";
3 |
4 | const azureApi = (ssml: string, key: string, region: string) => {
5 | const speechConfig = sdk.SpeechConfig.fromSubscription(key, region);
6 | speechConfig.setProperty(sdk.PropertyId.SpeechServiceResponse_RequestSentenceBoundary, "true");
7 | var audio_config = sdk.AudioConfig.fromDefaultSpeakerOutput();
8 | var speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig, audio_config);
9 | return new Promise((resolve, reject) => {
10 | speechSynthesizer.speakSsmlAsync(
11 | ssml,
12 | (result: any) => {
13 | if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
14 | logger.info(`Speech synthesized to speaker for text [${ssml}]`);
15 | resolve(Buffer.from(result.audioData));
16 | } else {
17 | logger.info("Speech synthesis canceled, " + result.errorDetails + "\nDid you update the subscription info?");
18 | reject(result);
19 | }
20 | speechSynthesizer.close();
21 | speechSynthesizer = null;
22 | },
23 | (err: any) => {
24 | logger.info("Error synthesizing. " + err);
25 | speechSynthesizer.close();
26 | speechSynthesizer = null;
27 | }
28 | );
29 | }
30 | );
31 | }
32 |
33 | export default azureApi;
--------------------------------------------------------------------------------
/electron/utils/edge-api.ts:
--------------------------------------------------------------------------------
1 | const { randomBytes } = require("crypto");
2 | const { WebSocket } = require("ws");
3 | import logger from "../utils/log";
4 |
5 | const FORMAT_CONTENT_TYPE = new Map([
6 | ["raw-16khz-16bit-mono-pcm", "audio/basic"],
7 | ["raw-48khz-16bit-mono-pcm", "audio/basic"],
8 | ["raw-8khz-8bit-mono-mulaw", "audio/basic"],
9 | ["raw-8khz-8bit-mono-alaw", "audio/basic"],
10 |
11 | ["raw-16khz-16bit-mono-truesilk", "audio/SILK"],
12 | ["raw-24khz-16bit-mono-truesilk", "audio/SILK"],
13 |
14 | ["riff-16khz-16bit-mono-pcm", "audio/x-wav"],
15 | ["riff-24khz-16bit-mono-pcm", "audio/x-wav"],
16 | ["riff-48khz-16bit-mono-pcm", "audio/x-wav"],
17 | ["riff-8khz-8bit-mono-mulaw", "audio/x-wav"],
18 | ["riff-8khz-8bit-mono-alaw", "audio/x-wav"],
19 |
20 | ["audio-16khz-32kbitrate-mono-mp3", "audio/mpeg"],
21 | ["audio-16khz-64kbitrate-mono-mp3", "audio/mpeg"],
22 | ["audio-16khz-128kbitrate-mono-mp3", "audio/mpeg"],
23 | ["audio-24khz-48kbitrate-mono-mp3", "audio/mpeg"],
24 | ["audio-24khz-96kbitrate-mono-mp3", "audio/mpeg"],
25 | ["audio-24khz-160kbitrate-mono-mp3", "audio/mpeg"],
26 | ["audio-48khz-96kbitrate-mono-mp3", "audio/mpeg"],
27 | ["audio-48khz-192kbitrate-mono-mp3", "audio/mpeg"],
28 |
29 | ["webm-16khz-16bit-mono-opus", "audio/webm; codec=opus"],
30 | ["webm-24khz-16bit-mono-opus", "audio/webm; codec=opus"],
31 |
32 | ["ogg-16khz-16bit-mono-opus", "audio/ogg; codecs=opus; rate=16000"],
33 | ["ogg-24khz-16bit-mono-opus", "audio/ogg; codecs=opus; rate=24000"],
34 | ["ogg-48khz-16bit-mono-opus", "audio/ogg; codecs=opus; rate=48000"],
35 | ]);
36 |
37 | class Service {
38 | ws = null;
39 |
40 | executorMap;
41 | bufferMap;
42 |
43 | timer = null;
44 |
45 | constructor() {
46 | this.executorMap = new Map();
47 | this.bufferMap = new Map();
48 | }
49 |
50 | async connect() {
51 | const connectionId = randomBytes(16).toString("hex").toLowerCase();
52 | let url = `wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4&ConnectionId=${connectionId}`;
53 | let ws = new WebSocket(url, {
54 | host: "speech.platform.bing.com",
55 | origin: "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold",
56 | headers: {
57 | "User-Agent":
58 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.66 Safari/537.36 Edg/103.0.1264.44",
59 | },
60 | });
61 | return new Promise((resolve, reject) => {
62 | ws.on("open", () => {
63 | resolve(ws);
64 | });
65 | ws.on("close", (code, reason) => {
66 | // 服务器会自动断开空闲超过30秒的连接
67 | this.ws = null;
68 | if (this.timer) {
69 | clearTimeout(this.timer);
70 | this.timer = null;
71 | }
72 | for (let [key, value] of this.executorMap) {
73 | value.reject(`连接已关闭: ${reason} ${code}`);
74 | }
75 | this.executorMap.clear();
76 | this.bufferMap.clear();
77 | logger.info(`连接已关闭: ${reason} ${code}`);
78 | });
79 |
80 | ws.on("message", (message, isBinary) => {
81 | let pattern = /X-RequestId:(?[a-z|0-9]*)/;
82 | if (!isBinary) {
83 | let data = message.toString();
84 | if (data.includes("Path:turn.start")) {
85 | // 开始传输
86 | let matches = data.match(pattern);
87 | let requestId = matches.groups.id;
88 | this.bufferMap.set(requestId, Buffer.from([]));
89 | } else if (data.includes("Path:turn.end")) {
90 | // 结束传输
91 | let matches = data.match(pattern);
92 | let requestId = matches.groups.id;
93 |
94 | let executor = this.executorMap.get(requestId);
95 | if (executor) {
96 | this.executorMap.delete(matches.groups.id);
97 | let result = this.bufferMap.get(requestId);
98 | executor.resolve(result);
99 | logger.info(`传输完成:${requestId}……`);
100 | } else {
101 | logger.info(`请求已被丢弃:${requestId}`);
102 | }
103 | }
104 | } else if (isBinary) {
105 | let separator = "Path:audio\r\n";
106 | let data = message;
107 | let contentIndex = data.indexOf(separator) + separator.length;
108 |
109 | let headers = data.slice(2, contentIndex).toString();
110 | let matches = headers.match(pattern);
111 | let requestId = matches.groups.id;
112 |
113 | let content = data.slice(contentIndex);
114 | let buffer = this.bufferMap.get(requestId);
115 | if (buffer) {
116 | buffer = Buffer.concat([buffer, content], buffer.length+content.length);
117 | this.bufferMap.set(requestId, buffer);
118 | } else {
119 | logger.info(`请求已被丢弃:${requestId}`);
120 | }
121 | }
122 | });
123 | ws.on("error", (error) => {
124 | logger.error(`连接失败: ${error}`);
125 | reject(`连接失败: ${error}`);
126 | });
127 | });
128 | }
129 |
130 | async convert(ssml, format) {
131 | if (this.ws == null || this.ws.readyState != WebSocket.OPEN) {
132 | logger.info("准备连接服务器……");
133 | let connection = await this.connect();
134 | this.ws = connection;
135 | logger.info("连接成功!");
136 | }
137 | const requestId = randomBytes(16).toString("hex").toLowerCase();
138 | let result = new Promise((resolve, reject) => {
139 | // 等待服务器返回后这个方法才会返回结果
140 | this.executorMap.set(requestId, {
141 | resolve,
142 | reject,
143 | });
144 | // 发送配置消息
145 | let configData = {
146 | context: {
147 | synthesis: {
148 | audio: {
149 | metadataoptions: {
150 | sentenceBoundaryEnabled: "false",
151 | wordBoundaryEnabled: "false",
152 | },
153 | outputFormat: format,
154 | },
155 | },
156 | },
157 | };
158 | let configMessage =
159 | `X-Timestamp:${Date()}\r\n` +
160 | "Content-Type:application/json; charset=utf-8\r\n" +
161 | "Path:speech.config\r\n\r\n" +
162 | JSON.stringify(configData);
163 | this.ws.send(configMessage, (configError) => {
164 | if (configError) {
165 | logger.error(`配置请求发送失败:${requestId}\n`);
166 | }
167 |
168 | // 发送SSML消息
169 | let ssmlMessage =
170 | `X-Timestamp:${Date()}\r\n` +
171 | `X-RequestId:${requestId}\r\n` +
172 | `Content-Type:application/ssml+xml\r\n` +
173 | `Path:ssml\r\n\r\n` +
174 | ssml;
175 | this.ws.send(ssmlMessage, (ssmlError) => {
176 | if (ssmlError) {
177 | logger.error(`SSML消息发送失败:${requestId}\n`);
178 | }
179 | });
180 | });
181 | });
182 |
183 | // 收到请求,清除超时定时器
184 | if (this.timer) {
185 | logger.info("收到新的请求,清除超时定时器");
186 | clearTimeout(this.timer);
187 | }
188 | // 设置定时器,超过10秒没有收到请求,主动断开连接
189 | this.timer = setTimeout(() => {
190 | if (this.ws && this.ws.readyState == WebSocket.OPEN) {
191 | this.ws.close(1000);
192 | this.timer = null;
193 | }
194 | }, 10000);
195 |
196 | let data = await Promise.race([
197 | result,
198 | new Promise((resolve, reject) => {
199 | // 如果超过 20 秒没有返回结果,则清除请求并返回超时
200 | setTimeout(() => {
201 | this.executorMap.delete(requestId);
202 | this.bufferMap.delete(requestId);
203 | reject("转换超时");
204 | }, 10000);
205 | }),
206 | ]);
207 | return data;
208 | }
209 | }
210 |
211 | const service = new Service();
212 | const retry = async function (fn, times, errorFn, failedMessage) {
213 | let reason = {
214 | message: failedMessage ?? "多次尝试后失败",
215 | errors: [],
216 | };
217 | for (let i = 0; i < times; i++) {
218 | try {
219 | return await fn();
220 | } catch (error) {
221 | if (errorFn) {
222 | errorFn(i, error);
223 | }
224 | reason.errors.push(error);
225 | }
226 | }
227 | throw reason;
228 | };
229 |
230 | const ra = async (text) => {
231 | try {
232 | // let format = "webm-24khz-16bit-mono-opus";
233 | let format = "audio-24khz-48kbitrate-mono-mp3";
234 | if (Array.isArray(format)) {
235 | throw `无效的音频格式:${format}`;
236 | }
237 | if (!FORMAT_CONTENT_TYPE.has(format)) {
238 | throw `无效的音频格式:${format}`;
239 | }
240 |
241 | let ssml = text;
242 | if (ssml == null) {
243 | throw `转换参数无效`;
244 | }
245 | let result = await retry(
246 | async () => {
247 | let result = await service.convert(ssml, format);
248 | return result;
249 | },
250 | 3,
251 | (index, error) => {
252 | logger.error(`第${index}次转换失败:${error}`);
253 | },
254 | "服务器多次尝试后转换失败"
255 | );
256 | return result;
257 | // response.sendDate = true;
258 | // response
259 | // .status(200)
260 | // .setHeader("Content-Type", FORMAT_CONTENT_TYPE.get(format));
261 | // response.end(result);
262 | } catch (error) {
263 | logger.error(`发生错误, ${error.message}`);
264 | // response.status(503).json(error);
265 | }
266 | };
267 |
268 | // ra(
269 | // ` 如果喜欢这个项目的话请点个 Star 吧。 `
270 | // );
271 | export default ra;
--------------------------------------------------------------------------------
/electron/utils/gpt-api.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from 'openai';
2 | import logger from "../utils/log";
3 |
4 | const gptApi = async (promptGPT: string, model: string, key: string) => {
5 | logger.info(`promptGPT: ${promptGPT}`);
6 | const openai = new OpenAI({
7 | apiKey: key,
8 | });
9 | logger.info(`model: ${model}`);
10 | const chatCompletion = await openai.chat.completions.create({
11 | messages: [{ role: 'user', content: promptGPT }],
12 | model: model,
13 | });
14 | logger.info(`chatCompletion: ${JSON.stringify(chatCompletion)}`);
15 |
16 | return chatCompletion.choices[0].message.content;
17 | }
18 |
19 | export { gptApi };
--------------------------------------------------------------------------------
/electron/utils/log.ts:
--------------------------------------------------------------------------------
1 | import logger from "electron-log";
2 | import { app } from "electron";
3 |
4 | logger.transports.file.level = "debug";
5 | logger.transports.file.maxSize = 1002430; // 10M
6 | logger.transports.file.format =
7 | "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}";
8 | const date = new Date();
9 | const now_date =
10 | date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
11 | const logFolder = app.getPath("userData") + "\\logs\\";
12 | logger.transports.file.file = logFolder + now_date + ".log";
13 | logger.transports.console.level = false;
14 | export default {
15 | logger,
16 | logFolder,
17 | info(param) {
18 | logger.info(param);
19 | },
20 | warn(param) {
21 | logger.warn(param);
22 | },
23 | error(param) {
24 | logger.error(param);
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Vite App
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tts-vue",
3 | "version": "1.9.15",
4 | "main": "dist/electron/main/index.js",
5 | "description": "🎤 微软语音合成工具,使用 Electron + Vue + ElementPlus + Vite 构建。",
6 | "author": "沫離Loker ",
7 | "license": "MIT",
8 | "private": true,
9 | "scripts": {
10 | "dev": "vite",
11 | "build": "vue-tsc --noEmit && vite build && electron-builder"
12 | },
13 | "engines": {
14 | "node": ">=14.17.0"
15 | },
16 | "devDependencies": {
17 | "@vitejs/plugin-vue": "2.3.3",
18 | "electron": "19.1.9",
19 | "electron-builder": "23.1.0",
20 | "typescript": "4.7.4",
21 | "vite": "2.9.13",
22 | "vite-plugin-electron": "0.8.1",
23 | "vue": "3.2.37",
24 | "vue-tsc": "0.38.3"
25 | },
26 | "env": {
27 | "VITE_DEV_SERVER_HOST": "127.0.0.1",
28 | "VITE_DEV_SERVER_PORT": 3344
29 | },
30 | "keywords": [
31 | "electron",
32 | "rollup",
33 | "vite",
34 | "vue3",
35 | "vue"
36 | ],
37 | "dependencies": {
38 | "@ffmpeg-installer/ffmpeg": "1.1.0",
39 | "@types/ws": "8.5.4",
40 | "axios": "0.27.2",
41 | "electron-log": "4.4.8",
42 | "electron-store": "8.0.2",
43 | "element-plus": "2.2.9",
44 | "fluent-ffmpeg": "2.1.2",
45 | "microsoft-cognitiveservices-speech-sdk": "1.30.1",
46 | "nodejs-websocket": "1.7.2",
47 | "openai": "^4.0.0",
48 | "pinia": "2.0.17",
49 | "uuid": "8.3.2",
50 | "vue-i18n": "9.6.5",
51 | "ws": "8.13.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/electron-vite-vue.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LokerL/tts-vue/23579190646e9ff4b3c4b61a1348baf488ae7002/public/electron-vite-vue.gif
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LokerL/tts-vue/23579190646e9ff4b3c4b61a1348baf488ae7002/public/favicon.ico
--------------------------------------------------------------------------------
/public/node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LokerL/tts-vue/23579190646e9ff4b3c4b61a1348baf488ae7002/public/node.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
69 |
--------------------------------------------------------------------------------
/src/assets/electron.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LokerL/tts-vue/23579190646e9ff4b3c4b61a1348baf488ae7002/src/assets/electron.png
--------------------------------------------------------------------------------
/src/assets/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | // src/assets/i18n/i18n.ts
2 | import { createI18n } from 'vue-i18n';
3 |
4 | const messages = {
5 | en: {
6 | // Mensajes en inglés
7 | aside: {
8 | text: "Text",
9 | batch: "Batch",
10 | settings: "Settings",
11 | documents: "Documents",
12 | },
13 | version: {
14 | checkUpdate: "Check for updates",
15 | currentVersion: "Current Version:",
16 | latestVersion: "Latest Version:",
17 | updateAvailable: "Update Available",
18 | noUpdate: "You are up to date!",
19 | updateInfo: "Update Information",
20 | confirm: "OK",
21 | downloadLinks: "Download Links",
22 | password: "Password: em1n",
23 | },
24 | bilibtn: {
25 | goToBilibili: "Go to Bilibili",
26 | },
27 | configPage: {
28 | downloadPath: "Download Path",
29 | retryCount: "Retry Count",
30 | retryInterval: "Retry Interval (s)",
31 | speechKey: "SpeechKey Azure",
32 | serviceRegion: "ServiceRegion Azure",
33 | autoplay: "Autoplay",
34 | language: "Language",
35 | updateNotification: "Update Notification",
36 | titleStyle: "Title Bar Style",
37 | auditionText: "Audition Text",
38 | templateEdit: "Template Edit",
39 | name: "Name",
40 | action: "Action",
41 | delete: "Delete",
42 | refreshConfig: "Refresh Configuration",
43 | configFile: "Configuration File",
44 | openLogs: "Open Logs",
45 | clearLogs: "Clear Logs",
46 | yes: "Yes",
47 | no: "No",
48 | serviceRegionPlaceHolder: "Fill in the service region, such as: westus",
49 | confirm: "OK",
50 | voice: "Voice",
51 | style: "Style",
52 | role: "Role",
53 | speed: "Speed",
54 | pitch: "Pitch",
55 | remove: "Remove",
56 | openAIKey: "OpenAI Key",
57 | gptModel: "Model GPT",
58 | // Otras traducciones...
59 | },
60 | donate: {
61 | appreciation: "If you think this project is good,",
62 | encouragement:
63 | "Feel free to Star, Fork, and PR. Your Star is the best encouragement for the author :)",
64 | guideReminder:
65 | 'If you encounter any problems, please read carefully the "Documentation" → "User Guide" section, including "Feature Introduction" and "FAQ".',
66 | feedback:
67 | 'For other opinions or suggestions, you can @mention or privately chat with the group owner or manager in "Documentation" → "Join Q Group", or raise issues on GitHub or Gitee.',
68 | buyCoffeeTitle: "Buy the author a coffee 🍻",
69 | wechatPayment: "Use WeChat for payment",
70 | hoverForAlipay: "Hover for Alipay payment",
71 | buyDrinkTitle: "Buy the author a drink ☕️",
72 | alipayPayment: "Use Alipay for payment",
73 | hoverForWechat: "Move the mouse away for WeChat payment",
74 | },
75 |
76 | footer: {
77 | downloadAudio: "Download Audio",
78 | format: "Format",
79 | // Otras traducciones...
80 | },
81 | styles: {
82 | assistant: "Assistant",
83 | chat: "Chat",
84 | customerservice: "Customer Service",
85 | newscast: "Newscast",
86 | affectionate: "Affectionate",
87 | angry: "Angry",
88 | calm: "Calm",
89 | cheerful: "Cheerful",
90 | disgruntled: "Disgruntled",
91 | fearful: "Fearful",
92 | gentle: "Gentle",
93 | lyrical: "Lyrical",
94 | sad: "Sad",
95 | serious: "Serious",
96 | "poetry-reading": "Poetry Reading",
97 | "narration-professional": "Professional Narration",
98 | "newscast-casual": "Casual Newscast",
99 | embarrassed: "Embarrassed",
100 | depressed: "Depressed",
101 | envious: "Envious",
102 | "narration-relaxed": "Relaxed Narration",
103 | Advertisement_upbeat: "Upbeat Advertisement",
104 | "Narration-relaxed": "Relaxed Narration",
105 | Sports_commentary: "Sports Commentary",
106 | Sports_commentary_excited: "Excited Sports Commentary",
107 | "documentary-narration": "Documentary Narration",
108 | excited: "Excited",
109 | friendly: "Friendly",
110 | terrified: "Terrified",
111 | shouting: "Shouting",
112 | unfriendly: "Unfriendly",
113 | whispering: "Whispering",
114 | hopeful: "Hopeful",
115 | },
116 | roles: {
117 | YoungAdultFemale: "Young Adult Female",
118 | YoungAdultMale: "Young Adult Male",
119 | OlderAdultFemale: "Older Adult Female",
120 | OlderAdultMale: "Older Adult Male",
121 | SeniorFemale: "Senior Female",
122 | SeniorMale: "Senior Male",
123 | Girl: "Girl",
124 | Boy: "Boy",
125 | Narrator: "Narrator",
126 | },
127 | main: {
128 | titleGenerateTextGPT: "Generate Text with GPT",
129 | descriptionGenerateTextGPT:
130 | "Generate text with GPT-3 or GPT-4, the most powerful AI model in the world.",
131 | placeholderGPT: "Please enter the prompt text",
132 | action: "Action",
133 | textTab: "Text",
134 | ssmlTab: "SSML",
135 | placeholder: "Please input",
136 | fileName: "File Name",
137 | filePath: "File Path",
138 | fileSize: "Word Count",
139 | status: "Status",
140 | ready: "Ready",
141 | remove: "Remove",
142 | play: "Play",
143 | openInFolder: "Open in Folder",
144 | selectFiles: "Select Files",
145 | fileFormatTip: "The format of text: *.txt",
146 | clearAll: "Clear All",
147 | doc: "Documentation",
148 | // Otros textos...
149 | },
150 | options: {
151 | api: "Interface",
152 | selectApi: "Select API",
153 | language: "Language",
154 | selectLanguage: "Select Language",
155 | voice: "Voice",
156 | selectVoice: "Select Voice",
157 | speakingStyle: "Speaking Style",
158 | selectSpeakingStyle: "Select Speaking Style",
159 | rolePlaying: "Role Playing",
160 | selectRole: "Select Role",
161 | speed: "Speed",
162 | pitch: "Pitch",
163 | saveConfig: "Save Configuration",
164 | selectConfig: "Select Configuration",
165 | startConversion: "Start Conversion",
166 | edgeApiWarning:
167 | "The Edge interface does not support automatic slicing and the maximum text length is unknown. Please preprocess text manually as needed.",
168 | configureAzure:
169 | "Please configure Azure's Speech service key and region first.",
170 | saveSuccess: "Configuration saved successfully.",
171 | cancelSave: "Save cancelled.",
172 | inputWarning: "Please enter text content.",
173 | emptyListWarning: "The list is empty.",
174 | waitMessage: "Please wait...",
175 | },
176 | lang: {
177 | AF_ZA: "Afrikaans (South Africa)",
178 | AM_ET: "Amharic (Ethiopia)",
179 | AR_AE: "Arabic (United Arab Emirates)",
180 | AR_BH: "Arabic (Bahrain)",
181 | AR_DZ: "Arabic (Algeria)",
182 | AR_EG: "Arabic (Egypt)",
183 | AR_IL: "Arabic (Israel)",
184 | AR_IQ: "Arabic (Iraq)",
185 | AR_JO: "Arabic (Jordan)",
186 | AR_KW: "Arabic (Kuwait)",
187 | AR_LB: "Arabic (Lebanon)",
188 | AR_LY: "Arabic (Libya)",
189 | AR_MA: "Arabic (Morocco)",
190 | AR_OM: "Arabic (Oman)",
191 | AR_PS: "Arabic (Palestinian Authority)",
192 | AR_QA: "Arabic (Qatar)",
193 | AR_SA: "Arabic (Saudi Arabia)",
194 | AR_SY: "Arabic (Syria)",
195 | AR_TN: "Arabic (Tunisia)",
196 | AR_YE: "Arabic (Yemen)",
197 | AS_IN: "Assamese (India)",
198 | AZ_AZ: "Azerbaijani (Azerbaijan)",
199 | BG_BG: "Bulgarian (Bulgaria)",
200 | BN_BD: "Bengali (Bangladesh)",
201 | BN_IN: "Bengali (India)",
202 | BS_BA: "Bosnian (Bosnia and Herzegovina)",
203 | CA_ES: "Catalan (Spain)",
204 | CS_CZ: "Czech (Czech Republic)",
205 | CY_GB: "Welsh (United Kingdom)",
206 | DA_DK: "Danish (Denmark)",
207 | DE_AT: "German (Austria)",
208 | DE_CH: "German (Switzerland)",
209 | DE_DE: "German (Germany)",
210 | EL_GR: "Greek (Greece)",
211 | EN_AU: "English (Australia)",
212 | EN_CA: "English (Canada)",
213 | EN_GB: "English (United Kingdom)",
214 | EN_GH: "English (Ghana)",
215 | EN_HK: "English (Hong Kong SAR)",
216 | EN_IE: "English (Ireland)",
217 | EN_IN: "English (India)",
218 | EN_KE: "English (Kenya)",
219 | EN_NG: "English (Nigeria)",
220 | EN_NZ: "English (New Zealand)",
221 | EN_PH: "English (Philippines)",
222 | EN_SG: "English (Singapore)",
223 | EN_TZ: "English (Tanzania)",
224 | EN_US: "English (United States)",
225 | EN_ZA: "English (South Africa)",
226 | ES_AR: "Spanish (Argentina)",
227 | ES_BO: "Spanish (Bolivia)",
228 | ES_CL: "Spanish (Chile)",
229 | ES_CO: "Spanish (Colombia)",
230 | ES_CR: "Spanish (Costa Rica)",
231 | ES_CU: "Spanish (Cuba)",
232 | ES_DO: "Spanish (Dominican Republic)",
233 | ES_EC: "Spanish (Ecuador)",
234 | ES_ES: "Spanish (Spain)",
235 | ES_GQ: "Spanish (Equatorial Guinea)",
236 | ES_GT: "Spanish (Guatemala)",
237 | ES_HN: "Spanish (Honduras)",
238 | ES_MX: "Spanish (Mexico)",
239 | ES_NI: "Spanish (Nicaragua)",
240 | ES_PA: "Spanish (Panama)",
241 | ES_PE: "Spanish (Peru)",
242 | ES_PR: "Spanish (Puerto Rico)",
243 | ES_PY: "Spanish (Paraguay)",
244 | ES_SV: "Spanish (El Salvador)",
245 | ES_US: "Spanish (United States)",
246 | ES_UY: "Spanish (Uruguay)",
247 | ES_VE: "Spanish (Venezuela)",
248 | ET_EE: "Estonian (Estonia)",
249 | EU_ES: "Basque (Basque)",
250 | FA_IR: "Persian (Iran)",
251 | FIL_PH: "Filipino (Philippines)",
252 | FI_FI: "Finnish (Finland)",
253 | FR_BE: "French (Belgium)",
254 | FR_CA: "French (Canada)",
255 | FR_CH: "French (Switzerland)",
256 | FR_FR: "French (France)",
257 | GA_IE: "Irish (Ireland)",
258 | GL_ES: "Galician (Galicia)",
259 | GU_IN: "Gujarati (India)",
260 | HE_IL: "Hebrew (Israel)",
261 | HI_IN: "Hindi (India)",
262 | HR_HR: "Croatian (Croatia)",
263 | HU_HU: "Hungarian (Hungary)",
264 | HY_AM: "Armenian (Armenia)",
265 | ID_ID: "Indonesian (Indonesia)",
266 | IS_IS: "Icelandic (Iceland)",
267 | IT_CH: "Italian (Switzerland)",
268 | IT_IT: "Italian (Italy)",
269 | JA_JP: "Japanese (Japan)",
270 | JV_ID: "Javanese (Indonesia)",
271 | KA_GE: "Georgian (Georgia)",
272 | KK_KZ: "Kazakh (Kazakhstan)",
273 | KM_KH: "Khmer (Cambodia)",
274 | KN_IN: "Kannada (India)",
275 | KO_KR: "Korean (South Korea)",
276 | LO_LA: "Lao (Laos)",
277 | LT_LT: "Lithuanian (Lithuania)",
278 | LV_LV: "Latvian (Latvia)",
279 | MK_MK: "Macedonian (North Macedonia)",
280 | ML_IN: "Malayalam (India)",
281 | MN_MN: "Mongolian (Mongolia)",
282 | MR_IN: "Marathi (India)",
283 | MS_MY: "Malay (Malaysia)",
284 | MT_MT: "Maltese (Malta)",
285 | MY_MM: "Burmese (Myanmar)",
286 | NB_NO: "Norwegian Bokmål (Norway)",
287 | NE_NP: "Nepali (Nepal)",
288 | NL_BE: "Dutch (Belgium)",
289 | NL_NL: "Dutch (Netherlands)",
290 | OR_IN: "Odia (India)",
291 | PA_IN: "Punjabi (India)",
292 | PL_PL: "Polish (Poland)",
293 | PS_AF: "Pashto (Afghanistan)",
294 | PT_BR: "Portuguese (Brazil)",
295 | PT_PT: "Portuguese (Portugal)",
296 | RO_MD: "Romanian (Moldova)",
297 | RO_RO: "Romanian (Romania)",
298 | RU_RU: "Russian (Russia)",
299 | SI_LK: "Sinhala (Sri Lanka)",
300 | SK_SK: "Slovak (Slovakia)",
301 | SL_SI: "Slovenian (Slovenia)",
302 | SO_SO: "Somali (Somalia)",
303 | SQ_AL: "Albanian (Albania)",
304 | SR_ME: "Serbian (Cyrillic, Montenegro)",
305 | SR_RS: "Serbian (Serbia)",
306 | SR_XK: "Serbian (Cyrillic, Kosovo)",
307 | SU_ID: "Sundanese (Indonesia)",
308 | SV_SE: "Swedish (Sweden)",
309 | SW_KE: "Swahili (Kenya)",
310 | SW_TZ: "Swahili (Tanzania)",
311 | TA_IN: "Tamil (India)",
312 | TA_LK: "Tamil (Sri Lanka)",
313 | TA_MY: "Tamil (Malaysia)",
314 | TA_SG: "Tamil (Singapore)",
315 | TE_IN: "Telugu (India)",
316 | TH_TH: "Thai (Thailand)",
317 | TR_TR: "Turkish (Turkey)",
318 | UK_UA: "Ukrainian (Ukraine)",
319 | UR_IN: "Urdu (India)",
320 | UR_PK: "Urdu (Pakistan)",
321 | UZ_UZ: "Uzbek (Uzbekistan)",
322 | VI_VN: "Vietnamese (Vietnam)",
323 | WUU_CN: "Chinese (Wu, Simplified)",
324 | X_CUSTOM: "Custom Language",
325 | YUE_CN: "Chinese (Cantonese, Simplified)",
326 | ZH_CN: "Chinese (Mandarin, Simplified)",
327 | ZH_CN_Bilingual: "Chinese (Mandarin, Simplified), English Bilingual",
328 | ZH_CN_HENAN: "Chinese (Central Plains Henan, Simplified)",
329 | ZH_CN_LIAONING: "Chinese (Northeastern Mandarin, Simplified)",
330 | ZH_CN_SHAANXI: "Chinese (Central Plains Shaanxi, Simplified)",
331 | ZH_CN_SHANDONG: "Chinese (Ji–Lu Mandarin, Simplified)",
332 | ZH_CN_SICHUAN: "Chinese (Southwestern Mandarin, Simplified)",
333 | ZH_HK: "Chinese (Cantonese, Traditional)",
334 | ZH_TW: "Chinese (Taiwan Mandarin)",
335 | ZU_ZA: "Zulu (South Africa)",
336 | nalytics: "Language analysis",
337 | onversationAnalysisPreviewHint:
338 | "The call summary is currently a closed public preview and is only available for approved resources.",
339 | fAudio: "Language of audio",
340 | esource: "Language resource",
341 | echnologiesUsed: "Language technologies used",
342 | InPreview: "Language in preview",
343 | },
344 | initialLocalStore: {
345 | audition:
346 | "If you think this project is good, Star, Fork and PR are welcome. Your Star is the best encouragement to the author.",
347 | },
348 | },
349 | es: {
350 | // Mensajes en español
351 | aside: {
352 | text: "Texto",
353 | batch: "Lote",
354 | settings: "Configuración",
355 | documents: "Documentos",
356 | },
357 | version: {
358 | checkUpdate: "Buscar actualizaciones",
359 | currentVersion: "Versión Actual:",
360 | latestVersion: "Última Versión:",
361 | updateAvailable: "Actualización disponible",
362 | noUpdate: "¡Estás actualizado!",
363 | updateInfo: "Información de la actualización",
364 | confirm: "OK",
365 | downloadLinks: "Enlaces de Descarga",
366 | password: "Contraseña: em1n",
367 | },
368 | bilibtn: {
369 | goToBilibili: "Ir a Bilibili",
370 | },
371 | donate: {
372 | appreciation: "Si piensas que este proyecto es bueno,",
373 | encouragement:
374 | "No dudes en dar Star, hacer Fork y PR. Tu Star es el mejor ánimo para el autor :)",
375 | guideReminder:
376 | 'Si encuentras algún problema, por favor lee detenidamente la sección "Documentación" → "Guía del Usuario", incluyendo "Introducción de Funciones" y "Preguntas Frecuentes".',
377 | feedback:
378 | 'Para otras opiniones o sugerencias, puedes mencionar o chatear en privado con el dueño del grupo o el administrador en "Documentación" → "Unirse al Grupo Q", o plantear problemas en GitHub o Gitee.',
379 | buyCoffeeTitle: "Compra al autor un café 🍻",
380 | wechatPayment: "Usa WeChat para el pago",
381 | hoverForAlipay: "Pasa el ratón para pagar con Alipay",
382 | buyDrinkTitle: "Compra al autor una bebida ☕️",
383 | alipayPayment: "Usa Alipay para el pago",
384 | hoverForWechat: "Aleja el ratón para usar WeChat para el pago",
385 | },
386 | configPage: {
387 | downloadPath: "Ruta de Descarga",
388 | retryCount: "Número de Intentos",
389 | retryInterval: "Intervalo de Reintentos (s)",
390 | speechKey: "SpeechKey Azure",
391 | serviceRegion: "ServiceRegion Azure",
392 | language: "Idioma",
393 | autoplay: "Reproducción Automática",
394 | updateNotification: "Notificación de Actualización",
395 | titleStyle: "Estilo de la Barra de Título",
396 | auditionText: "Texto de Audición",
397 | templateEdit: "Edición de Plantilla",
398 | name: "Nombre",
399 | action: "Acción",
400 | delete: "Eliminar",
401 | refreshConfig: "Refrescar Configuración",
402 | configFile: "Archivo Configuración",
403 | openLogs: "Abrir Registros",
404 | clearLogs: "Limpiar Registros",
405 | yes: "Sí",
406 | no: "No",
407 | serviceRegionPlaceHolder:
408 | "Complete la región de servicio, como por ejemplo: westus",
409 | confirm: "OK",
410 | voice: "Voz",
411 | style: "Estilo",
412 | role: "Rol",
413 | speed: "Velocidad",
414 | pitch: "Tono",
415 | remove: "Eliminar",
416 | openAIKey: "OpenAI key",
417 | gptModel: "Modelo GPT",
418 | // Otras traducciones...
419 | },
420 | footer: {
421 | downloadAudio: "Descargar Audio",
422 | format: "Formato",
423 | // Otras traducciones...
424 | },
425 | styles: {
426 | assistant: "Asistente",
427 | chat: "Charla",
428 | customerservice: "Servicio al Cliente",
429 | newscast: "Noticiero",
430 | affectionate: "Cariñoso",
431 | angry: "Enojado",
432 | calm: "Tranquilo",
433 | cheerful: "Alegre",
434 | disgruntled: "Disgustado",
435 | fearful: "Temeroso",
436 | gentle: "Suave",
437 | lyrical: "Lírico",
438 | sad: "Triste",
439 | serious: "Serio",
440 | "poetry-reading": "Lectura de Poesía",
441 | "narration-professional": "Narración Profesional",
442 | "newscast-casual": "Noticiero Informal",
443 | embarrassed: "Avergonzado",
444 | depressed: "Deprimido",
445 | envious: "Envidioso",
446 | "narration-relaxed": "Narración Relajada",
447 | Advertisement_upbeat: "Publicidad Optimista",
448 | "Narration-relaxed": "Narración Relajada",
449 | Sports_commentary: "Comentario Deportivo",
450 | Sports_commentary_excited: "Comentario Deportivo Emocionado",
451 | "documentary-narration": "Narración de Documentales",
452 | excited: "Emocionado",
453 | friendly: "Amigable",
454 | terrified: "Aterrorizado",
455 | shouting: "Gritando",
456 | unfriendly: "Antipático",
457 | whispering: "Susurrando",
458 | hopeful: "Esperanzado",
459 | },
460 | roles: {
461 | YoungAdultFemale: "Mujer Joven Adulta",
462 | YoungAdultMale: "Hombre Joven Adulto",
463 | OlderAdultFemale: "Mujer Adulta Mayor",
464 | OlderAdultMale: "Hombre Adulto Mayor",
465 | SeniorFemale: "Mujer Senior",
466 | SeniorMale: "Hombre Senior",
467 | Girl: "Niña",
468 | Boy: "Niño",
469 | Narrator: "Narrador",
470 | },
471 | main: {
472 | titleGenerateTextGPT: "Genera Texto con GPT",
473 | descriptionGenerateTextGPT:
474 | "Genera texto con GPT-3 o GPT-4, el modelo de IA más potente del mundo.",
475 | placeholderGPT: "Por favor ingrese el texto de la sugerencia",
476 | action: "Acción",
477 | textTab: "Texto",
478 | ssmlTab: "SSML",
479 | placeholder: "Por favor ingrese",
480 | fileName: "Nombre de Archivo",
481 | filePath: "Ruta de Archivo",
482 | fileSize: "Palabras",
483 | fileFormatTip: "El formato de texto: *.txt",
484 | status: "Estado",
485 | ready: "Listo",
486 | remove: "Eliminar",
487 | play: "Reproducir",
488 | openInFolder: "Abrir en Carpeta",
489 | selectFiles: "Seleccionar Archivos",
490 | clearAll: "Limpiar Todo",
491 | doc: "Documentación",
492 | // Otros textos...
493 | },
494 | options: {
495 | api: "Interfaz",
496 | selectApi: "Seleccionar API",
497 | language: "Idioma",
498 | selectLanguage: "Seleccionar Idioma",
499 | voice: "Voz",
500 | selectVoice: "Seleccionar Voz",
501 | speakingStyle: "Estilo de Habla",
502 | selectSpeakingStyle: "Seleccionar Estilo de Habla",
503 | rolePlaying: "Juego de Roles",
504 | selectRole: "Seleccionar Rol",
505 | speed: "Velocidad",
506 | pitch: "Tono",
507 | saveConfig: "Guardar Configuración",
508 | selectConfig: "Seleccionar Configuración",
509 | startConversion: "Iniciar Conversión",
510 | edgeApiWarning:
511 | "La interfaz de Edge no admite el corte automático y la longitud máxima del texto es desconocida. Por favor, procese manualmente el texto según sea necesario.",
512 | configureAzure:
513 | "Por favor, configure primero la clave y la región del servicio de voz de Azure.",
514 | saveSuccess: "Configuración guardada con éxito.",
515 | cancelSave: "Guardado cancelado.",
516 | inputWarning: "Por favor, introduzca el contenido del texto.",
517 | emptyListWarning: "La lista está vacía.",
518 | },
519 | lang: {
520 | AF_ZA: "Afrikáans (Sudáfrica)",
521 | AM_ET: "Amárico (Etiopía)",
522 | AR_AE: "Árabe (Emiratos Árabes Unidos)",
523 | AR_BH: "árabe (Bahrein)",
524 | AR_DZ: "árabe (Argelia)",
525 | AR_EG: "Árabe (Egipto)",
526 | AR_IL: "árabe (Israel)",
527 | AR_IQ: "árabe (Irak)",
528 | AR_JO: "árabe (Jordania)",
529 | AR_KW: "árabe (Kuwait)",
530 | AR_LB: "Árabe (Líbano)",
531 | AR_LY: "árabe (Libia)",
532 | AR_MA: "árabe (Marruecos)",
533 | AR_OM: "árabe (Omán)",
534 | AR_PS: "Árabe (Autoridad Palestina)",
535 | AR_QA: "árabe (Qatar)",
536 | AR_SA: "Árabe (Arabia Saudita)",
537 | AR_SY: "árabe (sirio)",
538 | AR_TN: "árabe (Túnez)",
539 | AR_YE: "árabe (Yemen)",
540 | AS_IN: "Asamés (India)",
541 | AZ_AZ: "Azerbaiyán (Azerbaiyán)",
542 | BG_BG: "búlgaro (Bulgaria)",
543 | BN_BD: "bengalí (bengalí)",
544 | BN_IN: "bengalí (India)",
545 | BS_BA: "bosnio (Bosnia y Herzegovina)",
546 | CA_ES: "Catalán (España)",
547 | CS_CZ: "Checo(Checo)",
548 | CY_GB: "Galés (Reino Unido)",
549 | DA_DK: "danés (Dinamarca)",
550 | DE_AT: "alemán (Austria)",
551 | DE_CH: "Alemán (Suiza)",
552 | DE_DE: "Alemán (Alemania)",
553 | EL_GR: "Griego (Grecia)",
554 | EN_AU: "Inglés (Australia)",
555 | EN_CA: "Inglés (Canadá)",
556 | EN_GB: "Inglés (Reino Unido)",
557 | EN_GH: "Inglés (Ghana)",
558 | EN_HK: "Inglés (RAE de Hong Kong)",
559 | EN_IE: "inglés (Irlanda)",
560 | EN_IN: "Inglés (India)",
561 | EN_KE: "Inglés (Kenia)",
562 | EN_NG: "Inglés (Nigeria)",
563 | EN_NZ: "Inglés (Nueva Zelanda)",
564 | EN_PH: "Inglés (Filipinas)",
565 | EN_SG: "Inglés (Singapur)",
566 | EN_TZ: "Inglés (Tanzania)",
567 | EN_US: "Inglés (Estados Unidos)",
568 | EN_ZA: "Inglés (Sudáfrica)",
569 | ES_AR: "Español (Argentina)",
570 | ES_BO: "Español (Bolivia)",
571 | ES_CL: "Español (Chile)",
572 | ES_CO: "Español (Colombia)",
573 | ES_CR: "Español (Costa Rica)",
574 | ES_CU: "Español (Cuba)",
575 | ES_DO: "Español (República Dominicana)",
576 | ES_EC: "Español (Ecuador)",
577 | ES_ES: "Español (España)",
578 | ES_GQ: "Español (Guinea Ecuatorial)",
579 | ES_GT: "Español (Guatemala)",
580 | ES_HN: "Español(Honduras)",
581 | ES_MX: "Español (México)",
582 | ES_NI: "Español(Nicaragua)",
583 | ES_PA: "Español (Panamá)",
584 | ES_PE: "Español (Perú)",
585 | ES_PR: "Español (Puerto Rico)",
586 | ES_PY: "Español(Paraguay)",
587 | ES_SV: "Español(El Salvador)",
588 | ES_US: "Español (Estados Unidos)",
589 | ES_UY: "Español (Uruguay)",
590 | ES_VE: "Español (Venezuela)",
591 | ET_EE: "Estonio (Estonia)",
592 | EU_ES: "euskera (euskera)",
593 | FA_IR: "persa (Irán)",
594 | FIL_PH: "Filipino (Filipinas)",
595 | FI_FI: "finlandés (Finlandia)",
596 | FR_BE: "Francés (Bélgica)",
597 | FR_CA: "Francés (Canadá)",
598 | FR_CH: "Francés (Suiza)",
599 | FR_FR: "Francés (Francia)",
600 | GA_IE: "irlandés (Irlanda)",
601 | GL_ES: "gallego (gallego)",
602 | GU_IN: "Gujarati (India)",
603 | HE_IL: "hebreo (Israel)",
604 | HI_IN: "Hindi(India)",
605 | HR_HR: "croata (croata)",
606 | HU_HU: "húngaro (Hungría)",
607 | HY_AM: "armenio (armenio)",
608 | ID_ID: "indonesio (Indonesia)",
609 | IS_IS: "islandés (Islandia)",
610 | IT_CH: "Italiano (Suiza)",
611 | IT_IT: "Italiano (Italia)",
612 | JA_JP: "japonés (Japón)",
613 | JV_ID: "javanés (Indonesia)",
614 | KA_GE: "georgiano (Georgia)",
615 | KK_KZ: "Kazajo (Kazajstán)",
616 | KM_KH: "jemer (Camboya)",
617 | KN_IN: "Canarés (India)",
618 | KO_KR: "coreano (Corea del Sur)",
619 | LO_LA: "Lao (Laos)",
620 | LT_LT: "lituano (Lituania)",
621 | LV_LV: "Letón (letón)",
622 | MK_MK: "Macedonio (Macedonia del Norte)",
623 | ML_IN: "Malayalam (India)",
624 | MN_MN: "mongol (mongol)",
625 | MR_IN: "maratí (India)",
626 | MS_MY: "Malayo (Malasia)",
627 | MT_MT: "Maltés (Malta)",
628 | MY_MM: "Birmano (Myanmar)",
629 | NB_NO: "Escrito en noruego (Noruega)",
630 | NE_NP: "Nepalí (Nepal)",
631 | NL_BE: "Holandés (Bélgica)",
632 | NL_NL: "Holandés (Países Bajos)",
633 | OR_IN: "Odia (India)",
634 | PA_IN: "Punjabí (India)",
635 | PL_PL: "polaco (Polonia)",
636 | PS_AF: "Pashto (Afganistán)",
637 | PT_BR: "portugués (Brasil)",
638 | PT_PT: "portugués (Portugal)",
639 | RO_MD: "rumano(Molvador)",
640 | RO_RO: "rumano (Rumania)",
641 | RU_RU: "ruso (ruso)",
642 | SI_LK: "cingalés (Sri Lanka)",
643 | SK_SK: "eslovaco (Eslovaquia)",
644 | SL_SI: "Esloveno (Eslovenia)",
645 | SO_SO: "Somalí (Somalí)",
646 | SQ_AL: "Albanés (Albania)",
647 | SR_ME: "serbio (cirílico, montenegro)",
648 | SR_RS: "serbio (serbio)",
649 | SR_XK: "serbio (cirílico, Kosovo)",
650 | SU_ID: "Sundanés (Indonesia)",
651 | SV_SE: "sueco (Suecia)",
652 | SW_KE: "Suajili (Kenia)",
653 | SW_TZ: "Suajili (Tanzania)",
654 | TA_IN: "Tamil (India)",
655 | TA_LK: "Tamil (Sri Lanka)",
656 | TA_MY: "Tamil (Malasia)",
657 | TA_SG: "Tamil (Singapur)",
658 | TE_IN: "Telugu (India)",
659 | TH_TH: "Tailandés (Tailandia)",
660 | TR_TR: "Turco (Türkiye)",
661 | UK_UA: "ucraniano (ucraniano)",
662 | UR_IN: "Urdu (India)",
663 | UR_PK: "Urdu (Pakistán)",
664 | UZ_UZ: "uzbeko (Uzbekistán)",
665 | VI_VN: "vietnamita (Vietnam)",
666 | WUU_CN: "Chino (dialecto Wu, simplificado)",
667 | X_CUSTOM: "Idioma personalizado",
668 | YUE_CN: "Chino (cantonés, simplificado)",
669 | ZH_CN: "Chino (mandarín, simplificado)",
670 | ZH_CN_Bilingual: "Chino (mandarín, simplificado), inglés bilingüe",
671 | ZH_CN_HENAN:
672 | "Chino (mandarín Henan de las llanuras centrales, simplificado)",
673 | ZH_CN_LIAONING: "Chino (mandarín nororiental, simplificado)",
674 | ZH_CN_SHAANXI: "Chino (chino mandarín Shaanxi, simplificado)",
675 | ZH_CN_SHANDONG: "Chino (Jilu Mandarín, simplificado)",
676 | ZH_CN_SICHUAN: "Chino (mandarín del suroeste, simplificado)",
677 | ZH_HK: "Chino (cantonés, tradicional)",
678 | ZH_TW: "Chino (mandarín de Taiwán)",
679 | ZU_ZA: "Zulu (Sudáfrica)",
680 | nalytics: "Análisis del lenguaje",
681 | onversationAnalysisPreviewHint:
682 | "Los resúmenes de las llamadas se encuentran actualmente en vista previa pública cerrada y solo están disponibles para los recursos aprobados.",
683 | fAudio: "Idioma del audio",
684 | esource: "Recurso de idioma",
685 | echnologiesUsed: "Tecnologías lingüísticas utilizadas",
686 | InPreview: "Idioma en vista previa",
687 | },
688 | initialLocalStore: {
689 | audition:
690 | "Si piensas que este proyecto es bueno, Star, Fork y PR son bienvenidos. Tu Star es el mejor ánimo para el autor.",
691 | },
692 | },
693 | zh: {
694 | // Mensajes en chino
695 | aside: {
696 | text: "文本",
697 | batch: "批量",
698 | settings: "设置",
699 | documents: "文档",
700 | },
701 | version: {
702 | checkUpdate: "检查更新",
703 | currentVersion: "当前版本:",
704 | latestVersion: "最新版本:",
705 | updateAvailable: "有可用更新",
706 | noUpdate: "您的软件是最新的!",
707 | updateInfo: "更新信息",
708 | confirm: "确定",
709 | downloadLinks: "下载链接",
710 | password: "密码:em1n",
711 | },
712 | bilibtn: {
713 | goToBilibili: "前往三连",
714 | },
715 | configPage: {
716 | downloadPath: "下载路径",
717 | retryCount: "重试次数",
718 | retryInterval: "重试间隔(s)",
719 | speechKey: "SpeechKey Azure",
720 | serviceRegion: "ServiceRegion Azure",
721 | autoplay: "自动播放",
722 | language: "语言",
723 | updateNotification: "新版本提醒",
724 | titleStyle: "标题栏样式",
725 | auditionText: "试听文本",
726 | templateEdit: "模板编辑",
727 | name: "名字",
728 | action: "操作",
729 | delete: "删除",
730 | refreshConfig: "刷新配置",
731 | configFile: "配置文件",
732 | openLogs: "打开日志",
733 | clearLogs: "清理日志",
734 | yes: "是",
735 | no: "否",
736 | serviceRegionPlaceHolder: "请填写ServiceRegion,如:westus",
737 | confirm: "确认",
738 | voice: "语音",
739 | style: "风格",
740 | role: "角色",
741 | speed: "语速",
742 | pitch: "音调",
743 | remove: "删除",
744 | openAIKey: "OpenAIKey",
745 | gptModel: "模型",
746 | // Otras traducciones...
747 | },
748 | donate: {
749 | appreciation: "如果你觉得这个项目还不错,",
750 | encouragement: "欢迎给予Star、Fork和PR。你的Star是对作者最好的鼓励 :)",
751 | guideReminder:
752 | '使用遇到问题请仔细阅读"文档"→"使用指南"中的"功能介绍"和"常见问题"。',
753 | feedback:
754 | '其他意见或建议可以在"文档"→"加入Q群"中艾特或私聊群主或者管理,也可以在GitHub或者Gitee提出issues。',
755 | buyCoffeeTitle: "请作者喝杯咖啡 🍻",
756 | wechatPayment: "使用微信支付",
757 | hoverForAlipay: "鼠标悬停使用支付宝支付",
758 | buyDrinkTitle: "请作者喝杯饮料 ☕️",
759 | alipayPayment: "使用支付宝支付",
760 | hoverForWechat: "移开鼠标使用微信支付",
761 | },
762 | footer: {
763 | downloadAudio: "下载音频",
764 | format: "格式",
765 | // Otras traducciones...
766 | },
767 | styles: {
768 | assistant: "助手",
769 | chat: "聊天",
770 | customerservice: "客服",
771 | newscast: "新闻播报",
772 | affectionate: "深情的",
773 | angry: "愤怒的",
774 | calm: "冷静的",
775 | cheerful: "快乐的",
776 | disgruntled: "不满的",
777 | fearful: "害怕的",
778 | gentle: "温柔的",
779 | lyrical: "抒情的",
780 | sad: "悲伤的",
781 | serious: "严肃的",
782 | "poetry-reading": "诗歌朗诵",
783 | "narration-professional": "专业旁白",
784 | "newscast-casual": "随意新闻播报",
785 | embarrassed: "尴尬的",
786 | depressed: "沮丧的",
787 | envious: "嫉妒的",
788 | "narration-relaxed": "轻松旁白",
789 | Advertisement_upbeat: "积极向上的广告",
790 | "Narration-relaxed": "轻松旁白",
791 | Sports_commentary: "体育解说",
792 | Sports_commentary_excited: "激动的体育解说",
793 | "documentary-narration": "纪录片旁白",
794 | excited: "兴奋的",
795 | friendly: "友好的",
796 | terrified: "恐惧的",
797 | shouting: "大喊",
798 | unfriendly: "不友好的",
799 | whispering: "耳语",
800 | hopeful: "充满希望的",
801 | },
802 | roles: {
803 | YoungAdultFemale: "年轻成年女性",
804 | YoungAdultMale: "年轻成年男性",
805 | OlderAdultFemale: "年长成年女性",
806 | OlderAdultMale: "年长成年男性",
807 | SeniorFemale: "老年女性",
808 | SeniorMale: "老年男性",
809 | Girl: "女孩",
810 | Boy: "男孩",
811 | Narrator: "旁白",
812 | },
813 | main: {
814 | action: "操作",
815 | titleGenerateTextGPT: "生成文本GPT",
816 | descriptionGenerateTextGPT:
817 | "使用GPT-3或GPT-4,世界上最强大的AI模型,生成文本。",
818 | placeholderGPT: "请输入提示文本",
819 | textTab: "文本",
820 | ssmlTab: "SSML",
821 | placeholder: "请输入",
822 | fileName: "文件名",
823 | filePath: "文件路径",
824 | fileSize: "字数统计",
825 | fileFormatTip: "文本格式:*.txt",
826 | status: "状态",
827 | ready: "就绪",
828 | remove: "移除",
829 | play: "播放",
830 | openInFolder: "在文件夹中打开",
831 | selectFiles: "选择文件",
832 | clearAll: "清空",
833 | doc: "文档",
834 | // Otros textos...
835 | },
836 | options: {
837 | api: "接口",
838 | selectApi: "选择接口",
839 | language: "语言",
840 | selectLanguage: "选择语言",
841 | voice: "语音",
842 | selectVoice: "选择语音",
843 | speakingStyle: "说话风格",
844 | selectSpeakingStyle: "选择说话风格",
845 | rolePlaying: "角色扮演",
846 | selectRole: "选择角色",
847 | speed: "语速",
848 | pitch: "音调",
849 | saveConfig: "保存配置",
850 | selectConfig: "选择配置",
851 | startConversion: "开始转换",
852 | edgeApiWarning:
853 | "Edge接口不支持自动切片,最长支持文本长度未知。请根据自身需求手动预处理文本。",
854 | configureAzure: "请先配置Azure的Speech服务密钥和区域。",
855 | saveSuccess: "保存成功。",
856 | cancelSave: "取消保存。",
857 | inputWarning: "请输入文字内容。",
858 | emptyListWarning: "列表内容为空。",
859 | waitMessage: "请稍候...",
860 | },
861 | lang: {
862 | AF_ZA: "南非荷兰语(南非)",
863 | AM_ET: "阿姆哈拉语(埃塞俄比亚)",
864 | AR_AE: "阿拉伯语(阿拉伯联合酋长国)",
865 | AR_BH: "阿拉伯语(巴林)",
866 | AR_DZ: "阿拉伯语(阿尔及利亚)",
867 | AR_EG: "阿拉伯语(埃及)",
868 | AR_IL: "阿拉伯语(以色列)",
869 | AR_IQ: "阿拉伯语(伊拉克)",
870 | AR_JO: "阿拉伯语(约旦)",
871 | AR_KW: "阿拉伯语(科威特)",
872 | AR_LB: "阿拉伯语(黎巴嫩)",
873 | AR_LY: "阿拉伯语(利比亚)",
874 | AR_MA: "阿拉伯语(摩洛哥)",
875 | AR_OM: "阿拉伯语(阿曼)",
876 | AR_PS: "阿拉伯语(巴勒斯坦民族权力机构)",
877 | AR_QA: "阿拉伯语(卡塔尔)",
878 | AR_SA: "阿拉伯语(沙特阿拉伯)",
879 | AR_SY: "阿拉伯语(叙利亚)",
880 | AR_TN: "阿拉伯语(突尼斯)",
881 | AR_YE: "阿拉伯语(也门)",
882 | AS_IN: "阿萨姆语(印度)",
883 | AZ_AZ: "阿塞拜疆语(阿塞拜疆) ",
884 | BG_BG: "保加利亚语(保加利亚)",
885 | BN_BD: "孟加拉语(孟加拉)",
886 | BN_IN: "孟加拉语(印度)",
887 | BS_BA: "波斯尼亚语(波斯尼亚和黑塞哥维那)",
888 | CA_ES: "加泰罗尼亚语(西班牙)",
889 | CS_CZ: "捷克语(捷克)",
890 | CY_GB: "威尔士语(英国)",
891 | DA_DK: "丹麦语(丹麦)",
892 | DE_AT: "德语(奥地利)",
893 | DE_CH: "德语(瑞士)",
894 | DE_DE: "德语(德国)",
895 | EL_GR: "希腊语(希腊)",
896 | EN_AU: "英语(澳大利亚)",
897 | EN_CA: "英语(加拿大)",
898 | EN_GB: "英语(英国)",
899 | EN_GH: "英语(加纳)",
900 | EN_HK: "英语(香港特别行政区)",
901 | EN_IE: "英语(爱尔兰)",
902 | EN_IN: "英语(印度)",
903 | EN_KE: "英语(肯尼亚)",
904 | EN_NG: "英语(尼日利亚)",
905 | EN_NZ: "英语(新西兰)",
906 | EN_PH: "英语(菲律宾)",
907 | EN_SG: "英语(新加坡)",
908 | EN_TZ: "英语(坦桑尼亚)",
909 | EN_US: "英语(美国)",
910 | EN_ZA: "英语(南非)",
911 | ES_AR: "西班牙语(阿根廷)",
912 | ES_BO: "西班牙语(玻利维亚)",
913 | ES_CL: "西班牙语(智利)",
914 | ES_CO: "西班牙语(哥伦比亚)",
915 | ES_CR: "西班牙语(哥斯达黎加)",
916 | ES_CU: "西班牙语(古巴)",
917 | ES_DO: "西班牙语(多米尼加共和国)",
918 | ES_EC: "西班牙语(厄瓜多尔)",
919 | ES_ES: "西班牙语(西班牙)",
920 | ES_GQ: "西班牙语(赤道几内亚)",
921 | ES_GT: "西班牙语(危地马拉)",
922 | ES_HN: "西班牙语(洪都拉斯)",
923 | ES_MX: "西班牙语(墨西哥)",
924 | ES_NI: "西班牙语(尼加拉瓜)",
925 | ES_PA: "西班牙语(巴拿马)",
926 | ES_PE: "西班牙语(秘鲁)",
927 | ES_PR: "西班牙语(波多黎各)",
928 | ES_PY: "西班牙语(巴拉圭)",
929 | ES_SV: "西班牙语(萨尔瓦多)",
930 | ES_US: "西班牙语(美国)",
931 | ES_UY: "西班牙语(乌拉圭)",
932 | ES_VE: "西班牙语(委内瑞拉)",
933 | ET_EE: "爱沙尼亚语(爱沙尼亚)",
934 | EU_ES: "巴斯克语(巴斯克语)",
935 | FA_IR: "波斯语(伊朗)",
936 | FIL_PH: "菲律宾语(菲律宾)",
937 | FI_FI: "芬兰语(芬兰)",
938 | FR_BE: "法语(比利时)",
939 | FR_CA: "法语(加拿大)",
940 | FR_CH: "法语(瑞士)",
941 | FR_FR: "法语(法国)",
942 | GA_IE: "爱尔兰语(爱尔兰)",
943 | GL_ES: "加利西亚语(加利西亚语)",
944 | GU_IN: "古吉拉特语(印度)",
945 | HE_IL: "希伯来语(以色列)",
946 | HI_IN: "印地语(印度)",
947 | HR_HR: "克罗地亚语(克罗地亚)",
948 | HU_HU: "匈牙利语(匈牙利)",
949 | HY_AM: "亚美尼亚语(亚美尼亚)",
950 | ID_ID: "印度尼西亚语(印度尼西亚)",
951 | IS_IS: "冰岛语(冰岛)",
952 | IT_CH: "意大利语(瑞士)",
953 | IT_IT: "意大利语(意大利)",
954 | JA_JP: "日语(日本)",
955 | JV_ID: "爪哇语(印度尼西亚)",
956 | KA_GE: "格鲁吉亚语(格鲁吉亚)",
957 | KK_KZ: "哈萨克语(哈萨克斯坦)",
958 | KM_KH: "高棉语(柬埔寨)",
959 | KN_IN: "埃纳德语(印度)",
960 | KO_KR: "韩语(韩国)",
961 | LO_LA: "老挝语(老挝) ",
962 | LT_LT: "立陶宛语(立陶宛)",
963 | LV_LV: "拉脱维亚语(拉脱维亚)",
964 | MK_MK: "马其顿语(北马其顿)",
965 | ML_IN: "马拉雅拉姆语(印度)",
966 | MN_MN: "蒙古语(蒙古)",
967 | MR_IN: "马拉地语(印度)",
968 | MS_MY: "马来语(马来西亚)",
969 | MT_MT: "马耳他语(马耳他)",
970 | MY_MM: "缅甸语(缅甸)",
971 | NB_NO: "书面挪威语(挪威)",
972 | NE_NP: "尼泊尔语(尼泊尔)",
973 | NL_BE: "荷兰语(比利时)",
974 | NL_NL: "荷兰语(荷兰)",
975 | OR_IN: "奥里亚语(印度)",
976 | PA_IN: "旁遮普语(印度)",
977 | PL_PL: "波兰语(波兰)",
978 | PS_AF: "普什图语(阿富汗)",
979 | PT_BR: "葡萄牙语(巴西)",
980 | PT_PT: "葡萄牙语(葡萄牙)",
981 | RO_MD: "罗马尼亚语(摩尔瓦多)",
982 | RO_RO: "罗马尼亚语(罗马尼亚)",
983 | RU_RU: "俄语(俄罗斯)",
984 | SI_LK: "僧伽罗语(斯里兰卡)",
985 | SK_SK: "斯洛伐克语(斯洛伐克)",
986 | SL_SI: "斯洛文尼亚语(斯洛文尼亚)",
987 | SO_SO: "索马里语(索马里)",
988 | SQ_AL: "阿尔巴尼亚语(阿尔巴尼亚)",
989 | SR_ME: "塞尔维亚语(西里尔文,黑山)",
990 | SR_RS: "塞尔维亚语(塞尔维亚)",
991 | SR_XK: "塞尔维亚语(西里尔语,科索沃)",
992 | SU_ID: "巽他语(印度尼西亚)",
993 | SV_SE: "瑞典语(瑞典)",
994 | SW_KE: "斯瓦希里语(肯尼亚)",
995 | SW_TZ: "斯瓦希里语(坦桑尼亚)",
996 | TA_IN: "泰米尔语(印度)",
997 | TA_LK: "泰米尔语(斯里兰卡)",
998 | TA_MY: "泰米尔语(马来西亚)",
999 | TA_SG: "泰米尔语(新加坡)",
1000 | TE_IN: "泰卢固语(印度)",
1001 | TH_TH: "泰语(泰国)",
1002 | TR_TR: "土耳其语(Türkiye)",
1003 | UK_UA: "乌克兰语(乌克兰)",
1004 | UR_IN: "乌尔都语(印度)",
1005 | UR_PK: "乌尔都语(巴基斯坦)",
1006 | UZ_UZ: "乌兹别克语(乌兹别克斯坦)",
1007 | VI_VN: "越南语(越南)",
1008 | WUU_CN: "中文(吴语,简体)",
1009 | X_CUSTOM: "自定义语言",
1010 | YUE_CN: "中文(粤语,简体)",
1011 | ZH_CN: "中文(普通话,简体)",
1012 | ZH_CN_Bilingual: "中文(普通话,简体),英语双语",
1013 | ZH_CN_HENAN: "中文(中原官话河南,简体)",
1014 | ZH_CN_LIAONING: "中文(东北官话,简体)",
1015 | ZH_CN_SHAANXI: "中文(中原官话陕西,简体)",
1016 | ZH_CN_SHANDONG: "中文(冀鲁官话,简体)",
1017 | ZH_CN_SICHUAN: "中文(西南官话,简体)",
1018 | ZH_HK: "中文(粤语,繁体)",
1019 | ZH_TW: "中文(台湾普通话)",
1020 | ZU_ZA: "祖鲁语(南非)",
1021 | nalytics: "语言分析",
1022 | onversationAnalysisPreviewHint:
1023 | "通话摘要目前为封闭公共预览版,仅适用于已批准的资源。",
1024 | fAudio: "Language of audio",
1025 | esource: "语言资源",
1026 | echnologiesUsed: "使用的语言技术",
1027 | InPreview: "预览中的语言",
1028 | },
1029 | initialLocalStore: {
1030 | audition:
1031 | "如果你觉得这个项目还不错, 欢迎Star、Fork和PR。你的Star是对作者最好的鼓励。",
1032 | },
1033 | },
1034 | // Otros idiomas...
1035 | };
1036 | const language = process.env.LANG || "zh";
1037 | let defaultLanguage = language.substring(0, 2);
1038 | defaultLanguage = Object.keys(messages).includes(defaultLanguage)
1039 | ? defaultLanguage
1040 | : "zh";
1041 |
1042 | const i18n = createI18n({
1043 | legacy: false, // Usa la Composition API
1044 | locale: defaultLanguage, // Idioma por defecto
1045 | fallbackLocale: defaultLanguage, // Idioma de reserva
1046 | messages,
1047 | });
1048 | // const i18nLegacy = createI18n({
1049 | // locale: 'es', // Idioma por defecto
1050 | // fallbackLocale: 'en', // Idioma de reserva
1051 | // messages,
1052 | // });
1053 |
1054 | export default i18n;
1055 |
--------------------------------------------------------------------------------
/src/assets/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/vue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LokerL/tts-vue/23579190646e9ff4b3c4b61a1348baf488ae7002/src/assets/vue.png
--------------------------------------------------------------------------------
/src/components/aside/Aside.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
26 |
27 |
28 |
29 |
44 |
45 |
69 |
--------------------------------------------------------------------------------
/src/components/aside/Version.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Version:{{ version }}
4 |
7 |
8 |
9 |
10 |
128 |
129 |
146 |
169 |
--------------------------------------------------------------------------------
/src/components/configpage/BiliBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
22 |
23 |
78 |
--------------------------------------------------------------------------------
/src/components/configpage/ConfigPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
18 |
19 |
20 |
21 |
27 |
28 | 确认
29 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
51 |
52 |
53 |
59 |
60 |
61 |
68 |
69 |
70 |
76 |
77 |
78 |
84 |
90 |
91 |
92 |
93 |
100 |
101 |
102 |
109 |
110 |
111 |
117 |
118 |
119 |
120 |
121 | {{ t('configPage.confirm') }}
122 |
123 |
124 |
125 |
126 |
131 |
132 |
133 |
139 | -->
140 | {{ t('configPage.language') }}: {{ scope.row.content.languageSelect }}
141 | {{ t('configPage.voice') }}: {{ scope.row.content.voiceSelect }}
142 | {{ t('configPage.style') }}: {{ scope.row.content.voiceStyleSelect }}
143 | {{ t('configPage.role') }}: {{ scope.row.content.role }}
144 | {{ t('configPage.speed') }}: {{ scope.row.content.speed }}
145 | {{ t('configPage.pitch') }}: {{ scope.row.content.pitch }}
146 |
147 |
148 |
149 | {{ scope.row.tagName }}
150 |
151 |
152 |
153 |
154 |
155 |
156 | {{ t('configPage.remove') }}
162 |
163 |
164 |
165 |
166 |
167 | {{ t('configPage.refreshConfig') }}
170 | {{ t('configPage.configFile') }}
173 |
174 | {{ t('configPage.openLogs') }}
175 |
176 |
177 | {{ t('configPage.clearLogs') }}
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
337 |
338 |
405 |
--------------------------------------------------------------------------------
/src/components/configpage/Donate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ t('donate.appreciation') }}
5 |
6 | {{ t('donate.encouragement') }}
7 |
8 |
9 |
{{ t('donate.guideReminder') }}
10 |
{{ t('donate.feedback') }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
{{ t('donate.buyCoffeeTitle') }}
19 |
20 | {{ t('donate.wechatPayment') }} {{ t('donate.hoverForAlipay') }}
21 |
22 |

28 |
29 |
30 |
31 |
{{ t('donate.buyDrinkTitle') }}
32 |
33 | {{ t('donate.alipayPayment') }} {{ t('donate.hoverForWechat') }}
34 |
35 |

41 |
42 |
43 |
44 |
45 |
46 |
47 |
53 |
54 |
145 |
--------------------------------------------------------------------------------
/src/components/configpage/GiteeBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 |
19 |
25 |
26 |
82 |
--------------------------------------------------------------------------------
/src/components/configpage/GithubBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
27 |
28 |
63 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
44 |
45 |
46 |
71 |
72 |
100 |
--------------------------------------------------------------------------------
/src/components/header/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
48 |
49 |
50 |
70 |
71 |
93 |
--------------------------------------------------------------------------------
/src/components/header/Logo.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
17 |
18 |
90 |
--------------------------------------------------------------------------------
/src/components/main/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
25 |
26 |
27 |
47 |
48 |
49 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
132 |
--------------------------------------------------------------------------------
/src/components/main/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
29 |
36 |
37 |
38 | {{ t('main.descriptionGenerateTextGPT') }}
39 |
40 |
41 |
42 |
43 |
51 |
52 |
53 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
143 |
144 |
145 |
146 |
147 |
249 |
250 |
317 |
--------------------------------------------------------------------------------
/src/components/main/MainOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
35 |
41 |
42 | {{
43 | item.DisplayName + "-" + item.LocalName
44 | }}
45 |
52 |
53 |
54 |
55 |
56 |
57 |
62 |
68 |
69 | {{
70 | getStyleDes(item)?.emoji
71 | }}
72 | {{ getStyleDes(item)?.word || item }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
85 |
86 | {{
87 | getRoleDes(item)?.emoji
88 | }}
89 | {{ getRoleDes(item)?.word || item }}
90 |
91 |
92 |
93 |
94 |
95 |
103 |
104 |
105 |
113 |
114 |
115 |
116 | {{ t('options.saveConfig') }}
124 |
132 |
133 |
134 |
135 |
136 |
137 | {{ t('options.startConversion') }}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
349 |
350 |
453 |
--------------------------------------------------------------------------------
/src/components/main/emoji-config.ts:
--------------------------------------------------------------------------------
1 |
2 | // import { useI18n } from 'vue-i18n';
3 | // const { t } = useI18n();
4 | import i18n from '@/assets/i18n/i18n';
5 | const { t } = i18n.global;
6 | const styleDes = [
7 | { keyword: "assistant", emoji: "🔊", word: t('assistant') },
8 | { keyword: "chat", emoji: "🔊", word: t('chat') },
9 | { keyword: "customerservice", emoji: "🔊", word: t('customerservice') },
10 | { keyword: "newscast", emoji: "🎤", word: t('newscast') },
11 | { keyword: "affectionate", emoji: "😘", word: t('affectionate') },
12 | { keyword: "angry", emoji: "😡", word: t('angry') },
13 | { keyword: "calm", emoji: "😶", word: t('calm') },
14 | { keyword: "cheerful", emoji: "😄", word: t('cheerful') },
15 | { keyword: "disgruntled", emoji: "😠", word: t('disgruntled') },
16 | { keyword: "fearful", emoji: "😨", word: t('fearful') },
17 | { keyword: "gentle", emoji: "😇", word: t('gentle') },
18 | { keyword: "lyrical", emoji: "😍", word: t('lyrical') },
19 | { keyword: "sad", emoji: "😭", word: t('sad') },
20 | { keyword: "serious", emoji: "😐", word: t('serious') },
21 | { keyword: "poetry-reading", emoji: "🔊", word: t('poetry-reading') },
22 | { keyword: "narration-professional", emoji: "👩💼", word: t('narration-professional') },
23 | { keyword: "newscast-casual", emoji: "🔊", word: t('newscast-casual') },
24 | { keyword: "embarrassed", emoji: "😓", word: t('embarrassed') },
25 | { keyword: "depressed", emoji: "😔", word: t('depressed') },
26 | { keyword: "envious", emoji: "😒", word: t('envious') },
27 | { keyword: "narration-relaxed", emoji: "🎻", word: t('narration-relaxed') },
28 | {
29 | keyword: "Advertisement_upbeat",
30 | emoji: "🗣",
31 | word: t('Advertisement_upbeat'),
32 | },
33 | { keyword: "Narration-relaxed", emoji: "🎻", word: t('Narration-relaxed') },
34 | { keyword: "Sports_commentary", emoji: "⛹", word: t('Sports_commentary') },
35 | {
36 | keyword: "Sports_commentary_excited",
37 | emoji: "🥇",
38 | word: t('Sports_commentary_excited'),
39 | },
40 | { keyword: "documentary-narration", emoji: "🎞", word: t('documentary-narration') },
41 | { keyword: "excited", emoji: "😁", word: t('excited') },
42 | { keyword: "friendly", emoji: "😋", word: t('friendly') },
43 | { keyword: "terrified", emoji: "😱", word: t('terrified') },
44 | { keyword: "shouting", emoji: "📢", word: t('shouting') },
45 | { keyword: "unfriendly", emoji: "😤", word: t('unfriendly') },
46 | { keyword: "whispering", emoji: "😶", word: t('whispering') },
47 | { keyword: "hopeful", emoji: "☀️", word: t('hopeful') },
48 | ];
49 | const roleDes = [
50 | { keyword: "YoungAdultFemale", emoji: "👱♀️", word: t('YoungAdultFemale') },
51 | { keyword: "YoungAdultMale", emoji: "👱", word: t('YoungAdultMale') },
52 | { keyword: "OlderAdultFemale", emoji: "👩", word: t('OlderAdultFemale') },
53 | { keyword: "OlderAdultMale", emoji: "👨", word: t('OlderAdultMale') },
54 | { keyword: "SeniorFemale", emoji: "👵", word: t('SeniorFemale') },
55 | { keyword: "SeniorMale", emoji: "👴", word: t('SeniorMale') },
56 | { keyword: "Girl", emoji: "👧", word: t('Girl') },
57 | { keyword: "Boy", emoji: "👦", word: t('Boy') },
58 | { keyword: "Narrator", emoji: "🔊", word: t('Narrator') },
59 | ];
60 | const getStyleDes = (key: string) => {
61 | return styleDes.find((item) => item.keyword === key);
62 | };
63 |
64 | const getRoleDes = (key: string) => {
65 | return roleDes.find((item) => item.keyword === key);
66 | };
67 |
68 | export { getStyleDes, getRoleDes };
69 |
--------------------------------------------------------------------------------
/src/components/main/options-config.ts:
--------------------------------------------------------------------------------
1 |
2 | // import { useI18n } from 'vue-i18n';
3 | import i18n from '@/assets/i18n/i18n';
4 | import { voices } from './../../global/voices';
5 | const { t } = i18n.global;
6 |
7 | let msVoicesList;
8 | if (localStorage.getItem("msVoicesList") !== null) {
9 | msVoicesList = JSON.parse(localStorage.getItem("msVoicesList") || "[]");
10 | } else {
11 | msVoicesList = voices;
12 | }
13 |
14 | const voicesList = msVoicesList.map((item: any) => {
15 | item.properties.locale = item.locale;
16 | // ZH_CN_SHANDONG有BUG很奇怪
17 | // if (lang.hasOwnProperty(item.locale.toUpperCase().replace("-", "_").replace("-", "_"))) {
18 | // item.properties.localeZH =
19 | // lang[item.locale.toUpperCase().replace("-", "_").replace("-", "_")];
20 | // } else {
21 | // item.properties.localeZH = item.locale
22 | // }
23 | item.properties.localeZH = t('lang.' + item.locale.toUpperCase().replace("-", "_").replace("-", "_"));
24 |
25 | return item.properties;
26 | });
27 |
28 | const list = voicesList
29 | .map((item: any) => {
30 | return {
31 | value: item.locale,
32 | label: item.localeZH,
33 | };
34 | })
35 | .sort((a: any, b: any) => b.value.localeCompare(a.value, "en"));
36 |
37 | const tempMap = new Map();
38 | const languageSelect = list.filter(
39 | (item: any) => !tempMap.has(item.value) && tempMap.set(item.value, 1)
40 | );
41 |
42 | const findVoicesByLocaleName = (localeName: any) => {
43 | const voices = voicesList.filter((item: any) => item.locale == localeName);
44 | return voices;
45 | };
46 |
47 | const apiSelect = [
48 | {
49 | value: 1,
50 | label: "Microsoft Speech API",
51 | },
52 | {
53 | value: 2,
54 | label: "Edge Speech API",
55 | },
56 | {
57 | value: 3,
58 | label: "Azure Speech API",
59 | },
60 | ];
61 |
62 | export const optionsConfig = {
63 | voicesList,
64 | languageSelect,
65 | findVoicesByLocaleName,
66 | apiSelect,
67 | };
68 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6 | const component: DefineComponent<{}, {}, any>
7 | export default component
8 | }
9 |
--------------------------------------------------------------------------------
/src/global/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from "vue";
2 | import registerElement from "./registerElement";
3 | import initLocalStore from "./initLocalStore";
4 |
5 | export function globalRegister(app: App) {
6 | initLocalStore();
7 | app.use(registerElement);
8 | }
9 |
--------------------------------------------------------------------------------
/src/global/initLocalStore.ts:
--------------------------------------------------------------------------------
1 | import i18n from '@/assets/i18n/i18n';
2 | import { voices } from './voices';
3 | const Store = require("electron-store");
4 | const store = new Store();
5 | const { ipcRenderer } = require("electron");
6 | const { t } = i18n.global;
7 |
8 | export default async function initStore() {
9 | try {
10 | const msVoicesList = await ipcRenderer.invoke("voices");
11 | localStorage.setItem("msVoicesList", JSON.stringify(msVoicesList));
12 | } catch (error) {
13 | // 如果网络请求失败并且localStorage的msVoicesList为空
14 | if (localStorage.getItem("msVoicesList") == null) {
15 | localStorage.setItem("msVoicesList", JSON.stringify(voices));
16 | }
17 | }
18 | const locale = i18n.global.locale.value;
19 |
20 | const formConfigDefault = {
21 | es: {
22 | languageSelect: "es-MX",
23 | voiceSelect: "es-MX-DaliaNeural",
24 | voiceStyleSelect: "Default",
25 | role: "",
26 | speed: 1.0,
27 | pitch: 1.0,
28 | api: 1,
29 | },
30 | en: {
31 | languageSelect: "en-US",
32 | voiceSelect: "en-US-JennyNeural",
33 | voiceStyleSelect: "Default",
34 | role: "",
35 | speed: 1.0,
36 | pitch: 1.0,
37 | api: 1,
38 | },
39 | zh: {
40 | languageSelect: "zh-CN",
41 | voiceSelect: "zh-CN-XiaoxiaoNeural",
42 | voiceStyleSelect: "Default",
43 | role: "",
44 | speed: 1.0,
45 | pitch: 1.0,
46 | api: 1,
47 | },
48 | };
49 |
50 | store.set("FormConfig.默认", formConfigDefault[locale]);
51 |
52 | if (!store.has("savePath")) {
53 | store.set("savePath", ipcRenderer.sendSync("getDesktopPath"));
54 | }
55 | if (!store.has("audition")) {
56 | store.set(
57 | "audition",
58 | t("initialLocalStore.audition")
59 | );
60 | }
61 | if (!store.has("language")) {
62 | store.set("language", locale);
63 | }
64 | if (!store.has("autoplay")) {
65 | store.set("autoplay", true);
66 | }
67 | if (!store.has("updateNotification")) {
68 | store.set("updateNotification", true);
69 | }
70 | if (!store.has("titleStyle")) {
71 | store.set("titleStyle", true);
72 | }
73 | if (!store.has("speechKey")) {
74 | store.set("speechKey", "");
75 | }
76 | if (!store.has("serviceRegion")) {
77 | store.set("serviceRegion", "");
78 | }
79 | if (!store.has("disclaimers")) {
80 | store.set("disclaimers", false);
81 | }
82 | if (!store.has("retryCount")) {
83 | store.set("retryCount", 10);
84 | }
85 | if (!store.has("retryInterval")) {
86 | store.set("retryInterval", 3);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/global/registerElement.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ElContainer,
3 | ElIcon,
4 | ElButton,
5 | ElButtonGroup,
6 | ElMenu,
7 | ElInput,
8 | ElForm,
9 | ElSelect,
10 | ElSlider,
11 | ElOption,
12 | ElSelectV2,
13 | ElMenuItem,
14 | ElTable,
15 | ElTag,
16 | ElUpload,
17 | ElDialog,
18 | ElDivider,
19 | ElSwitch,
20 | ElPopover,
21 | ElDropdown,
22 | } from "element-plus";
23 | import "element-plus/dist/index.css";
24 | import "element-plus/theme-chalk/display.css";
25 | import * as Icons from "@element-plus/icons-vue";
26 |
27 | const components = [
28 | ElContainer,
29 | ElContainer.Header,
30 | ElContainer.Main,
31 | ElContainer.Aside,
32 | ElContainer.Footer,
33 |
34 | ElIcon,
35 | ElButton,
36 | ElButtonGroup,
37 | ElMenu,
38 | ElMenu.MenuItem,
39 |
40 | ElInput,
41 | ElForm,
42 | ElForm.FormItem,
43 | ElSelect,
44 | ElSelectV2,
45 | ElSelect.Option,
46 | ElSlider,
47 | ElMenu,
48 | ElMenu.MenuItem,
49 |
50 | ElTable,
51 | ElTable.TableColumn,
52 | ElTag,
53 |
54 | ElUpload,
55 | ElDialog,
56 | ElSwitch,
57 | ElPopover,
58 | ElDivider,
59 |
60 | ElDropdown,
61 | ElDropdown.DropdownMenu,
62 | ElDropdown.DropdownItem,
63 | ];
64 |
65 | export default function (app: any) {
66 | for (const component of components) {
67 | app.component(component.name, component);
68 | }
69 |
70 | for (const name in Icons) {
71 | app.component(name, (Icons as any)[name]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import App from "./App.vue";
3 | import { globalRegister } from "./global";
4 | import { createPinia } from "pinia";
5 | import i18n from './assets/i18n/i18n';
6 | // import { useI18n } from 'vue-i18n';
7 |
8 | // const App = {
9 | // setup() {
10 | // const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
11 | // return { t } // return render context that included `t`
12 | // }
13 | // }
14 | const app = createApp(App) as any;
15 | const pinia = createPinia();
16 |
17 | app.use(i18n);
18 | app.use(pinia);
19 | app.use(globalRegister);
20 | app.mount("#app").$nextTick(() => {
21 | postMessage({ payload: "removeLoading" }, "*");
22 | });
23 |
--------------------------------------------------------------------------------
/src/store/play.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from "electron";
2 | import { PromptGPT } from "@/types/prompGPT";
3 |
4 | async function getTTSData(
5 | inps: any,
6 | voice: string,
7 | express: string,
8 | role: string,
9 | rate = 0,
10 | pitch = 0,
11 | api: number,
12 | key: string,
13 | region: string,
14 | retryCount: number,
15 | retryInterval = 1,
16 | ) {
17 | // 判断retryCount是否为0或者null,如果是则不重试
18 | if (!retryCount) {
19 | retryCount = 1;
20 | }
21 | if (!retryInterval) {
22 | retryInterval = 1;
23 | }
24 | let SSML = "";
25 | if (inps.activeIndex == "1" && (api == 1 || api == 3)) {
26 | SSML = `
27 |
28 |
29 |
31 |
32 | ${inps.inputValue}
33 |
34 |
35 |
36 |
37 | `;
38 | }
39 | else if (inps.activeIndex == "1" && api == 2) {
40 | SSML = `
41 |
42 |
43 |
44 | ${inps.inputValue}
45 |
46 |
47 |
48 | `;
49 | }
50 | else {
51 | SSML = inps.inputValue;
52 | }
53 | ipcRenderer.send("log.info", SSML);
54 | console.log(SSML);
55 | if (api == 1) {
56 | const result = await retrySpeechInvocation(SSML, retryCount, retryInterval * 1000);
57 | return result;
58 | } else if (api == 2) {
59 | const result = await ipcRenderer.invoke("edgeApi", SSML);
60 | return result;
61 | } else {
62 | const result = await ipcRenderer.invoke("azureApi", SSML, key, region);
63 | return result;
64 | }
65 | }
66 | async function retrySpeechInvocation(SSML: string, retryCount: number, delay: number) {
67 | let retry = 0;
68 | while (retry < retryCount) {
69 | try {
70 | console.log("语音调用尝试:", retry + 1);
71 | const result = await ipcRenderer.invoke("speech", SSML);
72 | return result; // 执行成功,返回结果
73 | } catch (error) {
74 | console.error("Speech invocation failed:", error);
75 | await sleep(delay); // 暂停一段时间后再重试
76 | }
77 | retry++;
78 | }
79 | throw new Error(`${retryCount} 次重试后仍转换失败。`); // 重试次数用尽,抛出异常
80 | }
81 | function sleep(ms: number) {
82 | return new Promise(resolve => setTimeout(resolve, ms));
83 | }
84 | // promptGPT
85 | async function getDataGPT(options: PromptGPT) {
86 | let { promptGPT, model, key, retryCount, retryInterval=1 } = options;
87 | // 判断retryCount是否为0或者null,如果是则不重试
88 | if (!retryCount) {
89 | retryCount = 1;
90 | }
91 | if (!retryInterval) {
92 | retryInterval = 1;
93 | }
94 | const result = await ipcRenderer.invoke("promptGPT", promptGPT, model, key);
95 | return result;
96 | }
97 |
98 | export { getTTSData, getDataGPT };
99 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | // @/store/firstStore.js
2 |
3 | import { defineStore } from "pinia";
4 | import { getTTSData, getDataGPT } from "./play";
5 | import { ElMessage, ElMessageBox } from "element-plus";
6 | import { h } from "vue";
7 | const fs = require("fs");
8 | const path = require("path");
9 | const Store = require("electron-store");
10 | const { ipcRenderer } = require("electron");
11 | const ffmpeg = require("fluent-ffmpeg");
12 | const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
13 | const { Readable } = require('stream');
14 |
15 | if (process.env.NODE_ENV === 'development') {
16 | // 处于开发状态
17 | console.log('开发状态');
18 | ffmpeg.setFfmpegPath(ffmpegInstaller.path);
19 | } else if (process.env.NODE_ENV === 'production') {
20 | // 处于打包状态
21 | console.log('打包状态');
22 | ffmpeg.setFfmpegPath(ffmpegInstaller.path.replace("app.asar", "app.asar.unpacked"));
23 | }
24 |
25 | const store = new Store();
26 | // 定义并导出容器,第一个参数是容器id,必须唯一,用来将所有的容器
27 | // 挂载到根容器上
28 | export const useTtsStore = defineStore("ttsStore", {
29 | // 定义state,用来存储状态的
30 | state: () => {
31 | return {
32 | inputs: {
33 | inputValue: "你好啊\n今天天气怎么样?",
34 | ssmlValue: "你好啊\n今天天气怎么样?",
35 | },
36 | formConfig: store.get("FormConfig.默认"),
37 | page: {
38 | asideIndex: "1",
39 | tabIndex: "1",
40 | },
41 | tableData: [], // 文件列表的数据
42 | currConfigName: "默认", // 当前配置的名字
43 | config: {
44 | language: store.get("language"),
45 | formConfigJson: store.get("FormConfig"),
46 | formConfigList: [],
47 | configLabel: [],
48 | savePath: store.get("savePath"),
49 | audition: store.get("audition"),
50 | autoplay: store.get("autoplay"),
51 | updateNotification: store.get("updateNotification"),
52 | titleStyle: store.get("titleStyle"),
53 | api: store.get("api"),
54 | formatType: store.get("formatType"),
55 | speechKey: store.get("speechKey"),
56 | serviceRegion: store.get("serviceRegion"),
57 | disclaimers: store.get("disclaimers"),
58 | retryCount: store.get("retryCount"),
59 | retryInterval: store.get("retryInterval"),
60 | openAIKey: store.get("openAIKey"),
61 | gptModel: store.get("gptModel"),
62 | },
63 | isLoading: false,
64 | currMp3Buffer: Buffer.alloc(0),
65 | currMp3Url: "",
66 | audioPlayer: null,
67 | };
68 | },
69 | // 定义getters,类似于computed,具有缓存g功能
70 | getters: {},
71 | // 定义actions,类似于methods,用来修改state,做一些业务逻辑
72 | actions: {
73 | setDoneStatus(filePath: string) {
74 | for (const item of this.tableData) {
75 | if (item.filePath == filePath) {
76 | item.status = "done";
77 | return;
78 | }
79 | }
80 | },
81 | setSSMLValue(text = "") {
82 | if (text === "") text = this.inputs.inputValue;
83 | const voice = this.formConfig.voiceSelect;
84 | const express = this.formConfig.voiceStyleSelect;
85 | const role = this.formConfig.role;
86 | const rate = (this.formConfig.speed - 1) * 100;
87 | const pitch = (this.formConfig.pitch - 1) * 50;
88 |
89 | this.inputs.ssmlValue = `
90 |
91 |
93 |
94 | ${text}
95 |
96 |
97 |
98 |
99 | `;
100 | },
101 | setSavePath() {
102 | store.set("savePath", this.config.savePath);
103 | },
104 | setLanguage() {
105 | store.set("language", this.config.language);
106 | },
107 | setAuditionConfig() {
108 | store.set("audition", this.config.audition);
109 | },
110 | updateNotificationChange() {
111 | store.set("updateNotification", this.config.updateNotification);
112 | },
113 | updateTitleStyle() {
114 | store.set("titleStyle", this.config.titleStyle);
115 | },
116 | setFormatType() {
117 | store.set("formatType", this.config.formatType);
118 | },
119 | setAutoPlay() {
120 | store.set("autoplay", this.config.autoplay);
121 | },
122 | setSpeechKey() {
123 | store.set("speechKey", this.config.speechKey);
124 | },
125 | setOpenAIKey() {
126 | store.set("openAIKey", this.config.openAIKey);
127 | },
128 | setGPTModel() {
129 | store.set("gptModel", this.config.gptModel);
130 | },
131 | setServiceRegion() {
132 | store.set("serviceRegion", this.config.serviceRegion);
133 | },
134 | setRetryCount() {
135 | store.set("retryCount", parseInt(this.config.retryCount));
136 | },
137 | setRetryInterval() {
138 | store.set("retryInterval", parseInt(this.config.retryInterval));
139 | },
140 | addFormConfig() {
141 | this.config.formConfigJson[this.currConfigName] = this.formConfig;
142 | this.genFormConfig();
143 | },
144 | genFormConfig() {
145 | // store.set("FormConfig", this.config.formConfigJson);
146 | this.config.formConfigList = Object.keys(this.config.formConfigJson).map(
147 | (item) => ({
148 | tagName: item,
149 | content: this.config.formConfigJson[item],
150 | })
151 | );
152 | this.config.configLabel = Object.keys(this.config.formConfigJson).map(
153 | (item) => ({
154 | value: item,
155 | label: item,
156 | })
157 | );
158 | },
159 | async startChatGPT(promptGPT: string) {
160 | await getDataGPT(
161 | {
162 | promptGPT: promptGPT,
163 | key: this.config.openAIKey,
164 | model: this.config.gptModel,
165 | retryCount: this.config.retryCount,
166 | retryInterval: this.config.retryInterval,
167 | }
168 | )
169 | .then((res: any) => {
170 | this.inputs.inputValue = res;
171 | this.setSSMLValue();
172 | console.log(res);
173 | ElMessage({
174 | message: "Response Success!",
175 | type: "success",
176 | duration: 2000,
177 | });
178 | // this.start();
179 | })
180 | .catch((err: any) => {
181 | console.error(err);
182 | ElMessage({
183 | message: "转换失败\n" + String(err),
184 | type: "error",
185 | duration: 3000,
186 | });
187 | });
188 | },
189 | async start() {
190 | console.log("清空缓存中");
191 | let resFlag = true;
192 | this.currMp3Buffer = Buffer.alloc(0);
193 | this.currMp3Url = "";
194 | // this.page.asideIndex == "1"单文本转换
195 | if (this.page.asideIndex == "1") {
196 | this.currMp3Url = "";
197 | const value = {
198 | activeIndex: this.page.tabIndex,
199 | inputValue:
200 | this.page.tabIndex == "1"
201 | ? this.inputs.inputValue
202 | : this.inputs.ssmlValue,
203 | };
204 | if (
205 | this.page.tabIndex == "1" &&
206 | this.formConfig.api == 1 &&
207 | this.inputs.inputValue.length > 400
208 | ) {
209 | const delimiters = [",", "。", "?", ",", ".", "?", "\n"];
210 | const maxSize = 300;
211 | ipcRenderer.send("log.info", "字数过多,正在对文本切片。。。");
212 |
213 | const textHandler = this.inputs.inputValue.split("").reduce(
214 | (obj: any, char, index, arr) => {
215 | obj.buffer.push(char);
216 | if (delimiters.indexOf(char) >= 0) obj.end = index;
217 | if (obj.buffer.length === maxSize) {
218 | obj.res.push(
219 | obj.buffer.splice(0, obj.end + 1 - obj.offset).join("")
220 | );
221 | obj.offset += obj.res[obj.res.length - 1].length;
222 | }
223 | return obj;
224 | },
225 | {
226 | buffer: [],
227 | end: 0,
228 | offset: 0,
229 | res: [],
230 | }
231 | );
232 | textHandler.res.push(textHandler.buffer.join(""));
233 | const tasks = textHandler.res;
234 | for (let index = 0; index < tasks.length; index++) {
235 | try {
236 | ipcRenderer.send(
237 | "log.info",
238 | `正在执行第${index + 1}次转换。。。`
239 | );
240 | const element = tasks[index];
241 | value.inputValue = element;
242 | const buffers: any = await getTTSData(
243 | value,
244 | this.formConfig.voiceSelect,
245 | this.formConfig.voiceStyleSelect,
246 | this.formConfig.role,
247 | (this.formConfig.speed - 1) * 100,
248 | (this.formConfig.pitch - 1) * 50,
249 | this.formConfig.api,
250 | this.config.speechKey,
251 | this.config.serviceRegion,
252 | this.config.retryCount,
253 | );
254 | this.currMp3Buffer = Buffer.concat([this.currMp3Buffer, buffers]);
255 | ipcRenderer.send(
256 | "log.info",
257 | `第${index + 1}次转换完成,此时Buffer长度为:${this.currMp3Buffer.length
258 | }`
259 | );
260 | } catch (error) {
261 | resFlag = false;
262 | console.error(error);
263 | ipcRenderer.send("log.error", error);
264 | this.isLoading = false;
265 | ElMessage({
266 | message: "网络异常!\n" + String(error),
267 | type: "error",
268 | duration: 3000,
269 | });
270 | if (this.currMp3Buffer.length > 0) {
271 | const svlob = new Blob([this.currMp3Buffer]);
272 | this.currMp3Url = URL.createObjectURL(svlob);
273 | }
274 | return;
275 | }
276 | }
277 |
278 | if (this.currMp3Buffer.length > 0) {
279 | const svlob = new Blob([this.currMp3Buffer]);
280 | this.currMp3Url = URL.createObjectURL(svlob);
281 | }
282 | this.isLoading = false;
283 | } else {
284 | // 字数少直接转换
285 | await getTTSData(
286 | value,
287 | this.formConfig.voiceSelect,
288 | this.formConfig.voiceStyleSelect,
289 | this.formConfig.role,
290 | (this.formConfig.speed - 1) * 100,
291 | (this.formConfig.pitch - 1) * 50,
292 | this.formConfig.api,
293 | this.config.speechKey,
294 | this.config.serviceRegion,
295 | this.config.retryCount,
296 | )
297 | .then((mp3buffer: any) => {
298 | this.currMp3Buffer = mp3buffer;
299 | const svlob = new Blob([mp3buffer]);
300 | this.currMp3Url = URL.createObjectURL(svlob);
301 | this.isLoading = false;
302 | })
303 | .catch((err) => {
304 | resFlag = false;
305 | this.isLoading = false;
306 | console.error(err);
307 | ElMessage({
308 | message: "转换失败\n" + String(err),
309 | type: "error",
310 | duration: 2000,
311 | });
312 | });
313 | }
314 | if (resFlag) {
315 | ElMessage({
316 | message: this.config.autoplay
317 | ? "成功,正在试听~"
318 | : "成功,请手动播放。",
319 | type: "success",
320 | duration: 2000,
321 | });
322 | }
323 |
324 | ipcRenderer.send("log.info", `转换完成`);
325 | } else {
326 | // this.page.asideIndex == "2" 批量转换
327 | this.page.tabIndex == "1";
328 |
329 | // 分割方法
330 |
331 | this.tableData.forEach(async (item: any) => {
332 | const inps = {
333 | activeIndex: 1, // 值转换普通文本
334 | inputValue: "",
335 | tableValue: item,
336 | };
337 | const filePath = path.join(
338 | this.config.savePath,
339 | item.fileName.split(path.extname(item.fileName))[0] + ".mp3"
340 | );
341 | await fs.readFile(
342 | item.filePath,
343 | "utf8",
344 | async (err: any, datastr: any) => {
345 | if (err) console.log(err);
346 |
347 | inps.inputValue = datastr;
348 | let buffer = Buffer.alloc(0);
349 |
350 | if (datastr.length > 400 && this.formConfig.api == 1) {
351 | const delimiters = ",。?,.? ".split("");
352 | const maxSize = 300;
353 | ipcRenderer.send("log.info", "字数过多,正在对文本切片。。。");
354 |
355 | const textHandler = datastr.split("").reduce(
356 | (obj: any, char: any, index: any, arr: any) => {
357 | obj.buffer.push(char);
358 | if (delimiters.indexOf(char) >= 0) obj.end = index;
359 | if (obj.buffer.length === maxSize) {
360 | obj.res.push(
361 | obj.buffer.splice(0, obj.end + 1 - obj.offset).join("")
362 | );
363 | obj.offset += obj.res[obj.res.length - 1].length;
364 | }
365 | return obj;
366 | },
367 | {
368 | buffer: [],
369 | end: 0,
370 | offset: 0,
371 | res: [],
372 | }
373 | );
374 | textHandler.res.push(textHandler.buffer.join(""));
375 | const tasks = textHandler.res;
376 | for (let index = 0; index < tasks.length; index++) {
377 | try {
378 | ipcRenderer.send(
379 | "log.info",
380 | `正在执行第${index + 1}次转换。。。`
381 | );
382 | const element = tasks[index];
383 | inps.inputValue = element;
384 | const buffers: any = await getTTSData(
385 | inps,
386 | this.formConfig.voiceSelect,
387 | this.formConfig.voiceStyleSelect,
388 | this.formConfig.role,
389 | (this.formConfig.speed - 1) * 100,
390 | (this.formConfig.pitch - 1) * 50,
391 | this.formConfig.api,
392 | this.config.speechKey,
393 | this.config.serviceRegion,
394 | this.config.retryCount,
395 | );
396 | buffer = Buffer.concat([buffer, buffers]);
397 | ipcRenderer.send(
398 | "log.info",
399 | `第${index + 1}次转换完成,此时Buffer长度为:${buffer.length
400 | }`
401 | );
402 | } catch (error) {
403 | console.error(error);
404 | resFlag = false;
405 | ipcRenderer.send("log.error", error);
406 | this.isLoading = false;
407 | ElMessage({
408 | message: "转换失败\n" + String(error),
409 | type: "error",
410 | duration: 3000,
411 | });
412 | if (buffer.length > 0) {
413 | fs.writeFileSync(filePath, buffer);
414 | this.setDoneStatus(item.filePath);
415 | }
416 | return;
417 | }
418 | }
419 | fs.writeFileSync(filePath, buffer);
420 | this.setDoneStatus(item.filePath);
421 | if (resFlag) {
422 | ElMessage({
423 | message: "成功,正在写入" + filePath,
424 | type: "success",
425 | duration: 2000,
426 | });
427 | }
428 |
429 | this.isLoading = false;
430 | } else {
431 | await getTTSData(
432 | inps,
433 | this.formConfig.voiceSelect,
434 | this.formConfig.voiceStyleSelect,
435 | this.formConfig.role,
436 | (this.formConfig.speed - 1) * 100,
437 | (this.formConfig.pitch - 1) * 50,
438 | this.formConfig.api,
439 | this.config.speechKey,
440 | this.config.serviceRegion,
441 | this.config.retryCount,
442 | )
443 | .then((mp3buffer: any) => {
444 | fs.writeFileSync(filePath, mp3buffer);
445 | this.setDoneStatus(item.filePath);
446 | ElMessage({
447 | message: "成功,正在写入" + filePath,
448 | type: "success",
449 | duration: 2000,
450 | });
451 | this.isLoading = false;
452 | })
453 | .catch((err) => {
454 | this.isLoading = false;
455 | console.error(err);
456 | ElMessage({
457 | message: "转换失败\n" + String(err),
458 | type: "error",
459 | duration: 3000,
460 | });
461 | });
462 | }
463 | }
464 | );
465 | });
466 | // this.isLoading = false;
467 | }
468 | },
469 | writeFileSync() {
470 | const currTime = new Date().getTime().toString();
471 |
472 | console.log('当前设置的格式:', this.config.formatType);
473 |
474 | //-------------------------------------------------------------------------------------------------------------------------------------
475 |
476 | const filePath = path.join(this.config.savePath, currTime + this.config.formatType);
477 | if (this.config.formatType == ".mp3") {
478 | fs.writeFileSync(path.resolve(filePath), this.currMp3Buffer);
479 | ElMessage({
480 | dangerouslyUseHTMLString: true,
481 | message: h("p", null, [
482 | h("span", null, "下载完成:"),
483 | h(
484 | "span",
485 | {
486 | on: {
487 | click: this.showItemInFolder(filePath),
488 | },
489 | },
490 | filePath
491 | ),
492 | ]),
493 | type: "success",
494 | duration: 4000,
495 | });
496 | ipcRenderer.send("log.info", `下载完成:${filePath}`);
497 | }
498 | else {
499 | // 将 this.currMp3Buffer 转换为可读流
500 | const inputStream = new Readable();
501 | inputStream.push(this.currMp3Buffer);
502 | inputStream.push(null); // 结束流
503 | // 使用 fluent-ffmpeg 进行转码
504 | ffmpeg(inputStream)
505 | .output(filePath)
506 | .audioCodec('pcm_s16le') // 示例:使用 PCM 16位音频编码
507 | .audioChannels(2) // 示例:设置音频通道数为2
508 | .audioFrequency(44100) // 示例:设置音频采样率为44100Hz
509 | .on('end', () => {
510 | console.log('转码完成!音频已保存为文件:', filePath);
511 | ipcRenderer.send("showItemInFolder", filePath);
512 |
513 | ElMessage({
514 | dangerouslyUseHTMLString: true,
515 | message: h("p", null, [
516 | h("span", null, "下载完成:"),
517 | h(
518 | "span",
519 | {
520 | on: {
521 | click: this.showItemInFolder(filePath),
522 | },
523 | },
524 | filePath
525 | ),
526 | ]),
527 | type: "success",
528 | duration: 3000,
529 | });
530 |
531 | })
532 | .on('error', (err: any) => {
533 | console.error('转码出错:', err);
534 |
535 | ElMessage({
536 | dangerouslyUseHTMLString: true,
537 | message: h("p", null, [
538 | h("span", null, "转码失败!!!:" + err)
539 | ]),
540 | type: "error",
541 | duration: 3000,
542 | });
543 |
544 | })
545 | .run();
546 | }
547 | //-------------------------------------------------------------------------------------------------------------------------------------
548 |
549 | },
550 | async audition(val: string) {
551 | const inps = {
552 | activeIndex: 1, // 值转换普通文本
553 | inputValue: this.config.audition,
554 | };
555 | await getTTSData(
556 | inps,
557 | val,
558 | this.formConfig.voiceStyleSelect,
559 | this.formConfig.role,
560 | (this.formConfig.speed - 1) * 100,
561 | (this.formConfig.pitch - 1) * 50,
562 | this.formConfig.api,
563 | this.config.speechKey,
564 | this.config.serviceRegion,
565 | this.config.retryCount,
566 | )
567 | .then((mp3buffer: any) => {
568 | this.currMp3Buffer = mp3buffer;
569 | const svlob = new Blob([mp3buffer]);
570 | const sound = new Audio(URL.createObjectURL(svlob));
571 | sound.play();
572 | })
573 | .catch((err: any) => {
574 | console.log(err);
575 | });
576 | },
577 | showItemInFolder(filePath: string) {
578 | ipcRenderer.send("showItemInFolder", filePath);
579 | },
580 | showDisclaimers() {
581 | if (!this.config.disclaimers) {
582 | ElMessageBox.confirm(
583 | "该软件以及代码仅为个人学习测试使用,请在下载后24小时内删除,不得用于商业用途,否则后果自负。任何违规使用造成的法律后果与本人无关。该软件也永远不会收费,如果您使用该软件前支付了额外费用,或付费获得源码以及成品软件,那么你一定被骗了!",
584 | "注意!",
585 | {
586 | confirmButtonText: "我已确认,不再弹出",
587 | cancelButtonText: "取消",
588 | type: "warning"
589 | }
590 | ).then(() => {
591 | store.set("disclaimers", true);
592 | });
593 | }
594 | }
595 | },
596 | });
597 |
--------------------------------------------------------------------------------
/src/types/prompGPT.ts:
--------------------------------------------------------------------------------
1 | interface PromptGPT {
2 | promptGPT: string,
3 | model: string,
4 | key: string,
5 | retryCount: number,
6 | retryInterval: number,
7 | }
8 |
9 | export { PromptGPT}
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "ignoreDeprecations": "5.0",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "importHelpers": true,
8 | "jsx": "preserve",
9 | "esModuleInterop": true,
10 | "resolveJsonModule": true,
11 | "sourceMap": true,
12 | "baseUrl": "./",
13 | "strict": true,
14 | "paths": { "@/*": ["src/*"] },
15 | "allowSyntheticDefaultImports": true,
16 | "skipLibCheck": true,
17 | "suppressImplicitAnyIndexErrors": true
18 | },
19 | "references": [{ "path": "./tsconfig.node.json" }],
20 | "include": [
21 | "src/**/*.ts",
22 | "src/**/*.tsx",
23 | "src/**/*.vue",
24 | "tests/**/*.ts",
25 | "tests/**/*.tsx"
26 | ],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "composite": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "jsx": "preserve",
8 | "resolveJsonModule": true,
9 | "allowSyntheticDefaultImports": true
10 | },
11 | "include": ["vite.config.ts", "electron", "package.json"]
12 | }
13 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { rmSync } from "fs";
2 | import { join } from "path";
3 | import { defineConfig, Plugin, UserConfig } from "vite";
4 | import vue from "@vitejs/plugin-vue";
5 | import electron from "vite-plugin-electron";
6 | import pkg from "./package.json";
7 |
8 | rmSync("dist", { recursive: true, force: true }); // v14.14.0
9 |
10 | // https://vitejs.dev/config/
11 | export default defineConfig({
12 | resolve: {
13 | // +++
14 | alias: {
15 | "@": join(__dirname, "./src"),
16 | "@components": join(__dirname, "./src/components"),
17 | }, // +++
18 | },
19 | plugins: [
20 | vue(),
21 | electron({
22 | main: {
23 | entry: "electron/main/index.ts",
24 | vite: withDebug({
25 | build: {
26 | outDir: "dist/electron/main",
27 | },
28 | }),
29 | },
30 | preload: {
31 | input: {
32 | // You can configure multiple preload here
33 | index: join(__dirname, "electron/preload/index.ts"),
34 | },
35 | vite: {
36 | build: {
37 | // For Debug
38 | sourcemap: "inline",
39 | outDir: "dist/electron/preload",
40 | },
41 | },
42 | },
43 | // Enables use of Node.js API in the Renderer-process
44 | renderer: {},
45 | }),
46 | ],
47 | server: {
48 | host: pkg.env.VITE_DEV_SERVER_HOST,
49 | port: pkg.env.VITE_DEV_SERVER_PORT,
50 | },
51 | });
52 |
53 | function withDebug(config: UserConfig): UserConfig {
54 | if (process.env.VSCODE_DEBUG) {
55 | if (!config.build) config.build = {};
56 | config.build.sourcemap = true;
57 | config.plugins = (config.plugins || []).concat({
58 | name: "electron-vite-debug",
59 | configResolved(config) {
60 | const index = config.plugins.findIndex(
61 | (p) => p.name === "electron-main-watcher"
62 | );
63 | // At present, Vite can only modify plugins in configResolved hook.
64 | (config.plugins as Plugin[]).splice(index, 1);
65 | },
66 | });
67 | }
68 | return config;
69 | }
70 |
--------------------------------------------------------------------------------
/vite.config.ts.js:
--------------------------------------------------------------------------------
1 | // vite.config.ts
2 | import { rmSync } from "fs";
3 | import { join } from "path";
4 | import { defineConfig } from "vite";
5 | import vue from "@vitejs/plugin-vue";
6 | import electron from "vite-plugin-electron";
7 |
8 | // package.json
9 | var package_default = {
10 | name: "tts-vue",
11 | version: "1.9.15",
12 | main: "dist/electron/main/index.js",
13 | description: "\u{1F3A4} \u5FAE\u8F6F\u8BED\u97F3\u5408\u6210\u5DE5\u5177\uFF0C\u4F7F\u7528 Electron + Vue + ElementPlus + Vite \u6784\u5EFA\u3002",
14 | author: "\u6CAB\u96E2Loker ",
15 | license: "MIT",
16 | private: true,
17 | type: "module",
18 | scripts: {
19 | dev: "vite",
20 | build: "vue-tsc --noEmit && vite build && electron-builder"
21 | },
22 | engines: {
23 | node: ">=14.17.0"
24 | },
25 | devDependencies: {
26 | "@vitejs/plugin-vue": "^2.3.3",
27 | electron: "^19.1.9",
28 | "electron-builder": "^23.1.0",
29 | typescript: "^4.7.4",
30 | vite: "^2.9.13",
31 | "vite-plugin-electron": "^0.8.1",
32 | vue: "^3.2.37",
33 | "vue-tsc": "^0.38.3"
34 | },
35 | env: {
36 | VITE_DEV_SERVER_HOST: "127.0.0.1",
37 | VITE_DEV_SERVER_PORT: 3344
38 | },
39 | keywords: [
40 | "electron",
41 | "rollup",
42 | "vite",
43 | "vue3",
44 | "vue"
45 | ],
46 | dependencies: {
47 | "@types/ws": "^8.5.4",
48 | axios: "^0.27.2",
49 | "electron-log": "^4.4.8",
50 | "electron-store": "^8.0.2",
51 | "element-plus": "2.2.9",
52 | "microsoft-cognitiveservices-speech-sdk": "^1.30.1",
53 | "nodejs-websocket": "^1.7.2",
54 | pinia: "^2.0.17",
55 | uuid: "^8.3.2",
56 | "vue-i18n": "^9.6.5",
57 | ws: "^8.13.0"
58 | }
59 | };
60 |
61 | // vite.config.ts
62 | rmSync("dist", { recursive: true, force: true });
63 | var vite_config_default = defineConfig({
64 | resolve: {
65 | alias: {
66 | "@": join("D:\\xampp\\htdocs\\Transformes\\tts-vue", "./src"),
67 | "@components": join("D:\\xampp\\htdocs\\Transformes\\tts-vue", "./src/components")
68 | }
69 | },
70 | plugins: [
71 | vue(),
72 | electron({
73 | main: {
74 | entry: "electron/main/index.ts",
75 | vite: withDebug({
76 | build: {
77 | outDir: "dist/electron/main"
78 | }
79 | })
80 | },
81 | preload: {
82 | input: {
83 | index: join("D:\\xampp\\htdocs\\Transformes\\tts-vue", "electron/preload/index.ts")
84 | },
85 | vite: {
86 | build: {
87 | sourcemap: "inline",
88 | outDir: "dist/electron/preload"
89 | }
90 | }
91 | },
92 | renderer: {}
93 | })
94 | ],
95 | server: {
96 | host: package_default.env.VITE_DEV_SERVER_HOST,
97 | port: package_default.env.VITE_DEV_SERVER_PORT
98 | }
99 | });
100 | function withDebug(config) {
101 | if (process.env.VSCODE_DEBUG) {
102 | if (!config.build)
103 | config.build = {};
104 | config.build.sourcemap = true;
105 | config.plugins = (config.plugins || []).concat({
106 | name: "electron-vite-debug",
107 | configResolved(config2) {
108 | const index = config2.plugins.findIndex(
109 | (p) => p.name === "electron-main-watcher"
110 | );
111 | config2.plugins.splice(index, 1);
112 | }
113 | });
114 | }
115 | return config;
116 | }
117 | export {
118 | vite_config_default as default
119 | };
120 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB7IHJtU3luYyB9IGZyb20gXCJmc1wiO1xyXG5pbXBvcnQgeyBqb2luIH0gZnJvbSBcInBhdGhcIjtcclxuaW1wb3J0IHsgZGVmaW5lQ29uZmlnLCBQbHVnaW4sIFVzZXJDb25maWcgfSBmcm9tIFwidml0ZVwiO1xyXG5pbXBvcnQgdnVlIGZyb20gXCJAdml0ZWpzL3BsdWdpbi12dWVcIjtcclxuaW1wb3J0IGVsZWN0cm9uIGZyb20gXCJ2aXRlLXBsdWdpbi1lbGVjdHJvblwiO1xyXG5pbXBvcnQgcGtnIGZyb20gXCIuL3BhY2thZ2UuanNvblwiO1xyXG5cclxucm1TeW5jKFwiZGlzdFwiLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSk7IC8vIHYxNC4xNC4wXHJcblxyXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gIHJlc29sdmU6IHtcclxuICAgIC8vICsrK1xyXG4gICAgYWxpYXM6IHtcclxuICAgICAgXCJAXCI6IGpvaW4oXCJEOlxcXFx4YW1wcFxcXFxodGRvY3NcXFxcVHJhbnNmb3JtZXNcXFxcdHRzLXZ1ZVwiLCBcIi4vc3JjXCIpLFxyXG4gICAgICBcIkBjb21wb25lbnRzXCI6IGpvaW4oXCJEOlxcXFx4YW1wcFxcXFxodGRvY3NcXFxcVHJhbnNmb3JtZXNcXFxcdHRzLXZ1ZVwiLCBcIi4vc3JjL2NvbXBvbmVudHNcIiksXHJcbiAgICB9LCAvLyArKytcclxuICB9LFxyXG4gIHBsdWdpbnM6IFtcclxuICAgIHZ1ZSgpLFxyXG4gICAgZWxlY3Ryb24oe1xyXG4gICAgICBtYWluOiB7XHJcbiAgICAgICAgZW50cnk6IFwiZWxlY3Ryb24vbWFpbi9pbmRleC50c1wiLFxyXG4gICAgICAgIHZpdGU6IHdpdGhEZWJ1Zyh7XHJcbiAgICAgICAgICBidWlsZDoge1xyXG4gICAgICAgICAgICBvdXREaXI6IFwiZGlzdC9lbGVjdHJvbi9tYWluXCIsXHJcbiAgICAgICAgICB9LFxyXG4gICAgICAgIH0pLFxyXG4gICAgICB9LFxyXG4gICAgICBwcmVsb2FkOiB7XHJcbiAgICAgICAgaW5wdXQ6IHtcclxuICAgICAgICAgIC8vIFlvdSBjYW4gY29uZmlndXJlIG11bHRpcGxlIHByZWxvYWQgaGVyZVxyXG4gICAgICAgICAgaW5kZXg6IGpvaW4oXCJEOlxcXFx4YW1wcFxcXFxodGRvY3NcXFxcVHJhbnNmb3JtZXNcXFxcdHRzLXZ1ZVwiLCBcImVsZWN0cm9uL3ByZWxvYWQvaW5kZXgudHNcIiksXHJcbiAgICAgICAgfSxcclxuICAgICAgICB2aXRlOiB7XHJcbiAgICAgICAgICBidWlsZDoge1xyXG4gICAgICAgICAgICAvLyBGb3IgRGVidWdcclxuICAgICAgICAgICAgc291cmNlbWFwOiBcImlubGluZVwiLFxyXG4gICAgICAgICAgICBvdXREaXI6IFwiZGlzdC9lbGVjdHJvbi9wcmVsb2FkXCIsXHJcbiAgICAgICAgICB9LFxyXG4gICAgICAgIH0sXHJcbiAgICAgIH0sXHJcbiAgICAgIC8vIEVuYWJsZXMgdXNlIG9mIE5vZGUuanMgQVBJIGluIHRoZSBSZW5kZXJlci1wcm9jZXNzXHJcbiAgICAgIHJlbmRlcmVyOiB7fSxcclxuICAgIH0pLFxyXG4gIF0sXHJcbiAgc2VydmVyOiB7XHJcbiAgICBob3N0OiBwa2cuZW52LlZJVEVfREVWX1NFUlZFUl9IT1NULFxyXG4gICAgcG9ydDogcGtnLmVudi5WSVRFX0RFVl9TRVJWRVJfUE9SVCxcclxuICB9LFxyXG59KTtcclxuXHJcbmZ1bmN0aW9uIHdpdGhEZWJ1Zyhjb25maWc6IFVzZXJDb25maWcpOiBVc2VyQ29uZmlnIHtcclxuICBpZiAocHJvY2Vzcy5lbnYuVlNDT0RFX0RFQlVHKSB7XHJcbiAgICBpZiAoIWNvbmZpZy5idWlsZCkgY29uZmlnLmJ1aWxkID0ge307XHJcbiAgICBjb25maWcuYnVpbGQuc291cmNlbWFwID0gdHJ1ZTtcclxuICAgIGNvbmZpZy5wbHVnaW5zID0gKGNvbmZpZy5wbHVnaW5zIHx8IFtdKS5jb25jYXQoe1xyXG4gICAgICBuYW1lOiBcImVsZWN0cm9uLXZpdGUtZGVidWdcIixcclxuICAgICAgY29uZmlnUmVzb2x2ZWQoY29uZmlnKSB7XHJcbiAgICAgICAgY29uc3QgaW5kZXggPSBjb25maWcucGx1Z2lucy5maW5kSW5kZXgoXHJcbiAgICAgICAgICAocCkgPT4gcC5uYW1lID09PSBcImVsZWN0cm9uLW1haW4td2F0Y2hlclwiXHJcbiAgICAgICAgKTtcclxuICAgICAgICAvLyBBdCBwcmVzZW50LCBWaXRlIGNhbiBvbmx5IG1vZGlmeSBwbHVnaW5zIGluIGNvbmZpZ1Jlc29sdmVkIGhvb2suXHJcbiAgICAgICAgKGNvbmZpZy5wbHVnaW5zIGFzIFBsdWdpbltdKS5zcGxpY2UoaW5kZXgsIDEpO1xyXG4gICAgICB9LFxyXG4gICAgfSk7XHJcbiAgfVxyXG4gIHJldHVybiBjb25maWc7XHJcbn1cclxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBLFNBQVMsY0FBYztBQUN2QixTQUFTLFlBQVk7QUFDckIsU0FBUyxvQkFBd0M7QUFDakQsT0FBTyxTQUFTO0FBQ2hCLE9BQU8sY0FBYzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFHckIsT0FBTyxRQUFRLEVBQUUsV0FBVyxNQUFNLE9BQU8sS0FBSyxDQUFDO0FBRy9DLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVM7QUFBQSxJQUVQLE9BQU87QUFBQSxNQUNMLEtBQUssS0FBSywyQ0FBMkMsT0FBTztBQUFBLE1BQzVELGVBQWUsS0FBSywyQ0FBMkMsa0JBQWtCO0FBQUEsSUFDbkY7QUFBQSxFQUNGO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsSUFDSixTQUFTO0FBQUEsTUFDUCxNQUFNO0FBQUEsUUFDSixPQUFPO0FBQUEsUUFDUCxNQUFNLFVBQVU7QUFBQSxVQUNkLE9BQU87QUFBQSxZQUNMLFFBQVE7QUFBQSxVQUNWO0FBQUEsUUFDRixDQUFDO0FBQUEsTUFDSDtBQUFBLE1BQ0EsU0FBUztBQUFBLFFBQ1AsT0FBTztBQUFBLFVBRUwsT0FBTyxLQUFLLDJDQUEyQywyQkFBMkI7QUFBQSxRQUNwRjtBQUFBLFFBQ0EsTUFBTTtBQUFBLFVBQ0osT0FBTztBQUFBLFlBRUwsV0FBVztBQUFBLFlBQ1gsUUFBUTtBQUFBLFVBQ1Y7QUFBQSxRQUNGO0FBQUEsTUFDRjtBQUFBLE1BRUEsVUFBVSxDQUFDO0FBQUEsSUFDYixDQUFDO0FBQUEsRUFDSDtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sTUFBTSxnQkFBSSxJQUFJO0FBQUEsSUFDZCxNQUFNLGdCQUFJLElBQUk7QUFBQSxFQUNoQjtBQUNGLENBQUM7QUFFRCxTQUFTLFVBQVUsUUFBZ0M7QUFDakQsTUFBSSxRQUFRLElBQUksY0FBYztBQUM1QixRQUFJLENBQUMsT0FBTztBQUFPLGFBQU8sUUFBUSxDQUFDO0FBQ25DLFdBQU8sTUFBTSxZQUFZO0FBQ3pCLFdBQU8sV0FBVyxPQUFPLFdBQVcsQ0FBQyxHQUFHLE9BQU87QUFBQSxNQUM3QyxNQUFNO0FBQUEsTUFDTixlQUFlQSxTQUFRO0FBQ3JCLGNBQU0sUUFBUUEsUUFBTyxRQUFRO0FBQUEsVUFDM0IsQ0FBQyxNQUFNLEVBQUUsU0FBUztBQUFBLFFBQ3BCO0FBRUEsUUFBQ0EsUUFBTyxRQUFxQixPQUFPLE9BQU8sQ0FBQztBQUFBLE1BQzlDO0FBQUEsSUFDRixDQUFDO0FBQUEsRUFDSDtBQUNBLFNBQU87QUFDVDsiLAogICJuYW1lcyI6IFsiY29uZmlnIl0KfQo=
121 |
--------------------------------------------------------------------------------