ai-chatbot/tests/routes/video-canvas-model-matching.test.ts

86 lines
6.8 KiB
TypeScript

import {test, expect} from '@playwright/test';
import {chooseStoryVideoSettings, filterCanvasModels, getCanvasVideoModes, getCanvasVideoParameters, validateCanvasVideoInputs} from '@/lib/video-canvas/model-matching';
const language = {id: 'text', modelId: 'agnes-1.5-flash', providerType:'custom', supportsVision:true, capabilities:{kind:'text', provider:'agnes-ai'}};
const image = {id:'image', modelId:'gpt-image-2', providerType:'vip98k', supportsImageGen:true, supportsVision:true, capabilities:{kind:'image'}};
const threeD = {id:'3d', modelId:'tencent/hunyuan3d-pro/image-to-3d', providerType:'atlascloud', supportsVision:true, capabilities:{kind:'3d'}};
const video = {id:'video', modelId:'bytedance/seedance-2.0-mini/image-to-video', providerType:'atlascloud', capabilities:{kind:'video', supportedModes:['first-frame','first-last-frame'], supportedResolutions:['720p','1080p'], supportedRatios:['16:9','9:16'], durationOptions:[5,10]}};
test('never offers text, image or 3D models when video is unavailable', () => {
expect(filterCanvasModels([language,image,threeD], 'video')).toEqual([]);
});
test('vision analysis excludes image generation and 3D models even when supportsVision is true', () => {
expect(filterCanvasModels([language,image,threeD,video], 'multimodal')).toEqual([language]);
});
test('video choices honor enabled status, executable adapter and exact input mode', () => {
expect(filterCanvasModels([video], 'video','text-to-video')).toEqual([]);
expect(filterCanvasModels([video], 'video','first-last-frame')).toEqual([video]);
expect(filterCanvasModels([{...video,isEnabled:false}], 'video')).toEqual([]);
expect(filterCanvasModels([{...video,providerType:'unimplemented'}], 'video')).toEqual([]);
});
test('normalizes provider mode aliases without inventing unknown capabilities', () => {
expect(getCanvasVideoModes({...video, capabilities:{kind:'video',supportedModes:['image-to-video','reference-to-video','made-up']}})).toEqual(['first-frame','reference-images']);
expect(getCanvasVideoModes({...video,capabilities:{kind:'video'}})).toEqual([]);
});
test('image editing rejects a model that does not accept references', () => {
expect(filterCanvasModels([{...image,supportsVision:false}], 'image-edit')).toEqual([]);
expect(filterCanvasModels([image], 'image-edit')).toEqual([image]);
});
test('mode parameter constraints override generic choices and never add 4K', () => {
const options = getCanvasVideoParameters({...video,capabilities:{...video.capabilities,modeConstraints:{'first-last-frame':{durationOptions:[5],resolutionOptions:['720p'],ratioOptions:['16:9']}}}},'first-last-frame');
expect(options).toMatchObject({durations:[5],resolutions:['720p'],ratios:['16:9']});
expect(getCanvasVideoParameters(undefined).resolutions).toEqual([]);
});
test('preserves configured discrete duration choices', () => {
expect(getCanvasVideoParameters(video).durations).toEqual([5,10]);
});
test('story generation never submits a 4K-only model through the 1080P pipeline', () => {
const textVideo = {...video, capabilities:{...video.capabilities, supportedModes:['text-to-video'], supportedResolutions:['4k']}};
expect(() => chooseStoryVideoSettings([textVideo], false, 5)).toThrow('可执行');
expect(chooseStoryVideoSettings([{...textVideo, capabilities:{...textVideo.capabilities, supportedResolutions:['4k','1080p']}}], false, 5).resolution).toBe('1080p');
});
test('TVstar snaps a five-second shot to the configured Veo eight-second duration', () => {
const veo = {
id: 'veo', modelId: 'veo-3.1-fast', providerType: 'vip98k',
capabilities: { kind: 'video', supportedModes: ['first-frame'], supportedResolutions: ['720p'], supportedRatios: ['16:9'], durationOptions: [8] },
};
expect(chooseStoryVideoSettings([veo], true, 5, 'veo')).toMatchObject({
mode: 'first-frame', model: 'veo', duration: 8, ratio: '16:9', resolution: '720p',
});
});
test('rejects missing tail frame and references silently attached to text-to-video', () => {
expect(()=>validateCanvasVideoInputs('first-last-frame',['head'])).toThrow('两张');
expect(()=>validateCanvasVideoInputs('text-to-video',['head'])).toThrow('切换');
expect(()=>validateCanvasVideoInputs('reference-images',[])).toThrow('至少');
expect(()=>validateCanvasVideoInputs('first-frame',['head','tail'])).toThrow('一张');
expect(()=>validateCanvasVideoInputs('first-last-frame',['head','tail'])).not.toThrow();
});
import {buildVip98kModelPresets} from '@/lib/ai/vip98k-models';
import {getVip98kImageBindingKeys, selectVip98kTaskInvocation} from '@/lib/ai/vip98k-task-invocation';
test('reads task schema rather than a text-only example for Veo frame and ingredient inputs', () => {
const model:any = {id:'veo-3.1-fast',type:'video',status:'published',available:true,capabilities:['media.tasks'],input_modes:['prompt','url'],output_modes:['video'],supported_resolutions:['720p','1080p'],duration:{allowed_seconds:[8]},invocation:{kind:'task',path:'/extensions/v1/tasks',method:'POST',request_example:{model:'veo-3.1-fast',prompt:'test'},input_schema:{properties:{prompt:{type:'string'},mode_image:{enum:['frame','ingredient']},file_urls:{type:'array',maxItems:3},aspect_ratio:{enum:['16:9']}}}}};
const preset = buildVip98kModelPresets([model])[0];
expect(preset.capabilities).toMatchObject({supportedModes:['text-to-video','first-frame','first-last-frame','reference-images'],durationOptions:[8],supportedRatios:['16:9'],maxReferenceImages:3});
const invocation:any = (preset.capabilities.vip98k as any).invocation;
expect(getVip98kImageBindingKeys(invocation)).toContain('file_urls');
expect(selectVip98kTaskInvocation({modelId:model.id,configuredInvocation:invocation,imageCount:2})?.path).toBe('/extensions/v1/tasks');
});
import {toVideoPixelRect} from '@/lib/video-canvas/media-processing';
import {isPublicMediaIpv4} from '@/lib/video-canvas/download-media';
test('video crop preserves selected geometry with even output dimensions', () => {
expect(toVideoPixelRect({x:.1,y:.1,width:.8,height:.8},1920,1080)).toEqual({x:192,y:108,width:1536,height:864});
expect(()=>toVideoPixelRect({x:.9,y:0,width:.8,height:1},1920,1080)).toThrow();
expect(()=>toVideoPixelRect({x:NaN,y:0,width:1,height:1},1920,1080)).toThrow();
});
test('video downloads reject private, metadata and loopback networks', () => {
for(const ip of ['127.0.0.1','10.0.0.1','169.254.169.254','172.16.0.1','192.168.1.1','100.64.0.1','0.0.0.0','224.0.0.1']) expect(isPublicMediaIpv4(ip)).toBe(false);
expect(isPublicMediaIpv4('8.8.8.8')).toBe(true);
});
test('audio-driven video requires a real audio reference contract', () => {
expect(filterCanvasModels([video], 'audio-video')).toEqual([]);
const audioVideo={...video,imageGenConfig:{atlasCloud:{propertyNames:['reference_audios']}}};
expect(filterCanvasModels([audioVideo], 'audio-video')).toEqual([audioVideo]);
});