Skip to content

Commit eb73cc8

Browse files
committed
fix(video-generator): preserve saved settings and fix CI
1 parent 9f13a2e commit eb73cc8

13 files changed

Lines changed: 195 additions & 34 deletions

‎apps/sim/blocks/blocks/video_generator.ts‎

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,22 @@ function selectedDirectModel(values?: Record<string, unknown>) {
156156

157157
const directVideoFields = (
158158
[
159-
['directDuration', 'Duration (seconds)', 'durations'],
160-
['directAspectRatio', 'Aspect Ratio', 'aspectRatios'],
161-
['directResolution', 'Resolution', 'resolutions'],
159+
['duration', 'Duration (seconds)', 'durations'],
160+
['aspectRatio', 'Aspect Ratio', 'aspectRatios'],
161+
['resolution', 'Resolution', 'resolutions'],
162162
['directGenerateAudio', 'Generate Audio', 'audio'],
163-
['directPromptOptimizer', 'Prompt Optimizer', 'promptOptimizer'],
163+
['promptOptimizer', 'Prompt Optimizer', 'promptOptimizer'],
164164
] as const
165165
).map(
166166
([id, title, key]): SubBlockConfig => ({
167167
id,
168168
title,
169-
type: 'dropdown',
170-
value: () => FAL_MODEL_DEFAULT.id,
169+
type: key === 'promptOptimizer' ? 'switch' : 'dropdown',
170+
value: key === 'promptOptimizer' ? undefined : () => FAL_MODEL_DEFAULT.id,
171171
dependsOn: ['provider', 'model', 'visualReferenceInput', 'visualReferenceUpload'],
172172
condition: (values) => ({
173-
field: 'model',
174-
value: selectedDirectModel(values)?.settings?.[key] ? [String(values?.model)] : [],
173+
field: 'provider',
174+
value: selectedDirectModel(values)?.settings?.[key] ? [String(values?.provider)] : [],
175175
}),
176176
options: (params) => {
177177
if (!params) {
@@ -193,9 +193,8 @@ const directVideoFields = (
193193
...choices
194194
.filter((value) => {
195195
if (model?.protocol !== 'minimax-v1') return true
196-
if (key === 'durations' && params?.values.directResolution === '1080P')
197-
return value === 6
198-
if (key === 'resolutions' && Number(params?.values.directDuration) === 10)
196+
if (key === 'durations' && params?.values.resolution === '1080P') return value === 6
197+
if (key === 'resolutions' && Number(params?.values.duration) === 10)
199198
return value !== '1080P'
200199
return true
201200
})
@@ -1936,26 +1935,26 @@ export const VideoGeneratorV3Block: BlockConfig = {
19361935
? normalizeFileInput(params.visualReference, { single: true })
19371936
: undefined
19381937
const model = getDirectVideoModel(params.provider, params.model, Boolean(visualReference))
1939-
const setting = (legacy: string, current: string, supported: unknown) => {
1938+
const setting = (key: string, supported: unknown) => {
19401939
if (!supported) return undefined
1941-
const value = params[current] ?? params[legacy]
1940+
const value = params[key]
19421941
return value === FAL_MODEL_DEFAULT.id ? undefined : value
19431942
}
1944-
const duration = setting('duration', 'directDuration', model?.settings?.durations)
1943+
const duration = setting('duration', model?.settings?.durations)
19451944
return {
19461945
provider: params.provider,
19471946
model: params.model,
19481947
apiKey: params.apiKey,
19491948
prompt: params.prompt,
19501949
visualReference,
19511950
duration: duration ? Number(duration) : undefined,
1952-
aspectRatio: setting('aspectRatio', 'directAspectRatio', model?.settings?.aspectRatios),
1953-
resolution: setting('resolution', 'directResolution', model?.settings?.resolutions),
1951+
aspectRatio: setting('aspectRatio', model?.settings?.aspectRatios),
1952+
resolution: setting('resolution', model?.settings?.resolutions),
19541953
generateAudio: parseOptionalBooleanInput(
1955-
setting('generateAudio', 'directGenerateAudio', model?.settings?.audio)
1954+
setting('directGenerateAudio', model?.settings?.audio)
19561955
),
19571956
promptOptimizer: parseOptionalBooleanInput(
1958-
setting('promptOptimizer', 'directPromptOptimizer', model?.settings?.promptOptimizer)
1957+
setting('promptOptimizer', model?.settings?.promptOptimizer)
19591958
),
19601959
endpoint: model?.protocol === 'minimax-v1' ? params.endpoint : undefined,
19611960
cameraControl:
@@ -1970,14 +1969,14 @@ export const VideoGeneratorV3Block: BlockConfig = {
19701969
},
19711970
inputs: {
19721971
...VideoGeneratorV2Block.inputs,
1973-
directDuration: { type: 'number', description: 'Duration for the selected provider model' },
1974-
directAspectRatio: {
1972+
duration: { type: 'number', description: 'Duration for the selected provider model' },
1973+
aspectRatio: {
19751974
type: 'string',
19761975
description: 'Frame size or aspect ratio for the selected provider model',
19771976
},
1978-
directResolution: { type: 'string', description: 'Resolution for the selected provider model' },
1977+
resolution: { type: 'string', description: 'Resolution for the selected provider model' },
19791978
directGenerateAudio: { type: 'boolean', description: 'Generate audio when supported' },
1980-
directPromptOptimizer: {
1979+
promptOptimizer: {
19811980
type: 'boolean',
19821981
description: 'Enable prompt optimization when supported',
19831982
},

‎apps/sim/lib/internal/video/client.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,48 @@ describe('Video provider client', () => {
192192
expect(submissions).toBe(1)
193193
})
194194

195+
it('reports a failed MiniMax job immediately instead of polling until timeout', async () => {
196+
const fetchMock = vi
197+
.fn()
198+
.mockResolvedValueOnce(jsonResponse({ task_id: 'job' }))
199+
.mockResolvedValue(jsonResponse({ status: 'Failed', failure_reason: 'Generation rejected' }))
200+
vi.stubGlobal('fetch', fetchMock)
201+
const result = generateVideo(
202+
{ provider: 'minimax', apiKey: 'key', prompt: 'A cinematic sunrise' },
203+
{ requestId: 'failed-minimax' }
204+
)
205+
const rejected = expect(result).rejects.toThrow('Generation rejected')
206+
await Promise.all([rejected, vi.advanceTimersByTimeAsync(10_000)])
207+
expect(fetchMock).toHaveBeenCalledTimes(2)
208+
})
209+
210+
it('keeps a saved MiniMax Pro preference compatible with a 720P-only model', async () => {
211+
const fetchMock = vi
212+
.fn<typeof fetch>()
213+
.mockResolvedValueOnce(jsonResponse({ task_id: 'job' }))
214+
.mockResolvedValueOnce(jsonResponse({ status: 'Success', file_id: 'file' }))
215+
.mockResolvedValueOnce(
216+
jsonResponse({ file: { download_url: 'https://cdn.example/video.mp4' } })
217+
)
218+
vi.stubGlobal('fetch', fetchMock)
219+
const result = generateVideo(
220+
{
221+
provider: 'minimax',
222+
model: 'T2V-01',
223+
endpoint: 'pro',
224+
apiKey: 'key',
225+
prompt: 'A cinematic sunrise',
226+
},
227+
{ requestId: 'legacy-pro' }
228+
)
229+
const completion = expect(result).resolves.toMatchObject({ buffer: Buffer.from('video') })
230+
await Promise.all([completion, vi.advanceTimersByTimeAsync(5000)])
231+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({
232+
model: 'T2V-01',
233+
resolution: '720P',
234+
})
235+
})
236+
195237
it('rejects malformed camera controls before submitting a paid Luma job', async () => {
196238
const fetchMock = vi.fn(() => {
197239
throw new Error('Unexpected paid request')

‎apps/sim/lib/internal/video/client.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ async function generateWithDirectProvider(
289289
'canceled',
290290
'incomplete',
291291
'Fail',
292+
'Failed',
292293
].includes(status || '')
293294
) {
294295
throw new Error(

‎apps/sim/lib/internal/video/direct-request.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function buildDirectVideoRequest(input: VideoGenerationInput) {
3939
model.protocol === 'minimax-v1' &&
4040
!resolution &&
4141
input.endpoint === 'pro' &&
42+
spec.resolutions?.includes('1080P') &&
4243
(input.duration ?? 6) === 6
4344
)
4445
resolution = '1080P'

‎apps/sim/lib/media/runway-video-models.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,8 @@
691691
"durations": [2, 3, 4, 5, 6, 7, 8, 9, 10],
692692
"maxPromptLength": 1000,
693693
"defaults": {
694-
"ratio": "1280:720"
694+
"ratio": "1280:720",
695+
"duration": 5
695696
}
696697
}
697698
}

‎apps/sim/lib/workflows/application/replace-workflow-state.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ describe('replaceWorkflowState', () => {
159159
})
160160

161161
it.each([
162-
{ provider: 'runway', model: 'gen4.5', directDuration: '5', directAspectRatio: '1280:720' },
163-
{ provider: 'minimax', model: 'MiniMax-H3', directResolution: '2K', directDuration: '7' },
162+
{ provider: 'runway', model: 'gen4.5', duration: '5', aspectRatio: '1280:720' },
163+
{ provider: 'minimax', model: 'MiniMax-H3', resolution: '2K', duration: '7' },
164164
{ provider: 'falai', model: 'fal-ai/kandinsky5/text-to-video', falDuration: '5' },
165165
])('saves configured $provider video workflows through the state API', async (settings) => {
166166
const block = {

‎apps/sim/lib/workflows/application/replace-workflow-state.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {
2929
} from '@/lib/workflows/persistence/replace-normalized-state'
3030
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
3131
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
32+
import {
33+
buildSubBlockValues,
34+
evaluateSubBlockCondition,
35+
} from '@/lib/workflows/subblocks/visibility'
3236
import {
3337
getToolBindingAuthoringSchema,
3438
validateToolBindingAuthoring,
@@ -140,6 +144,10 @@ export const replaceWorkflowState = defineAuthorizedWorkflowUseCase({
140144
const config = getBlock(block.type)
141145
if (!config) continue
142146
const fields = new Map(config.subBlocks.map((field) => [field.id, field]))
147+
const values = buildSubBlockValues(block.subBlocks)
148+
for (const field of config.subBlocks) {
149+
if (evaluateSubBlockCondition(field.condition, values)) fields.set(field.id, field)
150+
}
143151
for (const [fieldId, stored] of Object.entries(block.subBlocks ?? {})) {
144152
const field = fields.get(fieldId)
145153
if (!field) continue

‎apps/sim/scripts/test-organization-search-e2e.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,8 +1024,31 @@ try {
10241024
await check('Search-only members recover legacy drafts without a remembered mode', () =>
10251025
verifyLegacyDraft(undefined, 'assistant')
10261026
)
1027-
await check('The UI completes without browser runtime errors', async () => {
1028-
assert.deepEqual(browserErrors, [])
1027+
await check('The UI completes without unexpected browser runtime errors', async () => {
1028+
await expect.poll(() => browserErrorEventCount).toBe(browserErrorCount)
1029+
const deniedRecoveries = browserErrorEvents.filter(
1030+
(event) =>
1031+
event.digest === 'NEXT_HTTP_ERROR_FALLBACK;404' &&
1032+
event.url === `${baseUrl.origin}${root}/search/chat/${deniedChatId}` &&
1033+
[
1034+
'Assistant history rejects a chat from another organization',
1035+
'A streamed denied chat remains navigable after hydration',
1036+
].includes(event.stage)
1037+
)
1038+
const recoveryCount = deniedRecoveries.reduce((total, event) => total + event.count, 0)
1039+
assert.equal(browserErrorEventCount, recoveryCount, 'Unexpected browser error events')
1040+
assert.equal(browserErrorCount, recoveryCount, 'Unexpected browser runtime errors')
1041+
for (const sample of browserErrorSamples) {
1042+
const matchedCount = deniedRecoveries
1043+
.filter(
1044+
(event) =>
1045+
event.stage === sample.stage &&
1046+
event.url === sample.url &&
1047+
event.message === sample.message
1048+
)
1049+
.reduce((total, event) => total + event.count, 0)
1050+
assert.equal(sample.count, matchedCount, `Unexpected browser error: ${sample.message}`)
1051+
}
10291052
})
10301053
} catch (error) {
10311054
logger.error('Organization Search E2E failed', { error: getErrorMessage(error) })

‎apps/sim/scripts/validate-runway-video-models.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ for (const mode of ['text', 'image'] as const) {
9595
assert(validate(request.body), JSON.stringify(validate.errors))
9696
assert.equal(request.body.model, model.id)
9797
const wireKeys: Record<string, string> = {
98-
directDuration: 'duration',
99-
directAspectRatio: 'ratio',
100-
directResolution: 'resolution',
98+
duration: 'duration',
99+
aspectRatio: 'ratio',
100+
resolution: 'resolution',
101101
directGenerateAudio: 'audio',
102102
}
103103
for (const [field, value] of Object.entries(settings)) {
@@ -108,7 +108,10 @@ for (const mode of ['text', 'image'] as const) {
108108
}
109109
check({})
110110
for (const field of VideoGeneratorV3Block.subBlocks) {
111-
if (!field.id.startsWith('direct') || !evaluateSubBlockCondition(field.condition, base))
111+
if (
112+
!['duration', 'aspectRatio', 'resolution', 'directGenerateAudio'].includes(field.id) ||
113+
!evaluateSubBlockCondition(field.condition, base)
114+
)
112115
continue
113116
const options =
114117
typeof field.options === 'function' ? field.options({ values: base }) : field.options

‎apps/sim/serializer/field-analysis.test.ts‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ vi.mock('@/blocks', () => ({
1414
getBlock: (type: string) => (type === 'svc' ? svcConfig.value : blocksMock.getBlock(type)),
1515
}))
1616

17+
import { videoToolBodySchema } from '@/lib/api/contracts/tools/media/video'
18+
import { buildDirectVideoRequest } from '@/lib/internal/video/direct-request'
19+
import { VideoGeneratorV3Block } from '@/blocks/blocks/video_generator'
1720
import { collectBlockFieldIssues, extractBlockParams } from '@/serializer/index'
1821
import { getToolMetadata, getToolParams } from '@/tools/metadata'
1922

@@ -135,6 +138,76 @@ describe('collectBlockFieldIssues', () => {
135138
})
136139

137140
describe('extractBlockParams', () => {
141+
it.each([
142+
{
143+
name: 'saved Luma duration, portrait ratio and resolution',
144+
settings: {
145+
provider: 'luma' as const,
146+
model: 'ray-2',
147+
duration: '9',
148+
aspectRatio: '9:16',
149+
resolution: '4k',
150+
},
151+
payload: { duration: '9s', aspect_ratio: '9:16', resolution: '4k' },
152+
},
153+
{
154+
name: 'saved MiniMax duration and disabled prompt optimization',
155+
settings: {
156+
provider: 'minimax' as const,
157+
duration: '10',
158+
promptOptimizer: false,
159+
},
160+
payload: { duration: 10, prompt_optimizer: false },
161+
},
162+
{
163+
name: 'MiniMax Pro preference on a 720P-only model',
164+
settings: { provider: 'minimax' as const, model: 'T2V-01', endpoint: 'pro' },
165+
payload: { model: 'T2V-01', resolution: '720P' },
166+
},
167+
{
168+
name: 'Runway default duration with a reference image',
169+
settings: {
170+
provider: 'runway' as const,
171+
visualReferenceUpload: [
172+
{
173+
id: 'reference',
174+
name: 'image.png',
175+
url: '/api/files/image.png',
176+
key: 'workspace/reference.png',
177+
size: 5,
178+
type: 'image/png',
179+
},
180+
],
181+
},
182+
payload: { model: 'gen4_turbo', duration: 5 },
183+
},
184+
{
185+
name: 'current Google default without a retired duration',
186+
settings: { provider: 'veo' as const },
187+
payload: {
188+
model: 'gemini-omni-1.1-flash',
189+
response_format: { type: 'video', delivery: 'uri' },
190+
},
191+
},
192+
])('preserves $name through serialization into the provider request', ({ settings, payload }) => {
193+
svcConfig.value = VideoGeneratorV3Block
194+
const values = { apiKey: 'test-key', prompt: 'A cinematic sunrise', ...settings }
195+
const params = extractBlockParams(
196+
block({
197+
type: 'svc',
198+
subBlocks: Object.fromEntries(
199+
Object.entries(values).map(([id, value]) => [id, { id, type: 'short-input', value }])
200+
),
201+
})
202+
)
203+
const mapped = VideoGeneratorV3Block.tools.config?.params?.(params)
204+
const request = buildDirectVideoRequest({
205+
...videoToolBodySchema.parse(mapped),
206+
provider: settings.provider,
207+
})
208+
expect(request.body).toMatchObject(payload)
209+
})
210+
138211
it('resolves a canonical pair to its canonical id (advanced value wins when basic empty)', () => {
139212
svcConfig.value = config([
140213
{

0 commit comments

Comments
 (0)