Initial commit: Nat20 Notes — TTRPG session transcription & summarization
This commit is contained in:
190
frontend/src/api.ts
Normal file
190
frontend/src/api.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user