Added ui sopport for multi control samples and datasets. Added qwen image edit 5209 to the ui

This commit is contained in:
Jaret Burkett
2025-09-25 11:10:02 -06:00
parent 454be0958a
commit 1069dee0e4
10 changed files with 574 additions and 88 deletions

View File

@@ -0,0 +1,105 @@
---
job: extension
config:
# this name will be the folder and filename name
name: "my_first_qwen_image_edit_2509_lora_v1"
process:
- type: 'diffusion_trainer'
# root folder to save training sessions/samples/weights
training_folder: "output"
# uncomment to see performance stats in the terminal every N steps
# performance_log_every: 1000
device: cuda:0
network:
type: "lora"
linear: 16
linear_alpha: 16
save:
dtype: float16 # precision to save
save_every: 250 # save every this many steps
max_step_saves_to_keep: 4 # how many intermittent saves to keep
datasets:
# datasets are a folder of images. captions need to be txt files with the same name as the image
# for instance image2.jpg and image2.txt. Only jpg, jpeg, and png are supported currently
# images will automatically be resized and bucketed into the resolution specified
# on windows, escape back slashes with another backslash so
# "C:\\path\\to\\images\\folder"
- folder_path: "/path/to/images/folder"
# can do up to 3 control image folders, file names must match target file names, but aspect/size can be different
control_path:
- "/path/to/control/images/folder1"
- "/path/to/control/images/folder2"
- "/path/to/control/images/folder3"
caption_ext: "txt"
# default_caption: "a person" # if caching text embeddings, if you don't have captions, this will get cached
caption_dropout_rate: 0.05 # will drop out the caption 5% of time
resolution: [ 512, 768, 1024 ] # qwen image enjoys multiple resolutions
# a trigger word that can be cached with the text embeddings
# trigger_word: "optional trigger word"
train:
batch_size: 1
# caching text embeddings is required for 32GB
cache_text_embeddings: true
# unload_text_encoder: true
steps: 3000 # total number of steps to train 500 - 4000 is a good range
gradient_accumulation: 1
timestep_type: "weighted"
train_unet: true
train_text_encoder: false # probably won't work with qwen image
gradient_checkpointing: true # need the on unless you have a ton of vram
noise_scheduler: "flowmatch" # for training only
optimizer: "adamw8bit"
lr: 1e-4
# uncomment this to skip the pre training sample
# skip_first_sample: true
# uncomment to completely disable sampling
# disable_sampling: true
dtype: bf16
model:
# huggingface model name or path
name_or_path: "Qwen/Qwen-Image-Edit-2509"
arch: "qwen_image_edit_plus"
quantize: true
# to use the ARA use the | pipe to point to hf path, or a local path if you have one.
# 3bit is required for 32GB
qtype: "uint3|ostris/accuracy_recovery_adapters/qwen_image_edit_2509_torchao_uint3.safetensors"
quantize_te: true
qtype_te: "qfloat8"
low_vram: true
sample:
sampler: "flowmatch" # must match train.noise_scheduler
sample_every: 250 # sample every this many steps
width: 1024
height: 1024
# you can provide up to 3 control images here
samples:
- prompt: "Do whatever with Image1 and Image2"
ctrl_img_1: "/path/to/image1.png"
ctrl_img_2: "/path/to/image2.png"
# ctrl_img_3: "/path/to/image3.png"
- prompt: "Do whatever with Image1 and Image2"
ctrl_img_1: "/path/to/image1.png"
ctrl_img_2: "/path/to/image2.png"
# ctrl_img_3: "/path/to/image3.png"
- prompt: "Do whatever with Image1 and Image2"
ctrl_img_1: "/path/to/image1.png"
ctrl_img_2: "/path/to/image2.png"
# ctrl_img_3: "/path/to/image3.png"
- prompt: "Do whatever with Image1 and Image2"
ctrl_img_1: "/path/to/image1.png"
ctrl_img_2: "/path/to/image2.png"
# ctrl_img_3: "/path/to/image3.png"
- prompt: "Do whatever with Image1 and Image2"
ctrl_img_1: "/path/to/image1.png"
ctrl_img_2: "/path/to/image2.png"
# ctrl_img_3: "/path/to/image3.png"
neg: ""
seed: 42
walk_seed: true
guidance_scale: 3
sample_steps: 25
# you can add any additional meta info here. [name] is replaced with config name at top
meta:
name: "[name]"
version: '1.0'

