diff --git a/src/components/ImageStudio.js b/src/components/ImageStudio.js index 3e9daf1..41a462a 100644 --- a/src/components/ImageStudio.js +++ b/src/components/ImageStudio.js @@ -1,6 +1,7 @@ import { muapi } from '../lib/muapi.js'; -import { t2iModels, getAspectRatiosForModel } from '../lib/models.js'; +import { t2iModels, getAspectRatiosForModel, i2iModels, getAspectRatiosForI2IModel, getResolutionsForI2IModel } from '../lib/models.js'; import { AuthModal } from './AuthModal.js'; +import { createUploadPicker } from './UploadPicker.js'; export function ImageStudio() { const container = document.createElement('div'); @@ -10,31 +11,14 @@ export function ImageStudio() { const defaultModel = t2iModels[0]; let selectedModel = defaultModel.id; let selectedModelName = defaultModel.name; - let selectedAr = '1:1'; + let selectedAr = defaultModel.inputs?.aspect_ratio?.default || '1:1'; let dropdownOpen = null; + let uploadedImageUrl = null; + let imageMode = false; // false = t2i models, true = i2i models - // Helper: Get valid resolutions/quality options for a model - const getResolutionsForModel = (modelId) => { - const model = t2iModels.find(m => m.id === modelId); - if (!model) return ['1K']; // Default - - // Check for specific resolution enum - if (model.inputs?.resolution?.enum) { - return model.inputs.resolution.enum.map(r => r.toUpperCase()); - } - - // Check for megapixels enum - if (model.inputs?.megapixels?.enum) { - return model.inputs.megapixels.enum; - } - - // Fallback logic based on common models - if (modelId.includes('flux')) return ['1K']; // Flux usually fixed - if (modelId.includes('midjourney')) return ['1K']; - - // Default set for others if not specified - return ['1K', '2K', '4K']; - }; + const getCurrentModels = () => imageMode ? i2iModels : t2iModels; + const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2IModel(id) : getAspectRatiosForModel(id); + const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2IModel(id) : []; // ========================================== // 1. HERO SECTION @@ -59,8 +43,8 @@ export function ImageStudio() {
Create stunning, high-aesthetic images in seconds
+Transform images with AI — upscale, stylize, animate and more
`; container.appendChild(hero); @@ -78,10 +62,41 @@ export function ImageStudio() { const topRow = document.createElement('div'); topRow.className = 'flex items-start gap-5 px-2'; - topRow.innerHTML = ``; + // --- Image Upload Picker (Image-to-Image) --- + const picker = createUploadPicker({ + anchorContainer: container, + onSelect: ({ url }) => { + uploadedImageUrl = url; + if (!imageMode) { + imageMode = true; + selectedModel = i2iModels[0].id; + selectedModelName = i2iModels[0].name; + selectedAr = getAspectRatiosForI2IModel(selectedModel)[0]; + document.getElementById('model-btn-label').textContent = selectedModelName; + document.getElementById('ar-btn-label').textContent = selectedAr; + const validResolutions = getResolutionsForI2IModel(selectedModel); + qualityBtn.style.display = validResolutions.length > 0 ? 'flex' : 'none'; + if (validResolutions.length > 0) document.getElementById('quality-btn-label').textContent = validResolutions[0]; + } + textarea.placeholder = 'Describe how to transform this image (optional)'; + }, + onClear: () => { + uploadedImageUrl = null; + imageMode = false; + selectedModel = t2iModels[0].id; + selectedModelName = t2iModels[0].name; + selectedAr = getAspectRatiosForModel(selectedModel)[0]; + document.getElementById('model-btn-label').textContent = selectedModelName; + document.getElementById('ar-btn-label').textContent = selectedAr; + qualityBtn.style.display = 'none'; + textarea.placeholder = 'Describe the image you want to create'; + } + }); + topRow.appendChild(picker.trigger); + container.appendChild(picker.panel); const textarea = document.createElement('textarea'); - textarea.placeholder = 'Describe the scene you imagine'; + textarea.placeholder = 'Describe the image you want to create'; textarea.className = 'flex-1 bg-transparent border-none text-white text-base md:text-xl placeholder:text-muted focus:outline-none resize-none pt-2.5 leading-relaxed min-h-[40px] max-h-[150px] md:max-h-[250px] overflow-y-auto custom-scrollbar'; textarea.rows = 1; textarea.oninput = () => { @@ -124,16 +139,12 @@ export function ImageStudio() { const qualityBtn = createControlBtn(` - `, '1K', 'quality-btn'); + `, '720p', 'quality-btn'); controlsLeft.appendChild(modelBtn); controlsLeft.appendChild(arBtn); controlsLeft.appendChild(qualityBtn); - - // Initial Resolution Visibility (only show for models with explicit resolution/megapixels enums) - const initialModel = t2iModels[0]; - const hasInitialRes = initialModel?.inputs?.resolution?.enum || initialModel?.inputs?.megapixels?.enum; - qualityBtn.style.display = hasInitialRes ? 'flex' : 'none'; + qualityBtn.style.display = 'none'; // hidden in t2i mode, shown when i2i model has resolutions const generateBtn = document.createElement('button'); generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg'; @@ -175,14 +186,14 @@ export function ImageStudio() { const renderModels = (filter = '') => { list.innerHTML = ''; - const filtered = t2iModels.filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase())); + const filtered = getCurrentModels().filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase())); filtered.forEach(m => { const item = document.createElement('div'); item.className = `flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all border border-transparent hover:border-white/5 ${selectedModel === m.id ? 'bg-white/5 border-white/5' : ''}`; item.innerHTML = `Create stunning AI videos from text in seconds
+Animate images into stunning AI videos with motion effects
`; container.appendChild(hero); @@ -55,8 +63,35 @@ export function VideoStudio() { const topRow = document.createElement('div'); topRow.className = 'flex items-start gap-5 px-2'; + // --- Image Upload Picker (Image-to-Video) --- + const picker = createUploadPicker({ + anchorContainer: container, + onSelect: ({ url }) => { + uploadedImageUrl = url; + if (!imageMode) { + imageMode = true; + selectedModel = i2vModels[0].id; + selectedModelName = i2vModels[0].name; + document.getElementById('v-model-btn-label').textContent = selectedModelName; + updateControlsForModel(selectedModel); + } + textarea.placeholder = 'Describe the motion or effect (optional)'; + }, + onClear: () => { + uploadedImageUrl = null; + imageMode = false; + selectedModel = t2vModels[0].id; + selectedModelName = t2vModels[0].name; + document.getElementById('v-model-btn-label').textContent = selectedModelName; + updateControlsForModel(selectedModel); + textarea.placeholder = 'Describe the video you want to create'; + } + }); + topRow.appendChild(picker.trigger); + container.appendChild(picker.panel); + const textarea = document.createElement('textarea'); - textarea.placeholder = 'Describe the video you imagine'; + textarea.placeholder = 'Describe the video you want to create'; textarea.className = 'flex-1 bg-transparent border-none text-white text-base md:text-xl placeholder:text-muted focus:outline-none resize-none pt-2.5 leading-relaxed min-h-[40px] max-h-[150px] md:max-h-[250px] overflow-y-auto custom-scrollbar'; textarea.rows = 1; textarea.oninput = () => { @@ -110,7 +145,7 @@ export function VideoStudio() { controlsLeft.appendChild(durationBtn); controlsLeft.appendChild(resolutionBtn); - // Initial visibility + // Initial visibility (t2v mode) const initDurations = getDurationsForModel(defaultModel.id); durationBtn.style.display = initDurations.length > 0 ? 'flex' : 'none'; const initResolutions = getResolutionsForVideoModel(defaultModel.id); @@ -133,11 +168,11 @@ export function VideoStudio() { dropdown.className = 'absolute bottom-[102%] left-2 z-50 transition-all opacity-0 pointer-events-none scale-95 origin-bottom-left glass rounded-3xl p-3 translate-y-2 w-[calc(100vw-3rem)] max-w-xs shadow-4xl border border-white/10 flex flex-col'; const updateControlsForModel = (modelId) => { - const availableArs = getAspectRatiosForVideoModel(modelId); + const availableArs = getCurrentAspectRatios(modelId); selectedAr = availableArs[0]; document.getElementById('v-ar-btn-label').textContent = selectedAr; - const durations = getDurationsForModel(modelId); + const durations = getCurrentDurations(modelId); if (durations.length > 0) { selectedDuration = durations[0]; document.getElementById('v-duration-btn-label').textContent = `${selectedDuration}s`; @@ -146,7 +181,7 @@ export function VideoStudio() { durationBtn.style.display = 'none'; } - const resolutions = getResolutionsForVideoModel(modelId); + const resolutions = getCurrentResolutions(modelId); if (resolutions.length > 0) { selectedResolution = resolutions[0]; document.getElementById('v-resolution-btn-label').textContent = selectedResolution; @@ -180,7 +215,7 @@ export function VideoStudio() { const renderModels = (filter = '') => { list.innerHTML = ''; - const filtered = t2vModels.filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase())); + const filtered = getCurrentModels().filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase())); filtered.forEach(m => { const item = document.createElement('div'); item.className = `flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all border border-transparent hover:border-white/5 ${selectedModel === m.id ? 'bg-white/5 border-white/5' : ''}`; @@ -215,7 +250,7 @@ export function VideoStudio() { dropdown.innerHTML = `