mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
comments + codebase index
This commit is contained in:
+113
-104
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user