added backend-mgmt for dataset/dataset_entry and utterances, this includes upload and also displaying of the dataset on the admin pages

This commit is contained in:
averel10
2026-03-05 11:31:06 +01:00
parent 28c45bc133
commit c08dc46305
33 changed files with 5609 additions and 37 deletions

View File

@@ -0,0 +1,46 @@
'use server';
import db from '@/lib/db';
import { dataset } from '@/lib/model/dataset';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
export async function createDataset({ name }: { name: string }) {
if (!name || !name.trim()) {
throw new Error('Dataset name is required');
}
try {
const result = await db.insert(dataset).values({ name: name.trim() });
revalidatePath('/admin');
return result;
} catch (error) {
console.error('Error creating dataset:', error);
throw new Error('Failed to create dataset');
}
}
export async function updateDataset(
id: number,
updates: { name?: string }
) {
if (updates.name !== undefined && !updates.name.trim()) {
throw new Error('Dataset name cannot be empty');
}
try {
const result = await db
.update(dataset)
.set({
name: updates.name?.trim(),
updatedAt: new Date(),
})
.where(eq(dataset.id, id));
revalidatePath(`/admin/datasets/${id}`);
revalidatePath('/admin');
return result;
} catch (error) {
console.error('Error updating dataset:', error);
throw new Error('Failed to update dataset');
}
}

View File

@@ -0,0 +1,70 @@
'use server';
import db from '@/lib/db';
import { dataset_entry } from '@/lib/model/dataset_entry';
import { count, eq, and, like } from 'drizzle-orm';
const ENTRIES_PER_PAGE = 20;
interface FilterParams {
speakerId?: string;
modelName?: string;
dialect?: string;
iteration?: number;
utteranceId?: string;
}
export async function getDatasetEntries(
datasetId: number,
page: number = 1,
filters?: FilterParams
) {
const offset = (page - 1) * ENTRIES_PER_PAGE;
// Build where conditions
const conditions = [eq(dataset_entry.datasetId, datasetId)];
if (filters?.speakerId) {
conditions.push(like(dataset_entry.speakerId, `%${filters.speakerId}%`));
}
if (filters?.modelName) {
conditions.push(like(dataset_entry.modelName, `%${filters.modelName}%`));
}
if (filters?.dialect) {
conditions.push(like(dataset_entry.dialect, `%${filters.dialect}%`));
}
if (filters?.iteration !== undefined) {
conditions.push(eq(dataset_entry.iteration, filters.iteration));
}
if (filters?.utteranceId) {
conditions.push(eq(dataset_entry.utteranceId, filters.utteranceId));
}
const whereClause = and(...conditions);
const entries = await db
.select()
.from(dataset_entry)
.where(whereClause)
.limit(ENTRIES_PER_PAGE)
.offset(offset);
const totalResult = await db
.select({ count: count() })
.from(dataset_entry)
.where(whereClause);
const total = totalResult[0]?.count || 0;
const hasMore = offset + ENTRIES_PER_PAGE < total;
return {
entries,
hasMore,
total,
page,
};
}

View File

@@ -0,0 +1,27 @@
'use server';
import db from '@/lib/db';
import { dataset_entry } from '@/lib/model/dataset_entry';
import { eq } from 'drizzle-orm';
export async function getFilterOptions(datasetId: number) {
const entries = await db
.select()
.from(dataset_entry)
.where(eq(dataset_entry.datasetId, datasetId));
// Extract unique values for each filter
const speakerIds = [...new Set(entries.map(e => e.speakerId))].sort();
const modelNames = [...new Set(entries.map(e => e.modelName))].sort();
const dialects = [...new Set(entries.map(e => e.dialect))].sort();
const iterations = [...new Set(entries.map(e => e.iteration))].sort((a, b) => a - b);
const utteranceIds = [...new Set(entries.map(e => e.utteranceId).filter(Boolean))].sort();
return {
speakerIds,
modelNames,
dialects,
iterations,
utteranceIds,
};
}

