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:
@@ -1,13 +1,3 @@
|
||||
const ASPECTS: Record<string, { width: number; height: number }> = {
|
||||
'16:9': { width: 1344, height: 768 },
|
||||
'9:16': { width: 768, height: 1344 },
|
||||
'1:1': { width: 1024, height: 1024 }
|
||||
}
|
||||
|
||||
function snap32(value: number) {
|
||||
return Math.max(32, Math.round(value / 32) * 32)
|
||||
}
|
||||
|
||||
function frameLength(seconds: number) {
|
||||
const raw = Math.max(5, Math.round(seconds * 24))
|
||||
return raw + (5 - (raw % 17) + 17) % 17
|
||||
@@ -37,9 +27,7 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, statusMessage: 'A motion prompt is required' })
|
||||
}
|
||||
|
||||
const aspect = ASPECTS[fields.aspect] || ASPECTS['16:9']
|
||||
const width = snap32(Number(fields.width || aspect.width))
|
||||
const height = snap32(Number(fields.height || aspect.height))
|
||||
const { width, height } = resolveOutputSize(fields.aspect, image.data, Number(fields.width), Number(fields.height))
|
||||
const turbo = fields.turbo !== 'false'
|
||||
const steps = Number(fields.steps || (turbo ? 8 : 20))
|
||||
const seed = fields.seed && fields.seed !== 'random'
|
||||
@@ -64,6 +52,7 @@ export default defineEventHandler(async (event) => {
|
||||
seed,
|
||||
width,
|
||||
height,
|
||||
aspect: fields.aspect || 'auto',
|
||||
steps,
|
||||
turbo
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
const SHORT_EDGE = 768
|
||||
const LONG_EDGE = 1344
|
||||
|
||||
export const ASPECT_PRESETS: Record<string, { width: number; height: number }> = {
|
||||
'16:9': { width: 1344, height: 768 },
|
||||
'9:16': { width: 768, height: 1344 },
|
||||
'1:1': { width: 1024, height: 1024 }
|
||||
}
|
||||
|
||||
export function snap32(value: number) {
|
||||
return Math.max(32, Math.round(value / 32) * 32)
|
||||
}
|
||||
|
||||
/** Fit the source still to H3's native 768 short-edge canvas, 32px grid, 1344 long-edge cap. */
|
||||
export function autoResolution(srcWidth: number, srcHeight: number) {
|
||||
const ratio = srcWidth / Math.max(srcHeight, 1)
|
||||
let width: number
|
||||
let height: number
|
||||
if (ratio >= 1) {
|
||||
height = SHORT_EDGE
|
||||
width = Math.min(LONG_EDGE, height * ratio)
|
||||
} else {
|
||||
width = SHORT_EDGE
|
||||
height = Math.min(LONG_EDGE, width / ratio)
|
||||
}
|
||||
width = snap32(width)
|
||||
height = snap32(height)
|
||||
if (width > LONG_EDGE) width = LONG_EDGE
|
||||
if (height > LONG_EDGE) height = LONG_EDGE
|
||||
return { width: Math.max(32, width), height: Math.max(32, height) }
|
||||
}
|
||||
|
||||
export function imageDimensions(buf: Buffer) {
|
||||
if (buf.length < 24) return null
|
||||
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
||||
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) }
|
||||
}
|
||||
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) {
|
||||
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) }
|
||||
}
|
||||
if (buf[0] === 0x42 && buf[1] === 0x4d && buf.length >= 26) {
|
||||
return { width: buf.readInt32LE(18), height: Math.abs(buf.readInt32LE(22)) }
|
||||
}
|
||||
if (buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
|
||||
return webpSize(buf)
|
||||
}
|
||||
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
||||
return jpegSize(buf)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function webpSize(buf: Buffer) {
|
||||
const kind = buf.toString('ascii', 12, 16)
|
||||
if (kind === 'VP8X' && buf.length >= 30) {
|
||||
const width = 1 + buf[24] + (buf[25] << 8) + (buf[26] << 16)
|
||||
const height = 1 + buf[27] + (buf[28] << 8) + (buf[29] << 16)
|
||||
return { width, height }
|
||||
}
|
||||
if (kind === 'VP8 ' && buf.length >= 30) {
|
||||
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff }
|
||||
}
|
||||
if (kind === 'VP8L' && buf.length >= 25) {
|
||||
const bits = buf.readUInt32LE(21)
|
||||
return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function jpegSize(buf: Buffer) {
|
||||
let offset = 2
|
||||
while (offset + 9 < buf.length) {
|
||||
if (buf[offset] !== 0xff) return null
|
||||
const marker = buf[offset + 1]
|
||||
if (marker === 0xd8 || marker === 0xd9) {
|
||||
offset += 2
|
||||
continue
|
||||
}
|
||||
const length = buf.readUInt16BE(offset + 2)
|
||||
if (marker >= 0xc0 && marker <= 0xc3) {
|
||||
return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5) }
|
||||
}
|
||||
offset += 2 + length
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveOutputSize(aspect: string | undefined, image: Buffer, fallbackWidth?: number, fallbackHeight?: number) {
|
||||
if (aspect && ASPECT_PRESETS[aspect]) {
|
||||
return ASPECT_PRESETS[aspect]
|
||||
}
|
||||
const fromFields = Number(fallbackWidth) && Number(fallbackHeight)
|
||||
? { width: snap32(Number(fallbackWidth)), height: snap32(Number(fallbackHeight)) }
|
||||
: null
|
||||
const size = imageDimensions(image)
|
||||
if (size?.width && size?.height) {
|
||||
return autoResolution(size.width, size.height)
|
||||
}
|
||||
return fromFields || ASPECT_PRESETS['16:9']
|
||||
}
|
||||
Reference in New Issue
Block a user