mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
feat(video-studio): first/last frame support for i2v models
Adds an optional end-frame upload picker for i2v models whose server
schema accepts a second image (kling v2.1/v3.0/o1, veo3.1, seedance
lite/v1.5-pro, wan2.2, minimax-hailuo-02). Catalog entries declare the
server-side param via `lastImageField` ("last_image" or "end_image_url")
so the picker is gated per-model and unsupported models stay unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4fc6606d14
commit
8cbaf7fc3f
3 changed files with 73 additions and 0 deletions
|
|
@ -45,6 +45,7 @@ export function VideoStudio() {
|
||||||
let lastGenerationModel = null;
|
let lastGenerationModel = null;
|
||||||
let dropdownOpen = null;
|
let dropdownOpen = null;
|
||||||
let uploadedImageUrl = null;
|
let uploadedImageUrl = null;
|
||||||
|
let uploadedEndImageUrl = null; // optional end-frame for FLF i2v models
|
||||||
let imageMode = false; // false = t2v models, true = i2v models
|
let imageMode = false; // false = t2v models, true = i2v models
|
||||||
let v2vMode = false; // true = video-to-video tools mode
|
let v2vMode = false; // true = video-to-video tools mode
|
||||||
let uploadedVideoUrl = null;
|
let uploadedVideoUrl = null;
|
||||||
|
|
@ -140,6 +141,9 @@ export function VideoStudio() {
|
||||||
onClear: () => {
|
onClear: () => {
|
||||||
uploadedImageUrl = null;
|
uploadedImageUrl = null;
|
||||||
imageMode = false;
|
imageMode = false;
|
||||||
|
// Clearing the start frame invalidates any selected end frame.
|
||||||
|
uploadedEndImageUrl = null;
|
||||||
|
endPicker?.reset();
|
||||||
selectedModel = allT2V[0].id;
|
selectedModel = allT2V[0].id;
|
||||||
selectedModelName = allT2V[0].name;
|
selectedModelName = allT2V[0].name;
|
||||||
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||||
|
|
@ -155,6 +159,46 @@ export function VideoStudio() {
|
||||||
topRow.appendChild(picker.trigger);
|
topRow.appendChild(picker.trigger);
|
||||||
container.appendChild(picker.panel);
|
container.appendChild(picker.panel);
|
||||||
|
|
||||||
|
// --- End-Frame Upload Picker (FLF i2v models — kling/veo/seedance/etc.) ---
|
||||||
|
// Shown only when imageMode is on AND the selected i2v model declares a
|
||||||
|
// `lastImageField` in its catalog entry. Reuses the same UploadPicker UI;
|
||||||
|
// a corner badge differentiates it from the start-frame picker.
|
||||||
|
const endPicker = createUploadPicker({
|
||||||
|
anchorContainer: container,
|
||||||
|
onSelect: ({ url }) => { uploadedEndImageUrl = url; },
|
||||||
|
onClear: () => { uploadedEndImageUrl = null; },
|
||||||
|
uploadFn: (file) => muapi.uploadFile(file),
|
||||||
|
requireApiKey: () => true,
|
||||||
|
});
|
||||||
|
endPicker.trigger.title = 'End frame (optional)';
|
||||||
|
// Visual marker: small "L" badge in the corner so users can tell the two
|
||||||
|
// pickers apart at a glance. The wrapper keeps it from interfering with
|
||||||
|
// UploadPicker's own thumbnail/spinner state swapping.
|
||||||
|
const endBadge = document.createElement('div');
|
||||||
|
endBadge.className = 'absolute top-0.5 left-0.5 px-1 h-4 bg-white/20 rounded-md flex items-center justify-center pointer-events-none';
|
||||||
|
endBadge.innerHTML = '<span class="text-[8px] font-black text-white leading-none">END</span>';
|
||||||
|
endPicker.trigger.appendChild(endBadge);
|
||||||
|
endPicker.trigger.classList.add('hidden'); // start hidden until updateEndFrameVisibility flips it on
|
||||||
|
topRow.appendChild(endPicker.trigger);
|
||||||
|
container.appendChild(endPicker.panel);
|
||||||
|
|
||||||
|
const updateEndFrameVisibility = () => {
|
||||||
|
const model = getCurrentModel();
|
||||||
|
const supports = imageMode && !!model?.lastImageField;
|
||||||
|
if (supports) {
|
||||||
|
endPicker.trigger.classList.remove('hidden');
|
||||||
|
endPicker.trigger.classList.add('flex');
|
||||||
|
} else {
|
||||||
|
endPicker.trigger.classList.add('hidden');
|
||||||
|
endPicker.trigger.classList.remove('flex');
|
||||||
|
// Drop any stale end-frame selection when leaving FLF-capable state
|
||||||
|
if (uploadedEndImageUrl) {
|
||||||
|
uploadedEndImageUrl = null;
|
||||||
|
endPicker.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Video Upload Picker (Video-to-Video) ---
|
// --- Video Upload Picker (Video-to-Video) ---
|
||||||
const videoFileInput = document.createElement('input');
|
const videoFileInput = document.createElement('input');
|
||||||
videoFileInput.type = 'file';
|
videoFileInput.type = 'file';
|
||||||
|
|
@ -381,6 +425,9 @@ export function VideoStudio() {
|
||||||
const updateControlsForModel = (modelId) => {
|
const updateControlsForModel = (modelId) => {
|
||||||
const model = getCurrentModels().find(m => m.id === modelId);
|
const model = getCurrentModels().find(m => m.id === modelId);
|
||||||
|
|
||||||
|
// End-frame picker visibility depends on imageMode + model.lastImageField.
|
||||||
|
updateEndFrameVisibility();
|
||||||
|
|
||||||
// In v2v mode, hide all parameter controls — no prompt/AR/duration/etc needed
|
// In v2v mode, hide all parameter controls — no prompt/AR/duration/etc needed
|
||||||
if (v2vMode) {
|
if (v2vMode) {
|
||||||
arBtn.style.display = 'none';
|
arBtn.style.display = 'none';
|
||||||
|
|
@ -1105,6 +1152,9 @@ export function VideoStudio() {
|
||||||
};
|
};
|
||||||
i2vParams.prompt = prompt || '';
|
i2vParams.prompt = prompt || '';
|
||||||
i2vParams.aspect_ratio = selectedAr;
|
i2vParams.aspect_ratio = selectedAr;
|
||||||
|
if (uploadedEndImageUrl && getCurrentModel()?.lastImageField) {
|
||||||
|
i2vParams.last_image = uploadedEndImageUrl;
|
||||||
|
}
|
||||||
const durations = getCurrentDurations(selectedModel);
|
const durations = getCurrentDurations(selectedModel);
|
||||||
if (durations.length > 0) i2vParams.duration = selectedDuration;
|
if (durations.length > 0) i2vParams.duration = selectedDuration;
|
||||||
const resolutions = getCurrentResolutions(selectedModel);
|
const resolutions = getCurrentResolutions(selectedModel);
|
||||||
|
|
|
||||||
|
|
@ -5583,6 +5583,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-v2.1-master-i2v",
|
"endpoint": "kling-v2.1-master-i2v",
|
||||||
"family": "kling-v2.1",
|
"family": "kling-v2.1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -5624,6 +5625,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-v2.1-standard-i2v",
|
"endpoint": "kling-v2.1-standard-i2v",
|
||||||
"family": "kling-v2.1",
|
"family": "kling-v2.1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -5665,6 +5667,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-v2.1-pro-i2v",
|
"endpoint": "kling-v2.1-pro-i2v",
|
||||||
"family": "kling-v2.1",
|
"family": "kling-v2.1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -5706,6 +5709,7 @@ export const i2vModels = [
|
||||||
"endpoint": "wan2.2-image-to-video",
|
"endpoint": "wan2.2-image-to-video",
|
||||||
"family": "wan2.2",
|
"family": "wan2.2",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -5932,6 +5936,7 @@ export const i2vModels = [
|
||||||
"endpoint": "minimax-hailuo-02-standard-i2v",
|
"endpoint": "minimax-hailuo-02-standard-i2v",
|
||||||
"family": "minimax-2",
|
"family": "minimax-2",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "end_image_url",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -5973,6 +5978,7 @@ export const i2vModels = [
|
||||||
"endpoint": "minimax-hailuo-02-pro-i2v",
|
"endpoint": "minimax-hailuo-02-pro-i2v",
|
||||||
"family": "minimax-2",
|
"family": "minimax-2",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "end_image_url",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -6072,6 +6078,7 @@ export const i2vModels = [
|
||||||
"endpoint": "seedance-lite-i2v",
|
"endpoint": "seedance-lite-i2v",
|
||||||
"family": "bytedance",
|
"family": "bytedance",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -6753,6 +6760,7 @@ export const i2vModels = [
|
||||||
"endpoint": "veo3.1-image-to-video",
|
"endpoint": "veo3.1-image-to-video",
|
||||||
"family": "veo3.1",
|
"family": "veo3.1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -6803,6 +6811,7 @@ export const i2vModels = [
|
||||||
"endpoint": "veo3.1-fast-image-to-video",
|
"endpoint": "veo3.1-fast-image-to-video",
|
||||||
"family": "veo3.1",
|
"family": "veo3.1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -6853,6 +6862,7 @@ export const i2vModels = [
|
||||||
"endpoint": "veo3.1-lite-image-to-video",
|
"endpoint": "veo3.1-lite-image-to-video",
|
||||||
"family": "veo3.1",
|
"family": "veo3.1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -7433,6 +7443,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-o1-image-to-video",
|
"endpoint": "kling-o1-image-to-video",
|
||||||
"family": "kling-o1",
|
"family": "kling-o1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -7753,6 +7764,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-o1-standard-image-to-video",
|
"endpoint": "kling-o1-standard-image-to-video",
|
||||||
"family": "kling-o1",
|
"family": "kling-o1",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -7825,6 +7837,7 @@ export const i2vModels = [
|
||||||
"endpoint": "seedance-v1.5-pro-i2v",
|
"endpoint": "seedance-v1.5-pro-i2v",
|
||||||
"family": "seedance-v1.5-pro",
|
"family": "seedance-v1.5-pro",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -7895,6 +7908,7 @@ export const i2vModels = [
|
||||||
"endpoint": "seedance-v1.5-pro-i2v-fast",
|
"endpoint": "seedance-v1.5-pro-i2v-fast",
|
||||||
"family": "seedance-v1.5-pro",
|
"family": "seedance-v1.5-pro",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -8005,6 +8019,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-v3.0-pro-image-to-video",
|
"endpoint": "kling-v3.0-pro-image-to-video",
|
||||||
"family": "kling-v3.0",
|
"family": "kling-v3.0",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
@ -8041,6 +8056,7 @@ export const i2vModels = [
|
||||||
"endpoint": "kling-v3.0-standard-image-to-video",
|
"endpoint": "kling-v3.0-standard-image-to-video",
|
||||||
"family": "kling-v3.0",
|
"family": "kling-v3.0",
|
||||||
"imageField": "image_url",
|
"imageField": "image_url",
|
||||||
|
"lastImageField": "last_image",
|
||||||
"hasPrompt": true,
|
"hasPrompt": true,
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|
|
||||||
|
|
@ -326,6 +326,13 @@ export class MuapiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional end-frame image — only for models declaring lastImageField.
|
||||||
|
// Server-side param name varies (last_image vs end_image_url).
|
||||||
|
const lastImageField = modelInfo?.lastImageField;
|
||||||
|
if (lastImageField && params.last_image) {
|
||||||
|
finalPayload[lastImageField] = params.last_image;
|
||||||
|
}
|
||||||
|
|
||||||
if (params.aspect_ratio) finalPayload.aspect_ratio = params.aspect_ratio;
|
if (params.aspect_ratio) finalPayload.aspect_ratio = params.aspect_ratio;
|
||||||
if (params.duration) finalPayload.duration = params.duration;
|
if (params.duration) finalPayload.duration = params.duration;
|
||||||
if (params.resolution) finalPayload.resolution = params.resolution;
|
if (params.resolution) finalPayload.resolution = params.resolution;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue