improved feature 11 and UI

This commit is contained in:
2026-04-15 21:25:47 -06:00
parent 3fa67383c4
commit 168676a9e9
3 changed files with 111 additions and 41 deletions

View File

@ -48,9 +48,12 @@ export default function App() {
language, language,
isTranscribing, isTranscribing,
transcriptionStatus, transcriptionStatus,
markInTime,
markOutTime,
loadVideo, loadVideo,
setProjectFilePath, setProjectFilePath,
setBackendUrl, setBackendUrl,
clearMarkRange,
setTranscription, setTranscription,
setTranscriptionModel, setTranscriptionModel,
setTranscribing, setTranscribing,
@ -333,6 +336,17 @@ export default function App() {
}; };
const handleCut = () => { const handleCut = () => {
if (markInTime !== null && markOutTime !== null) {
const startTime = Math.min(markInTime, markOutTime);
const endTime = Math.max(markInTime, markOutTime);
if (endTime - startTime >= 0.01) {
addCutRange(startTime, endTime);
setActivePanel('zones');
}
clearMarkRange();
return;
}
if (selectedWordIndices.length > 0) { if (selectedWordIndices.length > 0) {
// If words are selected, apply cut immediately // If words are selected, apply cut immediately
const sorted = [...selectedWordIndices].sort((a, b) => a - b); const sorted = [...selectedWordIndices].sort((a, b) => a - b);
@ -349,6 +363,17 @@ export default function App() {
}; };
const handleMute = () => { const handleMute = () => {
if (markInTime !== null && markOutTime !== null) {
const startTime = Math.min(markInTime, markOutTime);
const endTime = Math.max(markInTime, markOutTime);
if (endTime - startTime >= 0.01) {
addMuteRange(startTime, endTime);
setActivePanel('zones');
}
clearMarkRange();
return;
}
if (selectedWordIndices.length > 0) { if (selectedWordIndices.length > 0) {
// If words are selected, apply mute immediately // If words are selected, apply mute immediately
const sorted = [...selectedWordIndices].sort((a, b) => a - b); const sorted = [...selectedWordIndices].sort((a, b) => a - b);
@ -365,6 +390,17 @@ export default function App() {
}; };
const handleGain = () => { const handleGain = () => {
if (markInTime !== null && markOutTime !== null) {
const startTime = Math.min(markInTime, markOutTime);
const endTime = Math.max(markInTime, markOutTime);
if (endTime - startTime >= 0.01) {
addGainRange(startTime, endTime, gainModeDb);
setActivePanel('zones');
}
clearMarkRange();
return;
}
if (selectedWordIndices.length > 0) { if (selectedWordIndices.length > 0) {
const sorted = [...selectedWordIndices].sort((a, b) => a - b); const sorted = [...selectedWordIndices].sort((a, b) => a - b);
const startTime = words[sorted[0]].start; const startTime = words[sorted[0]].start;
@ -379,6 +415,17 @@ export default function App() {
}; };
const handleSpeed = () => { const handleSpeed = () => {
if (markInTime !== null && markOutTime !== null) {
const startTime = Math.min(markInTime, markOutTime);
const endTime = Math.max(markInTime, markOutTime);
if (endTime - startTime >= 0.01) {
addSpeedRange(startTime, endTime, speedModeValue);
setActivePanel('zones');
}
clearMarkRange();
return;
}
if (selectedWordIndices.length > 0) { if (selectedWordIndices.length > 0) {
const sorted = [...selectedWordIndices].sort((a, b) => a - b); const sorted = [...selectedWordIndices].sort((a, b) => a - b);
const startTime = words[sorted[0]].start; const startTime = words[sorted[0]].start;

View File

@ -278,11 +278,13 @@ export default function WaveformTimeline({
const [showMuteZones, setShowMuteZones] = useState(true); const [showMuteZones, setShowMuteZones] = useState(true);
const [showGainZones, setShowGainZones] = useState(true); const [showGainZones, setShowGainZones] = useState(true);
const [showSpeedZones, setShowSpeedZones] = useState(true); const [showSpeedZones, setShowSpeedZones] = useState(true);
const [showAdjustedTimeline, setShowAdjustedTimeline] = useState(false);
const sourceDuration = duration || waveformDataRef.current?.duration || 0; const sourceDuration = duration || waveformDataRef.current?.duration || 0;
const timelineCutRanges = showAdjustedTimeline ? cutRanges : [];
const { segments: timelineSegments, displayDuration } = useMemo( const { segments: timelineSegments, displayDuration } = useMemo(
() => buildTimelineSegments(sourceDuration, cutRanges), () => buildTimelineSegments(sourceDuration, timelineCutRanges),
[sourceDuration, cutRanges], [sourceDuration, timelineCutRanges],
); );
useEffect(() => { useEffect(() => {
@ -447,15 +449,16 @@ export default function WaveformTimeline({
const x1 = (sourceToDisplayTime(range.start, timelineSegments, dur) - scroll) * pxPerSec; const x1 = (sourceToDisplayTime(range.start, timelineSegments, dur) - scroll) * pxPerSec;
const x2 = (sourceToDisplayTime(range.end, timelineSegments, dur) - scroll) * pxPerSec; const x2 = (sourceToDisplayTime(range.end, timelineSegments, dur) - scroll) * pxPerSec;
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id; const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
ctx.fillStyle = isSelected ? 'rgba(239, 68, 68, 0.5)' : 'rgba(239, 68, 68, 0.3)'; ctx.fillStyle = isSelected ? 'rgba(239, 68, 68, 0.62)' : 'rgba(239, 68, 68, 0.46)';
ctx.fillRect(x1, waveTop, x2 - x1, waveH); ctx.fillRect(x1, waveTop, x2 - x1, waveH);
// Keep cut ranges clearly visible even when not selected.
ctx.strokeStyle = isSelected ? '#ef4444' : 'rgba(239, 68, 68, 0.9)';
ctx.lineWidth = isSelected ? 2.8 : 1.8;
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
if (isSelected) { if (isSelected) {
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 2;
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
// Draw resize handles // Draw resize handles
ctx.fillStyle = '#ef4444'; ctx.fillStyle = '#ef4444';
ctx.beginPath(); ctx.beginPath();
@ -634,6 +637,7 @@ export default function WaveformTimeline({
gainMode, gainMode,
speedMode, speedMode,
selectedZone, selectedZone,
showAdjustedTimeline,
markInTime, markInTime,
markOutTime, markOutTime,
displayDuration, displayDuration,
@ -1184,35 +1188,47 @@ export default function WaveformTimeline({
{markOutTime !== null && <span className="text-[10px] text-yellow-300">O {markOutTime.toFixed(2)}s</span>} {markOutTime !== null && <span className="text-[10px] text-yellow-300">O {markOutTime.toFixed(2)}s</span>}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <label className="flex items-center gap-1 text-[10px] text-editor-text-muted select-none">
onClick={() => setShowCutZones((v) => !v)} <input
className={`px-1.5 py-0.5 rounded text-[10px] border ${showCutZones ? 'border-red-500/60 text-red-300 bg-red-500/10' : 'border-editor-border text-editor-text-muted'}`} type="checkbox"
title="Toggle cut zones" checked={showAdjustedTimeline}
> onChange={(e) => setShowAdjustedTimeline(e.target.checked)}
Cut className="h-3 w-3 rounded border-editor-border bg-editor-surface"
</button> />
<button Show adjusted timeline
onClick={() => setShowMuteZones((v) => !v)} </label>
className={`px-1.5 py-0.5 rounded text-[10px] border ${showMuteZones ? 'border-blue-500/60 text-blue-300 bg-blue-500/10' : 'border-editor-border text-editor-text-muted'}`} <div className="flex items-center gap-2 pl-2 ml-1 border-l border-editor-border/80">
title="Toggle mute zones" <span className="text-[10px] text-editor-text-muted">Show zones</span>
> <button
Mute onClick={() => setShowCutZones((v) => !v)}
</button> className={`px-1.5 py-0.5 rounded text-[10px] border ${showCutZones ? 'border-red-500/60 text-red-300 bg-red-500/10' : 'border-editor-border text-editor-text-muted'}`}
<button title="Toggle cut zones"
onClick={() => setShowGainZones((v) => !v)} >
className={`px-1.5 py-0.5 rounded text-[10px] border ${showGainZones ? 'border-amber-500/60 text-amber-300 bg-amber-500/10' : 'border-editor-border text-editor-text-muted'}`} Cut
title="Toggle gain zones" </button>
> <button
Gain onClick={() => setShowMuteZones((v) => !v)}
</button> className={`px-1.5 py-0.5 rounded text-[10px] border ${showMuteZones ? 'border-blue-500/60 text-blue-300 bg-blue-500/10' : 'border-editor-border text-editor-text-muted'}`}
<button title="Toggle mute zones"
onClick={() => setShowSpeedZones((v) => !v)} >
className={`px-1.5 py-0.5 rounded text-[10px] border ${showSpeedZones ? 'border-emerald-500/60 text-emerald-300 bg-emerald-500/10' : 'border-editor-border text-editor-text-muted'}`} Mute
title="Toggle speed zones" </button>
> <button
Speed onClick={() => setShowGainZones((v) => !v)}
</button> className={`px-1.5 py-0.5 rounded text-[10px] border ${showGainZones ? 'border-amber-500/60 text-amber-300 bg-amber-500/10' : 'border-editor-border text-editor-text-muted'}`}
<span className="text-[10px] text-editor-text-muted"> title="Toggle gain zones"
>
Gain
</button>
<button
onClick={() => setShowSpeedZones((v) => !v)}
className={`px-1.5 py-0.5 rounded text-[10px] border ${showSpeedZones ? 'border-emerald-500/60 text-emerald-300 bg-emerald-500/10' : 'border-editor-border text-editor-text-muted'}`}
title="Toggle speed zones"
>
Speed
</button>
</div>
<span className="text-[10px] text-editor-text-muted pl-2 ml-1 border-l border-editor-border/80">
Scroll · Ctrl+Scroll to zoom Scroll · Ctrl+Scroll to zoom
</span> </span>
</div> </div>

View File

@ -2,6 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useEditorStore } from '../store/editorStore'; import { useEditorStore } from '../store/editorStore';
import { Trash2, Scissors, Volume2, SlidersHorizontal, Gauge, Play } from 'lucide-react'; import { Trash2, Scissors, Volume2, SlidersHorizontal, Gauge, Play } from 'lucide-react';
function formatTimelineLikeTime(secs: number): string {
const m = Math.floor(secs / 60);
const s = secs % 60;
if (m > 0) return `${m}:${String(Math.floor(s)).padStart(2, '0')}.${Math.floor((s % 1) * 10)}`;
return `${s.toFixed(1)}s`;
}
export default function ZoneEditor() { export default function ZoneEditor() {
const [viewMode, setViewMode] = useState<'all' | 'cut' | 'mute' | 'gain' | 'speed'>('all'); const [viewMode, setViewMode] = useState<'all' | 'cut' | 'mute' | 'gain' | 'speed'>('all');
const [focusedZone, setFocusedZone] = useState<{ type: 'cut' | 'mute' | 'gain' | 'speed'; id: string } | null>(null); const [focusedZone, setFocusedZone] = useState<{ type: 'cut' | 'mute' | 'gain' | 'speed'; id: string } | null>(null);
@ -256,7 +263,7 @@ export default function ZoneEditor() {
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium truncate"> <div className="font-medium truncate">
{range.start.toFixed(2)}s {range.end.toFixed(2)}s {formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
</div> </div>
<div className="text-editor-text-muted text-[10px]">{range.id}</div> <div className="text-editor-text-muted text-[10px]">{range.id}</div>
</div> </div>
@ -293,7 +300,7 @@ export default function ZoneEditor() {
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium truncate"> <div className="font-medium truncate">
{range.start.toFixed(2)}s {range.end.toFixed(2)}s {formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
</div> </div>
<div className="text-editor-text-muted text-[10px]">{range.id}</div> <div className="text-editor-text-muted text-[10px]">{range.id}</div>
</div> </div>
@ -357,7 +364,7 @@ export default function ZoneEditor() {
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium truncate"> <div className="font-medium truncate">
{range.start.toFixed(2)}s {range.end.toFixed(2)}s {formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
</div> </div>
<div className="text-editor-text-muted text-[10px]"> <div className="text-editor-text-muted text-[10px]">
{range.gainDb > 0 ? '+' : ''}{range.gainDb.toFixed(1)} dB {range.gainDb > 0 ? '+' : ''}{range.gainDb.toFixed(1)} dB
@ -407,7 +414,7 @@ export default function ZoneEditor() {
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium truncate"> <div className="font-medium truncate">
{range.start.toFixed(2)}s {range.end.toFixed(2)}s {formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
</div> </div>
<div className="text-editor-text-muted text-[10px]"> <div className="text-editor-text-muted text-[10px]">
{range.speed.toFixed(2)}x {range.speed.toFixed(2)}x