mirror of
https://github.com/ostris/ai-toolkit.git
synced 2026-04-30 19:21:39 +00:00
Made it easy to add control images to the samples in the UI
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -180,4 +180,5 @@ cython_debug/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
._.DS_Store
|
._.DS_Store
|
||||||
aitk_db.db
|
aitk_db.db
|
||||||
/notes.md
|
/notes.md
|
||||||
|
/data
|
||||||
@@ -425,6 +425,9 @@ Everything else should work the same including layer targeting.
|
|||||||
|
|
||||||
Only larger updates are listed here. There are usually smaller daily updated that are omitted.
|
Only larger updates are listed here. There are usually smaller daily updated that are omitted.
|
||||||
|
|
||||||
|
### Jul 17, 2025
|
||||||
|
- Make it easy to add control images to the samples in the ui
|
||||||
|
|
||||||
### Jul 11, 2025
|
### Jul 11, 2025
|
||||||
- Added better video config settings to the UI for video models.
|
- Added better video config settings to the UI for video models.
|
||||||
- Added Wan I2V training to the UI
|
- Added Wan I2V training to the UI
|
||||||
|
|||||||
13
ui/package-lock.json
generated
13
ui/package-lock.json
generated
@@ -24,6 +24,7 @@
|
|||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-select": "^5.10.1",
|
"react-select": "^5.10.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"yaml": "^2.7.0"
|
"yaml": "^2.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -5370,6 +5371,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-select": "^5.10.1",
|
"react-select": "^5.10.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"yaml": "^2.7.0"
|
"yaml": "^2.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getDatasetsRoot, getTrainingFolder } from '@/server/settings';
|
import { getDatasetsRoot, getTrainingFolder, getDataRoot } from '@/server/settings';
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: { params: { imagePath: string } }) {
|
export async function GET(request: NextRequest, { params }: { params: { imagePath: string } }) {
|
||||||
const { imagePath } = await params;
|
const { imagePath } = await params;
|
||||||
@@ -13,8 +13,9 @@ export async function GET(request: NextRequest, { params }: { params: { imagePat
|
|||||||
// Get allowed directories
|
// Get allowed directories
|
||||||
const datasetRoot = await getDatasetsRoot();
|
const datasetRoot = await getDatasetsRoot();
|
||||||
const trainingRoot = await getTrainingFolder();
|
const trainingRoot = await getTrainingFolder();
|
||||||
|
const dataRoot = await getDataRoot();
|
||||||
|
|
||||||
const allowedDirs = [datasetRoot, trainingRoot];
|
const allowedDirs = [datasetRoot, trainingRoot, dataRoot];
|
||||||
|
|
||||||
// Security check: Ensure path is in allowed directory
|
// Security check: Ensure path is in allowed directory
|
||||||
const isAllowed = allowedDirs.some(allowedDir => filepath.startsWith(allowedDir)) && !filepath.includes('..');
|
const isAllowed = allowedDirs.some(allowedDir => filepath.startsWith(allowedDir)) && !filepath.includes('..');
|
||||||
|
|||||||
58
ui/src/app/api/img/upload/route.ts
Normal file
58
ui/src/app/api/img/upload/route.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// src/app/api/datasets/upload/route.ts
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { writeFile, mkdir } from 'fs/promises';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { getDataRoot } from '@/server/settings';
|
||||||
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const dataRoot = await getDataRoot();
|
||||||
|
if (!dataRoot) {
|
||||||
|
return NextResponse.json({ error: 'Data root path not found' }, { status: 500 });
|
||||||
|
}
|
||||||
|
const imgRoot = join(dataRoot, 'images');
|
||||||
|
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const files = formData.getAll('files');
|
||||||
|
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'No files provided' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it recursive if it doesn't exist
|
||||||
|
await mkdir(imgRoot, { recursive: true });
|
||||||
|
const savedFiles = await Promise.all(
|
||||||
|
files.map(async (file: any) => {
|
||||||
|
const bytes = await file.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(bytes);
|
||||||
|
|
||||||
|
const extension = file.name.split('.').pop() || 'jpg';
|
||||||
|
|
||||||
|
// Clean filename and ensure it's unique
|
||||||
|
const fileName = `${uuidv4()}`; // Use UUID for unique file names
|
||||||
|
const filePath = join(imgRoot, `${fileName}.${extension}`);
|
||||||
|
|
||||||
|
await writeFile(filePath, buffer);
|
||||||
|
return filePath;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Files uploaded successfully',
|
||||||
|
files: savedFiles,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
return NextResponse.json({ error: 'Error uploading files' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase payload size limit (default is 4mb)
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
responseLimit: '50mb',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import YAML from 'yaml';
|
|||||||
import Editor, { OnMount } from '@monaco-editor/react';
|
import Editor, { OnMount } from '@monaco-editor/react';
|
||||||
import type { editor } from 'monaco-editor';
|
import type { editor } from 'monaco-editor';
|
||||||
import { Settings } from '@/hooks/useSettings';
|
import { Settings } from '@/hooks/useSettings';
|
||||||
|
import { migrateJobConfig } from './jobConfig';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
jobConfig: JobConfig;
|
jobConfig: JobConfig;
|
||||||
@@ -115,6 +116,7 @@ export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
|
migrateJobConfig(parsed);
|
||||||
setJobConfig(parsed);
|
setJobConfig(parsed);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { objectCopy } from '@/utils/basic';
|
|||||||
import { TextInput, SelectInput, Checkbox, FormGroup, NumberInput } from '@/components/formInputs';
|
import { TextInput, SelectInput, Checkbox, FormGroup, NumberInput } from '@/components/formInputs';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
import AddSingleImageModal, { openAddImageModal } from '@/components/AddSingleImageModal';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
jobConfig: JobConfig;
|
jobConfig: JobConfig;
|
||||||
@@ -116,6 +117,17 @@ export default function SimpleJob({
|
|||||||
return newDataset;
|
return newDataset;
|
||||||
});
|
});
|
||||||
setJobConfig(datasets, 'config.process[0].datasets');
|
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');
|
||||||
}}
|
}}
|
||||||
options={groupedModelOptions}
|
options={groupedModelOptions}
|
||||||
/>
|
/>
|
||||||
@@ -648,32 +660,58 @@ export default function SimpleJob({
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormGroup label={`Sample Prompts (${jobConfig.config.process[0].sample.prompts.length})`} className="pt-2">
|
<FormGroup label={`Sample Prompts (${jobConfig.config.process[0].sample.samples.length})`} className="pt-2">
|
||||||
{modelArch?.additionalSections?.includes('sample.ctrl_img') && (
|
<div></div>
|
||||||
<div className="text-sm text-gray-100 mb-2 py-2 px-4 bg-yellow-700 rounded-lg">
|
</FormGroup>
|
||||||
<p className="font-semibold mb-1">Control Images</p>
|
{jobConfig.config.process[0].sample.samples.map((sample, i) => (
|
||||||
To use control images on samples, add --ctrl_img to the prompts below.
|
<div key={i} className="rounded-lg pl-4 pr-1 mb-4 bg-gray-950">
|
||||||
<br />
|
<div className="flex items-center space-x-2">
|
||||||
Example: <code className="bg-yellow-900 p-1">make this a cartoon --ctrl_img /path/to/image.png</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{jobConfig.config.process[0].sample.prompts.map((prompt, i) => (
|
|
||||||
<div key={i} className="flex items-center space-x-2">
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<TextInput
|
<div className="flex">
|
||||||
value={prompt}
|
<div className="flex-1">
|
||||||
onChange={value => setJobConfig(value, `config.process[0].sample.prompts[${i}]`)}
|
<TextInput
|
||||||
placeholder="Enter prompt"
|
label={`Prompt`}
|
||||||
required
|
value={sample.prompt}
|
||||||
/>
|
onChange={value => setJobConfig(value, `config.process[0].sample.samples[${i}].prompt`)}
|
||||||
|
placeholder="Enter prompt"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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;
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setJobConfig(
|
setJobConfig(
|
||||||
jobConfig.config.process[0].sample.prompts.filter((_, index) => index !== i),
|
jobConfig.config.process[0].sample.samples.filter((_, index) => index !== i),
|
||||||
'config.process[0].sample.prompts',
|
'config.process[0].sample.samples',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="rounded-full p-1 text-sm"
|
className="rounded-full p-1 text-sm"
|
||||||
@@ -682,23 +720,27 @@ export default function SimpleJob({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
<button
|
))}
|
||||||
type="button"
|
<button
|
||||||
onClick={() =>
|
type="button"
|
||||||
setJobConfig([...jobConfig.config.process[0].sample.prompts, ''], 'config.process[0].sample.prompts')
|
onClick={() =>
|
||||||
}
|
setJobConfig(
|
||||||
className="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
[...jobConfig.config.process[0].sample.samples, { prompt: '' }],
|
||||||
>
|
'config.process[0].sample.samples',
|
||||||
Add Prompt
|
)
|
||||||
</button>
|
}
|
||||||
</FormGroup>
|
className="w-full px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Add Prompt
|
||||||
|
</button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status === 'success' && <p className="text-green-500 text-center">Training saved successfully!</p>}
|
{status === 'success' && <p className="text-green-500 text-center">Training saved successfully!</p>}
|
||||||
{status === 'error' && <p className="text-red-500 text-center">Error saving training. Please try again.</p>}
|
{status === 'error' && <p className="text-red-500 text-center">Error saving training. Please try again.</p>}
|
||||||
</form>
|
</form>
|
||||||
|
<AddSingleImageModal />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,17 +90,37 @@ export const defaultJobConfig: JobConfig = {
|
|||||||
sample_every: 250,
|
sample_every: 250,
|
||||||
width: 1024,
|
width: 1024,
|
||||||
height: 1024,
|
height: 1024,
|
||||||
prompts: [
|
samples: [
|
||||||
'woman with red hair, playing chess at the park, bomb going off in the background',
|
{
|
||||||
'a woman holding a coffee cup, in a beanie, sitting at a cafe',
|
prompt: 'woman with red hair, playing chess at the park, bomb going off in the background'
|
||||||
'a horse is a DJ at a night club, fish eye lens, smoke machine, lazer lights, holding a martini',
|
},
|
||||||
'a man showing off his cool new t shirt at the beach, a shark is jumping out of the water in the background',
|
{
|
||||||
'a bear building a log cabin in the snow covered mountains',
|
prompt: 'a woman holding a coffee cup, in a beanie, sitting at a cafe',
|
||||||
'woman playing the guitar, on stage, singing a song, laser lights, punk rocker',
|
},
|
||||||
'hipster man with a beard, building a chair, in a wood shop',
|
{
|
||||||
'photo of a man, white background, medium shot, modeling clothing, studio lighting, white backdrop',
|
prompt: 'a horse is a DJ at a night club, fish eye lens, smoke machine, lazer lights, holding a martini',
|
||||||
"a man holding a sign that says, 'this is a sign'",
|
},
|
||||||
'a bulldog, in a post apocalyptic world, with a shotgun, in a leather jacket, in a desert, with a motorcycle',
|
{
|
||||||
|
prompt: 'a man showing off his cool new t shirt at the beach, a shark is jumping out of the water in the background',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: 'a bear building a log cabin in the snow covered mountains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: 'woman playing the guitar, on stage, singing a song, laser lights, punk rocker',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: 'hipster man with a beard, building a chair, in a wood shop',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: 'photo of a man, white background, medium shot, modeling clothing, studio lighting, white backdrop',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: "a man holding a sign that says, 'this is a sign'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: 'a bulldog, in a post apocalyptic world, with a shotgun, in a leather jacket, in a desert, with a motorcycle',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
neg: '',
|
neg: '',
|
||||||
seed: 42,
|
seed: 42,
|
||||||
@@ -118,3 +138,23 @@ export const defaultJobConfig: JobConfig = {
|
|||||||
version: '1.0',
|
version: '1.0',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const migrateJobConfig = (jobConfig: JobConfig): JobConfig => {
|
||||||
|
// upgrade prompt strings to samples
|
||||||
|
if (
|
||||||
|
jobConfig?.config?.process &&
|
||||||
|
jobConfig.config.process[0]?.sample &&
|
||||||
|
Array.isArray(jobConfig.config.process[0].sample.prompts) &&
|
||||||
|
jobConfig.config.process[0].sample.prompts.length > 0
|
||||||
|
) {
|
||||||
|
let newSamples = [];
|
||||||
|
for (const prompt of jobConfig.config.process[0].sample.prompts) {
|
||||||
|
newSamples.push({
|
||||||
|
prompt: prompt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
jobConfig.config.process[0].sample.samples = newSamples;
|
||||||
|
delete jobConfig.config.process[0].sample.prompts;
|
||||||
|
}
|
||||||
|
return jobConfig;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import { defaultJobConfig, defaultDatasetConfig } from './jobConfig';
|
import { defaultJobConfig, defaultDatasetConfig, migrateJobConfig } from './jobConfig';
|
||||||
import { JobConfig } from '@/types';
|
import { JobConfig } from '@/types';
|
||||||
import { objectCopy } from '@/utils/basic';
|
import { objectCopy } from '@/utils/basic';
|
||||||
import { useNestedState } from '@/utils/hooks';
|
import { useNestedState } from '@/utils/hooks';
|
||||||
import { SelectInput} from '@/components/formInputs';
|
import { SelectInput } from '@/components/formInputs';
|
||||||
import useSettings from '@/hooks/useSettings';
|
import useSettings from '@/hooks/useSettings';
|
||||||
import useGPUInfo from '@/hooks/useGPUInfo';
|
import useGPUInfo from '@/hooks/useGPUInfo';
|
||||||
import useDatasetList from '@/hooks/useDatasetList';
|
import useDatasetList from '@/hooks/useDatasetList';
|
||||||
@@ -61,7 +61,7 @@ export default function TrainingForm() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
console.log('Training:', data);
|
console.log('Training:', data);
|
||||||
setGpuIDs(data.gpu_ids);
|
setGpuIDs(data.gpu_ids);
|
||||||
setJobConfig(JSON.parse(data.job_config));
|
setJobConfig(migrateJobConfig(JSON.parse(data.job_config)));
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching training:', error));
|
.catch(error => console.error('Error fetching training:', error));
|
||||||
}
|
}
|
||||||
@@ -181,11 +181,13 @@ export default function TrainingForm() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MainContent>
|
<MainContent>
|
||||||
<ErrorBoundary fallback={
|
<ErrorBoundary
|
||||||
<div className="flex items-center justify-center h-64 text-lg text-red-600 font-medium bg-red-100 dark:bg-red-900/20 dark:text-red-400 border border-red-300 dark:border-red-700 rounded-lg">
|
fallback={
|
||||||
Advanced job detected. Please switch to advanced view to continue.
|
<div className="flex items-center justify-center h-64 text-lg text-red-600 font-medium bg-red-100 dark:bg-red-900/20 dark:text-red-400 border border-red-300 dark:border-red-700 rounded-lg">
|
||||||
</div>
|
Advanced job detected. Please switch to advanced view to continue.
|
||||||
}>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SimpleJob
|
<SimpleJob
|
||||||
jobConfig={jobConfig}
|
jobConfig={jobConfig}
|
||||||
setJobConfig={setJobConfig}
|
setJobConfig={setJobConfig}
|
||||||
@@ -204,4 +206,4 @@ export default function TrainingForm() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
141
ui/src/components/AddSingleImageModal.tsx
Normal file
141
ui/src/components/AddSingleImageModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
'use client';
|
||||||
|
import { createGlobalState } from 'react-global-hooks';
|
||||||
|
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
|
||||||
|
import { FaUpload } from 'react-icons/fa';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import { apiClient } from '@/utils/api';
|
||||||
|
|
||||||
|
export interface AddSingleImageModalState {
|
||||||
|
|
||||||
|
onComplete?: (imagePath: string|null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addSingleImageModalState = createGlobalState<AddSingleImageModalState | null>(null);
|
||||||
|
|
||||||
|
export const openAddImageModal = (onComplete: (imagePath: string|null) => void) => {
|
||||||
|
addSingleImageModalState.set({onComplete });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AddSingleImageModal() {
|
||||||
|
const [addSingleImageModalInfo, setAddSingleImageModalInfo] = addSingleImageModalState.use();
|
||||||
|
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||||
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
|
const open = addSingleImageModalInfo !== null;
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
if (!isUploading) {
|
||||||
|
setAddSingleImageModalInfo(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDone = (imagePath: string|null) => {
|
||||||
|
if (addSingleImageModalInfo?.onComplete && !isUploading) {
|
||||||
|
addSingleImageModalInfo.onComplete(imagePath);
|
||||||
|
setAddSingleImageModalInfo(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
async (acceptedFiles: File[]) => {
|
||||||
|
if (acceptedFiles.length === 0) return;
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
acceptedFiles.forEach(file => {
|
||||||
|
formData.append('files', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await apiClient.post(`/api/img/upload`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
onUploadProgress: progressEvent => {
|
||||||
|
const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 100));
|
||||||
|
setUploadProgress(percentCompleted);
|
||||||
|
},
|
||||||
|
timeout: 0, // Disable timeout
|
||||||
|
});
|
||||||
|
console.log('Upload successful:', resp.data);
|
||||||
|
|
||||||
|
onDone(resp.data.files[0] || null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error);
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadProgress(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addSingleImageModalInfo],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
|
||||||
|
},
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} className="relative z-10">
|
||||||
|
<DialogBackdrop
|
||||||
|
transition
|
||||||
|
className="fixed inset-0 bg-gray-900/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<DialogPanel
|
||||||
|
transition
|
||||||
|
className="relative transform overflow-hidden rounded-lg bg-gray-800 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-lg data-closed:sm:translate-y-0 data-closed:sm:scale-95"
|
||||||
|
>
|
||||||
|
<div className="bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<DialogTitle as="h3" className="text-base font-semibold text-gray-200 mb-4">
|
||||||
|
Add Control Image
|
||||||
|
</DialogTitle>
|
||||||
|
<div className="w-full">
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={`h-40 w-full flex flex-col items-center justify-center border-2 border-dashed rounded-lg cursor-pointer transition-colors duration-200
|
||||||
|
${isDragActive ? 'border-blue-500 bg-blue-50/10' : 'border-gray-600'}`}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<FaUpload className="size-8 mb-3 text-gray-400" />
|
||||||
|
<p className="text-sm text-gray-200 text-center">
|
||||||
|
{isDragActive ? 'Drop the image here...' : 'Drag & drop an image here, or click to select one'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{isUploading && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="w-full bg-gray-700 rounded-full h-2.5">
|
||||||
|
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${uploadProgress}%` }}></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300 mt-2 text-center">Uploading... {uploadProgress}%</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-700 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-autofocus
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isUploading}
|
||||||
|
className={`mt-3 inline-flex w-full justify-center rounded-md bg-gray-800 px-3 py-2 text-sm font-semibold text-gray-200 hover:bg-gray-800 sm:mt-0 sm:w-auto ring-0
|
||||||
|
${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,11 @@ export default function SampleImages({ job }: SampleImagesProps) {
|
|||||||
if (job?.job_config) {
|
if (job?.job_config) {
|
||||||
const jobConfig = JSON.parse(job.job_config) as JobConfig;
|
const jobConfig = JSON.parse(job.job_config) as JobConfig;
|
||||||
const sampleConfig = jobConfig.config.process[0].sample;
|
const sampleConfig = jobConfig.config.process[0].sample;
|
||||||
return sampleConfig.prompts.length;
|
if (sampleConfig.prompts) {
|
||||||
|
return sampleConfig.prompts.length;
|
||||||
|
} else {
|
||||||
|
return sampleConfig.samples.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 10;
|
return 10;
|
||||||
}, [job]);
|
}, [job]);
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ import path from 'path';
|
|||||||
export const TOOLKIT_ROOT = path.resolve('@', '..', '..');
|
export const TOOLKIT_ROOT = path.resolve('@', '..', '..');
|
||||||
export const defaultTrainFolder = path.join(TOOLKIT_ROOT, 'output');
|
export const defaultTrainFolder = path.join(TOOLKIT_ROOT, 'output');
|
||||||
export const defaultDatasetsFolder = path.join(TOOLKIT_ROOT, 'datasets');
|
export const defaultDatasetsFolder = path.join(TOOLKIT_ROOT, 'datasets');
|
||||||
|
export const defaultDataRoot = path.join(TOOLKIT_ROOT, 'data');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { defaultDatasetsFolder } from '@/paths';
|
import { defaultDatasetsFolder, defaultDataRoot } from '@/paths';
|
||||||
import { defaultTrainFolder } from '@/paths';
|
import { defaultTrainFolder } from '@/paths';
|
||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
|
|
||||||
@@ -66,3 +66,22 @@ export const getHFToken = async () => {
|
|||||||
myCache.set(key, token);
|
myCache.set(key, token);
|
||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDataRoot = async () => {
|
||||||
|
const key = 'DATA_ROOT';
|
||||||
|
let dataRoot = myCache.get(key) as string;
|
||||||
|
if (dataRoot) {
|
||||||
|
return dataRoot;
|
||||||
|
}
|
||||||
|
let row = await prisma.settings.findFirst({
|
||||||
|
where: {
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dataRoot = defaultDataRoot;
|
||||||
|
if (row?.value && row.value !== '') {
|
||||||
|
dataRoot = row.value;
|
||||||
|
}
|
||||||
|
myCache.set(key, dataRoot);
|
||||||
|
return dataRoot;
|
||||||
|
};
|
||||||
|
|||||||
@@ -133,12 +133,27 @@ export interface ModelConfig {
|
|||||||
model_kwargs: { [key: string]: any };
|
model_kwargs: { [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SampleItem {
|
||||||
|
prompt: string;
|
||||||
|
width?: number
|
||||||
|
height?: number;
|
||||||
|
neg?: string;
|
||||||
|
seed?: number;
|
||||||
|
guidance_scale?: number;
|
||||||
|
sample_steps?: number;
|
||||||
|
fps?: number;
|
||||||
|
num_frames?: number;
|
||||||
|
ctrl_img?: string | null;
|
||||||
|
ctrl_idx?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SampleConfig {
|
export interface SampleConfig {
|
||||||
sampler: string;
|
sampler: string;
|
||||||
sample_every: number;
|
sample_every: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
prompts: string[];
|
prompts?: string[];
|
||||||
|
samples: SampleItem[];
|
||||||
neg: string;
|
neg: string;
|
||||||
seed: number;
|
seed: number;
|
||||||
walk_seed: boolean;
|
walk_seed: boolean;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VERSION = "0.3.7"
|
VERSION = "0.3.8"
|
||||||
Reference in New Issue
Block a user