JoseMSL's picture
Upload 5 files
d4eb85b verified
Raw
History Blame Contribute Delete
19.4 kB
# bert-api/bert_api.py
# Origen: D:\XDev.Projects\MEHEARSAL-NLP-CONTROLLER-RMIT (rama origin/part_6)
# archivo 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.
# Modelo HF asociado: MuseSceneLab/mehearsal-nlp-v7-part6 (46 intents: 16 musicales + 30 UI/UX).
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: v7-part6). 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-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 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},
'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()
# 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']
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']
}
# 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
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
# 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'
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
# 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)