Spaces:
Paused
Paused
Download apps/web/src/lib/session.ts from nathycodes/presentercoach: direct link, hf CLI and curl.
- Browser
- Download file 6.97 kB
-
https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/web/src/lib/session.ts
- Command line
-
hf download hf://spaces/nathycodes/presentercoach/apps/web/src/lib/session.ts
-
curl -L -o session.ts https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/web/src/lib/session.ts
6.97 kB
| import type { MetricsPoint, TranscriptSegment } from "@video/shared"; | |
| import { apiFetch } from "@/lib/api"; | |
| import type { DocumentBlock } from "@/lib/documentReader"; | |
| export type SessionPayload = { | |
| title?: string; | |
| mode: string; | |
| prompt: string; | |
| goal: string; | |
| curriculumDay?: number; | |
| exerciseId?: string; | |
| storyFramework?: string; | |
| targetKeywords?: string[]; | |
| teleprompterScript?: string; | |
| durationMs: number; | |
| wpmAvg: number; | |
| fillerCount: number; | |
| eyeContactPct: number; | |
| transcript: TranscriptSegment[]; | |
| metrics: MetricsPoint[]; | |
| tags?: string[]; | |
| score?: Record<string, unknown>; | |
| }; | |
| export type TranscriptionSelection = { | |
| backend?: string; | |
| model?: string; | |
| }; | |
| export type TranscriptionJobResponse = { | |
| status: string; | |
| job_id?: string; | |
| }; | |
| export type SessionDocumentPayload = { | |
| file: Blob; | |
| name: string; | |
| mimeType: string; | |
| blocks: DocumentBlock[]; | |
| }; | |
| export const mergeChunks = (chunks: Blob[], mimeType?: string) => | |
| new Blob(chunks, { type: mimeType || chunks[0]?.type || "video/webm" }); | |
| export const getVideoDurationMs = (file: Blob) => | |
| new Promise<number>((resolve) => { | |
| if (typeof document === "undefined") { | |
| resolve(0); | |
| return; | |
| } | |
| const media = document.createElement(file.type.startsWith("audio/") ? "audio" : "video"); | |
| const url = URL.createObjectURL(file); | |
| media.preload = "metadata"; | |
| media.onloadedmetadata = () => { | |
| const duration = Number.isFinite(media.duration) ? Math.round(media.duration * 1000) : 0; | |
| URL.revokeObjectURL(url); | |
| resolve(duration); | |
| }; | |
| media.onerror = () => { | |
| URL.revokeObjectURL(url); | |
| resolve(0); | |
| }; | |
| media.src = url; | |
| }); | |
| export const createSession = (payload: SessionPayload) => | |
| apiFetch<{ id: number }>("/sessions", { | |
| method: "POST", | |
| body: JSON.stringify({ | |
| title: payload.title, | |
| mode: payload.mode, | |
| prompt: payload.prompt, | |
| goal: payload.goal, | |
| curriculum_day: payload.curriculumDay, | |
| exercise_id: payload.exerciseId, | |
| story_framework: payload.storyFramework, | |
| target_keywords: payload.targetKeywords || [], | |
| teleprompter_script: payload.teleprompterScript, | |
| duration_ms: payload.durationMs, | |
| wpm_avg: payload.wpmAvg, | |
| filler_count: payload.fillerCount, | |
| eye_contact_pct: payload.eyeContactPct, | |
| transcript_segments: payload.transcript.map((seg) => ({ | |
| start_ms: seg.startMs, | |
| end_ms: seg.endMs, | |
| text: seg.text, | |
| })), | |
| metrics: payload.metrics.map((point) => ({ | |
| t: point.t, | |
| wpm: point.wpm, | |
| rms: point.rms, | |
| eye_contact: point.eyeContact, | |
| smile: point.smile, | |
| yaw: point.yaw, | |
| pitch: point.pitch, | |
| roll: point.roll, | |
| filler_count: point.fillerCount, | |
| pause_ms: point.pauseMs, | |
| })), | |
| tags: payload.tags || [], | |
| score: payload.score, | |
| }), | |
| }); | |
| export const updateSessionTranscript = (sessionId: number, transcript: TranscriptSegment[]) => | |
| apiFetch(`/sessions/${sessionId}/transcript`, { | |
| method: "PATCH", | |
| body: JSON.stringify({ | |
| transcript_segments: transcript.map((seg) => ({ | |
| start_ms: seg.startMs, | |
| end_ms: seg.endMs, | |
| text: seg.text, | |
| })), | |
| }), | |
| }); | |
| const resolveRecordingFormat = (mimeType?: string) => { | |
| const normalized = (mimeType || "").split(";")[0].trim().toLowerCase(); | |
| if (normalized === "audio/mp4" || normalized === "audio/x-m4a") { | |
| return { extension: "m4a", mimeType: "audio/mp4" }; | |
| } | |
| if (normalized === "audio/wav" || normalized === "audio/x-wav") { | |
| return { extension: "wav", mimeType: "audio/wav" }; | |
| } | |
| if (normalized === "audio/webm") { | |
| return { extension: "webm", mimeType: "audio/webm" }; | |
| } | |
| if (normalized.includes("mp4")) { | |
| return { extension: "mp4", mimeType: "video/mp4" }; | |
| } | |
| if (normalized.includes("quicktime") || normalized.endsWith(".mov")) { | |
| return { extension: "mov", mimeType: "video/quicktime" }; | |
| } | |
| if (normalized.includes("webm")) { | |
| return { extension: "webm", mimeType: "video/webm" }; | |
| } | |
| return { extension: "webm", mimeType: normalized || "video/webm" }; | |
| }; | |
| const buildUploadQuery = (params: Record<string, string | number>) => | |
| new URLSearchParams( | |
| Object.entries(params).reduce<Record<string, string>>((acc, [key, value]) => { | |
| acc[key] = String(value); | |
| return acc; | |
| }, {}) | |
| ).toString(); | |
| export const uploadChunks = async (sessionId: number, chunks: Blob[], mimeType?: string) => { | |
| const format = resolveRecordingFormat(mimeType || chunks[0]?.type); | |
| const uploadId = `${sessionId}-${Date.now()}`; | |
| for (let i = 0; i < chunks.length; i += 1) { | |
| const formData = new FormData(); | |
| formData.append("file", chunks[i], `chunk-${i}.${format.extension}`); | |
| await apiFetch( | |
| `/sessions/${sessionId}/upload?${buildUploadQuery({ | |
| chunk_index: i, | |
| total_chunks: chunks.length, | |
| upload_id: uploadId, | |
| mime_type: format.mimeType, | |
| })}`, | |
| { | |
| method: "POST", | |
| body: formData, | |
| } | |
| ); | |
| } | |
| }; | |
| export const uploadVideoFile = async (sessionId: number, file: File, chunkSize = 2 * 1024 * 1024) => { | |
| const format = resolveRecordingFormat(file.type || file.name); | |
| if (file.size <= chunkSize) { | |
| const formData = new FormData(); | |
| formData.append("file", file, file.name || `upload.${format.extension}`); | |
| await apiFetch(`/sessions/${sessionId}/upload`, { | |
| method: "POST", | |
| body: formData, | |
| }); | |
| return; | |
| } | |
| const uploadId = `${sessionId}-${Date.now()}`; | |
| const totalChunks = Math.ceil(file.size / chunkSize); | |
| for (let index = 0; index < totalChunks; index += 1) { | |
| const formData = new FormData(); | |
| const start = index * chunkSize; | |
| const end = Math.min(file.size, start + chunkSize); | |
| formData.append("file", file.slice(start, end, file.type), `chunk-${index}.${format.extension}`); | |
| await apiFetch( | |
| `/sessions/${sessionId}/upload?${buildUploadQuery({ | |
| chunk_index: index, | |
| total_chunks: totalChunks, | |
| upload_id: uploadId, | |
| mime_type: format.mimeType, | |
| })}`, | |
| { | |
| method: "POST", | |
| body: formData, | |
| } | |
| ); | |
| } | |
| }; | |
| export const triggerTranscription = (sessionId: number, selection?: TranscriptionSelection) => | |
| apiFetch<TranscriptionJobResponse>(`/sessions/${sessionId}/transcribe`, { | |
| method: "POST", | |
| body: selection ? JSON.stringify(selection) : undefined, | |
| }); | |
| export const uploadSessionDocument = async (sessionId: number, document: SessionDocumentPayload) => | |
| { | |
| const formData = new FormData(); | |
| formData.append("file", document.file, document.name); | |
| formData.append("blocks_json", JSON.stringify(document.blocks)); | |
| return apiFetch(`/sessions/${sessionId}/document`, { | |
| method: "POST", | |
| body: formData, | |
| }); | |
| }; | |
| export const deleteSession = (sessionId: number) => | |
| apiFetch(`/sessions/${sessionId}`, { | |
| method: "DELETE", | |
| }); | |