JoseMSL commited on
Commit
b04b4fe
·
verified ·
1 Parent(s): 66a187d

Upload 5 files

Browse files

Initial BERT API for MEHEARSAL (DistilBERT v6)

Files changed (5) hide show
  1. .dockerignore +8 -0
  2. Dockerfile +13 -0
  3. README.md +106 -9
  4. bert_api.py +356 -0
  5. requirements.txt +6 -0
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ .env.*
6
+ README.md
7
+ .dockerignore
8
+ .gitignore
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ ENV PORT=7860
11
+ EXPOSE 7860
12
+
13
+ CMD ["python", "bert_api.py"]
README.md CHANGED
@@ -1,12 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Msl Internal Api
3
- emoji: 👁
4
- colorFrom: gray
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: other
9
- short_description: MSL Internal API
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bert-api/
2
+
3
+ Backend Python Flask que sirve el clasificador BERT propio de MuseSceneLab para los comandos NLP de MEHEARSAL.
4
+
5
+ **Origen:** copiado de `D:\XDev.Projects\MEHEARSAL-NLP-CONTROLLER-RMIT\mehearsal_v5_and_v6\bert-api\` con 3 modificaciones mínimas para HF Spaces Docker (ver §"Adaptaciones" más abajo).
6
+
7
+ **Despliegue actual:** HuggingFace Space `MuseSceneLab/mehearsal-bert-api` (Docker SDK). Ver doc 33 §8 Fase R4-R7.
8
+
9
+ **Despliegue futuro:** AWS App Runner cuando se cumplan los disparadores de doc 33 §13.4.
10
+
11
+ ---
12
+
13
+ ## Estructura
14
+
15
+ ```
16
+ bert-api/
17
+ ├── bert_api.py Flask app. Carga modelo en arranque, expone /classify y /health
18
+ ├── requirements.txt flask, flask-cors, transformers, torch, huggingface_hub, python-dotenv
19
+ ├── Dockerfile Imagen para HF Spaces. Python 3.11-slim, expone 7860
20
+ ├── .dockerignore Excluye venv, __pycache__, README, etc. de la imagen
21
+ └── README.md Este archivo
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Endpoints
27
+
28
+ | Método | Path | Body | Devuelve |
29
+ |---|---|---|---|
30
+ | `POST` | `/classify` | `{ "utterance": "mute the guitar" }` | `{ intent_label, locale, target, params_json, confidence, model }` |
31
+ | `GET` | `/health` | — | `{ status, model }` |
32
+
33
  ---
34
+
35
+ ## Variables de entorno
36
+
37
+ | Var | Default | Para qué |
38
+ |---|---|---|
39
+ | `HF_TOKEN` | _(obligatorio)_ | Auth para descargar el modelo privado de HF. En HF Spaces se configura como Secret del Space |
40
+ | `BERT_MODEL_NAME` | `MuseSceneLab/mehearsal-nlp-v6` | Cambiar para usar v5 u otro modelo |
41
+ | `PORT` | `7860` | Puerto del servidor Flask. HF Spaces inyecta `7860` automáticamente |
42
+
43
  ---
44
 
