1// Data model shared by the server and the browser UI.
3export type TrackKind = 'screen' | 'camera';
5export interface Take {
6 id: string;
7 createdAt: string;
8 status: 'recording' | 'processing' | 'ready' | 'failed';
9 tracks: { screen: boolean; camera: boolean };
10 // Which track file carries the microphone audio (null if no mic was recorded).
11 audio: TrackKind | null;
12 // Seconds; 0 until the take is finalized.
13 duration: number;
14 transcript?: { status: 'pending' | 'done' | 'failed'; error?: string; updatedAt: string };
15 // Background removal of the camera track (cutout.webm, VP9 with alpha). queued/running are live
16 // states reported by the server; only done/failed are persisted.
17 matte?: { status: 'queued' | 'running' | 'done' | 'failed'; progress?: number; error?: string; updatedAt: string };
18 error?: string;
19}
21// A piece of a take, [start, end) in take time (seconds). The timeline is an ordered list of these.
22export interface Segment {
23 id: string;
24 takeId: string;
25 start: number;
26 end: number;
27}
29export type LayoutMode = 'pip' | 'side-by-side' | 'screen-only' | 'camera-only';
30export type Corner = 'tl' | 'tr' | 'bl' | 'br';
31export type CameraShape = 'rect' | 'rounded' | 'circle';
32export type BackgroundMode = 'none' | 'blur' | 'color' | 'cutout';
34// What replaces the room behind the speaker (requires background removal of the camera takes).
35export interface Background {
36 mode: BackgroundMode;
37 color: string; // #rrggbb, for 'color'
38 blur: number; // Gaussian sigma in pixels at 1080p output, for 'blur'
39}
41export const DEFAULT_BACKGROUND: Background = { mode: 'none', color: '#1e2530', blur: 18 };
43export const backgroundOf = (l: Layout): Background => ({ ...DEFAULT_BACKGROUND, ...l.background });
45// How screen and camera are combined in takes that have both.
46export interface Layout {
47 mode: LayoutMode;
48 corner: Corner; // PiP corner; for side-by-side, the left/right side of the camera column
49 size: number; // PiP width as a fraction of the output width
50 shape: CameraShape;
51 background?: Background; // absent in videos created before background removal existed
52}
54export interface OutputSettings {
55 height: 720 | 1080;
56 normalizeAudio: boolean;
57}
59export interface ExportInfo {
60 file: string;
61 exportedAt: string;
62 signature: string; // exportSignature() at the time of export, used to detect stale exports
63 duration: number;
64 bytes: number;
65}
67export interface Video {
68 id: string;
69 title: string;
70 description: string;
71 notes: string;
72 createdAt: string;
73 updatedAt: string;
74 takes: Take[];
75 timeline: Segment[];
76 layout: Layout;
77 output: OutputSettings;
78 thumbnail: { file: string; custom: boolean; updatedAt: string } | null;
79 export: ExportInfo | null;
80}
82export interface VideoSummary {
83 id: string;
84 title: string;
85 description: string;
86 createdAt: string;
87 updatedAt: string;
88 duration: number;
89 takeCount: number;
90 hasScreen: boolean;
91 hasCamera: boolean;
92 thumbnailUrl: string | null;
93 exportState: 'none' | 'current' | 'stale';
94}
96export interface ExportJob {
97 status: 'running' | 'done' | 'failed';
98 progress: number; // 0..1
99 startedAt: string;
100 error?: string;
101}
103export interface AppConfig {
104 deepgramModel: string;
105 autoTranscribe: boolean;
106 defaultCountdown: number;
107 defaultCameraHeight: 720 | 1080;
108 defaultOutputHeight: 720 | 1080;
109 matteHost: string; // ssh destination for background removal on a GPU; empty for this computer
110 matteSetup: string; // shell commands run on that host first, e.g. "module load python ffmpeg"
111}
113export interface PublicConfig extends AppConfig {
114 hasDeepgramKey: boolean;
115 libraryDir: string;
116}
118export interface Word {
119 word: string;
120 text: string; // punctuated form for display
121 start: number;
122 end: number;
123}
125export interface Transcript {
126 takeId: string;
127 model: string;
128 createdAt: string;
129 text: string;
130 words: Word[];
131}
133// Audio peaks for waveform display: `rate` values per second, each 0..255, base64-encoded bytes.
134export interface Peaks {
135 rate: number;
136 data: string;
137}
139export function exportSignature(v: Pick<Video, 'timeline' | 'layout' | 'output'>): string {
140 const seg = v.timeline.map((s) => [s.takeId, round3(s.start), round3(s.end)]);
141 const l = v.layout;
142 const o = v.output;
143 const b = backgroundOf(l);
144 // The background is omitted when off so that exports made before it existed stay current.
145 const bg = b.mode === 'color' ? ['color', b.color] : b.mode === 'blur' ? ['blur', b.blur] : b.mode;
146 return JSON.stringify({ seg, layout: [l.mode, l.corner, round3(l.size), l.shape], ...(b.mode === 'none' ? {} : { bg }), output: [o.height, o.normalizeAudio] });
147}
149function round3(x: number) {
150 return Math.round(x * 1000) / 1000;
151}