Skip to content

Commit ac95b13

Browse files
leochenpmclaude
andcommitted
fix(video): Atlas image-to-video must select the image-to-video model
Follow-up to #13. Atlas Cloud exposes image-to-video as its own model id (bytedance/seedance-2.0/image-to-video), and the text-to-video model's published schema has no `image` field — so the previous default (always text-to-video, image attached anyway) made the default-config image-to-video path ship a video that ignores the first frame, or get rejected, without any error naming the cause. GENERATE's character-consistency flow (locked portrait -> i2v) rides exactly that path. - Default the model by task type: image_url present -> the i2v model id, absent -> text-to-video, explicit model/config always wins. - Fail closed on an explicit mismatch, naming the fix in the error: a text-to-video model given a first frame would silently produce a wrong delivery, and an image-to-video model without a frame cannot run at all. Verified: build green; 218 tests pass — the prior test that pinned the broken combination (image + t2v default) now pins the i2v default, plus new cases for the no-image default and both mismatch refusals. Model ids and the schema difference confirmed against the Atlas model pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL7mRpmEphLAbcH7YmABnV
1 parent e6ccab6 commit ac95b13

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

packages/tools/src/video/video.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const ARK_DEFAULT_BASE = 'https://ark.cn-beijing.volces.com/api/v3';
77
const DEFAULT_MODEL = 'doubao-seedance-2-0-260128';
88
const ATLAS_DEFAULT_BASE = 'https://api.atlascloud.ai/api/v1';
99
const ATLAS_DEFAULT_MODEL = 'bytedance/seedance-2.0/text-to-video';
10+
// Atlas exposes image-to-video as its OWN model id, and the text-to-video
11+
// model's schema has no `image` field — a first frame sent to it is ignored
12+
// or rejected, never used.
13+
const ATLAS_DEFAULT_I2V_MODEL = 'bytedance/seedance-2.0/image-to-video';
1014
const POLL_INTERVAL_MS = 10_000;
1115
const POLL_TIMEOUT_MS = 30_000; // per-poll request timeout — one slow poll must not fail the task
1216
const TASK_TIMEOUT_MS = 60 * 60 * 1000;
@@ -57,11 +61,21 @@ export function buildAtlasCreateRequest(cfg: VideoProviderConfig, p: VideoParams
5761
if (!Number.isFinite(duration) || duration < 4 || duration > 15) {
5862
throw new Error('video: duration must be between 4 and 15 seconds');
5963
}
64+
// Default the model by task type, and fail closed on an explicit mismatch:
65+
// a text-to-video model given a first frame would silently produce a video
66+
// that ignores the image, which is a wrong delivery, not an error.
67+
const model = p.model ?? cfg.model ?? (p.image_url ? ATLAS_DEFAULT_I2V_MODEL : ATLAS_DEFAULT_MODEL);
68+
if (p.image_url && /text-to-video/i.test(model)) {
69+
throw new Error(`video: model "${model}" is text-to-video and has no image input; use an image-to-video model (e.g. ${ATLAS_DEFAULT_I2V_MODEL}) for a first-frame image_url`);
70+
}
71+
if (!p.image_url && /image-to-video/i.test(model)) {
72+
throw new Error(`video: model "${model}" requires a first-frame image_url; pass one, or use a text-to-video model (e.g. ${ATLAS_DEFAULT_MODEL})`);
73+
}
6074
return {
6175
url: `${atlasBase(cfg)}/model/generateVideo`,
6276
headers: { authorization: `Bearer ${cfg.api_key}`, 'content-type': 'application/json' },
6377
body: {
64-
model: p.model ?? cfg.model ?? ATLAS_DEFAULT_MODEL,
78+
model,
6579
prompt: p.prompt,
6680
duration,
6781
resolution: p.resolution ?? '720p',

packages/tools/test/gen.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,17 @@ describe('generateVideo (Doubao Seedance task + poll)', () => {
324324
});
325325

326326
describe('generateVideo (Atlas Cloud task + poll)', () => {
327-
it('builds the Atlas Cloud media request', () => {
327+
it('defaults the model by task type: image-to-video when a first frame is passed', () => {
328328
const req = buildAtlasCreateRequest(
329329
{ provider: 'atlas', api_key: 'atlas-key' },
330330
{ prompt: 'a sunrise', output: 'out.mp4', image_url: 'https://example.com/first.png' },
331331
);
332332
expect(req.url).toBe('https://api.atlascloud.ai/api/v1/model/generateVideo');
333333
expect(req.headers.authorization).toBe('Bearer atlas-key');
334334
expect(req.body).toMatchObject({
335-
model: 'bytedance/seedance-2.0/text-to-video',
335+
// The text-to-video model's schema has no `image` field — a first frame
336+
// must select the image-to-video model, or it is silently ignored.
337+
model: 'bytedance/seedance-2.0/image-to-video',
336338
prompt: 'a sunrise',
337339
image: 'https://example.com/first.png',
338340
duration: 5,
@@ -341,6 +343,26 @@ describe('generateVideo (Atlas Cloud task + poll)', () => {
341343
});
342344
});
343345

346+
it('defaults to text-to-video without a first frame', () => {
347+
const req = buildAtlasCreateRequest(
348+
{ provider: 'atlas', api_key: 'atlas-key' },
349+
{ prompt: 'a sunrise', output: 'out.mp4' },
350+
);
351+
expect(req.body).toMatchObject({ model: 'bytedance/seedance-2.0/text-to-video' });
352+
expect(req.body).not.toHaveProperty('image');
353+
});
354+
355+
it('fails closed on an explicit model/task mismatch instead of shipping the wrong video', () => {
356+
expect(() => buildAtlasCreateRequest(
357+
{ provider: 'atlas', api_key: 'atlas-key', model: 'bytedance/seedance-2.0/text-to-video' },
358+
{ prompt: 'a sunrise', output: 'out.mp4', image_url: 'https://example.com/first.png' },
359+
)).toThrow(/image-to-video/);
360+
expect(() => buildAtlasCreateRequest(
361+
{ provider: 'atlas', api_key: 'atlas-key', model: 'bytedance/seedance-2.0/image-to-video' },
362+
{ prompt: 'a sunrise', output: 'out.mp4' },
363+
)).toThrow(/requires a first-frame image_url/);
364+
});
365+
344366
it('creates, polls, and downloads an Atlas Cloud result', async () => {
345367
const srv = await startServer((req, res) => {
346368
const url = req.url ?? '';

0 commit comments

Comments
 (0)