├── .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 | [![Star History Chart](https://api.star-history.com/svg?repos=LokerL/tts-vue&type=Date)](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 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LokerL/tts-vue/23579190646e9ff4b3c4b61a1348baf488ae7002/src/assets/vue.png -------------------------------------------------------------------------------- /src/components/aside/Aside.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 44 | 45 | 69 | -------------------------------------------------------------------------------- /src/components/aside/Version.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 128 | 129 | 146 | 169 | -------------------------------------------------------------------------------- /src/components/configpage/BiliBtn.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | 78 | -------------------------------------------------------------------------------- /src/components/configpage/ConfigPage.vue: -------------------------------------------------------------------------------- 1 | 190 | 191 | 337 | 338 | 405 | -------------------------------------------------------------------------------- /src/components/configpage/Donate.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 53 | 54 | 145 | -------------------------------------------------------------------------------- /src/components/configpage/GiteeBtn.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | 26 | 82 | -------------------------------------------------------------------------------- /src/components/configpage/GithubBtn.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 27 | 28 | 63 | -------------------------------------------------------------------------------- /src/components/footer/Footer.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 71 | 72 | 100 | -------------------------------------------------------------------------------- /src/components/header/Header.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 70 | 71 | 93 | -------------------------------------------------------------------------------- /src/components/header/Logo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 90 | -------------------------------------------------------------------------------- /src/components/main/Loading.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 75 | 76 | 132 | -------------------------------------------------------------------------------- /src/components/main/Main.vue: -------------------------------------------------------------------------------- 1 | 146 | 147 | 249 | 250 | 317 | -------------------------------------------------------------------------------- /src/components/main/MainOptions.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------