View File

@@ -831,6 +831,21 @@ class DatasetConfig:
if self.control_path == '':
self.control_path = None
# handle multi control inputs from the ui. It is just easier to handle it here for a cleaner ui experience
control_path_1 = kwargs.get('control_path_1', None)
control_path_2 = kwargs.get('control_path_2', None)
control_path_3 = kwargs.get('control_path_3', None)
if any([control_path_1, control_path_2, control_path_3]):
control_paths = []
if control_path_1:
control_paths.append(control_path_1)
if control_path_2:
control_paths.append(control_path_2)
if control_path_3:
control_paths.append(control_path_3)
self.control_path = control_paths
# color for transparent reigon of control images with transparency
self.control_transparent_color: List[int] = kwargs.get('control_transparent_color', [0, 0, 0])
# inpaint images should be webp/png images with alpha channel. The alpha 0 (invisible) section will

View File

@@ -15,7 +15,9 @@ import { TextInput, SelectInput, Checkbox, FormGroup, NumberInput } from '@/comp
import Card from '@/components/Card';
import { X } from 'lucide-react';
import AddSingleImageModal, { openAddImageModal } from '@/components/AddSingleImageModal';
import SampleControlImage from '@/components/SampleControlImage';
import { FlipHorizontal2, FlipVertical2 } from 'lucide-react';
import { handleModelArchChange } from './utils';
type Props = {
jobConfig: JobConfig;
@@ -185,58 +187,7 @@ export default function SimpleJob({
label="Model Architecture"
value={jobConfig.config.process[0].model.arch}
onChange={value => {
const currentArch = modelArchs.find(a => a.name === jobConfig.config.process[0].model.arch);
if (!currentArch || currentArch.name === value) {
return;
}
// update the defaults when a model is selected
const newArch = modelArchs.find(model => model.name === value);
// update vram setting
if (!newArch?.additionalSections?.includes('model.low_vram')) {
setJobConfig(false, 'config.process[0].model.low_vram');
}
// revert defaults from previous model
for (const key in currentArch.defaults) {
setJobConfig(currentArch.defaults[key][1], key);
}
if (newArch?.defaults) {
for (const key in newArch.defaults) {
setJobConfig(newArch.defaults[key][0], key);
}
}
// set new model
setJobConfig(value, 'config.process[0].model.arch');
// update datasets
const hasControlPath = newArch?.additionalSections?.includes('datasets.control_path') || false;
const hasNumFrames = newArch?.additionalSections?.includes('datasets.num_frames') || false;
const controls = newArch?.controls ?? [];
const datasets = jobConfig.config.process[0].datasets.map(dataset => {
const newDataset = objectCopy(dataset);
newDataset.controls = controls;
if (!hasControlPath) {
newDataset.control_path = null; // reset control path if not applicable
}
if (!hasNumFrames) {
newDataset.num_frames = 1; // reset num_frames if not applicable
}
return newDataset;
});
setJobConfig(datasets, 'config.process[0].datasets');
// update samples
const hasSampleCtrlImg = newArch?.additionalSections?.includes('sample.ctrl_img') || false;
const samples = jobConfig.config.process[0].sample.samples.map(sample => {
const newSample = objectCopy(sample);
if (!hasSampleCtrlImg) {
delete newSample.ctrl_img; // remove ctrl_img if not applicable
}
return newSample;
});
setJobConfig(samples, 'config.process[0].sample.samples');
handleModelArchChange(jobConfig.config.process[0].model.arch, value, jobConfig, setJobConfig);
}}
options={groupedModelOptions}
/>
@@ -557,17 +508,19 @@ export default function SimpleJob({
)}
<FormGroup label="Text Encoder Optimizations" className="pt-2">
<Checkbox
label="Unload TE"
checked={jobConfig.config.process[0].train.unload_text_encoder || false}
docKey={'train.unload_text_encoder'}
onChange={value => {
setJobConfig(value, 'config.process[0].train.unload_text_encoder');
if (value) {
setJobConfig(false, 'config.process[0].train.cache_text_embeddings');
}
}}
/>
{!disableSections.includes('train.unload_text_encoder') && (
<Checkbox
label="Unload TE"
checked={jobConfig.config.process[0].train.unload_text_encoder || false}
docKey={'train.unload_text_encoder'}
onChange={value => {
setJobConfig(value, 'config.process[0].train.unload_text_encoder');
if (value) {
setJobConfig(false, 'config.process[0].train.cache_text_embeddings');
}
}}
/>
)}
<Checkbox
label="Cache Text Embeddings"
checked={jobConfig.config.process[0].train.cache_text_embeddings || false}
@@ -642,7 +595,7 @@ export default function SimpleJob({
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div>
<SelectInput
label="Dataset"
label="Target Dataset"
value={dataset.folder_path}
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].folder_path`)}
options={datasetOptions}
@@ -659,6 +612,49 @@ export default function SimpleJob({
options={[{ value: '', label: <>&nbsp;</> }, ...datasetOptions]}
/>
)}
{modelArch?.additionalSections?.includes('datasets.multi_control_paths') && (
<>
<SelectInput
label="Control Dataset 1"
docKey="datasets.multi_control_paths"
value={dataset.control_path_1 ?? ''}
className="pt-2"
onChange={value =>
setJobConfig(
value == '' ? null : value,
`config.process[0].datasets[${i}].control_path_1`,
)
}
options={[{ value: '', label: <>&nbsp;</> }, ...datasetOptions]}
/>
<SelectInput
label="Control Dataset 2"
docKey="datasets.multi_control_paths"
value={dataset.control_path_2 ?? ''}
className="pt-2"
onChange={value =>
setJobConfig(
value == '' ? null : value,
`config.process[0].datasets[${i}].control_path_2`,
)
}
options={[{ value: '', label: <>&nbsp;</> }, ...datasetOptions]}
/>
<SelectInput
label="Control Dataset 3"
docKey="datasets.multi_control_paths"
value={dataset.control_path_3 ?? ''}
className="pt-2"
onChange={value =>
setJobConfig(
value == '' ? null : value,
`config.process[0].datasets[${i}].control_path_3`,
)
}
options={[{ value: '', label: <>&nbsp;</> }, ...datasetOptions]}
/>
</>
)}
<NumberInput
label="LoRA Weight"
value={dataset.network_weight}
@@ -1062,30 +1058,43 @@ export default function SimpleJob({
/>
</div>
</div>
{modelArch?.additionalSections?.includes('datasets.multi_control_paths') && (
<FormGroup label="Control Images" className="pt-2 ml-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 mt-2 mt-2">
{['ctrl_img_1', 'ctrl_img_2', 'ctrl_img_3'].map((ctrlKey, ctrl_idx) => (
<SampleControlImage
key={ctrlKey}
instruction={`Add Control Image ${ctrl_idx + 1}`}
className=""
src={sample[ctrlKey as keyof typeof sample] as string}
onNewImageSelected={imagePath => {
if (!imagePath) {
let newSamples = objectCopy(jobConfig.config.process[0].sample.samples);
delete newSamples[i][ctrlKey as keyof typeof sample];
setJobConfig(newSamples, 'config.process[0].sample.samples');
} else {
setJobConfig(imagePath, `config.process[0].sample.samples[${i}].${ctrlKey}`);
}
}}
/>
))}
</div>
</FormGroup>
)}
{modelArch?.additionalSections?.includes('sample.ctrl_img') && (
<div
className="h-14 w-14 mt-2 ml-4 border border-gray-500 flex items-center justify-center rounded cursor-pointer hover:bg-gray-700 transition-colors"
style={{
backgroundImage: sample.ctrl_img
? `url(${`/api/img/${encodeURIComponent(sample.ctrl_img)}`})`
: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
marginBottom: '-1rem',
}}
onClick={() => {
openAddImageModal(imagePath => {
console.log('Selected image path:', imagePath);
if (!imagePath) return;
<SampleControlImage
className="mt-6 ml-4"
src={sample.ctrl_img}
onNewImageSelected={imagePath => {
if (!imagePath) {
let newSamples = objectCopy(jobConfig.config.process[0].sample.samples);
delete newSamples[i].ctrl_img;
setJobConfig(newSamples, 'config.process[0].sample.samples');
} else {
setJobConfig(imagePath, `config.process[0].sample.samples[${i}].ctrl_img`);
});
}
}}
>
{!sample.ctrl_img && (
<div className="text-gray-400 text-xs text-center font-bold">Add Control Image</div>
)}
</div>
/>
)}
</div>
<div className="pb-4"></div>

View File

@@ -2,7 +2,6 @@ import { JobConfig, DatasetConfig, SliderConfig } from '@/types';
export const defaultDatasetConfig: DatasetConfig = {
folder_path: '/path/to/images/folder',
control_path: null,
mask_path: null,
mask_min_value: 0.1,
default_caption: '',

View File

@@ -9,12 +9,15 @@ type DisableableSections =
| 'network.conv'
| 'trigger_word'
| 'train.diff_output_preservation'
| 'train.unload_text_encoder'
| 'slider';
type AdditionalSections =
| 'datasets.control_path'
| 'datasets.multi_control_paths'
| 'datasets.do_i2v'
| 'sample.ctrl_img'
| 'sample.multi_ctrl_imgs'
| 'datasets.num_frames'
| 'model.multistage'
| 'model.low_vram';
@@ -335,6 +338,28 @@ export const modelArchs: ModelArch[] = [
'3 bit with ARA': 'uint3|ostris/accuracy_recovery_adapters/qwen_image_edit_torchao_uint3.safetensors',
},
},
{
name: 'qwen_image_edit_plus',
label: 'Qwen-Image-Edit-2509',
group: 'instruction',
defaults: {
// default updates when [selected, unselected] in the UI
'config.process[0].model.name_or_path': ['Qwen/Qwen-Image-Edit-2509', defaultNameOrPath],
'config.process[0].model.quantize': [true, false],
'config.process[0].model.quantize_te': [true, false],
'config.process[0].model.low_vram': [true, false],
'config.process[0].train.unload_text_encoder': [false, false],
'config.process[0].sample.sampler': ['flowmatch', 'flowmatch'],
'config.process[0].train.noise_scheduler': ['flowmatch', 'flowmatch'],
'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
'config.process[0].model.qtype': ['qfloat8', 'qfloat8'],
},
disableSections: ['network.conv', 'train.unload_text_encoder'],
additionalSections: ['datasets.multi_control_paths', 'sample.multi_ctrl_imgs', 'model.low_vram'],
accuracyRecoveryAdapters: {
'3 bit with ARA': 'uint3|ostris/accuracy_recovery_adapters/qwen_image_edit_2509_torchao_uint3.safetensors',
},
},
{
name: 'hidream',
label: 'HiDream',

View File

@@ -0,0 +1,105 @@
import { GroupedSelectOption, JobConfig, SelectOption } from '@/types';
import { modelArchs, ModelArch } from './options';
import { objectCopy } from '@/utils/basic';
export const handleModelArchChange = (
currentArchName: string,
newArchName: string,
jobConfig: JobConfig,
setJobConfig: (value: any, key: string) => void,
) => {
const currentArch = modelArchs.find(a => a.name === currentArchName);
if (!currentArch || currentArch.name === newArchName) {
return;
}
// update the defaults when a model is selected
const newArch = modelArchs.find(model => model.name === newArchName);
// update vram setting
if (!newArch?.additionalSections?.includes('model.low_vram')) {
setJobConfig(false, 'config.process[0].model.low_vram');
}
// revert defaults from previous model
for (const key in currentArch.defaults) {
setJobConfig(currentArch.defaults[key][1], key);
}
if (newArch?.defaults) {
for (const key in newArch.defaults) {
setJobConfig(newArch.defaults[key][0], key);
}
}
// set new model
setJobConfig(newArchName, 'config.process[0].model.arch');
// update datasets
const hasControlPath = newArch?.additionalSections?.includes('datasets.control_path') || false;
const hasMultiControlPaths = newArch?.additionalSections?.includes('datasets.multi_control_paths') || false;
const hasNumFrames = newArch?.additionalSections?.includes('datasets.num_frames') || false;
const controls = newArch?.controls ?? [];
const datasets = jobConfig.config.process[0].datasets.map(dataset => {
const newDataset = objectCopy(dataset);
newDataset.controls = controls;
if (hasMultiControlPaths) {
// make sure the config has the multi control paths
newDataset.control_path_1 = newDataset.control_path_1 || null;
newDataset.control_path_2 = newDataset.control_path_2 || null;
newDataset.control_path_3 = newDataset.control_path_3 || null;
// if we previously had a single control path and now
// we selected a multi control model
if (newDataset.control_path && newDataset.control_path !== '') {
// only set if not overwriting
if (!newDataset.control_path_1) {
newDataset.control_path_1 = newDataset.control_path;
}
}
delete newDataset.control_path; // remove single control path
} else if (hasControlPath) {
newDataset.control_path = newDataset.control_path || null;
if (newDataset.control_path_1 && newDataset.control_path_1 !== '') {
newDataset.control_path = newDataset.control_path_1;
}
if (newDataset.control_path_1) {
delete newDataset.control_path_1;
}
if (newDataset.control_path_2) {
delete newDataset.control_path_2;
}
if (newDataset.control_path_3) {
delete newDataset.control_path_3;
}
} else {
// does not have control images
if (newDataset.control_path) {
delete newDataset.control_path;
}
if (newDataset.control_path_1) {
delete newDataset.control_path_1;
}
if (newDataset.control_path_2) {
delete newDataset.control_path_2;
}
if (newDataset.control_path_3) {
delete newDataset.control_path_3;
}
}
if (!hasNumFrames) {
newDataset.num_frames = 1; // reset num_frames if not applicable
}
return newDataset;
});
setJobConfig(datasets, 'config.process[0].datasets');
// update samples
const hasSampleCtrlImg = newArch?.additionalSections?.includes('sample.ctrl_img') || false;
const samples = jobConfig.config.process[0].sample.samples.map(sample => {
const newSample = objectCopy(sample);
if (!hasSampleCtrlImg) {
delete newSample.ctrl_img; // remove ctrl_img if not applicable
}
return newSample;
});
setJobConfig(samples, 'config.process[0].sample.samples');
};

View File

@@ -0,0 +1,206 @@
'use client';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { useDropzone } from 'react-dropzone';
import { FaUpload, FaImage, FaTimes } from 'react-icons/fa';
import { apiClient } from '@/utils/api';
import type { AxiosProgressEvent } from 'axios';
interface Props {
src: string | null | undefined;
className?: string;
instruction?: string;
onNewImageSelected: (imagePath: string | null) => void;
}
export default function SampleControlImage({
src,
className,
instruction = 'Add Control Image',
onNewImageSelected,
}: Props) {
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [localPreview, setLocalPreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const backgroundUrl = useMemo(() => {
if (localPreview) return localPreview;
if (src) return `/api/img/${encodeURIComponent(src)}`;
return null;
}, [src, localPreview]);
const handleUpload = useCallback(
async (file: File) => {
if (!file) return;
setIsUploading(true);
setUploadProgress(0);
const objectUrl = URL.createObjectURL(file);
setLocalPreview(objectUrl);
const formData = new FormData();
formData.append('files', file);
try {
const resp = await apiClient.post(`/api/img/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (evt: AxiosProgressEvent) => {
const total = evt.total ?? 100;
const loaded = evt.loaded ?? 0;
setUploadProgress(Math.round((loaded * 100) / total));
},
timeout: 0,
});
const uploaded = resp?.data?.files?.[0] ?? null;
onNewImageSelected(uploaded);
} catch (err) {
console.error('Upload failed:', err);
setLocalPreview(null);
} finally {
setIsUploading(false);
setUploadProgress(0);
URL.revokeObjectURL(objectUrl);
if (fileInputRef.current) fileInputRef.current.value = '';
}
},
[onNewImageSelected],
);
const onDrop = useCallback(
(acceptedFiles: File[]) => {
if (acceptedFiles.length === 0) return;
handleUpload(acceptedFiles[0]);
},
[handleUpload],
);
const clearImage = useCallback(
(e?: React.MouseEvent) => {
console.log('clearImage');
if (e) {
e.stopPropagation();
e.preventDefault();
}
setLocalPreview(null);
onNewImageSelected(null);
if (fileInputRef.current) fileInputRef.current.value = '';
},
[onNewImageSelected],
);
// Drag & drop only; click handled via our own hidden input
const { getRootProps, isDragActive } = useDropzone({
onDrop,
accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'] },
multiple: false,
noClick: true,
noKeyboard: true,
});
const rootProps = getRootProps();
return (
<div
{...rootProps}
className={classNames(
'group relative flex items-center justify-center rounded-xl cursor-pointer ring-1 ring-inset',
'transition-all duration-200 select-none overflow-hidden text-center',
'h-20 w-20',
backgroundUrl ? 'bg-gray-800 ring-gray-700' : 'bg-gradient-to-b from-gray-800 to-gray-900 ring-gray-700',
isDragActive ? 'outline outline-2 outline-blue-500' : 'hover:ring-gray-600',
className,
)}
style={
backgroundUrl
? {
backgroundImage: `url("${backgroundUrl}")`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}
: undefined
}
onClick={() => !isUploading && fileInputRef.current?.click()}
>
{/* Hidden input for click-to-open */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={e => {
const file = e.currentTarget.files?.[0];
if (file) handleUpload(file);
}}
/>
{/* Empty state — centered */}
{!backgroundUrl && (
<div className="flex flex-col items-center justify-center text-gray-300 text-center">
<FaImage className="opacity-80" />
<div className="mt-1 text-[10px] font-semibold tracking-wide opacity-80">{instruction}</div>
<div className="mt-0.5 text-[9px] opacity-60">Click or drop</div>
</div>
)}
{/* Existing image overlays */}
{backgroundUrl && !isUploading && (
<>
<div
className={classNames(
'pointer-events-none absolute inset-0 flex items-center justify-center',
'bg-black/0 group-hover:bg-black/20',
isDragActive && 'bg-black/35',
'transition-colors',
)}
>
<div
className={classNames(
'inline-flex items-center gap-1 rounded-md px-2 py-1',
'text-[10px] font-semibold',
'bg-black/45 text-white/90 backdrop-blur-sm',
'opacity-0 group-hover:opacity-100 transition-opacity',
)}
>
<FaUpload className="text-[10px]" />
<span>Replace</span>
</div>
</div>
{/* Clear (X) button */}
<button
type="button"
onClick={clearImage}
title="Clear image"
aria-label="Clear image"
className={classNames(
'absolute right-1.5 top-1.5 z-10 inline-flex items-center justify-center',
'h-5 w-5 rounded-md bg-black/55 text-white/90',
'opacity-0 group-hover:opacity-100 transition-opacity',
'hover:bg-black/70',
)}
>
<FaTimes className="text-[10px]" />
</button>
</>
)}
{/* Uploading overlay */}
{isUploading && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/60 backdrop-blur-[1px] text-center">
<div className="w-4/5 max-w-40">
<div className="h-1.5 w-full rounded-full bg-white/15">
<div
className="h-1.5 rounded-full bg-white/80 transition-[width]"
style={{ width: `${uploadProgress}%` }}
/>
</div>
<div className="mt-1 text-[10px] font-medium text-white/90">Uploading {uploadProgress}%</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -53,10 +53,26 @@ const docs: { [key: string]: ConfigDoc } = {
},
'datasets.control_path': {
title: 'Control Dataset',
description: (
<>
The control dataset needs to have files that match the filenames of your training dataset. They should be
matching file pairs. These images are fed as control/input images during training. The control images will be
resized to match the training images.
</>
),
},
'datasets.multi_control_paths': {
title: 'Multi Control Dataset',
description: (
<>
The control dataset needs to have files that match the filenames of your training dataset. They should be
matching file pairs. These images are fed as control/input images during training.
<br />
<br />
For multi control datasets, the controls will all be applied in the order they are listed. If the model does not
require the images to be the same aspect ratios, such as with Qwen/Qwen-Image-Edit-2509, then the control images
do not need to match the aspect size or aspect ratio of the target image and they will be automatically resized to
the ideal resolutions for the model / target images.
</>
),
},

View File

@@ -83,12 +83,15 @@ export interface DatasetConfig {
cache_latents_to_disk?: boolean;
resolution: number[];
controls: string[];
control_path: string | null;
control_path?: string | null;
num_frames: number;
shrink_video_to_frames: boolean;
do_i2v: boolean;
flip_x: boolean;
flip_y: boolean;
control_path_1?: string | null;
control_path_2?: string | null;
control_path_3?: string | null;
}
export interface EMAConfig {
@@ -155,6 +158,9 @@ export interface SampleItem {
ctrl_img?: string | null;
ctrl_idx?: number;
network_multiplier?: number;
ctrl_img_1?: string | null;
ctrl_img_2?: string | null;
ctrl_img_3?: string | null;
}
export interface SampleConfig {

View File

@@ -1 +1 @@
VERSION = "0.5.10"
VERSION = "0.6.0"