comments + codebase index

This commit is contained in:
Maze Winther
2026-02-02 23:17:30 +01:00
parent 1dbc22b2f4
commit e84c0cfda6
2 changed files with 161 additions and 137 deletions
+113 -104
View File
@@ -16,16 +16,6 @@ When implementing new features:
3. Understand the API surface of your codebase 3. Understand the API surface of your codebase
```typescript ```typescript
## apps/web/src/components/editor/timeline
audio-waveform.tsx
export function AudioWaveform({␍
audioUrl,␍
audioBuffer,␍
height = 32,␍
className = "",␍
}: AudioWaveformProps)
## apps/web/src/constants ## apps/web/src/constants
editor-constants.ts editor-constants.ts
@@ -464,46 +454,46 @@ rate-limit.ts
scenes.ts scenes.ts
export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null
export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[] export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[]
export function buildDefaultScene({ export function buildDefaultScene({
name, name,
isMain, isMain,
}: { }: {
name: string; name: string;
isMain: boolean; isMain: boolean;
}): TScene }): TScene
export function canDeleteScene({ scene }: { scene: TScene }): { export function canDeleteScene({ scene }: { scene: TScene }): {
canDelete: boolean; canDelete: boolean;
reason?: string; reason?: string;
} }
export function getFallbackSceneAfterDelete({ export function getFallbackSceneAfterDelete({
scenes, scenes,
deletedSceneId, deletedSceneId,
currentSceneId, currentSceneId,
}: { }: {
scenes: TScene[]; scenes: TScene[];
deletedSceneId: string; deletedSceneId: string;
currentSceneId: string | null; currentSceneId: string | null;
}): TScene | null }): TScene | null
export function findCurrentScene({ export function findCurrentScene({
scenes, scenes,
currentSceneId, currentSceneId,
}: { }: {
scenes: TScene[]; scenes: TScene[];
currentSceneId: string; currentSceneId: string;
}): TScene | null }): TScene | null
export function getProjectDurationFromScenes({ export function getProjectDurationFromScenes({
scenes, scenes,
}: { }: {
scenes: TScene[]; scenes: TScene[];
}): number }): number
export function updateSceneInArray({ export function updateSceneInArray({
scenes, scenes,
sceneId, sceneId,
updates, updates,
}: { }: {
scenes: TScene[]; scenes: TScene[];
sceneId: string; sceneId: string;
updates: Partial<TScene>; updates: Partial<TScene>;
}): TScene[] }): TScene[]
time.ts time.ts
@@ -699,27 +689,28 @@ parser.ts
## apps/web/src/lib/media ## apps/web/src/lib/media
audio.ts audio.ts
export type CollectedAudioElement = Omit< export type CollectedAudioElement = Omit<
AudioElement, AudioElement,
"type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl... "type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl"
...
export function createAudioContext(): AudioContext export function createAudioContext(): AudioContext
export interface DecodedAudio { export interface DecodedAudio {
samples: Float32Array samples: Float32Array
sampleRate: number sampleRate: number
} }
export function decodeAudioToFloat32({ export function decodeAudioToFloat32({
audioBlob, audioBlob,
}: { }: {
audioBlob: Blob; audioBlob: Blob;
}): Promise<DecodedAudio> }): Promise<DecodedAudio>
export function collectAudioElements({ export function collectAudioElements({
tracks, tracks,
mediaAssets, mediaAssets,
audioContext, audioContext,
}: { }: {
tracks: TimelineTrack[]; tracks: TimelineTrack[];
mediaAssets: MediaAsset[]; mediaAssets: MediaAsset[];
audioContext: AudioContext; audioContext: AudioContext;
}): Promise<CollectedAudioElement[]> }): Promise<CollectedAudioElement[]>
export interface AudioClipSource { export interface AudioClipSource {
id: string id: string
@@ -731,32 +722,32 @@ audio.ts
trimEnd: number trimEnd: number
muted: boolean muted: boolean
} }
export function collectAudioMixSources({ export function collectAudioMixSources({
tracks, tracks,
mediaAssets, mediaAssets,
}: { }: {
tracks: TimelineTrack[]; tracks: TimelineTrack[];
mediaAssets: MediaAsset[]; mediaAssets: MediaAsset[];
}): Promise<AudioMixSource[]> }): Promise<AudioMixSource[]>
export function collectAudioClips({ export function collectAudioClips({
tracks, tracks,
mediaAssets, mediaAssets,
}: { }: {
tracks: TimelineTrack[]; tracks: TimelineTrack[];
mediaAssets: MediaAsset[]; mediaAssets: MediaAsset[];
}): Promise<AudioClipSource[]> }): Promise<AudioClipSource[]>
export function createTimelineAudioBuffer({ export function createTimelineAudioBuffer({
tracks, tracks,
mediaAssets, mediaAssets,
duration, duration,
sampleRate = 44100, sampleRate = 44100,
audioContext, audioContext,
}: { }: {
tracks: TimelineTrack[]; tracks: TimelineTrack[];
mediaAssets: MediaAsset[]; mediaAssets: MediaAsset[];
duration: number; duration: number;
sampleRate?: number; sampleRate?: number;
audioContext?: AudioContext; audioContext?: AudioContext;
}): Promise<AudioBuffer | null> }): Promise<AudioBuffer | null>
media-utils.ts media-utils.ts
@@ -1091,28 +1082,46 @@ track-utils.ts
}): { isValid: boolean; errorMessage?: string } }): { isValid: boolean; errorMessage?: string }
zoom-utils.ts zoom-utils.ts
export function getTimelineZoomMin({ export function getTimelineZoomMin({
duration, duration,
containerWidth, containerWidth,
}: { }: {
duration: number; duration: number;
containerWidth: number | null | undefined; containerWidth: number | null | undefined;
}): number }): number
export function getTimelinePaddingPx({ export function getTimelinePaddingPx({
containerWidth, containerWidth,
zoomLevel, zoomLevel,
minZoom, minZoom,
}: { }: {
containerWidth: number; containerWidth: number;
zoomLevel: number; zoomLevel: number;
minZoom: number; minZoom: number;
}): number }): number
export function getZoomPercent({ export function getZoomPercent({
zoomLevel, zoomLevel,
minZoom, minZoom,
}: { }: {
zoomLevel: number; zoomLevel: number;
minZoom: number; minZoom: number;
}): number
export function sliderToZoom({␍
sliderPosition,␍
minZoom,␍
maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX,␍
}: {␍
sliderPosition: number;␍
minZoom: number;␍
maxZoom?: number;␍
}): number
export function zoomToSlider({␍
zoomLevel,␍
minZoom,␍
maxZoom = TIMELINE_CONSTANTS.ZOOM_MAX,␍
}: {␍
zoomLevel: number;␍
minZoom: number;␍
maxZoom?: number;␍
}): number }): number
## apps/web/src/lib/transcription ## apps/web/src/lib/transcription
+48 -33
View File
@@ -1,51 +1,51 @@
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
/** /**
* Frame intervals for labels - starts at 2 so there's always at least * frame intervals for labels - starts at 2 so there's always at least
* one tick between labels even at max zoom. * one tick between labels even at max zoom.
* Pattern: 2, 3, 5, 10, 15 (matches CapCut) * pattern: 2, 3, 5, 10, 15 (matches CapCut)
*/ */
const LABEL_FRAME_INTERVALS = [2, 3, 5, 10, 15] as const; const LABEL_FRAME_INTERVALS = [2, 3, 5, 10, 15] as const;
/** /**
* Frame intervals for ticks - can go down to 1 for max granularity. * frame intervals for ticks - can go down to 1 for max granularity.
*/ */
const TICK_FRAME_INTERVALS = [1, 2, 3, 5, 10, 15] as const; const TICK_FRAME_INTERVALS = [1, 2, 3, 5, 10, 15] as const;
/** /**
* Second intervals for when we're zoomed out past frame-level detail. * second intervals for when we're zoomed out past frame-level detail.
*/ */
const SECOND_MULTIPLIERS = [1, 2, 3, 5, 10, 15, 30, 60] as const; const SECOND_MULTIPLIERS = [1, 2, 3, 5, 10, 15, 30, 60] as const;
/** /**
* Minimum pixel spacing between labels to keep them readable * minimum pixel spacing between labels to keep them readable
*/ */
const MIN_LABEL_SPACING_PX = 120; const MIN_LABEL_SPACING_PX = 120;
/** /**
* Minimum pixel spacing between ticks. Much denser than labels. * minimum pixel spacing between ticks. much denser than labels.
*/ */
const MIN_TICK_SPACING_PX = 18; const MIN_TICK_SPACING_PX = 18;
export interface RulerConfig { export interface RulerConfig {
/** Time interval in seconds between each label */ /** time interval in seconds between each label */
labelIntervalSeconds: number; labelIntervalSeconds: number;
/** Time interval in seconds between each tick */ /** time interval in seconds between each tick */
tickIntervalSeconds: number; tickIntervalSeconds: number;
} }
/** /**
* Determines the optimal label and tick intervals based on zoom level and FPS. * determines the optimal label and tick intervals based on zoom level and FPS.
* *
* Labels and ticks scale independently: * labels and ticks scale independently:
* - Labels need wide spacing (~50px) to stay readable * - labels need wide spacing (~50px) to stay readable
* - Ticks can be denser (~8px) to show finer subdivisions * - ticks can be denser (~8px) to show finer subdivisions
* *
* Example at different zoom levels: * example at different zoom levels:
* - Very zoomed in: labels every 2f, ticks every 1f * - very zoomed in: labels every 2f, ticks every 1f
* - Zoomed in: labels every 10f, ticks every 1f * - zoomed in: labels every 10f, ticks every 1f
* - Zoomed out: labels every 15f, ticks every 3f * - zoomed out: labels every 15f, ticks every 3f
* - Very zoomed out: labels every 1s, ticks every 5f * - very zoomed out: labels every 1s, ticks every 5f
*/ */
export function getRulerConfig({ export function getRulerConfig({
zoomLevel, zoomLevel,
@@ -73,11 +73,12 @@ export function getRulerConfig({
frameIntervals: TICK_FRAME_INTERVALS, frameIntervals: TICK_FRAME_INTERVALS,
}); });
// Ensure tick interval divides evenly into label interval so labels always land on ticks // ensure tick interval divides evenly into label interval so labels always land on ticks
const tickIntervalSeconds = ensureTickDividesLabel({ const tickIntervalSeconds = ensureTickDividesLabel({
tickIntervalSeconds: rawTickIntervalSeconds, tickIntervalSeconds: rawTickIntervalSeconds,
labelIntervalSeconds, labelIntervalSeconds,
pixelsPerFrame, pixelsPerFrame,
pixelsPerSecond,
fps, fps,
}); });
@@ -85,40 +86,54 @@ export function getRulerConfig({
} }
/** /**
* Adjusts tick interval to ensure it divides evenly into the label interval. * adjusts tick interval to ensure it divides evenly into the label interval.
* This guarantees labels always land on tick positions. * this guarantees labels always land on tick positions.
*/ */
function ensureTickDividesLabel({ function ensureTickDividesLabel({
tickIntervalSeconds, tickIntervalSeconds,
labelIntervalSeconds, labelIntervalSeconds,
pixelsPerFrame, pixelsPerFrame,
pixelsPerSecond,
fps, fps,
}: { }: {
tickIntervalSeconds: number; tickIntervalSeconds: number;
labelIntervalSeconds: number; labelIntervalSeconds: number;
pixelsPerFrame: number; pixelsPerFrame: number;
pixelsPerSecond: number;
fps: number; fps: number;
}): number { }): number {
const labelFrames = Math.round(labelIntervalSeconds * fps); const labelFrames = Math.round(labelIntervalSeconds * fps);
const tickFrames = Math.round(tickIntervalSeconds * fps); const tickFrames = Math.round(tickIntervalSeconds * fps);
// If tick already divides label evenly, we're good // if tick already divides label evenly, we're good
if (labelFrames % tickFrames === 0) { if (labelFrames % tickFrames === 0) {
return tickIntervalSeconds; return tickIntervalSeconds;
} }
// Find the smallest tick interval that divides the label interval and has adequate spacing // find the smallest tick interval that divides the label interval and has adequate spacing
for (const candidateFrames of TICK_FRAME_INTERVALS) { for (const candidateFrames of TICK_FRAME_INTERVALS) {
if (labelFrames % candidateFrames === 0) { if (labelFrames % candidateFrames === 0) {
const candidateSpacing = pixelsPerFrame * candidateFrames; const candidateSpacing = pixelsPerFrame * candidateFrames;
// Accept if spacing meets minimum threshold // accept if spacing meets minimum threshold
if (candidateSpacing >= MIN_TICK_SPACING_PX) { if (candidateSpacing >= MIN_TICK_SPACING_PX) {
return candidateFrames / fps; return candidateFrames / fps;
} }
} }
} }
// Fallback: use the label interval itself (no intermediate ticks) // try second-level tick intervals that divide the label interval cleanly
for (const candidateSeconds of SECOND_MULTIPLIERS) {
const ratio = labelIntervalSeconds / candidateSeconds;
const isDivisor = Math.abs(ratio - Math.round(ratio)) < 0.0001;
if (isDivisor) {
const candidateSpacing = pixelsPerSecond * candidateSeconds;
if (candidateSpacing >= MIN_TICK_SPACING_PX) {
return candidateSeconds;
}
}
}
// fallback: use the label interval itself (no intermediate ticks)
return labelIntervalSeconds; return labelIntervalSeconds;
} }
@@ -135,7 +150,7 @@ function findOptimalInterval({
minSpacingPx: number; minSpacingPx: number;
frameIntervals: readonly number[]; frameIntervals: readonly number[];
}): number { }): number {
// Try frame-level intervals first // try frame-level intervals first
for (const frameInterval of frameIntervals) { for (const frameInterval of frameIntervals) {
const pixelSpacing = pixelsPerFrame * frameInterval; const pixelSpacing = pixelsPerFrame * frameInterval;
if (pixelSpacing >= minSpacingPx) { if (pixelSpacing >= minSpacingPx) {
@@ -143,7 +158,7 @@ function findOptimalInterval({
} }
} }
// Then try second-level intervals // then try second-level intervals
for (const secondMultiplier of SECOND_MULTIPLIERS) { for (const secondMultiplier of SECOND_MULTIPLIERS) {
const pixelSpacing = pixelsPerSecond * secondMultiplier; const pixelSpacing = pixelsPerSecond * secondMultiplier;
if (pixelSpacing >= minSpacingPx) { if (pixelSpacing >= minSpacingPx) {
@@ -155,7 +170,7 @@ function findOptimalInterval({
} }
/** /**
* Checks if a time should have a label based on the label interval. * checks if a time should have a label based on the label interval.
*/ */
export function shouldShowLabel({ export function shouldShowLabel({
time, time,
@@ -170,10 +185,10 @@ export function shouldShowLabel({
} }
/** /**
* Formats a ruler tick label. * formats a ruler tick label.
* *
* - On second boundaries: "MM:SS" (e.g., "00:00", "01:30") * - on second boundaries: "MM:SS" (e.g., "00:00", "01:30")
* - Between seconds: "Xf" (e.g., "5f", "15f") * - between seconds: "Xf" (e.g., "5f", "15f")
*/ */
export function formatRulerLabel({ export function formatRulerLabel({
timeInSeconds, timeInSeconds,
@@ -191,7 +206,7 @@ export function formatRulerLabel({
} }
/** /**
* Checks if a time falls exactly on a second boundary. * checks if a time falls exactly on a second boundary.
*/ */
function isSecondBoundary({ function isSecondBoundary({
timeInSeconds, timeInSeconds,
@@ -204,7 +219,7 @@ function isSecondBoundary({
} }
/** /**
* Gets the frame number within the current second. * gets the frame number within the current second.
*/ */
function getFrameWithinSecond({ function getFrameWithinSecond({
timeInSeconds, timeInSeconds,
@@ -218,7 +233,7 @@ function getFrameWithinSecond({
} }
/** /**
* Formats a timestamp as MM:SS. * formats a timestamp as MM:SS.
*/ */
function formatTimestamp({ timeInSeconds }: { timeInSeconds: number }): string { function formatTimestamp({ timeInSeconds }: { timeInSeconds: number }): string {
const totalSeconds = Math.round(timeInSeconds); const totalSeconds = Math.round(timeInSeconds);