45
+ ## Desarrollo local
46
+
47
+ ```bash
48
+ cd bert-api
49
+ python -m venv venv
50
+ .\venv\Scripts\activate # Windows PowerShell: .\venv\Scripts\Activate.ps1
51
+ pip install -r requirements.txt
52
+ $env:HF_TOKEN = "hf_..." # tu token con permiso de lectura sobre el modelo
53
+ python bert_api.py
54
+ ```
55
+
56
+ Levanta en `http://localhost:7860`. Probar con:
57
+
58
+ ```bash
59
+ curl -X POST http://localhost:7860/classify -H "Content-Type: application/json" -d '{"utterance":"mute the guitar"}'
60
+ ```
61
+
62
+ **Nota:** la primera ejecución descarga el modelo (~500 MB) desde HF a la caché local (`~/.cache/huggingface/`). Ejecuciones posteriores son instantáneas.
63
+
64
+ ---
65
+
66
+ ## Despliegue como Space (proceso R4-R7 de doc 33)
67
+
68
+ 1. Crear Space en `huggingface.co/new-space`:
69
+ - Owner: `MuseSceneLab`
70
+ - Name: `mehearsal-bert-api`
71
+ - SDK: **Docker**
72
+ - Hardware: CPU Basic (free)
73
+ - Visibility: Public _(el contenedor; los pesos del modelo siguen privados)_
74
+
75
+ 2. En el Space, ir a Settings → Repository secrets → añadir `HF_TOKEN`.
76
+
77
+ 3. Clonar el repo del Space y empujar el contenido de esta carpeta:
78
+
79
+ ```bash
80
+ git clone https://huggingface.co/spaces/MuseSceneLab/mehearsal-bert-api
81
+ cd mehearsal-bert-api
82
+ cp -r ../MEHEARSAL2025/bert-api/* .
83
+ git add .
84
+ git commit -m "Initial BERT API for MEHEARSAL"
85
+ git push
86
+ ```
87
+
88
+ 4. HF construye la imagen (~3-5 min). Verificar logs en la UI del Space.
89
+
90
+ 5. URL pública: `https://musescenelab-mehearsal-bert-api.hf.space/classify`.
91
+
92
+ ---
93
+
94
+ ## Adaptaciones respecto al código fuente
95
+
96
+ Tres diffs vs `mehearsal_v5_and_v6/bert-api/bert_api.py`:
97
+
98
+ 1. **Import `os`** al principio del archivo.
99
+ 2. **`MODEL_NAME` configurable** vía env var (default `v6`). El fuente lo tenía hardcoded a `v5`.
100
+ 3. **Puerto y debug** en `app.run()`: puerto leído de env var `PORT` (default 7860, requisito de Spaces), `debug=False` para producción.
101
+
102
+ La lógica de `extract_entities()`, `INTENT_METADATA` y el upgrade de intents en `/classify` está **intacta**. Esto evita cualquier divergencia funcional respecto al desarrollo original.
103
+
104
+ ---
105
+
106
+ ## Roadmap
107
+
108
+ - **Ahora:** HF Spaces Docker (este folder = código fuente, sin cambios entre dev local y prod Space)
109
+ - **Futuro:** migración a AWS App Runner. Ver doc 33 §13. El Dockerfile actual sirve igual sin cambios.
bert_api.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bert-api/bert_api.py
2
+ # Origen: D:\XDev.Projects\MEHEARSAL-NLP-CONTROLLER-RMIT\mehearsal_v5_and_v6\bert-api\bert_api.py
3
+ # Adaptaciones mínimas para HF Spaces Docker: import os + MODEL_NAME desde env + puerto desde env + debug=False.
4
+ # Lógica de extract_entities() y upgrade de intents intacta respecto al fuente.
5
+ import os
6
+ from flask import Flask, request, jsonify
7
+ from flask_cors import CORS
8
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
9
+ import torch
10
+ from pathlib import Path
11
+ import re
12
+
13
+
14
+ # Get the project root (parent of bert-api folder)
15
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
16
+
17
+ app = Flask(__name__)
18
+ CORS(app) # Allow requests from Next.js
19
+
20
+ # Load model and tokenizer from Hugging Face.
21
+ # MODEL_NAME es configurable por env var (default: v6). HF_TOKEN se lee automáticamente
22
+ # por huggingface_hub para autenticar el acceso al modelo privado.
23
+
24
+ MODEL_NAME = os.environ.get('BERT_MODEL_NAME', 'MuseSceneLab/mehearsal-nlp-v6')
25
+
26
+ print(f"Loading model from Hugging Face: {MODEL_NAME}...")
27
+
28
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
29
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
30
+
31
+ model.eval()
32
+
33
+ print("Model loaded successfully!")
34
+
35
+
36
+ # Intent to target/params mapping (simplified - you can expand this)
37
+ INTENT_METADATA = {
38
+ 'MUTE_INSTRUMENT': {'requires_target': True, 'target_type': 'instrument'},
39
+ 'NOT_A_COMMAND': {'requires_target': False},
40
+ 'UNMUTE_INSTRUMENT': {'requires_target': True, 'target_type': 'instrument'},
41
+ 'SOLO_INSTRUMENT': {'requires_target': True, 'target_type': 'instrument'},
42
+ 'MUTE_ALL': {'requires_target': False},
43
+ 'VOLUME_ADJUST_RELATIVE': {'requires_target': True, 'target_type': 'instrument'},
44
+ 'VOLUME_SET_ABSOLUTE': {'requires_target': True, 'target_type': 'instrument'},
45
+ 'TEMPO_SET_ABSOLUTE': {'requires_target': False},
46
+ 'TEMPO_ADJUST_RELATIVE': {'requires_target': False},
47
+ 'TEMPO_GRADUAL': {'requires_target': False},
48
+ 'TEMPO_STYLE_MARKING': {'requires_target': False},
49
+ 'JUMP_TO_BAR': {'requires_target': False},
50
+ 'JUMP_TO_SECTION': {'requires_target': True, 'target_type': 'section'},
51
+ 'LOOP_BARS': {'requires_target': False},
52
+ 'LOOP_SECTION': {'requires_target': True, 'target_type': 'section'},
53
+ 'STOP_LOOP': {'requires_target': False},
54
+ }
55
+
56
+ def extract_entities(text, intent):
57
+ """
58
+ Simple entity extraction based on keywords.
59
+ This is a basic implementation - you might want to enhance this.
60
+ """
61
+
62
+ text_lower = text.lower()
63
+
64
+ # Instrument keywords
65
+ instruments = ['drums', 'guitar', 'bass', 'piano', 'synth', 'vocals','strings',
66
+ 'batería', 'guitarra', 'bajo']
67
+
68
+ # Section keywords
69
+ sections = ['verse', 'chorus', 'bridge', 'outro',
70
+ 'introducción', 'verso', 'estribillo', 'puente', 'intro']
71
+
72
+ # Detect language
73
+ locale = 'es' if any(word in text_lower for word in ['batería', 'guitarra', 'bajo']) else 'en'
74
+
75
+ # Extract target
76
+ target = None
77
+ metadata = INTENT_METADATA.get(intent, {})
78
+
79
+ if metadata.get('requires_target'):
80
+ if metadata.get('target_type') == 'instrument':
81
+ for inst in instruments:
82
+ if inst in text_lower:
83
+ target = inst
84
+ break
85
+ elif metadata.get('target_type') == 'section':
86
+ # Use word boundary regex to avoid partial matches
87
+ for sec in sections:
88
+ # Match whole word only - use word boundaries
89
+ if re.search(r'\b' + re.escape(sec) + r'\b', text_lower):
90
+ target = sec
91
+ break
92
+ # If no section found, check for instruments (for cases like "loop the guitar")
93
+ if not target:
94
+ for inst in instruments:
95
+ if re.search(r'\b' + re.escape(inst) + r'\b', text_lower):
96
+ target = inst
97
+ break
98
+
99
+ # Extract parameters (basic implementation)
100
+ params_json = {}
101
+
102
+ # Extract numbers for tempo, volume, bars, etc.
103
+ numbers = re.findall(r'\d+(?:\.\d+)?|\.\d+', text)
104
+
105
+ if 'TEMPO_SET_ABSOLUTE' in intent and numbers:
106
+ params_json['bpm'] = int(numbers[0])
107
+ elif 'TEMPO_ADJUST_RELATIVE' in intent:
108
+ # Determine direction
109
+ if 'up' in text_lower or 'increase' in text_lower or 'faster' in text_lower or 'sube' in text_lower or 'más' in text_lower:
110
+ params_json['direction'] = 'up'
111
+ elif 'down' in text_lower or 'decrease' in text_lower or 'slower' in text_lower or 'off' in text_lower or 'ease' in text_lower or 'baja' in text_lower or 'menos' in text_lower:
112
+ params_json['direction'] = 'down'
113
+
114
+ # Extract percentage if provided
115
+ if numbers:
116
+ params_json['bpm_change_percent'] = int(numbers[0])
117
+ else:
118
+ # Default percentage if not specified
119
+ params_json['bpm_change_percent'] = 10
120
+ elif 'TEMPO_FACTOR' in intent:
121
+ # Handle tempo multipliers
122
+ if 'double' in text_lower or 'twice' in text_lower or 'doble' in text_lower:
123
+ params_json['multiplier'] = 2.0
124
+ elif 'triple' in text_lower or 'three times' in text_lower:
125
+ params_json['multiplier'] = 3.0
126
+ elif 'half' in text_lower or 'mitad' in text_lower:
127
+ params_json['multiplier'] = 0.5
128
+ elif 'quarter' in text_lower or 'cuarto' in text_lower:
129
+ params_json['multiplier'] = 0.25
130
+ elif numbers:
131
+ # If a number is found, use it as multiplier (e.g., "4 times faster")
132
+ multiplier = float(numbers[0])
133
+ # Check if it's a fraction (e.g., "1/2 speed")
134
+ if '/' in text:
135
+ fraction_match = re.search(r'(\d+)/(\d+)', text)
136
+ if fraction_match:
137
+ numerator = float(fraction_match.group(1))
138
+ denominator = float(fraction_match.group(2))
139
+ multiplier = numerator / denominator
140
+ params_json['multiplier'] = multiplier
141
+ else:
142
+ # Default multiplier
143
+ params_json['multiplier'] = 1.0
144
+ elif 'TEMPO_STYLE_MARKING' in intent:
145
+ # Common tempo markings (Italian musical terms)
146
+ tempo_markings = {
147
+ 'grave': 'Grave',
148
+ 'largo': 'Largo',
149
+ 'lento': 'Lento',
150
+ 'adagio': 'Adagio',
151
+ 'andante': 'Andante',
152
+ 'moderato': 'Moderato',
153
+ 'allegretto': 'Allegretto',
154
+ 'allegro': 'Allegro',
155
+ 'vivace': 'Vivace',
156
+ 'presto': 'Presto',
157
+ 'prestissimo': 'Prestissimo'
158
+ }
159
+
160
+ # Find which tempo marking is in the text
161
+ for marking_lower, marking_proper in tempo_markings.items():
162
+ if marking_lower in text_lower:
163
+ params_json['style_marking'] = marking_proper
164
+ break
165
+
166
+ # If no specific marking found, default to empty
167
+ if 'style_marking' not in params_json:
168
+ params_json['style_marking'] = ''
169
+ elif 'TEMPO_GRADUAL' in intent:
170
+ # Determine direction (accelerando = speed up, ritardando/rallentando = slow down)
171
+ if any(word in text_lower for word in ['accelerando', 'accel', 'speed up', 'faster', 'acelerar', "crescendo"]):
172
+ params_json['direction'] = 'up'
173
+ elif any(word in text_lower for word in ['ritardando', 'rallentando', 'rit', 'rall', 'slow down', 'slower', 'ralentizar', "diminuendo"]):
174
+ params_json['direction'] = 'down'
175
+ else:
176
+ params_json['direction'] = 'down' # Default to slowing down
177
+
178
+ # Extract bpm_change_percent (default to 10 if not specified)
179
+ percent_match = re.search(r'(\d+\.?\d*)\s*(?:%|percent|por ciento)', text_lower)
180
+ if percent_match:
181
+ params_json['bpm_change_percent'] = float(percent_match.group(1))
182
+ else:
183
+ params_json['bpm_change_percent'] = 10
184
+
185
+ # Extract number of bars (look for "over X bars", "in X bars", etc.)
186
+ bars_match = re.search(r'(?:over|in|during|en|durante)\s*(\d+)\s*(?:bar|bars|measure|measures|compás|compases)', text_lower)
187
+ if bars_match:
188
+ params_json['bars'] = int(bars_match.group(1))
189
+ elif numbers:
190
+ # If no explicit "bars" keyword, use first number found
191
+ params_json['bars'] = int(numbers[0])
192
+ else:
193
+ params_json['bars'] = 4 # Default to 4 bars
194
+ elif 'LOOP_BARS' in intent or intent == 'LOOP':
195
+ # Look for bar range patterns like "19-21", "19 to 21", "19 through 21"
196
+ range_match = re.search(r'(?:bar|bars|compás|compases)?\s*(\d+)\s*(?:-|to|through|hasta|a)\s*(\d+)', text_lower)
197
+
198
+ if range_match:
199
+ params_json['start_bar'] = int(range_match.group(1))
200
+ params_json['end_bar'] = int(range_match.group(2))
201
+ elif numbers and len(numbers) >= 2:
202
+ # If two numbers found without explicit range pattern
203
+ params_json['start_bar'] = int(numbers[0])
204
+ params_json['end_bar'] = int(numbers[1])
205
+ else:
206
+ # No bar range specified
207
+ params_json['start_bar'] = None
208
+ params_json['end_bar'] = None
209
+ elif 'LOOP_SECTION' in intent:
210
+ # Extract bars if specified
211
+ bars_match = re.search(r'(?:for|during|en|durante)?\s*(\d+)\s*(?:bar|bars|measure|measures|compás|compases)', text_lower)
212
+
213
+ if bars_match:
214
+ params_json['bars'] = int(bars_match.group(1))
215
+ else:
216
+ params_json['bars'] = None
217
+
218
+ # Set start_bar and end_bar to null for section loops
219
+ params_json['start_bar'] = None
220
+ params_json['end_bar'] = None
221
+ elif 'JUMP_TO_BAR' in intent and numbers:
222
+ params_json['to_bar'] = int(float(numbers[0]))
223
+ elif 'JUMP_RELATIVE' in intent:
224
+ # Determine direction
225
+ if any(word in text_lower for word in ['ahead', 'forward', 'next', 'adelante', 'siguiente']):
226
+ params_json['direction'] = 'up'
227
+ elif any(word in text_lower for word in ['back', 'backward', 'previous', 'atrás', 'anterior']):
228
+ params_json['direction'] = 'down'
229
+ else:
230
+ params_json['direction'] = 'up' # Default to forward
231
+
232
+ # Extract number of bars
233
+ bars_match = re.search(r'(\d+\.?\d*)\s*(?:bar|bars|measure|measures|compás|compases)', text_lower)
234
+ if bars_match:
235
+ params_json['relative_bars'] = int(float(bars_match.group(1)))
236
+ elif numbers:
237
+ params_json['relative_bars'] = int(float(numbers[0]))
238
+ else:
239
+ params_json['relative_bars'] = 1 # Default to 1 bar
240
+ elif 'VOLUME' in intent and numbers:
241
+ params_json['vol'] = int(float(numbers[0]))
242
+
243
+ # Detect direction from keywords
244
+ if any(word in text_lower for word in ['up', 'increase', 'boost', 'raise', 'louder', 'sube', 'aumenta', 'más fuerte']):
245
+ params_json['direction'] = 'up'
246
+ elif any(word in text_lower for word in ['down', 'decrease', 'lower', 'reduce', 'quieter', 'baja', 'reduce', 'más bajo']):
247
+ params_json['direction'] = 'down'
248
+ else:
249
+ # If no explicit direction, default based on context or leave it out
250
+ params_json['direction'] = 'up' # Default assumption
251
+ elif 'MUTE_ALL' in intent:
252
+ # Detect if it's turning mute all on or off
253
+ if any(word in text_lower for word in ['unmute', 'activate', 'turn on', 'on', 'back', 'enable', 'activar', 'encender']):
254
+ params_json['mute_all'] = 'off'
255
+ else:
256
+ # Default to muting (turning on)
257
+ params_json['mute_all'] = 'on'
258
+
259
+ return target, locale, params_json
260
+
261
+ @app.route('/classify', methods=['POST'])
262
+ def classify():
263
+ try:
264
+ data = request.json
265
+ utterance = data.get('utterance', '')
266
+
267
+ if not utterance:
268
+ return jsonify({'error': 'No utterance provided'}), 400
269
+
270
+ # Tokenize input
271
+ inputs = tokenizer(utterance, return_tensors="pt", truncation=True, max_length=512)
272
+
273
+ # Run inference
274
+ with torch.no_grad():
275
+ outputs = model(**inputs)
276
+ logits = outputs.logits
277
+ predicted_class = torch.argmax(logits, dim=1).item()
278
+ confidence = torch.softmax(logits, dim=1)[0][predicted_class].item()
279
+
280
+ # Get intent label
281
+ intent_label = model.config.id2label[predicted_class]
282
+
283
+ # Fallback: Upgrade generic intents when specific targets are detected
284
+ text_lower = utterance.lower()
285
+
286
+ # Section keywords
287
+ sections = ['verse', 'chorus', 'bridge', 'outro', 'intro',
288
+ 'introducción', 'verso', 'estribillo', 'puente']
289
+
290
+ # Instrument keywords
291
+ instruments = ['drums', 'guitar', 'vocals','bass', 'piano', 'synth', 'strings',
292
+ 'batería', 'guitarra', 'bajo']
293
+
294
+ # Check for sections
295
+ has_section = any(re.search(r'\b' + re.escape(sec) + r'\b', text_lower) for sec in sections)
296
+
297
+ # Check for instruments
298
+ has_instrument = any(re.search(r'\b' + re.escape(inst) + r'\b', text_lower) for inst in instruments)
299
+
300
+ # Check for bar/measure navigation keywords
301
+ has_bar_navigation = any(word in text_lower for word in ['go to', 'move to', 'jump to', 'ir a', 'mover a', 'saltar a']) and \
302
+ any(word in text_lower for word in ['bar', 'measure', 'compás'])
303
+
304
+ # Check for volume keywords
305
+ has_volume = any(word in text_lower for word in ['volume', 'volumen', 'loud', 'quiet', 'louder', 'quieter', 'más fuerte', 'más bajo'])
306
+
307
+ # Upgrade intents based on detected entities
308
+ if intent_label == 'LOOP' and has_section:
309
+ intent_label = 'LOOP_SECTION'
310
+ elif intent_label == 'MUTE' and has_instrument:
311
+ intent_label = 'MUTE_INSTRUMENT'
312
+ elif intent_label == 'UNMUTE' and has_instrument:
313
+ intent_label = 'UNMUTE_INSTRUMENT'
314
+ elif intent_label == 'SOLO' and has_instrument:
315
+ intent_label = 'SOLO_INSTRUMENT'
316
+ elif intent_label in ['VOLUME_SET_ABSOLUTE', 'VOLUME_ADJUST_RELATIVE'] and has_bar_navigation:
317
+ intent_label = 'JUMP_TO_BAR'
318
+ elif intent_label == 'TEMPO_ADJUST_RELATIVE' and has_volume:
319
+ # If model predicts tempo but volume keyword is present, correct to volume
320
+ if has_instrument:
321
+ intent_label = 'VOLUME_ADJUST_RELATIVE'
322
+ else:
323
+ intent_label = 'VOLUME_ADJUST_RELATIVE'
324
+
325
+ # Extract entities and parameters
326
+ target, locale, params_json = extract_entities(utterance, intent_label)
327
+
328
+ # Build response
329
+ result = {
330
+ 'intent_label': intent_label,
331
+ 'locale': locale,
332
+ 'target': target,
333
+ 'params_json': params_json,
334
+ 'confidence': round(confidence * 100, 2),
335
+ 'model': 'DistilBERT'
336
+ }
337
+
338
+ return jsonify(result)
339
+
340
+ except Exception as e:
341
+ print(f"Error: {str(e)}")
342
+ return jsonify({
343
+ 'intent_label': 'UNKNOWN',
344
+ 'locale': 'en',
345
+ 'target': None,
346
+ 'params_json': {'error': str(e)},
347
+ 'model': 'DistilBERT'
348
+ }), 500
349
+
350
+ @app.route('/health', methods=['GET'])
351
+ def health():
352
+ return jsonify({'status': 'healthy', 'model': MODEL_NAME})
353
+
354
+ if __name__ == '__main__':
355
+ # Puerto adaptado para HF Spaces (default 7860). Debug off para producción.
356
+ app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)), debug=False)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ transformers
4
+ torch
5
+ huggingface_hub
6
+ python-dotenv