From 50d657f501589797da5ce9e15d20cd21503a8ba8 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 9 Dec 2025 01:31:01 +0000 Subject: [PATCH] feat: implement API client module for frontend - Create TypeScript interfaces (Role, RoleWithVoice, RolesResponse, GenerateRequest) - Implement detectRoles() function for role detection - Implement generateAudiobook() function for audio generation - Implement checkHealth() function for backend health checks - Add comprehensive error handling for network failures - Use VITE_API_BASE_URL environment variable for configuration - Add ApiError class for structured error handling Requirements: 9.6, 9.7 --- .kiro/specs/text-to-audiobook/tasks.md | 2 +- frontend/src/api/tts.ts | 211 +++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 frontend/src/api/tts.ts diff --git a/.kiro/specs/text-to-audiobook/tasks.md b/.kiro/specs/text-to-audiobook/tasks.md index 3571bed..dfd32c7 100644 --- a/.kiro/specs/text-to-audiobook/tasks.md +++ b/.kiro/specs/text-to-audiobook/tasks.md @@ -137,7 +137,7 @@ - Configure environment variables (.env file with VITE_API_BASE_URL) - _Requirements: 9.6, 12.3_ -- [ ] 8. Implement API client module +- [x] 8. Implement API client module - Create src/api/tts.ts with TypeScript interfaces (Role, RoleWithVoice, RolesResponse, GenerateRequest) - Implement detectRoles() function - Implement generateAudiobook() function diff --git a/frontend/src/api/tts.ts b/frontend/src/api/tts.ts new file mode 100644 index 0000000..4562741 --- /dev/null +++ b/frontend/src/api/tts.ts @@ -0,0 +1,211 @@ +/** + * 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(); +}