mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
record audio node support (#4289)
Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
217
package-lock.json
generated
217
package-lock.json
generated
@@ -31,6 +31,8 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
@@ -877,13 +879,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -5724,6 +5723,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/automation-events": {
|
||||
"version": "7.1.11",
|
||||
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-7.1.11.tgz",
|
||||
"integrity": "sha512-TnclbJ0482ydRenzrR9FIbqalHScBBdQTIXv8tVunhYx8dq7E0Eq5v5CSAo67YmLXNbx5jCstHcLZDJ33iONDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
@@ -5974,6 +5986,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/broker-factory": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.7.tgz",
|
||||
"integrity": "sha512-RxbMXWq/Qvw9aLZMvuooMtVTm2/SV9JEpxpBbMuFhYAnDaZxctbJ+1b9ucHxADk/eQNqDijvWQjLVARqExAeyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||
@@ -8330,6 +8354,56 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extendable-media-recorder": {
|
||||
"version": "9.2.27",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder/-/extendable-media-recorder-9.2.27.tgz",
|
||||
"integrity": "sha512-2X+Ixi1cxLek0Cj9x9atmhQ+apG+LwJpP2p3ypP8Pxau0poDnicrg7FTfPVQV5PW/3DHFm/eQ16vbgo5Yk3HGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"media-encoder-host": "^9.0.20",
|
||||
"multi-buffer-data-view": "^6.0.22",
|
||||
"recorder-audio-worklet": "^6.0.48",
|
||||
"standardized-audio-context": "^25.3.77",
|
||||
"subscribable-things": "^2.1.53",
|
||||
"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",
|
||||
"integrity": "sha512-BLrFOnqFLpsmmNpSk/TfjNs4j6ImCSGtoryIpRlqNu5S/Avt6gRJI0s4UYvdK7h17PCi+8vaDr75blvmU1sYlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"broker-factory": "^3.1.7",
|
||||
"extendable-media-recorder-wav-encoder-worker": "^8.0.116",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extendable-media-recorder-wav-encoder-worker": {
|
||||
"version": "8.0.116",
|
||||
"resolved": "https://registry.npmjs.org/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.116.tgz",
|
||||
"integrity": "sha512-bJPR0B7ZHeoqi9YoSie+UXAfEYya3efQ9eLiWuyK4KcOv+SuYQvWCoyzX5kjvb6GqIBCUnev5xulfeHRlyCwvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -8373,6 +8447,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-unique-numbers": {
|
||||
"version": "9.0.22",
|
||||
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.22.tgz",
|
||||
"integrity": "sha512-dBR+30yHAqBGvOuxxQdnn2lTLHCO6r/9B+M4yF8mNrzr3u1yiF+YVJ6u3GTyPN/VRWqaE1FcscZDdBgVKmrmQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
|
||||
@@ -11408,6 +11495,43 @@
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/media-encoder-host": {
|
||||
"version": "9.0.20",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host/-/media-encoder-host-9.0.20.tgz",
|
||||
"integrity": "sha512-IyEYxw6az97RNuETOAZV4YZqNAPOiF9GKIp5mVZb4HOyWd6mhkWQ34ydOzhqAWogMyc4W05kjN/VCgTtgyFmsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"media-encoder-host-broker": "^8.0.19",
|
||||
"media-encoder-host-worker": "^10.0.19",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/media-encoder-host-broker": {
|
||||
"version": "8.0.19",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host-broker/-/media-encoder-host-broker-8.0.19.tgz",
|
||||
"integrity": "sha512-lTpsNuaZdTCdtTHsOyww7Ae0Mwv+7mFS+O4YkFYWhXwVs0rm6XbRK5jRRn5JmcX3n1eTE1lQS5RgX8qbNaIjSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"broker-factory": "^3.1.7",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"media-encoder-host-worker": "^10.0.19",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/media-encoder-host-worker": {
|
||||
"version": "10.0.19",
|
||||
"resolved": "https://registry.npmjs.org/media-encoder-host-worker/-/media-encoder-host-worker-10.0.19.tgz",
|
||||
"integrity": "sha512-I8fwc6f41peER3RFSiwDxnIHbqU7p3pc2ghQozcw9CQfL0mWEo4IjQJtyswrrlL/HO2pgVSMQbaNzE4q/0mfDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"extendable-media-recorder-wav-encoder-broker": "^7.0.119",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
@@ -12182,6 +12306,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multi-buffer-data-view": {
|
||||
"version": "6.0.22",
|
||||
"resolved": "https://registry.npmjs.org/multi-buffer-data-view/-/multi-buffer-data-view-6.0.22.tgz",
|
||||
"integrity": "sha512-SsI/exkodHsh+ofCV7An2PZWRaJC7eFVl7gtHQlMWFEDmWtb7cELr/GK32Nhe/6dZQhbr81o+Moswx9aXN3RRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
@@ -13754,10 +13891,31 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
"node_modules/recorder-audio-worklet": {
|
||||
"version": "6.0.48",
|
||||
"resolved": "https://registry.npmjs.org/recorder-audio-worklet/-/recorder-audio-worklet-6.0.48.tgz",
|
||||
"integrity": "sha512-PVlq/1hjCrPcUGqARg8rR30A303xDCao0jmlBTaUaKkN3Xme58RI7EQxurv8rw2eDwVrN+nrni0UoJoa5/v+zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"broker-factory": "^3.1.7",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"recorder-audio-worklet-processor": "^5.0.35",
|
||||
"standardized-audio-context": "^25.3.77",
|
||||
"subscribable-things": "^2.1.53",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.43"
|
||||
}
|
||||
},
|
||||
"node_modules/recorder-audio-worklet-processor": {
|
||||
"version": "5.0.35",
|
||||
"resolved": "https://registry.npmjs.org/recorder-audio-worklet-processor/-/recorder-audio-worklet-processor-5.0.35.tgz",
|
||||
"integrity": "sha512-5Nzbk/6QzC3QFQ1EG2SE34c1ygLE22lIOvLyjy7N6XxE/jpAZrL4e7xR+yihiTaG3ajiWy6UjqL4XEBMM9ahFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/registry-auth-token": {
|
||||
"version": "5.0.3",
|
||||
@@ -14274,6 +14432,12 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs-interop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs-interop/-/rxjs-interop-2.0.0.tgz",
|
||||
"integrity": "sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sade": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
@@ -14666,6 +14830,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/standardized-audio-context": {
|
||||
"version": "25.3.77",
|
||||
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.77.tgz",
|
||||
"integrity": "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"automation-events": "^7.0.9",
|
||||
"tslib": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -14845,6 +15020,17 @@
|
||||
"integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/subscribable-things": {
|
||||
"version": "2.1.53",
|
||||
"resolved": "https://registry.npmjs.org/subscribable-things/-/subscribable-things-2.1.53.tgz",
|
||||
"integrity": "sha512-zWvN9F/eYQWDKszXl4NXkyqPXvMDZDmXfcHiM5C5WQZTTY2OK+2TZeDlA9oio69FEPqPu9T6yeEcAhQ2uRmnaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"rxjs-interop": "^2.0.0",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
@@ -17374,6 +17560,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-factory": {
|
||||
"version": "7.0.43",
|
||||
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.43.tgz",
|
||||
"integrity": "sha512-SACVoj3gWKtMVyT9N+VD11Pd/Xe58+ZFfp8b7y/PagOvj3i8lU3Uyj+Lj7WYTmSBvNLC0JFaQkx44E6DhH5+WA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"fast-unique-numbers": "^9.0.22",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IStringWidget
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recorder'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
@@ -9,6 +13,7 @@ import { t } from '@/i18n'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import { useAudioService } from '@/services/audioService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { NodeLocatorId } from '@/types'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
@@ -257,3 +262,167 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.RecordAudio',
|
||||
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
AUDIO_RECORD(node, inputName: string) {
|
||||
const audio = document.createElement('audio')
|
||||
audio.controls = true
|
||||
audio.classList.add('comfy-audio')
|
||||
audio.setAttribute('name', 'media')
|
||||
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
|
||||
|
||||
let mediaRecorder: MediaRecorder | null = null
|
||||
let isRecording = false
|
||||
let audioChunks: Blob[] = []
|
||||
let currentStream: MediaStream | null = null
|
||||
let recordWidget: IBaseWidget | null = null
|
||||
|
||||
let stopPromise: Promise<void> | null = null
|
||||
let stopResolve: (() => void) | null = null
|
||||
|
||||
audioUIWidget.serializeValue = async () => {
|
||||
if (isRecording && mediaRecorder) {
|
||||
stopPromise = new Promise((resolve) => {
|
||||
stopResolve = resolve
|
||||
})
|
||||
|
||||
mediaRecorder.stop()
|
||||
|
||||
await stopPromise
|
||||
}
|
||||
|
||||
const audioSrc = audioUIWidget.element.src
|
||||
|
||||
if (!audioSrc) {
|
||||
useToastStore().addAlert(t('g.noAudioRecorded'))
|
||||
return ''
|
||||
}
|
||||
|
||||
const blob = await fetch(audioSrc).then((r) => r.blob())
|
||||
|
||||
return await useAudioService().convertBlobToFileAndSubmit(blob)
|
||||
}
|
||||
|
||||
recordWidget = node.addWidget(
|
||||
'button',
|
||||
inputName,
|
||||
'',
|
||||
async () => {
|
||||
if (!isRecording) {
|
||||
try {
|
||||
currentStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true
|
||||
})
|
||||
|
||||
mediaRecorder = new ExtendableMediaRecorder(currentStream, {
|
||||
mimeType: 'audio/wav'
|
||||
}) as unknown as MediaRecorder
|
||||
|
||||
audioChunks = []
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
audioChunks.push(event.data)
|
||||
}
|
||||
|
||||
mediaRecorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' })
|
||||
|
||||
useAudioService().stopAllTracks(currentStream)
|
||||
|
||||
if (
|
||||
audioUIWidget.element.src &&
|
||||
audioUIWidget.element.src.startsWith('blob:')
|
||||
) {
|
||||
URL.revokeObjectURL(audioUIWidget.element.src)
|
||||
}
|
||||
|
||||
audioUIWidget.element.src = URL.createObjectURL(audioBlob)
|
||||
|
||||
isRecording = false
|
||||
|
||||
if (recordWidget) {
|
||||
recordWidget.label = t('g.startRecording')
|
||||
}
|
||||
|
||||
if (stopResolve) {
|
||||
stopResolve()
|
||||
stopResolve = null
|
||||
stopPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
mediaRecorder.onerror = (event) => {
|
||||
console.error('MediaRecorder error:', event)
|
||||
useAudioService().stopAllTracks(currentStream)
|
||||
isRecording = false
|
||||
|
||||
if (recordWidget) {
|
||||
recordWidget.label = t('g.startRecording')
|
||||
}
|
||||
|
||||
if (stopResolve) {
|
||||
stopResolve()
|
||||
stopResolve = null
|
||||
stopPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
mediaRecorder.start()
|
||||
isRecording = true
|
||||
if (recordWidget) {
|
||||
recordWidget.label = t('g.stopRecording')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error accessing microphone:', err)
|
||||
useToastStore().addAlert(t('g.micPermissionDenied'))
|
||||
|
||||
if (mediaRecorder) {
|
||||
try {
|
||||
mediaRecorder.stop()
|
||||
} catch {}
|
||||
}
|
||||
useAudioService().stopAllTracks(currentStream)
|
||||
currentStream = null
|
||||
isRecording = false
|
||||
if (recordWidget) {
|
||||
recordWidget.label = t('g.startRecording')
|
||||
}
|
||||
}
|
||||
} else if (mediaRecorder && isRecording) {
|
||||
mediaRecorder.stop()
|
||||
}
|
||||
},
|
||||
{ serialize: false }
|
||||
)
|
||||
|
||||
recordWidget.label = t('g.startRecording')
|
||||
|
||||
const originalOnRemoved = node.onRemoved
|
||||
node.onRemoved = function () {
|
||||
if (isRecording && mediaRecorder) {
|
||||
mediaRecorder.stop()
|
||||
}
|
||||
useAudioService().stopAllTracks(currentStream)
|
||||
if (audioUIWidget.element.src?.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(audioUIWidget.element.src)
|
||||
}
|
||||
originalOnRemoved?.call(this)
|
||||
}
|
||||
|
||||
return { widget: recordWidget }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.constructor.comfyClass !== 'RecordAudio') return
|
||||
|
||||
await useAudioService().registerWavEncoder()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle the Custom Nodes Manager Progress Bar"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "Decrease Brush Size in MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "Increase Brush Size in MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Open Mask Editor for Selected Node"
|
||||
},
|
||||
|
||||
@@ -134,6 +134,10 @@
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"micPermissionDenied": "Microphone permission denied",
|
||||
"noAudioRecorded": "No audio recorded",
|
||||
"nodesRunning": "nodes running"
|
||||
},
|
||||
"manager": {
|
||||
@@ -973,6 +977,8 @@
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
|
||||
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
|
||||
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Alternar diálogo de progreso del administrador"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "Disminuir tamaño del pincel en MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "Aumentar tamaño del pincel en MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Abrir editor de máscara para el nodo seleccionado"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "Cargando panel {panel}...",
|
||||
"login": "Iniciar sesión",
|
||||
"logs": "Registros",
|
||||
"micPermissionDenied": "Permiso de micrófono denegado",
|
||||
"migrate": "Migrar",
|
||||
"missing": "Faltante",
|
||||
"name": "Nombre",
|
||||
"newFolder": "Nueva carpeta",
|
||||
"next": "Siguiente",
|
||||
"no": "No",
|
||||
"noAudioRecorded": "No se grabó audio",
|
||||
"noResultsFound": "No se encontraron resultados",
|
||||
"noTasksFound": "No se encontraron tareas",
|
||||
"noTasksFoundMessage": "No hay tareas en la cola.",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "Mostrar informe",
|
||||
"sort": "Ordenar",
|
||||
"source": "Fuente",
|
||||
"startRecording": "Iniciar grabación",
|
||||
"status": "Estado",
|
||||
"stopRecording": "Detener grabación",
|
||||
"success": "Éxito",
|
||||
"systemInfo": "Información del sistema",
|
||||
"terminal": "Terminal",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "Contactar soporte",
|
||||
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
|
||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||
"Decrease Brush Size in MaskEditor": "Disminuir tamaño del pincel en MaskEditor",
|
||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "Réduire la taille du pinceau dans MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "Augmenter la taille du pinceau dans MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "Chargement du panneau {panel}...",
|
||||
"login": "Connexion",
|
||||
"logs": "Journaux",
|
||||
"micPermissionDenied": "Permission du microphone refusée",
|
||||
"migrate": "Migrer",
|
||||
"missing": "Manquant",
|
||||
"name": "Nom",
|
||||
"newFolder": "Nouveau dossier",
|
||||
"next": "Suivant",
|
||||
"no": "Non",
|
||||
"noAudioRecorded": "Aucun audio enregistré",
|
||||
"noResultsFound": "Aucun résultat trouvé",
|
||||
"noTasksFound": "Aucune tâche trouvée",
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "Afficher le rapport",
|
||||
"sort": "Trier",
|
||||
"source": "Source",
|
||||
"startRecording": "Commencer l’enregistrement",
|
||||
"status": "Statut",
|
||||
"stopRecording": "Arrêter l’enregistrement",
|
||||
"success": "Succès",
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "Contacter le support",
|
||||
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Decrease Brush Size in MaskEditor": "Réduire la taille du pinceau dans MaskEditor",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "マスクエディタでブラシサイズを縮小"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "マスクエディタでブラシサイズを大きくする"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "選択したノードのマスクエディタを開く"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "{panel} パネルを読み込み中...",
|
||||
"login": "ログイン",
|
||||
"logs": "ログ",
|
||||
"micPermissionDenied": "マイクの許可が拒否されました",
|
||||
"migrate": "移行する",
|
||||
"missing": "不足している",
|
||||
"name": "名前",
|
||||
"newFolder": "新しいフォルダー",
|
||||
"next": "次へ",
|
||||
"no": "いいえ",
|
||||
"noAudioRecorded": "音声が録音されていません",
|
||||
"noResultsFound": "結果が見つかりません",
|
||||
"noTasksFound": "タスクが見つかりません",
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "レポートを表示",
|
||||
"sort": "並び替え",
|
||||
"source": "ソース",
|
||||
"startRecording": "録音開始",
|
||||
"status": "ステータス",
|
||||
"stopRecording": "録音停止",
|
||||
"success": "成功",
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Decrease Brush Size in MaskEditor": "マスクエディタでブラシサイズを小さくする",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "진행 상황 대화 상자 전환"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "MaskEditor에서 브러시 크기 줄이기"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "MaskEditor에서 브러시 크기 늘리기"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "선택한 노드 마스크 편집기 열기"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "{panel} 패널 불러오는 중...",
|
||||
"login": "로그인",
|
||||
"logs": "로그",
|
||||
"micPermissionDenied": "마이크 권한이 거부되었습니다",
|
||||
"migrate": "이전(migrate)",
|
||||
"missing": "누락됨",
|
||||
"name": "이름",
|
||||
"newFolder": "새 폴더",
|
||||
"next": "다음",
|
||||
"no": "아니오",
|
||||
"noAudioRecorded": "녹음된 오디오가 없습니다",
|
||||
"noResultsFound": "결과를 찾을 수 없습니다.",
|
||||
"noTasksFound": "작업을 찾을 수 없습니다.",
|
||||
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "보고서 보기",
|
||||
"sort": "정렬",
|
||||
"source": "소스",
|
||||
"startRecording": "녹음 시작",
|
||||
"status": "상태",
|
||||
"stopRecording": "녹음 중지",
|
||||
"success": "성공",
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Decrease Brush Size in MaskEditor": "MaskEditor에서 브러시 크기 줄이기",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Increase Brush Size in MaskEditor": "MaskEditor에서 브러시 크기 늘리기",
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "Уменьшить размер кисти в MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "Увеличить размер кисти в MaskEditor"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Открыть редактор масок для выбранной ноды"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "Загрузка панели {panel}...",
|
||||
"login": "Вход",
|
||||
"logs": "Логи",
|
||||
"micPermissionDenied": "Доступ к микрофону запрещён",
|
||||
"migrate": "Мигрировать",
|
||||
"missing": "Отсутствует",
|
||||
"name": "Имя",
|
||||
"newFolder": "Новая папка",
|
||||
"next": "Далее",
|
||||
"no": "Нет",
|
||||
"noAudioRecorded": "Аудио не записано",
|
||||
"noResultsFound": "Результатов не найдено",
|
||||
"noTasksFound": "Задачи не найдены",
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "Показать отчёт",
|
||||
"sort": "Сортировать",
|
||||
"source": "Источник",
|
||||
"startRecording": "Начать запись",
|
||||
"status": "Статус",
|
||||
"stopRecording": "Остановить запись",
|
||||
"success": "Успех",
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Decrease Brush Size in MaskEditor": "Уменьшить размер кисти в MaskEditor",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切換自訂節點管理器進度條"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "減少 MaskEditor 畫筆大小"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "增加 MaskEditor 畫筆大小"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "為選取的節點開啟 Mask 編輯器"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "正在載入{panel}面板...",
|
||||
"login": "登入",
|
||||
"logs": "日誌",
|
||||
"micPermissionDenied": "麥克風權限被拒絕",
|
||||
"migrate": "遷移",
|
||||
"missing": "缺少",
|
||||
"name": "名稱",
|
||||
"newFolder": "新資料夾",
|
||||
"next": "下一步",
|
||||
"no": "否",
|
||||
"noAudioRecorded": "沒有錄製到音訊",
|
||||
"noResultsFound": "找不到結果",
|
||||
"noTasksFound": "找不到任務",
|
||||
"noTasksFoundMessage": "佇列中沒有任務。",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "顯示報告",
|
||||
"sort": "排序",
|
||||
"source": "來源",
|
||||
"startRecording": "開始錄音",
|
||||
"status": "狀態",
|
||||
"stopRecording": "停止錄音",
|
||||
"success": "成功",
|
||||
"systemInfo": "系統資訊",
|
||||
"terminal": "終端機",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "聯絡支援",
|
||||
"Convert Selection to Subgraph": "將選取內容轉為子圖",
|
||||
"Convert selected nodes to group node": "將選取節點轉為群組節點",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Delete Selected Items": "刪除選取項目",
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Interrupt": "中斷",
|
||||
"Load Default Workflow": "載入預設工作流程",
|
||||
"Manage group nodes": "管理群組節點",
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "減小 MaskEditor 中的筆刷大小"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "增加 MaskEditor 畫筆大小"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "打开选中节点的遮罩编辑器"
|
||||
},
|
||||
|
||||
@@ -331,12 +331,14 @@
|
||||
"loadingPanel": "正在加载{panel}面板...",
|
||||
"login": "登录",
|
||||
"logs": "日志",
|
||||
"micPermissionDenied": "麦克风权限被拒绝",
|
||||
"migrate": "迁移",
|
||||
"missing": "缺失",
|
||||
"name": "名称",
|
||||
"newFolder": "新文件夹",
|
||||
"next": "下一个",
|
||||
"no": "否",
|
||||
"noAudioRecorded": "未录制音频",
|
||||
"noResultsFound": "未找到结果",
|
||||
"noTasksFound": "未找到任务",
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
@@ -376,7 +378,9 @@
|
||||
"showReport": "显示报告",
|
||||
"sort": "排序",
|
||||
"source": "来源",
|
||||
"startRecording": "开始录音",
|
||||
"status": "状态",
|
||||
"stopRecording": "停止录音",
|
||||
"success": "成功",
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
@@ -747,6 +751,7 @@
|
||||
"Contact Support": "联系支持",
|
||||
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
@@ -758,6 +763,7 @@
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
|
||||
84
src/services/audioService.ts
Normal file
84
src/services/audioService.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { register } from 'extendable-media-recorder'
|
||||
import { connect } from 'extendable-media-recorder-wav-encoder'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
export interface AudioRecordingError {
|
||||
type: 'permission' | 'not_supported' | 'encoder' | 'recording' | 'unknown'
|
||||
message: string
|
||||
originalError?: unknown
|
||||
}
|
||||
|
||||
let isEncoderRegistered: boolean = false
|
||||
|
||||
export const useAudioService = () => {
|
||||
const handleError = (
|
||||
type: AudioRecordingError['type'],
|
||||
message: string,
|
||||
originalError?: unknown
|
||||
) => {
|
||||
console.error(`Audio Service Error (${type}):`, message, originalError)
|
||||
}
|
||||
|
||||
const stopAllTracks = (currentStream: MediaStream | null) => {
|
||||
if (currentStream) {
|
||||
currentStream.getTracks().forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
currentStream = null
|
||||
}
|
||||
}
|
||||
|
||||
const registerWavEncoder = async (): Promise<void> => {
|
||||
if (isEncoderRegistered) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await register(await connect())
|
||||
isEncoderRegistered = true
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
err.message.includes('already an encoder stored')
|
||||
) {
|
||||
isEncoderRegistered = true
|
||||
} else {
|
||||
handleError('encoder', 'Failed to register WAV encoder', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const convertBlobToFileAndSubmit = async (blob: Blob): Promise<string> => {
|
||||
const name = `recording-${Date.now()}.wav`
|
||||
const file = new File([blob], name, { type: blob.type || 'audio/wav' })
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'audio')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading temp file: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
const tempAudio = await resp.json()
|
||||
|
||||
return `audio/${tempAudio.name} [temp]`
|
||||
}
|
||||
|
||||
return {
|
||||
// Methods
|
||||
convertBlobToFileAndSubmit,
|
||||
registerWavEncoder,
|
||||
stopAllTracks
|
||||
}
|
||||
}
|
||||
332
tests-ui/tests/audioService.test.ts
Normal file
332
tests-ui/tests/audioService.test.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
type AudioRecordingError,
|
||||
useAudioService
|
||||
} from '@/services/audioService'
|
||||
|
||||
const mockRegister = vi.hoisted(() => vi.fn())
|
||||
const mockConnect = vi.hoisted(() => vi.fn())
|
||||
|
||||
const mockApi = vi.hoisted(() => ({
|
||||
fetchApi: vi.fn()
|
||||
}))
|
||||
|
||||
const mockToastStore = vi.hoisted(() => ({
|
||||
addAlert: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('extendable-media-recorder', () => ({
|
||||
register: mockRegister
|
||||
}))
|
||||
|
||||
vi.mock('extendable-media-recorder-wav-encoder', () => ({
|
||||
connect: mockConnect
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: mockApi
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => mockToastStore)
|
||||
}))
|
||||
|
||||
describe('useAudioService', () => {
|
||||
let service: ReturnType<typeof useAudioService>
|
||||
|
||||
const mockBlob = new Blob(['test audio data'], { type: 'audio/wav' })
|
||||
const mockUploadResponse = {
|
||||
name: 'test-audio-123.wav'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
mockConnect.mockResolvedValue('mock-encoder')
|
||||
mockRegister.mockResolvedValue(undefined)
|
||||
mockApi.fetchApi.mockResolvedValue({
|
||||
status: 200,
|
||||
json: () => Promise.resolve(mockUploadResponse)
|
||||
})
|
||||
|
||||
service = useAudioService()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize service with required methods', () => {
|
||||
expect(service).toHaveProperty('registerWavEncoder')
|
||||
expect(service).toHaveProperty('stopAllTracks')
|
||||
expect(service).toHaveProperty('convertBlobToFileAndSubmit')
|
||||
expect(typeof service.registerWavEncoder).toBe('function')
|
||||
expect(typeof service.stopAllTracks).toBe('function')
|
||||
expect(typeof service.convertBlobToFileAndSubmit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('registerWavEncoder', () => {
|
||||
it('should register WAV encoder successfully on first call', async () => {
|
||||
await service.registerWavEncoder()
|
||||
|
||||
expect(mockConnect).toHaveBeenCalledTimes(1)
|
||||
expect(mockRegister).toHaveBeenCalledWith('mock-encoder')
|
||||
})
|
||||
|
||||
it('should not register again if already registered', async () => {
|
||||
await service.registerWavEncoder()
|
||||
|
||||
mockConnect.mockClear()
|
||||
mockRegister.mockClear()
|
||||
|
||||
await service.registerWavEncoder()
|
||||
|
||||
expect(mockConnect).not.toHaveBeenCalled()
|
||||
expect(mockRegister).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle "already an encoder stored" error gracefully', async () => {
|
||||
const error = new Error(
|
||||
'There is already an encoder stored which handles exactly the same mime types.'
|
||||
)
|
||||
mockRegister.mockRejectedValueOnce(error)
|
||||
|
||||
await service.registerWavEncoder()
|
||||
|
||||
expect(mockConnect).toHaveBeenCalledTimes(0)
|
||||
expect(mockRegister).toHaveBeenCalledTimes(0)
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('stopAllTracks', () => {
|
||||
it('should stop all tracks in a stream', () => {
|
||||
const mockTrack1 = { stop: vi.fn() }
|
||||
const mockTrack2 = { stop: vi.fn() }
|
||||
const mockStream = {
|
||||
getTracks: vi.fn().mockReturnValue([mockTrack1, mockTrack2])
|
||||
} as unknown as MediaStream
|
||||
|
||||
service.stopAllTracks(mockStream)
|
||||
|
||||
expect(mockStream.getTracks).toHaveBeenCalledTimes(1)
|
||||
expect(mockTrack1.stop).toHaveBeenCalledTimes(1)
|
||||
expect(mockTrack2.stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle null stream gracefully', () => {
|
||||
expect(() => service.stopAllTracks(null)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle stream with no tracks', () => {
|
||||
const mockStream = {
|
||||
getTracks: vi.fn().mockReturnValue([])
|
||||
} as unknown as MediaStream
|
||||
|
||||
expect(() => service.stopAllTracks(mockStream)).not.toThrow()
|
||||
expect(mockStream.getTracks).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle tracks that throw on stop', () => {
|
||||
const mockTrack1 = { stop: vi.fn() }
|
||||
const mockTrack2 = {
|
||||
stop: vi.fn().mockImplementation(() => {
|
||||
throw new Error('Stop failed')
|
||||
})
|
||||
}
|
||||
const mockStream = {
|
||||
getTracks: vi.fn().mockReturnValue([mockTrack1, mockTrack2])
|
||||
} as unknown as MediaStream
|
||||
|
||||
expect(() => service.stopAllTracks(mockStream)).toThrow()
|
||||
expect(mockTrack1.stop).toHaveBeenCalledTimes(1)
|
||||
expect(mockTrack2.stop).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertBlobToFileAndSubmit', () => {
|
||||
it('should convert blob to file and upload successfully', async () => {
|
||||
const result = await service.convertBlobToFileAndSubmit(mockBlob)
|
||||
|
||||
expect(mockApi.fetchApi).toHaveBeenCalledWith('/upload/image', {
|
||||
method: 'POST',
|
||||
body: expect.any(FormData)
|
||||
})
|
||||
|
||||
expect(result).toBe('audio/test-audio-123.wav [temp]')
|
||||
})
|
||||
|
||||
it('should create file with correct name and type', async () => {
|
||||
const mockTimestamp = 1640995200000
|
||||
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp)
|
||||
|
||||
await service.convertBlobToFileAndSubmit(mockBlob)
|
||||
|
||||
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
|
||||
const uploadedFile = formDataCall.get('image') as File
|
||||
|
||||
expect(uploadedFile).toBeInstanceOf(File)
|
||||
expect(uploadedFile.name).toBe(`recording-${mockTimestamp}.wav`)
|
||||
expect(uploadedFile.type).toBe('audio/wav')
|
||||
})
|
||||
|
||||
it('should set correct form data fields', async () => {
|
||||
await service.convertBlobToFileAndSubmit(mockBlob)
|
||||
|
||||
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
|
||||
|
||||
expect(formDataCall.get('subfolder')).toBe('audio')
|
||||
expect(formDataCall.get('type')).toBe('temp')
|
||||
expect(formDataCall.get('image')).toBeInstanceOf(File)
|
||||
})
|
||||
|
||||
it('should handle blob with different type', async () => {
|
||||
const customBlob = new Blob(['test'], { type: 'audio/ogg' })
|
||||
|
||||
await service.convertBlobToFileAndSubmit(customBlob)
|
||||
|
||||
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
|
||||
const uploadedFile = formDataCall.get('image') as File
|
||||
|
||||
expect(uploadedFile.type).toBe('audio/ogg')
|
||||
})
|
||||
|
||||
it('should handle blob with no type', async () => {
|
||||
const customBlob = new Blob(['test'])
|
||||
|
||||
await service.convertBlobToFileAndSubmit(customBlob)
|
||||
|
||||
const formDataCall = mockApi.fetchApi.mock.calls[0][1].body as FormData
|
||||
const uploadedFile = formDataCall.get('image') as File
|
||||
|
||||
expect(uploadedFile.type).toBe('audio/wav') // Should default to audio/wav
|
||||
})
|
||||
|
||||
it('should handle upload failure with error status', async () => {
|
||||
mockApi.fetchApi.mockResolvedValueOnce({
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error'
|
||||
})
|
||||
|
||||
await expect(
|
||||
service.convertBlobToFileAndSubmit(mockBlob)
|
||||
).rejects.toThrow(
|
||||
'Error uploading temp file: 500 - Internal Server Error'
|
||||
)
|
||||
|
||||
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
|
||||
'Error uploading temp file: 500 - Internal Server Error'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
const networkError = new Error('Network Error')
|
||||
mockApi.fetchApi.mockRejectedValueOnce(networkError)
|
||||
|
||||
await expect(
|
||||
service.convertBlobToFileAndSubmit(mockBlob)
|
||||
).rejects.toThrow('Network Error')
|
||||
})
|
||||
|
||||
it('should handle different status codes', async () => {
|
||||
const testCases = [
|
||||
{ status: 400, statusText: 'Bad Request' },
|
||||
{ status: 403, statusText: 'Forbidden' },
|
||||
{ status: 404, statusText: 'Not Found' },
|
||||
{ status: 413, statusText: 'Payload Too Large' }
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
mockApi.fetchApi.mockResolvedValueOnce(testCase)
|
||||
|
||||
await expect(
|
||||
service.convertBlobToFileAndSubmit(mockBlob)
|
||||
).rejects.toThrow(
|
||||
`Error uploading temp file: ${testCase.status} - ${testCase.statusText}`
|
||||
)
|
||||
|
||||
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
|
||||
`Error uploading temp file: ${testCase.status} - ${testCase.statusText}`
|
||||
)
|
||||
|
||||
mockToastStore.addAlert.mockClear()
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle malformed response JSON', async () => {
|
||||
mockApi.fetchApi.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
json: () => Promise.reject(new Error('Invalid JSON'))
|
||||
})
|
||||
|
||||
await expect(
|
||||
service.convertBlobToFileAndSubmit(mockBlob)
|
||||
).rejects.toThrow('Invalid JSON')
|
||||
})
|
||||
|
||||
it('should handle empty response', async () => {
|
||||
mockApi.fetchApi.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
json: () => Promise.resolve({})
|
||||
})
|
||||
|
||||
const result = await service.convertBlobToFileAndSubmit(mockBlob)
|
||||
|
||||
expect(result).toBe('audio/undefined [temp]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle AudioRecordingError interface correctly', () => {
|
||||
const error: AudioRecordingError = {
|
||||
type: 'permission',
|
||||
message: 'Microphone access denied',
|
||||
originalError: new Error('Permission denied')
|
||||
}
|
||||
|
||||
expect(error.type).toBe('permission')
|
||||
expect(error.message).toBe('Microphone access denied')
|
||||
expect(error.originalError).toBeInstanceOf(Error)
|
||||
})
|
||||
|
||||
it('should support all error types', () => {
|
||||
const errorTypes = [
|
||||
'permission',
|
||||
'not_supported',
|
||||
'encoder',
|
||||
'recording',
|
||||
'unknown'
|
||||
] as const
|
||||
|
||||
errorTypes.forEach((type) => {
|
||||
const error: AudioRecordingError = {
|
||||
type,
|
||||
message: `Test error for ${type}`
|
||||
}
|
||||
|
||||
expect(error.type).toBe(type)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle very large blobs', async () => {
|
||||
const largeData = new Array(1000000).fill('a').join('')
|
||||
const largeBlob = new Blob([largeData], { type: 'audio/wav' })
|
||||
|
||||
const result = await service.convertBlobToFileAndSubmit(largeBlob)
|
||||
|
||||
expect(result).toBe('audio/test-audio-123.wav [temp]')
|
||||
expect(mockApi.fetchApi).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle empty blob', async () => {
|
||||
const emptyBlob = new Blob([], { type: 'audio/wav' })
|
||||
|
||||
const result = await service.convertBlobToFileAndSubmit(emptyBlob)
|
||||
|
||||
expect(result).toBe('audio/test-audio-123.wav [temp]')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user