Add automatic aspect matching with explicit ratio overrides.
Keep video output on the still by default, and only force 16:9, 9:16, or 1:1 when a preset is selected.
This commit is contained in:
+65
-12
@@ -60,7 +60,7 @@
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-zinc-300">Aspect ratio / quality</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
<button
|
||||
v-for="option in aspects"
|
||||
:key="option.id"
|
||||
@@ -70,9 +70,10 @@
|
||||
@click="aspect = option.id"
|
||||
>
|
||||
<span class="block font-semibold">{{ option.label }}</span>
|
||||
<span class="text-xs text-zinc-400">{{ option.hint }}</span>
|
||||
<span class="text-xs text-zinc-400">{{ option.id === 'auto' ? autoHint : option.hint }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-zinc-500">Output {{ outputSize.width }} × {{ outputSize.height }}{{ aspect === 'auto' ? ' · matching the still' : ' · override' }}</p>
|
||||
</div>
|
||||
|
||||
<details class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
|
||||
@@ -140,13 +141,14 @@
|
||||
<video
|
||||
v-if="videoUrl"
|
||||
:src="videoUrl"
|
||||
class="aspect-video w-full bg-black"
|
||||
class="w-full bg-black"
|
||||
:style="{ aspectRatio: `${outputSize.width} / ${outputSize.height}` }"
|
||||
controls
|
||||
autoplay
|
||||
loop
|
||||
playsinline
|
||||
/>
|
||||
<div v-else class="flex aspect-video items-center justify-center text-sm text-zinc-500">
|
||||
<div v-else class="flex items-center justify-center text-sm text-zinc-500" :style="{ aspectRatio: `${outputSize.width} / ${outputSize.height}` }">
|
||||
Generated MP4 appears here
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,9 +195,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type AspectId = 'auto' | '16:9' | '9:16' | '1:1'
|
||||
|
||||
interface HistoryItem {
|
||||
id: string
|
||||
prompt: string
|
||||
aspect?: AspectId
|
||||
width: number
|
||||
height: number
|
||||
steps: number
|
||||
@@ -206,18 +211,27 @@ interface HistoryItem {
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
const PRESETS: Record<Exclude<AspectId, 'auto'>, { width: number; height: number }> = {
|
||||
'16:9': { width: 1344, height: 768 },
|
||||
'9:16': { width: 768, height: 1344 },
|
||||
'1:1': { width: 1024, height: 1024 }
|
||||
}
|
||||
|
||||
const aspects = [
|
||||
{ id: '16:9', label: '16:9 Landscape', hint: '1344 × 768 · 0.98 MP' },
|
||||
{ id: '9:16', label: '9:16 Portrait', hint: '768 × 1344 · 0.98 MP' },
|
||||
{ id: '1:1', label: '1:1 Square', hint: '1024 × 1024' }
|
||||
] as const
|
||||
{ id: 'auto' as const, label: 'Automatic', hint: 'Match the still' },
|
||||
{ id: '16:9' as const, label: '16:9 Landscape', hint: '1344 × 768 · 0.98 MP' },
|
||||
{ id: '9:16' as const, label: '9:16 Portrait', hint: '768 × 1344 · 0.98 MP' },
|
||||
{ id: '1:1' as const, label: '1:1 Square', hint: '1024 × 1024' }
|
||||
]
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const file = ref<File | null>(null)
|
||||
const preview = ref('')
|
||||
const dragging = ref(false)
|
||||
const prompt = ref('')
|
||||
const aspect = ref<'16:9' | '9:16' | '1:1'>('16:9')
|
||||
const aspect = ref<AspectId>('auto')
|
||||
const imageWidth = ref(0)
|
||||
const imageHeight = ref(0)
|
||||
const turbo = ref(true)
|
||||
const duration = ref(5)
|
||||
const seedInput = ref('random')
|
||||
@@ -244,6 +258,36 @@ const elapsedLabel = computed(() => {
|
||||
return `${mm}:${ss}`
|
||||
})
|
||||
|
||||
function snap32(value: number) {
|
||||
return Math.max(32, Math.round(value / 32) * 32)
|
||||
}
|
||||
|
||||
function autoResolution(srcWidth: number, srcHeight: number) {
|
||||
const ratio = srcWidth / Math.max(srcHeight, 1)
|
||||
let width: number
|
||||
let height: number
|
||||
if (ratio >= 1) {
|
||||
height = 768
|
||||
width = Math.min(1344, height * ratio)
|
||||
} else {
|
||||
width = 768
|
||||
height = Math.min(1344, width / ratio)
|
||||
}
|
||||
return { width: snap32(width), height: snap32(height) }
|
||||
}
|
||||
|
||||
const outputSize = computed(() => {
|
||||
if (aspect.value !== 'auto') return PRESETS[aspect.value]
|
||||
if (imageWidth.value && imageHeight.value) return autoResolution(imageWidth.value, imageHeight.value)
|
||||
return { width: 1344, height: 768 }
|
||||
})
|
||||
|
||||
const autoHint = computed(() => {
|
||||
if (!imageWidth.value || !imageHeight.value) return 'Match the still'
|
||||
const size = autoResolution(imageWidth.value, imageHeight.value)
|
||||
return `${size.width} × ${size.height} · from still`
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
history.value = JSON.parse(localStorage.getItem('aigen-history') || '[]')
|
||||
const [health, me] = await Promise.all([
|
||||
@@ -271,12 +315,20 @@ function toast(message: string) {
|
||||
function resetImage() {
|
||||
file.value = null
|
||||
preview.value = ''
|
||||
imageWidth.value = 0
|
||||
imageHeight.value = 0
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
}
|
||||
|
||||
function readFile(next: File) {
|
||||
file.value = next
|
||||
preview.value = URL.createObjectURL(next)
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
imageWidth.value = img.naturalWidth
|
||||
imageHeight.value = img.naturalHeight
|
||||
}
|
||||
img.src = preview.value
|
||||
}
|
||||
|
||||
function onFile(event: Event) {
|
||||
@@ -313,9 +365,7 @@ function rerun(item: HistoryItem) {
|
||||
prompt.value = item.prompt
|
||||
turbo.value = item.turbo
|
||||
seedInput.value = String(item.seed)
|
||||
if (item.width === 768) aspect.value = '9:16'
|
||||
else if (item.width === 1024) aspect.value = '1:1'
|
||||
else aspect.value = '16:9'
|
||||
aspect.value = item.aspect || 'auto'
|
||||
toast('Settings restored. Drop the same still if it is not already loaded.')
|
||||
}
|
||||
|
||||
@@ -331,6 +381,8 @@ async function generate() {
|
||||
body.append('image', file.value)
|
||||
body.append('prompt', prompt.value.trim())
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('width', String(outputSize.value.width))
|
||||
body.append('height', String(outputSize.value.height))
|
||||
body.append('turbo', String(turbo.value))
|
||||
body.append('steps', String(turbo.value ? 8 : 20))
|
||||
body.append('duration', String(duration.value))
|
||||
@@ -369,6 +421,7 @@ function listen(id: string, meta: { seed: number; width: number; height: number;
|
||||
history.value.unshift({
|
||||
id,
|
||||
prompt: prompt.value.trim(),
|
||||
aspect: aspect.value,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
steps: meta.steps,
|
||||
|
||||
Reference in New Issue
Block a user