| |
| |
| |
| |
| |
| |
| 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 |
|
|
|
|
| |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent |
|
|
| app = Flask(__name__) |
| CORS(app) |
|
|
| |
| |
| |
|
|
| MODEL_NAME = os.environ.get('BERT_MODEL_NAME', 'MuseSceneLab/mehearsal-nlp-v7-part6') |
|
|
| 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_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}, |
| 'LOAD_SONG': {'requires_target': True, 'target_type': 'song'}, |
| 'SEARCH_SONG': {'requires_target': True, 'target_type': 'song'}, |
| 'CREATE_PROJECT': {'requires_target': False}, |
| 'SAVE_PROJECT': {'requires_target': False}, |
| 'CLOSE_PROJECT': {'requires_target': False}, |
| 'ADD_MUSICIAN': {'requires_target': True, 'target_type': 'musician'}, |
| 'REMOVE_MUSICIAN': {'requires_target': True, 'target_type': 'musician'}, |
| 'SELECT_MUSICIAN': {'requires_target': True, 'target_type': 'musician'}, |
| 'ASSIGN_PART': {'requires_target': True, 'target_type': 'musician'}, |
| 'SHOW_MUSICIAN': {'requires_target': True, 'target_type': 'musician'}, |
| 'OPEN_VIEW': {'requires_target': True, 'target_type': 'view'}, |
| 'GO_BACK': {'requires_target': False}, |
| 'GO_HOME': {'requires_target': False}, |
| 'OPEN_SETTINGS': {'requires_target': False}, |
| 'OPEN_HELP': {'requires_target': False}, |
| 'ZOOM_IN': {'requires_target': False}, |
| 'ZOOM_OUT': {'requires_target': False}, |
| 'SHOW_LYRICS': {'requires_target': False}, |
| 'HIDE_LYRICS': {'requires_target': False}, |
| 'SHOW_SCORE': {'requires_target': False}, |
| 'HIDE_SCORE': {'requires_target': False}, |
| 'PLAYBACK_PLAY': {'requires_target': False}, |
| 'PLAYBACK_PAUSE': {'requires_target': False}, |
| 'PLAYBACK_STOP': {'requires_target': False}, |
| 'PLAYBACK_RESTART': {'requires_target': False}, |
| 'PLAYBACK_NEXT': {'requires_target': False}, |
| 'WAKE_WORD_ENABLE': {'requires_target': False}, |
| 'SLEEP_MODE': {'requires_target': False}, |
| 'UNDO_ACTION': {'requires_target': False}, |
| 'REDO_ACTION': {'requires_target': False}, |
| 'CANCEL_ACTION': {'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() |
|
|
| |
| instruments = ['drums', 'guitar', 'bass', 'piano', 'synth', 'vocals','strings', |
| 'batería', 'guitarra', 'bajo'] |
|
|
| |
| sections = ['verse', 'chorus', 'bridge', 'outro', |
| 'introducción', 'verso', 'estribillo', 'puente', 'intro'] |
|
|
| musicians = ['drummer', 'guitarist', 'bassist', 'singer', 'pianist', |
| 'baterista', 'guitarrista', 'bajista', 'cantante', 'pianista'] |
|
|
| views = { |
| 'settings': ['settings', 'preferences', 'configuration', 'ajustes', 'preferencias', 'configuracion'], |
| 'mixer': ['mixer', 'mix', 'console', 'mezclador'], |
| 'score': ['score', 'sheet music', 'notation', 'partitura', 'notacion'], |
| 'lyrics': ['lyrics', 'words', 'letra'], |
| 'timeline': ['timeline', 'arrangement', 'linea de tiempo'], |
| 'dashboard': ['dashboard', 'home', 'inicio', 'tablero'], |
| 'practice view': ['practice view', 'vista de practica'] |
| } |
|
|
| |
| locale = 'es' if any(word in text_lower for word in ['batería', 'guitarra', 'bajo']) else 'en' |
|
|
| |
| 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': |
| |
| for sec in sections: |
| |
| if re.search(r'\b' + re.escape(sec) + r'\b', text_lower): |
| target = sec |
| break |
| |
| if not target: |
| for inst in instruments: |
| if re.search(r'\b' + re.escape(inst) + r'\b', text_lower): |
| target = inst |
| break |
| elif metadata.get('target_type') == 'musician': |
| for musician in musicians: |
| if re.search(r'\b' + re.escape(musician) + r'\b', text_lower): |
| target = musician |
| break |
| elif metadata.get('target_type') == 'view': |
| for view_name, aliases in views.items(): |
| if any(re.search(r'\b' + re.escape(alias) + r'\b', text_lower) for alias in aliases): |
| target = view_name |
| break |
| elif metadata.get('target_type') == 'song': |
| prefixes = [ |
| 'load', 'open', 'pull up', 'start', 'bring up', 'search for', 'find', 'look up', |
| 'carga', 'abre', 'pon', 'muestra', 'trae', 'busca', 'encuentra', 'localiza' |
| ] |
| target = text_lower |
| for prefix in prefixes: |
| if target.startswith(prefix + ' '): |
| target = target[len(prefix):].strip() |
| break |
|
|
| |
| params_json = {} |
|
|
| |
| 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: |
| |
| 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' |
|
|
| |
| if numbers: |
| params_json['bpm_change_percent'] = int(numbers[0]) |
| else: |
| |
| params_json['bpm_change_percent'] = 10 |
| elif 'TEMPO_FACTOR' in intent: |
| |
| 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: |
| |
| multiplier = float(numbers[0]) |
| |
| 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: |
| |
| params_json['multiplier'] = 1.0 |
| elif 'TEMPO_STYLE_MARKING' in intent: |
| |
| tempo_markings = { |
| 'grave': 'Grave', |
| 'largo': 'Largo', |
| 'lento': 'Lento', |
| 'adagio': 'Adagio', |
| 'andante': 'Andante', |
| 'moderato': 'Moderato', |
| 'allegretto': 'Allegretto', |
| 'allegro': 'Allegro', |
| 'vivace': 'Vivace', |
| 'presto': 'Presto', |
| 'prestissimo': 'Prestissimo' |
| } |
|
|
| |
| for marking_lower, marking_proper in tempo_markings.items(): |
| if marking_lower in text_lower: |
| params_json['style_marking'] = marking_proper |
| break |
|
|
| |
| if 'style_marking' not in params_json: |
| params_json['style_marking'] = '' |
| elif 'TEMPO_GRADUAL' in intent: |
| |
| 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' |
|
|
| |
| 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 |
|
|
| |
| 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: |
| |
| params_json['bars'] = int(numbers[0]) |
| else: |
| params_json['bars'] = 4 |
| elif 'LOOP_BARS' in intent or intent == 'LOOP': |
| |
| 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: |
| |
| params_json['start_bar'] = int(numbers[0]) |
| params_json['end_bar'] = int(numbers[1]) |
| else: |
| |
| params_json['start_bar'] = None |
| params_json['end_bar'] = None |
| elif 'LOOP_SECTION' in intent: |
| |
| 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 |
|
|
| |
| 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: |
| |
| 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' |
|
|
| |
| 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 |
| elif 'VOLUME' in intent and numbers: |
| params_json['vol'] = int(float(numbers[0])) |
|
|
| |
| 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: |
| |
| params_json['direction'] = 'up' |
| elif 'MUTE_ALL' in intent: |
| |
| if any(word in text_lower for word in ['unmute', 'activate', 'turn on', 'on', 'back', 'enable', 'activar', 'encender']): |
| params_json['mute_all'] = 'off' |
| else: |
| |
| params_json['mute_all'] = 'on' |
| elif intent in ['LOAD_SONG', 'SEARCH_SONG'] and target: |
| params_json['song' if intent == 'LOAD_SONG' else 'query'] = target |
| elif intent in ['ADD_MUSICIAN', 'REMOVE_MUSICIAN', 'SELECT_MUSICIAN', 'ASSIGN_PART', 'SHOW_MUSICIAN'] and target: |
| params_json['musician'] = target |
| elif intent == 'OPEN_VIEW' and target: |
| params_json['view'] = target |
|
|
| 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 |
|
|
| |
| inputs = tokenizer(utterance, return_tensors="pt", truncation=True, max_length=512) |
|
|
| |
| 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() |
|
|
| |
| intent_label = model.config.id2label[predicted_class] |
|
|
| |
| text_lower = utterance.lower() |
|
|
| |
| sections = ['verse', 'chorus', 'bridge', 'outro', 'intro', |
| 'introducción', 'verso', 'estribillo', 'puente'] |
|
|
| |
| instruments = ['drums', 'guitar', 'vocals','bass', 'piano', 'synth', 'strings', |
| 'batería', 'guitarra', 'bajo'] |
|
|
| |
| has_section = any(re.search(r'\b' + re.escape(sec) + r'\b', text_lower) for sec in sections) |
|
|
| |
| has_instrument = any(re.search(r'\b' + re.escape(inst) + r'\b', text_lower) for inst in instruments) |
|
|
| |
| 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']) |
|
|
| |
| has_volume = any(word in text_lower for word in ['volume', 'volumen', 'loud', 'quiet', 'louder', 'quieter', 'más fuerte', 'más bajo']) |
|
|
| |
| 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 has_instrument: |
| intent_label = 'VOLUME_ADJUST_RELATIVE' |
| else: |
| intent_label = 'VOLUME_ADJUST_RELATIVE' |
|
|
| |
| target, locale, params_json = extract_entities(utterance, intent_label) |
|
|
| |
| 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__': |
| |
| app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)), debug=False) |
|
|