Spaces:
Running
Running
deploy from github actions
Browse files- Dockerfile +6 -1
- btc_toolkit/inference.py +19 -1
- btc_toolkit/weights_manager.py +22 -10
- requirements.txt +1 -1
- tasks.py +45 -31
Dockerfile
CHANGED
|
@@ -2,6 +2,7 @@ FROM python:3.10-slim
|
|
| 2 |
|
| 3 |
# Install system dependencies
|
| 4 |
RUN apt-get update && apt-get install -y \
|
|
|
|
| 5 |
ffmpeg \
|
| 6 |
git \
|
| 7 |
build-essential \
|
|
@@ -16,11 +17,15 @@ RUN pip install --upgrade pip "setuptools<70" wheel && \
|
|
| 16 |
pip install --no-build-isolation madmom && \
|
| 17 |
pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# Copy application files
|
| 20 |
COPY . .
|
| 21 |
|
| 22 |
# Create writable folders with permissions for Hugging Face user 1000
|
| 23 |
-
RUN mkdir -p /code/processed /code/uploads /code/logs && chmod -R 777 /code
|
| 24 |
|
| 25 |
# Grant execution rights to start script
|
| 26 |
RUN chmod +x /code/start.sh
|
|
|
|
| 2 |
|
| 3 |
# Install system dependencies
|
| 4 |
RUN apt-get update && apt-get install -y \
|
| 5 |
+
curl \
|
| 6 |
ffmpeg \
|
| 7 |
git \
|
| 8 |
build-essential \
|
|
|
|
| 17 |
pip install --no-build-isolation madmom && \
|
| 18 |
pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 19 |
|
| 20 |
+
# Pre-descargar pesos del modelo BTC de acordes en la imagen
|
| 21 |
+
RUN mkdir -p /code/models && \
|
| 22 |
+
curl -L -o /code/models/btc_chord.pth https://huggingface.co/amaai-lab/music2emo/resolve/main/inference/data/btc_model.pt
|
| 23 |
+
|
| 24 |
# Copy application files
|
| 25 |
COPY . .
|
| 26 |
|
| 27 |
# Create writable folders with permissions for Hugging Face user 1000
|
| 28 |
+
RUN mkdir -p /code/processed /code/uploads /code/logs /code/models && chmod -R 777 /code
|
| 29 |
|
| 30 |
# Grant execution rights to start script
|
| 31 |
RUN chmod +x /code/start.sh
|
btc_toolkit/inference.py
CHANGED
|
@@ -144,7 +144,25 @@ class BTCChordRecognizer:
|
|
| 144 |
# ==========================================
|
| 145 |
# EXTRACCIÓN DE CARACTERÍSTICAS CQT
|
| 146 |
# ==========================================
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
# Transformada de Q constante (CQT) - representación frecuencial de alta resolución
|
| 150 |
# NOTA: Se ha omitido librosa.effects.harmonic por su altísimo costo en CPU
|
|
|
|
| 144 |
# ==========================================
|
| 145 |
# EXTRACCIÓN DE CARACTERÍSTICAS CQT
|
| 146 |
# ==========================================
|
| 147 |
+
try:
|
| 148 |
+
y, sr = librosa.load(audio_path, sr=SAMPLE_RATE, mono=True)
|
| 149 |
+
except Exception as load_err:
|
| 150 |
+
logger.warning(f"[BTC] librosa.load falló ({load_err}), usando fallback soundfile/ffmpeg...")
|
| 151 |
+
try:
|
| 152 |
+
import soundfile as sf
|
| 153 |
+
y, orig_sr = sf.read(audio_path, dtype='float32')
|
| 154 |
+
if y.ndim > 1:
|
| 155 |
+
y = np.mean(y, axis=1)
|
| 156 |
+
if orig_sr != SAMPLE_RATE:
|
| 157 |
+
try:
|
| 158 |
+
import soxr
|
| 159 |
+
y = soxr.resample(y, orig_sr, SAMPLE_RATE)
|
| 160 |
+
except Exception:
|
| 161 |
+
y = librosa.resample(y, orig_sr=orig_sr, target_sr=SAMPLE_RATE)
|
| 162 |
+
sr = SAMPLE_RATE
|
| 163 |
+
except Exception:
|
| 164 |
+
from app.utils.harmony import _load_audio_via_ffmpeg
|
| 165 |
+
y, sr = _load_audio_via_ffmpeg(audio_path, sr=SAMPLE_RATE)
|
| 166 |
|
| 167 |
# Transformada de Q constante (CQT) - representación frecuencial de alta resolución
|
| 168 |
# NOTA: Se ha omitido librosa.effects.harmonic por su altísimo costo en CPU
|
btc_toolkit/weights_manager.py
CHANGED
|
@@ -73,16 +73,28 @@ def ensure_weights_available() -> str:
|
|
| 73 |
logger.info(f"[BTC] Destino: {weights_path}")
|
| 74 |
|
| 75 |
try:
|
| 76 |
-
|
| 77 |
-
"""
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
# Verificar que el archivo se descargó correctamente
|
| 88 |
if not os.path.exists(weights_path) or os.path.getsize(weights_path) == 0:
|
|
|
|
| 73 |
logger.info(f"[BTC] Destino: {weights_path}")
|
| 74 |
|
| 75 |
try:
|
| 76 |
+
headers = {
|
| 77 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
| 78 |
+
}
|
| 79 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 80 |
+
if hf_token:
|
| 81 |
+
headers["Authorization"] = f"Bearer {hf_token}"
|
| 82 |
+
|
| 83 |
+
req = urllib.request.Request(BTC_WEIGHTS_URL, headers=headers)
|
| 84 |
+
with urllib.request.urlopen(req, timeout=60) as response, open(weights_path, 'wb') as out_file:
|
| 85 |
+
total_size = int(response.headers.get('content-length', 0))
|
| 86 |
+
downloaded = 0
|
| 87 |
+
block_size = 65536
|
| 88 |
+
while True:
|
| 89 |
+
chunk = response.read(block_size)
|
| 90 |
+
if not chunk:
|
| 91 |
+
break
|
| 92 |
+
out_file.write(chunk)
|
| 93 |
+
downloaded += len(chunk)
|
| 94 |
+
if total_size > 0:
|
| 95 |
+
percent = min(100, int(downloaded * 100 / total_size))
|
| 96 |
+
if percent % 25 == 0:
|
| 97 |
+
logger.debug(f"[BTC] Descargando: {percent}% ({downloaded / (1024*1024):.1f} MB / {total_size / (1024*1024):.1f} MB)")
|
| 98 |
|
| 99 |
# Verificar que el archivo se descargó correctamente
|
| 100 |
if not os.path.exists(weights_path) or os.path.getsize(weights_path) == 0:
|
requirements.txt
CHANGED
|
@@ -50,7 +50,7 @@ curl-cffi>=0.7.0 # Permite a yt-dlp impersonar Chrome (bypass bot-ch
|
|
| 50 |
# Nota: ffmpeg debe estar instalado en el sistema
|
| 51 |
|
| 52 |
# --- UTILIDADES ---
|
| 53 |
-
setuptools
|
| 54 |
aiofiles==23.2.1
|
| 55 |
pydantic[email]>=2.5.3
|
| 56 |
pydantic-settings==2.1.0
|
|
|
|
| 50 |
# Nota: ffmpeg debe estar instalado en el sistema
|
| 51 |
|
| 52 |
# --- UTILIDADES ---
|
| 53 |
+
setuptools<70.0.0
|
| 54 |
aiofiles==23.2.1
|
| 55 |
pydantic[email]>=2.5.3
|
| 56 |
pydantic-settings==2.1.0
|
tasks.py
CHANGED
|
@@ -431,7 +431,29 @@ def detectar_y_corregir_afinacion_440(audio_path: str, output_base: str):
|
|
| 431 |
return tuning_info
|
| 432 |
|
| 433 |
# Cargar los primeros 45s para estimación rápida y precisa
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
if len(y) == 0:
|
| 436 |
return tuning_info
|
| 437 |
|
|
@@ -646,47 +668,39 @@ def extraer_acordes_pro(audio_path, bpm=None, beats=None):
|
|
| 646 |
Intenta usar primero madmom, y si no está disponible o falla, utiliza
|
| 647 |
el fallback a BTCChordRecognizer (PyTorch Transformer).
|
| 648 |
"""
|
| 649 |
-
# Intentar madmom primero
|
| 650 |
acordes = extraer_acordes_madmom(audio_path)
|
| 651 |
-
if acordes is not None:
|
| 652 |
-
logger.info("[Acordes] Utilizando resultado exitoso de Madmom.")
|
| 653 |
return acordes
|
| 654 |
|
| 655 |
# Fallback a BTC Transformer
|
| 656 |
-
logger.info("[Acordes] Madmom no disponible o
|
| 657 |
if BTCChordRecognizer is None or torch is None:
|
| 658 |
logger.warning("[BTC] Modelo de acordes o PyTorch no disponible (no importado).")
|
| 659 |
return []
|
| 660 |
|
| 661 |
try:
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
acordes_normalizados = []
|
| 675 |
-
prev_chord = None
|
| 676 |
-
for item in btc_acordes:
|
| 677 |
-
mapped_chord = normalize_chord_to_maj_min(item["ch"])
|
| 678 |
-
if mapped_chord != 'N' and mapped_chord != prev_chord:
|
| 679 |
-
acordes_normalizados.append({"t": item["t"], "ch": mapped_chord})
|
| 680 |
-
prev_chord = mapped_chord
|
| 681 |
-
|
| 682 |
-
if acordes_normalizados and acordes_normalizados[0]["t"] > 0.5:
|
| 683 |
-
acordes_normalizados.insert(0, {"t": 0.0, "ch": acordes_normalizados[0]["ch"]})
|
| 684 |
|
| 685 |
-
|
| 686 |
-
|
|
|
|
|
|
|
|
|
|
| 687 |
except Exception as e:
|
| 688 |
-
logger.
|
| 689 |
-
logger.exception("Error en fallback a BTC:")
|
| 690 |
return []
|
| 691 |
|
| 692 |
|
|
|
|
| 431 |
return tuning_info
|
| 432 |
|
| 433 |
# Cargar los primeros 45s para estimación rápida y precisa
|
| 434 |
+
try:
|
| 435 |
+
y, sr = librosa.load(audio_path, sr=22050, duration=45.0, mono=True)
|
| 436 |
+
except Exception:
|
| 437 |
+
try:
|
| 438 |
+
y, orig_sr = sf.read(audio_path, dtype='float32')
|
| 439 |
+
if y.ndim > 1:
|
| 440 |
+
y = np.mean(y, axis=1)
|
| 441 |
+
max_samples = int(orig_sr * 45.0)
|
| 442 |
+
if len(y) > max_samples:
|
| 443 |
+
y = y[:max_samples]
|
| 444 |
+
if orig_sr != 22050:
|
| 445 |
+
try:
|
| 446 |
+
import soxr
|
| 447 |
+
y = soxr.resample(y, orig_sr, 22050)
|
| 448 |
+
except Exception:
|
| 449 |
+
y = librosa.resample(y, orig_sr=orig_sr, target_sr=22050)
|
| 450 |
+
sr = 22050
|
| 451 |
+
except Exception:
|
| 452 |
+
from app.utils.harmony import _load_audio_via_ffmpeg
|
| 453 |
+
y, sr = _load_audio_via_ffmpeg(audio_path, sr=22050)
|
| 454 |
+
if len(y) > 22050 * 45:
|
| 455 |
+
y = y[:22050 * 45]
|
| 456 |
+
|
| 457 |
if len(y) == 0:
|
| 458 |
return tuning_info
|
| 459 |
|
|
|
|
| 668 |
Intenta usar primero madmom, y si no está disponible o falla, utiliza
|
| 669 |
el fallback a BTCChordRecognizer (PyTorch Transformer).
|
| 670 |
"""
|
| 671 |
+
# Intentar madmom primero si está disponible
|
| 672 |
acordes = extraer_acordes_madmom(audio_path)
|
| 673 |
+
if acordes is not None and len(acordes) > 0:
|
| 674 |
+
logger.info(f"[Acordes] Utilizando resultado exitoso de Madmom ({len(acordes)} acordes).")
|
| 675 |
return acordes
|
| 676 |
|
| 677 |
# Fallback a BTC Transformer
|
| 678 |
+
logger.info("[Acordes] Madmom no disponible o no retornó acordes. Usando BTC Transformer...")
|
| 679 |
if BTCChordRecognizer is None or torch is None:
|
| 680 |
logger.warning("[BTC] Modelo de acordes o PyTorch no disponible (no importado).")
|
| 681 |
return []
|
| 682 |
|
| 683 |
try:
|
| 684 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 685 |
+
recognizer = BTCChordRecognizer(device=device)
|
| 686 |
+
btc_acordes = recognizer.recognize(audio_path, bpm=bpm, beats=beats)
|
| 687 |
+
|
| 688 |
+
# Normalizar los acordes obtenidos de BTC por si acaso
|
| 689 |
+
acordes_normalizados = []
|
| 690 |
+
prev_chord = None
|
| 691 |
+
for item in btc_acordes:
|
| 692 |
+
mapped_chord = normalize_chord_to_maj_min(item["ch"])
|
| 693 |
+
if mapped_chord != 'N' and mapped_chord != prev_chord:
|
| 694 |
+
acordes_normalizados.append({"t": item["t"], "ch": mapped_chord})
|
| 695 |
+
prev_chord = mapped_chord
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
|
| 697 |
+
if acordes_normalizados and acordes_normalizados[0]["t"] > 0.5:
|
| 698 |
+
acordes_normalizados.insert(0, {"t": 0.0, "ch": acordes_normalizados[0]["ch"]})
|
| 699 |
+
|
| 700 |
+
logger.info(f"[BTC] ✅ Acordes detectados tras normalización: {len(acordes_normalizados)}")
|
| 701 |
+
return acordes_normalizados
|
| 702 |
except Exception as e:
|
| 703 |
+
logger.error(f"[BTC] Error en fallback de acordes a BTC: {e}", exc_info=True)
|
|
|
|
| 704 |
return []
|
| 705 |
|
| 706 |
|