/** * API Client for Text-to-Audiobook Backend * Handles communication with the FastAPI backend for role detection and audiobook generation */ // ============================================================================ // TypeScript Interfaces // ============================================================================ /** * Represents a detected role/speaker in the text */ export interface Role { id: string; label: string; } /** * Extends Role with an assigned voice ID */ export interface RoleWithVoice extends Role { voice: string; } /** * Response from the role detection endpoint */ export interface RolesResponse { roles: Role[]; } /** * Request payload for audiobook generation */ export interface GenerateRequest { text: string; lang: string; roles: { id: string; voice: string }[]; speed?: number; } /** * Health check response */ export interface HealthResponse { status: string; } // ============================================================================ // Configuration // ============================================================================ /** * Get the API base URL from environment variables * Falls back to localhost if not configured */ const getApiBaseUrl = (): string => { const baseUrl = import.meta.env.VITE_API_BASE_URL; if (!baseUrl) { console.warn('VITE_API_BASE_URL not configured, using default: http://localhost:8000'); return 'http://localhost:8000'; } return baseUrl; }; // ============================================================================ // Error Handling // ============================================================================ /** * Custom error class for API errors */ export class ApiError extends Error { statusCode?: number; details?: unknown; constructor( message: string, statusCode?: number, details?: unknown ) { super(message); this.name = 'ApiError'; this.statusCode = statusCode; this.details = details; } } /** * Handle fetch errors and convert to ApiError */ const handleFetchError = async (response: Response): Promise => { let errorMessage = `HTTP ${response.status}: ${response.statusText}`; let errorDetails: unknown = undefined; try { const errorData = await response.json(); if (errorData.message) { errorMessage = errorData.message; } errorDetails = errorData; } catch { // If response is not JSON, use status text } throw new ApiError(errorMessage, response.status, errorDetails); }; /** * Wrapper for fetch with error handling */ const fetchWithErrorHandling = async ( url: string, options?: RequestInit ): Promise => { try { const response = await fetch(url, options); if (!response.ok) { await handleFetchError(response); } return response; } catch (error) { if (error instanceof ApiError) { throw error; } // Network errors (connection refused, timeout, etc.) if (error instanceof TypeError) { throw new ApiError( 'Network error: Unable to connect to the server. Please check if the backend is running.', undefined, error ); } // Unknown errors throw new ApiError( 'An unexpected error occurred', undefined, error ); } }; // ============================================================================ // API Functions // ============================================================================ /** * Check the health status of the backend service * @returns Promise resolving to health status * @throws ApiError if the request fails */ export async function checkHealth(): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/health`; const response = await fetchWithErrorHandling(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); return await response.json(); } /** * Detect roles/speakers in the provided text * @param text - The input text to analyze for roles * @returns Promise resolving to detected roles * @throws ApiError if the request fails */ export async function detectRoles(text: string): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/tts/roles`; const response = await fetchWithErrorHandling(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text }), }); return await response.json(); } /** * Generate an audiobook from text with assigned voices * @param payload - Generation request with text, language, roles, and speed * @returns Promise resolving to audio blob * @throws ApiError if the request fails */ export async function generateAudiobook(payload: GenerateRequest): Promise { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/tts`; const response = await fetchWithErrorHandling(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); // Convert response to blob for audio data return await response.blob(); }