[Feature] Add audio preview support to queue sidebar (#3954)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Christian Byrne
2025-05-21 18:31:38 -07:00
committed by GitHub
parent b531d34027
commit 4ad6475283
12 changed files with 121 additions and 3 deletions

View File

@@ -0,0 +1,19 @@
<template>
<audio controls width="100%" height="100%">
<source :src="url" :type="htmlAudioType" />
{{ $t('g.audioFailedToLoad') }}
</audio>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ResultItemImpl } from '@/stores/queueStore'
const { result } = defineProps<{
result: ResultItemImpl
}>()
const url = computed(() => result.url)
const htmlAudioType = computed(() => result.htmlAudioType)
</script>

View File

@@ -35,6 +35,7 @@
class="galleria-image" class="galleria-image"
/> />
<ResultVideo v-else-if="item.isVideo" :result="item" /> <ResultVideo v-else-if="item.isVideo" :result="item" />
<ResultAudio v-else-if="item.isAudio" :result="item" />
</template> </template>
</Galleria> </Galleria>
</template> </template>
@@ -46,6 +47,7 @@ import { onMounted, onUnmounted, ref, watch } from 'vue'
import ComfyImage from '@/components/common/ComfyImage.vue' import ComfyImage from '@/components/common/ComfyImage.vue'
import { ResultItemImpl } from '@/stores/queueStore' import { ResultItemImpl } from '@/stores/queueStore'
import ResultAudio from './ResultAudio.vue'
import ResultVideo from './ResultVideo.vue' import ResultVideo from './ResultVideo.vue'
const galleryVisible = ref(false) const galleryVisible = ref(false)

View File

