🎵 Initial commit: MusiaIA - AI Music Generator
✨ Features: - ALS file generator (creates Ableton Live projects) - ALS parser (reads and analyzes projects) - AI clients (GLM4.6 + Minimax M2) - Multiple music genres (House, Techno, Hip-Hop) - Complete documentation 🤖 Ready to generate music with AI!
This commit is contained in:
547
docs/api_chatbot.md
Normal file
547
docs/api_chatbot.md
Normal file
@@ -0,0 +1,547 @@
|
||||
# API & Chatbot - Documentación
|
||||
|
||||
## 🤖 Integración con IA (GLM4.6 & Minimax M2)
|
||||
|
||||
### Proveedores de IA
|
||||
|
||||
```python
|
||||
# ai_providers.py
|
||||
class GLM46Provider:
|
||||
"""Cliente para GLM4.6 API"""
|
||||
def __init__(self, api_key: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://open.bigmodel.cn/api/paas/v4"
|
||||
|
||||
def complete(self, prompt: str, **kwargs) -> str:
|
||||
response = requests.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
json={
|
||||
"model": "glm-4-plus",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
**kwargs
|
||||
}
|
||||
)
|
||||
return response.json()['choices'][0]['message']['content']
|
||||
|
||||
class MinimaxM2Provider:
|
||||
"""Cliente para Minimax M2 API"""
|
||||
def __init__(self, api_key: str):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.minimax.chat/v1"
|
||||
|
||||
def complete(self, prompt: str, **kwargs) -> str:
|
||||
# Implementar según documentación de Minimax
|
||||
pass
|
||||
|
||||
class AIOrchestrator:
|
||||
"""Orquestador que usa múltiples proveedores"""
|
||||
def __init__(self):
|
||||
self.providers = {
|
||||
'glm46': GLM46Provider(os.getenv('GLM46_API_KEY')),
|
||||
'minimax': MinimaxM2Provider(os.getenv('MINIMAX_API_KEY'))
|
||||
}
|
||||
|
||||
async def chat(self, message: str, context: list) -> str:
|
||||
# Determinar qué modelo usar
|
||||
model = self._select_model(message)
|
||||
|
||||
# Obtener respuesta
|
||||
provider = self.providers[model]
|
||||
return await provider.complete(message, context=context)
|
||||
|
||||
def _select_model(self, message: str) -> str:
|
||||
"""Selecciona el mejor modelo para la query"""
|
||||
# Lógica para elegir entre GLM4.6 y Minimax M2
|
||||
# Ejemplo: usar Minimax para conversación, GLM para análisis técnico
|
||||
if 'generar' in message.lower() or 'crear' in message.lower():
|
||||
return 'glm46' # Mejor para generación estructurada
|
||||
return 'minimax' # Mejor para conversación
|
||||
```
|
||||
|
||||
## 💬 Sistema de Chat
|
||||
|
||||
### WebSocket Handler (Real-time)
|
||||
|
||||
```python
|
||||
# chat_websocket.py
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
import json
|
||||
|
||||
class ChatManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket, user_id: str):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_message(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(json.dumps({
|
||||
"type": "message",
|
||||
"content": message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
async def broadcast_progress(self, progress: dict):
|
||||
"""Envía actualizaciones de progreso"""
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(json.dumps({
|
||||
"type": "progress",
|
||||
"data": progress
|
||||
}))
|
||||
|
||||
@router.websocket("/chat/{user_id}")
|
||||
async def chat_endpoint(websocket: WebSocket, user_id: str):
|
||||
chat_manager = ChatManager()
|
||||
await chat_manager.connect(websocket, user_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Recibir mensaje
|
||||
data = await websocket.receive_text()
|
||||
message_data = json.loads(data)
|
||||
|
||||
# Procesar mensaje
|
||||
processor = ChatProcessor(user_id)
|
||||
response = await processor.process_message(message_data['content'])
|
||||
|
||||
# Enviar respuesta
|
||||
await chat_manager.send_message(response, websocket)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
chat_manager.disconnect(websocket)
|
||||
```
|
||||
|
||||
### Procesador de Chat
|
||||
|
||||
```python
|
||||
# chat_processor.py
|
||||
class ChatProcessor:
|
||||
"""Procesa mensajes y coordina generación"""
|
||||
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
self.ai_orchestrator = AIOrchestrator()
|
||||
self.project_generator = ProjectGenerator()
|
||||
|
||||
async def process_message(self, message: str) -> str:
|
||||
# 1. Determinar intención
|
||||
intent = await self._analyze_intent(message)
|
||||
|
||||
# 2. Responder según intención
|
||||
if intent['type'] == 'generate_project':
|
||||
return await self._handle_generation(message, intent)
|
||||
elif intent['type'] == 'chat':
|
||||
return await self._handle_chat(message)
|
||||
elif intent['type'] == 'modify_project':
|
||||
return await self._handle_modification(message, intent)
|
||||
|
||||
async def _analyze_intent(self, message: str) -> dict:
|
||||
"""Analiza la intención del mensaje"""
|
||||
prompt = f"""
|
||||
Analiza este mensaje y determina la intención:
|
||||
"{message}"
|
||||
|
||||
Clasifica como:
|
||||
- generate_project: quiere crear un nuevo proyecto
|
||||
- modify_project: quiere modificar un proyecto existente
|
||||
- chat: conversación general
|
||||
|
||||
Responde en JSON: {{"type": "valor", "params": {{}}}}
|
||||
"""
|
||||
|
||||
response = await self.ai_orchestrator.chat(message, [])
|
||||
return json.loads(response)
|
||||
|
||||
async def _handle_generation(self, message: str, intent: dict):
|
||||
"""Maneja solicitud de generación de proyecto"""
|
||||
# Enviar mensaje inicial
|
||||
await self._send_progress("🎵 Analizando tu solicitud...")
|
||||
|
||||
# 2. Generar proyecto
|
||||
als_path = await self.project_generator.create_from_chat(
|
||||
user_id=self.user_id,
|
||||
requirements=intent['params']
|
||||
)
|
||||
|
||||
# 3. Responder con éxito
|
||||
return f"""
|
||||
✅ ¡Proyecto generado con éxito!
|
||||
|
||||
🎹 Proyecto: {os.path.basename(als_path)}
|
||||
📁 Ubicación: /projects/{self.user_id}/{als_path}
|
||||
|
||||
💡 Puedes abrir este archivo directamente en Ableton Live.
|
||||
"""
|
||||
```
|
||||
|
||||
## 🎼 Motor de Generación Musical
|
||||
|
||||
```python
|
||||
# project_generator.py
|
||||
class ProjectGenerator:
|
||||
"""Genera proyectos ALS basado en chat"""
|
||||
|
||||
def __init__(self):
|
||||
self.musical_ai = MusicalIntelligence()
|
||||
self.sample_db = SampleDatabase()
|
||||
self.als_generator = ALSGenerator()
|
||||
|
||||
async def create_from_chat(self, user_id: str, requirements: dict) -> str:
|
||||
"""Crea proyecto desde chat input"""
|
||||
|
||||
# 1. Analizar musicalmente
|
||||
await self._send_progress("🎼 Analizando estructura musical...")
|
||||
analysis = await self.musical_ai.analyze_requirements(requirements)
|
||||
|
||||
# 2. Seleccionar samples
|
||||
await self._send_progress("🥁 Seleccionando samples...")
|
||||
selected_samples = await self._select_samples_for_project(analysis)
|
||||
|
||||
# 3. Generar layout
|
||||
await self._send_progress("🎨 Diseñando layout...")
|
||||
layout = self._generate_track_layout(analysis, selected_samples)
|
||||
|
||||
# 4. Crear archivo ALS
|
||||
await self._send_progress("⚙️ Generando archivo ALS...")
|
||||
project_config = {
|
||||
'name': f"IA Project {datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
||||
'bpm': analysis['bpm'],
|
||||
'key': analysis['key'],
|
||||
'tracks': layout,
|
||||
'metadata': {
|
||||
'generated_by': 'MusiaIA',
|
||||
'style': analysis['style'],
|
||||
'mood': analysis['mood']
|
||||
}
|
||||
}
|
||||
|
||||
als_path = self.als_generator.create_project(project_config)
|
||||
|
||||
# 5. Guardar en historial
|
||||
await self._save_to_history(user_id, requirements, als_path)
|
||||
|
||||
return als_path
|
||||
|
||||
async def _select_samples_for_project(self, analysis: dict) -> dict:
|
||||
"""Selecciona samples automáticamente"""
|
||||
selected = {}
|
||||
|
||||
for track_type in ['drums', 'bass', 'leads', 'pads', 'fx']:
|
||||
if track_type in analysis.get('required_tracks', []):
|
||||
samples = self.sample_db.search({
|
||||
'type': track_type,
|
||||
'style': analysis['style'],
|
||||
'bpm_range': [analysis['bpm'] - 5, analysis['bpm'] + 5]
|
||||
})
|
||||
selected[track_type] = samples[:4] # Top 4 matches
|
||||
|
||||
return selected
|
||||
```
|
||||
|
||||
## 📡 API REST Endpoints
|
||||
|
||||
```python
|
||||
# api_endpoints.py
|
||||
from fastapi import FastAPI, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
router = FastAPI()
|
||||
|
||||
@router.post("/chat/message")
|
||||
async def send_message(request: ChatRequest):
|
||||
"""Envía mensaje al chatbot"""
|
||||
processor = ChatProcessor(request.user_id)
|
||||
response = await processor.process_message(request.message)
|
||||
return {"response": response}
|
||||
|
||||
@router.post("/projects/generate")
|
||||
async def generate_project(request: GenerationRequest):
|
||||
"""Genera nuevo proyecto ALS"""
|
||||
generator = ProjectGenerator()
|
||||
als_path = await generator.create_from_chat(
|
||||
user_id=request.user_id,
|
||||
requirements=request.requirements
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"project_path": als_path,
|
||||
"download_url": f"/projects/{request.user_id}/{os.path.basename(als_path)}"
|
||||
}
|
||||
|
||||
@router.get("/projects/{user_id}/{project_name}")
|
||||
async def download_project(user_id: str, project_name: str):
|
||||
"""Descarga proyecto generado"""
|
||||
project_path = f"/data/projects/{user_id}/{project_name}"
|
||||
return FileResponse(project_path, filename=project_name)
|
||||
|
||||
@router.get("/projects/{user_id}")
|
||||
async def list_projects(user_id: str):
|
||||
"""Lista proyectos del usuario"""
|
||||
projects = db.get_user_projects(user_id)
|
||||
return {"projects": projects}
|
||||
|
||||
@router.get("/samples")
|
||||
async def list_samples(filters: SampleFilters = None):
|
||||
"""Lista samples disponibles"""
|
||||
samples = sample_db.search(filters.dict() if filters else {})
|
||||
return {"samples": samples}
|
||||
|
||||
@router.post("/samples/upload")
|
||||
async def upload_sample(file: UploadFile = File(...)):
|
||||
"""Sube nuevo sample"""
|
||||
sample_id = sample_manager.upload(file)
|
||||
return {"sample_id": sample_id, "status": "uploaded"}
|
||||
|
||||
@router.get("/chat/history/{user_id}")
|
||||
async def get_chat_history(user_id: str, limit: int = 50):
|
||||
"""Obtiene historial de chat"""
|
||||
history = db.get_chat_history(user_id, limit=limit)
|
||||
return {"history": history}
|
||||
```
|
||||
|
||||
## 💾 Base de Datos (SQLAlchemy Models)
|
||||
|
||||
```python
|
||||
# models.py
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(50), unique=True)
|
||||
email = Column(String(100), unique=True)
|
||||
api_provider = Column(String(20)) # glm46 or minimax
|
||||
|
||||
projects = relationship("Project", back_populates="user")
|
||||
chat_history = relationship("ChatMessage", back_populates="user")
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = 'projects'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
name = Column(String(100))
|
||||
als_path = Column(String(255))
|
||||
style = Column(String(50))
|
||||
bpm = Column(Integer)
|
||||
key = Column(String(10))
|
||||
config = Column(JSON) # Project configuration
|
||||
|
||||
user = relationship("User", back_populates="projects")
|
||||
samples = relationship("ProjectSample", back_populates="project")
|
||||
|
||||
class ChatMessage(Base):
|
||||
__tablename__ = 'chat_messages'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
message = Column(String(1000))
|
||||
response = Column(String(1000))
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
user = relationship("User", back_populates="chat_history")
|
||||
|
||||
class Sample(Base):
|
||||
__tablename__ = 'samples'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100))
|
||||
type = Column(String(50)) # kick, snare, bass, etc
|
||||
file_path = Column(String(255))
|
||||
bpm = Column(Integer)
|
||||
key = Column(String(10))
|
||||
tags = Column(JSON)
|
||||
|
||||
class ProjectSample(Base):
|
||||
__tablename__ = 'project_samples'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
project_id = Column(Integer, ForeignKey('projects.id'))
|
||||
sample_id = Column(Integer, ForeignKey('samples.id'))
|
||||
track_name = Column(String(50))
|
||||
|
||||
project = relationship("Project", back_populates="samples")
|
||||
sample = relationship("Sample")
|
||||
```
|
||||
|
||||
## 🔐 Autenticación
|
||||
|
||||
```python
|
||||
# auth.py
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
import jwt
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
||||
user_id: int = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials"
|
||||
)
|
||||
user = db.get_user(user_id)
|
||||
return user
|
||||
except jwt.PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token"
|
||||
)
|
||||
|
||||
@router.post("/auth/login")
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
user = db.authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password"
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": user.id})
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
```
|
||||
|
||||
## 📊 Request/Response Models
|
||||
|
||||
```python
|
||||
# schemas.py
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
user_id: str
|
||||
message: str
|
||||
|
||||
class GenerationRequest(BaseModel):
|
||||
user_id: str
|
||||
requirements: Dict[str, Any]
|
||||
|
||||
class ProjectResponse(BaseModel):
|
||||
status: str
|
||||
project_path: str
|
||||
download_url: str
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
response: str
|
||||
timestamp: str
|
||||
|
||||
class SampleFilters(BaseModel):
|
||||
type: Optional[str] = None
|
||||
bpm_min: Optional[int] = None
|
||||
bpm_max: Optional[int] = None
|
||||
key: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
|
||||
class ProjectSummary(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
style: str
|
||||
bpm: int
|
||||
key: str
|
||||
created_at: str
|
||||
als_path: str
|
||||
```
|
||||
|
||||
## 🚀 Inicio del Servidor
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI(title="MusiaIA - AI Music Generator", version="1.0.0")
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Incluir routers
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
|
||||
# WebSocket
|
||||
app.websocket_route("/ws/chat/{user_id}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
```
|
||||
|
||||
## 🔄 Flujo de Ejemplo
|
||||
|
||||
```python
|
||||
# Ejemplo de flujo completo
|
||||
async def example_usage():
|
||||
# 1. Usuario envía mensaje
|
||||
user_message = "Genera un track de house a 124 BPM en La menor"
|
||||
|
||||
# 2. Chat API recibe mensaje
|
||||
chat_request = ChatRequest(user_id="user123", message=user_message)
|
||||
response = await send_message(chat_request)
|
||||
|
||||
# 3. IA analiza
|
||||
analysis = await musical_ai.analyze_requirements(user_message)
|
||||
# Returns: {'style': 'house', 'bpm': 124, 'key': 'Am', ...}
|
||||
|
||||
# 4. Genera proyecto
|
||||
als_path = await project_generator.create_from_chat(
|
||||
user_id="user123",
|
||||
requirements=analysis
|
||||
)
|
||||
|
||||
# 5. Retorna URL de descarga
|
||||
download_url = f"/projects/user123/{os.path.basename(als_path)}"
|
||||
|
||||
return {
|
||||
"response": "¡Proyecto generado!",
|
||||
"download_url": download_url
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Logging y Monitoreo
|
||||
|
||||
```python
|
||||
# logging_config.py
|
||||
import logging
|
||||
from pythonjsonlogger import jsonlogger
|
||||
|
||||
logHandler = logging.StreamHandler()
|
||||
formatter = jsonlogger.JsonFormatter()
|
||||
logHandler.setFormatter(formatter)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(logHandler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Uso
|
||||
logger.info("User generated project", extra={
|
||||
"user_id": user_id,
|
||||
"project_type": "house",
|
||||
"bpm": 124,
|
||||
"generation_time": generation_time
|
||||
})
|
||||
```
|
||||
177
docs/arquitectura.md
Normal file
177
docs/arquitectura.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Arquitectura del Sistema - MusiaIA
|
||||
|
||||
## 📋 Descripción General
|
||||
|
||||
Sistema de generación de música por IA que crea proyectos compatibles con Ableton Live (.als) a partir de conversaciones con un chatbot.
|
||||
|
||||
## 🏗️ Componentes Principales
|
||||
|
||||
### 1. **Frontend - Dashboard Web**
|
||||
- **Framework**: React/Next.js + TypeScript
|
||||
- **Características**:
|
||||
- Chat interfaz para interactuar con la IA
|
||||
- Visualización de proyectos generados
|
||||
- Gestión de samples y presets
|
||||
- Preview de información de proyectos
|
||||
- Sistema de descarga de archivos .als
|
||||
|
||||
### 2. **Backend - API Server**
|
||||
- **Framework**: Python (FastAPI) o Node.js (Express)
|
||||
- **Responsabilidades**:
|
||||
- Procesamiento de requests del chat
|
||||
- Integración con APIs de IA (GLM4.6, Minimax M2)
|
||||
- Generación de archivos ALS
|
||||
- Gestión de samples y proyectos
|
||||
- Base de datos de usuarios y proyectos
|
||||
|
||||
### 3. **Generador ALS (Core)**
|
||||
- **Lenguaje**: Python
|
||||
- **Funcionalidad**:
|
||||
- Parser XML para archivos ALS existentes
|
||||
- Generador de XML programático
|
||||
- Compresión gzip para crear archivos .als válidos
|
||||
- Validación de estructura ALS
|
||||
- Templates para diferentes estilos musicales
|
||||
|
||||
### 4. **Motor de IA Musical**
|
||||
- **Componentes**:
|
||||
- Analizador de requests del usuario
|
||||
- Generador de estructuras musicales (bpm, clave, estilo)
|
||||
- Selector de samples basado en criterios
|
||||
- Configurador de tracks y efectos
|
||||
- Orquestador de todos los elementos
|
||||
|
||||
### 5. **Gestión de Samples**
|
||||
- **Sistema**: Base de datos + Almacenamiento de archivos
|
||||
- **Características**:
|
||||
- Upload y procesamiento de samples
|
||||
- Tagging automático (kick, snare, bass, etc.)
|
||||
- Análisis de BPM y tonalidad
|
||||
- Búsqueda inteligente
|
||||
- Presets de samples
|
||||
|
||||
### 6. **Base de Datos**
|
||||
- **Tecnología**: PostgreSQL/MongoDB
|
||||
- **Esquemas**:
|
||||
- Usuarios y autenticación
|
||||
- Proyectos generados
|
||||
- Catálogo de samples
|
||||
- Historial de chats
|
||||
- Templates y presets
|
||||
|
||||
## 🔄 Flujo de Trabajo
|
||||
|
||||
```
|
||||
1. Usuario → Chat Interface (Dashboard)
|
||||
2. Chat Interface → Backend API
|
||||
3. Backend → GLM4.6/Minimax M2 (análisis de request)
|
||||
4. Backend → Motor IA Musical (generación estructura)
|
||||
5. Motor IA → Selector de samples
|
||||
6. Generador ALS → Crea XML → Comprime → Archivo .als
|
||||
7. Backend → Dashboard → Usuario descarga archivo
|
||||
```
|
||||
|
||||
## 📁 Estructura del Proyecto
|
||||
|
||||
```
|
||||
/
|
||||
├── als/ # Archivos ALS de ejemplo
|
||||
├── source/ # Samples organizados por tipo
|
||||
│ ├── kicks/
|
||||
│ ├── snares/
|
||||
│ ├── hats/
|
||||
│ ├── bass/
|
||||
│ ├── leads/
|
||||
│ ├── pads/
|
||||
│ ├── fx/
|
||||
│ └── vox/
|
||||
├── src/
|
||||
│ ├── backend/
|
||||
│ │ ├── api/ # Endpoints REST
|
||||
│ │ ├── core/ # Motor de generación
|
||||
│ │ ├── ai/ # Integración IA
|
||||
│ │ ├── als/ # Parser/Generador ALS
|
||||
│ │ ├── db/ # Models y schemas
|
||||
│ │ └── utils/
|
||||
│ └── dashboard/ # Frontend React
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ ├── hooks/
|
||||
│ └── services/
|
||||
├── tests/
|
||||
├── docs/
|
||||
└── docker/ # Configuración de contenedores
|
||||
```
|
||||
|
||||
## 🔧 Tecnologías Clave
|
||||
|
||||
### Backend
|
||||
- **Python 3.11+**
|
||||
- FastAPI (API framework)
|
||||
- lxml (XML parsing/generation)
|
||||
- pydantic (Data validation)
|
||||
- SQLAlchemy (ORM)
|
||||
- celery (async tasks)
|
||||
|
||||
### Frontend
|
||||
- **React/Next.js 14+**
|
||||
- **TypeScript**
|
||||
- **TailwindCSS**
|
||||
- **Socket.io** (real-time chat)
|
||||
- **Axios** (API client)
|
||||
|
||||
### Infra
|
||||
- **PostgreSQL** / MongoDB
|
||||
- **Redis** (caching y queues)
|
||||
- **Docker & Docker Compose**
|
||||
- **Nginx** (reverse proxy)
|
||||
|
||||
## 🎯 Funcionalidades Clave
|
||||
|
||||
### Chatbot IA
|
||||
- Conversación natural sobre música
|
||||
- Interpretación de requests musicales
|
||||
- Generación de prompts estructurados
|
||||
- Historial de conversaciones
|
||||
|
||||
### Generación Musical
|
||||
- Análisis de BPM y tonalidad
|
||||
- Selección inteligente de samples
|
||||
- Configuración de tracks automática
|
||||
- Aplicación de efectos y procesamiento
|
||||
|
||||
### Gestión de Samples
|
||||
- Auto-tagging basado en ML
|
||||
- Búsqueda por características musicales
|
||||
- Organización por categorías
|
||||
- Sistema de favorites
|
||||
|
||||
### Compatibilidad ALS
|
||||
- Estructura XML completa
|
||||
- Compresión gzip adecuada
|
||||
- Referencias a samples correctas
|
||||
- Metadatos válidos
|
||||
|
||||
## 🔐 Seguridad
|
||||
|
||||
- Autenticación JWT
|
||||
- Validación de inputs
|
||||
- Sanitización de XML
|
||||
- Rate limiting
|
||||
- CORS configurado
|
||||
|
||||
## 📊 Métricas y Monitoreo
|
||||
|
||||
- Logs estructurados
|
||||
- Métricas de uso
|
||||
- Performance monitoring
|
||||
- Error tracking (Sentry)
|
||||
- Health checks
|
||||
|
||||
## 🚀 Despliegue
|
||||
|
||||
- Contenerización con Docker
|
||||
- CI/CD con GitHub Actions
|
||||
- Ambiente de staging y producción
|
||||
- Backup automático de datos
|
||||
- Auto-scaling para负载
|
||||
442
docs/generador_als.md
Normal file
442
docs/generador_als.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# Generador ALS - Documentación Técnica
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
El generador ALS es el corazón del sistema. Su función es crear archivos .als válidos programáticamente parseando y generando XML compatible con Ableton Live.
|
||||
|
||||
## 📋 Estructura de Archivos ALS
|
||||
|
||||
### Descompresión
|
||||
```
|
||||
archivo.als (gzip) → XML → Modificación → gzip → nuevo.als
|
||||
```
|
||||
|
||||
### Estructura XML (Ableton Live 12.x)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Ableton MajorVersion="5" MinorVersion="12.0_12203" SchemaChangeCount="3" Creator="Ableton Live 12.2" Revision="...">
|
||||
<LiveSet>
|
||||
<!-- Metadatos del proyecto -->
|
||||
<NextPointeeId Value="..."/>
|
||||
<OverwriteProtectionNumber Value="..."/>
|
||||
|
||||
<!-- Tracks -->
|
||||
<Tracks>
|
||||
<!-- AudioTrack, MidiTrack, ReturnTrack, MasterTrack -->
|
||||
</Tracks>
|
||||
|
||||
<!-- Clips -->
|
||||
<Scene>
|
||||
<Scene/>
|
||||
</Scene>
|
||||
|
||||
<!-- Automation -->
|
||||
<AutomationEnvelopes/>
|
||||
|
||||
<!-- Devices y efectos -->
|
||||
<DevicesList/>
|
||||
</LiveSet>
|
||||
</Ableton>
|
||||
```
|
||||
|
||||
## 🛠️ Componentes del Generador
|
||||
|
||||
### 1. ALS Parser (`als_parser.py`)
|
||||
|
||||
```python
|
||||
class ALSParser:
|
||||
"""Parsea archivos ALS existentes"""
|
||||
|
||||
def __init__(self):
|
||||
self.tree = None
|
||||
self.root = None
|
||||
|
||||
def load_from_file(self, filepath: str):
|
||||
"""Carga archivo ALS (descomprime + parse XML)"""
|
||||
with gzip.open(filepath, 'rt', encoding='utf-8') as f:
|
||||
tree = et.parse(f)
|
||||
return tree
|
||||
|
||||
def parse_tracks(self):
|
||||
"""Extrae información de tracks"""
|
||||
tracks = []
|
||||
for track in self.root.findall('.//Tracks/*'):
|
||||
track_info = {
|
||||
'id': track.get('Id'),
|
||||
'type': track.tag,
|
||||
'name': self._get_track_name(track),
|
||||
'devices': self._get_devices(track),
|
||||
'clips': self._get_clips(track)
|
||||
}
|
||||
tracks.append(track_info)
|
||||
return tracks
|
||||
|
||||
def extract_samples_used(self):
|
||||
"""Lista samples referenciados en el proyecto"""
|
||||
samples = []
|
||||
for clip in self.root.findall('.//AudioClip'):
|
||||
file_ref = clip.find('.//FileRef')
|
||||
if file_ref is not None:
|
||||
samples.append(file_ref.get('FilePath'))
|
||||
return samples
|
||||
```
|
||||
|
||||
### 2. ALS Generator (`als_generator.py`)
|
||||
|
||||
```python
|
||||
class ALSGenerator:
|
||||
"""Genera archivos ALS desde cero"""
|
||||
|
||||
def __init__(self):
|
||||
self.builder = ALSBuilder()
|
||||
self.sample_manager = SampleManager()
|
||||
|
||||
def create_project(self, project_config: dict):
|
||||
"""
|
||||
Crea un proyecto ALS completo
|
||||
|
||||
Args:
|
||||
project_config: {
|
||||
'name': str,
|
||||
'bpm': int,
|
||||
'key': str,
|
||||
'tracks': [
|
||||
{
|
||||
'type': 'AudioTrack' | 'MidiTrack',
|
||||
'name': str,
|
||||
'samples': [list of sample paths],
|
||||
'effects': [list of effects],
|
||||
'automation': {...}
|
||||
}
|
||||
],
|
||||
'scenes': [...]
|
||||
}
|
||||
"""
|
||||
# 1. Crear estructura base
|
||||
root = self.builder.create_root()
|
||||
|
||||
# 2. Configurar LiveSet
|
||||
liveset = self.builder.create_liveset()
|
||||
|
||||
# 3. Crear tracks
|
||||
for track_config in project_config['tracks']:
|
||||
track = self._create_track(track_config)
|
||||
liveset.append(track)
|
||||
|
||||
# 4. Crear scenes
|
||||
self._create_scenes(liveset, project_config.get('scenes', []))
|
||||
|
||||
# 5. Agregar master track
|
||||
master = self._create_master_track()
|
||||
liveset.append(master)
|
||||
|
||||
root.append(liveset)
|
||||
|
||||
# 6. Serializar y comprimir
|
||||
return self._serialize_and_compress(root)
|
||||
|
||||
def _create_track(self, config: dict):
|
||||
"""Crea un track individual"""
|
||||
if config['type'] == 'AudioTrack':
|
||||
return self._create_audio_track(config)
|
||||
elif config['type'] == 'MidiTrack':
|
||||
return self._create_midi_track(config)
|
||||
|
||||
def _create_audio_track(self, config: dict):
|
||||
"""Crea un track de audio"""
|
||||
track = Element('AudioTrack')
|
||||
track.set('Id', str(self.builder.get_next_id()))
|
||||
|
||||
# Nombre del track
|
||||
name = SubElement(track, 'Name')
|
||||
SubElement(name, 'EffectiveName', Value=config['name'])
|
||||
SubElement(name, 'UserName', Value=config['name'])
|
||||
|
||||
# Color aleatorio
|
||||
SubElement(track, 'Color', Value=str(random.randint(0, 100)))
|
||||
|
||||
# Dispositivos (efectos, etc.)
|
||||
devices = SubElement(track, 'DevicesListWrapper')
|
||||
for effect in config.get('effects', []):
|
||||
device = self._create_effect(effect)
|
||||
devices.append(device)
|
||||
|
||||
# Clips (referencias a samples)
|
||||
clip_slots = SubElement(track, 'ClipSlotsListWrapper')
|
||||
for sample_path in config['samples']:
|
||||
clip_slot = self._create_clip_slot(sample_path)
|
||||
clip_slots.append(clip_slot)
|
||||
|
||||
return track
|
||||
```
|
||||
|
||||
### 3. ALS Builder (`als_builder.py`)
|
||||
|
||||
```python
|
||||
class ALSBuilder:
|
||||
"""Construye elementos XML válidos para ALS"""
|
||||
|
||||
def __init__(self):
|
||||
self.next_id = 1000
|
||||
|
||||
def create_root(self):
|
||||
"""Crea el elemento root <Ableton>"""
|
||||
root = Element('Ableton')
|
||||
root.set('MajorVersion', '5')
|
||||
root.set('MinorVersion', '12.0_12203')
|
||||
root.set('SchemaChangeCount', '3')
|
||||
root.set('Creator', 'Ableton Live 12.2')
|
||||
root.set('Revision', self._generate_revision())
|
||||
return root
|
||||
|
||||
def create_liveset(self):
|
||||
"""Crea el elemento <LiveSet>"""
|
||||
liveset = Element('LiveSet')
|
||||
SubElement(liveset, 'NextPointeeId', Value=str(self.next_id))
|
||||
SubElement(liveset, 'OverwriteProtectionNumber', Value='3074')
|
||||
|
||||
# Tracks container
|
||||
SubElement(liveset, 'Tracks')
|
||||
|
||||
# Scenes
|
||||
scenes = SubElement(liveset, 'Scenes')
|
||||
SubElement(scenes, 'Scene', Id=str(self._next_id()))
|
||||
|
||||
return liveset
|
||||
|
||||
def create_clip_slot(self, sample_path: str):
|
||||
"""Crea un ClipSlot con referencia a sample"""
|
||||
clip_slot = Element('AudioClipSlot')
|
||||
|
||||
# FileRef - referencia al archivo
|
||||
file_ref = SubElement(clip_slot, 'FileRef')
|
||||
file_ref.set('FilePath', sample_path)
|
||||
file_ref.set('RelativePath', 'true')
|
||||
|
||||
return clip_slot
|
||||
|
||||
def create_effect(self, effect_type: str):
|
||||
"""Crea un dispositivo/efecto"""
|
||||
devices_map = {
|
||||
'reverb': ReverbDevice,
|
||||
'delay': DelayDevice,
|
||||
'eq': EQDevice,
|
||||
'compressor': CompressorDevice,
|
||||
}
|
||||
|
||||
device_class = devices_map.get(effect_type, BasicDevice)
|
||||
return device_class().create_xml()
|
||||
```
|
||||
|
||||
### 4. Sample Manager (`sample_manager.py`)
|
||||
|
||||
```python
|
||||
class SampleManager:
|
||||
"""Gestiona la biblioteca de samples"""
|
||||
|
||||
def __init__(self, source_dir: str):
|
||||
self.source_dir = source_dir
|
||||
self.db = SampleDatabase()
|
||||
|
||||
def find_samples(self, criteria: dict):
|
||||
"""
|
||||
Encuentra samples basados en criterios
|
||||
|
||||
Args:
|
||||
criteria: {
|
||||
'type': 'kick' | 'snare' | 'bass' | etc,
|
||||
'bpm_range': [min, max],
|
||||
'key': 'C' | 'Am' | etc,
|
||||
'mood': 'dark' | 'bright' | etc,
|
||||
'count': int
|
||||
}
|
||||
"""
|
||||
return self.db.search(criteria)
|
||||
|
||||
def get_sample_path(self, sample_id: str):
|
||||
"""Retorna la ruta absoluta de un sample"""
|
||||
sample = self.db.get(sample_id)
|
||||
return os.path.join(self.source_dir, sample.type, sample.filename)
|
||||
```
|
||||
|
||||
## 🎵 Motor de Generación Musical
|
||||
|
||||
### Musical Intelligence (`musical_intelligence.py`)
|
||||
|
||||
```python
|
||||
class MusicalIntelligence:
|
||||
"""Analiza requests y genera estructuras musicales"""
|
||||
|
||||
def __init__(self, ai_client):
|
||||
self.ai = ai_client
|
||||
|
||||
def analyze_request(self, user_input: str) -> dict:
|
||||
"""
|
||||
Analiza el input del usuario y extrae parámetros musicales
|
||||
|
||||
Returns: {
|
||||
'style': 'house' | 'techno' | 'hip-hop' | etc,
|
||||
'bpm': int,
|
||||
'key': str,
|
||||
'mood': str,
|
||||
'instruments': [list],
|
||||
'structure': [verse, chorus, etc],
|
||||
'duration': int (beats)
|
||||
}
|
||||
"""
|
||||
prompt = f"""
|
||||
Analiza este request musical y extrae parámetros estructurados:
|
||||
"{user_input}"
|
||||
|
||||
Responde en formato JSON con:
|
||||
- style: género musical
|
||||
- bpm: tempo sugerido (80-140)
|
||||
- key: tonalidad
|
||||
- mood: estado de ánimo
|
||||
- instruments: lista de instrumentos
|
||||
- structure: estructura de la canción
|
||||
"""
|
||||
response = self.ai.complete(prompt)
|
||||
return json.loads(response)
|
||||
|
||||
def generate_track_layout(self, analysis: dict) -> list:
|
||||
"""
|
||||
Genera la disposición de tracks basada en el análisis
|
||||
"""
|
||||
tracks = []
|
||||
|
||||
# Track de drums (siempre)
|
||||
tracks.append({
|
||||
'type': 'AudioTrack',
|
||||
'name': 'Drums',
|
||||
'samples': self._select_samples('drums', analysis),
|
||||
'effects': ['compressor', 'eq']
|
||||
})
|
||||
|
||||
# Track de bass (según estilo)
|
||||
if analysis['style'] in ['house', 'techno', 'hip-hop']:
|
||||
tracks.append({
|
||||
'type': 'MidiTrack',
|
||||
'name': 'Bass',
|
||||
'midi_pattern': self._generate_bass_pattern(analysis),
|
||||
'effects': ['saturator', 'eq']
|
||||
})
|
||||
|
||||
# Tracks adicionales según instrumentos
|
||||
for instrument in analysis.get('instruments', []):
|
||||
tracks.append({
|
||||
'type': 'AudioTrack' if instrument == 'vocals' else 'MidiTrack',
|
||||
'name': instrument.title(),
|
||||
'samples': self._select_samples(instrument, analysis),
|
||||
})
|
||||
|
||||
return tracks
|
||||
```
|
||||
|
||||
## 🔄 Pipeline Completo
|
||||
|
||||
```python
|
||||
def generate_als_project(user_message: str, user_id: str) -> str:
|
||||
"""
|
||||
Pipeline completo de generación de proyecto ALS
|
||||
"""
|
||||
# 1. Analizar request con IA
|
||||
ai_client = AIClient() # GLM4.6 o Minimax M2
|
||||
musical_ai = MusicalIntelligence(ai_client)
|
||||
analysis = musical_ai.analyze_request(user_message)
|
||||
|
||||
# 2. Generar layout de tracks
|
||||
track_layout = musical_ai.generate_track_layout(analysis)
|
||||
|
||||
# 3. Configurar proyecto
|
||||
project_config = {
|
||||
'name': f"AI Project {datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
||||
'bpm': analysis['bpm'],
|
||||
'key': analysis['key'],
|
||||
'tracks': track_layout
|
||||
}
|
||||
|
||||
# 4. Generar archivo ALS
|
||||
generator = ALSGenerator()
|
||||
als_path = generator.create_project(project_config)
|
||||
|
||||
# 5. Guardar en DB
|
||||
db.save_project(user_id, project_config, als_path)
|
||||
|
||||
return als_path
|
||||
```
|
||||
|
||||
## ✅ Validación
|
||||
|
||||
```python
|
||||
def validate_als_file(filepath: str) -> bool:
|
||||
"""Valida que un archivo ALS sea válido"""
|
||||
try:
|
||||
# Intentar descomprimir
|
||||
with gzip.open(filepath, 'rt') as f:
|
||||
tree = et.parse(f)
|
||||
|
||||
# Validar estructura XML
|
||||
root = tree.getroot()
|
||||
if root.tag != 'Ableton':
|
||||
return False
|
||||
|
||||
# Verificar elementos requeridos
|
||||
if not root.find('.//LiveSet'):
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
## 📝 Ejemplo de Uso
|
||||
|
||||
```python
|
||||
# Crear un proyecto house básico
|
||||
config = {
|
||||
'name': 'My House Track',
|
||||
'bpm': 124,
|
||||
'key': 'Am',
|
||||
'tracks': [
|
||||
{
|
||||
'type': 'AudioTrack',
|
||||
'name': 'Drums',
|
||||
'samples': ['kicks/kick_001.wav', 'snares/snare_001.wav'],
|
||||
'effects': ['compressor', 'eq']
|
||||
},
|
||||
{
|
||||
'type': 'MidiTrack',
|
||||
'name': 'Bass',
|
||||
'midi_pattern': [60, 62, 64, 65],
|
||||
'effects': ['saturator']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
generator = ALSGenerator()
|
||||
als_file = generator.create_project(config)
|
||||
|
||||
# El archivo está listo para Ableton Live!
|
||||
print(f"Generated: {als_file}")
|
||||
```
|
||||
|
||||
## 🎯 Próximos Pasos
|
||||
|
||||
1. **Implementar parser completo** - Mapear todos los elementos XML
|
||||
2. **Crear templates** - Plantillas para diferentes géneros
|
||||
3. **Integrar IA musical** - Análisis y generación más sofisticada
|
||||
4. **Sistema de samples** - Base de datos y gestión automática
|
||||
5. **Validación robusta** - Verificación de integridad de archivos
|
||||
|
||||
## ⚠️ Consideraciones Técnicas
|
||||
|
||||
- **XML Encoding**: UTF-8 siempre
|
||||
- **Gzip compression**: nivel 9 (máxima compresión)
|
||||
- **File paths**: usar rutas relativas en FileRef
|
||||
- **IDs**: mantener secuencia única
|
||||
- **Version compatibility**: mayor versión 5 (Live 11+)
|
||||
- **Memory**: cargar samples lazily, no en memoria
|
||||
- **Threading**: generación asíncrona para múltiples requests
|
||||
Reference in New Issue
Block a user