Add support for ACE-Step 1.5 and ACE-Step 1.5 XL. Also added dataset captioning through the UI. (#785)
* Base ace step 1.5 xl added. Generating, still wip on training and ui * Base training code done * Fix some issues with caching text embeddings. Update sample cards to show audio * Fix issue with quantizing ace step * Add album artwork to samples with waveform. * Cleanup logs * Add album art endpoint to speed up album art loading * Made an make video with artwork script * Make ui handle basic audio models. Make multi line adjustments to the editor and better syntax hilighting. * Add prompt tagging system for special tagged models. * prompt tagging processing for ui working. * Moved default samples to a special file so we can add more when needed and they can be adjusted for a specific model * Add a captioner job with music captioner that is prepped for use with the ui * Add basit ui setup for captioning modal and handeling captioning jobs * Starting captioning job from ui working. Still better management for it. * Better filtering of job options in the job view for captioning jobs * Added qwen3 vl as a captioner for images * Have an indicator when a dataset is being captioned. * Adjust the way caption jobs look in the queue * Fix a few issues. Adjust defaults. * Version bump * Added ace step to the readme.
This commit is contained in:
184
ui/src/app/api/audio/art/[...audioPath]/route.ts
Normal file
184
ui/src/app/api/audio/art/[...audioPath]/route.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/* eslint-disable */
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getDatasetsRoot, getTrainingFolder, getDataRoot } from '@/server/settings';
|
||||
|
||||
/**
|
||||
* Serves embedded album art from an MP3 file's ID3v2 tag.
|
||||
* Reads only the tag header from disk (no full-file buffering).
|
||||
* Returns the raw image bytes with correct Content-Type.
|
||||
*/
|
||||
|
||||
function synchsafeToInt(b0: number, b1: number, b2: number, b3: number) {
|
||||
return ((b0 & 0x7f) << 21) | ((b1 & 0x7f) << 14) | ((b2 & 0x7f) << 7) | (b3 & 0x7f);
|
||||
}
|
||||
|
||||
function deUnsync(bytes: Buffer) {
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
out.push(bytes[i]);
|
||||
if (bytes[i] === 0xff && i + 1 < bytes.length && bytes[i + 1] === 0x00) i += 1;
|
||||
}
|
||||
return Buffer.from(out);
|
||||
}
|
||||
|
||||
function readNullTerminated(buf: Buffer, start: number, wide: boolean): { text: string; next: number } {
|
||||
if (wide) {
|
||||
let i = start;
|
||||
while (i + 1 < buf.length && !(buf[i] === 0 && buf[i + 1] === 0)) i += 2;
|
||||
return { text: buf.slice(start, i).toString('utf16le'), next: i + 2 };
|
||||
}
|
||||
let i = start;
|
||||
while (i < buf.length && buf[i] !== 0) i++;
|
||||
return { text: buf.slice(start, i).toString('latin1'), next: i + 1 };
|
||||
}
|
||||
|
||||
type ArtResult = { mime: string; data: Buffer } | null;
|
||||
|
||||
function extractArtFromTag(buf: Buffer): ArtResult {
|
||||
if (buf.length < 10) return null;
|
||||
if (buf[0] !== 0x49 || buf[1] !== 0x44 || buf[2] !== 0x33) return null; // "ID3"
|
||||
|
||||
const verMajor = buf[3]; // 2, 3, or 4
|
||||
const flags = buf[5];
|
||||
const tagSize = synchsafeToInt(buf[6], buf[7], buf[8], buf[9]);
|
||||
const tagEnd = Math.min(10 + tagSize, buf.length);
|
||||
|
||||
let tagData = buf.slice(10, tagEnd);
|
||||
if ((flags & 0x80) !== 0) tagData = deUnsync(tagData);
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// Skip extended header
|
||||
if ((verMajor === 3 || verMajor === 4) && (flags & 0x40) !== 0 && tagData.length >= 4) {
|
||||
const extSize =
|
||||
verMajor === 4
|
||||
? synchsafeToInt(tagData[0], tagData[1], tagData[2], tagData[3])
|
||||
: (tagData[0] << 24) | (tagData[1] << 16) | (tagData[2] << 8) | tagData[3];
|
||||
offset += 4 + Math.max(0, extSize);
|
||||
}
|
||||
|
||||
while (offset < tagData.length) {
|
||||
if (tagData[offset] === 0x00) break;
|
||||
|
||||
if (verMajor === 2) {
|
||||
// ID3v2.2: 3-byte frame ID, 3-byte size
|
||||
if (offset + 6 > tagData.length) break;
|
||||
const id = tagData.slice(offset, offset + 3).toString('latin1');
|
||||
const size = (tagData[offset + 3] << 16) | (tagData[offset + 4] << 8) | tagData[offset + 5];
|
||||
offset += 6;
|
||||
if (!id.trim() || size <= 0 || offset + size > tagData.length) break;
|
||||
|
||||
if (id === 'PIC' && size > 6) {
|
||||
const frame = tagData.slice(offset, offset + size);
|
||||
const fmt = frame.slice(1, 4).toString('latin1').toLowerCase();
|
||||
const mime = fmt === 'png' ? 'image/png' : 'image/jpeg';
|
||||
// skip: encoding(1) + format(3) + pictureType(1) = 5, then null-terminated description
|
||||
let p = 5;
|
||||
const enc = frame[0];
|
||||
const wide = enc === 1 || enc === 2;
|
||||
const desc = readNullTerminated(frame as any, p, wide);
|
||||
p = desc.next;
|
||||
if (p < frame.length) {
|
||||
const img = frame.slice(p);
|
||||
if (img.length > 64) return { mime, data: Buffer.from(img) };
|
||||
}
|
||||
}
|
||||
offset += size;
|
||||
} else {
|
||||
// ID3v2.3/v2.4: 4-byte frame ID, 4-byte size, 2-byte flags
|
||||
if (offset + 10 > tagData.length) break;
|
||||
const id = tagData.slice(offset, offset + 4).toString('latin1');
|
||||
let size =
|
||||
verMajor === 4
|
||||
? synchsafeToInt(tagData[offset + 4], tagData[offset + 5], tagData[offset + 6], tagData[offset + 7])
|
||||
: (tagData[offset + 4] << 24) |
|
||||
(tagData[offset + 5] << 16) |
|
||||
(tagData[offset + 6] << 8) |
|
||||
tagData[offset + 7];
|
||||
const flag2 = tagData[offset + 9];
|
||||
offset += 10;
|
||||
if (!id.trim() || size <= 0 || offset + size > tagData.length) break;
|
||||
|
||||
if (id === 'APIC') {
|
||||
let frame = tagData.slice(offset, offset + size);
|
||||
if (verMajor === 4 && (flag2 & 0x02) !== 0) frame = deUnsync(frame);
|
||||
|
||||
const enc = frame[0];
|
||||
// mime type: null-terminated latin1
|
||||
const mimeZ = readNullTerminated(frame as any, 1, false);
|
||||
const mime = mimeZ.text || 'image/jpeg';
|
||||
let p = mimeZ.next;
|
||||
if (p < frame.length) p += 1; // picture type byte
|
||||
const wide = enc === 1 || enc === 2;
|
||||
const desc = readNullTerminated(frame as any, p, wide);
|
||||
p = desc.next;
|
||||
if (p < frame.length) {
|
||||
const img = frame.slice(p);
|
||||
if (img.length > 64) return { mime, data: Buffer.from(img) };
|
||||
}
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: { audioPath: string } }) {
|
||||
const { audioPath } = await params;
|
||||
try {
|
||||
const filepath = decodeURIComponent(audioPath);
|
||||
|
||||
// Security check
|
||||
const datasetRoot = await getDatasetsRoot();
|
||||
const trainingRoot = await getTrainingFolder();
|
||||
const dataRoot = await getDataRoot();
|
||||
const allowedDirs = [datasetRoot, trainingRoot, dataRoot];
|
||||
const isAllowed = allowedDirs.some(d => filepath.startsWith(d)) && !filepath.includes('..');
|
||||
if (!isAllowed) {
|
||||
return new NextResponse('Access denied', { status: 403 });
|
||||
}
|
||||
|
||||
const stat = await fs.promises.stat(filepath).catch(() => null);
|
||||
if (!stat || !stat.isFile()) {
|
||||
return new NextResponse('File not found', { status: 404 });
|
||||
}
|
||||
|
||||
// Read only the ID3 tag (first min(tagSize, 4MB) bytes).
|
||||
// First read 10 bytes to get tag size, then read the full tag.
|
||||
const fd = await fs.promises.open(filepath, 'r');
|
||||
try {
|
||||
const headerBuf = Buffer.alloc(10);
|
||||
await fd.read(headerBuf, 0, 10, 0);
|
||||
|
||||
if (headerBuf[0] !== 0x49 || headerBuf[1] !== 0x44 || headerBuf[2] !== 0x33) {
|
||||
return new NextResponse('No ID3 tag', { status: 404 });
|
||||
}
|
||||
|
||||
const tagSize = synchsafeToInt(headerBuf[6], headerBuf[7], headerBuf[8], headerBuf[9]);
|
||||
const totalRead = Math.min(10 + tagSize, 4_000_000);
|
||||
|
||||
const tagBuf = Buffer.alloc(totalRead);
|
||||
await fd.read(tagBuf, 0, totalRead, 0);
|
||||
|
||||
const art = extractArtFromTag(tagBuf);
|
||||
if (!art) {
|
||||
return new NextResponse('No album art found', { status: 404 });
|
||||
}
|
||||
|
||||
return new NextResponse(art.data, {
|
||||
headers: {
|
||||
'Content-Type': art.mime,
|
||||
'Content-Length': String(art.data.length),
|
||||
'Cache-Control': 'public, max-age=604800, immutable',
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await fd.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error extracting album art:', error);
|
||||
return new NextResponse('Internal Server Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ const prisma = new PrismaClient();
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
const job_ref = searchParams.get('job_ref');
|
||||
const job_type = searchParams.get('job_type');
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
@@ -15,8 +17,16 @@ export async function GET(request: Request) {
|
||||
});
|
||||
return NextResponse.json(job);
|
||||
}
|
||||
if (job_ref) {
|
||||
const job = await prisma.job.findFirst({
|
||||
where: { job_ref },
|
||||
orderBy: { updated_at: 'desc' },
|
||||
});
|
||||
return NextResponse.json(job);
|
||||
}
|
||||
|
||||
const jobs = await prisma.job.findMany({
|
||||
where: job_type ? { job_type } : undefined,
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
return NextResponse.json({ jobs: jobs });
|
||||
@@ -36,6 +46,15 @@ export async function POST(request: Request) {
|
||||
gpu_ids = "mps";
|
||||
}
|
||||
|
||||
const extra: any = {};
|
||||
if ("job_ref" in body) {
|
||||
extra["job_ref"] = body.job_ref;
|
||||
}
|
||||
|
||||
if ("job_type" in body) {
|
||||
extra["job_type"] = body.job_type;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
// Update existing training
|
||||
const training = await prisma.job.update({
|
||||
@@ -44,6 +63,7 @@ export async function POST(request: Request) {
|
||||
name,
|
||||
gpu_ids,
|
||||
job_config: JSON.stringify(job_config),
|
||||
...extra,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(training);
|
||||
@@ -63,6 +83,7 @@ export async function POST(request: Request) {
|
||||
gpu_ids,
|
||||
job_config: JSON.stringify(job_config),
|
||||
queue_position: newQueuePosition,
|
||||
...extra,
|
||||
},
|
||||
});
|
||||
return NextResponse.json(training);
|
||||
|
||||
@@ -8,12 +8,18 @@ import { Button } from '@headlessui/react';
|
||||
import AddImagesModal, { openImagesModal, useOpenImagesModalOnDrag } from '@/components/AddImagesModal';
|
||||
import { TopBar, MainContent } from '@/components/layout';
|
||||
import { apiClient } from '@/utils/api';
|
||||
import { CaptionDatasetModal, openCaptionDatasetModal } from '@/components/CaptionDatasetModal';
|
||||
import useSettings from '@/hooks/useSettings';
|
||||
import { pathJoin } from '@/utils/basic';
|
||||
import AutoCaptionButton from '@/components/AutoCaptionButton';
|
||||
|
||||
export default function DatasetPage({ params }: { params: { datasetName: string } }) {
|
||||
const [imgList, setImgList] = useState<{ img_path: string }[]>([]);
|
||||
const [isAutoCaptioning, setIsAutoCaptioning] = useState(false);
|
||||
const usableParams = use(params as any) as { datasetName: string };
|
||||
const datasetName = usableParams.datasetName;
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const { settings, isSettingsLoaded } = useSettings();
|
||||
|
||||
const refreshImageList = (dbName: string) => {
|
||||
setStatus('loading');
|
||||
@@ -105,6 +111,10 @@ export default function DatasetPage({ params }: { params: { datasetName: string
|
||||
</div>
|
||||
<div className="flex-1"></div>
|
||||
<div>
|
||||
<AutoCaptionButton
|
||||
datasetPath={`${pathJoin(settings.DATASETS_FOLDER, datasetName)}`}
|
||||
setIsAutoCaptioning={setIsAutoCaptioning}
|
||||
/>
|
||||
<Button
|
||||
className="text-white bg-slate-600 px-3 py-1 rounded-md"
|
||||
onClick={() => openImagesModal(datasetName, () => refreshImageList(datasetName))}
|
||||
@@ -121,6 +131,7 @@ export default function DatasetPage({ params }: { params: { datasetName: string
|
||||
<DatasetImageCard
|
||||
key={img.img_path}
|
||||
alt="image"
|
||||
isAutoCaptioning={isAutoCaptioning}
|
||||
imageUrl={img.img_path}
|
||||
onDelete={() => refreshImageList(datasetName)}
|
||||
/>
|
||||
@@ -129,6 +140,7 @@ export default function DatasetPage({ params }: { params: { datasetName: string
|
||||
)}
|
||||
</MainContent>
|
||||
<AddImagesModal />
|
||||
<CaptionDatasetModal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Page {
|
||||
component: React.ComponentType<{ job: Job }>;
|
||||
menuItem?: React.ComponentType<{ job?: Job | null }> | null;
|
||||
mainCss?: string;
|
||||
jobTypes?: string[]; // if specified, only show this page for these job types
|
||||
}
|
||||
|
||||
const pages: Page[] = [
|
||||
@@ -40,6 +41,7 @@ const pages: Page[] = [
|
||||
component: SampleImages,
|
||||
menuItem: SampleImagesMenu,
|
||||
mainCss: 'pt-24',
|
||||
jobTypes: ['train'],
|
||||
},
|
||||
{
|
||||
name: 'Loss Graph',
|
||||
@@ -47,6 +49,7 @@ const pages: Page[] = [
|
||||
icon: MdShowChart,
|
||||
component: JobLossGraph,
|
||||
mainCss: 'pt-24',
|
||||
jobTypes: ['train'],
|
||||
},
|
||||
{
|
||||
name: 'Config File',
|
||||
@@ -65,6 +68,13 @@ export default function JobPage({ params }: { params: { jobID: string } }) {
|
||||
|
||||
const page = pages.find(p => p.value === pageKey);
|
||||
|
||||
const jobType = job?.job_type || 'unknown';
|
||||
|
||||
let title = `Job: ${job?.name || 'Loading...'}`;
|
||||
if (jobType === 'caption') {
|
||||
title = `Captioning: ${job?.job_ref || 'Loading...'}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Fixed top bar */}
|
||||
@@ -75,7 +85,7 @@ export default function JobPage({ params }: { params: { jobID: string } }) {
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg">Job: {job?.name}</h1>
|
||||
<h1 className="text-lg">{title}</h1>
|
||||
</div>
|
||||
<div className="flex-1"></div>
|
||||
{job && (
|
||||
@@ -103,16 +113,21 @@ export default function JobPage({ params }: { params: { jobID: string } }) {
|
||||
)}
|
||||
</MainContent>
|
||||
<div className="bg-gray-800 absolute top-12 left-0 w-full h-8 flex items-center px-2 text-sm">
|
||||
{pages.map(page => (
|
||||
<Button
|
||||
key={page.value}
|
||||
onClick={() => setPageKey(page.value)}
|
||||
className={`px-4 py-1 h-8 flex items-center gap-1.5 ${page.value === pageKey ? 'bg-gray-300 dark:bg-gray-700 text-white' : ''}`}
|
||||
>
|
||||
<page.icon className="text-sm" />
|
||||
{page.name}
|
||||
</Button>
|
||||
))}
|
||||
{pages.map(page => {
|
||||
if (page.jobTypes && !page.jobTypes.includes(jobType)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={page.value}
|
||||
onClick={() => setPageKey(page.value)}
|
||||
className={`px-4 py-1 h-8 flex items-center gap-1.5 ${page.value === pageKey ? 'bg-gray-300 dark:bg-gray-700 text-white' : ''}`}
|
||||
>
|
||||
<page.icon className="text-sm" />
|
||||
{page.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{page?.menuItem && (
|
||||
<>
|
||||
<div className="flex-grow"></div>
|
||||
|
||||
@@ -35,23 +35,38 @@ const yamlConfig: YAML.DocumentOptions &
|
||||
directives: true,
|
||||
};
|
||||
|
||||
function toYaml(obj: any): string {
|
||||
const doc = new YAML.Document(obj, yamlConfig);
|
||||
YAML.visit(doc, {
|
||||
Scalar(_key, node) {
|
||||
if (typeof node.value === 'string' && node.value.includes('\n')) {
|
||||
node.type = YAML.Scalar.BLOCK_LITERAL;
|
||||
}
|
||||
},
|
||||
});
|
||||
return doc.toString(yamlConfig);
|
||||
}
|
||||
|
||||
export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props) {
|
||||
const { theme } = useTheme();
|
||||
const [editorValue, setEditorValue] = useState<string>('');
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const lastJobConfigUpdateStringRef = useRef('');
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
const monacoRef = useRef<any>(null);
|
||||
|
||||
// Track if the editor has been mounted
|
||||
const isEditorMounted = useRef(false);
|
||||
|
||||
// Handler for editor mounting
|
||||
const handleEditorDidMount: OnMount = editor => {
|
||||
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
isEditorMounted.current = true;
|
||||
|
||||
// Initial content setup
|
||||
try {
|
||||
const yamlContent = YAML.stringify(jobConfig, yamlConfig);
|
||||
const yamlContent = toYaml(jobConfig);
|
||||
setEditorValue(yamlContent);
|
||||
lastJobConfigUpdateStringRef.current = JSON.stringify(jobConfig);
|
||||
} catch (e) {
|
||||
@@ -78,7 +93,7 @@ export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props
|
||||
const scrollTop = editor.getScrollTop();
|
||||
|
||||
// Update content
|
||||
const yamlContent = YAML.stringify(jobConfig, yamlConfig);
|
||||
const yamlContent = toYaml(jobConfig);
|
||||
|
||||
// Only update if the content is actually different
|
||||
if (yamlContent !== editor.getValue()) {
|
||||
@@ -98,11 +113,29 @@ export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props
|
||||
}
|
||||
}, [jobConfig]);
|
||||
|
||||
const setMarkers = (errors: { message: string; line: number }[]) => {
|
||||
const monaco = monacoRef.current;
|
||||
const model = editorRef.current?.getModel();
|
||||
if (!monaco || !model) return;
|
||||
const markers = errors.map(err => ({
|
||||
severity: monaco.MarkerSeverity.Error,
|
||||
message: err.message,
|
||||
startLineNumber: err.line,
|
||||
startColumn: 1,
|
||||
endLineNumber: err.line,
|
||||
endColumn: model.getLineMaxColumn(err.line),
|
||||
}));
|
||||
monaco.editor.setModelMarkers(model, 'yaml', markers);
|
||||
};
|
||||
|
||||
const handleChange = (value: string | undefined) => {
|
||||
if (value === undefined) return;
|
||||
|
||||
try {
|
||||
const parsed = YAML.parse(value);
|
||||
setHasError(false);
|
||||
setMarkers([]);
|
||||
|
||||
// Don't update jobConfig if the change came from the editor itself
|
||||
// to avoid a circular update loop
|
||||
if (JSON.stringify(parsed) !== lastJobConfigUpdateStringRef.current) {
|
||||
@@ -121,14 +154,21 @@ export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props
|
||||
migrateJobConfig(parsed);
|
||||
setJobConfig(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't update on parsing errors
|
||||
console.warn(e);
|
||||
} catch (e: any) {
|
||||
setHasError(true);
|
||||
const line = e?.linePos?.[0]?.line ?? e?.linePos?.line ?? 1;
|
||||
setMarkers([{ message: e?.message ?? 'Invalid YAML', line }]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative h-full w-full">
|
||||
{hasError && (
|
||||
<div
|
||||
className="absolute inset-0 z-10 pointer-events-none rounded-sm"
|
||||
style={{ boxShadow: 'inset 0 0 12px 2px rgba(239, 68, 68, 0.5)' }}
|
||||
/>
|
||||
)}
|
||||
<Editor
|
||||
height="100%"
|
||||
width="100%"
|
||||
@@ -143,6 +183,6 @@ export default function AdvancedJob({ jobConfig, setJobConfig, settings }: Props
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,20 @@ import {
|
||||
quantizationOptions,
|
||||
defaultQtype,
|
||||
jobTypeOptions,
|
||||
SampleTags,
|
||||
} from './options';
|
||||
import { defaultDatasetConfig } from './jobConfig';
|
||||
import { GroupedSelectOption, JobConfig, SelectOption } from '@/types';
|
||||
import { objectCopy } from '@/utils/basic';
|
||||
import { TextInput, SelectInput, Checkbox, FormGroup, NumberInput, SliderInput } from '@/components/formInputs';
|
||||
import { objectCopy, tagsToObj, objToTags } from '@/utils/basic';
|
||||
import {
|
||||
TextInput,
|
||||
TextAreaInput,
|
||||
SelectInput,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
NumberInput,
|
||||
SliderInput,
|
||||
} from '@/components/formInputs';
|
||||
import Card from '@/components/Card';
|
||||
import { X, Copy } from 'lucide-react';
|
||||
import AddSingleImageModal, { openAddImageModal } from '@/components/AddSingleImageModal';
|
||||
@@ -68,6 +77,44 @@ export default function SimpleJob({
|
||||
}, [modelArch, jobType]);
|
||||
|
||||
const isVideoModel = !!(modelArch?.group === 'video');
|
||||
const isAudioModel = !!(modelArch?.group === 'audio');
|
||||
|
||||
const taggedSampleArr: Record<string, any>[] | null = useMemo(() => {
|
||||
if (!modelArch) return null;
|
||||
if (!modelArch.sampleTags) return null;
|
||||
if (!jobConfig.config.process[0].sample.samples) return null;
|
||||
let sampleArr: any[] = [];
|
||||
for (let i = 0; i < jobConfig.config.process[0].sample.samples.length; i++) {
|
||||
const taggedPrompt = jobConfig.config.process[0].sample.samples[i].prompt;
|
||||
const tagsObj = tagsToObj(taggedPrompt);
|
||||
sampleArr.push(tagsObj);
|
||||
}
|
||||
return sampleArr;
|
||||
}, [modelArch, jobConfig.config.process[0].sample.samples]);
|
||||
|
||||
const modelArchTagSections: SampleTags[] | null = useMemo(() => {
|
||||
if (!modelArch?.sampleTags) return null;
|
||||
const maxPerGroup = 5;
|
||||
let sections: SampleTags[] = [];
|
||||
let subSection: SampleTags = {};
|
||||
for (const [tagKey, tag] of Object.entries(modelArch.sampleTags)) {
|
||||
if ((tag.full && Object.keys(subSection).length > 0) || Object.keys(subSection).length >= maxPerGroup) {
|
||||
// reset the sub section build if the next tag is full or max per group is reached
|
||||
sections.push(subSection);
|
||||
subSection = {};
|
||||
}
|
||||
subSection[tagKey] = tag;
|
||||
if (tag.full) {
|
||||
// if the tag is full, push the section immediately and reset the sub section build
|
||||
sections.push(subSection);
|
||||
subSection = {};
|
||||
}
|
||||
}
|
||||
if (Object.keys(subSection).length > 0) {
|
||||
sections.push(subSection);
|
||||
}
|
||||
return sections.length > 0 ? sections : null;
|
||||
}, [modelArch]);
|
||||
|
||||
const numTopCards = useMemo(() => {
|
||||
let count = 4; // job settings, model config, target config, save config
|
||||
@@ -149,6 +196,26 @@ export default function SimpleJob({
|
||||
|
||||
const showGPUSelect = !isMac();
|
||||
|
||||
let numDatasetCols = 4;
|
||||
let numSampleTopCols = 4;
|
||||
let datasetStyleClass = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6';
|
||||
let sampleTopStyleClass = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6';
|
||||
if (isVideoModel) {
|
||||
numSampleTopCols += 1;
|
||||
}
|
||||
if (isAudioModel) {
|
||||
numDatasetCols -= 1;
|
||||
numSampleTopCols -= 1;
|
||||
}
|
||||
if (numDatasetCols == 3) {
|
||||
datasetStyleClass = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6';
|
||||
}
|
||||
if (numSampleTopCols == 5) {
|
||||
sampleTopStyleClass = 'grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6';
|
||||
}
|
||||
if (numSampleTopCols == 3) {
|
||||
sampleTopStyleClass = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6';
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
@@ -790,7 +857,7 @@ export default function SimpleJob({
|
||||
</button>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold mb-4">Dataset {i + 1}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className={datasetStyleClass}>
|
||||
<div>
|
||||
<SelectInput
|
||||
label="Target Dataset"
|
||||
@@ -971,53 +1038,57 @@ export default function SimpleJob({
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label="Flipping" docKey={'datasets.flip'} className="mt-2">
|
||||
<Checkbox
|
||||
label={
|
||||
<>
|
||||
Flip X <FlipHorizontal2 className="inline-block w-4 h-4 ml-1" />
|
||||
</>
|
||||
}
|
||||
checked={dataset.flip_x || false}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].flip_x`)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={
|
||||
<>
|
||||
Flip Y <FlipVertical2 className="inline-block w-4 h-4 ml-1" />
|
||||
</>
|
||||
}
|
||||
checked={dataset.flip_y || false}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].flip_y`)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FormGroup label="Resolutions" className="pt-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
[256, 512, 768, 1024],
|
||||
[1280, 1328, 1536],
|
||||
].map(resGroup => (
|
||||
<div key={resGroup[0]} className="space-y-2">
|
||||
{resGroup.map(res => (
|
||||
<Checkbox
|
||||
key={res}
|
||||
label={res.toString()}
|
||||
checked={dataset.resolution.includes(res)}
|
||||
onChange={value => {
|
||||
const resolutions = dataset.resolution.includes(res)
|
||||
? dataset.resolution.filter(r => r !== res)
|
||||
: [...dataset.resolution, res];
|
||||
setJobConfig(resolutions, `config.process[0].datasets[${i}].resolution`);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormGroup>
|
||||
{!isAudioModel && (
|
||||
<FormGroup label="Flipping" docKey={'datasets.flip'} className="mt-2">
|
||||
<Checkbox
|
||||
label={
|
||||
<>
|
||||
Flip X <FlipHorizontal2 className="inline-block w-4 h-4 ml-1" />
|
||||
</>
|
||||
}
|
||||
checked={dataset.flip_x || false}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].flip_x`)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={
|
||||
<>
|
||||
Flip Y <FlipVertical2 className="inline-block w-4 h-4 ml-1" />
|
||||
</>
|
||||
}
|
||||
checked={dataset.flip_y || false}
|
||||
onChange={value => setJobConfig(value, `config.process[0].datasets[${i}].flip_y`)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</div>
|
||||
{!isAudioModel && (
|
||||
<div>
|
||||
<FormGroup label="Resolutions" className="pt-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
[256, 512, 768, 1024],
|
||||
[1280, 1328, 1536],
|
||||
].map(resGroup => (
|
||||
<div key={resGroup[0]} className="space-y-2">
|
||||
{resGroup.map(res => (
|
||||
<Checkbox
|
||||
key={res}
|
||||
label={res.toString()}
|
||||
checked={dataset.resolution.includes(res)}
|
||||
onChange={value => {
|
||||
const resolutions = dataset.resolution.includes(res)
|
||||
? dataset.resolution.filter(r => r !== res)
|
||||
: [...dataset.resolution, res];
|
||||
setJobConfig(resolutions, `config.process[0].datasets[${i}].resolution`);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -1039,13 +1110,7 @@ export default function SimpleJob({
|
||||
</div>
|
||||
<div>
|
||||
<Card title="Sample">
|
||||
<div
|
||||
className={
|
||||
isVideoModel
|
||||
? 'grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6'
|
||||
: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'
|
||||
}
|
||||
>
|
||||
<div className={sampleTopStyleClass}>
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Sample Every"
|
||||
@@ -1084,47 +1149,50 @@ export default function SimpleJob({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Width"
|
||||
value={jobConfig.config.process[0].sample.width}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.width')}
|
||||
placeholder="eg. 1024"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Height"
|
||||
value={jobConfig.config.process[0].sample.height}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.height')}
|
||||
placeholder="eg. 1024"
|
||||
className="pt-2"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
{isVideoModel && (
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Num Frames"
|
||||
value={jobConfig.config.process[0].sample.num_frames}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.num_frames')}
|
||||
placeholder="eg. 0"
|
||||
className="pt-2"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="FPS"
|
||||
value={jobConfig.config.process[0].sample.fps}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.fps')}
|
||||
placeholder="eg. 0"
|
||||
className="pt-2"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isAudioModel && (
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Width"
|
||||
value={jobConfig.config.process[0].sample.width}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.width')}
|
||||
placeholder="eg. 1024"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="Height"
|
||||
value={jobConfig.config.process[0].sample.height}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.height')}
|
||||
placeholder="eg. 1024"
|
||||
className="pt-2"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
{isVideoModel && (
|
||||
<div>
|
||||
<NumberInput
|
||||
label="Num Frames"
|
||||
value={jobConfig.config.process[0].sample.num_frames}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.num_frames')}
|
||||
placeholder="eg. 0"
|
||||
className="pt-2"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
<NumberInput
|
||||
label="FPS"
|
||||
value={jobConfig.config.process[0].sample.fps}
|
||||
onChange={value => setJobConfig(value, 'config.process[0].sample.fps')}
|
||||
placeholder="eg. 0"
|
||||
className="pt-2"
|
||||
min={0}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
@@ -1199,68 +1267,143 @@ export default function SimpleJob({
|
||||
<div className="flex-1">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
label={`Prompt`}
|
||||
value={sample.prompt}
|
||||
onChange={value => setJobConfig(value, `config.process[0].sample.samples[${i}].prompt`)}
|
||||
placeholder="Enter prompt"
|
||||
required
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-2">
|
||||
<TextInput
|
||||
label={`Width`}
|
||||
value={sample.width ? `${sample.width}` : ''}
|
||||
onChange={value => {
|
||||
// remove any non-numeric characters
|
||||
value = value.replace(/\D/g, '');
|
||||
if (value === '') {
|
||||
// remove the key from the config if empty
|
||||
let newConfig = objectCopy(jobConfig);
|
||||
if (newConfig.config.process[0].sample.samples[i]) {
|
||||
delete newConfig.config.process[0].sample.samples[i].width;
|
||||
setJobConfig(
|
||||
newConfig.config.process[0].sample.samples,
|
||||
'config.process[0].sample.samples',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const intValue = parseInt(value);
|
||||
if (!isNaN(intValue)) {
|
||||
setJobConfig(intValue, `config.process[0].sample.samples[${i}].width`);
|
||||
{modelArch?.sampleTags && taggedSampleArr && modelArchTagSections ? (
|
||||
<>
|
||||
{modelArchTagSections.map((sampleTagSection, sti) => (
|
||||
<div key={sti} className="grid w-full lg:grid-flow-col lg:auto-cols-fr gap-4 mt-2">
|
||||
{Object.entries(sampleTagSection).map(([tagKey, tag]) => (
|
||||
<div key={tagKey} className="mb-2">
|
||||
{tag.type === 'text' && (
|
||||
<TextInput
|
||||
label={tag.title}
|
||||
value={taggedSampleArr[i][tagKey] ?? ''}
|
||||
onChange={value => {
|
||||
let taggedSample = { ...taggedSampleArr[i] };
|
||||
taggedSample[tagKey] = value;
|
||||
setJobConfig(
|
||||
objToTags(taggedSample),
|
||||
`config.process[0].sample.samples[${i}].prompt`,
|
||||
);
|
||||
}}
|
||||
placeholder={`Enter ${tag.title.toLowerCase()}`}
|
||||
/>
|
||||
)}
|
||||
{tag.type === 'multiline' && (
|
||||
<TextAreaInput
|
||||
label={tag.title}
|
||||
value={taggedSampleArr[i][tagKey] ?? ''}
|
||||
onChange={value => {
|
||||
let taggedSample = { ...taggedSampleArr[i] };
|
||||
taggedSample[tagKey] = value;
|
||||
setJobConfig(
|
||||
objToTags(taggedSample),
|
||||
`config.process[0].sample.samples[${i}].prompt`,
|
||||
);
|
||||
}}
|
||||
placeholder={`Enter ${tag.title.toLowerCase()}`}
|
||||
/>
|
||||
)}
|
||||
{tag.type === 'number' && (
|
||||
<NumberInput
|
||||
label={tag.title}
|
||||
value={taggedSampleArr[i][tagKey] ?? ''}
|
||||
onChange={value => {
|
||||
let taggedSample = { ...taggedSampleArr[i] };
|
||||
taggedSample[tagKey] = value;
|
||||
setJobConfig(
|
||||
objToTags(taggedSample),
|
||||
`config.process[0].sample.samples[${i}].prompt`,
|
||||
);
|
||||
}}
|
||||
placeholder={`Enter ${tag.title.toLowerCase()}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{modelArch?.hasMultiLinePrompts ? (
|
||||
<TextAreaInput
|
||||
label={`Prompt`}
|
||||
value={sample.prompt}
|
||||
onChange={value => setJobConfig(value, `config.process[0].sample.samples[${i}].prompt`)}
|
||||
placeholder="Enter prompt"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
label={`Prompt`}
|
||||
value={sample.prompt}
|
||||
onChange={value => setJobConfig(value, `config.process[0].sample.samples[${i}].prompt`)}
|
||||
placeholder="Enter prompt"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid w-full lg:grid-flow-col lg:auto-cols-fr gap-4 mt-2">
|
||||
{!isAudioModel && (
|
||||
<TextInput
|
||||
label={`Width`}
|
||||
value={sample.width ? `${sample.width}` : ''}
|
||||
onChange={value => {
|
||||
// remove any non-numeric characters
|
||||
value = value.replace(/\D/g, '');
|
||||
if (value === '') {
|
||||
// remove the key from the config if empty
|
||||
let newConfig = objectCopy(jobConfig);
|
||||
if (newConfig.config.process[0].sample.samples[i]) {
|
||||
delete newConfig.config.process[0].sample.samples[i].width;
|
||||
setJobConfig(
|
||||
newConfig.config.process[0].sample.samples,
|
||||
'config.process[0].sample.samples',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn('Invalid width value:', value);
|
||||
const intValue = parseInt(value);
|
||||
if (!isNaN(intValue)) {
|
||||
setJobConfig(intValue, `config.process[0].sample.samples[${i}].width`);
|
||||
} else {
|
||||
console.warn('Invalid width value:', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={`${jobConfig.config.process[0].sample.width} (default)`}
|
||||
/>
|
||||
<TextInput
|
||||
label={`Height`}
|
||||
value={sample.height ? `${sample.height}` : ''}
|
||||
onChange={value => {
|
||||
// remove any non-numeric characters
|
||||
value = value.replace(/\D/g, '');
|
||||
if (value === '') {
|
||||
// remove the key from the config if empty
|
||||
let newConfig = objectCopy(jobConfig);
|
||||
if (newConfig.config.process[0].sample.samples[i]) {
|
||||
delete newConfig.config.process[0].sample.samples[i].height;
|
||||
setJobConfig(
|
||||
newConfig.config.process[0].sample.samples,
|
||||
'config.process[0].sample.samples',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const intValue = parseInt(value);
|
||||
if (!isNaN(intValue)) {
|
||||
setJobConfig(intValue, `config.process[0].sample.samples[${i}].height`);
|
||||
}}
|
||||
placeholder={`${jobConfig.config.process[0].sample.width} (default)`}
|
||||
/>
|
||||
)}
|
||||
{!isAudioModel && (
|
||||
<TextInput
|
||||
label={`Height`}
|
||||
value={sample.height ? `${sample.height}` : ''}
|
||||
onChange={value => {
|
||||
// remove any non-numeric characters
|
||||
value = value.replace(/\D/g, '');
|
||||
if (value === '') {
|
||||
// remove the key from the config if empty
|
||||
let newConfig = objectCopy(jobConfig);
|
||||
if (newConfig.config.process[0].sample.samples[i]) {
|
||||
delete newConfig.config.process[0].sample.samples[i].height;
|
||||
setJobConfig(
|
||||
newConfig.config.process[0].sample.samples,
|
||||
'config.process[0].sample.samples',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn('Invalid height value:', value);
|
||||
const intValue = parseInt(value);
|
||||
if (!isNaN(intValue)) {
|
||||
setJobConfig(intValue, `config.process[0].sample.samples[${i}].height`);
|
||||
} else {
|
||||
console.warn('Invalid height value:', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={`${jobConfig.config.process[0].sample.height} (default)`}
|
||||
/>
|
||||
}}
|
||||
placeholder={`${jobConfig.config.process[0].sample.height} (default)`}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
label={`Seed`}
|
||||
value={sample.seed ? `${sample.seed}` : ''}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
import { isMac } from '@/helpers/basic';
|
||||
import { JobConfig, DatasetConfig, SliderConfig } from '@/types';
|
||||
import { defaultSampleConfig } from '@/helpers/defaultSamples';
|
||||
import { JobConfig, SampleConfig, DatasetConfig, SliderConfig } from '@/types';
|
||||
|
||||
export const defaultDatasetConfig: DatasetConfig = {
|
||||
folder_path: '/path/to/images/folder',
|
||||
@@ -108,54 +109,7 @@ export const defaultJobConfig: JobConfig = {
|
||||
low_vram: false,
|
||||
model_kwargs: {},
|
||||
},
|
||||
sample: {
|
||||
sampler: 'flowmatch',
|
||||
sample_every: 250,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
samples: [
|
||||
{
|
||||
prompt: 'woman with red hair, playing chess at the park, bomb going off in the background',
|
||||
},
|
||||
{
|
||||
prompt: 'a woman holding a coffee cup, in a beanie, sitting at a cafe',
|
||||
},
|
||||
{
|
||||
prompt: 'a horse is a DJ at a night club, fish eye lens, smoke machine, lazer lights, holding a martini',
|
||||
},
|
||||
{
|
||||
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: '',
|
||||
seed: 42,
|
||||
walk_seed: true,
|
||||
guidance_scale: 4,
|
||||
sample_steps: 25,
|
||||
num_frames: 1,
|
||||
fps: 1,
|
||||
},
|
||||
sample: defaultSampleConfig,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GroupedSelectOption, SelectOption, JobConfig } from '@/types';
|
||||
import { defaultSliderConfig } from './jobConfig';
|
||||
import { defaultAudioSampleConfig, defaultSampleConfig } from '@/helpers/defaultSamples';
|
||||
|
||||
type Control = 'depth' | 'line' | 'pose' | 'inpaint';
|
||||
|
||||
@@ -31,7 +32,17 @@ type AdditionalSections =
|
||||
| 'model.qie.match_target_res'
|
||||
| 'model.assistant_lora_path';
|
||||
|
||||
type ModelGroup = 'image' | 'instruction' | 'video' | 'experimental';
|
||||
type ModelGroup = 'image' | 'instruction' | 'video' | 'experimental' | 'audio';
|
||||
|
||||
export type SampleTag = {
|
||||
title: string;
|
||||
type: 'text' | 'multiline' | 'number'
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export interface SampleTags {
|
||||
[key: string]: SampleTag;
|
||||
}
|
||||
|
||||
export interface ModelArch {
|
||||
name: string;
|
||||
@@ -39,10 +50,12 @@ export interface ModelArch {
|
||||
group: ModelGroup;
|
||||
controls?: Control[];
|
||||
isVideoModel?: boolean;
|
||||
hasMultiLinePrompts?: boolean;
|
||||
defaults?: { [key: string]: any };
|
||||
disableSections?: DisableableSections[];
|
||||
additionalSections?: AdditionalSections[];
|
||||
accuracyRecoveryAdapters?: { [key: string]: string };
|
||||
sampleTags?: SampleTags;
|
||||
}
|
||||
|
||||
const defaultNameOrPath = '';
|
||||
@@ -760,6 +773,116 @@ export const modelArchs: ModelArch[] = [
|
||||
'model.qie.match_target_res',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ace_step_15_xl',
|
||||
label: 'ACE-Step 1.5 XL',
|
||||
group: 'audio',
|
||||
defaults: {
|
||||
// default updates when [selected, unselected] in the UI
|
||||
'config.process[0].model.name_or_path': ['ostris/ace_step_1.5_ComfyUI_files/ace_step_1.5_xl_base_aio.safetensors', 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].train.noise_scheduler': ['flowmatch', 'flowmatch'],
|
||||
'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
|
||||
'config.process[0].model.qtype': ['qfloat8', 'qfloat8'],
|
||||
'config.process[0].sample': [defaultAudioSampleConfig, defaultSampleConfig],
|
||||
},
|
||||
sampleTags: {
|
||||
"CAPTION": {
|
||||
title: "Audio Prompt",
|
||||
type: "text",
|
||||
full: true,
|
||||
},
|
||||
"LYRICS": {
|
||||
title: "Lyrics",
|
||||
type: "multiline",
|
||||
full: true,
|
||||
},
|
||||
"BPM": {
|
||||
title: "BPM",
|
||||
type: "number",
|
||||
},
|
||||
"KEYSCALE": {
|
||||
title: "Key Scale",
|
||||
type: "text",
|
||||
},
|
||||
"TIMESIGNATURE": {
|
||||
title: "Time Signature",
|
||||
type: "text",
|
||||
},
|
||||
"DURATION": {
|
||||
title: "Duration (sec)",
|
||||
type: "number",
|
||||
},
|
||||
"LANGUAGE": {
|
||||
title: "Language",
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
disableSections: ['network.conv'],
|
||||
additionalSections: [
|
||||
'sample.multi_ctrl_imgs',
|
||||
'model.low_vram',
|
||||
'model.layer_offloading',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ace_step_15',
|
||||
label: 'ACE-Step 1.5',
|
||||
group: 'audio',
|
||||
defaults: {
|
||||
// default updates when [selected, unselected] in the UI
|
||||
'config.process[0].model.name_or_path': ['ostris/ace_step_1.5_ComfyUI_files/ace_step_1.5_base_aio.safetensors', 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].train.noise_scheduler': ['flowmatch', 'flowmatch'],
|
||||
'config.process[0].train.timestep_type': ['weighted', 'sigmoid'],
|
||||
'config.process[0].model.qtype': ['qfloat8', 'qfloat8'],
|
||||
'config.process[0].sample': [defaultAudioSampleConfig, defaultSampleConfig],
|
||||
},
|
||||
sampleTags: {
|
||||
"CAPTION": {
|
||||
title: "Audio Prompt",
|
||||
type: "text",
|
||||
full: true,
|
||||
},
|
||||
"LYRICS": {
|
||||
title: "Lyrics",
|
||||
type: "multiline",
|
||||
full: true,
|
||||
},
|
||||
"BPM": {
|
||||
title: "BPM",
|
||||
type: "number",
|
||||
},
|
||||
"KEYSCALE": {
|
||||
title: "Key Scale",
|
||||
type: "text",
|
||||
},
|
||||
"TIMESIGNATURE": {
|
||||
title: "Time Signature",
|
||||
type: "text",
|
||||
},
|
||||
"DURATION": {
|
||||
title: "Duration (sec)",
|
||||
type: "number",
|
||||
},
|
||||
"LANGUAGE": {
|
||||
title: "Language",
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
disableSections: ['network.conv'],
|
||||
additionalSections: [
|
||||
'sample.multi_ctrl_imgs',
|
||||
'model.low_vram',
|
||||
'model.layer_offloading',
|
||||
],
|
||||
},
|
||||
].sort((a, b) => {
|
||||
// Sort by label, case-insensitive
|
||||
return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' });
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Dashboard() {
|
||||
<>
|
||||
<TopBar>
|
||||
<div>
|
||||
<h1 className="text-lg">Training Queue</h1>
|
||||
<h1 className="text-lg">Queue</h1>
|
||||
</div>
|
||||
<div className="flex-1"></div>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user