fix: 默认 1920x1920,支持首帧选择上一个的尾帧
This commit is contained in:
@@ -107,7 +107,7 @@ func NewGeminiImageClient(baseURL, apiKey, model, endpoint string) *GeminiImageC
|
|||||||
|
|
||||||
func (c *GeminiImageClient) GenerateImage(prompt string, opts ...ImageOption) (*ImageResult, error) {
|
func (c *GeminiImageClient) GenerateImage(prompt string, opts ...ImageOption) (*ImageResult, error) {
|
||||||
options := &ImageOptions{
|
options := &ImageOptions{
|
||||||
Size: "1024x1024",
|
Size: "1920x1920",
|
||||||
Quality: "standard",
|
Quality: "standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func NewVolcEngineImageClient(baseURL, apiKey, model, endpoint, queryEndpoint st
|
|||||||
|
|
||||||
func (c *VolcEngineImageClient) GenerateImage(prompt string, opts ...ImageOption) (*ImageResult, error) {
|
func (c *VolcEngineImageClient) GenerateImage(prompt string, opts ...ImageOption) (*ImageResult, error) {
|
||||||
options := &ImageOptions{
|
options := &ImageOptions{
|
||||||
Size: "1024x1024",
|
Size: "1920x1920",
|
||||||
Quality: "standard",
|
Quality: "standard",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -360,6 +360,33 @@
|
|||||||
<!-- 首帧 -->
|
<!-- 首帧 -->
|
||||||
<div v-show="selectedVideoFrameType === 'first'" class="image-scroll-container"
|
<div v-show="selectedVideoFrameType === 'first'" class="image-scroll-container"
|
||||||
style="max-height: 280px; overflow-y: auto; overflow-x: hidden;">
|
style="max-height: 280px; overflow-y: auto; overflow-x: hidden;">
|
||||||
|
|
||||||
|
<!-- 上一镜头尾帧推荐(紧凑版) -->
|
||||||
|
<div v-if="previousStoryboardLastFrames.length > 0"
|
||||||
|
style="margin-bottom: 12px; padding: 8px; background: #1a1a2e; border: 1px solid #4a4a6a; border-radius: 6px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 6px;">
|
||||||
|
<el-tag size="small" type="primary">
|
||||||
|
上一镜头 #{{ previousStoryboard?.storyboard_number }} 尾帧
|
||||||
|
</el-tag>
|
||||||
|
<span style="color: #aaa; font-size: 11px;">点击添加为首帧参考</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<div v-for="img in previousStoryboardLastFrames" :key="'prev-' + img.id"
|
||||||
|
class="reference-item"
|
||||||
|
:class="{ selected: selectedImagesForVideo.includes(img.id) }"
|
||||||
|
style="position: relative; border: 2px solid #1890ff; border-radius: 4px; overflow: hidden; cursor: pointer;"
|
||||||
|
@click="selectPreviousLastFrame(img)">
|
||||||
|
<el-image :src="img.image_url" fit="cover"
|
||||||
|
style="width: 60px; height: 40px; display: block; pointer-events: none;" />
|
||||||
|
<div v-if="selectedImagesForVideo.includes(img.id)"
|
||||||
|
style="position: absolute; top: 0; right: 0; background: #52c41a; color: #fff; font-size: 10px; padding: 1px 4px;">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前镜头首帧列表 -->
|
||||||
<div class="reference-grid"
|
<div class="reference-grid"
|
||||||
style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; max-width: 600px;">
|
style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; max-width: 600px;">
|
||||||
<div
|
<div
|
||||||
@@ -378,7 +405,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty
|
<el-empty
|
||||||
v-if="!videoReferenceImages.some(i => i.status === 'completed' && i.image_url && i.frame_type === 'first')"
|
v-if="!videoReferenceImages.some(i => i.status === 'completed' && i.image_url && i.frame_type === 'first') && previousStoryboardLastFrames.length === 0"
|
||||||
description="暂无首帧图片" size="small" />
|
description="暂无首帧图片" size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -883,7 +910,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import {
|
import {
|
||||||
ArrowLeft, Plus, Picture, VideoPlay, VideoPause, View, Setting,
|
ArrowLeft, Plus, Picture, VideoPlay, VideoPause, View, Setting,
|
||||||
Upload, MagicStick, VideoCamera, ZoomIn, ZoomOut, Top, Bottom, Check, Close, Right,
|
Upload, MagicStick, VideoCamera, ZoomIn, ZoomOut, Top, Bottom, Check, Close, Right,
|
||||||
Timer, Calendar, Clock, Loading, WarningFilled, Delete
|
Timer, Calendar, Clock, Loading, WarningFilled, Delete, Connection
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { dramaAPI } from '@/api/drama'
|
import { dramaAPI } from '@/api/drama'
|
||||||
import { generateFramePrompt, type FrameType } from '@/api/frame'
|
import { generateFramePrompt, type FrameType } from '@/api/frame'
|
||||||
@@ -1226,6 +1253,67 @@ const currentStoryboard = computed(() => {
|
|||||||
return storyboards.value.find(s => String(s.id) === String(currentStoryboardId.value)) || null
|
return storyboards.value.find(s => String(s.id) === String(currentStoryboardId.value)) || null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取上一个镜头
|
||||||
|
const previousStoryboard = computed(() => {
|
||||||
|
if (!currentStoryboardId.value || storyboards.value.length < 2) return null
|
||||||
|
const currentIndex = storyboards.value.findIndex(s => String(s.id) === String(currentStoryboardId.value))
|
||||||
|
if (currentIndex <= 0) return null
|
||||||
|
return storyboards.value[currentIndex - 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上一个镜头的尾帧图片列表(支持多个)
|
||||||
|
const previousStoryboardLastFrames = ref<any[]>([])
|
||||||
|
|
||||||
|
// 加载上一个镜头的尾帧
|
||||||
|
const loadPreviousStoryboardLastFrame = async () => {
|
||||||
|
if (!previousStoryboard.value) {
|
||||||
|
previousStoryboardLastFrames.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await imageAPI.listImages({
|
||||||
|
storyboard_id: previousStoryboard.value.id,
|
||||||
|
frame_type: 'last',
|
||||||
|
page: 1,
|
||||||
|
page_size: 10
|
||||||
|
})
|
||||||
|
const images = result.items || []
|
||||||
|
previousStoryboardLastFrames.value = images.filter((img: any) => img.status === 'completed' && img.image_url)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载上一镜头尾帧失败:', error)
|
||||||
|
previousStoryboardLastFrames.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择上一镜头尾帧作为首帧参考
|
||||||
|
const selectPreviousLastFrame = (img: any) => {
|
||||||
|
// 检查是否已选中,已选中则取消
|
||||||
|
const currentIndex = selectedImagesForVideo.value.indexOf(img.id)
|
||||||
|
if (currentIndex > -1) {
|
||||||
|
selectedImagesForVideo.value.splice(currentIndex, 1)
|
||||||
|
ElMessage.success('已取消首帧参考')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参考handleImageSelect的逻辑,根据模式处理
|
||||||
|
if (!selectedReferenceMode.value || selectedReferenceMode.value === 'single') {
|
||||||
|
// 单图模式或未选模式:直接替换
|
||||||
|
selectedImagesForVideo.value = [img.id]
|
||||||
|
} else if (selectedReferenceMode.value === 'first_last') {
|
||||||
|
// 首尾帧模式:作为首帧参考
|
||||||
|
selectedImagesForVideo.value = [img.id]
|
||||||
|
} else if (selectedReferenceMode.value === 'multiple') {
|
||||||
|
// 多图模式:添加到列表
|
||||||
|
const capability = currentModelCapability.value
|
||||||
|
if (capability && selectedImagesForVideo.value.length >= capability.maxImages) {
|
||||||
|
ElMessage.warning(`最多只能选择${capability.maxImages}张图片`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedImagesForVideo.value.push(img.id)
|
||||||
|
}
|
||||||
|
ElMessage.success('已添加为首帧参考')
|
||||||
|
}
|
||||||
|
|
||||||
// 监听帧类型切换,从存储中加载或清空
|
// 监听帧类型切换,从存储中加载或清空
|
||||||
watch(selectedFrameType, (newType) => {
|
watch(selectedFrameType, (newType) => {
|
||||||
// 切换帧类型时,停止之前的轮询,避免旧结果覆盖新帧类型
|
// 切换帧类型时,停止之前的轮询,避免旧结果覆盖新帧类型
|
||||||
@@ -1269,6 +1357,7 @@ watch(currentStoryboard, async (newStoryboard) => {
|
|||||||
generatedImages.value = []
|
generatedImages.value = []
|
||||||
generatedVideos.value = []
|
generatedVideos.value = []
|
||||||
videoReferenceImages.value = []
|
videoReferenceImages.value = []
|
||||||
|
previousStoryboardLastFrames.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,6 +1386,9 @@ watch(currentStoryboard, async (newStoryboard) => {
|
|||||||
|
|
||||||
// 加载该分镜的视频列表
|
// 加载该分镜的视频列表
|
||||||
await loadStoryboardVideos(newStoryboard.id)
|
await loadStoryboardVideos(newStoryboard.id)
|
||||||
|
|
||||||
|
// 加载上一镜头的尾帧
|
||||||
|
await loadPreviousStoryboardLastFrame()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听提示词变化,自动保存到sessionStorage
|
// 监听提示词变化,自动保存到sessionStorage
|
||||||
@@ -1801,13 +1893,17 @@ const selectedImageObjects = computed(() => {
|
|||||||
const firstFrameSlotImage = computed(() => {
|
const firstFrameSlotImage = computed(() => {
|
||||||
if (selectedImagesForVideo.value.length === 0) return null
|
if (selectedImagesForVideo.value.length === 0) return null
|
||||||
const firstImageId = selectedImagesForVideo.value[0]
|
const firstImageId = selectedImagesForVideo.value[0]
|
||||||
|
// 同时搜索当前镜头图片和上一镜头尾帧
|
||||||
return videoReferenceImages.value.find(img => img.id === firstImageId)
|
return videoReferenceImages.value.find(img => img.id === firstImageId)
|
||||||
|
|| previousStoryboardLastFrames.value.find(img => img.id === firstImageId)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 首尾帧模式:获取尾帧图片
|
// 首尾帧模式:获取尾帧图片
|
||||||
const lastFrameSlotImage = computed(() => {
|
const lastFrameSlotImage = computed(() => {
|
||||||
if (!selectedLastImageForVideo.value) return null
|
if (!selectedLastImageForVideo.value) return null
|
||||||
|
// 同时搜索当前镜头图片和上一镜头尾帧
|
||||||
return videoReferenceImages.value.find(img => img.id === selectedLastImageForVideo.value)
|
return videoReferenceImages.value.find(img => img.id === selectedLastImageForVideo.value)
|
||||||
|
|| previousStoryboardLastFrames.value.find(img => img.id === selectedLastImageForVideo.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 移除已选择的图片
|
// 移除已选择的图片
|
||||||
|
|||||||
Reference in New Issue
Block a user