File size: 15,509 Bytes
b04b4fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# bert-api/bert_api.py
# Origen: D:\XDev.Projects\MEHEARSAL-NLP-CONTROLLER-RMIT\mehearsal_v5_and_v6\bert-api\bert_api.py
# Adaptaciones mínimas para HF Spaces Docker: import os + MODEL_NAME desde env + puerto desde env + debug=False.
# Lógica de extract_entities() y upgrade de intents intacta respecto al fuente.
import os
from flask import Flask, request, jsonify
from flask_cors import CORS
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
from pathlib import Path
import re


# Get the project root (parent of bert-api folder)
PROJECT_ROOT = Path(__file__).resolve().parent.parent

app = Flask(__name__)
CORS(app)  # Allow requests from Next.js

# Load model and tokenizer from Hugging Face.
# MODEL_NAME es configurable por env var (default: v6). HF_TOKEN se lee automáticamente
# por huggingface_hub para autenticar el acceso al modelo privado.

MODEL_NAME = os.environ.get('BERT_MODEL_NAME', 'MuseSceneLab/mehearsal-nlp-v6')

print(f"Loading model from Hugging Face: {MODEL_NAME}...")

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)

model.eval()

print("Model loaded successfully!")


# Intent to target/params mapping (simplified - you can expand this)
INTENT_METADATA = {
    'MUTE_INSTRUMENT': {'requires_target': True, 'target_type': 'instrument'},
    'NOT_A_COMMAND': {'requires_target': False},
    'UNMUTE_INSTRUMENT': {'requires_target': True, 'target_type': 'instrument'},
    'SOLO_INSTRUMENT': {'requires_target': True, 'target_type': 'instrument'},
    'MUTE_ALL': {'requires_target': False},
    'VOLUME_ADJUST_RELATIVE': {'requires_target': True, 'target_type': 'instrument'},
    'VOLUME_SET_ABSOLUTE': {'requires_target': True, 'target_type': 'instrument'},
    'TEMPO_SET_ABSOLUTE': {'requires_target': False},
    'TEMPO_ADJUST_RELATIVE': {'requires_target': False},
    'TEMPO_GRADUAL': {'requires_target': False},
    'TEMPO_STYLE_MARKING': {'requires_target': False},
    'JUMP_TO_BAR': {'requires_target': False},
    'JUMP_TO_SECTION': {'requires_target': True, 'target_type': 'section'},
    'LOOP_BARS': {'requires_target': False},
    'LOOP_SECTION': {'requires_target': True, 'target_type': 'section'},
    'STOP_LOOP': {'requires_target': False},
}

