feat: implement section 11 - span export endpoints (JSON, windowed CSV, BIO-tagged CSV)
This commit is contained in:
parent
b5e4d6573e
commit
842a58f12b
2 changed files with 185 additions and 3 deletions
182
src/app/api/export/spans/route.ts
Normal file
182
src/app/api/export/spans/route.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { spanAnnotations, candles } from '@/db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const chartId = searchParams.get('chartId');
|
||||
const format = searchParams.get('format') || 'json';
|
||||
const contextPadding = parseInt(searchParams.get('context_padding') || '10', 10);
|
||||
|
||||
if (!chartId) {
|
||||
return NextResponse.json({ error: 'chartId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const chartIdNum = parseInt(chartId, 10);
|
||||
|
||||
// Fetch span annotations for the chart
|
||||
const spans = await db
|
||||
.select()
|
||||
.from(spanAnnotations)
|
||||
.where(eq(spanAnnotations.chart_id, chartIdNum))
|
||||
.orderBy(spanAnnotations.start_time);
|
||||
|
||||
// Fetch candles for the chart
|
||||
const chartCandles = await db
|
||||
.select()
|
||||
.from(candles)
|
||||
.where(eq(candles.chart_id, chartIdNum))
|
||||
.orderBy(candles.time);
|
||||
|
||||
if (format === 'json') {
|
||||
// Raw Annotations JSON export
|
||||
const exportData = {
|
||||
chart_id: chartIdNum,
|
||||
export_date: new Date().toISOString(),
|
||||
total_spans: spans.length,
|
||||
spans: spans.map((span) => ({
|
||||
id: span.id,
|
||||
start_time: span.start_time,
|
||||
end_time: span.end_time,
|
||||
label: span.label,
|
||||
confidence: span.confidence,
|
||||
outcome: span.outcome,
|
||||
notes: span.notes,
|
||||
sub_spans: span.sub_spans ? JSON.parse(span.sub_spans as string) : null,
|
||||
color: span.color,
|
||||
created_at: span.created_at,
|
||||
})),
|
||||
summary: {
|
||||
labels: spans.reduce((acc, span) => {
|
||||
acc[span.label] = (acc[span.label] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
},
|
||||
};
|
||||
|
||||
return NextResponse.json(exportData, {
|
||||
headers: {
|
||||
'Content-Disposition': `attachment; filename="span-annotations-${chartId}.json"`,
|
||||
},
|
||||
});
|
||||
} else if (format === 'windowed') {
|
||||
// Windowed Classification CSV
|
||||
const csvRows: string[] = [
|
||||
'span_id,label,confidence,outcome,start_time,end_time,context_start,context_end,' +
|
||||
'candle_time,open,high,low,close,position',
|
||||
];
|
||||
|
||||
for (const span of spans) {
|
||||
// Find candles in the span range
|
||||
const spanCandles = chartCandles.filter(
|
||||
(c) => c.time >= span.start_time && c.time <= span.end_time
|
||||
);
|
||||
|
||||
// Add context padding
|
||||
const contextStart = span.start_time - contextPadding * 60; // Assuming 1-minute candles
|
||||
const contextEnd = span.end_time + contextPadding * 60;
|
||||
|
||||
const contextCandles = chartCandles.filter(
|
||||
(c) => c.time >= contextStart && c.time <= contextEnd
|
||||
);
|
||||
|
||||
// Create one row per candle in the context window
|
||||
contextCandles.forEach((candle, idx) => {
|
||||
let position = 'context';
|
||||
if (candle.time >= span.start_time && candle.time <= span.end_time) {
|
||||
position = 'span';
|
||||
}
|
||||
|
||||
csvRows.push(
|
||||
[
|
||||
span.id,
|
||||
span.label,
|
||||
span.confidence || '',
|
||||
span.outcome || '',
|
||||
span.start_time,
|
||||
span.end_time,
|
||||
contextStart,
|
||||
contextEnd,
|
||||
candle.time,
|
||||
candle.open,
|
||||
candle.high,
|
||||
candle.low,
|
||||
candle.close,
|
||||
position,
|
||||
].join(',')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const csvContent = csvRows.join('\n');
|
||||
|
||||
return new NextResponse(csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="span-annotations-windowed-${chartId}.csv"`,
|
||||
},
|
||||
});
|
||||
} else if (format === 'bio') {
|
||||
// BIO-tagged CSV
|
||||
// One row per candle, with BIO tags for each label type
|
||||
|
||||
// Get all unique labels
|
||||
const labelTypes = Array.from(new Set(spans.map((s) => s.label)));
|
||||
|
||||
// Create header
|
||||
const header = ['time', 'open', 'high', 'low', 'close'];
|
||||
labelTypes.forEach((label) => {
|
||||
header.push(`bio_${label}`);
|
||||
});
|
||||
|
||||
const csvRows: string[] = [header.join(',')];
|
||||
|
||||
// Process each candle
|
||||
chartCandles.forEach((candle) => {
|
||||
const row = [
|
||||
candle.time.toString(),
|
||||
candle.open.toString(),
|
||||
candle.high.toString(),
|
||||
candle.low.toString(),
|
||||
candle.close.toString(),
|
||||
];
|
||||
|
||||
// For each label type, determine BIO tag
|
||||
labelTypes.forEach((label) => {
|
||||
const spansWithLabel = spans.filter((s) => s.label === label);
|
||||
|
||||
let bioTag = 'O'; // Outside by default
|
||||
|
||||
for (const span of spansWithLabel) {
|
||||
if (candle.time >= span.start_time && candle.time <= span.end_time) {
|
||||
// Check if this is the first candle in the span
|
||||
const isFirst = candle.time === span.start_time;
|
||||
bioTag = isFirst ? `B-${label}` : `I-${label}`;
|
||||
break; // Use first matching span
|
||||
}
|
||||
}
|
||||
|
||||
row.push(bioTag);
|
||||
});
|
||||
|
||||
csvRows.push(row.join(','));
|
||||
});
|
||||
|
||||
const csvContent = csvRows.join('\n');
|
||||
|
||||
return new NextResponse(csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="span-annotations-bio-${chartId}.csv"`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid format. Use json, windowed, or bio' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
return NextResponse.json({ error: 'Failed to export span annotations' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue