mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
fix(local): route Video Studio uploads + generate through Wan2GP when a local model is selected
Closes the gap reported in #126 where local users hit "Not authorized: missing or invalid credentials" on upload and couldn't generate video locally even with WanGP installed. Image, video, and reference uploads were all hard-wired to the Muapi-hosted upload endpoint, and Video Studio had no branch into the local Wan2GP provider for generate. - electron/wan2gpProvider: new wan2gp:upload-file IPC that POSTs to the Gradio /upload endpoint, caches the returned path, and rehydrates it into a Gradio FileData descriptor on generate. Adds wan2gp:wan22-i2v. - preload + localInferenceClient: expose uploadFileToWan2gp(file). - localModels: wan22-i2v entry, isWan2gpModelId, localT2VModels/localI2VModels. - UploadPicker: accept optional uploadFn + requireApiKey so callers can bypass the Muapi auth modal when the active provider is local. - VideoStudio: merge Wan2GP video models into t2v/i2v lists, route the reference-image upload through the local provider when a Wan2GP model is selected, skip the Muapi key gate for local generations, call localAI.generate, and surface step progress in the button label. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
35b7103c26
commit
d4e645defc
6 changed files with 217 additions and 29 deletions
|
|
@ -58,6 +58,20 @@ const WAN2GP_CATALOG = [
|
|||
defaultGuidance: 5.0,
|
||||
tags: ['video', 'wan', 'text-to-video'],
|
||||
},
|
||||
{
|
||||
id: 'wan2gp:wan22-i2v',
|
||||
name: 'Wan 2.2 (Image-to-Video)',
|
||||
description: 'Video — Wan 2.2 image-to-video. Provide a start frame.',
|
||||
type: 'video',
|
||||
family: 'wan',
|
||||
provider: 'wan2gp',
|
||||
fn: 'wan22_i2v',
|
||||
needsImage: true,
|
||||
aspectRatios: ['16:9', '1:1', '9:16'],
|
||||
defaultSteps: 25,
|
||||
defaultGuidance: 5.0,
|
||||
tags: ['video', 'wan', 'image-to-video'],
|
||||
},
|
||||
{
|
||||
id: 'wan2gp:hunyuan-video',
|
||||
name: 'Hunyuan Video (Wan2GP)',
|
||||
|
|
@ -100,6 +114,10 @@ function normalizeUrl(url) { return (url || '').trim().replace(/\/+$/, ''); }
|
|||
// ─── State ────────────────────────────────────────────────────────────────────
|
||||
let activeAbort = null;
|
||||
|
||||
// Map of uploaded source URL → { path, url, orig_name } so generate() can
|
||||
// rehydrate the Gradio file descriptor when the renderer passes the URL back.
|
||||
const uploadedFiles = new Map();
|
||||
|
||||
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
function httpJson(urlStr, { method = 'GET', body = null, timeoutMs = 5000 } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -135,6 +153,37 @@ async function probe(url) {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── Upload (Gradio v4 /upload) ───────────────────────────────────────────────
|
||||
// Renderer hands us { name, type, bytes:Uint8Array }. We POST as multipart to
|
||||
// <base>/upload?upload_id=<id>; Gradio replies with an array of server paths.
|
||||
// We expose those as a stable HTTP URL the renderer can preview AND stash the
|
||||
// raw path for generate() to feed back into Gradio's file descriptor.
|
||||
async function uploadFile({ name, type, bytes }) {
|
||||
const { url } = readConfig();
|
||||
if (!url) throw new Error('Wan2GP server URL not set. Open Settings → Local Models to configure.');
|
||||
const base = normalizeUrl(url);
|
||||
|
||||
if (!bytes || !bytes.length) throw new Error('Empty file payload');
|
||||
const safeName = name || 'upload.bin';
|
||||
const mime = type || 'application/octet-stream';
|
||||
|
||||
const blob = new Blob([new Uint8Array(bytes)], { type: mime });
|
||||
const form = new FormData();
|
||||
form.append('files', blob, safeName);
|
||||
|
||||
const uploadId = Math.random().toString(36).slice(2, 12);
|
||||
const res = await fetch(`${base}/upload?upload_id=${uploadId}`, { method: 'POST', body: form });
|
||||
if (!res.ok) throw new Error(`Wan2GP upload failed: HTTP ${res.status}`);
|
||||
|
||||
const paths = await res.json();
|
||||
const path = Array.isArray(paths) ? paths[0] : paths;
|
||||
if (!path || typeof path !== 'string') throw new Error('Wan2GP upload returned no path');
|
||||
|
||||
const fileUrl = `${base}/file=${path.replace(/^\/+/, '')}`;
|
||||
uploadedFiles.set(fileUrl, { path, url: fileUrl, orig_name: safeName, mime_type: mime });
|
||||
return { url: fileUrl, path };
|
||||
}
|
||||
|
||||
async function listModels() {
|
||||
const { url } = readConfig();
|
||||
const reachable = url ? (await probe(url)).ok : false;
|
||||
|
|
@ -233,13 +282,29 @@ async function generate(params, mainWindow) {
|
|||
const steps = params.steps ?? model.defaultSteps;
|
||||
const guidance = params.guidance_scale ?? model.defaultGuidance;
|
||||
|
||||
// Image input → resolve to a Gradio file descriptor if we uploaded it.
|
||||
let imageDescriptor = null;
|
||||
if (params.image) {
|
||||
const cached = uploadedFiles.get(params.image);
|
||||
if (cached) {
|
||||
imageDescriptor = { path: cached.path, url: cached.url, orig_name: cached.orig_name, mime_type: cached.mime_type, meta: { _type: 'gradio.FileData' } };
|
||||
} else if (typeof params.image === 'string') {
|
||||
imageDescriptor = params.image; // raw URL — Gradio fetches it
|
||||
} else {
|
||||
imageDescriptor = params.image;
|
||||
}
|
||||
}
|
||||
if (model.needsImage && !imageDescriptor) {
|
||||
throw new Error(`${model.name} requires a start-frame image — upload one first.`);
|
||||
}
|
||||
|
||||
// Generic positional input — adjust upstream `fn` if signature differs.
|
||||
const payload = {
|
||||
data: [
|
||||
params.prompt || '',
|
||||
params.negative_prompt || '',
|
||||
width, height, steps, guidance, seed,
|
||||
params.image || null,
|
||||
imageDescriptor,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -282,6 +347,7 @@ function register() {
|
|||
ipcMain.handle('wan2gp:list-models', () => listModels());
|
||||
ipcMain.handle('wan2gp:generate', (_, params) => generate(params, getMainWindow()));
|
||||
ipcMain.handle('wan2gp:cancel-generation', () => cancelGeneration());
|
||||
ipcMain.handle('wan2gp:upload-file', (_, payload) => uploadFile(payload));
|
||||
}
|
||||
|
||||
module.exports = { register, WAN2GP_CATALOG };
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ contextBridge.exposeInMainWorld('localAI', {
|
|||
listModels: () => ipcRenderer.invoke('wan2gp:list-models'),
|
||||
generate: (params) => ipcRenderer.invoke('wan2gp:generate', params),
|
||||
cancelGeneration: () => ipcRenderer.invoke('wan2gp:cancel-generation'),
|
||||
uploadFile: (payload) => ipcRenderer.invoke('wan2gp:upload-file', payload),
|
||||
},
|
||||
|
||||
// Progress events — both engines emit on local-ai:progress
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ import { getUploadHistory, saveUpload, removeUpload, generateThumbnail } from '.
|
|||
* @param {number} [options.maxImages=1] - Maximum number of images selectable
|
||||
* @returns {{ trigger: HTMLElement, panel: HTMLElement, reset: function, setMaxImages: function }}
|
||||
*/
|
||||
export function createUploadPicker({ anchorContainer, onSelect, onClear, maxImages: initialMaxImages = 1 }) {
|
||||
export function createUploadPicker({ anchorContainer, onSelect, onClear, maxImages: initialMaxImages = 1, uploadFn, requireApiKey }) {
|
||||
// uploadFn(file) → Promise<string url>. Defaults to Muapi-hosted upload.
|
||||
// requireApiKey() → boolean. Lets the caller suppress the AuthModal when
|
||||
// the active provider doesn't need a Muapi key (e.g. local Wan2GP).
|
||||
const doUpload = uploadFn || ((file) => muapi.uploadFile(file));
|
||||
const needsKey = typeof requireApiKey === 'function' ? requireApiKey : () => true;
|
||||
let panelOpen = false;
|
||||
let maxImages = initialMaxImages;
|
||||
let selectedEntries = []; // [{ url, thumbnail }, ...]
|
||||
|
|
@ -318,10 +323,12 @@ export function createUploadPicker({ anchorContainer, onSelect, onClear, maxImag
|
|||
const files = Array.from(e.target.files);
|
||||
if (!files.length) return;
|
||||
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) {
|
||||
AuthModal(() => fileInput.click());
|
||||
return;
|
||||
if (needsKey()) {
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) {
|
||||
AuthModal(() => fileInput.click());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showSpinner();
|
||||
|
|
@ -330,10 +337,11 @@ export function createUploadPicker({ anchorContainer, onSelect, onClear, maxImag
|
|||
if (maxImages === 1) {
|
||||
// Single mode: upload first file only, replace selection
|
||||
const file = files[0];
|
||||
const [uploadedUrl, thumbnail] = await Promise.all([
|
||||
muapi.uploadFile(file),
|
||||
const [uploadResult, thumbnail] = await Promise.all([
|
||||
doUpload(file),
|
||||
generateThumbnail(file)
|
||||
]);
|
||||
const uploadedUrl = typeof uploadResult === 'string' ? uploadResult : uploadResult?.url;
|
||||
const entry = { id: Date.now().toString(), name: file.name, uploadedUrl, thumbnail, timestamp: new Date().toISOString() };
|
||||
saveUpload(entry);
|
||||
selectedEntries = [{ url: uploadedUrl, thumbnail }];
|
||||
|
|
@ -346,10 +354,11 @@ export function createUploadPicker({ anchorContainer, onSelect, onClear, maxImag
|
|||
|
||||
// Upload all in parallel
|
||||
const results = await Promise.all(toUpload.map(async (file) => {
|
||||
const [uploadedUrl, thumbnail] = await Promise.all([
|
||||
muapi.uploadFile(file),
|
||||
const [uploadResult, thumbnail] = await Promise.all([
|
||||
doUpload(file),
|
||||
generateThumbnail(file)
|
||||
]);
|
||||
const uploadedUrl = typeof uploadResult === 'string' ? uploadResult : uploadResult?.url;
|
||||
return { id: Date.now().toString() + Math.random(), name: file.name, uploadedUrl, thumbnail, timestamp: new Date().toISOString() };
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,36 @@ import { t2vModels, getAspectRatiosForVideoModel, getDurationsForModel, getResol
|
|||
import { AuthModal } from './AuthModal.js';
|
||||
import { createUploadPicker } from './UploadPicker.js';
|
||||
import { savePendingJob, removePendingJob, getPendingJobs } from '../lib/pendingJobs.js';
|
||||
import { localAI, isLocalAIAvailable } from '../lib/localInferenceClient.js';
|
||||
import { isWan2gpModelId, getLocalModelById, localT2VModels, localI2VModels } from '../lib/localModels.js';
|
||||
|
||||
// Promotes a wan2gp catalog entry (lib/localModels.js shape) into the
|
||||
// `inputs`-shaped descriptor the Video Studio dropdowns/controls expect.
|
||||
const adaptLocalToVideoEntry = (m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
provider: 'wan2gp',
|
||||
inputs: {
|
||||
prompt: { type: 'string', name: 'prompt', title: 'Prompt' },
|
||||
aspect_ratio: { type: 'string', name: 'aspect_ratio', enum: m.aspectRatios || ['16:9', '1:1', '9:16'], default: (m.aspectRatios || ['16:9'])[0] },
|
||||
},
|
||||
});
|
||||
|
||||
export function VideoStudio() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'w-full h-full flex flex-col items-center justify-center bg-app-bg relative p-4 md:p-6 overflow-y-auto custom-scrollbar overflow-x-hidden';
|
||||
|
||||
// Merge Wan2GP video models in only when running inside Electron AND the
|
||||
// user has a Wan2GP server configured. We can't probe synchronously, so
|
||||
// we always include them when isLocalAIAvailable() — getCurrentModel()
|
||||
// reads from these arrays, so they need to be present from init.
|
||||
const localT2V = isLocalAIAvailable() ? localT2VModels.map(adaptLocalToVideoEntry) : [];
|
||||
const localI2V = isLocalAIAvailable() ? localI2VModels.map(adaptLocalToVideoEntry) : [];
|
||||
const allT2V = [...t2vModels, ...localT2V];
|
||||
const allI2V = [...i2vModels, ...localI2V];
|
||||
|
||||
// --- State ---
|
||||
const defaultModel = t2vModels[0];
|
||||
const defaultModel = allT2V[0];
|
||||
let selectedModel = defaultModel.id;
|
||||
let selectedModelName = defaultModel.name;
|
||||
let selectedAr = defaultModel.inputs?.aspect_ratio?.default || '16:9';
|
||||
|
|
@ -26,10 +49,22 @@ export function VideoStudio() {
|
|||
let v2vMode = false; // true = video-to-video tools mode
|
||||
let uploadedVideoUrl = null;
|
||||
|
||||
const getCurrentModels = () => v2vMode ? v2vModels : (imageMode ? i2vModels : t2vModels);
|
||||
const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2VModel(id) : getAspectRatiosForVideoModel(id);
|
||||
const getCurrentDurations = (id) => imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(id);
|
||||
const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2VModel(id) : getResolutionsForVideoModel(id);
|
||||
const getCurrentModels = () => v2vMode ? v2vModels : (imageMode ? allI2V : allT2V);
|
||||
// Local Wan2GP entries don't live in the Muapi-derived helpers, so we
|
||||
// resolve aspect ratios off the catalog when the selected id is local.
|
||||
const getCurrentAspectRatios = (id) => {
|
||||
const local = getLocalModelById(id);
|
||||
if (local) return local.aspectRatios || ['16:9', '1:1', '9:16'];
|
||||
return imageMode ? getAspectRatiosForI2VModel(id) : getAspectRatiosForVideoModel(id);
|
||||
};
|
||||
const getCurrentDurations = (id) => {
|
||||
if (getLocalModelById(id)) return [];
|
||||
return imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(id);
|
||||
};
|
||||
const getCurrentResolutions = (id) => {
|
||||
if (getLocalModelById(id)) return [];
|
||||
return imageMode ? getResolutionsForI2VModel(id) : getResolutionsForVideoModel(id);
|
||||
};
|
||||
const getCurrentModes = (id) => getModesForModel(id);
|
||||
const getCurrentModel = () => getCurrentModels().find(m => m.id === selectedModel);
|
||||
const getQualitiesForModel = (id) => {
|
||||
|
|
@ -94,8 +129,8 @@ export function VideoStudio() {
|
|||
}
|
||||
if (!imageMode) {
|
||||
imageMode = true;
|
||||
selectedModel = i2vModels[0].id;
|
||||
selectedModelName = i2vModels[0].name;
|
||||
selectedModel = allI2V[0].id;
|
||||
selectedModelName = allI2V[0].name;
|
||||
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||
updateControlsForModel(selectedModel);
|
||||
}
|
||||
|
|
@ -105,13 +140,17 @@ export function VideoStudio() {
|
|||
onClear: () => {
|
||||
uploadedImageUrl = null;
|
||||
imageMode = false;
|
||||
selectedModel = t2vModels[0].id;
|
||||
selectedModelName = t2vModels[0].name;
|
||||
selectedModel = allT2V[0].id;
|
||||
selectedModelName = allT2V[0].name;
|
||||
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||
updateControlsForModel(selectedModel);
|
||||
textarea.placeholder = 'Describe the video you want to create';
|
||||
textarea.disabled = false;
|
||||
}
|
||||
},
|
||||
// Route the upload through the configured Wan2GP server when the active
|
||||
// model is local; otherwise fall back to the Muapi-hosted upload.
|
||||
uploadFn: (file) => isWan2gpModelId(selectedModel) ? localAI.uploadFileToWan2gp(file) : muapi.uploadFile(file),
|
||||
requireApiKey: () => !isWan2gpModelId(selectedModel),
|
||||
});
|
||||
topRow.appendChild(picker.trigger);
|
||||
container.appendChild(picker.panel);
|
||||
|
|
@ -172,8 +211,8 @@ export function VideoStudio() {
|
|||
uploadedVideoUrl = null;
|
||||
v2vMode = false;
|
||||
showVideoIcon();
|
||||
selectedModel = t2vModels[0].id;
|
||||
selectedModelName = t2vModels[0].name;
|
||||
selectedModel = allT2V[0].id;
|
||||
selectedModelName = allT2V[0].name;
|
||||
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||
updateControlsForModel(selectedModel);
|
||||
textarea.placeholder = 'Describe the video you want to create';
|
||||
|
|
@ -502,7 +541,7 @@ export function VideoStudio() {
|
|||
const lf = filter.toLowerCase();
|
||||
|
||||
// Regular generation models (always t2v or i2v, never v2v)
|
||||
const generationModels = imageMode ? i2vModels : t2vModels;
|
||||
const generationModels = imageMode ? allI2V : allT2V;
|
||||
const filteredMain = generationModels
|
||||
.filter(m => m.name.toLowerCase().includes(lf) || m.id.toLowerCase().includes(lf));
|
||||
filteredMain.forEach(m => list.appendChild(makeModelItem(m, false)));
|
||||
|
|
@ -926,8 +965,8 @@ export function VideoStudio() {
|
|||
uploadedVideoUrl = null;
|
||||
v2vMode = false;
|
||||
showVideoIcon();
|
||||
selectedModel = t2vModels[0].id;
|
||||
selectedModelName = t2vModels[0].name;
|
||||
selectedModel = allT2V[0].id;
|
||||
selectedModelName = allT2V[0].name;
|
||||
document.getElementById('v-model-btn-label').textContent = selectedModelName;
|
||||
updateControlsForModel(selectedModel);
|
||||
textarea.placeholder = 'Describe the video you want to create';
|
||||
|
|
@ -980,16 +1019,30 @@ export function VideoStudio() {
|
|||
}
|
||||
}
|
||||
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) {
|
||||
AuthModal(() => generateBtn.click());
|
||||
return;
|
||||
const isLocal = isWan2gpModelId(selectedModel);
|
||||
|
||||
// Local Wan2GP generations don't go through Muapi — skip the auth gate.
|
||||
if (!isLocal) {
|
||||
const apiKey = localStorage.getItem('muapi_key');
|
||||
if (!apiKey) {
|
||||
AuthModal(() => generateBtn.click());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hero.classList.add('opacity-0', 'scale-95', '-translate-y-10', 'pointer-events-none');
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> Generating...`;
|
||||
|
||||
// For local generations, surface step progress in the button label.
|
||||
let unsubscribeProgress = null;
|
||||
if (isLocal) {
|
||||
unsubscribeProgress = localAI.onProgress(({ status, progress }) => {
|
||||
const pct = typeof progress === 'number' ? Math.round(progress * 100) : null;
|
||||
generateBtn.innerHTML = `<span class="animate-spin inline-block mr-2 text-black">◌</span> ${status || 'Generating'}${pct != null ? ` ${pct}%` : '…'}`;
|
||||
});
|
||||
}
|
||||
|
||||
let hadError = false;
|
||||
let capturedRequestId = null;
|
||||
const historyMeta = { prompt, model: selectedModel, aspect_ratio: selectedAr, duration: selectedDuration };
|
||||
|
|
@ -1000,6 +1053,32 @@ export function VideoStudio() {
|
|||
};
|
||||
|
||||
try {
|
||||
// ─── Local Wan2GP path ───────────────────────────────────────────
|
||||
// Uploaded image URLs were minted by uploadFileToWan2gp(), so
|
||||
// wan2gpProvider can rehydrate the Gradio file descriptor.
|
||||
if (isLocal) {
|
||||
const localParams = {
|
||||
model: selectedModel,
|
||||
prompt: prompt || '',
|
||||
aspect_ratio: selectedAr,
|
||||
};
|
||||
if (imageMode && uploadedImageUrl) localParams.image = uploadedImageUrl;
|
||||
const res = await localAI.generate(localParams);
|
||||
console.log('[VideoStudio] Local response:', res);
|
||||
if (res && res.url) {
|
||||
const genId = Date.now().toString();
|
||||
lastGenerationId = null;
|
||||
lastGenerationModel = null;
|
||||
addToHistory({ id: genId, url: res.url, prompt, model: selectedModel, aspect_ratio: selectedAr, timestamp: new Date().toISOString() });
|
||||
showVideoInCanvas(res.url, selectedModel);
|
||||
} else {
|
||||
throw new Error('No video URL returned by Wan2GP');
|
||||
}
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.innerHTML = `Generate ✨`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (v2vMode) {
|
||||
const res = await muapi.processV2V({ model: selectedModel, video_url: uploadedVideoUrl, onRequestId });
|
||||
console.log('[VideoStudio] V2V response:', res);
|
||||
|
|
@ -1119,6 +1198,7 @@ export function VideoStudio() {
|
|||
}, 4000);
|
||||
} finally {
|
||||
generateBtn.disabled = false;
|
||||
if (typeof unsubscribeProgress === 'function') unsubscribeProgress();
|
||||
// Only reset the label on success; the catch timeout handles the error case
|
||||
if (!hadError) generateBtn.innerHTML = `Generate ✨`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,19 @@ class LocalInferenceClient {
|
|||
if (!isLocalAIAvailable()) return { ok: false, error: 'Not in desktop app' };
|
||||
return window.localAI.wan2gp.probe(url);
|
||||
}
|
||||
// Pushes a File/Blob to the configured Wan2GP server's /upload endpoint
|
||||
// and returns { url, path }. URL is a previewable HTTP link; the provider
|
||||
// also remembers the path so a subsequent generate(params.image=url) call
|
||||
// can rehydrate it as a Gradio file descriptor.
|
||||
async uploadFileToWan2gp(file) {
|
||||
if (!isLocalAIAvailable()) throw new Error('Local AI only available in the desktop app.');
|
||||
const buf = await file.arrayBuffer();
|
||||
return window.localAI.wan2gp.uploadFile({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
bytes: new Uint8Array(buf),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Unified model list (both providers merged) ────────────────────────
|
||||
async listModels() {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,19 @@ export const LOCAL_MODEL_CATALOG = [
|
|||
defaultGuidance: 5.0,
|
||||
tags: ['video', 'wan', 'text-to-video'],
|
||||
},
|
||||
{
|
||||
id: 'wan2gp:wan22-i2v',
|
||||
name: 'Wan 2.2 (Image-to-Video)',
|
||||
description: 'Video — Wan 2.2 image-to-video. Provide a start frame.',
|
||||
type: 'video',
|
||||
family: 'wan',
|
||||
provider: 'wan2gp',
|
||||
needsImage: true,
|
||||
aspectRatios: ['16:9', '1:1', '9:16'],
|
||||
defaultSteps: 25,
|
||||
defaultGuidance: 5.0,
|
||||
tags: ['video', 'wan', 'image-to-video'],
|
||||
},
|
||||
{
|
||||
id: 'wan2gp:hunyuan-video',
|
||||
name: 'Hunyuan Video (Wan2GP)',
|
||||
|
|
@ -155,3 +168,9 @@ export const LOCAL_MODEL_CATALOG = [
|
|||
export function getLocalModelById(id) {
|
||||
return LOCAL_MODEL_CATALOG.find(m => m.id === id) || null;
|
||||
}
|
||||
|
||||
export const isWan2gpModelId = (id) => getLocalModelById(id)?.provider === 'wan2gp';
|
||||
export const isLocalModelId = (id) => !!getLocalModelById(id);
|
||||
|
||||
export const localT2VModels = LOCAL_MODEL_CATALOG.filter(m => m.provider === 'wan2gp' && m.type === 'video' && !m.needsImage);
|
||||
export const localI2VModels = LOCAL_MODEL_CATALOG.filter(m => m.provider === 'wan2gp' && m.type === 'video' && m.needsImage);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue