ai tools finished
This commit is contained in:
@ -47,14 +47,12 @@ export default function App() {
|
||||
transcriptionModel,
|
||||
language,
|
||||
isTranscribing,
|
||||
transcriptionProgress,
|
||||
transcriptionStatus,
|
||||
loadVideo,
|
||||
setBackendUrl,
|
||||
setTranscription,
|
||||
setTranscriptionModel,
|
||||
setTranscribing,
|
||||
backendUrl,
|
||||
selectedWordIndices,
|
||||
addCutRange,
|
||||
addMuteRange,
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user