def extract_entities(text, intent):
    """
    Simple entity extraction based on keywords.
    This is a basic implementation - you might want to enhance this.
    """

    text_lower = text.lower()

    # Instrument keywords
    instruments = ['drums', 'guitar', 'bass', 'piano', 'synth', 'vocals','strings',
                   'batería', 'guitarra', 'bajo']

    # Section keywords
    sections = ['verse', 'chorus', 'bridge', 'outro',
                'introducción', 'verso', 'estribillo', 'puente', 'intro']

    # Detect language
    locale = 'es' if any(word in text_lower for word in ['batería', 'guitarra', 'bajo']) else 'en'

    # Extract target
    target = None
    metadata = INTENT_METADATA.get(intent, {})

    if metadata.get('requires_target'):
        if metadata.get('target_type') == 'instrument':
            for inst in instruments:
                if inst in text_lower:
                    target = inst
                    break
        elif metadata.get('target_type') == 'section':
            # Use word boundary regex to avoid partial matches
            for sec in sections:
                # Match whole word only - use word boundaries
                if re.search(r'\b' + re.escape(sec) + r'\b', text_lower):
                    target = sec
                    break
                    # If no section found, check for instruments (for cases like "loop the guitar")
            if not target:
                for inst in instruments:
                    if re.search(r'\b' + re.escape(inst) + r'\b', text_lower):
                        target = inst
                        break

    # Extract parameters (basic implementation)
    params_json = {}

    # Extract numbers for tempo, volume, bars, etc.
    numbers = re.findall(r'\d+(?:\.\d+)?|\.\d+', text)

    if 'TEMPO_SET_ABSOLUTE' in intent and numbers:
        params_json['bpm'] = int(numbers[0])
    elif 'TEMPO_ADJUST_RELATIVE' in intent:
        # Determine direction
        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:
            params_json['direction'] = 'up'
        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:
            params_json['direction'] = 'down'

        # Extract percentage if provided
        if numbers:
            params_json['bpm_change_percent'] = int(numbers[0])
        else:
            # Default percentage if not specified
            params_json['bpm_change_percent'] = 10
    elif 'TEMPO_FACTOR' in intent:
        # Handle tempo multipliers
        if 'double' in text_lower or 'twice' in text_lower or 'doble' in text_lower:
            params_json['multiplier'] = 2.0
        elif 'triple' in text_lower or 'three times' in text_lower:
            params_json['multiplier'] = 3.0
        elif 'half' in text_lower or 'mitad' in text_lower:
            params_json['multiplier'] = 0.5
        elif 'quarter' in text_lower or 'cuarto' in text_lower:
            params_json['multiplier'] = 0.25
        elif numbers:
            # If a number is found, use it as multiplier (e.g., "4 times faster")
            multiplier = float(numbers[0])
            # Check if it's a fraction (e.g., "1/2 speed")
            if '/' in text:
                fraction_match = re.search(r'(\d+)/(\d+)', text)
                if fraction_match:
                    numerator = float(fraction_match.group(1))
                    denominator = float(fraction_match.group(2))
                    multiplier = numerator / denominator
            params_json['multiplier'] = multiplier
        else:
            # Default multiplier
            params_json['multiplier'] = 1.0
    elif 'TEMPO_STYLE_MARKING' in intent:
        # Common tempo markings (Italian musical terms)
        tempo_markings = {
            'grave': 'Grave',
            'largo': 'Largo',
            'lento': 'Lento',
            'adagio': 'Adagio',
            'andante': 'Andante',
            'moderato': 'Moderato',
            'allegretto': 'Allegretto',
            'allegro': 'Allegro',
            'vivace': 'Vivace',
            'presto': 'Presto',
            'prestissimo': 'Prestissimo'
        }

        # Find which tempo marking is in the text
        for marking_lower, marking_proper in tempo_markings.items():
            if marking_lower in text_lower:
                params_json['style_marking'] = marking_proper
                break

        # If no specific marking found, default to empty
        if 'style_marking' not in params_json:
            params_json['style_marking'] = ''
    elif 'TEMPO_GRADUAL' in intent:
        # Determine direction (accelerando = speed up, ritardando/rallentando = slow down)
        if any(word in text_lower for word in ['accelerando', 'accel', 'speed up', 'faster', 'acelerar', "crescendo"]):
            params_json['direction'] = 'up'
        elif any(word in text_lower for word in ['ritardando', 'rallentando', 'rit', 'rall', 'slow down', 'slower', 'ralentizar', "diminuendo"]):
            params_json['direction'] = 'down'
        else:
            params_json['direction'] = 'down'  # Default to slowing down

        # Extract bpm_change_percent (default to 10 if not specified)
        percent_match = re.search(r'(\d+\.?\d*)\s*(?:%|percent|por ciento)', text_lower)
        if percent_match:
            params_json['bpm_change_percent'] = float(percent_match.group(1))
        else:
            params_json['bpm_change_percent'] = 10

        # Extract number of bars (look for "over X bars", "in X bars", etc.)
        bars_match = re.search(r'(?:over|in|during|en|durante)\s*(\d+)\s*(?:bar|bars|measure|measures|compás|compases)', text_lower)
        if bars_match:
            params_json['bars'] = int(bars_match.group(1))
        elif numbers:
            # If no explicit "bars" keyword, use first number found
            params_json['bars'] = int(numbers[0])
        else:
            params_json['bars'] = 4  # Default to 4 bars
    elif 'LOOP_BARS' in intent or intent == 'LOOP':
        # Look for bar range patterns like "19-21", "19 to 21", "19 through 21"
        range_match = re.search(r'(?:bar|bars|compás|compases)?\s*(\d+)\s*(?:-|to|through|hasta|a)\s*(\d+)', text_lower)

        if range_match:
            params_json['start_bar'] = int(range_match.group(1))
            params_json['end_bar'] = int(range_match.group(2))
        elif numbers and len(numbers) >= 2:
            # If two numbers found without explicit range pattern
            params_json['start_bar'] = int(numbers[0])
            params_json['end_bar'] = int(numbers[1])
        else:
            # No bar range specified
            params_json['start_bar'] = None
            params_json['end_bar'] = None
    elif 'LOOP_SECTION' in intent:
        # Extract bars if specified
        bars_match = re.search(r'(?:for|during|en|durante)?\s*(\d+)\s*(?:bar|bars|measure|measures|compás|compases)', text_lower)

        if bars_match:
            params_json['bars'] = int(bars_match.group(1))
        else:
            params_json['bars'] = None

        # Set start_bar and end_bar to null for section loops
        params_json['start_bar'] = None
        params_json['end_bar'] = None
    elif 'JUMP_TO_BAR' in intent and numbers:
        params_json['to_bar'] = int(float(numbers[0]))
    elif 'JUMP_RELATIVE' in intent:
        # Determine direction
        if any(word in text_lower for word in ['ahead', 'forward', 'next', 'adelante', 'siguiente']):
            params_json['direction'] = 'up'
        elif any(word in text_lower for word in ['back', 'backward', 'previous', 'atrás', 'anterior']):
            params_json['direction'] = 'down'
        else:
            params_json['direction'] = 'up'  # Default to forward

        # Extract number of bars
        bars_match = re.search(r'(\d+\.?\d*)\s*(?:bar|bars|measure|measures|compás|compases)', text_lower)
        if bars_match:
            params_json['relative_bars'] = int(float(bars_match.group(1)))
        elif numbers:
            params_json['relative_bars'] = int(float(numbers[0]))
        else:
            params_json['relative_bars'] = 1  # Default to 1 bar
    elif 'VOLUME' in intent and numbers:
        params_json['vol'] = int(float(numbers[0]))

        # Detect direction from keywords
        if any(word in text_lower for word in ['up', 'increase', 'boost', 'raise', 'louder', 'sube', 'aumenta', 'más fuerte']):
            params_json['direction'] = 'up'
        elif any(word in text_lower for word in ['down', 'decrease', 'lower', 'reduce', 'quieter', 'baja', 'reduce', 'más bajo']):
            params_json['direction'] = 'down'
        else:
            # If no explicit direction, default based on context or leave it out
            params_json['direction'] = 'up'  # Default assumption
    elif 'MUTE_ALL' in intent:
        # Detect if it's turning mute all on or off
        if any(word in text_lower for word in ['unmute', 'activate', 'turn on', 'on', 'back', 'enable', 'activar', 'encender']):
            params_json['mute_all'] = 'off'
        else:
            # Default to muting (turning on)
            params_json['mute_all'] = 'on'

    return target, locale, params_json

