feat(canvas): 生成输入框支持视频/音频参考文件
- 参考上限改为模型三类字段合计(图+视+音),不再被 min(8) 截断;custom 模型的 9图+3视频+0音频 显示为 0/12 - 文件选择器按模型能力放开 accept;按类分别计数和限数 (超图片/视频/音频各自上限时给出对应提示) - 非图片参考在缩略位显示 🎬/🔊;单文件上限放宽到 20MB 与后端一致 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -73,9 +73,24 @@ const countOptions = [1, 2, 3, 4].map((n) => ({ value: n, label: `${n} 张` }))
|
|||||||
const canvasOptions = computed(() =>
|
const canvasOptions = computed(() =>
|
||||||
canvases.value.map((c) => ({ value: c.id, label: c.title || '未命名画布' })),
|
canvases.value.map((c) => ({ value: c.id, label: c.title || '未命名画布' })),
|
||||||
)
|
)
|
||||||
const maxRefs = computed(() => Math.min(8, Math.max(
|
// 参考文件分类上限:custom 模型直接读模型三个字段;老模型只有
|
||||||
Number(model.value?.max_reference_images || 0), model.value?.image_to_image ? 1 : 0,
|
// max_reference_images(各类合计)或 image_to_image 兜底 1 张图。
|
||||||
)))
|
const refCaps = computed(() => {
|
||||||
|
const m = model.value || {}
|
||||||
|
const imgs = Number(m.max_reference_images || 0) || (m.image_to_image ? 1 : 0)
|
||||||
|
const vids = Number(m.max_reference_videos || 0)
|
||||||
|
const auds = Number(m.max_reference_audios || 0)
|
||||||
|
return { imgs, vids, auds, total: imgs + vids + auds }
|
||||||
|
})
|
||||||
|
const maxRefs = computed(() => Math.min(20, refCaps.value.total))
|
||||||
|
const supportsMediaRefs = computed(() => refCaps.value.vids > 0 || refCaps.value.auds > 0)
|
||||||
|
const refFileAccept = computed(() => (supportsMediaRefs.value ? 'image/*,video/*,audio/*' : 'image/*'))
|
||||||
|
const refCounts = computed(() => {
|
||||||
|
const imgs = refImages.value.filter((f) => !f.fileType || f.fileType === 'image').length
|
||||||
|
const vids = refImages.value.filter((f) => f.fileType === 'video').length
|
||||||
|
const auds = refImages.value.filter((f) => f.fileType === 'audio').length
|
||||||
|
return { imgs, vids, auds }
|
||||||
|
})
|
||||||
// 换类型/模型时重置参数默认值(和画图台一致)
|
// 换类型/模型时重置参数默认值(和画图台一致)
|
||||||
watch(() => gen.kind, () => {
|
watch(() => gen.kind, () => {
|
||||||
const first = kindModels.value[0]
|
const first = kindModels.value[0]
|
||||||
@@ -576,7 +591,7 @@ function onPaste(e) {
|
|||||||
// ---- 参考图上传(与画图台一致:直接传文件,不再用选中节点) ----
|
// ---- 参考图上传(与画图台一致:直接传文件,不再用选中节点) ----
|
||||||
const refImages = ref([]) // { name, dataUrl }
|
const refImages = ref([]) // { name, dataUrl }
|
||||||
const refInput = ref(null)
|
const refInput = ref(null)
|
||||||
const MAX_REF_BYTES = 15 * 1024 * 1024
|
const MAX_REF_BYTES = 20 * 1024 * 1024
|
||||||
|
|
||||||
function fileToDataUrl(f) {
|
function fileToDataUrl(f) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -587,23 +602,37 @@ function fileToDataUrl(f) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
async function addRefFile(f) {
|
async function addRefFile(f) {
|
||||||
if (!f?.type?.startsWith('image/')) return
|
const t = f?.type || ''
|
||||||
|
const isImage = t.startsWith('image/')
|
||||||
|
const isVideo = t.startsWith('video/')
|
||||||
|
const isAudio = t.startsWith('audio/')
|
||||||
|
if (!isImage && !(supportsMediaRefs.value && (isVideo || isAudio))) return
|
||||||
if (refImages.value.length >= maxRefs.value) {
|
if (refImages.value.length >= maxRefs.value) {
|
||||||
genMsg.value = `参考图最多 ${maxRefs.value} 张`
|
genMsg.value = `参考文件最多 ${maxRefs.value} 个`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (f.size > MAX_REF_BYTES) {
|
if (f.size > MAX_REF_BYTES) {
|
||||||
genMsg.value = '单张参考图不能超过 15MB'
|
genMsg.value = '单个参考文件不能超过 20MB'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
refImages.value.push({ name: f.name, dataUrl: await fileToDataUrl(f) })
|
const caps = refCaps.value
|
||||||
|
const counts = refCounts.value
|
||||||
|
const kind = isVideo ? 'video' : isAudio ? 'audio' : 'image'
|
||||||
|
if (isVideo && counts.vids >= caps.vids) { genMsg.value = `视频参考最多 ${caps.vids} 个`; return }
|
||||||
|
if (isAudio && counts.auds >= caps.auds) { genMsg.value = `音频参考最多 ${caps.auds} 个`; return }
|
||||||
|
if (isImage && counts.imgs >= caps.imgs) { genMsg.value = `图片参考最多 ${caps.imgs} 张`; return }
|
||||||
|
refImages.value.push({ name: f.name, dataUrl: await fileToDataUrl(f), fileType: kind })
|
||||||
}
|
}
|
||||||
// 画布图片节点 → 参考图(记录 nodeId,生成时自动连边标明来源)
|
// 画布图片节点 → 参考图(记录 nodeId,生成时自动连边标明来源)
|
||||||
async function addNodeAsRef(n) {
|
async function addNodeAsRef(n) {
|
||||||
if (!n || n.type !== 'image' || !n.src) return
|
if (!n || n.type !== 'image' || !n.src) return
|
||||||
if (refImages.value.some((r) => r.nodeId === n.id)) return
|
if (refImages.value.some((r) => r.nodeId === n.id)) return
|
||||||
if (refImages.value.length >= maxRefs.value) {
|
if (refImages.value.length >= maxRefs.value) {
|
||||||
genMsg.value = `参考图最多 ${maxRefs.value} 张`
|
genMsg.value = `参考文件最多 ${maxRefs.value} 个`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (refCounts.value.imgs >= refCaps.value.imgs) {
|
||||||
|
genMsg.value = `图片参考最多 ${refCaps.value.imgs} 张`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -952,21 +981,22 @@ const saveLabel = computed(() => ({
|
|||||||
<div class="cv-composer">
|
<div class="cv-composer">
|
||||||
<div v-if="maxRefs > 0" class="cv-composer__refs">
|
<div v-if="maxRefs > 0" class="cv-composer__refs">
|
||||||
<div v-for="(r, i) in refImages" :key="i" class="cv-refthumb">
|
<div v-for="(r, i) in refImages" :key="i" class="cv-refthumb">
|
||||||
<img :src="r.dataUrl" :alt="r.name">
|
<img v-if="!r.fileType || r.fileType === 'image'" :src="r.dataUrl" :alt="r.name">
|
||||||
|
<span v-else class="cv-refthumb__media">{{ r.fileType === 'video' ? '🎬' : '🔊' }}</span>
|
||||||
<button type="button" class="cv-refthumb__x" title="移除" @click="removeRef(i)">✕</button>
|
<button type="button" class="cv-refthumb__x" title="移除" @click="removeRef(i)">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="refImages.length < maxRefs"
|
v-if="refImages.length < maxRefs"
|
||||||
type="button"
|
type="button"
|
||||||
class="cv-refadd"
|
class="cv-refadd"
|
||||||
title="上传参考图"
|
:title="supportsMediaRefs ? '上传参考图/视频/音频' : '上传参考图'"
|
||||||
@click="refInput?.click()"
|
@click="refInput?.click()"
|
||||||
>
|
>
|
||||||
<span class="cv-refadd__icon">+</span>
|
<span class="cv-refadd__icon">+</span>
|
||||||
<span>参考图</span>
|
<span>参考图</span>
|
||||||
<span class="cv-refadd__n">{{ refImages.length }}/{{ maxRefs }}</span>
|
<span class="cv-refadd__n">{{ refImages.length }}/{{ maxRefs }}</span>
|
||||||
</button>
|
</button>
|
||||||
<input ref="refInput" type="file" accept="image/*" multiple hidden @change="onRefPicked">
|
<input ref="refInput" type="file" :accept="refFileAccept" multiple hidden @change="onRefPicked">
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="gen.prompt"
|
v-model="gen.prompt"
|
||||||
@@ -1340,6 +1370,13 @@ const saveLabel = computed(() => ({
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.cv-refthumb__media {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
.cv-refthumb__x {
|
.cv-refthumb__x {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|||||||
Reference in New Issue
Block a user