From 5949d0870c1a89b971cf01b8604c1c60473be266 Mon Sep 17 00:00:00 2001 From: nae Date: Sun, 21 Jul 2024 21:12:57 -0500 Subject: [PATCH] add yukumo and tiktok voices for TTS --- .github/workflows/build.yml | 2 +- electron/main/index.ts | 2 +- electron/main/modules/transformers.ts | 15 +- package-lock.json | 17 +- package.json | 4 +- src/components/Footer.vue | 2 +- .../connections/dialogs/ConnectionDialog.vue | 3 + src/constants/voices/tiktok.ts | 457 ++++++++++++++++++ src/constants/voices/yukumo.ts | 170 +++++++ src/helpers/fetch.ts | 18 + src/pages/settings/TTS.vue | 44 +- src/stores/default.ts | 2 + src/stores/speech.ts | 88 +++- src/worker.mjs | 4 +- 14 files changed, 784 insertions(+), 44 deletions(-) create mode 100644 src/constants/voices/tiktok.ts create mode 100644 src/constants/voices/yukumo.ts create mode 100644 src/helpers/fetch.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bba5bff2..511a8da4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: 20.x - name: Install Dependencies run: npm install diff --git a/electron/main/index.ts b/electron/main/index.ts index c850386d..46895f88 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -229,4 +229,4 @@ ipcMain.on('transformers-translate', async (event, args) => { // transformersWorker.postMessage({ type: 'transformers-translate', data: args }) const translator = new TranslationPipeline() translator.translate(win, args) -}) \ No newline at end of file +}) diff --git a/electron/main/modules/transformers.ts b/electron/main/modules/transformers.ts index c4f16f67..42fe6c7e 100644 --- a/electron/main/modules/transformers.ts +++ b/electron/main/modules/transformers.ts @@ -1,5 +1,6 @@ // see: https://github.com/xenova/transformers.js -import { PipelineType, pipeline } from '@xenova/transformers' +import type { PipelineType } from '@xenova/transformers' +import { pipeline } from '@xenova/transformers' export class TranslationPipeline { static task: PipelineType = 'translation' @@ -16,15 +17,15 @@ export class TranslationPipeline { async translate(win: any, data: any) { // call translator. downloads and caches model if first load const translator = await TranslationPipeline.getInstance((x: any) => { - // self.postMessage(x) - win.webContents.send('transformers-translate-render', x) + // self.postMessage(x) + win.webContents.send('transformers-translate-render', x) }) - + // console.log(data) const output = await translator(data.text, { tgt_lang: data.tgt_lang, src_lang: data.src_lang, - + // partial outputs callback_function: (x: any) => { win.webContents.send('transformers-translate-render', { @@ -34,7 +35,7 @@ export class TranslationPipeline { }) }, }) - + // send back to main thread win.webContents.send('transformers-translate-render', { status: 'complete', @@ -70,4 +71,4 @@ export class TranslationPipeline { // output, // index: event.data.index, // }) -// }) \ No newline at end of file +// }) diff --git a/package-lock.json b/package-lock.json index 83f9f7b1..8fa65ac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ "pinia": "^2.1.7", "roboto-fontface": "^0.10.0", "vue-i18n": "^9.13.1", - "vue-router": "^4.3.2", - "vuetify": "^3.6.5", + "vue-router": "^4.4.0", + "vuetify": "^3.6.13", "webfontloader": "^1.6.28", "ws": "^8.17.0" }, @@ -553,7 +553,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -9554,9 +9553,9 @@ } }, "node_modules/vue-router": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", - "integrity": "sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", + "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", "dependencies": { "@vue/devtools-api": "^6.5.1" }, @@ -9610,9 +9609,9 @@ } }, "node_modules/vuetify": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.5.tgz", - "integrity": "sha512-YrHTM1vb7UllAtfH9tWfTo1wYMjyCSybu4WtXrfMRpMwAaZWgfrMmqD/4Tc+0KqDsDsYMXaYs0nJ6HtdMJZbyA==", + "version": "3.6.13", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.13.tgz", + "integrity": "sha512-Gz7jxXAkmff2m6CM0EUWOo/72TM322/3I6aDna++k1nPOW1/hNx4td1MZG4u75fzdn3r+uIe0dbF7SWuhu6DWA==", "engines": { "node": "^12.20 || >=14.13" }, diff --git a/package.json b/package.json index 8f783648..ad2bc780 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "pinia": "^2.1.7", "roboto-fontface": "^0.10.0", "vue-i18n": "^9.13.1", - "vue-router": "^4.3.2", - "vuetify": "^3.6.5", + "vue-router": "^4.4.0", + "vuetify": "^3.6.13", "webfontloader": "^1.6.28", "ws": "^8.17.0" }, diff --git a/src/components/Footer.vue b/src/components/Footer.vue index 6f52b90f..21af958c 100644 --- a/src/components/Footer.vue +++ b/src/components/Footer.vue @@ -16,7 +16,7 @@ - +
{{ $t('settings.connections.wh.description') }} + + => { transcript: 'text here' } + diff --git a/src/constants/voices/tiktok.ts b/src/constants/voices/tiktok.ts new file mode 100644 index 00000000..6cf4c967 --- /dev/null +++ b/src/constants/voices/tiktok.ts @@ -0,0 +1,457 @@ +export const api = 'https://tiktok-tts.weilnet.workers.dev/api/generation' + +export const voices = [ + { + lang: 'en_us_002', + name: 'Jessie [Female]', + local_service: false, + }, + { + lang: 'en_au_001', + name: 'Metro (Eddie) [Female]', + local_service: false, + }, + { + lang: 'en_au_002', + name: 'Smooth (Alex) [Male]', + local_service: false, + }, + { + lang: 'en_uk_001', + name: 'Narrator (Chris) [Male]', + local_service: false, + }, + { + lang: 'en_uk_003', + name: 'UK Male 2', + local_service: false, + }, + { + lang: 'en_female_emotional', + name: 'Peaceful [Female]', + local_service: false, + }, + { + lang: 'en_us_006', + name: 'Joey [Male]', + local_service: false, + }, + { + lang: 'en_us_007', + name: 'Professor [Male]', + local_service: false, + }, + { + lang: 'en_us_009', + name: 'Scientist [Male]', + local_service: false, + }, + { + lang: 'en_us_010', + name: 'Confidence [Male]', + local_service: false, + }, + { + lang: 'en_female_samc', + name: 'Empathetic [Female]', + local_service: false, + }, + { + lang: 'en_male_cody', + name: 'Serious [Male]', + local_service: false, + }, + { + lang: 'en_male_narration', + name: 'Story Teller [Male]', + local_service: false, + }, + { + lang: 'en_male_funny', + name: 'Wacky [Male]', + local_service: false, + }, + { + lang: 'en_male_jarvis', + name: 'Alfred', + local_service: false, + }, + { + lang: 'en_male_santa_narration', + name: 'Author [Male]', + local_service: false, + }, + { + lang: 'en_female_betty', + name: 'Bae [Female]', + local_service: false, + }, + { + lang: 'en_female_makeup', + name: 'Beauty Guru [Female]', + local_service: false, + }, + { + lang: 'en_female_richgirl', + name: 'Bestie [Female]', + local_service: false, + }, + { + lang: 'en_male_cupid', + name: 'Cupid', + local_service: false, + }, + { + lang: 'en_female_shenna', + name: 'Debutante [Female]', + local_service: false, + }, + { + lang: 'en_male_ghosthost', + name: 'Ghost Host', + local_service: false, + }, + { + lang: 'en_female_grandma', + name: 'Grandma', + local_service: false, + }, + { + lang: 'en_male_ukneighbor', + name: 'Lord Cringe', + local_service: false, + }, + { + lang: 'en_male_wizard', + name: 'Magician', + local_service: false, + }, + { + lang: 'en_male_trevor', + name: 'Marty', + local_service: false, + }, + { + lang: 'en_male_deadpool', + name: 'Mr. GoodGuy (Deadpool)', + local_service: false, + }, + { + lang: 'en_male_ukbutler', + name: 'Mr. Meticulous', + local_service: false, + }, + { + lang: 'en_male_pirate', + name: 'Pirate', + local_service: false, + }, + { + lang: 'en_male_santa', + name: 'Santa', + local_service: false, + }, + { + lang: 'en_male_santa_effect', + name: 'Santa (w/ effect)', + local_service: false, + }, + { + lang: 'en_female_pansino', + name: 'Varsity [Female]', + local_service: false, + }, + { + lang: 'en_male_grinch', + name: 'Trickster (Grinch)', + local_service: false, + }, + { + lang: 'en_us_ghostface', + name: 'Ghostface (Scream)', + local_service: false, + }, + { + lang: 'en_us_chewbacca', + name: 'Chewbacca (Star Wars)', + local_service: false, + }, + { + lang: 'en_us_c3po', + name: 'C-3PO (Star Wars)', + local_service: false, + }, + { + lang: 'en_us_stormtrooper', + name: 'Stormtrooper (Star Wars)', + local_service: false, + }, + { + lang: 'en_us_stitch', + name: 'Stitch (Lilo & Stitch)', + local_service: false, + }, + { + lang: 'en_us_rocket', + name: 'Rocket (Guardians of the Galaxy)', + local_service: false, + }, + { + lang: 'en_female_madam_leota', + name: 'Madame Leota (Haunted Mansion)', + local_service: false, + }, + { + lang: 'en_male_sing_deep_jingle', + name: 'Song: Caroler', + local_service: false, + }, + { + lang: 'en_male_m03_classical', + name: 'Song: Classic Electric', + local_service: false, + }, + { + lang: 'en_female_f08_salut_damour', + name: 'Song: Cottagecore (Salut d\'Amour)', + local_service: false, + }, + { + lang: 'en_male_m2_xhxs_m03_christmas', + name: 'Song: Cozy', + local_service: false, + }, + { + lang: 'en_female_f08_warmy_breeze', + name: 'Song: Open Mic (Warmy Breeze)', + local_service: false, + }, + { + lang: 'en_female_ht_f08_halloween', + name: 'Song: Opera (Halloween)', + local_service: false, + }, + { + lang: 'en_female_ht_f08_glorious', + name: 'Song: Euphoric (Glorious)', + local_service: false, + }, + { + lang: 'en_male_sing_funny_it_goes_up', + name: 'Song: Hypetrain (It Goes Up)', + local_service: false, + }, + { + lang: 'en_male_m03_lobby', + name: 'Song: Jingle (Lobby)', + local_service: false, + }, + { + lang: 'en_female_ht_f08_wonderful_world', + name: 'Song: Melodrama (Wonderful World)', + local_service: false, + }, + { + lang: 'en_female_ht_f08_newyear', + name: 'Song: NYE 2023', + local_service: false, + }, + { + lang: 'en_male_sing_funny_thanksgiving', + name: 'Song: Thanksgiving', + local_service: false, + }, + { + lang: 'en_male_m03_sunshine_soon', + name: 'Song: Toon Beat (Sunshine Soon)', + local_service: false, + }, + { + lang: 'en_female_f08_twinkle', + name: 'Song: Pop Lullaby', + local_service: false, + }, + { + lang: 'en_male_m2_xhxs_m03_silly', + name: 'Song: Quirky Time', + local_service: false, + }, + { + lang: 'fr_001', + name: 'French Male 1', + local_service: false, + }, + { + lang: 'fr_002', + name: 'French Male 2', + local_service: false, + }, + { + lang: 'de_001', + name: 'German Female', + local_service: false, + }, + { + lang: 'de_002', + name: 'German Male', + local_service: false, + }, + { + lang: 'id_male_darma', + name: 'Darma (Indonesian)', + local_service: false, + }, + { + lang: 'id_female_icha', + name: 'Icha (Indonesian)', + local_service: false, + }, + { + lang: 'id_female_noor', + name: 'Noor (Indonesian)', + local_service: false, + }, + { + lang: 'id_male_putra', + name: 'Putra (Indonesian)', + local_service: false, + }, + { + lang: 'it_male_m18', + name: 'Italian Male', + local_service: false, + }, + { + lang: 'jp_001', + name: 'Miho (美穂)', + local_service: false, + }, + { + lang: 'jp_003', + name: 'Keiko (恵子)', + local_service: false, + }, + { + lang: 'jp_005', + name: 'Sakura (さくら)', + local_service: false, + }, + { + lang: 'jp_006', + name: 'Naoki (直樹)', + local_service: false, + }, + { + lang: 'jp_male_osada', + name: 'モリスケ (Morisuke)', + local_service: false, + }, + { + lang: 'jp_male_matsuo', + name: 'モジャオ (Matsuo)', + local_service: false, + }, + { + lang: 'jp_female_machikoriiita', + name: 'まちこりーた (Machikoriiita)', + local_service: false, + }, + { + lang: 'jp_male_matsudake', + name: 'マツダ家の日常 (Matsudake)', + local_service: false, + }, + { + lang: 'jp_male_shuichiro', + name: '修一朗 (Shuichiro)', + local_service: false, + }, + { + lang: 'jp_female_rei', + name: '丸山礼 (Maruyama Rei)', + local_service: false, + }, + { + lang: 'jp_male_hikakin', + name: 'ヒカキン (Hikakin)', + local_service: false, + }, + { + lang: 'jp_female_yagishaki', + name: '八木沙季 (Yagi Saki)', + local_service: false, + }, + { + lang: 'kr_002', + name: 'Korean Male 1', + local_service: false, + }, + { + lang: 'kr_004', + name: 'Korean Male 2', + local_service: false, + }, + { + lang: 'kr_003', + name: 'Korean Female', + local_service: false, + }, + { + lang: 'br_003', + name: 'Júlia (Portuguese)', + local_service: false, + }, + { + lang: 'br_004', + name: 'Ana (Portuguese)', + local_service: false, + }, + { + lang: 'br_005', + name: 'Lucas (Portuguese)', + local_service: false, + }, + { + lang: 'pt_female_lhays', + name: 'Lhays Macedo (Portuguese)', + local_service: false, + }, + { + lang: 'pt_female_laizza', + name: 'Laizza (Portuguese)', + local_service: false, + }, + { + lang: 'es_002', + name: 'Spanish Male', + local_service: false, + }, + { + lang: 'es_male_m3', + name: 'Julio (Spanish)', + local_service: false, + }, + { + lang: 'es_female_f6', + name: 'Alejandra (Spanish)', + local_service: false, + }, + { + lang: 'es_female_fp1', + name: 'Mariana (Spanish)', + local_service: false, + }, + { + lang: 'es_mx_002', + name: 'Álex (Warm) (Mexican)', + local_service: false, + }, + { + lang: 'es_mx_female_supermom', + name: 'Super Mamá (Mexican)', + local_service: false, + }, + +] + +export default { api, voices } diff --git a/src/constants/voices/yukumo.ts b/src/constants/voices/yukumo.ts new file mode 100644 index 00000000..4189e746 --- /dev/null +++ b/src/constants/voices/yukumo.ts @@ -0,0 +1,170 @@ +// at1 = aqtk1 +// at2 = aqtk2 +// at10 = aqtk10 + +export const api = 'https://www.yukumo.net/api/v2' + +export const sub_type = { + at1: 'aqtk1', + at2: 'aqtk2', + at10: 'aqtk10', +} + +export function build_api(name: string, input: string) { + return `${api}/${(sub_type as any)[name.split('-')[0]]}/koe.mp3?type=${name.split('-')[1]}&kanji=${encodeURIComponent(input)}` +} + +export const voices = [ + { + lang: 'ja-JP', + // voice: 'at1', + name: 'at1-f1', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-f2', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-m1', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-m2', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-dvd', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-imd4', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-jgr', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at1-r1', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-rm', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-f1c', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-f3a', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-huskey', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-m4b', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-mf1', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-rb2', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-rb3', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-robo', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-yukkuri', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-f4', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-m5', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-mf2', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at2-rm3', + local_service: false, + }, + { + lang: 'ja-JP', + name: 'at10-f1', + local_service: false, + // options: { + // speed: 100, + // volume: 100, + // pitch: 100, + // accent: 100, + // lmd: 100, + // fsc: 100, + // }, + }, + { + lang: 'ja-JP', + name: 'at10-f2', + local_service: false, + // options: { + // speed: 100, + // volume: 100, + // pitch: 77, + // accent: 150, + // lmd: 100, + // fsc: 100, + // }, + }, + { + lang: 'ja-JP', + name: 'at10-m1', + local_service: false, + // options: { + // speed: 100, + // volume: 100, + // pitch: 30, + // accent: 100, + // lmd: 100, + // fsc: 100, + // }, + }, +] + +export default { api, build_api, sub_type, voices } diff --git a/src/helpers/fetch.ts b/src/helpers/fetch.ts new file mode 100644 index 00000000..d36161f0 --- /dev/null +++ b/src/helpers/fetch.ts @@ -0,0 +1,18 @@ +export async function post(url: string, body: any) { + return new Promise((resolve, reject) => { + fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }).then(async (response) => { + response.json().then(json => resolve(json)) + }).catch((err) => { + reject(err) + }) + }) +} + +export default { post } diff --git a/src/pages/settings/TTS.vue b/src/pages/settings/TTS.vue index 4793e204..903ce057 100644 --- a/src/pages/settings/TTS.vue +++ b/src/pages/settings/TTS.vue @@ -19,17 +19,19 @@ @@ -100,7 +103,7 @@
{{ language.name }}
- +
@@ -112,30 +115,43 @@ diff --git a/src/stores/default.ts b/src/stores/default.ts index 81c39f25..f82b81d7 100644 --- a/src/stores/default.ts +++ b/src/stores/default.ts @@ -13,6 +13,7 @@ export const useDefaultStore = defineStore('default', () => { const connections = ref(0) const typing_limited = ref(false) const speech = ref({}) + const audio = ref(new Audio()) const snackbar = ref({ enabled: false, type: 'error', @@ -85,6 +86,7 @@ export const useDefaultStore = defineStore('default', () => { connections, typing_limited, speech, + audio, snackbar, show_snackbar, toggle_broadcast, diff --git a/src/stores/speech.ts b/src/stores/speech.ts index f5f6ed0d..a6096d95 100644 --- a/src/stores/speech.ts +++ b/src/stores/speech.ts @@ -8,9 +8,12 @@ import { useTranslationStore } from '@/stores/translation' import { useConnectionsStore } from '@/stores/connections' import { useWordReplaceStore } from '@/stores/word_replace' import webhook from '@/helpers/webhook' +import { post } from '@/helpers/fetch' import is_electron from '@/helpers/is_electron' import { i18n } from '@/plugins/i18n' import { WebSpeech } from '@/modules/speech' +import yukumo from '@/constants/voices/yukumo' +import tiktok from '@/constants/voices/tiktok' export interface ListItem { title: string @@ -35,10 +38,7 @@ export const useSpeechStore = defineStore('speech', () => { }) const tts = ref({ enabled: false, - type: { - title: 'Web Speech API', - value: 'webspeech', - }, + type: 'webspeech', voice: '', rate: 1, pitch: 1, @@ -108,9 +108,54 @@ export const useSpeechStore = defineStore('speech', () => { } } - function speak(input: string) { - const { speech } = useDefaultStore() - speech.speak(input) + async function speak(input: string) { + let response: any + const defaultStore = useDefaultStore() + switch (tts.value.type) { + case 'tiktok': + const body = { + text: input, + voice: tiktok.voices.find(voice => voice.name === tts.value.voice)?.lang, + } + try { + response = await post(tiktok.api, body) + } + catch (e) { + console.error(e) + response = await post(tiktok.api, body) + } + if (!defaultStore.audio.src || defaultStore.audio.ended) { + defaultStore.audio.src = `data:audio/mpeg;base64,${response.data}` + defaultStore.audio.play() + } + else { + defaultStore.audio.onended = function () { + defaultStore.audio.src = `data:audio/mpeg;base64,${response.data}` + defaultStore.audio.play() + defaultStore.audio.onended = null + } + } + break + + case 'webspeech': + const { speech } = useDefaultStore() + speech.speak(input) + break + + case 'yukumo': + if (!defaultStore.audio.src || defaultStore.audio.ended) { + defaultStore.audio.src = yukumo.build_api(tts.value.voice, input) + defaultStore.audio.play() + } + else { + defaultStore.audio.onended = function () { + defaultStore.audio.src = yukumo.build_api(tts.value.voice, input) + defaultStore.audio.play() + defaultStore.audio.onended = null + } + } + break + } } async function on_submit(log: any, index: number) { @@ -231,6 +276,34 @@ export const useSpeechStore = defineStore('speech', () => { } } + // temp + interface Voice { + lang: string + name: string + local_service: boolean + } + function load_voices(option: string): Voice[] { + let voices: Voice[] = [] + switch (option) { + case 'tiktok': + voices = tiktok.voices + break + case 'webspeech': + const synth = window.speechSynthesis + voices = synth.getVoices().map((lang: SpeechSynthesisVoice) => ({ + lang: lang.lang, + name: lang.name, + local_service: lang.localService, + } as Voice)) + break + case 'yukumo': + voices = yukumo.voices + break + } + + return voices + } + function pin_language(selected_language: ListItem) { const pins = pinned_languages @@ -270,6 +343,7 @@ export const useSpeechStore = defineStore('speech', () => { typing_event, speak, on_submit, + load_voices, pin_language, unpin_language, is_pinned_language, diff --git a/src/worker.mjs b/src/worker.mjs index 7095e1b3..8ef72f97 100644 --- a/src/worker.mjs +++ b/src/worker.mjs @@ -39,7 +39,7 @@ parentPort.on('message', async (message) => { status: 'update', output: translator.tokenizer.decode(x[0].output_token_ids, { skip_special_tokens: true }), index: message.data.index, - }}) + } }) }, }) @@ -48,6 +48,6 @@ parentPort.on('message', async (message) => { status: 'complete', output, index: message.data.index, - }}) + } }) } })