crashing from wav file size i think

This commit is contained in:
2026-04-08 00:48:05 -06:00
parent 56be227245
commit 38ca9cfbad
5 changed files with 303 additions and 120 deletions

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useEditorStore } from '../store/editorStore';
import { Loader2, Scissors } from 'lucide-react';
import { Loader2, Scissors, Trash2, Play, Pause } from 'lucide-react';
type SilenceRange = {
start: number;
@ -8,14 +8,31 @@ type SilenceRange = {
duration: number;
};
type TrimAction = 'cut' | 'mute';
export default function SilenceTrimmerPanel() {
const { videoPath, backendUrl, addCutRange, duration } = useEditorStore();
const {
videoPath,
backendUrl,
addCutRange,
addMuteRange,
removeCutRange,
removeMuteRange,
cutRanges,
muteRanges,
duration,
pauseUndo,
resumeUndo
} = useEditorStore();
const [minSilenceMs, setMinSilenceMs] = useState(500);
const [silenceDb, setSilenceDb] = useState(-35);
const [preBufferMs, setPreBufferMs] = useState(80);
const [postBufferMs, setPostBufferMs] = useState(120);
const [isDetecting, setIsDetecting] = useState(false);
const [ranges, setRanges] = useState<SilenceRange[]>([]);
const [trimAction, setTrimAction] = useState<TrimAction>('cut');
const [isActive, setIsActive] = useState(false);
const detectSilence = async () => {
if (!videoPath) return;
@ -58,6 +75,9 @@ export default function SilenceTrimmerPanel() {
};
const applyAsCuts = () => {
// Pause undo tracking to group all cuts into a single undo operation
pauseUndo();
const preBufferSeconds = preBufferMs / 1000;
const postBufferSeconds = postBufferMs / 1000;
const maxEnd = duration > 0 ? duration : Number.POSITIVE_INFINITY;
@ -67,11 +87,71 @@ export default function SilenceTrimmerPanel() {
const start = Math.max(0, r.start + preBufferSeconds);
const end = Math.min(maxEnd, r.end - postBufferSeconds);
if (end - start >= 0.01) {
addCutRange(start, end);
if (trimAction === 'cut') {
addCutRange(start, end);
} else {
addMuteRange(start, end);
}
}
}
// Resume undo tracking - this creates a single undo entry for the entire batch
resumeUndo();
};
const removeExistingTrims = () => {
pauseUndo();
try {
// Remove all cut ranges that match detected silence ranges
cutRanges.forEach(range => {
ranges.forEach(silenceRange => {
if (Math.abs(range.start - silenceRange.start) < 0.1 &&
Math.abs(range.end - silenceRange.end) < 0.1) {
removeCutRange(range.id);
}
});
});
// Remove all mute ranges that match detected silence ranges
muteRanges.forEach(range => {
ranges.forEach(silenceRange => {
if (Math.abs(range.start - silenceRange.start) < 0.1 &&
Math.abs(range.end - silenceRange.end) < 0.1) {
removeMuteRange(range.id);
}
});
});
} finally {
resumeUndo();
}
};
const toggleActive = () => {
setIsActive(!isActive);
if (!isActive) {
// When activating, detect silence and apply
detectSilence().then(() => {
if (ranges.length > 0) {
applyAsCuts();
}
});
} else {
// When deactivating, remove existing trims
removeExistingTrims();
}
};
// Auto-detect when video changes
useEffect(() => {
if (videoPath && isActive) {
detectSilence().then(() => {
if (ranges.length > 0) {
applyAsCuts();
}
});
}
}, [videoPath]);
return (
<div className="p-4 space-y-4">
<div className="space-y-1">
@ -142,43 +222,95 @@ export default function SilenceTrimmerPanel() {
</div>
</div>
<button
onClick={detectSilence}
disabled={isDetecting || !videoPath}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-editor-accent hover:bg-editor-accent-hover disabled:opacity-50 rounded-lg text-sm font-medium transition-colors"
>
{isDetecting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Detecting pauses...
</>
) : (
'Detect Pauses'
)}
</button>
</div>
{ranges.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
<button
onClick={applyAsCuts}
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-accent/20 text-editor-accent rounded hover:bg-editor-accent/30"
>
<Scissors className="w-3 h-3" />
Apply As Cuts
</button>
</div>
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
{ranges.slice(0, 50).map((r, i) => (
<div key={`${r.start}-${r.end}-${i}`} className="px-2 py-1.5 rounded bg-editor-surface border border-editor-border text-xs">
{r.start.toFixed(2)}s - {r.end.toFixed(2)}s ({r.duration.toFixed(2)}s)
</div>
))}
</div>
<div className="space-y-1.5">
<label className="text-[11px] text-editor-text-muted font-medium">
Trim Action
</label>
<select
value={trimAction}
onChange={(e) => setTrimAction(e.target.value as TrimAction)}
className="w-full px-2.5 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
>
<option value="cut">Cut (remove silence)</option>
<option value="mute">Mute (silence audio)</option>
</select>
</div>
)}
<div className="flex items-center justify-between">
<label className="text-[11px] text-editor-text-muted font-medium">
Active Mode
</label>
<button
onClick={toggleActive}
className={`flex items-center gap-2 px-3 py-1.5 text-xs rounded transition-colors ${
isActive
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-editor-surface border border-editor-border hover:bg-editor-surface-hover'
}`}
>
{isActive ? (
<>
<Pause className="w-3 h-3" />
Active
</>
) : (
<>
<Play className="w-3 h-3" />
Inactive
</>
)}
</button>
</div>
<div className="flex gap-2">
<button
onClick={detectSilence}
disabled={isDetecting || !videoPath}
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-editor-accent hover:bg-editor-accent-hover disabled:opacity-50 rounded text-xs font-medium transition-colors"
>
{isDetecting ? (
<>
<Loader2 className="w-3 h-3 animate-spin" />
Detecting...
</>
) : (
'Detect Pauses'
)}
</button>
{ranges.length > 0 && (
<button
onClick={removeExistingTrims}
className="flex items-center gap-1 px-3 py-2 text-xs bg-red-600 hover:bg-red-700 text-white rounded transition-colors"
>
<Trash2 className="w-3 h-3" />
Remove Trims
</button>
)}
</div>
{ranges.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
<button
onClick={applyAsCuts}
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-accent/20 text-editor-accent rounded hover:bg-editor-accent/30"
>
<Scissors className="w-3 h-3" />
Apply As {trimAction === 'cut' ? 'Cuts' : 'Mutes'}
</button>
</div>
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
{ranges.slice(0, 50).map((r, i) => (
<div key={`${r.start}-${r.end}-${i}`} className="px-2 py-1.5 rounded bg-editor-surface border border-editor-border text-xs">
{r.start.toFixed(2)}s - {r.end.toFixed(2)}s ({r.duration.toFixed(2)}s)
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -5,24 +5,31 @@ import { Play, Pause, SkipBack, SkipForward, Volume2 } from 'lucide-react';
export default function VideoPlayer() {
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
const videoUrl = useEditorStore((s) => s.videoUrl);
const isPlaying = useEditorStore((s) => s.isPlaying);
const duration = useEditorStore((s) => s.duration);
const { seekTo, togglePlay } = useVideoSync(videoRef);
// Determine if this is an audio file based on the URL
const isAudioFile = videoUrl && (videoUrl.includes('.wav') || videoUrl.includes('.mp3') || videoUrl.includes('.m4a') || videoUrl.includes('.aac'));
const mediaRef = isAudioFile ? audioRef : videoRef;
const { seekTo, togglePlay } = useVideoSync(mediaRef as React.RefObject<HTMLVideoElement | HTMLAudioElement | null>);
const [displayTime, setDisplayTime] = useState(0);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const media = mediaRef.current;
if (!media) return;
let raf = 0;
const tick = () => {
setDisplayTime(video.currentTime);
setDisplayTime(media.currentTime);
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [videoUrl]);
}, [videoUrl, mediaRef]);
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
@ -41,11 +48,11 @@ export default function VideoPlayer() {
const skip = useCallback(
(delta: number) => {
const video = videoRef.current;
if (!video) return;
seekTo(Math.max(0, Math.min(duration, video.currentTime + delta)));
const media = mediaRef.current;
if (!media) return;
seekTo(Math.max(0, Math.min(duration, media.currentTime + delta)));
},
[seekTo, duration],
[seekTo, duration, mediaRef],
);
if (!videoUrl) {
@ -59,13 +66,37 @@ export default function VideoPlayer() {
return (
<div className="w-full h-full flex flex-col">
<div className="flex-1 flex items-center justify-center bg-black rounded-lg overflow-hidden min-h-0">
<video
ref={videoRef}
src={videoUrl}
className="max-w-full max-h-full object-contain"
playsInline
onClick={togglePlay}
/>
{isAudioFile ? (
<audio
ref={audioRef}
src={videoUrl}
className="max-w-full max-h-full"
controls={false}
onClick={togglePlay}
onError={(e) => {
console.error('Audio load error:', e);
console.error('Audio src:', videoUrl);
}}
onLoadStart={() => console.log('Audio load start:', videoUrl)}
onLoadedData={() => console.log('Audio loaded data')}
onCanPlay={() => console.log('Audio can play')}
/>
) : (
<video
ref={videoRef}
src={videoUrl}
className="max-w-full max-h-full object-contain"
playsInline
onClick={togglePlay}
onError={(e) => {
console.error('Video load error:', e);
console.error('Video src:', videoUrl);
}}
onLoadStart={() => console.log('Video load start:', videoUrl)}
onLoadedData={() => console.log('Video loaded data')}
onCanPlay={() => console.log('Video can play')}
/>
)}
</div>
<div className="pt-2 space-y-1.5 shrink-0">

View File

@ -1,7 +1,7 @@
import { useCallback, useRef, useEffect } from 'react';
import { useEditorStore } from '../store/editorStore';
export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>) {
export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | HTMLAudioElement | null>) {
const rafRef = useRef<number>(0);
const {
setCurrentTime,
@ -52,13 +52,13 @@ export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>)
}, [videoRef]);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const media = videoRef.current;
if (!media) return;
const onTimeUpdate = () => {
cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
let t = video.currentTime;
let t = media.currentTime;
// Skip over deleted ranges and cut ranges (handle overlapping/chained ranges)
const allSkipRanges = [...deletedRanges, ...cutRanges];
@ -79,19 +79,21 @@ export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>)
}
if (skipCount > 0) {
video.currentTime = t;
media.currentTime = t;
return;
}
// Mute/unmute based on mute ranges
let shouldMute = false;
for (const range of muteRanges) {
if (t >= range.start && t < range.end) {
shouldMute = true;
break;
// Mute/unmute based on mute ranges (only for video elements)
if ('muted' in media) {
let shouldMute = false;
for (const range of muteRanges) {
if (t >= range.start && t < range.end) {
shouldMute = true;
break;
}
}
media.muted = shouldMute;
}
video.muted = shouldMute;
setCurrentTime(t);
});
@ -99,18 +101,18 @@ export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>)
const onPlay = () => setIsPlaying(true);
const onPause = () => setIsPlaying(false);
const onLoadedMetadata = () => setDuration(video.duration);
const onLoadedMetadata = () => setDuration(media.duration);
video.addEventListener('timeupdate', onTimeUpdate);
video.addEventListener('play', onPlay);
video.addEventListener('pause', onPause);
video.addEventListener('loadedmetadata', onLoadedMetadata);
media.addEventListener('timeupdate', onTimeUpdate);
media.addEventListener('play', onPlay);
media.addEventListener('pause', onPause);
media.addEventListener('loadedmetadata', onLoadedMetadata);
return () => {
video.removeEventListener('timeupdate', onTimeUpdate);
video.removeEventListener('play', onPlay);
video.removeEventListener('pause', onPause);
video.removeEventListener('loadedmetadata', onLoadedMetadata);
media.removeEventListener('timeupdate', onTimeUpdate);
media.removeEventListener('play', onPlay);
media.removeEventListener('pause', onPause);
media.removeEventListener('loadedmetadata', onLoadedMetadata);
cancelAnimationFrame(rafRef.current);
};
}, [videoRef, deletedRanges, cutRanges, muteRanges, setCurrentTime, setIsPlaying, setDuration]);

View File

@ -55,6 +55,8 @@ interface EditorActions {
getWordAtTime: (time: number) => number;
loadProject: (projectData: any) => void;
reset: () => void;
pauseUndo: () => void;
resumeUndo: () => void;
}
const initialState: EditorState = {
@ -327,6 +329,22 @@ export const useEditorStore = create<EditorState & EditorActions>()(
},
reset: () => set(initialState),
pauseUndo: () => {
// Access the temporal store through the useEditorStore
const temporalStore = (useEditorStore as any).temporal;
if (temporalStore) {
temporalStore.getState().pause();
}
},
resumeUndo: () => {
// Access the temporal store through the useEditorStore
const temporalStore = (useEditorStore as any).temporal;
if (temporalStore) {
temporalStore.getState().resume();
}
},
}),
{ limit: 100 },
),