@app.route('/classify', methods=['POST'])
def classify():
    try:
        data = request.json
        utterance = data.get('utterance', '')

        if not utterance:
            return jsonify({'error': 'No utterance provided'}), 400

        # Tokenize input
        inputs = tokenizer(utterance, return_tensors="pt", truncation=True, max_length=512)

        # Run inference
        with torch.no_grad():
            outputs = model(**inputs)
            logits = outputs.logits
            predicted_class = torch.argmax(logits, dim=1).item()
            confidence = torch.softmax(logits, dim=1)[0][predicted_class].item()

        # Get intent label
        intent_label = model.config.id2label[predicted_class]

        # Fallback: Upgrade generic intents when specific targets are detected
        text_lower = utterance.lower()

        # Section keywords
        sections = ['verse', 'chorus', 'bridge', 'outro', 'intro',
                    'introducción', 'verso', 'estribillo', 'puente']

        # Instrument keywords
        instruments = ['drums', 'guitar', 'vocals','bass', 'piano', 'synth', 'strings',
                    'batería', 'guitarra', 'bajo']

        # Check for sections
        has_section = any(re.search(r'\b' + re.escape(sec) + r'\b', text_lower) for sec in sections)

        # Check for instruments
        has_instrument = any(re.search(r'\b' + re.escape(inst) + r'\b', text_lower) for inst in instruments)

        # Check for bar/measure navigation keywords
        has_bar_navigation = any(word in text_lower for word in ['go to', 'move to', 'jump to', 'ir a', 'mover a', 'saltar a']) and \
                            any(word in text_lower for word in ['bar', 'measure', 'compás'])

        # Check for volume keywords
        has_volume = any(word in text_lower for word in ['volume', 'volumen', 'loud', 'quiet', 'louder', 'quieter', 'más fuerte', 'más bajo'])

        # Upgrade intents based on detected entities
        if intent_label == 'LOOP' and has_section:
            intent_label = 'LOOP_SECTION'
        elif intent_label == 'MUTE' and has_instrument:
            intent_label = 'MUTE_INSTRUMENT'
        elif intent_label == 'UNMUTE' and has_instrument:
            intent_label = 'UNMUTE_INSTRUMENT'
        elif intent_label == 'SOLO' and has_instrument:
            intent_label = 'SOLO_INSTRUMENT'
        elif intent_label in ['VOLUME_SET_ABSOLUTE', 'VOLUME_ADJUST_RELATIVE'] and has_bar_navigation:
            intent_label = 'JUMP_TO_BAR'
        elif intent_label == 'TEMPO_ADJUST_RELATIVE' and has_volume:
            # If model predicts tempo but volume keyword is present, correct to volume
            if has_instrument:
                intent_label = 'VOLUME_ADJUST_RELATIVE'
            else:
                intent_label = 'VOLUME_ADJUST_RELATIVE'

        # Extract entities and parameters
        target, locale, params_json = extract_entities(utterance, intent_label)

        # Build response
        result = {
            'intent_label': intent_label,
            'locale': locale,
            'target': target,
            'params_json': params_json,
            'confidence': round(confidence * 100, 2),
            'model': 'DistilBERT'
        }

        return jsonify(result)

    except Exception as e:
        print(f"Error: {str(e)}")
        return jsonify({
            'intent_label': 'UNKNOWN',
            'locale': 'en',
            'target': None,
            'params_json': {'error': str(e)},
            'model': 'DistilBERT'
        }), 500

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'healthy', 'model': MODEL_NAME})

if __name__ == '__main__':
    # Puerto adaptado para HF Spaces (default 7860). Debug off para producción.
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)), debug=False)