video-accessibility/frontend/src/lib/api.ts
michael e371dc401a feat: add save button to voice settings panel for TTS regeneration
Add ability to save voice settings changes in QC Review screen without
needing to approve the job. When saved, all TTS segments are regenerated
across all languages with the new voice settings.

Changes:
- Add PUT /jobs/{id}/tts-preferences endpoint to update TTS preferences
- Add UpdateTTSPreferencesRequest schema
- Add updateTTSPreferences API method and useUpdateTTSPreferences hook
- Add Save Voice Settings button with change detection to QCDetail

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:05:56 -06:00

455 lines
No EOL
14 KiB
TypeScript

import axios from 'axios';
import type { AxiosInstance } from 'axios';
import type {
LoginRequest,
LoginResponse,
RefreshResponse,
MicrosoftLoginResponse,
Job,
JobCreateRequest,
JobListResponse,
JobDownloadsResponse,
VttContentResponse,
VttUpdateRequest,
AssetValidationResponse,
BulkDeleteRequest,
BulkDeleteResponse,
BulkApproveRequest,
BulkApproveResponse,
JobDeleteResponse,
User,
UserListResponse,
CreateUserRequest,
UpdateUserRequest,
ResetPasswordResponse,
AdminStatsResponse,
VoicesResponse,
LanguagesResponse,
TTSPreferences,
TTSOptionsResponse,
TTSStylePreset,
AccessibleVideoMethod,
ReviewNote,
ReviewNoteCreateRequest,
ReviewNoteUpdateRequest,
ReviewNotesListResponse,
AccessibleVideoEditState,
PausePointData,
} from '../types/api';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
class ApiClient {
private client: AxiosInstance;
private accessToken: string | null = null;
constructor() {
this.client = axios.create({
baseURL: `${API_BASE_URL}/api/v1`,
withCredentials: true,
timeout: 30000,
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor to add auth token
this.client.interceptors.request.use(
(config) => {
if (this.accessToken) {
config.headers.Authorization = `Bearer ${this.accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor to handle token refresh
this.client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Don't try to refresh if this is already the refresh endpoint
if (error.response?.status === 401 &&
!originalRequest._retry &&
originalRequest.url !== '/auth/refresh') {
originalRequest._retry = true;
try {
const refreshResponse = await this.client.post('/auth/refresh');
this.accessToken = refreshResponse.data.access_token;
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${this.accessToken}`;
return this.client(originalRequest);
} catch (refreshError) {
// Refresh failed, clear token and update auth state
this.accessToken = null;
// Clear auth state in the store
const { useAuthStore } = await import('./auth');
useAuthStore.getState().logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
}
setAccessToken(token: string) {
this.accessToken = token;
}
clearAccessToken() {
this.accessToken = null;
}
getAccessToken(): string | null {
return this.accessToken;
}
// Auth endpoints
async login(credentials: LoginRequest): Promise<LoginResponse> {
const response = await this.client.post('/auth/login', credentials);
this.setAccessToken(response.data.access_token);
return response.data;
}
async loginWithMicrosoft(idToken: string): Promise<MicrosoftLoginResponse> {
const response = await this.client.post('/auth/microsoft', { id_token: idToken });
this.setAccessToken(response.data.access_token);
return response.data;
}
async refresh(): Promise<RefreshResponse> {
const response = await this.client.post('/auth/refresh');
this.setAccessToken(response.data.access_token);
return response.data;
}
async logout(): Promise<void> {
await this.client.post('/auth/logout');
this.clearAccessToken();
}
// Job endpoints
async getJobs(filters?: { status?: string; mine?: boolean; page?: number; size?: number }): Promise<JobListResponse> {
const params = new URLSearchParams();
if (filters?.status) params.append('status', filters.status);
if (filters?.mine) params.append('mine', 'true');
if (filters?.page) params.append('page', filters.page.toString());
if (filters?.size) params.append('size', filters.size.toString());
const response = await this.client.get(`/jobs?${params.toString()}`);
return response.data;
}
async getJob(id: string): Promise<Job> {
const response = await this.client.get(`/jobs/${id}`);
return response.data;
}
async createJob(data: JobCreateRequest, file: File, onUploadProgress?: (progressEvent: { loaded: number; total: number }) => void): Promise<Job> {
const formData = new FormData();
formData.append('title', data.title);
formData.append('requested_outputs', JSON.stringify(data.requested_outputs));
formData.append('file', file);
const response = await this.client.post('/jobs', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 0, // No timeout for video uploads (can be large files)
onUploadProgress: onUploadProgress ? (progressEvent) => {
if (progressEvent.total) {
onUploadProgress({
loaded: progressEvent.loaded,
total: progressEvent.total
});
}
} : undefined,
});
return response.data;
}
async updateJob(id: string, data: Partial<Job>): Promise<Job> {
const response = await this.client.patch(`/jobs/${id}`, data);
return response.data;
}
async approveEnglish(id: string, notes?: string): Promise<Job> {
// Legacy method - calls approve_source for backwards compatibility
return this.approveSource(id, notes);
}
async approveSource(
id: string,
notes?: string,
tts_preferences?: TTSPreferences,
accessible_video_method?: AccessibleVideoMethod
): Promise<Job> {
const response = await this.client.post(`/jobs/${id}/actions/approve_source`, {
notes,
tts_preferences,
accessible_video_method
});
return response.data;
}
async rejectJob(id: string, notes: string): Promise<Job> {
const response = await this.client.post(`/jobs/${id}/actions/reject`, { notes });
return response.data;
}
async completeJob(id: string, notes?: string): Promise<Job> {
const response = await this.client.post(`/jobs/${id}/actions/complete`, { notes });
return response.data;
}
async rejectFinalReview(id: string, notes: string): Promise<Job> {
const response = await this.client.post(`/jobs/${id}/actions/reject_final`, { notes });
return response.data;
}
async updateTTSPreferences(id: string, tts_preferences: TTSPreferences): Promise<Job> {
const response = await this.client.put(`/jobs/${id}/tts-preferences`, {
tts_preferences
});
return response.data;
}
async getJobDownloads(id: string): Promise<JobDownloadsResponse> {
const response = await this.client.get(`/jobs/${id}/downloads`);
return response.data;
}
async getJobVttContent(id: string, language?: string): Promise<VttContentResponse> {
const params = language ? `?language=${language}` : '';
const response = await this.client.get(`/jobs/${id}/vtt${params}`);
return response.data;
}
async updateJobVttContent(id: string, data: VttUpdateRequest): Promise<Job> {
const response = await this.client.patch(`/jobs/${id}/vtt`, data);
return response.data;
}
async adjustVttTiming(id: string, data: {
offset_seconds: number;
language?: string;
adjust_captions?: boolean;
adjust_audio_description?: boolean;
}): Promise<Job> {
const response = await this.client.post(`/jobs/${id}/vtt/adjust-timing`, data);
return response.data;
}
async validateJobAssets(id: string): Promise<AssetValidationResponse> {
const response = await this.client.get(`/jobs/${id}/validate`);
return response.data;
}
async deleteJob(id: string): Promise<JobDeleteResponse> {
const response = await this.client.delete(`/jobs/${id}`);
return response.data;
}
async bulkDeleteJobs(data: BulkDeleteRequest): Promise<BulkDeleteResponse> {
const response = await this.client.delete('/jobs/bulk', { data });
return response.data;
}
async bulkApproveJobs(data: BulkApproveRequest): Promise<BulkApproveResponse> {
const response = await this.client.post('/jobs/bulk/approve', data);
return response.data;
}
async bulkDownloadJobs(jobIds: string[]): Promise<Blob> {
const response = await this.client.post(
'/jobs/bulk/download',
{ job_ids: jobIds },
{
responseType: 'blob',
timeout: 0, // No timeout for large downloads
}
);
return response.data;
}
async reprocessJob(id: string): Promise<{ message: string }> {
const response = await this.client.post(`/admin/maintenance/reprocess-job/${id}`);
return response.data;
}
async retryTts(id: string): Promise<Job> {
const response = await this.client.post(`/jobs/${id}/actions/retry_tts`);
return response.data;
}
// User Management endpoints
async listUsers(filters?: {
page?: number;
size?: number;
role?: string;
active_only?: boolean;
}): Promise<UserListResponse> {
const params = new URLSearchParams();
if (filters?.page) params.append('page', filters.page.toString());
if (filters?.size) params.append('size', filters.size.toString());
if (filters?.role) params.append('role', filters.role);
if (filters?.active_only !== undefined) params.append('active_only', filters.active_only.toString());
const response = await this.client.get(`/admin/users?${params.toString()}`);
return response.data;
}
async getUser(userId: string): Promise<User> {
const response = await this.client.get(`/admin/users/${userId}`);
return response.data;
}
async createUser(data: CreateUserRequest): Promise<User> {
const response = await this.client.post('/admin/users', data);
return response.data;
}
async updateUser(userId: string, data: UpdateUserRequest): Promise<User> {
const response = await this.client.patch(`/admin/users/${userId}`, data);
return response.data;
}
async deactivateUser(userId: string): Promise<{ message: string }> {
const response = await this.client.delete(`/admin/users/${userId}`);
return response.data;
}
async resetUserPassword(userId: string): Promise<ResetPasswordResponse> {
const response = await this.client.post(`/admin/users/${userId}/password/reset`);
return response.data;
}
async getAdminStats(): Promise<AdminStatsResponse> {
const response = await this.client.get('/admin/stats');
return response.data;
}
// TTS endpoints
async getVoices(): Promise<VoicesResponse> {
const response = await this.client.get('/tts/voices');
return response.data;
}
async getLanguages(): Promise<LanguagesResponse> {
const response = await this.client.get('/tts/languages');
return response.data;
}
async getTTSOptions(): Promise<TTSOptionsResponse> {
const response = await this.client.get('/tts/options');
return response.data;
}
async previewVoice(
voiceName: string,
language: string,
model?: string,
speed?: number,
stylePreset?: TTSStylePreset,
customStylePrompt?: string
): Promise<Blob> {
const response = await this.client.post(
'/tts/preview',
{
voice_name: voiceName,
language,
model: model || 'flash',
speed: speed || 1.0,
style_preset: stylePreset || 'neutral',
custom_style_prompt: customStylePrompt
},
{ responseType: 'blob' }
);
return response.data;
}
// Review Notes endpoints
async getReviewNotes(jobId: string, assetKey?: string): Promise<ReviewNotesListResponse> {
const params = assetKey ? `?asset_key=${encodeURIComponent(assetKey)}` : '';
const response = await this.client.get(`/jobs/${jobId}/review-notes${params}`);
return response.data;
}
async createReviewNote(jobId: string, data: ReviewNoteCreateRequest): Promise<ReviewNote> {
const response = await this.client.post(`/jobs/${jobId}/review-notes`, data);
return response.data;
}
async updateReviewNote(jobId: string, noteId: string, data: ReviewNoteUpdateRequest): Promise<ReviewNote> {
const response = await this.client.patch(`/jobs/${jobId}/review-notes/${noteId}`, data);
return response.data;
}
async deleteReviewNote(jobId: string, noteId: string): Promise<void> {
await this.client.delete(`/jobs/${jobId}/review-notes/${noteId}`);
}
// Accessible Video QC Editing endpoints
async getAccessibleVideoEditState(jobId: string, language: string): Promise<AccessibleVideoEditState> {
const response = await this.client.get(`/jobs/${jobId}/accessible-video/${language}/edit-state`);
return response.data;
}
async updatePausePoint(
jobId: string,
language: string,
cueIndex: number,
adjustedMs: number
): Promise<PausePointData> {
const response = await this.client.patch(
`/jobs/${jobId}/accessible-video/${language}/pause-points/${cueIndex}`,
{ adjusted_ms: adjustedMs }
);
return response.data;
}
async queueTTSRegeneration(
jobId: string,
language: string,
cueIndices: number[]
): Promise<{ message: string; queued_cues: number[] }> {
const response = await this.client.post(
`/jobs/${jobId}/accessible-video/${language}/tts-regeneration`,
{ cue_indices: cueIndices }
);
return response.data;
}
async removeTTSRegeneration(
jobId: string,
language: string,
cueIndex: number
): Promise<{ message: string }> {
const response = await this.client.delete(
`/jobs/${jobId}/accessible-video/${language}/tts-regeneration/${cueIndex}`
);
return response.data;
}
async rerenderAccessibleVideo(
jobId: string,
language: string,
whisperRefine: boolean = false
): Promise<Job> {
const response = await this.client.post(
`/jobs/${jobId}/accessible-video/${language}/re-render`,
{ whisper_refine: whisperRefine }
);
return response.data;
}
}
export const apiClient = new ApiClient();
export const api = apiClient;