Files
audiobook/frontend/src/api/tts.ts
Paul Bailey 50d657f501 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
2025-12-09 01:31:01 +00:00

212 lines
5.1 KiB
TypeScript

/**
* 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<never> => {
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<Response> => {
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<HealthResponse> {
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<RolesResponse> {
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<Blob> {
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();
}