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

View File

@ -16,16 +16,6 @@ When implementing new features:
3. Understand the API surface of your codebase
```typescript
## apps/web/src/components/editor/timeline
audio-waveform.tsx
export function AudioWaveform({
audioUrl,
audioBuffer,
height = 32,
className = "",
}: AudioWaveformProps)
## apps/web/src/constants
editor-constants.ts
@ -464,46 +454,46 @@ rate-limit.ts
scenes.ts
export function getMainScene({ scenes }: { scenes: TScene[] }): TScene | null
export function ensureMainScene({ scenes }: { scenes: TScene[] }): TScene[]
export function buildDefaultScene({
name,
isMain,
}: {
name: string;
isMain: boolean;
export function buildDefaultScene({
name,
isMain,
}: {
name: string;
isMain: boolean;
}): TScene
export function canDeleteScene({ scene }: { scene: TScene }): {
canDelete: boolean;
reason?: string;
export function canDeleteScene({ scene }: { scene: TScene }): {
canDelete: boolean;
reason?: string;
}
export function getFallbackSceneAfterDelete({
scenes,
deletedSceneId,
currentSceneId,
}: {
scenes: TScene[];
deletedSceneId: string;
currentSceneId: string | null;
export function getFallbackSceneAfterDelete({
scenes,
deletedSceneId,
currentSceneId,
}: {
scenes: TScene[];
deletedSceneId: string;
currentSceneId: string | null;
}): TScene | null
export function findCurrentScene({
scenes,
currentSceneId,
}: {
scenes: TScene[];
currentSceneId: string;
export function findCurrentScene({
scenes,
currentSceneId,
}: {
scenes: TScene[];
currentSceneId: string;
}): TScene | null
export function getProjectDurationFromScenes({
scenes,
}: {
scenes: TScene[];
export function getProjectDurationFromScenes({
scenes,
}: {
scenes: TScene[];
}): number
export function updateSceneInArray({
scenes,
sceneId,
updates,
}: {
scenes: TScene[];
sceneId: string;
updates: Partial<TScene>;
export function updateSceneInArray({
scenes,
sceneId,
updates,
}: {
scenes: TScene[];
sceneId: string;
updates: Partial<TScene>;
}): TScene[]
time.ts
@ -699,27 +689,28 @@ parser.ts
## apps/web/src/lib/media
audio.ts
export type CollectedAudioElement = Omit<
AudioElement,
"type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl...
export type CollectedAudioElement = Omit<
AudioElement,
"type" | "mediaId" | "volume" | "id" | "name" | "sourceType" | "sourceUrl"
...
export function createAudioContext(): AudioContext
export interface DecodedAudio {
samples: Float32Array
sampleRate: number
}
export function decodeAudioToFloat32({
audioBlob,
}: {
audioBlob: Blob;
export function decodeAudioToFloat32({
audioBlob,
}: {
audioBlob: Blob;
}): Promise<DecodedAudio>
export function collectAudioElements({
tracks,
mediaAssets,
audioContext,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
audioContext: AudioContext;
export function collectAudioElements({
tracks,
mediaAssets,
audioContext,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
audioContext: AudioContext;
}): Promise<CollectedAudioElement[]>
export interface AudioClipSource {
id: string
@ -731,32 +722,32 @@ audio.ts
trimEnd: number
muted: boolean
}
export function collectAudioMixSources({
tracks,
mediaAssets,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
export function collectAudioMixSources({
tracks,
mediaAssets,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
}): Promise<AudioMixSource[]>
export function collectAudioClips({
tracks,
mediaAssets,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
export function collectAudioClips({
tracks,
mediaAssets,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
}): Promise<AudioClipSource[]>
export function createTimelineAudioBuffer({
tracks,
mediaAssets,
duration,
sampleRate = 44100,
audioContext,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
duration: number;
sampleRate?: number;
audioContext?: AudioContext;
export function createTimelineAudioBuffer({
tracks,
mediaAssets,
duration,
sampleRate = 44100,
audioContext,
}: {
tracks: TimelineTrack[];
mediaAssets: MediaAsset[];
duration: number;
sampleRate?: number;
audioContext?: AudioContext;
}): Promise<AudioBuffer | null>
media-utils.ts
@ -1091,28 +1082,46 @@ track-utils.ts
}): { isValid: boolean; errorMessage?: string }
zoom-utils.ts
export function getTimelineZoomMin({
duration,
containerWidth,
}: {
duration: number;
containerWidth: number | null | undefined;
export function getTimelineZoomMin({
duration,
containerWidth,
}: {
duration: number;
containerWidth: number | null | undefined;
}): number
export function getTimelinePaddingPx({
containerWidth,
zoomLevel,
minZoom,
}: {
containerWidth: number;
zoomLevel: number;
minZoom: number;
export function getTimelinePaddingPx({
containerWidth,
zoomLevel,
minZoom,
}: {
containerWidth: number;
zoomLevel: number;
minZoom: number;
}): number
export function getZoomPercent({
zoomLevel,
minZoom,
}: {
zoomLevel: number;
minZoom: number;
export function getZoomPercent({
zoomLevel,
minZoom,
}: {
zoomLevel: 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
## apps/web/src/lib/transcription

View File

@ -1,51 +1,51 @@
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.
* 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;
/**
* 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;
/**
* 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;
/**
* Minimum pixel spacing between labels to keep them readable
* minimum pixel spacing between labels to keep them readable
*/
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;
export interface RulerConfig {
/** Time interval in seconds between each label */
/** time interval in seconds between each label */
labelIntervalSeconds: number;
/** Time interval in seconds between each tick */
/** time interval in seconds between each tick */
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 need wide spacing (~50px) to stay readable
* - Ticks can be denser (~8px) to show finer subdivisions
* labels and ticks scale independently:
* - labels need wide spacing (~50px) to stay readable
* - ticks can be denser (~8px) to show finer subdivisions
*
* Example at different zoom levels:
* - Very zoomed in: labels every 2f, ticks every 1f
* - Zoomed in: labels every 10f, ticks every 1f
* - Zoomed out: labels every 15f, ticks every 3f
* - Very zoomed out: labels every 1s, ticks every 5f
* example at different zoom levels:
* - very zoomed in: labels every 2f, ticks every 1f
* - zoomed in: labels every 10f, ticks every 1f
* - zoomed out: labels every 15f, ticks every 3f
* - very zoomed out: labels every 1s, ticks every 5f
*/
export function getRulerConfig({
zoomLevel,
@ -73,11 +73,12 @@ export function getRulerConfig({
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({
tickIntervalSeconds: rawTickIntervalSeconds,
labelIntervalSeconds,
pixelsPerFrame,
pixelsPerSecond,
fps,
});
@ -85,40 +86,54 @@ export function getRulerConfig({
}
/**
* Adjusts tick interval to ensure it divides evenly into the label interval.
* This guarantees labels always land on tick positions.
* adjusts tick interval to ensure it divides evenly into the label interval.
* this guarantees labels always land on tick positions.
*/
function ensureTickDividesLabel({
tickIntervalSeconds,
labelIntervalSeconds,
pixelsPerFrame,
pixelsPerSecond,
fps,
}: {
tickIntervalSeconds: number;
labelIntervalSeconds: number;
pixelsPerFrame: number;
pixelsPerSecond: number;
fps: number;
}): number {
const labelFrames = Math.round(labelIntervalSeconds * 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) {
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) {
if (labelFrames % candidateFrames === 0) {
const candidateSpacing = pixelsPerFrame * candidateFrames;
// Accept if spacing meets minimum threshold
// accept if spacing meets minimum threshold
if (candidateSpacing >= MIN_TICK_SPACING_PX) {
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;
}
@ -135,7 +150,7 @@ function findOptimalInterval({
minSpacingPx: number;
frameIntervals: readonly number[];
}): number {
// Try frame-level intervals first
// try frame-level intervals first
for (const frameInterval of frameIntervals) {
const pixelSpacing = pixelsPerFrame * frameInterval;
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) {
const pixelSpacing = pixelsPerSecond * secondMultiplier;
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({
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")
* - Between seconds: "Xf" (e.g., "5f", "15f")
* - on second boundaries: "MM:SS" (e.g., "00:00", "01:30")
* - between seconds: "Xf" (e.g., "5f", "15f")
*/
export function formatRulerLabel({
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({
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({
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 {
const totalSeconds = Math.round(timeInSeconds);