ai tools finished

This commit is contained in:
2026-04-15 17:13:56 -06:00
parent d11e26cf2d
commit 024b9bd806
17 changed files with 566 additions and 328 deletions

View File

@ -47,14 +47,12 @@ export default function App() {
transcriptionModel,
language,
isTranscribing,
transcriptionProgress,
transcriptionStatus,
loadVideo,
setBackendUrl,
setTranscription,
setTranscriptionModel,
setTranscribing,
backendUrl,
selectedWordIndices,
addCutRange,
addMuteRange,

View File

@ -1,10 +1,10 @@
import { useState, useCallback, useMemo } from 'react';
import { useState, useCallback } from 'react';
import { useEditorStore } from '../store/editorStore';
import { Download, Loader2, Zap, Cog, Info } from 'lucide-react';
import type { ExportOptions } from '../types/project';
export default function ExportDialog() {
const { videoPath, words, deletedRanges, cutRanges, muteRanges, gainRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments } =
const { videoPath, words, deletedRanges, muteRanges, gainRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments } =
useEditorStore();
const hasCuts = deletedRanges.length > 0;
@ -60,7 +60,7 @@ export default function ExportDialog() {
console.error('Export error:', err);
setExporting(false);
}
}, [videoPath, options, backendUrl, setExporting, getKeepSegments]);
}, [videoPath, options, backendUrl, setExporting, getKeepSegments, deletedRanges, muteRanges, gainRanges, globalGainDb, words]);
return (
<div className="p-4 space-y-5">

View File

@ -1,5 +1,5 @@
import { useAIStore } from '../store/aiStore';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import type { AIProvider } from '../types/project';
import { useEditorStore } from '../store/editorStore';
import { Bot, Cloud, Brain, RefreshCw } from 'lucide-react';
@ -10,7 +10,7 @@ export default function SettingsPanel() {
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
const [loadingModels, setLoadingModels] = useState(false);
const fetchOllamaModels = async () => {
const fetchOllamaModels = useCallback(async () => {
setLoadingModels(true);
try {
const res = await fetch(`${backendUrl}/ai/ollama-models`);
@ -23,11 +23,11 @@ export default function SettingsPanel() {
} finally {
setLoadingModels(false);
}
};
}, [backendUrl]);
useEffect(() => {
fetchOllamaModels();
}, [backendUrl]);
}, [fetchOllamaModels]);
const providerIcons: Record<AIProvider, React.ReactNode> = {
ollama: <Bot className="w-4 h-4" />,
@ -35,12 +35,6 @@ export default function SettingsPanel() {
claude: <Brain className="w-4 h-4" />,
};
const providerLabels: Record<AIProvider, string> = {
ollama: 'Ollama (Local)',
openai: 'OpenAI',
claude: 'Claude (Anthropic)',
};
return (
<div className="p-4 space-y-6">
<h3 className="text-sm font-semibold">AI Settings</h3>

View File

@ -13,7 +13,6 @@ export default function TranscriptEditor() {
const hoveredWordIndex = useEditorStore((s) => s.hoveredWordIndex);
const setSelectedWordIndices = useEditorStore((s) => s.setSelectedWordIndices);
const setHoveredWordIndex = useEditorStore((s) => s.setHoveredWordIndex);
const deleteSelectedWords = useEditorStore((s) => s.deleteSelectedWords);
const restoreRange = useEditorStore((s) => s.restoreRange);
const removeCutRange = useEditorStore((s) => s.removeCutRange);
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);

View File

@ -150,7 +150,6 @@ export default function WaveformTimeline({
const [selectionStart, setSelectionStart] = useState<number | null>(null);
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string} | null>(null);
const [editingZone, setEditingZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
const [hoverCursor, setHoverCursor] = useState<string>('crosshair');
const editingZoneRef = useRef<{type: 'cut' | 'mute' | 'gain', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
const [showCutZones, setShowCutZones] = useState(true);
@ -210,7 +209,7 @@ export default function WaveformTimeline({
);
if (cancelled) return;
waveformDataRef.current = waveformData;
drawStaticWaveform();
drawStaticWaveformRef.current();
} catch (err) {
if (cancelled || (err instanceof DOMException && err.name === 'AbortError')) {
console.log('[WaveformTimeline] req=', requestId, 'aborted/cancelled');
@ -594,7 +593,6 @@ export default function WaveformTimeline({
// Check if click is in waveform area
if (y < waveTop || y > waveTop + waveH) return null;
const clickTime = scroll + x / pxPerSec;
const handleSize = forHover ? 6 : 8; // Smaller hit area for hover, larger for click
// Check cut ranges
@ -743,7 +741,6 @@ export default function WaveformTimeline({
setSelectedZone({ type: zoneHit.type, id: zoneHit.id });
} else {
setSelectedZone({ type: zoneHit.type, id: zoneHit.id });
setEditingZone(zoneHit);
editingZoneRef.current = zoneHit;
}
isDraggingRef.current = true;
@ -795,7 +792,6 @@ export default function WaveformTimeline({
const onUp = () => {
isDraggingRef.current = false;
setIsDragging(false);
setEditingZone(null);
editingZoneRef.current = null;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
@ -808,7 +804,6 @@ export default function WaveformTimeline({
// Clear selection if clicking elsewhere
setSelectedZone(null);
setEditingZone(null);
if (cutMode || muteMode || gainMode) {
// Range selection mode
@ -886,7 +881,6 @@ export default function WaveformTimeline({
if (e.key === 'Escape') {
setSelectedZone(null);
setEditingZone(null);
editingZoneRef.current = null;
} else if (e.key === 'Delete' || e.key === 'Backspace') {
if (selectedZone) {
@ -901,7 +895,6 @@ export default function WaveformTimeline({
removeGainRange(selectedZone.id);
}
setSelectedZone(null);
setEditingZone(null);
editingZoneRef.current = null;
}
}

View File

@ -2,7 +2,6 @@ import { useEffect, useRef } from 'react';
import { useEditorStore } from '../store/editorStore';
export function useKeyboardShortcuts() {
const deleteSelectedWords = useEditorStore((s) => s.deleteSelectedWords);
const addCutRange = useEditorStore((s) => s.addCutRange);
const selectedWordIndices = useEditorStore((s) => s.selectedWordIndices);
const words = useEditorStore((s) => s.words);
@ -148,7 +147,7 @@ export function useKeyboardShortcuts() {
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [deleteSelectedWords, selectedWordIndices]);
}, [addCutRange, selectedWordIndices, words]);
}
async function saveProject() {
@ -190,24 +189,19 @@ async function saveProject() {
}
}
let cheatsheetVisible = false;
function toggleCheatsheet() {
const existing = document.getElementById('keyboard-cheatsheet');
if (existing) {
existing.remove();
cheatsheetVisible = false;
return;
}
cheatsheetVisible = true;
const overlay = document.createElement('div');
overlay.id = 'keyboard-cheatsheet';
overlay.style.cssText =
'position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);';
overlay.onclick = () => {
overlay.remove();
cheatsheetVisible = false;
};
const shortcuts = [

View File

@ -27,6 +27,7 @@ const EXPORT_FILTERS = [
window.electronAPI = {
openFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
void _options;
const result = await open({
multiple: false,
filters: VIDEO_FILTERS,
@ -35,6 +36,7 @@ window.electronAPI = {
},
saveFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
void _options;
const result = await save({ filters: EXPORT_FILTERS });
return result ?? null;
},

View File

@ -155,8 +155,12 @@ export const useEditorStore = create<EditorState & EditorActions>()(
const { videoPath, words, segments, deletedRanges, cutRanges, muteRanges, gainRanges, globalGainDb, silenceTrimGroups, transcriptionModel, language, exportedAudioPath } = get();
if (!videoPath) throw new Error('No video loaded');
const now = new Date().toISOString();
// Strip globalStartIndex (runtime-only field) before persisting
const persistSegments = segments.map(({ globalStartIndex: _drop, ...rest }) => rest);
// Strip globalStartIndex (runtime-only field) before persisting.
const persistSegments = segments.map((seg) => {
const rest = { ...seg };
delete (rest as Partial<Segment>).globalStartIndex;
return rest;
});
return {
version: 1,
videoPath,