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]); });