Name outputs from the input, engine, and LoRA keys instead of aigen_0001. Image-edit presets now store prompts, and aspect fill pads stills without stretching.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+146
-52
@@ -548,9 +548,10 @@
|
||||
<input
|
||||
v-model="clipName"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
|
||||
placeholder="Shown in the library and used for the download filename"
|
||||
:placeholder="autoOutputName"
|
||||
maxlength="80"
|
||||
>
|
||||
<span class="mt-1 block text-[11px] text-zinc-600">Leave blank to use the auto name. Type here to override.</span>
|
||||
</label>
|
||||
<form v-if="currentFolder?.protected && !currentFolder.unlocked" class="space-y-2" @submit.prevent="unlockFolder(folderId)">
|
||||
<p class="text-sm text-zinc-400">This folder is locked. You can still generate into it; unlock to view clips.</p>
|
||||
@@ -733,7 +734,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="studioMode === 'editv2'" class="space-y-3">
|
||||
<p class="text-xs text-zinc-500">Edit, Compose, Refine, and Generate are separate graphs. Shares the desktop GPU with video.</p>
|
||||
<p class="text-xs text-zinc-500">Edit, Compose, Refine, and Generate are separate graphs. Shares the desktop GPU with video. Landscape / Portrait / Square fill new edges without stretching the still.</p>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Image engine</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
@@ -784,22 +785,22 @@
|
||||
/>
|
||||
<p v-if="v2Engine === 'krea'" class="mt-1 text-[11px] text-zinc-500">Flux LoRAs stay on the Flux engine. Switch back to restore that chain.</p>
|
||||
</div>
|
||||
<div v-if="v2Mode === 'generate'" class="space-y-3 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
|
||||
<div>
|
||||
<p class="text-xs text-zinc-400">Size · near 1 MP</p>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="(pair, aspect) in IMAGE_V2_GENERATE_ASPECTS"
|
||||
:key="aspect"
|
||||
type="button"
|
||||
class="rounded-full border px-2.5 py-1 text-xs"
|
||||
:class="v2Width === pair[0] && v2Height === pair[1] ? 'border-amber-300/70 text-amber-100' : 'border-white/10 text-zinc-400'"
|
||||
@click="setGenerateSize(aspect)"
|
||||
>
|
||||
{{ aspect }} · {{ pair[0] }}×{{ pair[1] }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Aspect ratio</p>
|
||||
<div class="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
<button
|
||||
v-for="option in imageAspects"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm transition"
|
||||
:class="aspect === option.id ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
@click="setImageAspect(option.id)"
|
||||
>
|
||||
<span class="block font-semibold">{{ option.label }}</span>
|
||||
<span class="text-xs text-zinc-400">{{ option.hint }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-zinc-500">{{ imageAspectHelp }}</p>
|
||||
</div>
|
||||
<div v-if="v2Mode === 'refine' || (v2Engine === 'krea' && v2Mode !== 'generate')" class="space-y-2 rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
|
||||
<label class="block text-sm">
|
||||
@@ -882,6 +883,23 @@
|
||||
</div>
|
||||
<div v-else-if="studioMode === 'edit'" class="space-y-3">
|
||||
<p class="text-xs text-zinc-500">Flux.2 Klein 9B. Shares the desktop GPU with video.</p>
|
||||
<div>
|
||||
<p class="mb-2 text-xs uppercase tracking-wider text-zinc-500">Aspect ratio</p>
|
||||
<div class="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||
<button
|
||||
v-for="option in imageAspects"
|
||||
:key="'v1-' + option.id"
|
||||
type="button"
|
||||
class="rounded-xl border px-3 py-2 text-left text-sm transition"
|
||||
:class="aspect === option.id ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 hover:border-white/20'"
|
||||
@click="setImageAspect(option.id)"
|
||||
>
|
||||
<span class="block font-semibold">{{ option.label }}</span>
|
||||
<span class="text-xs text-zinc-400">{{ option.hint }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-zinc-500">{{ imageAspectHelp }}</p>
|
||||
</div>
|
||||
<div class="max-w-md space-y-2">
|
||||
<LoraSelect v-model="imageLoraStack" :names="imageLoraOptions" label="LoRA" empty-label="None" compact />
|
||||
<label class="flex cursor-pointer items-center gap-2 text-xs text-zinc-400">
|
||||
@@ -1412,7 +1430,7 @@
|
||||
<div v-if="editResultUrl && !editLockedSave && !editAwaitingReveal" class="mt-4 flex flex-wrap gap-3">
|
||||
<a
|
||||
:href="editResultUrl"
|
||||
:download="`${editDownloadName || 'aigen-edit'}.png`"
|
||||
:download="`${editDownloadName || 'edited'}.png`"
|
||||
class="inline-flex rounded-2xl border border-white/15 px-4 py-2 text-sm hover:border-amber-300/50"
|
||||
>
|
||||
Download still
|
||||
@@ -2014,6 +2032,8 @@ import {
|
||||
stillSettingsRows,
|
||||
type StillGenerationSettings
|
||||
} from '~/utils/libraryGeneration'
|
||||
import { parseImageAspect, generateSizeForAspect } from '~/utils/imageAspect'
|
||||
import { buildOutputBaseName, nextUniqueName } from '~/utils/outputNames'
|
||||
import {
|
||||
IMAGE_V2_CFG_DEFAULT,
|
||||
IMAGE_V2_CONSISTENCY_LORA,
|
||||
@@ -2021,7 +2041,6 @@ import {
|
||||
IMAGE_V2_DENOISE_MAX,
|
||||
IMAGE_V2_DENOISE_MIN,
|
||||
IMAGE_V2_DENOISE_STEP,
|
||||
IMAGE_V2_GENERATE_ASPECTS,
|
||||
IMAGE_V2_GENERATE_HEIGHT,
|
||||
IMAGE_V2_GENERATE_WIDTH,
|
||||
IMAGE_V2_KREA_CFG,
|
||||
@@ -2038,7 +2057,6 @@ import {
|
||||
IMAGE_V2_SNOFS_MODEL,
|
||||
IMAGE_V2_STEPS_DEFAULT,
|
||||
clampImageV2Denoise,
|
||||
clampImageV2Size,
|
||||
defaultImageV2LoraStack,
|
||||
imageV2ConceptLoraName,
|
||||
isImageV2ConceptLora,
|
||||
@@ -2048,7 +2066,6 @@ import {
|
||||
removeImageV2ConceptLora,
|
||||
upsertImageV2Lora,
|
||||
type ImageV2Engine,
|
||||
type ImageV2GenerateAspect,
|
||||
type ImageV2Mode,
|
||||
type ImageV2PresetSettings,
|
||||
type ImageV2Task
|
||||
@@ -2205,6 +2222,27 @@ const aspects = [
|
||||
{ id: '9:16' as const, label: '9:16 Portrait / Shorts', hint: '768 × 1344' },
|
||||
{ id: '1:1' as const, label: '1:1 Square', hint: '960 × 960' }
|
||||
]
|
||||
const imageAspects = computed(() => {
|
||||
const generate = studioMode.value === 'editv2' && v2Mode.value === 'generate'
|
||||
return [
|
||||
{ id: 'auto' as const, label: 'Automatic', hint: generate ? '1:1 · 1024 × 1024' : 'Keep the still’s shape' },
|
||||
{ id: '16:9' as const, label: '16:9 Landscape', hint: generate ? '1280 × 768' : 'Fill extra edges — no stretch' },
|
||||
{ id: '9:16' as const, label: '9:16 Portrait', hint: generate ? '768 × 1280' : 'Fill extra edges — no stretch' },
|
||||
{ id: '1:1' as const, label: '1:1 Square', hint: generate ? '1024 × 1024' : 'Fill extra edges — no stretch' }
|
||||
]
|
||||
})
|
||||
const imageAspectHelp = computed(() => {
|
||||
if (studioMode.value === 'editv2' && v2Mode.value === 'generate') {
|
||||
const size = generateSizeForAspect(aspect.value)
|
||||
return `Empty canvas ${size.width} × ${size.height}. Nothing is stretched.`
|
||||
}
|
||||
if (aspect.value === 'auto') return 'Output keeps the still’s aspect ratio. No padding.'
|
||||
return 'The still stays as-is. New edges are generated to fill the frame — no squish or stretch.'
|
||||
})
|
||||
const imageFillWithoutPrompt = computed(() => (
|
||||
aspect.value !== 'auto'
|
||||
&& (studioMode.value === 'edit' || (studioMode.value === 'editv2' && v2Mode.value !== 'generate'))
|
||||
))
|
||||
|
||||
const player = ref<HTMLVideoElement | null>(null)
|
||||
const outputPanel = ref<HTMLElement | null>(null)
|
||||
@@ -2238,8 +2276,8 @@ const shotScriptBeforeRecommend = ref(false)
|
||||
const extensionQueue = ref<QueuedExtension[]>([])
|
||||
const editPassQueue = ref<QueuedEditPass[]>([])
|
||||
const clipName = ref('')
|
||||
const videoDownloadName = ref('aigen')
|
||||
const editDownloadName = ref('aigen-edit')
|
||||
const videoDownloadName = ref('output')
|
||||
const editDownloadName = ref('edited')
|
||||
const aspect = ref<AspectId>('auto')
|
||||
const imageWidth = ref(0)
|
||||
const imageHeight = ref(0)
|
||||
@@ -2417,6 +2455,7 @@ const folders = ref<LibraryFolder[]>([])
|
||||
const clips = ref<LibraryClip[]>([])
|
||||
const stills = ref<LibraryStill[]>([])
|
||||
const drafts = ref<RetryDraft[]>([])
|
||||
const loraAliases = ref<Record<string, string>>({})
|
||||
const folderId = ref('')
|
||||
const browseFolderId = ref('')
|
||||
const renamingId = ref('')
|
||||
@@ -2487,6 +2526,29 @@ const imageV2LoraOptions = computed(() => mergeLoraOptions(
|
||||
...(v2ConceptLoraEnabled.value && imageV2ConceptFile.value ? [imageV2ConceptFile.value] : [])
|
||||
], v2Engine.value)
|
||||
))
|
||||
const autoOutputName = computed(() => {
|
||||
const engine = studioMode.value === 'editv2'
|
||||
? v2Engine.value
|
||||
: studioMode.value === 'edit'
|
||||
? 'klein'
|
||||
: videoEngine.value
|
||||
const stack = studioMode.value === 'editv2'
|
||||
? imageV2EngineLoras()
|
||||
: studioMode.value === 'edit'
|
||||
? imageLoraStack.value
|
||||
: videoLoraStack.value
|
||||
const base = buildOutputBaseName({
|
||||
inputFilename: file.value?.name || '',
|
||||
engine,
|
||||
loraStack: stack,
|
||||
aliases: loraAliases.value
|
||||
})
|
||||
const existing = [
|
||||
...stills.value.filter(item => item.folderId === folderId.value).map(item => item.name || ''),
|
||||
...clips.value.filter(item => item.folderId === folderId.value).map(item => item.name || '')
|
||||
]
|
||||
return nextUniqueName(existing, base)
|
||||
})
|
||||
const identityRefs = ref<(File | null)[]>([null, null, null, null])
|
||||
const identityPreviews = ref(['', '', '', ''])
|
||||
const globalLocks = ref('')
|
||||
@@ -2755,13 +2817,14 @@ const gpuSleepHint = computed(() => {
|
||||
return 'GPU is offline. Generate will try to start Comfy. Or click Wake GPU.'
|
||||
})
|
||||
const editBlocked = computed(() => {
|
||||
if (!file.value || !prompt.value.trim() || !folderId.value) return true
|
||||
if (!file.value || !folderId.value) return true
|
||||
if (!prompt.value.trim() && !imageFillWithoutPrompt.value) return true
|
||||
if (editPassQueue.value.some(item => !item.prompt.trim())) return true
|
||||
return false
|
||||
})
|
||||
const editBlockReason = computed(() => {
|
||||
if (!file.value) return 'Load a still first.'
|
||||
if (!prompt.value.trim()) return 'Write an edit prompt first.'
|
||||
if (!prompt.value.trim() && !imageFillWithoutPrompt.value) return 'Write an edit prompt first, or pick Landscape / Portrait / Square to fill.'
|
||||
if (!folderId.value) return 'Choose a library folder before editing.'
|
||||
if (editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
|
||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
||||
@@ -2774,7 +2837,8 @@ const stillBHint = computed(() => {
|
||||
return 'Still A is the edit image. Still B is style or background.'
|
||||
})
|
||||
const editV2Blocked = computed(() => {
|
||||
if (!prompt.value.trim() || !folderId.value) return true
|
||||
if (!folderId.value) return true
|
||||
if (!prompt.value.trim() && !imageFillWithoutPrompt.value) return true
|
||||
if (v2Mode.value !== 'generate' && !file.value) return true
|
||||
if (v2Mode.value === 'compose' && !editRefFile.value) return true
|
||||
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
|
||||
@@ -2784,7 +2848,11 @@ const editV2BlockReason = computed(() => {
|
||||
if (v2Mode.value !== 'generate' && !file.value) return 'Load still A first.'
|
||||
if (v2Mode.value === 'compose' && !editRefFile.value) return 'Compose requires still B. This will not fall back to one-image generation.'
|
||||
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return 'Paint a mask on Still A first. Refine will not fall back to Edit.'
|
||||
if (!prompt.value.trim()) return 'Write a prompt first.'
|
||||
if (!prompt.value.trim() && !imageFillWithoutPrompt.value) {
|
||||
return v2Mode.value === 'generate'
|
||||
? 'Write a prompt first.'
|
||||
: 'Write a prompt, or pick Landscape / Portrait / Square to fill the frame.'
|
||||
}
|
||||
if (!folderId.value) return 'Choose a library folder before generating.'
|
||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
|
||||
return ''
|
||||
@@ -3275,12 +3343,15 @@ function applyInstancePrivacy() {
|
||||
hideThumbnail.value = readPrivacyToggle('aigen-hide-output-preview', true)
|
||||
}
|
||||
|
||||
function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[] } | unknown) {
|
||||
const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[] }
|
||||
function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string> } | unknown) {
|
||||
const payload = (data || {}) as { folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string> }
|
||||
folders.value = payload.folders || []
|
||||
clips.value = payload.clips || []
|
||||
stills.value = payload.stills || []
|
||||
drafts.value = payload.drafts || []
|
||||
if (payload.loraAliases && typeof payload.loraAliases === 'object') {
|
||||
loraAliases.value = payload.loraAliases
|
||||
}
|
||||
if (!folderId.value || !folders.value.some(folder => folder.id === folderId.value)) {
|
||||
folderId.value = folders.value[0]?.id || ''
|
||||
}
|
||||
@@ -3290,7 +3361,7 @@ function applyLibrary(data: { folders?: LibraryFolder[]; clips?: LibraryClip[];
|
||||
}
|
||||
|
||||
async function loadLibrary(light = false) {
|
||||
const data = await $fetch<{ folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[] }>(light ? '/api/library?light=1' : '/api/library')
|
||||
const data = await $fetch<{ folders?: LibraryFolder[]; clips?: LibraryClip[]; stills?: LibraryStill[]; drafts?: RetryDraft[]; loraAliases?: Record<string, string> }>(light ? '/api/library?light=1' : '/api/library')
|
||||
applyLibrary(data)
|
||||
if (light) return
|
||||
const queued = await $fetch<{ pendingCount?: number }>('/api/queues').catch(() => ({ pendingCount: 0 }))
|
||||
@@ -3832,6 +3903,7 @@ function currentPresetSnapshot() {
|
||||
mode: v2Mode.value,
|
||||
engine: v2Engine.value,
|
||||
task: v2Mode.value === 'compose' ? v2Task.value : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene',
|
||||
prompt: prompt.value,
|
||||
negative: v2Negative.value,
|
||||
snofsModel: imageV2EngineLoras().find(item => isImageV2ConceptLora(item.name))?.strengthModel ?? 0,
|
||||
snofsClip: imageV2EngineLoras().find(item => isImageV2ConceptLora(item.name))?.strengthClip ?? 0,
|
||||
@@ -3842,8 +3914,9 @@ function currentPresetSnapshot() {
|
||||
megapixels: v2Mode.value === 'generate' ? undefined : clampImageScaleMegapixels(v2Megapixels.value, 1),
|
||||
turbo: v2Turbo.value === true,
|
||||
strength: v2Mode.value === 'generate' || (v2Mode.value !== 'refine' && v2Engine.value !== 'krea') ? undefined : clampImageV2Denoise(v2Denoise.value),
|
||||
width: v2Mode.value === 'generate' ? v2Width.value : undefined,
|
||||
height: v2Mode.value === 'generate' ? v2Height.value : undefined
|
||||
aspect: aspect.value,
|
||||
width: v2Mode.value === 'generate' ? generateSizeForAspect(aspect.value).width : undefined,
|
||||
height: v2Mode.value === 'generate' ? generateSizeForAspect(aspect.value).height : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3852,11 +3925,13 @@ function currentPresetSnapshot() {
|
||||
kind: 'image' as const,
|
||||
loraStack: filterLoraStackForStudio(imageLoraStack.value, ltxEnabled.value),
|
||||
settings: {
|
||||
prompt: prompt.value,
|
||||
steps: clampImageSteps(editSteps.value),
|
||||
cfg: clampImageCfg(editCfg.value),
|
||||
negative: editNegative.value,
|
||||
scaleToTotalPixels: editScaleToTotalPixels.value,
|
||||
scaleMegapixels: clampImageScaleMegapixels(editScaleMegapixels.value)
|
||||
scaleMegapixels: clampImageScaleMegapixels(editScaleMegapixels.value),
|
||||
aspect: aspect.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3886,6 +3961,7 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
||||
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
|
||||
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
|
||||
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
|
||||
if (typeof settings.prompt === 'string') prompt.value = settings.prompt
|
||||
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
|
||||
imageV2LoraStack.value = mergeLegacyImageV2Loras(filterLoraStackForImageEngine(stack, v2Engine.value), {
|
||||
engine: v2Engine.value,
|
||||
@@ -3902,8 +3978,9 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
||||
v2Megapixels.value = clampImageScaleMegapixels(settings.megapixels, 1)
|
||||
v2Turbo.value = settings.turbo === true
|
||||
if (v2Mode.value === 'refine' || (v2Engine.value === 'krea' && v2Mode.value !== 'generate')) v2Denoise.value = clampImageV2Denoise(settings.strength)
|
||||
aspect.value = parseImageAspect(settings.aspect)
|
||||
if (v2Mode.value === 'generate') {
|
||||
const size = clampImageV2Size(settings.width, settings.height)
|
||||
const size = generateSizeForAspect(aspect.value)
|
||||
v2Width.value = size.width
|
||||
v2Height.value = size.height
|
||||
}
|
||||
@@ -3913,17 +3990,21 @@ function applyGenerationPreset(preset: GenerationPreset) {
|
||||
} else if (preset.kind === 'image') {
|
||||
imageLoraStack.value = stack
|
||||
const settings = preset.settings as {
|
||||
prompt?: string
|
||||
steps?: number
|
||||
cfg?: number
|
||||
negative?: string
|
||||
scaleToTotalPixels?: boolean
|
||||
scaleMegapixels?: number
|
||||
aspect?: string
|
||||
}
|
||||
if (typeof settings.prompt === 'string') prompt.value = settings.prompt
|
||||
if (typeof settings.steps === 'number') editSteps.value = clampImageSteps(settings.steps)
|
||||
if (typeof settings.cfg === 'number') editCfg.value = clampImageCfg(settings.cfg)
|
||||
if (typeof settings.negative === 'string') editNegative.value = settings.negative
|
||||
editScaleToTotalPixels.value = settings.scaleToTotalPixels === true
|
||||
editScaleMegapixels.value = clampImageScaleMegapixels(settings.scaleMegapixels)
|
||||
aspect.value = parseImageAspect(settings.aspect)
|
||||
imagePresetId.value = preset.id
|
||||
imagePresetName.value = preset.name
|
||||
} else {
|
||||
@@ -4662,7 +4743,7 @@ async function restoreStudioJob(id: string) {
|
||||
const stillId = payload.stillId
|
||||
if (stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
readFile(new File([blob], payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId })
|
||||
readFile(new File([blob], stillFileName(stills.value.find(item => item.id === stillId)) || payload.stillFilename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId })
|
||||
} else {
|
||||
resetImage()
|
||||
}
|
||||
@@ -4819,7 +4900,13 @@ function stopTimer(kind?: 'video' | 'edit') {
|
||||
}
|
||||
|
||||
function clipFileName(name: string) {
|
||||
return name.replace(/[<>:"/\\|?*'`]/g, '-').replace(/\s+/g, ' ').trim().slice(0, 80) || 'aigen'
|
||||
return name.replace(/[<>:"/\\|?*'`]/g, '-').replace(/\s+/g, ' ').trim().slice(0, 80) || 'output'
|
||||
}
|
||||
|
||||
function stillFileName(still?: Pick<LibraryStill, 'name' | 'filename'> | null) {
|
||||
const named = (still?.name || '').trim()
|
||||
if (named) return `${named}.png`
|
||||
return still?.filename || 'still.png'
|
||||
}
|
||||
|
||||
function nextClipPartName(name: string) {
|
||||
@@ -4866,7 +4953,7 @@ async function loadRerunImage(item: LibraryClip, parts: LibraryClip[]) {
|
||||
if (stillId) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
if (still?.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], still?.filename || 'still.png', { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId })
|
||||
readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), { keepThumbnailPref: true, stillId })
|
||||
return true
|
||||
}
|
||||
const blob = await $fetch<Blob>(`/api/library/clips/${item.id}/thumb`, { responseType: 'blob' })
|
||||
@@ -5136,7 +5223,7 @@ function openClip(item: LibraryClip) {
|
||||
awaitingReveal.value = item.hideThumbnail
|
||||
playerSize.value = { width: item.width || 1344, height: item.height || 768 }
|
||||
browseFolderId.value = item.folderId
|
||||
videoDownloadName.value = clipFileName(item.name || item.prompt.split(/[.!?\n]/)[0] || 'aigen')
|
||||
videoDownloadName.value = clipFileName(item.name || item.prompt.split(/[.!?\n]/)[0] || 'output')
|
||||
if (!videoBusy.value) {
|
||||
statusMessage.value = item.hideThumbnail ? 'Hidden clip ready' : 'Loaded from library'
|
||||
progress.value = 100
|
||||
@@ -5180,8 +5267,9 @@ function applyStillSettings(still: LibraryStill) {
|
||||
if (settings.megapixels != null) v2Megapixels.value = clampImageScaleMegapixels(settings.megapixels, 1)
|
||||
v2Turbo.value = settings.turbo === true
|
||||
if ((mode === 'refine' || (v2Engine.value === 'krea' && mode !== 'generate')) && settings.strength != null) v2Denoise.value = clampImageV2Denoise(settings.strength)
|
||||
aspect.value = parseImageAspect(settings.aspect)
|
||||
if (mode === 'generate') {
|
||||
const size = clampImageV2Size(settings.width, settings.height)
|
||||
const size = generateSizeForAspect(aspect.value)
|
||||
v2Width.value = size.width
|
||||
v2Height.value = size.height
|
||||
}
|
||||
@@ -5214,12 +5302,13 @@ function applyStillSettings(still: LibraryStill) {
|
||||
imageLoraStack.value = filterLoraStackForStudio(readLoraStack(settings), ltxEnabled.value)
|
||||
}
|
||||
if (settings.seed != null) seedInput.value = String(Math.round(settings.seed))
|
||||
aspect.value = parseImageAspect(settings.aspect)
|
||||
}
|
||||
|
||||
async function loadStillAsEditRef(stillId: string) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
const still = stills.value.find(item => item.id === stillId)
|
||||
setEditRef(new File([blob], still?.filename || 'still-b.png', { type: blob.type || 'image/png' }))
|
||||
setEditRef(new File([blob], stillFileName(still) || 'still-b.png', { type: blob.type || 'image/png' }))
|
||||
}
|
||||
|
||||
function stillUseAsInputMessage(still: LibraryStill) {
|
||||
@@ -5245,7 +5334,7 @@ async function useStillAsInput(still: LibraryStill) {
|
||||
if (!generate) {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${still.id}`, { responseType: 'blob' })
|
||||
if (still.hideInput && showPrivacyToggles.value) hideInputPreview.value = true
|
||||
readFile(new File([blob], still.filename, { type: blob.type || 'image/png' }), { stillId: still.id })
|
||||
readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), { stillId: still.id })
|
||||
}
|
||||
if (dest === 'imagev2' && still.settings?.mode === 'compose' && still.settings.referenceStillId) {
|
||||
try {
|
||||
@@ -5611,7 +5700,7 @@ function startRename(item: LibraryClip, family = false) {
|
||||
const source = family
|
||||
? (familyBaseName(item.name || '') || item.name || '')
|
||||
: (item.name || '')
|
||||
renameDraft.value = source.trim() || clipFileName(item.prompt.split(/[.!?\n]/)[0] || 'aigen')
|
||||
renameDraft.value = source.trim() || clipFileName(item.prompt.split(/[.!?\n]/)[0] || 'output')
|
||||
nextTick(() => {
|
||||
renameInput.value?.focus()
|
||||
renameInput.value?.select()
|
||||
@@ -5698,7 +5787,7 @@ function removeEditPass(id: string) {
|
||||
}
|
||||
|
||||
async function editImage() {
|
||||
if (!file.value || !prompt.value.trim()) {
|
||||
if (!file.value || (!prompt.value.trim() && !imageFillWithoutPrompt.value)) {
|
||||
toast(editBlockReason.value || 'Load a still first.')
|
||||
return
|
||||
}
|
||||
@@ -5717,6 +5806,7 @@ async function editImage() {
|
||||
body.append('image', file.value)
|
||||
if (editRefFile.value) body.append('image2', editRefFile.value)
|
||||
body.append('prompt', prompt.value.trim())
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('negative', editNegative.value)
|
||||
body.append('steps', String(clampImageSteps(editSteps.value)))
|
||||
body.append('cfg', String(clampImageCfg(editCfg.value)))
|
||||
@@ -5727,9 +5817,10 @@ async function editImage() {
|
||||
body.append('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
|
||||
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
||||
body.append('name', clipName.value.trim())
|
||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
|
||||
if (imageLoraStack.value.length) body.append('loraStack', JSON.stringify(imageLoraStack.value))
|
||||
const downloadName = clipFileName(clipName.value.trim() || prompt.value.split(/[.!?\n]/)[0] || 'aigen-edit')
|
||||
const downloadName = clipFileName(clipName.value.trim() || autoOutputName.value)
|
||||
const started = await $fetch<{
|
||||
jobId: string
|
||||
studioJobId?: string
|
||||
@@ -5828,10 +5919,11 @@ function setV2Engine(engine: ImageV2Engine) {
|
||||
}
|
||||
}
|
||||
|
||||
function setGenerateSize(aspect: ImageV2GenerateAspect) {
|
||||
const [width, height] = IMAGE_V2_GENERATE_ASPECTS[aspect]
|
||||
v2Width.value = width
|
||||
v2Height.value = height
|
||||
function setImageAspect(id: AspectId) {
|
||||
aspect.value = id
|
||||
const size = generateSizeForAspect(id)
|
||||
v2Width.value = size.width
|
||||
v2Height.value = size.height
|
||||
}
|
||||
|
||||
function applyRefinePreset(kind: 'face' | 'hand' | 'heavy') {
|
||||
@@ -5930,10 +6022,11 @@ async function editImageV2() {
|
||||
body.append('strength', String(clampImageV2Denoise(v2Denoise.value)))
|
||||
}
|
||||
if (v2Mode.value === 'generate') {
|
||||
const size = clampImageV2Size(v2Width.value, v2Height.value)
|
||||
const size = generateSizeForAspect(aspect.value)
|
||||
body.append('width', String(size.width))
|
||||
body.append('height', String(size.height))
|
||||
}
|
||||
body.append('aspect', aspect.value)
|
||||
body.append('engine', v2Engine.value)
|
||||
body.append('prompt', prompt.value.trim())
|
||||
body.append('negative', v2Negative.value)
|
||||
@@ -5950,6 +6043,7 @@ async function editImageV2() {
|
||||
body.append('hideInput', String(showPrivacyToggles.value && hideInputPreview.value))
|
||||
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
|
||||
body.append('name', clipName.value.trim())
|
||||
if (inputStillId.value) body.append('stillId', inputStillId.value)
|
||||
const v2Stack = filterLoraStackForImageEngine(
|
||||
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
|
||||
v2Engine.value
|
||||
@@ -5962,7 +6056,7 @@ async function editImageV2() {
|
||||
}
|
||||
}
|
||||
if (v2Stack.length) body.append('loraStack', JSON.stringify(v2Stack))
|
||||
const downloadName = clipFileName(clipName.value.trim() || prompt.value.split(/[.!?\n]/)[0] || 'aigen-v2')
|
||||
const downloadName = clipFileName(clipName.value.trim() || autoOutputName.value)
|
||||
const started = await $fetch<{
|
||||
jobId: string
|
||||
studioJobId?: string
|
||||
@@ -6069,7 +6163,7 @@ async function ensureVideoInputStill() {
|
||||
const blob = await $fetch<Blob>(`/api/library/stills/${stillId}`, { responseType: 'blob' })
|
||||
if (!blob?.size) throw new Error('Could not load that still')
|
||||
const still = stills.value.find(item => item.id === stillId)
|
||||
readFile(new File([blob], still?.filename || 'still.png', { type: blob.type || 'image/png' }), {
|
||||
readFile(new File([blob], stillFileName(still), { type: blob.type || 'image/png' }), {
|
||||
keepThumbnailPref: true,
|
||||
stillId
|
||||
})
|
||||
@@ -6168,7 +6262,7 @@ async function generate() {
|
||||
}))))
|
||||
body.append('queueAutoRun', String(queueAutoRun.value))
|
||||
}
|
||||
const downloadName = clipFileName(clipName.value.trim() || initialPrompt.split(/[.!?\n]/)[0] || 'aigen')
|
||||
const downloadName = clipFileName(clipName.value.trim() || autoOutputName.value)
|
||||
const chainPlan = plannedChain.value.map(part => ({ ...part }))
|
||||
console.info('[aigen generate payload]', {
|
||||
keys: [...body.keys()],
|
||||
|
||||
Reference in New Issue
Block a user