View File

@@ -0,0 +1,34 @@
'use server';
import db from '@/lib/db';
import { dataset_entry } from '@/lib/model/dataset_entry';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { rm } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
export async function removeAllDatasetEntries(datasetId: number) {
try {
// Delete all entries from database
await db.delete(dataset_entry).where(eq(dataset_entry.datasetId, datasetId));
// Delete dataset files from public folder
const datasetDir = join(process.cwd(), 'public', 'datasets', datasetId.toString());
if (existsSync(datasetDir)) {
await rm(datasetDir, { recursive: true, force: true });
}
revalidatePath(`/admin/datasets/${datasetId}`);
return {
success: true,
message: 'All dataset entries have been removed',
};
} catch (error) {
console.error('Error removing dataset entries:', error);
throw new Error(
error instanceof Error ? error.message : 'Failed to remove dataset entries'
);
}
}

View File

@@ -0,0 +1,119 @@
'use server';
import db from '@/lib/db';
import { dataset_utterance } from '@/lib/model/utterance';
import { eq, and } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
export async function uploadDatasetUtterances(
datasetId: number,
formData: FormData
) {
const file = formData.get('file') as File;
if (!file) {
throw new Error('No file provided');
}
if (!file.name.endsWith('.csv')) {
throw new Error('File must be a CSV file');
}
try {
const csvContent = await file.text();
const lines = csvContent.split('\n').filter((line) => line.trim());
if (lines.length < 2) {
throw new Error('CSV is empty or invalid');
}
// Parse CSV header
const headers = lines[0].split(',').map((h) => h.trim());
if (!headers.includes('id') || !headers.includes('text')) {
throw new Error('CSV must have "id" and "text" columns');
}
// Get indices of id and text columns
const idIndex = headers.indexOf('id');
const textIndex = headers.indexOf('text');
// Parse utterances
const utterances: typeof dataset_utterance.$inferInsert[] = [];
for (let i = 1; i < lines.length; i++) {
// Handle CSV parsing with quotes
const line = lines[i];
const parts: string[] = [];
let current = '';
let inQuotes = false;
for (let j = 0; j < line.length; j++) {
const char = line[j];
const nextChar = line[j + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
current += '"';
j++; // Skip next quote
} else {
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
parts.push(current.trim());
current = '';
} else {
current += char;
}
}
parts.push(current.trim());
if (parts.length > Math.max(idIndex, textIndex)) {
const id = parts[idIndex]?.trim();
const text = parts[textIndex]?.trim();
if (id && text) {
utterances.push({
datasetId,
id,
text,
});
}
}
}
if (utterances.length === 0) {
throw new Error('No valid utterances found in CSV');
}
// Check for existing utterances to avoid duplicates
const existingUtterances = await db
.select({ id: dataset_utterance.id })
.from(dataset_utterance)
.where(eq(dataset_utterance.datasetId, datasetId));
const existingIds = new Set(existingUtterances.map((u) => u.id));
// Filter out duplicates
const newUtterances = utterances.filter((u) => !existingIds.has(u.id));
if (newUtterances.length === 0) {
throw new Error('All utterances already exist in the dataset');
}
// Insert new utterances into database
await db.insert(dataset_utterance).values(newUtterances);
revalidatePath(`/admin/datasets/${datasetId}`);
return {
success: true,
utterancesCreated: newUtterances.length,
};
} catch (error) {
console.error('Error uploading utterances:', error);
throw new Error(
error instanceof Error ? error.message : 'Failed to upload utterances'
);
}
}

157
src/app/actions/upload.ts Normal file
View File

