diff --git a/package-lock.json b/package-lock.json index 504ebafe..f8dcec5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "AGPL-3.0", "dependencies": { + "@breezystack/lamejs": "^1.2.7", "@mdi/js": "^7.4.47", "@mdi/svg": "^7.4.47", "@nextcloud/auth": "^2.0.0", @@ -21,6 +22,7 @@ "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^9.0.0-rc.2", "extendable-media-recorder": "^9.2.11", + "extendable-media-recorder-wav-encoder": "^7.0.129", "moment": "^2.30.1", "v-click-outside": "^3.2.0", "vue": "^3.5.16", @@ -1661,6 +1663,12 @@ "node": ">=6.9.0" } }, + "node_modules/@breezystack/lamejs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@breezystack/lamejs/-/lamejs-1.2.7.tgz", + "integrity": "sha512-6wc7ck65ctA75Hq7FYHTtTvGnYs6msgdxiSUICQ+A01nVOWg6rqouZB8IdyteRlfpYYiFovkf67dIeOgWIUzTA==", + "license": "LGPL-3.0" + }, "node_modules/@buttercup/fetch": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", @@ -7942,6 +7950,18 @@ "tslib": "^2.8.1" } }, + "node_modules/extendable-media-recorder-wav-encoder": { + "version": "7.0.129", + "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.129.tgz", + "integrity": "sha512-/wqM2hnzvLy/iUlg/EU3JIF8MJcidy8I77Z7CCm5+CVEClDfcs6bH9PgghuisndwKTaud0Dh48RTD83gkfEjCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "extendable-media-recorder-wav-encoder-broker": "^7.0.119", + "extendable-media-recorder-wav-encoder-worker": "^8.0.116", + "tslib": "^2.8.1" + } + }, "node_modules/extendable-media-recorder-wav-encoder-broker": { "version": "7.0.119", "resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.119.tgz", diff --git a/package.json b/package.json index d9129b3e..a49c0087 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "npm": "^10.0.0" }, "dependencies": { + "@breezystack/lamejs": "^1.2.7", "@mdi/js": "^7.4.47", "@mdi/svg": "^7.4.47", "@nextcloud/auth": "^2.0.0", @@ -50,6 +51,7 @@ "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^9.0.0-rc.2", "extendable-media-recorder": "^9.2.11", + "extendable-media-recorder-wav-encoder": "^7.0.129", "moment": "^2.30.1", "v-click-outside": "^3.2.0", "vue": "^3.5.16", diff --git a/src/audioUtils.js b/src/audioUtils.js new file mode 100644 index 00000000..79caff3a --- /dev/null +++ b/src/audioUtils.js @@ -0,0 +1,48 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import * as lamejs from '@breezystack/lamejs' + +export function convertWavToMp3(wavBlob) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + + reader.onload = function() { + const arrayBuffer = this.result + + // Create a WAV decoder + const wavDecoder = lamejs.WavHeader.readHeader(new DataView(arrayBuffer)) + + // Get the WAV audio data as an array of samples + const wavSamples = new Int16Array(arrayBuffer, wavDecoder.dataOffset, wavDecoder.dataLen / 2) + + // Create an MP3 encoder + const mp3Encoder = new lamejs.Mp3Encoder(wavDecoder.channels, wavDecoder.sampleRate, 128) + + // Encode the WAV samples to MP3 + const mp3Buffer = mp3Encoder.encodeBuffer(wavSamples) + + // Finalize the MP3 encoding + const mp3Data = mp3Encoder.flush() + + // Combine the MP3 header and data into a new ArrayBuffer + const mp3BufferWithHeader = new Uint8Array(mp3Buffer.length + mp3Data.length) + mp3BufferWithHeader.set(mp3Buffer, 0) + mp3BufferWithHeader.set(mp3Data, mp3Buffer.length) + + // Create a Blob from the ArrayBuffer + const mp3Blob = new Blob([mp3BufferWithHeader], { type: 'audio/mp3' }) + + resolve(mp3Blob) + } + + reader.onerror = function(error) { + reject(error) + } + + // Read the input blob as an ArrayBuffer + reader.readAsArrayBuffer(wavBlob) + }) +} diff --git a/src/components/AssistantFormOutputs.vue b/src/components/AssistantFormOutputs.vue index ffb7cb81..f01664a4 100644 --- a/src/components/AssistantFormOutputs.vue +++ b/src/components/AssistantFormOutputs.vue @@ -88,7 +88,10 @@ export default { return false }, hasInitialOutput() { - return !!this.outputs.output?.trim() + if (typeof this.outputs?.output === 'string') { + return !!this.outputs.output?.trim() + } + return false }, }, } diff --git a/src/components/AssistantHeaderMenuEntry.vue b/src/components/AssistantHeaderMenuEntry.vue index e0886956..6f623c68 100644 --- a/src/components/AssistantHeaderMenuEntry.vue +++ b/src/components/AssistantHeaderMenuEntry.vue @@ -41,7 +41,7 @@ export default { mounted() { }, - beforeDestroy() { + beforeUnmount() { }, methods: { diff --git a/src/components/TaskList.vue b/src/components/TaskList.vue index 5eb62000..78f9f51b 100644 --- a/src/components/TaskList.vue +++ b/src/components/TaskList.vue @@ -111,7 +111,7 @@ export default { subscribe('assistant:task:updated', this.updateTask) }, - beforeDestroy() { + beforeUnmount() { unsubscribe('assistant:task:updated', this.updateTask) }, diff --git a/src/components/TaskListItem.vue b/src/components/TaskListItem.vue index ea0875db..2763853c 100644 --- a/src/components/TaskListItem.vue +++ b/src/components/TaskListItem.vue @@ -16,6 +16,12 @@ style="margin-right: 8px;" :title="statusTitle" /> +