|
1 | 1 | import { VideoIcon } from '@/components/icons' |
| 2 | +import { |
| 3 | + directVideoDefaults, |
| 4 | + directVideoModels, |
| 5 | + getDirectVideoModel, |
| 6 | +} from '@/lib/media/direct-video-catalog' |
2 | 7 | import { falVideoModels, getFalVideoModel } from '@/lib/media/fal-video-catalog' |
3 | 8 | import { AuthMode, type BlockConfig, IntegrationType, type SubBlockConfig } from '@/blocks/types' |
4 | 9 | import { normalizeFileInput, parseOptionalBooleanInput } from '@/blocks/utils' |
@@ -57,6 +62,19 @@ function tuningField( |
57 | 62 | }, |
58 | 63 | dependsOn: ['model'], |
59 | 64 | options: (params) => { |
| 65 | + if (!params) { |
| 66 | + const values = endpointModels.flatMap((model) => { |
| 67 | + const choices = model[key] |
| 68 | + return Array.isArray(choices) |
| 69 | + ? choices.map((value) => |
| 70 | + key === 'durations' ? String(Number.parseFloat(String(value))) : String(value) |
| 71 | + ) |
| 72 | + : choices |
| 73 | + ? ['true', 'false'] |
| 74 | + : [] |
| 75 | + }) |
| 76 | + return [FAL_MODEL_DEFAULT, ...[...new Set(values)].map((id) => ({ id, label: id }))] |
| 77 | + } |
60 | 78 | const model = getFalVideoModel(params?.values.model) |
61 | 79 | if (!model?.[key]) return [] |
62 | 80 | if (key === 'audioField' || key === 'promptOptimizer') { |
@@ -127,6 +145,171 @@ function withFalVideoCatalog(subBlocks: SubBlockConfig[]): SubBlockConfig[] { |
127 | 145 | }) |
128 | 146 | } |
129 | 147 |
|
| 148 | +function selectedDirectModel(values?: Record<string, unknown>) { |
| 149 | + const withImage = [ |
| 150 | + values?.visualReference, |
| 151 | + values?.visualReferenceInput, |
| 152 | + values?.visualReferenceUpload, |
| 153 | + ].some((reference) => (Array.isArray(reference) ? reference.length > 0 : Boolean(reference))) |
| 154 | + return getDirectVideoModel(values?.provider, values?.model, withImage) |
| 155 | +} |
| 156 | + |
| 157 | +const directVideoFields = ( |
| 158 | + [ |
| 159 | + ['directDuration', 'Duration (seconds)', 'durations'], |
| 160 | + ['directAspectRatio', 'Aspect Ratio', 'aspectRatios'], |
| 161 | + ['directResolution', 'Resolution', 'resolutions'], |
| 162 | + ['directGenerateAudio', 'Generate Audio', 'audio'], |
| 163 | + ['directPromptOptimizer', 'Prompt Optimizer', 'promptOptimizer'], |
| 164 | + ] as const |
| 165 | +).map( |
| 166 | + ([id, title, key]): SubBlockConfig => ({ |
| 167 | + id, |
| 168 | + title, |
| 169 | + type: 'dropdown', |
| 170 | + value: () => FAL_MODEL_DEFAULT.id, |
| 171 | + dependsOn: ['provider', 'model', 'visualReferenceInput', 'visualReferenceUpload'], |
| 172 | + condition: (values) => ({ |
| 173 | + field: 'model', |
| 174 | + value: selectedDirectModel(values)?.settings?.[key] ? [String(values?.model)] : [], |
| 175 | + }), |
| 176 | + options: (params) => { |
| 177 | + if (!params) { |
| 178 | + const values = directVideoModels.flatMap((model) => |
| 179 | + [model.text, model.image].flatMap((settings) => { |
| 180 | + const choices = settings?.[key] |
| 181 | + return Array.isArray(choices) ? choices.map(String) : choices ? ['true', 'false'] : [] |
| 182 | + }) |
| 183 | + ) |
| 184 | + return [FAL_MODEL_DEFAULT, ...[...new Set(values)].map((id) => ({ id, label: id }))] |
| 185 | + } |
| 186 | + const model = selectedDirectModel(params?.values) |
| 187 | + const choices = model?.settings?.[key] |
| 188 | + if (!choices) return [] |
| 189 | + if (typeof choices === 'boolean') |
| 190 | + return [FAL_MODEL_DEFAULT, { id: 'true', label: 'On' }, { id: 'false', label: 'Off' }] |
| 191 | + return [ |
| 192 | + FAL_MODEL_DEFAULT, |
| 193 | + ...choices |
| 194 | + .filter((value) => { |
| 195 | + 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) |
| 199 | + return value !== '1080P' |
| 200 | + return true |
| 201 | + }) |
| 202 | + .map((value) => ({ id: String(value), label: String(value) })), |
| 203 | + ] |
| 204 | + }, |
| 205 | + }) |
| 206 | +) |
| 207 | + |
| 208 | +function withDirectVideoCatalog(subBlocks: SubBlockConfig[]): SubBlockConfig[] { |
| 209 | + return subBlocks.flatMap((field): SubBlockConfig[] => { |
| 210 | + if ( |
| 211 | + field.id === 'model' && |
| 212 | + typeof field.condition === 'object' && |
| 213 | + field.condition.value === 'falai' |
| 214 | + ) { |
| 215 | + const options = field.options |
| 216 | + return [ |
| 217 | + { |
| 218 | + ...field, |
| 219 | + options: (params) => { |
| 220 | + const falOptions = typeof options === 'function' ? options(params) : (options ?? []) |
| 221 | + return params |
| 222 | + ? falOptions |
| 223 | + : [ |
| 224 | + ...falOptions, |
| 225 | + ...directVideoModels.flatMap((model) => [ |
| 226 | + { id: model.id, label: model.label }, |
| 227 | + ...(model.aliases ?? []).map((id) => ({ |
| 228 | + id, |
| 229 | + label: model.label, |
| 230 | + hidden: true, |
| 231 | + })), |
| 232 | + ]), |
| 233 | + ] |
| 234 | + }, |
| 235 | + }, |
| 236 | + ] |
| 237 | + } |
| 238 | + if (field.id === 'provider') |
| 239 | + return [ |
| 240 | + { |
| 241 | + ...field, |
| 242 | + options: [ |
| 243 | + { id: 'runway', label: 'Runway' }, |
| 244 | + { id: 'veo', label: 'Google Gemini' }, |
| 245 | + { id: 'luma', label: 'Luma' }, |
| 246 | + { id: 'minimax', label: 'MiniMax' }, |
| 247 | + { id: 'falai', label: 'Fal.ai (Multi-Model)' }, |
| 248 | + ], |
| 249 | + }, |
| 250 | + ...(['runway', 'veo', 'luma', 'minimax'] as const).map( |
| 251 | + (provider): SubBlockConfig => ({ |
| 252 | + id: 'model', |
| 253 | + title: 'Model', |
| 254 | + type: 'dropdown', |
| 255 | + searchable: true, |
| 256 | + condition: { field: 'provider', value: provider }, |
| 257 | + options: directVideoModels |
| 258 | + .filter((model) => model.provider === provider) |
| 259 | + .flatMap((model) => [ |
| 260 | + { id: model.id, label: model.label }, |
| 261 | + ...(model.aliases ?? []).map((id) => ({ id, label: model.label, hidden: true })), |
| 262 | + ]), |
| 263 | + value: () => directVideoDefaults[provider], |
| 264 | + dependsOn: ['provider'], |
| 265 | + required: true, |
| 266 | + }) |
| 267 | + ), |
| 268 | + ] |
| 269 | + if (field.id === 'prompt') return [field, ...directVideoFields] |
| 270 | + const condition = field.condition |
| 271 | + const isDirect = |
| 272 | + typeof condition === 'object' && |
| 273 | + condition.field === 'provider' && |
| 274 | + typeof condition.value === 'string' && |
| 275 | + condition.value !== 'falai' |
| 276 | + if ( |
| 277 | + isDirect && |
| 278 | + ['model', 'duration', 'aspectRatio', 'resolution', 'promptOptimizer'].includes(field.id) |
| 279 | + ) |
| 280 | + return [] |
| 281 | + if (field.id === 'endpoint') |
| 282 | + return [ |
| 283 | + { |
| 284 | + ...field, |
| 285 | + condition: { |
| 286 | + field: 'provider', |
| 287 | + value: 'minimax', |
| 288 | + and: { field: 'model', value: ['hailuo-2.3', 'hailuo-02'] }, |
| 289 | + }, |
| 290 | + dependsOn: ['provider', 'model'], |
| 291 | + }, |
| 292 | + ] |
| 293 | + if (field.id === 'cameraControl') |
| 294 | + return [ |
| 295 | + { |
| 296 | + ...field, |
| 297 | + condition: { |
| 298 | + field: 'provider', |
| 299 | + value: 'luma', |
| 300 | + and: { field: 'model', value: ['ray-2', 'ray-flash-2'] }, |
| 301 | + }, |
| 302 | + dependsOn: ['provider', 'model'], |
| 303 | + }, |
| 304 | + ] |
| 305 | + if (field.id === 'visualReferenceUpload' || field.id === 'visualReferenceInput') |
| 306 | + return [ |
| 307 | + { ...field, required: { field: 'model', value: ['gen4_turbo', 'gen-4', 'gen-4-turbo'] } }, |
| 308 | + ] |
| 309 | + return [field] |
| 310 | + }) |
| 311 | +} |
| 312 | + |
130 | 313 | export const VideoGeneratorBlock: BlockConfig = { |
131 | 314 | type: 'video_generator', |
132 | 315 | name: 'Video Generator (Legacy)', |
@@ -1733,16 +1916,71 @@ export const VideoGeneratorV3Block: BlockConfig = { |
1733 | 1916 | name: 'Video Generator', |
1734 | 1917 | description: 'Generate videos from text using AI', |
1735 | 1918 | longDescription: |
1736 | | - 'Generate high-quality videos from text prompts using leading AI providers. Supports Runway, Google Veo, Luma, MiniMax, and Fal.ai multi-model generation with provider-specific durations, aspect ratios, resolutions, prompt optimization, and native audio controls.', |
| 1919 | + 'Generate high-quality videos from text prompts using leading AI providers. Supports Runway, Google Gemini, Luma, MiniMax, and Fal.ai multi-model generation with provider-specific durations, aspect ratios, resolutions, prompt optimization, and native audio controls.', |
1737 | 1920 | docsLink: 'https://docs.sim.ai/integrations/video_generator', |
1738 | 1921 | category: 'blocks', |
1739 | 1922 | integrationType: IntegrationType.AI, |
1740 | 1923 | bgColor: '#181C1E', |
1741 | 1924 | icon: VideoIcon, |
1742 | 1925 | hideFromToolbar: false, |
1743 | | - subBlocks: withFalVideoCatalog(VideoGeneratorV2Block.subBlocks), |
| 1926 | + subBlocks: withDirectVideoCatalog(withFalVideoCatalog(VideoGeneratorV2Block.subBlocks)), |
| 1927 | + tools: { |
| 1928 | + ...VideoGeneratorV2Block.tools, |
| 1929 | + config: { |
| 1930 | + tool: (params) => VideoGeneratorV2Block.tools.config?.tool(params) ?? 'video_runway', |
| 1931 | + params: (params) => { |
| 1932 | + if (params.provider === 'falai') |
| 1933 | + return VideoGeneratorV2Block.tools.config?.params?.(params) ?? params |
| 1934 | + const visualReference = |
| 1935 | + params.provider === 'runway' |
| 1936 | + ? normalizeFileInput(params.visualReference, { single: true }) |
| 1937 | + : undefined |
| 1938 | + const model = getDirectVideoModel(params.provider, params.model, Boolean(visualReference)) |
| 1939 | + const setting = (legacy: string, current: string, supported: unknown) => { |
| 1940 | + if (!supported) return undefined |
| 1941 | + const value = params[current] ?? params[legacy] |
| 1942 | + return value === FAL_MODEL_DEFAULT.id ? undefined : value |
| 1943 | + } |
| 1944 | + const duration = setting('duration', 'directDuration', model?.settings?.durations) |
| 1945 | + return { |
| 1946 | + provider: params.provider, |
| 1947 | + model: params.model, |
| 1948 | + apiKey: params.apiKey, |
| 1949 | + prompt: params.prompt, |
| 1950 | + visualReference, |
| 1951 | + duration: duration ? Number(duration) : undefined, |
| 1952 | + aspectRatio: setting('aspectRatio', 'directAspectRatio', model?.settings?.aspectRatios), |
| 1953 | + resolution: setting('resolution', 'directResolution', model?.settings?.resolutions), |
| 1954 | + generateAudio: parseOptionalBooleanInput( |
| 1955 | + setting('generateAudio', 'directGenerateAudio', model?.settings?.audio) |
| 1956 | + ), |
| 1957 | + promptOptimizer: parseOptionalBooleanInput( |
| 1958 | + setting('promptOptimizer', 'directPromptOptimizer', model?.settings?.promptOptimizer) |
| 1959 | + ), |
| 1960 | + endpoint: model?.protocol === 'minimax-v1' ? params.endpoint : undefined, |
| 1961 | + cameraControl: |
| 1962 | + model?.protocol === 'luma-legacy' && params.cameraControl |
| 1963 | + ? typeof params.cameraControl === 'string' |
| 1964 | + ? JSON.parse(params.cameraControl) |
| 1965 | + : params.cameraControl |
| 1966 | + : undefined, |
| 1967 | + } |
| 1968 | + }, |
| 1969 | + }, |
| 1970 | + }, |
1744 | 1971 | inputs: { |
1745 | 1972 | ...VideoGeneratorV2Block.inputs, |
| 1973 | + directDuration: { type: 'number', description: 'Duration for the selected provider model' }, |
| 1974 | + directAspectRatio: { |
| 1975 | + type: 'string', |
| 1976 | + description: 'Frame size or aspect ratio for the selected provider model', |
| 1977 | + }, |
| 1978 | + directResolution: { type: 'string', description: 'Resolution for the selected provider model' }, |
| 1979 | + directGenerateAudio: { type: 'boolean', description: 'Generate audio when supported' }, |
| 1980 | + directPromptOptimizer: { |
| 1981 | + type: 'boolean', |
| 1982 | + description: 'Enable prompt optimization when supported', |
| 1983 | + }, |
1746 | 1984 | falDuration: { |
1747 | 1985 | type: 'number', |
1748 | 1986 | description: 'Duration in seconds for the selected fal endpoint', |
|
0 commit comments