diff --git a/ui/package-lock.json b/ui/package-lock.json index eb2e8232..a86a7bac 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,6 +14,7 @@ "classnames": "^2.5.1", "lucide-react": "^0.475.0", "next": "15.1.7", + "node-cache": "^5.1.2", "prisma": "^6.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -1414,6 +1415,14 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2891,6 +2900,17 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", diff --git a/ui/package.json b/ui/package.json index c6b8b97a..88a938bc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,6 +17,7 @@ "classnames": "^2.5.1", "lucide-react": "^0.475.0", "next": "15.1.7", + "node-cache": "^5.1.2", "prisma": "^6.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/ui/src/app/api/caption/[...imagePath]/route.ts b/ui/src/app/api/caption/[...imagePath]/route.ts index 3a35b88b..7aa0e07f 100644 --- a/ui/src/app/api/caption/[...imagePath]/route.ts +++ b/ui/src/app/api/caption/[...imagePath]/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function GET(request: NextRequest, { params }: { params: { imagePath: string } }) { const { imagePath } = await params; diff --git a/ui/src/app/api/datasets/create/route.tsx b/ui/src/app/api/datasets/create/route.tsx index f552b57f..62976613 100644 --- a/ui/src/app/api/datasets/create/route.tsx +++ b/ui/src/app/api/datasets/create/route.tsx @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function POST(request: Request) { try { diff --git a/ui/src/app/api/datasets/delete/route.tsx b/ui/src/app/api/datasets/delete/route.tsx index a35b2182..9a1d970e 100644 --- a/ui/src/app/api/datasets/delete/route.tsx +++ b/ui/src/app/api/datasets/delete/route.tsx @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function POST(request: Request) { try { diff --git a/ui/src/app/api/datasets/list/route.ts b/ui/src/app/api/datasets/list/route.ts index 1d841681..dc829c65 100644 --- a/ui/src/app/api/datasets/list/route.ts +++ b/ui/src/app/api/datasets/list/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function GET() { try { diff --git a/ui/src/app/api/datasets/listImages/route.ts b/ui/src/app/api/datasets/listImages/route.ts index e563d65f..b1bb496a 100644 --- a/ui/src/app/api/datasets/listImages/route.ts +++ b/ui/src/app/api/datasets/listImages/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function POST(request: Request) { const datasetsPath = await getDatasetsRoot(); diff --git a/ui/src/app/api/datasets/upload/route.ts b/ui/src/app/api/datasets/upload/route.ts index 44b93ee1..0f7836fd 100644 --- a/ui/src/app/api/datasets/upload/route.ts +++ b/ui/src/app/api/datasets/upload/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function POST(request: NextRequest) { try { diff --git a/ui/src/app/api/datasets/utils.ts b/ui/src/app/api/datasets/utils.ts deleted file mode 100644 index 88361636..00000000 --- a/ui/src/app/api/datasets/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { defaultDatasetsFolder } from '@/paths'; - -const prisma = new PrismaClient(); - -export const getDatasetsRoot = async () => { - let row = await prisma.settings.findFirst({ - where: { - key: 'DATASETS_FOLDER', - }, - }); - let datasetsPath = defaultDatasetsFolder; - if (row?.value && row.value !== '') { - datasetsPath = row.value; - } - return datasetsPath; -}; \ No newline at end of file diff --git a/ui/src/app/api/img/[...imagePath]/route.ts b/ui/src/app/api/img/[...imagePath]/route.ts index 875cdd1a..45586c44 100644 --- a/ui/src/app/api/img/[...imagePath]/route.ts +++ b/ui/src/app/api/img/[...imagePath]/route.ts @@ -2,23 +2,25 @@ import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot, getTrainingFolder } from '@/server/settings'; export async function GET(request: NextRequest, { params }: { params: { imagePath: string } }) { const { imagePath } = await params; try { // Decode the path const filepath = decodeURIComponent(imagePath); - console.log('Serving image:', filepath); // Get allowed directories - const allowedDir = await getDatasetsRoot(); + const datasetRoot = await getDatasetsRoot(); + const trainingRoot = await getTrainingFolder(); + + const allowedDirs = [datasetRoot, trainingRoot]; // Security check: Ensure path is in allowed directory - const isAllowed = filepath.startsWith(allowedDir) && !filepath.includes('..'); + const isAllowed = allowedDirs.some(allowedDir => filepath.startsWith(allowedDir)) && !filepath.includes('..'); if (!isAllowed) { - console.warn(`Access denied: ${filepath} not in ${allowedDir}`); + console.warn(`Access denied: ${filepath} not in ${allowedDirs.join(', ')}`); return new NextResponse('Access denied', { status: 403 }); } diff --git a/ui/src/app/api/img/caption/route.ts b/ui/src/app/api/img/caption/route.ts index d2b22c68..d3a150c9 100644 --- a/ui/src/app/api/img/caption/route.ts +++ b/ui/src/app/api/img/caption/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function POST(request: Request) { try { diff --git a/ui/src/app/api/img/delete/route.ts b/ui/src/app/api/img/delete/route.ts index 178d602d..d4d968f8 100644 --- a/ui/src/app/api/img/delete/route.ts +++ b/ui/src/app/api/img/delete/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import fs from 'fs'; -import { getDatasetsRoot } from '@/app/api/datasets/utils'; +import { getDatasetsRoot } from '@/server/settings'; export async function POST(request: Request) { try { diff --git a/ui/src/app/api/jobs/[jobID]/samples/route.ts b/ui/src/app/api/jobs/[jobID]/samples/route.ts new file mode 100644 index 00000000..26af0c05 --- /dev/null +++ b/ui/src/app/api/jobs/[jobID]/samples/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { PrismaClient } from '@prisma/client'; +import path from 'path'; +import fs from 'fs'; +import { getTrainingFolder } from '@/server/settings'; + +const prisma = new PrismaClient(); + +export async function GET(request: NextRequest, { params }: { params: { jobID: string } }) { + const { jobID } = await params; + + const job = await prisma.job.findUnique({ + where: { id: jobID }, + }); + + if (!job) { + return NextResponse.json({ error: 'Job not found' }, { status: 404 }); + } + + // setup the training + const trainingFolder = await getTrainingFolder(); + + const samplesFolder = path.join(trainingFolder, job.name, 'samples'); + if (!fs.existsSync(samplesFolder)) { + return NextResponse.json({ samples: [] }); + } + + // find all img (png, jpg, jpeg) files in the samples folder + const samples = fs + .readdirSync(samplesFolder) + .filter(file => { + return file.endsWith('.png') || file.endsWith('.jpg') || file.endsWith('.jpeg'); + }) + .map(file => { + return path.join(samplesFolder, file); + }) + .sort(); + + return NextResponse.json({ samples }); +} diff --git a/ui/src/app/api/jobs/[jobID]/start/route.ts b/ui/src/app/api/jobs/[jobID]/start/route.ts index 1ca970ac..dcf5795d 100644 --- a/ui/src/app/api/jobs/[jobID]/start/route.ts +++ b/ui/src/app/api/jobs/[jobID]/start/route.ts @@ -4,7 +4,7 @@ import { TOOLKIT_ROOT, defaultTrainFolder } from '@/paths'; import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs'; - +import { getTrainingFolder } from '@/server/settings'; const prisma = new PrismaClient(); @@ -30,18 +30,10 @@ export async function GET(request: NextRequest, { params }: { params: { jobID: s }); // setup the training - const settings = await prisma.settings.findMany(); - const settingsObject = settings.reduce((acc: any, setting) => { - acc[setting.key] = setting.value; - return acc; - }, {}); - // if TRAINING_FOLDER is not set, use default - if (!settingsObject.TRAINING_FOLDER || settingsObject.TRAINING_FOLDER === '') { - settingsObject.TRAINING_FOLDER = defaultTrainFolder; - } + const trainingRoot = await getTrainingFolder(); - const trainingFolder = path.join(settingsObject.TRAINING_FOLDER, job.name); + const trainingFolder = path.join(trainingRoot, job.name); if (!fs.existsSync(trainingFolder)) { fs.mkdirSync(trainingFolder, { recursive: true }); } @@ -53,7 +45,6 @@ export async function GET(request: NextRequest, { params }: { params: { jobID: s const jobConfig = JSON.parse(job.job_config); jobConfig.config.process[0].sqlite_db_path = path.join(TOOLKIT_ROOT, 'aitk_db.db'); - // write the config file fs.writeFileSync(configPath, JSON.stringify(jobConfig, null, 2)); @@ -70,7 +61,10 @@ export async function GET(request: NextRequest, { params }: { params: { jobID: s return NextResponse.json({ error: 'run.py not found' }, { status: 500 }); } - console.log('Spawning command:', `AITK_JOB_ID=${jobID} CUDA_VISIBLE_DEVICES=${job.gpu_ids} ${pythonPath} ${runFilePath} ${configPath}`); + console.log( + 'Spawning command:', + `AITK_JOB_ID=${jobID} CUDA_VISIBLE_DEVICES=${job.gpu_ids} ${pythonPath} ${runFilePath} ${configPath}`, + ); // start job const subprocess = spawn(pythonPath, [runFilePath, configPath], { @@ -83,7 +77,7 @@ export async function GET(request: NextRequest, { params }: { params: { jobID: s }, cwd: TOOLKIT_ROOT, }); - + subprocess.unref(); return NextResponse.json(job); diff --git a/ui/src/app/api/settings/route.ts b/ui/src/app/api/settings/route.ts index 458c53c5..055cfbb6 100644 --- a/ui/src/app/api/settings/route.ts +++ b/ui/src/app/api/settings/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; import { defaultTrainFolder, defaultDatasetsFolder } from '@/paths'; +import {flushCache} from '@/server/settings'; const prisma = new PrismaClient(); @@ -49,6 +50,8 @@ export async function POST(request: Request) { }), ]); + flushCache(); + return NextResponse.json({ success: true }); } catch (error) { return NextResponse.json({ error: 'Failed to update settings' }, { status: 500 }); diff --git a/ui/src/app/jobs/[jobID]/page.tsx b/ui/src/app/jobs/[jobID]/page.tsx index e28ed956..f8efced3 100644 --- a/ui/src/app/jobs/[jobID]/page.tsx +++ b/ui/src/app/jobs/[jobID]/page.tsx @@ -1,16 +1,41 @@ 'use client'; -import { useEffect, useState, use } from 'react'; +import { useMemo, useState, use } from 'react'; import { FaChevronLeft } from 'react-icons/fa'; import { Button } from '@headlessui/react'; import { TopBar, MainContent } from '@/components/layout'; import useJob from '@/hooks/useJob'; import { startJob, stopJob } from '@/utils/jobs'; +import SampleImages from '@/components/SampleImages'; +import JobOverview from '@/components/JobOverview'; +import { JobConfig } from '@/types'; + +type PageKey = 'overview' | 'samples'; + +interface Page { + name: string; + value: PageKey; +} + +const pages: Page[] = [ + { name: 'Overview', value: 'overview' }, + { name: 'Samples', value: 'samples' }, +]; export default function JobPage({ params }: { params: { jobID: string } }) { const usableParams = use(params as any) as { jobID: string }; const jobID = usableParams.jobID; - const { job, status, refreshJobs } = useJob(jobID, 5000); + const { job, status, refreshJob } = useJob(jobID, 5000); + const [pageKey, setPageKey] = useState('overview'); + + const numSamples = useMemo(() => { + if (job?.job_config) { + const jobConfig = JSON.parse(job.job_config) as JobConfig; + const sampleConfig = jobConfig.config.process[0].sample; + return sampleConfig.prompts.length; + } + return 10; + }, [job]); return ( <> @@ -28,8 +53,8 @@ export default function JobPage({ params }: { params: { jobID: string } }) { {job?.status === 'running' && ( )} - + {status === 'loading' && job == null &&

