import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import * as Tone from 'tone'; import './KGOnePanel.css'; import { FaPlay, FaPause, FaDownload } from 'react-icons/fa'; import { FaCircleNotch, FaGripVertical } from 'react-icons/fa6'; import { useProjectStore } from '../stores/projectStore'; import { KGCore } from '../core/KGCore'; import { KGAudioRegion } from '../core/region/KGAudioRegion'; import { KGAudioFileStorage } from '../core/io/KGAudioFileStorage'; import { ConfigManager } from '../core/config/ConfigManager'; import { DEBUG_MODE } from '../constants/uiConstants'; import { fetchWithRetry } from '../util/retryUtil'; import { sliceAudioToWav } from '../util/audioUtil'; import type { KeySignature } from '../core/KGProject'; import { ImportStemsCommand } from '../core/commands'; import type { StemImportEntry } from '../core/commands'; import { showAlert } from '../util/dialogUtil'; import { LOCAL_SEPARATOR_MODEL_CONFIG, LOCAL_SEPARATOR_MODEL_FILENAME, LOCAL_SEPARATOR_DEFAULT_MODEL_URL, } from '../util/localSeparatorConfig'; import { LocalSeparatorModelCache } from '../util/localSeparatorModelCache'; import { runLocalSeparator } from '../util/localSeparatorRunner'; import { LocalOrtRuntimeManager, detectLocalRuntimeSupport } from '../util/localSeparatorRuntime'; // ─── Types ──────────────────────────────────────────────────────────────────── type Tab = 'clip' | 'fullsong' | 'remix' | 'repaint' | 'separator'; type KGOneMode = 'server' | 'local-separator'; type GenStatus = 'idle' | 'loading-model' | 'generating' | 'polling' | 'downloading' | 'done' | 'error'; const SERVER_SEPARATOR_MODELS = [ { label: 'Vocal and Instrument (Medium Accuracy)', value: 'UVR-MDX-NET-Inst_HQ_3.onnx' }, { label: 'Vocal and Instrument (High Accuracy)', value: 'MDX23C-8KFFT-InstVoc_HQ.ckpt' }, { label: 'Vocal, Drums, Bass, Guitar, Piano, and Others', value: 'htdemucs_6s.yaml' }, ] as const; const LOCAL_SEPARATOR_MODELS = [ { label: LOCAL_SEPARATOR_MODEL_CONFIG.displayName, value: LOCAL_SEPARATOR_MODEL_FILENAME }, ] as const; const KGONE_TABS = ['fullsong', 'remix', 'repaint', 'separator'] as const; const CLIP_NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; const FLAT_TO_SHARP: Record = { Bb: 'A#', Eb: 'D#', Ab: 'G#', Db: 'C#', Gb: 'F#', Cb: 'B', Fb: 'E', }; function parseKeySignature(ks: string): { note: string; scale: 'major' | 'minor' } { const parts = ks.split(' '); const rawNote = parts[0] ?? 'C'; const note = FLAT_TO_SHARP[rawNote] ?? rawNote; const scale = (parts[1] === 'minor' ? 'minor' : 'major') as 'major' | 'minor'; return { note, scale }; } // Debug helper — logs KGOne requests and responses when DEBUG_MODE.KGONE is on function kgoneLog(direction: 'REQ' | 'RES', label: string, payload: unknown) { if (!DEBUG_MODE.KGONE) return; console.log(`[KGOne ${direction}] ${label}`, payload); } function getKGOneBaseUrl(): string { return (ConfigManager.instance().get('general.kgone.base_url') as string) || 'http://127.0.0.1:8000'; } function formatTime(sec: number): string { if (!isFinite(sec)) return '0:00'; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s.toString().padStart(2, '0')}`; } function formatKGOneTabLabel(tab: Tab): string { if (tab === 'fullsong') return 'Full Song'; if (tab === 'remix') return 'Remix'; if (tab === 'repaint') return 'Repaint'; return 'Separator'; } function getDefaultKGOneTab(mode: KGOneMode): Tab { return mode === 'local-separator' ? 'separator' : 'fullsong'; } function getKGOneMode(): KGOneMode { const enabled = (ConfigManager.instance().get('general.kgone.enabled') as boolean | undefined) ?? false; return enabled ? 'server' : 'local-separator'; } // ─── Shared components ──────────────────────────────────────────────────────── interface ExpanderProps { label: string; children: React.ReactNode; } const Expander: React.FC = ({ label, children }) => { const [open, setOpen] = useState(false); return (
{open &&
{children}
}
); }; // ─── Audio Player ───────────────────────────────────────────────────────────── interface AudioPlayerProps { src: string; /** When provided, makes the player draggable (shows grip handle) and adds a download button */ dragData?: { midiUrl?: string; // full URL to /v1/clip/midi/{taskId}; absent = MIDI import not supported audioFileName: string; // filename used for download and OPFS storage }; } const AudioPlayer: React.FC = ({ src, dragData }) => { const audioRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const togglePlay = () => { const audio = audioRef.current; if (!audio) return; if (audio.paused) { audio.play().catch(() => { }); } else { audio.pause(); } }; const handleProgressClick = (e: React.MouseEvent) => { const audio = audioRef.current; if (!audio || duration === 0) return; const rect = e.currentTarget.getBoundingClientRect(); const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); audio.currentTime = ratio * duration; setCurrentTime(ratio * duration); }; const handleDragStart = (e: React.DragEvent) => { if (!dragData) return; e.dataTransfer.setData('application/kgone-clip', JSON.stringify({ midiUrl: dragData.midiUrl, audioUrl: src, audioDurationSeconds: duration, audioFileName: dragData.audioFileName, })); e.dataTransfer.effectAllowed = 'copy'; }; const handleDownload = () => { const a = document.createElement('a'); a.href = src; a.download = dragData?.audioFileName ?? 'kgone_clip.wav'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; return (