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
This commit is contained in:
211
frontend/src/api/tts.ts
Normal file
211
frontend/src/api/tts.ts
Normal file
@@ -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<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();
|
||||
}
|
||||
Reference in New Issue
Block a user