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>
455 lines
No EOL
14 KiB
TypeScript
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; |