@@ -12,6 +12,7 @@
:alt="result.filename" :alt="result.filename"
/> />
<ResultVideo v-else-if="result.isVideo" :result="result" /> <ResultVideo v-else-if="result.isVideo" :result="result" />
<ResultAudio v-else-if="result.isAudio" :result="result" />
<div v-else class="task-result-preview"> <div v-else class="task-result-preview">
<i class="pi pi-file" /> <i class="pi pi-file" />
<span>{{ result.mediaType }}</span> <span>{{ result.mediaType }}</span>
@@ -26,6 +27,7 @@ import ComfyImage from '@/components/common/ComfyImage.vue'
import { ResultItemImpl } from '@/stores/queueStore' import { ResultItemImpl } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore' import { useSettingStore } from '@/stores/settingStore'
import ResultAudio from './ResultAudio.vue'
import ResultVideo from './ResultVideo.vue' import ResultVideo from './ResultVideo.vue'
const props = defineProps<{ const props = defineProps<{

View File

@@ -13,6 +13,7 @@
"terminal": "Terminal", "terminal": "Terminal",
"logs": "Logs", "logs": "Logs",
"videoFailedToLoad": "Video failed to load", "videoFailedToLoad": "Video failed to load",
"audioFailedToLoad": "Audio failed to load",
"extensionName": "Extension Name", "extensionName": "Extension Name",
"reloadToApplyChanges": "Reload to apply changes", "reloadToApplyChanges": "Reload to apply changes",
"insert": "Insert", "insert": "Insert",

View File

@@ -248,6 +248,7 @@
"all": "Todo", "all": "Todo",
"amount": "Cantidad", "amount": "Cantidad",
"apply": "Aplicar", "apply": "Aplicar",
"audioFailedToLoad": "No se pudo cargar el audio",
"back": "Atrás", "back": "Atrás",
"cancel": "Cancelar", "cancel": "Cancelar",
"capture": "captura", "capture": "captura",

View File

@@ -248,6 +248,7 @@
"all": "Tout", "all": "Tout",
"amount": "Quantité", "amount": "Quantité",
"apply": "Appliquer", "apply": "Appliquer",
"audioFailedToLoad": "Échec du chargement de l'audio",
"back": "Retour", "back": "Retour",
"cancel": "Annuler", "cancel": "Annuler",
"capture": "capture", "capture": "capture",

View File

@@ -248,6 +248,7 @@
"all": "すべて", "all": "すべて",
"amount": "量", "amount": "量",
"apply": "適用する", "apply": "適用する",
"audioFailedToLoad": "オーディオの読み込みに失敗しました",
"back": "戻る", "back": "戻る",
"cancel": "キャンセル", "cancel": "キャンセル",
"capture": "キャプチャ", "capture": "キャプチャ",

View File

@@ -248,6 +248,7 @@
"all": "모두", "all": "모두",
"amount": "수량", "amount": "수량",
"apply": "적용", "apply": "적용",
"audioFailedToLoad": "오디오를 불러오지 못했습니다",
"back": "뒤로", "back": "뒤로",
"cancel": "취소", "cancel": "취소",
"capture": "캡처", "capture": "캡처",

View File

@@ -248,6 +248,7 @@
"all": "Все", "all": "Все",
"amount": "Количество", "amount": "Количество",
"apply": "Применить", "apply": "Применить",
"audioFailedToLoad": "Не удалось загрузить аудио",
"back": "Назад", "back": "Назад",
"cancel": "Отмена", "cancel": "Отмена",
"capture": "захват", "capture": "захват",

View File

@@ -248,6 +248,7 @@
"all": "全部", "all": "全部",
"amount": "数量", "amount": "数量",
"apply": "应用", "apply": "应用",
"audioFailedToLoad": "音频加载失败",
"back": "返回", "back": "返回",
"cancel": "取消", "cancel": "取消",
"capture": "捕获", "capture": "捕获",

View File

@@ -106,6 +106,22 @@ export class ResultItemImpl {
return undefined return undefined
} }
get htmlAudioType(): string | undefined {
if (this.isMp3) {
return 'audio/mpeg'
}
if (this.isWav) {
return 'audio/wav'
}
if (this.isOgg) {
return 'audio/ogg'
}
if (this.isFlac) {
return 'audio/flac'
}
return undefined
}
get isGif(): boolean { get isGif(): boolean {
return this.filename.endsWith('.gif') return this.filename.endsWith('.gif')
} }
@@ -130,21 +146,55 @@ export class ResultItemImpl {
return this.isGif || this.isWebp return this.isGif || this.isWebp
} }
get isMp3(): boolean {
return this.filename.endsWith('.mp3')
}
get isWav(): boolean {
return this.filename.endsWith('.wav')
}
get isOgg(): boolean {
return this.filename.endsWith('.ogg')
}
get isFlac(): boolean {
return this.filename.endsWith('.flac')
}
get isAudioBySuffix(): boolean {
return this.isMp3 || this.isWav || this.isOgg || this.isFlac
}
get isVideo(): boolean { get isVideo(): boolean {
const isVideoByType = const isVideoByType =
this.mediaType === 'video' || !!this.format?.startsWith('video/') this.mediaType === 'video' || !!this.format?.startsWith('video/')
return this.isVideoBySuffix || (isVideoByType && !this.isImageBySuffix) return (
this.isVideoBySuffix ||
(isVideoByType && !this.isImageBySuffix && !this.isAudioBySuffix)
)
} }
get isImage(): boolean { get isImage(): boolean {
return ( return (
this.isImageBySuffix || this.isImageBySuffix ||
(this.mediaType === 'images' && !this.isVideoBySuffix) (this.mediaType === 'images' &&
!this.isVideoBySuffix &&
!this.isAudioBySuffix)
)
}
get isAudio(): boolean {
const isAudioByType =
this.mediaType === 'audio' || !!this.format?.startsWith('audio/')
return (
this.isAudioBySuffix ||
(isAudioByType && !this.isImageBySuffix && !this.isVideoBySuffix)
) )
} }
get supportsPreview(): boolean { get supportsPreview(): boolean {
return this.isImage || this.isVideo return this.isImage || this.isVideo || this.isAudio
} }
} }

View File

@@ -115,4 +115,42 @@ describe('TaskItemImpl', () => {
expect(output.isVideo).toBe(true) expect(output.isVideo).toBe(true)
expect(output.isImage).toBe(false) expect(output.isImage).toBe(false)
}) })
describe('audio format detection', () => {
const audioFormats = [
{ extension: 'mp3', mimeType: 'audio/mpeg' },
{ extension: 'wav', mimeType: 'audio/wav' },
{ extension: 'ogg', mimeType: 'audio/ogg' },
{ extension: 'flac', mimeType: 'audio/flac' }
]
audioFormats.forEach(({ extension, mimeType }) => {
it(`should recognize ${extension} audio`, () => {
const taskItem = new TaskItemImpl(
'History',
[0, 'prompt-id', {}, { client_id: 'client-id' }, []],
{ status_str: 'success', messages: [], completed: true },
{
'node-1': {
audio: [
{
filename: `test.${extension}`,
type: 'output',
subfolder: ''
}
]
}
}
)
const output = taskItem.flatOutputs[0]
expect(output.htmlAudioType).toBe(mimeType)
expect(output.isAudio).toBe(true)
expect(output.isVideo).toBe(false)
expect(output.isImage).toBe(false)
expect(output.supportsPreview).toBe(true)
})
})
})
}) })