Add local queued video upscale with Real-ESRGAN and RIFE
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div v-if="clipId" class="space-y-2">
|
||||
<button type="button" class="rounded-2xl border border-amber-300/40 px-4 py-2 text-sm text-amber-100 disabled:opacity-50" :disabled="busy || loading || !state" @click="openDialog">Upscale</button>
|
||||
<NuxtLink v-if="state?.source.upscaledFromClipId" :to="`/?clip=${state.source.upscaledFromClipId}`" class="block text-xs text-amber-200 underline">Upscaled copy · Open original</NuxtLink>
|
||||
<div v-if="state?.job" class="max-w-sm text-xs text-zinc-400" aria-live="polite">
|
||||
<span class="capitalize">Upscale {{ state.job.status }}</span>
|
||||
<span v-if="state.job.width"> · {{ state.job.width }} × {{ state.job.height }}</span>
|
||||
<span v-if="state.job.elapsedMs"> · {{ Math.round(state.job.elapsedMs / 1000) }}s</span>
|
||||
<progress v-if="busy" class="mt-1 block h-1 w-full accent-amber-400" max="100" :value="state.job.progress || 0" />
|
||||
<p v-if="state.job.error" class="text-red-300">{{ state.job.error }}</p>
|
||||
<p v-else-if="busy">{{ state.job.message }}</p>
|
||||
<NuxtLink v-if="state.job.outputClipId" :to="`/?clip=${state.job.outputClipId}`" class="text-amber-200 underline">Open upscaled clip</NuxtLink>
|
||||
</div>
|
||||
<p v-if="error && !dialogOpen" role="alert" class="text-xs text-red-300">{{ error }}</p>
|
||||
<dialog ref="dialog" class="w-[min(92vw,28rem)] rounded-3xl border border-white/15 bg-zinc-950 p-6 text-zinc-100 backdrop:bg-black/70" @close="dialogOpen = false">
|
||||
<form @submit.prevent="submit">
|
||||
<h2 class="text-xl font-semibold">Upscale video</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">Process locally on your GPU. The original stays in your library.</p>
|
||||
<div class="my-5 grid grid-cols-2 gap-4">
|
||||
<label class="text-sm">Scale<select v-model="form.scale" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option :value="2">2x</option><option :value="4">4x</option></select></label>
|
||||
<label class="text-sm">Target<select v-model="form.target" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="preserve">Preserve aspect</option><option value="1080p">Fit 1080p</option><option value="4k">Fit 4K</option></select></label>
|
||||
<label class="text-sm">FPS<select v-model="form.fps" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="keep">Keep original</option><option v-for="fps in [48, 50, 60]" :key="fps" :value="fps">{{ fps }}</option></select></label>
|
||||
<label class="text-sm">Enhance<select v-model="form.enhance" class="mt-1 w-full rounded-xl bg-zinc-800 p-2"><option value="faithful">Faithful</option><option value="sharpen">Slight sharpen</option></select></label>
|
||||
</div>
|
||||
<p class="text-sm">{{ resolution }} <span class="text-zinc-500">estimated · original aspect</span></p>
|
||||
<p class="mt-1 break-all text-xs text-zinc-400">{{ outputName }}</p>
|
||||
<p class="mt-2 text-xs text-zinc-500">First use downloads the local tools and weights. Fit uses a 1920 / 3840 pixel long edge, with no cropping or padding.</p>
|
||||
<p v-if="error" role="alert" class="mt-3 text-sm text-red-300">{{ error }}</p>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<button type="button" class="rounded-full border border-white/15 px-4 py-2" @click="dialog?.close()">Close</button>
|
||||
<button type="submit" class="rounded-full bg-amber-400 px-4 py-2 font-semibold text-zinc-950 disabled:opacity-50" :disabled="busy || submitting">{{ submitting ? 'Queuing…' : 'Upscale' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { upscaleOptions, upscaleDimensions, upscaleName } from '~/shared/video-upscale.mjs'
|
||||
const props = defineProps<{ clipId: string; generationBusy?: boolean }>()
|
||||
const emit = defineEmits<{ complete: [] }>()
|
||||
const dialog = ref<HTMLDialogElement>(), dialogOpen = ref(false), state = ref<any>(null), error = ref(''), loading = ref(true), submitting = ref(false)
|
||||
const form = reactive(upscaleOptions())
|
||||
const busy = computed(() => props.generationBusy || state.value?.busy)
|
||||
const outputName = computed(() => upscaleName(state.value?.source.stem || 'video', form.scale))
|
||||
const resolution = computed(() => { try { const size = upscaleDimensions(state.value.source.width, state.value.source.height, form); return `${size.width} × ${size.height}` } catch { return 'Resolution checked before processing' } })
|
||||
let timer: ReturnType<typeof setTimeout> | undefined, disposed = false
|
||||
async function refresh() {
|
||||
const id = props.clipId
|
||||
if (!id) return
|
||||
try {
|
||||
const result = await $fetch<any>(`/api/library/clips/${encodeURIComponent(id)}/upscale`)
|
||||
if (disposed || id !== props.clipId) return
|
||||
if (state.value?.job?.status !== 'done' && result.job?.status === 'done') emit('complete')
|
||||
state.value = result; error.value = ''
|
||||
} catch (e: any) { if (id === props.clipId) error.value = e?.data?.statusMessage || 'Could not check local upscale status.' }
|
||||
finally { if (id === props.clipId) loading.value = false }
|
||||
}
|
||||
function openDialog() { error.value = ''; dialog.value?.showModal(); dialogOpen.value = true }
|
||||
async function submit() {
|
||||
if (busy.value || submitting.value) return
|
||||
submitting.value = true; error.value = ''
|
||||
try { await $fetch(`/api/library/clips/${encodeURIComponent(props.clipId)}/upscale`, { method: 'POST', body: { ...form } }); await refresh(); dialog.value?.close() }
|
||||
catch (e: any) { error.value = e?.data?.statusMessage || 'Could not queue upscale.' }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
watch(() => props.clipId, () => { state.value = null; loading.value = true; Object.assign(form, upscaleOptions()); dialog.value?.close(); void refresh() })
|
||||
onMounted(() => { const poll = async () => { await refresh(); if (!disposed) timer = setTimeout(poll, state.value?.busy ? 2000 : 10000) }; void poll() })
|
||||
onBeforeUnmount(() => { disposed = true; clearTimeout(timer) })
|
||||
</script>
|
||||
Reference in New Issue
Block a user