191 lines
7.0 KiB
TypeScript
191 lines
7.0 KiB
TypeScript
const BASE_URL = '/api';
|
|
|
|
// Flat settings schema - must mirror backend/app/config.py's DEFAULT_SETTINGS
|
|
// exactly. This is the single source of truth; don't reintroduce a nested
|
|
// shape here without updating config.py, routers/settings.py, Setup.tsx and
|
|
// Settings.tsx together.
|
|
export interface AppSettings {
|
|
onboarding_completed: boolean;
|
|
whisper_model: string;
|
|
whisper_compute_type: string;
|
|
hf_token: string;
|
|
llm_mode: 'ollama' | 'api';
|
|
ollama_host: string;
|
|
ollama_model: string;
|
|
api_base_url: string;
|
|
api_key: string;
|
|
api_model: string;
|
|
chunk_word_target: string;
|
|
world_context: string;
|
|
world_context_path: string;
|
|
}
|
|
|
|
export type JobStatus = 'queued' | 'running' | 'done' | 'error';
|
|
|
|
export interface Job {
|
|
id: string;
|
|
session_id: string;
|
|
job_type: 'transcribe' | 'summarize';
|
|
status: JobStatus;
|
|
progress: string | null;
|
|
error: string | null;
|
|
error_stage: string | null;
|
|
created_at: number;
|
|
updated_at: number;
|
|
}
|
|
|
|
export type DeleteStrategy = 'all' | 'artifacts_only' | 'none';
|
|
|
|
export const jobApi = {
|
|
getJob: async (jobId: string): Promise<Job> => {
|
|
const res = await fetch(`${BASE_URL}/jobs/${jobId}`);
|
|
if (!res.ok) throw new Error('Failed to fetch job');
|
|
return res.json();
|
|
},
|
|
cancelJob: async (jobId: string): Promise<any> => {
|
|
const res = await fetch(`${BASE_URL}/jobs/${jobId}/cancel`, { method: 'POST' });
|
|
return res.json();
|
|
},
|
|
deleteJob: async (jobId: string, strategy: DeleteStrategy): Promise<any> => {
|
|
const res = await fetch(`${BASE_URL}/jobs/${jobId}?strategy=${strategy}`, { method: 'DELETE' });
|
|
if (!res.ok) throw new Error('Failed to delete job');
|
|
return res.json();
|
|
},
|
|
};
|
|
|
|
export const api = {
|
|
// Settings
|
|
getSettings: async (): Promise<AppSettings> => {
|
|
const res = await fetch(`${BASE_URL}/settings`);
|
|
return res.json();
|
|
},
|
|
updateSettings: async (settings: Partial<AppSettings>): Promise<AppSettings> => {
|
|
const res = await fetch(`${BASE_URL}/settings`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings),
|
|
});
|
|
return res.json();
|
|
},
|
|
// kept as an alias since some pages call it by this name
|
|
saveSettings: async (settings: Partial<AppSettings>): Promise<AppSettings> => {
|
|
return api.updateSettings(settings);
|
|
},
|
|
|
|
// Sessions
|
|
listSessions: async (): Promise<any[]> => {
|
|
const res = await fetch(`${BASE_URL}/sessions`);
|
|
return res.json();
|
|
},
|
|
createSession: async (name: string, file: File, onProgress?: (pct: number) => void): Promise<{ session_id: string; job_id: string }> => {
|
|
const CHUNK_THRESHOLD = 90 * 1024 * 1024;
|
|
if (file.size > CHUNK_THRESHOLD) {
|
|
const result = await api.uploadFileInChunks(file, onProgress || (() => {}));
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
formData.append('upload_path', result.filepath);
|
|
const res = await fetch(`${BASE_URL}/sessions`, { method: 'POST', body: formData });
|
|
if (!res.ok) throw new Error('Failed to create session');
|
|
return res.json();
|
|
}
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
formData.append('file', file);
|
|
const res = await fetch(`${BASE_URL}/sessions`, { method: 'POST', body: formData });
|
|
if (!res.ok) throw new Error('Failed to create session');
|
|
return res.json();
|
|
},
|
|
getSession: async (sessionId: string): Promise<any> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}`);
|
|
if (!res.ok) throw new Error('Failed to fetch session');
|
|
return res.json();
|
|
},
|
|
retryTranscription: async (sessionId: string): Promise<{ job_id: string }> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/transcribe`, { method: 'POST' });
|
|
if (!res.ok) throw new Error('Failed to start transcription');
|
|
return res.json();
|
|
},
|
|
audioUrl: (sessionId: string): string => `${BASE_URL}/sessions/${sessionId}/audio`,
|
|
|
|
// Speakers
|
|
getSpeakers: async (sessionId: string): Promise<any[]> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers`);
|
|
return res.json();
|
|
},
|
|
getTurns: async (sessionId: string): Promise<any[]> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers/turns`);
|
|
return res.json();
|
|
},
|
|
setSpeakerName: async (sessionId: string, raw_label: string, display_name: string): Promise<void> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/speakers`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ raw_label, display_name }),
|
|
});
|
|
if (!res.ok) throw new Error('Failed to save speaker name');
|
|
},
|
|
|
|
// Notes
|
|
getNotes: async (sessionId: string): Promise<any> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/notes`);
|
|
if (!res.ok) throw new Error('Notes not generated yet');
|
|
return res.json();
|
|
},
|
|
generateNotes: async (sessionId: string): Promise<{ job_id: string }> => {
|
|
const res = await fetch(`${BASE_URL}/sessions/${sessionId}/notes/generate`, { method: 'POST' });
|
|
if (!res.ok) throw new Error('Failed to start note generation');
|
|
return res.json();
|
|
},
|
|
|
|
// Diagnostics
|
|
getDiagnostics: async (): Promise<{ ok: boolean; checks: any[] }> => {
|
|
const res = await fetch(`${BASE_URL}/diagnostics`);
|
|
if (!res.ok) throw new Error('Failed to reach backend');
|
|
return res.json();
|
|
},
|
|
|
|
// Jobs (re-exported here so pages that only import `api` still work)
|
|
deleteJob: jobApi.deleteJob,
|
|
cancelJob: jobApi.cancelJob,
|
|
getJob: jobApi.getJob,
|
|
|
|
// Large-file (chunked) upload, used automatically by createSession()
|
|
// when the file exceeds 90MB. Sends up to 3 chunks concurrently.
|
|
uploadFileInChunks: async (
|
|
file: File,
|
|
onProgress: (percent: number) => void
|
|
): Promise<any> => {
|
|
const CHUNK_SIZE = 1024 * 1024 * 15;
|
|
const CONCURRENCY = 3;
|
|
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
const uploadId = crypto.randomUUID();
|
|
|
|
const uploadOne = async (chunkIndex: number): Promise<any> => {
|
|
const start = chunkIndex * CHUNK_SIZE;
|
|
const end = Math.min(start + CHUNK_SIZE, file.size);
|
|
const chunk = file.slice(start, end);
|
|
const formData = new FormData();
|
|
formData.append('file', chunk, file.name);
|
|
formData.append('chunk_index', chunkIndex.toString());
|
|
formData.append('total_chunks', totalChunks.toString());
|
|
formData.append('filename', file.name);
|
|
formData.append('upload_id', uploadId);
|
|
const res = await fetch(`${BASE_URL}/sessions/upload-chunk`, { method: 'POST', body: formData });
|
|
if (!res.ok) throw new Error(`Failed to upload chunk ${chunkIndex}`);
|
|
return res.json();
|
|
};
|
|
|
|
let lastResult: any = null;
|
|
for (let i = 0; i < totalChunks; i += CONCURRENCY) {
|
|
const batch = [];
|
|
for (let j = i; j < Math.min(i + CONCURRENCY, totalChunks); j++) {
|
|
batch.push(uploadOne(j));
|
|
}
|
|
const results = await Promise.all(batch);
|
|
lastResult = results[results.length - 1];
|
|
onProgress(Math.round((Math.min(i + CONCURRENCY, totalChunks) / totalChunks) * 100));
|
|
}
|
|
return lastResult;
|
|
},
|
|
};
|