Loading...

} {status === 'error' && job == null &&

Error fetching job

} {job && ( <> -
-
-

Job Details

-

ID: {job.id}

-

Name: {job.name}

-

GPUs: {job.gpu_ids}

-

Status: {job.status}

-

Info: {job.info}

-

Step: {job.step}

-
-
+ {pageKey === 'overview' && } + {pageKey === 'samples' && } )}
+
+ {pages.map(page => ( + + ))} +
); } diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx index 8d359d83..7e76c2fa 100644 --- a/ui/src/app/layout.tsx +++ b/ui/src/app/layout.tsx @@ -19,7 +19,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-
{children}
+
{children}
diff --git a/ui/src/components/JobOverview.tsx b/ui/src/components/JobOverview.tsx new file mode 100644 index 00000000..3df5152c --- /dev/null +++ b/ui/src/components/JobOverview.tsx @@ -0,0 +1,23 @@ +import { Job } from '@prisma/client'; + +interface JobOverviewProps { + job: Job; +} + +export default function JobOverview({ job }: JobOverviewProps) { + return ( + <> +
+
+

Job Details

+

ID: {job.id}

+

Name: {job.name}

+

GPUs: {job.gpu_ids}

+

Status: {job.status}

+

Info: {job.info}

+

Step: {job.step}

+
+
+ + ); +} diff --git a/ui/src/components/SampleImageCard.tsx b/ui/src/components/SampleImageCard.tsx new file mode 100644 index 00000000..5a4fa91b --- /dev/null +++ b/ui/src/components/SampleImageCard.tsx @@ -0,0 +1,67 @@ +import React, { useRef, useEffect, useState, ReactNode } from 'react'; + +interface SampleImageCardProps { + imageUrl: string; + alt: string; + children?: ReactNode; + className?: string; + onDelete?: () => void; +} + +const SampleImageCard: React.FC = ({ imageUrl, alt, children, className = '' }) => { + const cardRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + // Create intersection observer to check visibility + const observer = new IntersectionObserver( + entries => { + if (entries[0].isIntersecting) { + setIsVisible(true); + observer.disconnect(); + } + }, + { threshold: 0.1 }, + ); + + if (cardRef.current) { + observer.observe(cardRef.current); + } + + return () => { + observer.disconnect(); + }; + }, []); + + const handleLoad = (): void => { + setLoaded(true); + }; + + return ( +
+ {/* Square image container */} +
+
+ {isVisible && ( + {alt} + )} + {children &&
{children}
} +
+
+
+ ); +}; + +export default SampleImageCard; diff --git a/ui/src/components/SampleImages.tsx b/ui/src/components/SampleImages.tsx new file mode 100644 index 00000000..ff2b1965 --- /dev/null +++ b/ui/src/components/SampleImages.tsx @@ -0,0 +1,89 @@ +import { useMemo } from 'react'; +import useSampleImages from '@/hooks/useSampleImages'; +import SampleImageCard from './SampleImageCard'; +import { Job } from '@prisma/client'; +import { JobConfig } from '@/types'; + +interface SampleImagesProps { + job: Job; +} + +export default function SampleImages({ job }: SampleImagesProps) { + const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000); + const numSamples = useMemo(() => { + if (job?.job_config) { + const jobConfig = JSON.parse(job.job_config) as JobConfig; + const sampleConfig = jobConfig.config.process[0].sample; + return sampleConfig.prompts.length; + } + return 10; + }, [job]); + + // Use direct Tailwind class without string interpolation + // This way Tailwind can properly generate the class + // I hate this, but it's the only way to make it work + const gridColsClass = useMemo(() => { + const cols = Math.min(numSamples, 20); + + switch (cols) { + case 1: + return 'grid-cols-1'; + case 2: + return 'grid-cols-2'; + case 3: + return 'grid-cols-3'; + case 4: + return 'grid-cols-4'; + case 5: + return 'grid-cols-5'; + case 6: + return 'grid-cols-6'; + case 7: + return 'grid-cols-7'; + case 8: + return 'grid-cols-8'; + case 9: + return 'grid-cols-9'; + case 10: + return 'grid-cols-10'; + case 11: + return 'grid-cols-11'; + case 12: + return 'grid-cols-12'; + case 13: + return 'grid-cols-13'; + case 14: + return 'grid-cols-14'; + case 15: + return 'grid-cols-15'; + case 16: + return 'grid-cols-16'; + case 17: + return 'grid-cols-17'; + case 18: + return 'grid-cols-18'; + case 19: + return 'grid-cols-19'; + case 20: + return 'grid-cols-20'; + default: + return 'grid-cols-1'; + } + }, [numSamples]); + + return ( +
+
+ {status === 'loading' && sampleImages.length === 0 &&

Loading...

} + {status === 'error' &&

Error fetching sample images

} + {sampleImages && ( +
+ {sampleImages.map((sample: string) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/ui/src/components/layout.tsx b/ui/src/components/layout.tsx index a23cc45b..c5cbef55 100644 --- a/ui/src/components/layout.tsx +++ b/ui/src/components/layout.tsx @@ -20,7 +20,7 @@ export const TopBar: React.FC = ({ children, className }) => { export const MainContent: React.FC = ({ children, className }) => { return ( -
+
{children ? children : null}
); diff --git a/ui/src/hooks/useJob.tsx b/ui/src/hooks/useJob.tsx index e612eb72..e4318233 100644 --- a/ui/src/hooks/useJob.tsx +++ b/ui/src/hooks/useJob.tsx @@ -7,7 +7,7 @@ export default function useJob(jobID: string, reloadInterval: null | number = nu const [job, setJob] = useState(null); const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); - const refreshJobs = () => { + const refreshJob = () => { setStatus('loading'); fetch(`/api/jobs?id=${jobID}`) .then(res => res.json()) @@ -23,11 +23,11 @@ export default function useJob(jobID: string, reloadInterval: null | number = nu }; useEffect(() => { - refreshJobs(); + refreshJob(); if (reloadInterval) { const interval = setInterval(() => { - refreshJobs(); + refreshJob(); }, reloadInterval); return () => { @@ -36,5 +36,5 @@ export default function useJob(jobID: string, reloadInterval: null | number = nu } }, [jobID]); - return { job, setJob, status, refreshJobs }; + return { job, setJob, status, refreshJob }; } diff --git a/ui/src/hooks/useSampleImages.tsx b/ui/src/hooks/useSampleImages.tsx new file mode 100644 index 00000000..5407f72a --- /dev/null +++ b/ui/src/hooks/useSampleImages.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Job } from '@prisma/client'; + +export default function useSampleImages(jobID: string, reloadInterval: null | number = null) { + const [sampleImages, setSampleImages] = useState([]); + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + + const refreshSampleImages = () => { + setStatus('loading'); + fetch(`/api/jobs/${jobID}/samples`) + .then(res => res.json()) + .then(data => { + if (data.samples) { + setSampleImages(data.samples); + } + setStatus('success'); + }) + .catch(error => { + console.error('Error fetching datasets:', error); + setStatus('error'); + }); + }; + + useEffect(() => { + refreshSampleImages(); + + if (reloadInterval) { + const interval = setInterval(() => { + refreshSampleImages(); + }, reloadInterval); + + return () => { + clearInterval(interval); + }; + } + }, [jobID]); + + return { sampleImages, setSampleImages, status, refreshSampleImages }; +} diff --git a/ui/src/server/settings.ts b/ui/src/server/settings.ts new file mode 100644 index 00000000..a93d2b95 --- /dev/null +++ b/ui/src/server/settings.ts @@ -0,0 +1,50 @@ +import { PrismaClient } from '@prisma/client'; +import { defaultDatasetsFolder } from '@/paths'; +import { defaultTrainFolder } from '@/paths'; +import NodeCache from 'node-cache'; + +const myCache = new NodeCache(); +const prisma = new PrismaClient(); + +export const flushCache = () => { + myCache.flushAll(); +}; + +export const getDatasetsRoot = async () => { + const key = 'DATASETS_FOLDER'; + let datasetsPath = myCache.get(key) as string; + if (datasetsPath) { + return datasetsPath; + } + let row = await prisma.settings.findFirst({ + where: { + key: 'DATASETS_FOLDER', + }, + }); + datasetsPath = defaultDatasetsFolder; + if (row?.value && row.value !== '') { + datasetsPath = row.value; + } + myCache.set(key, datasetsPath); + return datasetsPath as string; +}; + + +export const getTrainingFolder = async () => { + const key = 'TRAINING_FOLDER'; + let trainingRoot = myCache.get(key) as string; + if (trainingRoot) { + return trainingRoot; + } + let row = await prisma.settings.findFirst({ + where: { + key: key, + }, + }); + trainingRoot = defaultTrainFolder; + if (row?.value && row.value !== '') { + trainingRoot = row.value; + } + myCache.set(key, trainingRoot); + return trainingRoot as string; +};