diff --git a/backend/internal/service/v1.go b/backend/internal/service/v1.go index c464b29..4e194b3 100644 --- a/backend/internal/service/v1.go +++ b/backend/internal/service/v1.go @@ -1893,7 +1893,7 @@ func (s *V1Service) generateCustomVideo(ctx context.Context, eventID string, mod if len(active) == 0 { return nil, "", ErrNoProviderAccount } - size := upstreamSize(aspectRatio, resolution) + size := upstreamVideoSize(aspectRatio, resolution) var lastErr error var videoURL string busy := 0 @@ -1974,6 +1974,38 @@ func upstreamSize(aspectRatio, resolution string) string { return fmt.Sprintf("%dx%d", base*w/h, base) } +// upstreamVideoSize maps our (ratio, resolution) to a "WxH" size for video +// upstreams. Video "Np" tiers set the SHORT edge in pixels (like grok: +// 720p 1:1 → 720x720, 720p 16:9 → 1280x720); 2K/4K fall back to the +// long-edge mapping shared with images. +func upstreamVideoSize(aspectRatio, resolution string) string { + short := 0 + switch res := strings.ToLower(strings.TrimSpace(resolution)); res { + case "540p": + short = 540 + case "720p", "": + short = 720 + case "1080p": + short = 1080 + } + if short == 0 { + return upstreamSize(aspectRatio, resolution) + } + w, h := 1, 1 + parts := strings.Split(strings.ReplaceAll(strings.TrimSpace(aspectRatio), "x", ":"), ":") + if len(parts) == 2 { + if a, e1 := strconv.Atoi(strings.TrimSpace(parts[0])); e1 == nil && a > 0 { + if b, e2 := strconv.Atoi(strings.TrimSpace(parts[1])); e2 == nil && b > 0 { + w, h = a, b + } + } + } + if w >= h { + return fmt.Sprintf("%dx%d", short*w/h, short) + } + return fmt.Sprintf("%dx%d", short, short*h/w) +} + // upstreamQuality maps a resolution tier to the OpenAI quality enum. func upstreamQuality(resolution string) string { switch strings.ToUpper(strings.TrimSpace(resolution)) { diff --git a/frontend/src/views/PlaygroundView.vue b/frontend/src/views/PlaygroundView.vue index d0ce0f5..816f583 100644 --- a/frontend/src/views/PlaygroundView.vue +++ b/frontend/src/views/PlaygroundView.vue @@ -303,7 +303,13 @@ async function copyImage(url) { const count = ref(1) const batchCount = computed(() => (mode.value === 'image' ? Math.max(1, Math.min(4, count.value)) : 1)) +// Guard against accidental double-clicks: a second run() within 600ms is +// ignored (each click otherwise fires an independent, charged generation). +let lastRunAt = 0 async function run() { + const now = Date.now() + if (now - lastRunAt < 600) return + lastRunAt = now if (!modelId.value) { error.value = '请选择模型'; return } if (!prompt.value.trim()) { error.value = '请输入提示词'; return } if (refsRequired.value && refImages.value.length < 1) {