diff --git a/app/api/evaluations/stt/datasets/[dataset_id]/route.ts b/app/api/evaluations/stt/datasets/[dataset_id]/route.ts index 4455161..add9a77 100644 --- a/app/api/evaluations/stt/datasets/[dataset_id]/route.ts +++ b/app/api/evaluations/stt/datasets/[dataset_id]/route.ts @@ -1,4 +1,4 @@ -import { NextResponse, NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; export async function GET( request: Request, @@ -8,6 +8,13 @@ export async function GET( const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; const apiKey = request.headers.get('X-API-KEY'); + if (!apiKey) { + return NextResponse.json( + { success: false, error: 'Unauthorized: Missing API key', data: null }, + { status: 401 } + ); + } + try { // Get query parameters from the request const { searchParams } = new URL(request.url); @@ -19,13 +26,12 @@ export async function GET( if (includeSamples) backendParams.append('include_samples', includeSamples); if (includeAudio) backendParams.append('include_audio', includeAudio); - const backendUrlWithParams = `${backendUrl}/api/v1/evaluations/stt/datasets/${dataset_id}${ - backendParams.toString() ? `?${backendParams.toString()}` : '' - }`; + const backendUrlWithParams = `${backendUrl}/api/v1/evaluations/stt/datasets/${dataset_id}${backendParams.toString() ? `?${backendParams.toString()}` : '' + }`; const response = await fetch(backendUrlWithParams, { headers: { - 'X-API-KEY': apiKey || '', + 'X-API-KEY': apiKey, }, }); diff --git a/app/api/evaluations/tts/datasets/[dataset_id]/route.ts b/app/api/evaluations/tts/datasets/[dataset_id]/route.ts new file mode 100644 index 0000000..309b178 --- /dev/null +++ b/app/api/evaluations/tts/datasets/[dataset_id]/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ dataset_id: string }> } +) { + const { dataset_id } = await params; + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { success: false, error: 'Unauthorized: Missing API key', data: null }, + { status: 401 } + ); + } + + try { + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/datasets/${dataset_id}`, { + headers: { + 'X-API-KEY': apiKey, + }, + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Failed to fetch dataset', data: null }, + { status: 500 } + ); + } +} diff --git a/app/api/evaluations/tts/datasets/route.ts b/app/api/evaluations/tts/datasets/route.ts new file mode 100644 index 0000000..4fe9f11 --- /dev/null +++ b/app/api/evaluations/tts/datasets/route.ts @@ -0,0 +1,53 @@ +import { NextResponse, NextRequest } from 'next/server'; + +export async function GET(request: Request) { + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + try { + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/datasets`, { + headers: { + 'X-API-KEY': apiKey || '', + }, + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: error, data: null }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const apiKey = request.headers.get('X-API-KEY'); + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY. Either generate an API Key. Contact Kaapi team for more details' }, + { status: 401 } + ); + } + const body = await request.json(); + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/datasets`, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} diff --git a/app/api/evaluations/tts/results/[result_id]/route.ts b/app/api/evaluations/tts/results/[result_id]/route.ts new file mode 100644 index 0000000..126d21e --- /dev/null +++ b/app/api/evaluations/tts/results/[result_id]/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ result_id: string }> } +) { + const { result_id } = await params; + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + try { + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/results/${result_id}`, { + headers: { + 'X-API-KEY': apiKey || '', + }, + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Failed to fetch results', data: null }, + { status: 500 } + ); + } +} + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ result_id: string }> } +) { + const { result_id } = await params; + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY header' }, + { status: 401 } + ); + } + + try { + const body = await request.json(); + + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/results/${result_id}`, { + method: 'PATCH', + headers: { + 'X-API-KEY': apiKey || '', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Failed to update result feedback', data: null }, + { status: 500 } + ); + } +} diff --git a/app/api/evaluations/tts/runs/[run_id]/route.ts b/app/api/evaluations/tts/runs/[run_id]/route.ts new file mode 100644 index 0000000..e782d4a --- /dev/null +++ b/app/api/evaluations/tts/runs/[run_id]/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ run_id: string }> } +) { + const { run_id } = await params; + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + const { searchParams } = new URL(request.url); + const queryString = searchParams.toString(); + + try { + const backendUrlWithParams = queryString + ? `${backendUrl}/api/v1/evaluations/tts/runs/${run_id}?${queryString}` + : `${backendUrl}/api/v1/evaluations/tts/runs/${run_id}`; + + const response = await fetch(backendUrlWithParams, { + headers: { + 'X-API-KEY': apiKey || '', + }, + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Failed to fetch the run', data: null }, + { status: 500 } + ); + } +} diff --git a/app/api/evaluations/tts/runs/route.ts b/app/api/evaluations/tts/runs/route.ts new file mode 100644 index 0000000..9cd9143 --- /dev/null +++ b/app/api/evaluations/tts/runs/route.ts @@ -0,0 +1,53 @@ +import { NextResponse, NextRequest } from 'next/server'; + +export async function GET(request: Request) { + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + const apiKey = request.headers.get('X-API-KEY'); + + try { + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/runs`, { + headers: { + 'X-API-KEY': apiKey || '', + }, + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + return NextResponse.json( + { success: false, error: error, data: null }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const apiKey = request.headers.get('X-API-KEY'); + if (!apiKey) { + return NextResponse.json( + { error: 'Missing X-API-KEY. Either generate an API Key. Contact Kaapi team for more details' }, + { status: 401 } + ); + } + const body = await request.json(); + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'; + + const response = await fetch(`${backendUrl}/api/v1/evaluations/tts/runs`, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'X-API-KEY': apiKey, + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + console.error('Proxy error:', error); + return NextResponse.json( + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} diff --git a/app/components/DetailedResultsTable.tsx b/app/components/DetailedResultsTable.tsx index 8b7f7ef..f95540a 100644 --- a/app/components/DetailedResultsTable.tsx +++ b/app/components/DetailedResultsTable.tsx @@ -118,10 +118,10 @@ export default function DetailedResultsTable({ job }: DetailedResultsTableProps) Question - Answer + Ground Truth - Ground Truth + Answer {scoreNames.map((scoreName) => ( @@ -153,12 +153,12 @@ export default function DetailedResultsTable({ job }: DetailedResultsTableProps) }} > {/* Row Number */} - + {index + 1} {/* Question */} - +
- {/* Answer */} - + {/* Ground Truth */} +
- {answer} + {groundTruth}
- {/* Ground Truth */} - + {/* Answer */} +
- {groundTruth} + {answer}
@@ -205,7 +205,7 @@ export default function DetailedResultsTable({ job }: DetailedResultsTableProps) const { value, color, bg } = formatScoreValue(score); return ( - +
{/* Question ID - matching row format # column */} - + {group.question_id} {/* Question - matching row format text cell */} - +
{/* Ground Truth - matching row format text cell */} - +
+ {answer ? (
-
- {tabs.map((tab) => { - const isActive = activeTab === tab.id; - return ( - - ); - })} -
+
+ {tabs.map((tab) => { + const isActive = activeTab === tab.id; + return ( + + ); + })}
); } diff --git a/app/evaluations/page.tsx b/app/evaluations/page.tsx index 2b81ec7..b4a633b 100644 --- a/app/evaluations/page.tsx +++ b/app/evaluations/page.tsx @@ -9,6 +9,7 @@ "use client" import { useState, useEffect, useCallback, Suspense } from 'react'; import {format, toZonedTime} from "date-fns-tz" +import { colors } from '@/app/lib/colors'; import { useRouter, useSearchParams } from 'next/navigation' import { APIKey } from '../keystore/page'; import { STORAGE_KEY } from '../keystore/page'; @@ -280,7 +281,7 @@ function SimplifiedEvalContent() { }; return ( -
+
{/* Sidebar - Full Height */} @@ -288,52 +289,20 @@ function SimplifiedEvalContent() { {/* Main Content */}
{/* Title Section with Collapse Button */} -
+
-

Evaluations

+

Text Evaluation

+

Compare model response quality on your datasets across different configs

@@ -349,8 +318,8 @@ function SimplifiedEvalContent() { /> {/* Tab Content */} -
-
+
+
{activeTab === 'datasets' ? ( +
{/* API Key Selection Card */} -
-
-

Select API Key

+
+

Select API Key

{/* Dataset Selection Card */} -
-
-

Select QnA Dataset

+
+

Select QnA Dataset

{/* Loading State */} {isLoading && evalJobs.length === 0 && ( - + )} {/* Error State */} {error && ( -
@@ -1007,18 +967,17 @@ function EvaluationsTab({ apiKeys, selectedKeyId }: EvaluationsTabProps) {
)} - {/* No Jobs Yet */} + {/* No Runs Yet */} {!isLoading && evalJobs.length === 0 && !error && (
-

No evaluation jobs found. Create one from the Datasets tab!

+

No evaluation runs found. Create one from the Datasets tab!

)} - {/* Evaluation Job Cards */} + {/* Evaluation Run Cards */} {evalJobs.length > 0 && (
{evalJobs.map((job) => ( diff --git a/app/text-to-speech/page.tsx b/app/text-to-speech/page.tsx new file mode 100644 index 0000000..43445f2 --- /dev/null +++ b/app/text-to-speech/page.tsx @@ -0,0 +1,1679 @@ +/** + * Text-to-Speech Evaluation Page + * + * Tab 1 - Datasets: Create datasets with text samples + * Tab 2 - Evaluations: Run and monitor TTS evaluations + */ + +"use client" +import { useState, useEffect, useRef } from 'react'; +import { colors } from '@/app/lib/colors'; +import Sidebar from '@/app/components/Sidebar'; +import { useToast } from '@/app/components/Toast'; +import { APIKey, STORAGE_KEY } from '@/app/keystore/page'; +import ErrorModal from '@/app/components/ErrorModal'; + +type Tab = 'datasets' | 'evaluations'; + +// Types +interface TextSample { + id: string; + text: string; +} + +interface Language { + id: number; + code: string; + name: string; +} + +const DEFAULT_LANGUAGES: Language[] = [ + { id: 1, code: 'en', name: 'English' }, + { id: 2, code: 'hi', name: 'Hindi' }, +]; + +interface TTSDataset { + id: number; + name: string; + description?: string; + type: string; + object_store_url: string | null; + dataset_metadata: { + sample_count?: number; + [key: string]: any; + }; + organization_id: number; + project_id: number; + inserted_at: string; + updated_at: string; +} + +interface TTSRun { + id: number; + run_name: string; + dataset_name: string; + dataset_id: number; + type: string; + models: string[] | null; + status: string; + total_items: number; + score: { + [key: string]: any; + } | null; + error_message: string | null; + run_metadata: { + voice_name?: string; + style_prompt?: string; + [key: string]: any; + } | null; + organization_id: number; + project_id: number; + inserted_at: string; + updated_at: string; +} + +interface TTSResult { + id: number; + sample_text: string; + object_store_url: string | null; + duration_seconds: number | null; + size_bytes: number | null; + provider: string; + status: string; + score: { + [key: string]: any; + } | null; + is_correct: boolean | null; + comment: string | null; + error_message: string | null; + evaluation_run_id: number; + organization_id: number; + project_id: number; + inserted_at: string; + updated_at: string; + signedUrl?: string; // Enriched field - signed URL for audio playback +} + +// Audio Player Component for URL-based playback +function AudioPlayerFromUrl({ + signedUrl, + isPlaying, + onPlayToggle, + sampleLabel, + durationSeconds, + sizeBytes, +}: { + signedUrl: string; + isPlaying: boolean; + onPlayToggle: () => void; + sampleLabel?: string; + durationSeconds?: number | null; + sizeBytes?: number | null; +}) { + const audioRef = useRef(null); + const [duration, setDuration] = useState(0); + const [currentTime, setCurrentTime] = useState(0); + + useEffect(() => { + const audio = audioRef.current; + if (!audio) return; + + const handleLoadedMetadata = () => setDuration(audio.duration); + const handleTimeUpdate = () => setCurrentTime(audio.currentTime); + const handleEnded = () => onPlayToggle(); + + audio.addEventListener('loadedmetadata', handleLoadedMetadata); + audio.addEventListener('timeupdate', handleTimeUpdate); + audio.addEventListener('ended', handleEnded); + + return () => { + audio.removeEventListener('loadedmetadata', handleLoadedMetadata); + audio.removeEventListener('timeupdate', handleTimeUpdate); + audio.removeEventListener('ended', handleEnded); + }; + }, [onPlayToggle]); + + useEffect(() => { + if (!audioRef.current) return; + if (isPlaying) { + audioRef.current.play().catch(console.error); + } else { + audioRef.current.pause(); + } + }, [isPlaying]); + + const formatSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + }; + + return ( +
+