diff --git a/src/app/api/auth/hf/callback/route.ts b/src/app/api/auth/hf/callback/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e353f3149ff687b697cb541e787a42442de501a --- /dev/null +++ b/src/app/api/auth/hf/callback/route.ts @@ -0,0 +1,112 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +const TOKEN_ENDPOINT = 'https://huggingface.co/oauth/token'; +const USERINFO_ENDPOINT = 'https://huggingface.co/oauth/userinfo'; +const STATE_COOKIE = 'hf_oauth_state'; + +function htmlResponse(script: string) { + return new NextResponse( + `
`, + { + headers: { 'Content-Type': 'text/html; charset=utf-8' }, + }, + ); +} + +export async function GET(request: NextRequest) { + const clientId = process.env.HF_OAUTH_CLIENT_ID || process.env.NEXT_PUBLIC_HF_OAUTH_CLIENT_ID; + const clientSecret = process.env.HF_OAUTH_CLIENT_SECRET; + + if (!clientId || !clientSecret) { + return NextResponse.json({ error: 'OAuth application is not configured' }, { status: 500 }); + } + + const { searchParams } = new URL(request.url); + const code = searchParams.get('code'); + const incomingState = searchParams.get('state'); + + const cookieStore = cookies(); + const storedState = cookieStore.get(STATE_COOKIE)?.value; + + cookieStore.delete(STATE_COOKIE); + + const origin = request.nextUrl.origin; + + if (!code || !incomingState || !storedState || incomingState !== storedState) { + const script = ` + window.opener && window.opener.postMessage({ + type: 'HF_OAUTH_ERROR', + payload: { message: 'Invalid or expired OAuth state.' } + }, '${origin}'); + window.close(); + `; + return htmlResponse(script.trim()); + } + + const redirectUri = process.env.HF_OAUTH_REDIRECT_URI || process.env.NEXT_PUBLIC_HF_OAUTH_REDIRECT_URI || `${origin}/api/auth/hf/callback`; + + try { + const tokenResponse = await fetch(TOKEN_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + client_id: clientId, + client_secret: clientSecret, + }), + }); + + if (!tokenResponse.ok) { + const errorPayload = await tokenResponse.json().catch(() => ({})); + throw new Error(errorPayload?.error_description || 'Failed to exchange code for token'); + } + + const tokenData = await tokenResponse.json(); + const accessToken = tokenData?.access_token; + if (!accessToken) { + throw new Error('Access token missing in response'); + } + + const userResponse = await fetch(USERINFO_ENDPOINT, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!userResponse.ok) { + throw new Error('Failed to fetch user info'); + } + + const profile = await userResponse.json(); + const namespace = profile?.preferred_username || profile?.name || 'user'; + + const script = ` + window.opener && window.opener.postMessage({ + type: 'HF_OAUTH_SUCCESS', + payload: { + token: ${JSON.stringify(accessToken)}, + namespace: ${JSON.stringify(namespace)}, + } + }, '${origin}'); + window.close(); + `; + + return htmlResponse(script.trim()); + } catch (error: any) { + const message = error?.message || 'OAuth flow failed'; + const script = ` + window.opener && window.opener.postMessage({ + type: 'HF_OAUTH_ERROR', + payload: { message: ${JSON.stringify(message)} } + }, '${origin}'); + window.close(); + `; + + return htmlResponse(script.trim()); + } +} diff --git a/src/app/api/auth/hf/login/route.ts b/src/app/api/auth/hf/login/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..22c252217d8b94f9db7a495892a79193df05786a --- /dev/null +++ b/src/app/api/auth/hf/login/route.ts @@ -0,0 +1,36 @@ +import { randomUUID } from 'crypto'; +import { NextRequest, NextResponse } from 'next/server'; + +const HF_AUTHORIZE_URL = 'https://huggingface.co/oauth/authorize'; +const STATE_COOKIE = 'hf_oauth_state'; + +export async function GET(request: NextRequest) { + const clientId = process.env.HF_OAUTH_CLIENT_ID || process.env.NEXT_PUBLIC_HF_OAUTH_CLIENT_ID; + if (!clientId) { + return NextResponse.json({ error: 'OAuth client ID not configured' }, { status: 500 }); + } + + const state = randomUUID(); + const origin = request.nextUrl.origin; + const redirectUri = process.env.HF_OAUTH_REDIRECT_URI || process.env.NEXT_PUBLIC_HF_OAUTH_REDIRECT_URI || `${origin}/api/auth/hf/callback`; + + const authorizeUrl = new URL(HF_AUTHORIZE_URL); + authorizeUrl.searchParams.set('response_type', 'code'); + authorizeUrl.searchParams.set('client_id', clientId); + authorizeUrl.searchParams.set('redirect_uri', redirectUri); + authorizeUrl.searchParams.set('scope', 'openid profile read-repos'); + authorizeUrl.searchParams.set('state', state); + + const response = NextResponse.redirect(authorizeUrl.toString(), { status: 302 }); + response.cookies.set({ + name: STATE_COOKIE, + value: state, + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 5, + path: '/', + }); + + return response; +} diff --git a/src/app/api/auth/hf/validate/route.ts b/src/app/api/auth/hf/validate/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..32dc41fb4d3a7e82d8434ce577aa9e563c349203 --- /dev/null +++ b/src/app/api/auth/hf/validate/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { whoAmI } from '@huggingface/hub'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json().catch(() => ({})); + const token = (body?.token || '').trim(); + + if (!token) { + return NextResponse.json({ error: 'Token is required' }, { status: 400 }); + } + + const info = await whoAmI({ accessToken: token }); + return NextResponse.json({ + name: info?.name || info?.username || 'user', + email: info?.email || null, + orgs: info?.orgs || [], + }); + } catch (error: any) { + return NextResponse.json({ error: error?.message || 'Invalid token' }, { status: 401 }); + } +} diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..1dc229739fbbeaabf307e3be544dd7e2bc8ab66f --- /dev/null +++ b/src/app/api/auth/route.ts @@ -0,0 +1,6 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + // if this gets hit, auth has already been verified + return NextResponse.json({ isAuthenticated: true }); +} diff --git a/src/app/api/caption/get/route.ts b/src/app/api/caption/get/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f8d2818318805f97a80370e1a9cfc584cd9dc26 --- /dev/null +++ b/src/app/api/caption/get/route.ts @@ -0,0 +1,46 @@ +/* eslint-disable */ +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/server/settings'; + +export async function POST(request: NextRequest) { + + const body = await request.json(); + const { imgPath } = body; + console.log('Received POST request for caption:', imgPath); + try { + // Decode the path + const filepath = imgPath; + console.log('Decoded image path:', filepath); + + // caption name is the filepath without extension but with .txt + const captionPath = filepath.replace(/\.[^/.]+$/, '') + '.txt'; + + // Get allowed directories + const allowedDir = await getDatasetsRoot(); + + // Security check: Ensure path is in allowed directory + const isAllowed = filepath.startsWith(allowedDir) && !filepath.includes('..'); + + if (!isAllowed) { + console.warn(`Access denied: ${filepath} not in ${allowedDir}`); + return new NextResponse('Access denied', { status: 403 }); + } + + // Check if file exists + if (!fs.existsSync(captionPath)) { + // send back blank string if caption file does not exist + return new NextResponse(''); + } + + // Read caption file + const caption = fs.readFileSync(captionPath, 'utf-8'); + + // Return caption + return new NextResponse(caption); + } catch (error) { + console.error('Error getting caption:', error); + return new NextResponse('Error getting caption', { status: 500 }); + } +} diff --git a/src/app/api/datasets/create/route.tsx b/src/app/api/datasets/create/route.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e005d058f3423db41f4830b69a1d51c7872d1351 --- /dev/null +++ b/src/app/api/datasets/create/route.tsx @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/server/settings'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + let { name } = body; + // clean name by making lower case, removing special characters, and replacing spaces with underscores + name = name.toLowerCase().replace(/[^a-z0-9]+/g, '_'); + + let datasetsPath = await getDatasetsRoot(); + let datasetPath = path.join(datasetsPath, name); + + // if folder doesnt exist, create it + if (!fs.existsSync(datasetPath)) { + fs.mkdirSync(datasetPath); + } + + return NextResponse.json({ success: true, name: name, path: datasetPath }); + } catch (error) { + return NextResponse.json({ error: 'Failed to create dataset' }, { status: 500 }); + } +} diff --git a/src/app/api/datasets/delete/route.tsx b/src/app/api/datasets/delete/route.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9a1d970ee415c9d040596854ce74ad5401859259 --- /dev/null +++ b/src/app/api/datasets/delete/route.tsx @@ -0,0 +1,24 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/server/settings'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { name } = body; + let datasetsPath = await getDatasetsRoot(); + let datasetPath = path.join(datasetsPath, name); + + // if folder doesnt exist, ignore + if (!fs.existsSync(datasetPath)) { + return NextResponse.json({ success: true }); + } + + // delete it and return success + fs.rmdirSync(datasetPath, { recursive: true }); + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: 'Failed to create dataset' }, { status: 500 }); + } +} diff --git a/src/app/api/datasets/list/route.ts b/src/app/api/datasets/list/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc829c65f3cab2829221f85341967fc1b52a921c --- /dev/null +++ b/src/app/api/datasets/list/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import { getDatasetsRoot } from '@/server/settings'; + +export async function GET() { + try { + let datasetsPath = await getDatasetsRoot(); + + // if folder doesnt exist, create it + if (!fs.existsSync(datasetsPath)) { + fs.mkdirSync(datasetsPath); + } + + // find all the folders in the datasets folder + let folders = fs + .readdirSync(datasetsPath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .filter(dirent => !dirent.name.startsWith('.')) + .map(dirent => dirent.name); + + return NextResponse.json(folders); + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch datasets' }, { status: 500 }); + } +} diff --git a/src/app/api/datasets/listImages/route.ts b/src/app/api/datasets/listImages/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..06dca84ae780c7fddb200fc6de422b7a42e309ea --- /dev/null +++ b/src/app/api/datasets/listImages/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot } from '@/server/settings'; + +export async function POST(request: Request) { + const datasetsPath = await getDatasetsRoot(); + const body = await request.json(); + const { datasetName } = body; + const datasetFolder = path.join(datasetsPath, datasetName); + + try { + // Check if folder exists + if (!fs.existsSync(datasetFolder)) { + return NextResponse.json({ error: `Folder '${datasetName}' not found` }, { status: 404 }); + } + + // Find all images recursively + const imageFiles = findImagesRecursively(datasetFolder); + + // Format response + const result = imageFiles.map(imgPath => ({ + img_path: imgPath, + })); + + return NextResponse.json({ images: result }); + } catch (error) { + console.error('Error finding images:', error); + return NextResponse.json({ error: 'Failed to process request' }, { status: 500 }); + } +} + +/** + * Recursively finds all image files in a directory and its subdirectories + * @param dir Directory to search + * @returns Array of absolute paths to image files + */ +function findImagesRecursively(dir: string): string[] { + const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.mp4', '.avi', '.mov', '.mkv', '.wmv', '.m4v', '.flv']; + let results: string[] = []; + + const items = fs.readdirSync(dir); + + for (const item of items) { + const itemPath = path.join(dir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory() && item !== '_controls' && !item.startsWith('.')) { + // If it's a directory, recursively search it + results = results.concat(findImagesRecursively(itemPath)); + } else { + // If it's a file, check if it's an image + const ext = path.extname(itemPath).toLowerCase(); + if (imageExtensions.includes(ext)) { + results.push(itemPath); + } + } + } + + return results; +} diff --git a/src/app/api/datasets/upload/route.ts b/src/app/api/datasets/upload/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..51aff81fd3bf4b091f10a1df9f2da887910f4753 --- /dev/null +++ b/src/app/api/datasets/upload/route.ts @@ -0,0 +1,57 @@ +// src/app/api/datasets/upload/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import { writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { getDatasetsRoot } from '@/server/settings'; + +export async function POST(request: NextRequest) { + try { + const datasetsPath = await getDatasetsRoot(); + if (!datasetsPath) { + return NextResponse.json({ error: 'Datasets path not found' }, { status: 500 }); + } + const formData = await request.formData(); + const files = formData.getAll('files'); + const datasetName = formData.get('datasetName') as string; + + if (!files || files.length === 0) { + return NextResponse.json({ error: 'No files provided' }, { status: 400 }); + } + + // Create upload directory if it doesn't exist + const uploadDir = join(datasetsPath, datasetName); + await mkdir(uploadDir, { recursive: true }); + + const savedFiles: string[] = []; + + // Process files sequentially to avoid overwhelming the system + for (let i = 0; i < files.length; i++) { + const file = files[i] as any; + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + + // Clean filename and ensure it's unique + const fileName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_'); + const filePath = join(uploadDir, fileName); + + await writeFile(filePath, buffer); + savedFiles.push(fileName); + } + + 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', + }, +}; diff --git a/src/app/api/files/[...filePath]/route.ts b/src/app/api/files/[...filePath]/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..46eb5c4ab08b9c02ba4ff8d0fe7f6dc2cd15442a --- /dev/null +++ b/src/app/api/files/[...filePath]/route.ts @@ -0,0 +1,116 @@ +/* eslint-disable */ +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { getDatasetsRoot, getTrainingFolder } from '@/server/settings'; + +export async function GET(request: NextRequest, { params }: { params: { filePath: string } }) { + const { filePath } = await params; + try { + // Decode the path + const decodedFilePath = decodeURIComponent(filePath); + + // Get allowed directories + const datasetRoot = await getDatasetsRoot(); + const trainingRoot = await getTrainingFolder(); + const allowedDirs = [datasetRoot, trainingRoot]; + + // Security check: Ensure path is in allowed directory + const isAllowed = + allowedDirs.some(allowedDir => decodedFilePath.startsWith(allowedDir)) && !decodedFilePath.includes('..'); + + if (!isAllowed) { + console.warn(`Access denied: ${decodedFilePath} not in ${allowedDirs.join(', ')}`); + return new NextResponse('Access denied', { status: 403 }); + } + + // Check if file exists + if (!fs.existsSync(decodedFilePath)) { + console.warn(`File not found: ${decodedFilePath}`); + return new NextResponse('File not found', { status: 404 }); + } + + // Get file info + const stat = fs.statSync(decodedFilePath); + if (!stat.isFile()) { + return new NextResponse('Not a file', { status: 400 }); + } + + // Get filename for Content-Disposition + const filename = path.basename(decodedFilePath); + + // Determine content type + const ext = path.extname(decodedFilePath).toLowerCase(); + const contentTypeMap: { [key: string]: string } = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + '.safetensors': 'application/octet-stream', + '.zip': 'application/zip', + // Videos + '.mp4': 'video/mp4', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.mkv': 'video/x-matroska', + '.wmv': 'video/x-ms-wmv', + '.m4v': 'video/x-m4v', + '.flv': 'video/x-flv' + }; + + const contentType = contentTypeMap[ext] || 'application/octet-stream'; + + // Get range header for partial content support + const range = request.headers.get('range'); + + // Common headers for better download handling + const commonHeaders = { + 'Content-Type': contentType, + 'Accept-Ranges': 'bytes', + 'Cache-Control': 'public, max-age=86400', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(filename)}"`, + 'X-Content-Type-Options': 'nosniff', + }; + + if (range) { + // Parse range header + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : Math.min(start + 10 * 1024 * 1024, stat.size - 1); // 10MB chunks + const chunkSize = end - start + 1; + + const fileStream = fs.createReadStream(decodedFilePath, { + start, + end, + highWaterMark: 64 * 1024, // 64KB buffer + }); + + return new NextResponse(fileStream as any, { + status: 206, + headers: { + ...commonHeaders, + 'Content-Range': `bytes ${start}-${end}/${stat.size}`, + 'Content-Length': String(chunkSize), + }, + }); + } else { + // For full file download, read directly without streaming wrapper + const fileStream = fs.createReadStream(decodedFilePath, { + highWaterMark: 64 * 1024, // 64KB buffer + }); + + return new NextResponse(fileStream as any, { + headers: { + ...commonHeaders, + 'Content-Length': String(stat.size), + }, + }); + } + } catch (error) { + console.error('Error serving file:', error); + return new NextResponse('Internal Server Error', { status: 500 }); + } +} diff --git a/src/app/api/gpu/route.ts b/src/app/api/gpu/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b11dbb0e6d8e8de0f191bb1e78bb8687376881a --- /dev/null +++ b/src/app/api/gpu/route.ts @@ -0,0 +1,121 @@ +import { NextResponse } from 'next/server'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import os from 'os'; + +const execAsync = promisify(exec); + +export async function GET() { + try { + // Get platform + const platform = os.platform(); + const isWindows = platform === 'win32'; + + // Check if nvidia-smi is available + const hasNvidiaSmi = await checkNvidiaSmi(isWindows); + + if (!hasNvidiaSmi) { + return NextResponse.json({ + hasNvidiaSmi: false, + gpus: [], + error: 'nvidia-smi not found or not accessible', + }); + } + + // Get GPU stats + const gpuStats = await getGpuStats(isWindows); + + return NextResponse.json({ + hasNvidiaSmi: true, + gpus: gpuStats, + }); + } catch (error) { + console.error('Error fetching NVIDIA GPU stats:', error); + return NextResponse.json( + { + hasNvidiaSmi: false, + gpus: [], + error: `Failed to fetch GPU stats: ${error instanceof Error ? error.message : String(error)}`, + }, + { status: 500 }, + ); + } +} + +async function checkNvidiaSmi(isWindows: boolean): Promise+ {isAuthenticated + ? 'You are signed in with Hugging Face and can manage jobs, datasets, and submissions.' + : 'Authenticate with Hugging Face or add a personal access token to create jobs, upload datasets, and launch training.'} +
+You need to sign in with Hugging Face or provide a valid token to view this dataset.
+{subtitle}
+Sign in with Hugging Face or add an access token to manage datasets.
+Sign in with Hugging Face or add an access token to view job details.
+Loading...
} + {status === 'error' && job == null &&Error fetching job
} + {job && ( + <> + {pages.map(page => { + const Component = page.component; + return page.value === pageKey ?You need to sign in with Hugging Face or provide a valid access token before creating or editing jobs.
+Sign in with Hugging Face or add a personal access token to view and manage training jobs.
+{authDescription}
+{authError}
+ )} +
+
+ Drop files to upload
++ Destination: {datasetName || 'unknown'} +
+Images, videos, or .txt supported
+ > + ) : ( + <> +Uploading… {uploadProgress}%
+{gpuData.error}
} +Temperature
+{gpu.temperature}°C
+Fan Speed
+{gpu.fan.speed}%
+GPU Load
+ {gpu.utilization.gpu}% +Memory
+ + {((gpu.memory.used / gpu.memory.total) * 100).toFixed(1)}% + ++ {formatMemory(gpu.memory.used)} / {formatMemory(gpu.memory.total)} +
+Clock Speed
+{gpu.clocks.graphics} MHz
+Power Draw
++ {gpu.power.draw?.toFixed(1)}W + / {gpu.power.limit?.toFixed(1) || ' ? '}W +
++ First, let's validate your Hugging Face token and get your username for dataset uploads. +
+ + {validationResult && ( ++ ✓ Token valid! Logged in as: {validationResult.name} +
++ Choose whether to upload a new dataset or use an existing one from HF Hub. +
+ ++ Enter the full dataset ID (namespace/name) from HuggingFace Hub +
+ > + )} + + {uploadResult && ( ++ ✓ Dataset {uploadResult.existing ? 'validated' : 'uploaded'} successfully! +
++ {uploadResult.existing ? 'Using dataset:' : 'View at:'} {uploadResult.repoId} +
++ Configure and submit your training job to HF Jobs. +
+ ++ ✓ Job submitted successfully! +
+
+ Job ID: {jobResult.jobId}
+
+ Your training job has been submitted to Hugging Face Jobs and is now running in the cloud. +
+
+ Job ID: {jobResult?.jobId}
+
+ Monitor Job: + View on HF Jobs → + +
+ )} ++ Dataset: {uploadResult?.repoId} +
++ Hardware: {hardware} +
+Next steps:
+hf jobs logs {jobResult?.jobId}{namespace}/{jobConfig.config.name}-lora{error}
+Job Name
+{job.name}
++ {isHFJob ? 'Hardware' : 'Assigned GPUs'} +
++ {isHFJob ? (jobConfig?.hardware || job.gpu_ids) : `GPUs: ${job.gpu_ids}`} +
+Speed
+{job.speed_string == '' ? '?' : job.speed_string}
++ This job is running on HF Jobs. View logs and monitor progress on the HuggingFace platform. +
+ ++ This HF Job is ready for submission. Edit the job clicking on the pen on the top right. +
+{line};
+ })}
+ {subtitle}
+
+ Ostris
+ AI-Toolkit
+ Welcome, {namespace || 'user'}
+ )} +Empty
+ +| + {column.title} + | + ))} +
|---|
| + {column.render ? column.render(row) : row[column.key]} + | + ))} +
{'[trigger]'} placeholder in your captions. This will be automatically replaced with your trigger
+ word.
+ {'[trigger]'} placeholder in your test prompts as well.
+ >
+ ),
+ },
+ 'config.process[0].model.name_or_path': {
+ title: 'Name or Path',
+ description: (
+ <>
+ The name of a diffusers repo on Huggingface or the local path to the base model you want to train from. The
+ folder needs to be in diffusers format for most models. For some models, such as SDXL and SD1, you can put the
+ path to an all in one safetensors checkpoint here.
+ >
+ ),
+ },
+ '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.
+ >
+ ),
+ },
+ 'datasets.num_frames': {
+ title: 'Number of Frames',
+ description: (
+ <>
+ This sets the number of frames to shrink videos to for a video dataset. If this dataset is images, set this to 1
+ for one frame. If your dataset is only videos, frames will be extracted evenly spaced from the videos in the
+ dataset.
+