From 3c5730a8eb6f66a33aa0aceaa9db643e038bcbbb Mon Sep 17 00:00:00 2001 From: Berkan Cesur Date: Mon, 17 Nov 2025 21:44:30 +0300 Subject: [PATCH] possibility to use pre rendered stems --- .gitignore | 2 + public/worklets/metronome-processor.js | 242 ++++++++++++++++++ python_backend/blueprints/beats/routes.py | 7 +- python_backend/blueprints/beats/validators.py | 6 +- python_backend/models/beat_transformer.py | 138 +++++++++- .../services/audio/beat_detection_service.py | 11 +- .../detectors/beat_transformer_detector.py | 11 +- src/app/analyze/page.tsx | 45 +++- src/app/api/detect-beats/route.ts | 6 + src/components/analysis/StemsFolderInput.tsx | 87 +++++++ src/services/audio/audioAnalysisService.ts | 5 +- src/services/audio/beatDetectionService.ts | 6 +- .../chord-analysis/chordRecognitionService.ts | 5 +- 13 files changed, 537 insertions(+), 34 deletions(-) create mode 100644 public/worklets/metronome-processor.js create mode 100644 src/components/analysis/StemsFolderInput.tsx diff --git a/.gitignore b/.gitignore index 3cbc0e1..2834832 100644 --- a/.gitignore +++ b/.gitignore @@ -278,3 +278,5 @@ scripts/utilities __tests__/ __test-results__/ +/python_backend +python_backend/requirements.txt diff --git a/public/worklets/metronome-processor.js b/public/worklets/metronome-processor.js new file mode 100644 index 0000000..8199ae3 --- /dev/null +++ b/public/worklets/metronome-processor.js @@ -0,0 +1,242 @@ +/** + * MetronomeProcessor - AudioWorklet for precise metronome timing + * Runs in a separate thread, immune to UI interactions and main thread blocking + */ +class MetronomeProcessor extends AudioWorkletProcessor { + constructor() { + super(); + + // State management + this.isPlaying = false; + this.nextClickTime = 0; + this.bpm = 120; + this.clickDuration = 0.06; // 60ms clicks + this.volume = 1.0; + this.volumeBoost = 3.0; + this.beatInterval = 60 / this.bpm; + + // Timing and synchronization + this.startTime = 0; + this.pauseTime = 0; + this.currentBeatIndex = 0; + this.beats = []; + this.useBeatsArray = false; + + // Click sound generation parameters + this.clickFrequency = 1200; + this.clickAttack = 0.002; // 2ms attack + this.clickDecay = 25; // Exponential decay factor + + // Handle messages from main thread + this.port.onmessage = (event) => { + const { type, data } = event.data; + + switch (type) { + case 'start': + this.handleStart(data); + break; + case 'stop': + this.handleStop(); + break; + case 'seek': + this.handleSeek(data); + break; + case 'updateBPM': + this.updateBPM(data.bpm); + break; + case 'updateVolume': + this.updateVolume(data.volume); + break; + case 'setBeats': + this.setBeats(data.beats); + break; + case 'updateTime': + this.updateCurrentTime(data.time); + break; + } + }; + } + + handleStart(data) { + this.isPlaying = true; + this.startTime = currentTime; + this.pauseTime = 0; + + if (data.bpm) this.bpm = data.bpm; + if (data.volume !== undefined) this.volume = data.volume; + if (data.beats) this.setBeats(data.beats); + if (data.currentTime !== undefined) { + this.seekToTime(data.currentTime); + } else { + this.currentBeatIndex = 0; + this.calculateNextClickTime(); + } + + this.beatInterval = 60 / this.bpm; + + // Send confirmation + this.port.postMessage({ type: 'started' }); + } + + handleStop() { + this.isPlaying = false; + this.pauseTime = currentTime; + this.port.postMessage({ type: 'stopped' }); + } + + handleSeek(data) { + if (data.time !== undefined) { + this.seekToTime(data.time); + } + } + + seekToTime(targetTime) { + if (this.useBeatsArray && this.beats.length > 0) { + // Find the next beat after the target time + this.currentBeatIndex = 0; + for (let i = 0; i < this.beats.length; i++) { + if (this.beats[i] > targetTime) { + this.currentBeatIndex = i; + break; + } + } + + if (this.currentBeatIndex < this.beats.length) { + this.nextClickTime = this.beats[this.currentBeatIndex]; + } else { + this.nextClickTime = Infinity; // No more beats + } + } else { + // Calculate beat position based on BPM + const beatsSinceStart = Math.floor(targetTime / this.beatInterval); + this.currentBeatIndex = beatsSinceStart; + this.nextClickTime = (beatsSinceStart + 1) * this.beatInterval; + } + } + + updateBPM(bpm) { + if (bpm > 0 && bpm <= 300) { + this.bpm = bpm; + this.beatInterval = 60 / this.bpm; + + if (!this.useBeatsArray) { + this.calculateNextClickTime(); + } + } + } + + updateVolume(volume) { + this.volume = Math.max(0, Math.min(1, volume)); + } + + setBeats(beats) { + if (Array.isArray(beats) && beats.length > 0) { + // Filter out null values and ensure all are numbers + this.beats = beats.filter(b => typeof b === 'number' && !isNaN(b)); + this.useBeatsArray = this.beats.length > 0; + + if (this.useBeatsArray) { + this.currentBeatIndex = 0; + this.calculateNextClickTime(); + } + } else { + this.beats = []; + this.useBeatsArray = false; + } + } + + updateCurrentTime(time) { + // Synchronize with external time source + if (this.isPlaying) { + this.seekToTime(time); + } + } + + calculateNextClickTime() { + if (this.useBeatsArray && this.beats.length > 0) { + if (this.currentBeatIndex < this.beats.length) { + this.nextClickTime = this.beats[this.currentBeatIndex]; + } else { + this.nextClickTime = Infinity; // No more beats + } + } else if (this.beatInterval > 0) { + this.nextClickTime = (this.currentBeatIndex + 1) * this.beatInterval; + } + } + + generateClick(output, startSample, numSamples) { + const effectiveVolume = this.volume * this.volumeBoost; + const attackSamples = Math.floor(this.clickAttack * sampleRate); + const clickSamples = Math.floor(this.clickDuration * sampleRate); + + for (let channel = 0; channel < output.length; channel++) { + const outputChannel = output[channel]; + + for (let i = 0; i < numSamples && i < clickSamples; i++) { + const sampleIndex = startSample + i; + const t = sampleIndex / sampleRate; + + // Generate click sound (sine wave) + const phase = 2 * Math.PI * this.clickFrequency * t; + const sample = Math.sin(phase); + + // Apply envelope + let envelope; + if (sampleIndex < attackSamples) { + // Attack phase + envelope = sampleIndex / attackSamples; + } else { + // Decay phase + envelope = Math.exp(-t * this.clickDecay); + } + + outputChannel[i] = sample * envelope * effectiveVolume * 0.5; // 0.5 to prevent clipping + } + } + + // Send click event to main thread + this.port.postMessage({ + type: 'click', + beatIndex: this.currentBeatIndex, + time: this.nextClickTime + }); + + // Move to next beat + this.currentBeatIndex++; + this.calculateNextClickTime(); + } + + process(inputs, outputs, parameters) { + if (!this.isPlaying || outputs.length === 0) { + return true; // Keep processor alive + } + + const output = outputs[0]; + const numSamples = output[0].length; + + // Clear output buffer first + for (let channel = 0; channel < output.length; channel++) { + output[channel].fill(0); + } + + // Check if we need to generate a click in this render quantum + const quantumDuration = numSamples / sampleRate; + const quantumStartTime = currentTime; + const quantumEndTime = quantumStartTime + quantumDuration; + + // Check if next click falls within this quantum + if (this.nextClickTime >= quantumStartTime && this.nextClickTime < quantumEndTime) { + // Calculate sample position for the click + const clickOffsetTime = this.nextClickTime - quantumStartTime; + const clickStartSample = Math.floor(clickOffsetTime * sampleRate); + + // Generate the click + this.generateClick(output, clickStartSample, numSamples - clickStartSample); + } + + return true; // Keep processor alive + } +} + +// Register the processor +registerProcessor('metronome-processor', MetronomeProcessor); \ No newline at end of file diff --git a/python_backend/blueprints/beats/routes.py b/python_backend/blueprints/beats/routes.py index 04e5ad8..3dd2f3f 100644 --- a/python_backend/blueprints/beats/routes.py +++ b/python_backend/blueprints/beats/routes.py @@ -38,6 +38,7 @@ def detect_beats(): - audio_path: Alternative to file, path to an existing audio file on the server - detector: 'beat-transformer', 'madmom', 'librosa', or 'auto' (default) - force: Set to 'true' to force using requested detector even for large files + - stems_folder: Optional path to folder containing pre-rendered stems (for beat-transformer) Returns: - JSON with beat and downbeat information @@ -71,7 +72,8 @@ def detect_beats(): result = beat_service.detect_beats( file_path=file_path, detector=params['detector'], - force=params['force'] + force=params['force'], + stems_folder=params.get('stems_folder') ) else: # Use provided audio path @@ -83,7 +85,8 @@ def detect_beats(): result = beat_service.detect_beats( file_path=file_path, detector=params['detector'], - force=params['force'] + force=params['force'], + stems_folder=params.get('stems_folder') ) # Return result diff --git a/python_backend/blueprints/beats/validators.py b/python_backend/blueprints/beats/validators.py index 0fd6987..c602e42 100644 --- a/python_backend/blueprints/beats/validators.py +++ b/python_backend/blueprints/beats/validators.py @@ -42,10 +42,14 @@ def validate_beat_detection_request() -> Tuple[bool, Optional[str], Optional[Fil if file and file.filename == '': return False, "No file selected", None, {} + # Get stems_folder parameter if provided + stems_folder = request.form.get('stems_folder') + params = { 'detector': detector, 'force': force, - 'audio_path': audio_path + 'audio_path': audio_path, + 'stems_folder': stems_folder } return True, None, file, params diff --git a/python_backend/models/beat_transformer.py b/python_backend/models/beat_transformer.py index 0850e39..2b91018 100644 --- a/python_backend/models/beat_transformer.py +++ b/python_backend/models/beat_transformer.py @@ -580,13 +580,34 @@ def get_device_info(self): return info - def demix_audio_to_spectrogram(self, audio_file, sr=44100, n_fft=4096, n_mels=128, fmin=30, fmax=11000): - """Enhanced demixing with real Spleeter - now used for both local and production + def demix_audio_to_spectrogram(self, audio_file, sr=44100, n_fft=4096, n_mels=128, fmin=30, fmax=11000, stems_folder=None): + """Enhanced demixing with real Spleeter or pre-rendered stems - This method uses real Spleeter 5-stems separation for better beat detection accuracy. - Librosa fallback is commented out to ensure Spleeter is always used. + This method can use either: + 1. Pre-rendered stems from a specified folder (if stems_folder is provided) + 2. Real Spleeter 5-stems separation (default) + + Args: + audio_file: Path to the audio file + sr: Sample rate + n_fft: FFT window size + n_mels: Number of mel bins + fmin: Minimum frequency + fmax: Maximum frequency + stems_folder: Optional path to folder containing pre-rendered stems """ - # CHANGED: Always use Spleeter, no fallback to librosa + # Check if pre-rendered stems are available + if stems_folder: + if DEBUG: + print(f"🎵 Checking for pre-rendered stems in: {stems_folder}") + try: + return self._use_prerendered_stems(stems_folder, sr, n_fft, n_mels, fmin, fmax) + except Exception as e: + if DEBUG: + print(f"⚠️ Failed to use pre-rendered stems: {e}") + print("🔄 Falling back to Spleeter...") + + # Default: Use Spleeter if DEBUG: print("🎵 Using real Spleeter 5-stems separation...") return self._demix_with_real_spleeter(audio_file, sr, n_fft, n_mels, fmin, fmax) @@ -604,6 +625,106 @@ def demix_audio_to_spectrogram(self, audio_file, sr=44100, n_fft=4096, n_mels=12 # print("🎼 Using librosa-based spectrogram creation (production mode)") # return self._demix_with_librosa_fallback(audio_file, sr, n_fft, n_mels, fmin, fmax) + def _use_prerendered_stems(self, stems_folder, sr=44100, n_fft=4096, n_mels=128, fmin=30, fmax=11000): + """Use pre-rendered stems instead of running Spleeter + + Args: + stems_folder: Path to folder containing pre-rendered stems + sr: Sample rate + n_fft: FFT window size + n_mels: Number of mel bins + fmin: Minimum frequency + fmax: Maximum frequency + + Returns: + Stacked spectrograms from the pre-rendered stems + """ + import os + from pathlib import Path + + if DEBUG: + print(f"🎵 Loading pre-rendered stems from: {stems_folder}") + + # Define expected stem files (Spleeter 5-stems naming) + stem_files = { + 'vocals': 'vocals.mp3', + 'drums': 'drums.mp3', + 'bass': 'bass.mp3', + 'piano': 'other.mp3', # Spleeter calls it 'piano' but file might be 'other' + 'other': 'other.mp3' # Alternative naming + } + + # Check if folder exists + if not os.path.exists(stems_folder): + raise ValueError(f"Stems folder does not exist: {stems_folder}") + + # Load each stem and create spectrograms + spectrograms = [] + loaded_stems = [] + + # Create Mel filter bank + mel_f = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels, fmin=fmin, fmax=fmax).T + + # Try to load stems in order (vocals, drums, bass, other, other) + # This matches Spleeter's 5-stem output order + stem_order = ['vocals', 'drums', 'bass', 'other', 'other'] # Use 'other' twice for 5 channels + + for stem_name in stem_order: + stem_path = None + + # Try different file naming conventions + possible_names = [f"{stem_name}.mp3", f"{stem_name}.wav"] + if stem_name == 'piano': + possible_names.extend(['other.mp3', 'other.wav']) + + for filename in possible_names: + test_path = os.path.join(stems_folder, filename) + if os.path.exists(test_path): + stem_path = test_path + break + + if not stem_path: + # For the 5th channel, we can duplicate 'other' if we already have 4 stems + if len(spectrograms) == 4: + if DEBUG: + print(f"⚠️ Using duplicated 'other' stem for 5th channel") + # Duplicate the last spectrogram + spectrograms.append(spectrograms[-1]) + loaded_stems.append(f"{stem_name} (duplicated)") + continue + else: + raise FileNotFoundError(f"Stem file not found for '{stem_name}' in {stems_folder}") + + if DEBUG: + print(f"📁 Loading stem: {stem_path}") + + # Load the stem audio + stem_audio, _ = librosa.load(stem_path, sr=sr, mono=True) + + # Create spectrogram using same parameters as Spleeter processing + stft = librosa.stft(stem_audio, n_fft=n_fft, hop_length=n_fft//4) + stft_power = np.abs(stft)**2 + spec = np.dot(stft_power.T, mel_f) + spec_db = librosa.power_to_db(spec, ref=np.max) + spectrograms.append(spec_db) + loaded_stems.append(os.path.basename(stem_path)) + + # Ensure we have exactly 5 channels for Beat-Transformer + while len(spectrograms) < 5: + if DEBUG: + print(f"⚠️ Padding with duplicated spectrogram to reach 5 channels") + spectrograms.append(spectrograms[-1]) + loaded_stems.append("(duplicated)") + + # Stack all stem spectrograms (shape: num_channels x time x mel_bins) + result = np.stack(spectrograms[:5], axis=0) # Ensure exactly 5 channels + + if DEBUG: + print(f"✅ Pre-rendered stems loaded successfully: {loaded_stems}") + print(f"🎯 Output shape: {result.shape}") + + return result + def _demix_with_real_spleeter(self, audio_file, sr=44100, n_fft=4096, n_mels=128, fmin=30, fmax=11000): """Real Spleeter-based demixing implementation""" import tempfile @@ -816,11 +937,12 @@ def _demix_with_librosa_fallback(self, audio_file, sr=44100, n_fft=4096, n_mels= return result - def detect_beats(self, audio_file): + def detect_beats(self, audio_file, stems_folder=None): """Detect beats and downbeats from an audio file using Beat Transformer Args: audio_file (str): Path to audio file + stems_folder (str, optional): Path to folder containing pre-rendered stems Returns: dict: Dictionary containing beat and downbeat information @@ -833,7 +955,9 @@ def detect_beats(self, audio_file): # Step 1: Demix audio and create spectrograms if DEBUG: print(f"Demixing audio and creating spectrograms: {audio_file}") - demixed_spec = self.demix_audio_to_spectrogram(audio_file) + if stems_folder: + print(f"Using pre-rendered stems from: {stems_folder}") + demixed_spec = self.demix_audio_to_spectrogram(audio_file, stems_folder=stems_folder) # Step 2: Prepare input for the model with proper device handling if DEBUG: diff --git a/python_backend/services/audio/beat_detection_service.py b/python_backend/services/audio/beat_detection_service.py index 9a38299..a0dea29 100644 --- a/python_backend/services/audio/beat_detection_service.py +++ b/python_backend/services/audio/beat_detection_service.py @@ -161,7 +161,7 @@ def _select_fallback_detector(self, available_detectors: List[str], file_size_mb return max(available_detectors, key=lambda d: self.size_limits[d]) def detect_beats(self, file_path: str, detector: str = 'auto', - force: bool = False) -> Dict[str, Any]: + force: bool = False, stems_folder: Optional[str] = None) -> Dict[str, Any]: """ Detect beats in an audio file. @@ -169,6 +169,7 @@ def detect_beats(self, file_path: str, detector: str = 'auto', file_path: Path to the audio file detector: Detector to use ('beat-transformer', 'madmom', 'librosa', 'auto') force: Force use of requested detector even if file is large + stems_folder: Optional path to folder containing pre-rendered stems (for beat-transformer) Returns: Dict containing beat detection results with normalized format @@ -203,7 +204,13 @@ def detect_beats(self, file_path: str, detector: str = 'auto', # Run detection detector_service = self.detectors[selected_detector] - result = detector_service.detect_beats(file_path) + + # Pass stems_folder if using beat-transformer + if selected_detector == 'beat-transformer' and stems_folder: + log_info(f"Using pre-rendered stems from: {stems_folder}") + result = detector_service.detect_beats(file_path, stems_folder=stems_folder) + else: + result = detector_service.detect_beats(file_path) # Add metadata result['file_size_mb'] = file_size_mb diff --git a/python_backend/services/detectors/beat_transformer_detector.py b/python_backend/services/detectors/beat_transformer_detector.py index 8380e91..2284ef8 100644 --- a/python_backend/services/detectors/beat_transformer_detector.py +++ b/python_backend/services/detectors/beat_transformer_detector.py @@ -70,13 +70,14 @@ def _get_detector(self): return self._detector - def detect_beats(self, file_path: str, **kwargs) -> Dict[str, Any]: + def detect_beats(self, file_path: str, stems_folder: Optional[str] = None, **kwargs) -> Dict[str, Any]: """ Detect beats in an audio file using Beat Transformer. Args: file_path: Path to the audio file - **kwargs: Additional parameters (unused for Beat Transformer) + stems_folder: Optional path to folder containing pre-rendered stems + **kwargs: Additional parameters Returns: Dict containing normalized beat detection results: @@ -106,9 +107,11 @@ def detect_beats(self, file_path: str, **kwargs) -> Dict[str, Any]: try: detector = self._get_detector() log_info(f"Running Beat Transformer detection on: {file_path}") + if stems_folder: + log_info(f"Using pre-rendered stems from: {stems_folder}") - # Run beat detection - result = detector.detect_beats(file_path) + # Run beat detection with optional stems folder + result = detector.detect_beats(file_path, stems_folder=stems_folder) # Normalize the result format if result.get("success"): diff --git a/src/app/analyze/page.tsx b/src/app/analyze/page.tsx index 86133a8..8de9592 100644 --- a/src/app/analyze/page.tsx +++ b/src/app/analyze/page.tsx @@ -31,6 +31,13 @@ const HeroUIChordModelSelector = dynamic(() => import('@/components/analysis/Her ssr: false }); +const StemsFolderInput = dynamic(() => import('@/components/analysis/StemsFolderInput'), { + loading: () => ( +
+ ), + ssr: false +}); + // Lyrics section (dynamic) const LyricsSectionDyn = dynamic(() => import('@/components/lyrics/LyricsSection').then(mod => ({ default: mod.LyricsSection })), { @@ -89,6 +96,7 @@ export default function LocalAudioAnalyzePage() { const [playbackRate, setPlaybackRate] = useState(1); const [lyricSearchTitle, setLyricSearchTitle] = useState(''); const [lyricSearchArtist, setLyricSearchArtist] = useState(''); + const [stemsFolder, setStemsFolder] = useState(null); const audioRef = useRef(null); const objectUrlRef = useRef(null); @@ -493,7 +501,7 @@ export default function LocalAudioAnalyzePage() { // Start chord and beat analysis with selected detectors using original File object // This avoids the 10x size bloat from AudioBuffer conversion (3.6MB → 41.7MB) - const results = await analyzeAudioWithRateLimit(audioFile, beatDetector, chordDetector); + const results = await analyzeAudioWithRateLimit(audioFile, beatDetector, chordDetector, undefined, stemsFolder); // FIXED: Clear the stage timeout to prevent it from overriding completion if (stageTimeoutRef.current) { @@ -992,18 +1000,29 @@ const simplifiedChordGridData = useMemo(() => { {/* Model Selectors - Hide when analysis is complete */} {!analysisResults && ( -
- - +
+
+ + +
+ + {/* Pre-rendered Stems Input - Only show when beat-transformer is selected */} + {beatDetector === 'beat-transformer' && ( + + )}
)} diff --git a/src/app/api/detect-beats/route.ts b/src/app/api/detect-beats/route.ts index 636ae81..7214d90 100644 --- a/src/app/api/detect-beats/route.ts +++ b/src/app/api/detect-beats/route.ts @@ -102,6 +102,12 @@ export async function POST(request: NextRequest) { } } + // Check for stems_folder in the request + const stemsFolder = formData.get('stems_folder') as string; + if (stemsFolder) { + console.log(`🎵 Using pre-rendered stems from: ${stemsFolder}`); + } + // Forward the request to the backend with extended timeout console.log(`📡 Making fetch request to Python backend...`); const requestedDetector = (formData.get('detector') as string) || 'madmom'; diff --git a/src/components/analysis/StemsFolderInput.tsx b/src/components/analysis/StemsFolderInput.tsx new file mode 100644 index 0000000..03ede8c --- /dev/null +++ b/src/components/analysis/StemsFolderInput.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { useState } from 'react'; +import { Button, Input, Tooltip } from '@heroui/react'; + +interface StemsFolderInputProps { + onChange: (path: string | null) => void; + disabled?: boolean; + className?: string; +} + +export default function StemsFolderInput({ onChange, disabled = false, className = '' }: StemsFolderInputProps) { + const [stemsPath, setStemsPath] = useState(''); + const [isEnabled, setIsEnabled] = useState(false); + + const handleToggle = () => { + const newEnabled = !isEnabled; + setIsEnabled(newEnabled); + + if (!newEnabled) { + onChange(null); + setStemsPath(''); + } else if (stemsPath) { + onChange(stemsPath); + } + }; + + const handlePathChange = (value: string) => { + setStemsPath(value); + if (isEnabled && value) { + onChange(value); + } else if (isEnabled && !value) { + onChange(null); + } + }; + + return ( +
+
+ + + + + {isEnabled && ( + + (Skips Spleeter separation) + + )} +
+ + {isEnabled && ( +
+ +

+ Expected files: vocals.mp3, drums.mp3, bass.mp3, other.mp3 +

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/services/audio/audioAnalysisService.ts b/src/services/audio/audioAnalysisService.ts index a26743b..1321801 100644 --- a/src/services/audio/audioAnalysisService.ts +++ b/src/services/audio/audioAnalysisService.ts @@ -238,7 +238,8 @@ export async function analyzeAudioWithRateLimit( audioInput: File | AudioBuffer | string, beatDetector: 'auto' | 'madmom' | 'beat-transformer' = 'beat-transformer', chordDetector: ChordDetectorType = 'chord-cnn-lstm', - videoId?: string + videoId?: string, + stemsFolder?: string | null ): Promise { const { isLocalBackend } = await import('@/utils/backendConfig'); const isLocalhost = isLocalBackend(); @@ -284,7 +285,7 @@ export async function analyzeAudioWithRateLimit( if (isLocalhost && typeof audioInput === 'string' && audioInput.includes('firebasestorage.googleapis.com')) { results = await detectBeatsFromFirebaseUrl(audioInput, beatDetector, videoId); } else { - results = await detectBeatsWithRateLimit(audioFile, beatDetector); + results = await detectBeatsWithRateLimit(audioFile, beatDetector, stemsFolder); } if (!results || !results.beats) throw new Error('Beat detection failed: missing beats data'); diff --git a/src/services/audio/beatDetectionService.ts b/src/services/audio/beatDetectionService.ts index 54b9b50..5ca29cf 100644 --- a/src/services/audio/beatDetectionService.ts +++ b/src/services/audio/beatDetectionService.ts @@ -183,7 +183,8 @@ export async function getModelInfo(): Promise { */ export async function detectBeatsWithRateLimit( audioFile: File, - detector: 'auto' | 'madmom' | 'beat-transformer' = 'beat-transformer' + detector: 'auto' | 'madmom' | 'beat-transformer' = 'beat-transformer', + stemsFolder?: string | null ): Promise { try { // Enhanced input validation @@ -210,6 +211,9 @@ export async function detectBeatsWithRateLimit( if (detector === 'beat-transformer') { formData.append('force', 'true'); } + if (stemsFolder) { + formData.append('stems_folder', stemsFolder); + } // Create a safe timeout signal that works across environments const timeoutValue = 800000; // 13+ minutes timeout to match API routes diff --git a/src/services/chord-analysis/chordRecognitionService.ts b/src/services/chord-analysis/chordRecognitionService.ts index eae44b1..be0fa32 100644 --- a/src/services/chord-analysis/chordRecognitionService.ts +++ b/src/services/chord-analysis/chordRecognitionService.ts @@ -15,9 +15,10 @@ export async function analyzeAudioWithRateLimit( audioInput: File | AudioBuffer | string, beatDetector: 'auto' | 'madmom' | 'beat-transformer' = 'beat-transformer', chordDetector: ChordDetectorType = 'chord-cnn-lstm', - videoId?: string + videoId?: string, + stemsFolder?: string | null ): Promise { - return analyzeAudioWithRateLimitService(audioInput, beatDetector, chordDetector, videoId); + return analyzeAudioWithRateLimitService(audioInput, beatDetector, chordDetector, videoId, stemsFolder); } export async function analyzeAudio(