@@ -0,0 +1,157 @@
'use server';
import db from '@/lib/db';
import { dataset_entry } from '@/lib/model/dataset_entry';
import { dataset_utterance } from '@/lib/model/utterance';
import { revalidatePath } from 'next/cache';
import { eq } from 'drizzle-orm';
import { writeFile, mkdir, readFile, rm, readdir } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import extract from 'extract-zip';
interface DatasetEntryInput {
id: string;
audio_file: string;
duration_ms: string;
utt_id: string;
speaker: string;
model: string;
dialect: string;
iteration: string;
}
export async function uploadDatasetEntries(
datasetId: number,
formData: FormData
) {
const file = formData.get('file') as File;
if (!file) {
throw new Error('No file provided');
}
if (!file.name.endsWith('.zip')) {
throw new Error('File must be a ZIP archive');
}
const tempDir = join(process.cwd(), 'tmp', `upload-${Date.now()}`);
const datasetDir = join(process.cwd(), 'public', 'datasets', datasetId.toString());
try {
// Create temp directory
await mkdir(tempDir, { recursive: true });
// Save uploaded file to temp location
const tempZipPath = join(tempDir, file.name);
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
await writeFile(tempZipPath, buffer);
// Extract ZIP file
await extract( tempZipPath, { dir: tempDir } );
// Read metadata.csv
const metadataPath = join(tempDir, 'metadata.csv');
if (!existsSync(metadataPath)) {
throw new Error('metadata.csv not found in ZIP');
}
const metadataContent = await readFile(metadataPath, 'utf-8');
const lines = metadataContent.split('\n').filter(line => line.trim());
if (lines.length < 2) {
throw new Error('metadata.csv is empty or invalid');
}
// Parse CSV header
const headers = lines[0].split(',').map(h => h.trim());
// Create dataset directory
await mkdir(datasetDir, { recursive: true });
// Parse and insert entries
const entries: typeof dataset_entry.$inferInsert[] = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim());
if (values.length !== headers.length) {
continue; // Skip malformed lines
}
const row: Record<string, string> = {};
headers.forEach((header, index) => {
row[header] = values[index];
});
const audioFile = row.audio_file as string;
// Try different path separators and remove /output prefix
const relativePath = audioFile.replace(/\\/g, '/').replace(/^output\//, '');
const audioPath = join(tempDir, relativePath);
if (existsSync(audioPath)) {
// Copy audio file to dataset directory with external ID as filename
const audioBuffer = await readFile(audioPath);
const fileExtension = audioFile.substring(audioFile.lastIndexOf('.'));
const destPath = join(datasetDir, `${row.id}${fileExtension}`);
await writeFile(destPath, audioBuffer);
entries.push({
datasetId,
externalId: row.id,
speakerId: row.speaker,
modelName: row.model,
fileName: relativePath,
utteranceId: row.utt_id || undefined,
dialect: row.dialect,
iteration: parseInt(row.iteration, 10),
});
}
}
if (entries.length === 0) {
throw new Error('No valid entries found in metadata.csv');
}
// Check for existing entries to avoid duplicates
const existingEntries = await db
.select({ externalId: dataset_entry.externalId })
.from(dataset_entry)
.where(eq(dataset_entry.datasetId, datasetId));
const existingExternalIds = new Set(existingEntries.map(e => e.externalId));
// Filter out duplicates
const newEntries = entries.filter(entry => !existingExternalIds.has(entry.externalId));
if (newEntries.length === 0) {
throw new Error('All entries already exist in the dataset');
}
// Insert new entries into database
await db.insert(dataset_entry).values(newEntries);
revalidatePath(`/admin/datasets/${datasetId}`);
return {
success: true,
entriesCreated: entries.length,
};
} catch (error) {
console.error('Error uploading dataset entries:', error);
throw new Error(
error instanceof Error ? error.message : 'Failed to upload dataset entries'
);
} finally {
// Clean up temp directory
try {
if (existsSync(tempDir)) {
await rm(tempDir, { recursive: true, force: true });
}
} catch (cleanupError) {
console.error('Error cleaning up temp directory:', cleanupError);
}
}
}