added calibration-entry-management options

This commit is contained in:
averel10
2026-03-27 06:53:13 +01:00
parent 8c3f009e1a
commit 5e3a96426a
8 changed files with 1106 additions and 2 deletions

View File

@@ -0,0 +1,135 @@
'use server';
import db from '@/lib/db';
import { experiment_calibration } from '@/lib/model/experiment_calibration';
import { eq, and } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { requireAdmin } from '@/lib/auth';
import { unlink } from 'fs/promises';
import { join } from 'path';
export async function getCalibrationEntries(experimentId: number) {
try {
const entries = await db
.select()
.from(experiment_calibration)
.where(eq(experiment_calibration.experimentId, experimentId));
return { success: true, data: entries };
} catch (error) {
console.error('Error fetching calibration entries:', error);
throw new Error('Failed to fetch calibration entries');
}
}
export async function deleteCalibrationEntry(calibrationId: number, experimentId: number) {
const result = await requireAdmin();
if (!result.authenticated || !result.admin) {
throw new Error('Unauthorized');
}
try {
// Get the entry to find the file path
const entries = await db
.select()
.from(experiment_calibration)
.where(
and(
eq(experiment_calibration.id, calibrationId),
eq(experiment_calibration.experimentId, experimentId)
)
);
if (entries.length === 0) {
throw new Error('Calibration entry not found');
}
const filePath = entries[0].file;
// Delete from database
await db
.delete(experiment_calibration)
.where(eq(experiment_calibration.id, calibrationId));
// Delete the audio file
try {
const fullPath = join(process.cwd(), 'public', filePath);
await unlink(fullPath);
} catch (err) {
console.warn('Could not delete audio file:', err);
// Don't fail the operation if file deletion fails
}
revalidatePath(`/admin/experiments/${experimentId}`);
return { success: true };
} catch (error) {
console.error('Error deleting calibration entry:', error);
throw new Error('Failed to delete calibration entry');
}
}
export async function updateCalibrationOrder(experimentId: number, orderMap: Record<number, number>) {
const result = await requireAdmin();
if (!result.authenticated || !result.admin) {
throw new Error('Unauthorized');
}
try {
// Update all entries with new order values
const updates = Object.entries(orderMap).map(([calibrationId, newOrder]) =>
db
.update(experiment_calibration)
.set({ order: newOrder })
.where(
and(
eq(experiment_calibration.id, parseInt(calibrationId)),
eq(experiment_calibration.experimentId, experimentId)
)
)
);
await Promise.all(updates);
revalidatePath(`/admin/experiments/${experimentId}`);
return { success: true };
} catch (error) {
console.error('Error updating calibration order:', error);
throw new Error('Failed to update calibration order');
}
}
export async function deleteAllCalibrationEntries(experimentId: number) {
const result = await requireAdmin();
if (!result.authenticated || !result.admin) {
throw new Error('Unauthorized');
}
try {
// Get all entries to delete files
const entries = await db
.select()
.from(experiment_calibration)
.where(eq(experiment_calibration.experimentId, experimentId));
// Delete audio files
for (const entry of entries) {
try {
const fullPath = join(process.cwd(), 'public', entry.file);
await unlink(fullPath);
} catch (err) {
console.warn('Could not delete audio file:', err);
// Don't fail the operation if file deletion fails
}
}
// Delete all entries from database
await db
.delete(experiment_calibration)
.where(eq(experiment_calibration.experimentId, experimentId));
revalidatePath(`/admin/experiments/${experimentId}`);
return { success: true, deletedCount: entries.length };
} catch (error) {
console.error('Error deleting all calibration entries:', error);
throw new Error('Failed to delete all calibration entries');
}
}

View File

@@ -2,6 +2,7 @@
import db from '@/lib/db';
import { dataset_entry } from '@/lib/model/dataset_entry';
import { experiment_calibration } from '@/lib/model/experiment_calibration';
import { parseCSVLine } from '@/lib/csv-parser';
import { revalidatePath } from 'next/cache';
import { eq } from 'drizzle-orm';
@@ -302,3 +303,122 @@ export async function updateDatasetEntriesMetadata(
}
}
}
/**
* Process calibration audio files for an experiment
* Expects metadata.csv with columns: id, audio_file, dialect, order
* Audio files should be in a 'wavs' subfolder
*/
export async function processCalibrationEntries(
experimentId: number,
tempDir: string
) {
try {
const result = await requireAdmin();
if (!result.authenticated || !result.admin) {
return {
success: false,
error: 'Unauthorized',
entriesCreated: 0,
};
}
const calibrationDir = join(process.cwd(), 'public', 'calibration', experimentId.toString());
// Validate temp directory exists
if (!existsSync(tempDir)) {
return {
success: false,
error: 'Temporary extraction directory not found',
entriesCreated: 0,
};
}
// Parse metadata from directory
const { rows } = await parseMetadataFromDirectory(tempDir);
const entries: typeof experiment_calibration.$inferInsert[] = [];
// Create calibration directory
await mkdir(calibrationDir, { recursive: true });
// Process each row
for (let lineIndex = 0; lineIndex < rows.length; lineIndex++) {
const row = rows[lineIndex];
const audioFile = row.audio_file as string;
// Audio files are in wavs subfolder
const audioPath = join(tempDir, audioFile);
if (existsSync(audioPath)) {
// Copy audio file to calibration directory, preserving filename from audio_file
const audioBuffer = await readFile(audioPath);
const filename = audioFile.split('/').pop() || audioFile;
const destPath = join(calibrationDir, filename);
await writeFile(destPath, audioBuffer);
entries.push({
experimentId,
dialectLabel: row.dialect,
order: lineIndex + 1,
file: `/calibration/${experimentId}/${filename}`,
});
}
}
if (entries.length === 0) {
return {
success: false,
error: 'No valid audio files found in wavs folder',
entriesCreated: 0,
};
}
// Check for existing calibration entries to avoid duplicates
const existingEntries = await db
.select({ file: experiment_calibration.file })
.from(experiment_calibration)
.where(eq(experiment_calibration.experimentId, experimentId));
const existingFiles = new Set(existingEntries.map(e => e.file));
// Filter out duplicates
const newEntries = entries.filter(entry => !existingFiles.has(entry.file));
if (newEntries.length === 0) {
return {
success: false,
error: 'All calibration entries already exist for this experiment',
entriesCreated: 0,
};
}
// Insert new entries into database
await db.insert(experiment_calibration).values(newEntries);
revalidatePath(`/admin/experiments/${experimentId}`);
return {
success: true,
entriesCreated: newEntries.length,
message: `Successfully processed and inserted ${newEntries.length} calibration entries.`,
};
} catch (error) {
console.error('Error processing calibration entries:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to process calibration entries';
return {
success: false,
error: errorMessage,
entriesCreated: 0,
};
} 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);
}
}
}