feat: fix frontend and activate imports
This commit is contained in:
@@ -36,9 +36,6 @@ MYSQL_DATABASE_URL=mysql://db_user:db_password@localhost:3306/external_database_
|
|||||||
EXTERNAL_TABLE_GRADES=notas
|
EXTERNAL_TABLE_GRADES=notas
|
||||||
EXTERNAL_ID_TIPO_NOTA=1
|
EXTERNAL_ID_TIPO_NOTA=1
|
||||||
|
|
||||||
# Bark TTS API (Text-to-Speech for questions)
|
|
||||||
BARK_API_URL=http://t-800:8443
|
|
||||||
|
|
||||||
# Branding Defaults
|
# Branding Defaults
|
||||||
DEFAULT_ORG_NAME="Norteamericano"
|
DEFAULT_ORG_NAME="Norteamericano"
|
||||||
DEFAULT_PLATFORM_NAME="Norteamericano Learning"
|
DEFAULT_PLATFORM_NAME="Norteamericano Learning"
|
||||||
|
|||||||
@@ -290,10 +290,6 @@ if user_usage > DAILY_LIMIT {
|
|||||||
- Gráficos de tendencia
|
- Gráficos de tendencia
|
||||||
- Comparativa mes a mes
|
- Comparativa mes a mes
|
||||||
|
|
||||||
4. **Integrar con Bark**
|
|
||||||
- Track tokens de audio generation
|
|
||||||
- Costos específicos de TTS
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Implementación: 100% Completa** 🎉
|
**Implementación: 100% Completa** 🎉
|
||||||
|
|||||||
+10
-62
@@ -1,4 +1,4 @@
|
|||||||
# 🚀 Resumen de Implementación - Question Bank con Audio
|
# 🚀 Resumen de Implementación - Question Bank
|
||||||
|
|
||||||
## ✅ Estado de la Implementación
|
## ✅ Estado de la Implementación
|
||||||
|
|
||||||
@@ -6,21 +6,18 @@
|
|||||||
- ✅ Migración de base de datos con `skill_assessed`
|
- ✅ Migración de base de datos con `skill_assessed`
|
||||||
- ✅ Endpoints CRUD para Question Bank
|
- ✅ Endpoints CRUD para Question Bank
|
||||||
- ✅ Importación desde MySQL
|
- ✅ Importación desde MySQL
|
||||||
- ✅ Generación de audio con Bark
|
|
||||||
- ✅ RAG con verificación de 4 habilidades
|
- ✅ RAG con verificación de 4 habilidades
|
||||||
- ✅ Compilación exitosa (8 warnings menores)
|
- ✅ Compilación exitosa
|
||||||
|
|
||||||
### Frontend (TypeScript/React) - COMPLETO
|
### Frontend (TypeScript/React) - COMPLETO
|
||||||
- ✅ Página `/question-bank` con dashboard
|
- ✅ Página `/question-bank` con dashboard
|
||||||
- ✅ Componente QuestionBankCard con badge de skills
|
- ✅ Componente QuestionBankCard con badge de skills
|
||||||
- ✅ QuestionBankEditor con generación IA de skills
|
- ✅ QuestionBankEditor con generación IA de skills
|
||||||
- ✅ MySQLImportModal
|
- ✅ MySQLImportModal
|
||||||
- ✅ AudioGeneratorModal
|
|
||||||
- ✅ Navegación actualizada con link
|
- ✅ Navegación actualizada con link
|
||||||
- ✅ TypeScript: 3 errores menores (admin, no críticos)
|
- ✅ TypeScript: 3 errores menores (admin, no críticos)
|
||||||
|
|
||||||
### Infraestructura - LISTO PARA DESPLEGAR
|
### Infraestructura - LISTO
|
||||||
- ✅ Scripts de instalación de Bark
|
|
||||||
- ✅ install.sh actualizado con detección dev/prod
|
- ✅ install.sh actualizado con detección dev/prod
|
||||||
- ✅ Documentación completa
|
- ✅ Documentación completa
|
||||||
|
|
||||||
@@ -43,52 +40,19 @@ web/studio/src/app/question-bank/page.tsx (NUEVO)
|
|||||||
web/studio/src/components/QuestionBank/QuestionBankCard.tsx (NUEVO)
|
web/studio/src/components/QuestionBank/QuestionBankCard.tsx (NUEVO)
|
||||||
web/studio/src/components/QuestionBank/QuestionBankEditor.tsx (NUEVO)
|
web/studio/src/components/QuestionBank/QuestionBankEditor.tsx (NUEVO)
|
||||||
web/studio/src/components/QuestionBank/MySQLImportModal.tsx (NUEVO)
|
web/studio/src/components/QuestionBank/MySQLImportModal.tsx (NUEVO)
|
||||||
web/studio/src/components/QuestionBank/AudioGeneratorModal.tsx (NUEVO)
|
|
||||||
web/studio/src/components/Navbar.tsx (link agregado)
|
web/studio/src/components/Navbar.tsx (link agregado)
|
||||||
web/studio/src/lib/api.ts (API client)
|
web/studio/src/lib/api.ts (API client)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scripts & Docs
|
### Scripts & Docs
|
||||||
```
|
```
|
||||||
scripts/install_bark_tts.sh (NUEVO)
|
|
||||||
scripts/deploy_to_t800.sh (NUEVO)
|
|
||||||
docs/BARK_TTS_GUIDE.md (NUEVO)
|
|
||||||
docs/QUESTION_BANK_UI.md (NUEVO)
|
docs/QUESTION_BANK_UI.md (NUEVO)
|
||||||
docs/BARK_MANUAL_INSTALL.md (NUEVO)
|
docs/EXCEL_IMPORT_TEMPLATE.md
|
||||||
install.sh (actualizado con Bark)
|
install.sh (actualizado)
|
||||||
.env.example (BARK_API_URL agregado)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Instalación de Bark en t-800
|
|
||||||
|
|
||||||
### Opción Automática (Recomendada)
|
|
||||||
```bash
|
|
||||||
cd /home/juan/dev/openccb
|
|
||||||
./scripts/deploy_to_t800.sh
|
|
||||||
# Ingresar contraseña: apoca11
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opción Manual
|
|
||||||
```bash
|
|
||||||
# Copiar script
|
|
||||||
scp scripts/install_bark_tts.sh juan@t-800:/tmp/
|
|
||||||
|
|
||||||
# Conectarse
|
|
||||||
ssh juan@t-800
|
|
||||||
|
|
||||||
# Ejecutar
|
|
||||||
sudo /tmp/install_bark_tts.sh
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ver documentación completa en:** `docs/BARK_MANUAL_INSTALL.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Características de 4 Habilidades
|
## 🎯 Características de 4 Habilidades
|
||||||
|
|
||||||
### Implementación
|
### Implementación
|
||||||
@@ -123,14 +87,12 @@ curl http://localhost:8000/health
|
|||||||
|
|
||||||
**Desarrollo:**
|
**Desarrollo:**
|
||||||
```bash
|
```bash
|
||||||
BARK_API_URL=http://t-800:8000
|
|
||||||
OLLAMA_URL=http://t-800:11434
|
OLLAMA_URL=http://t-800:11434
|
||||||
WHISPER_URL=http://t-800:9000
|
WHISPER_URL=http://t-800:9000
|
||||||
```
|
```
|
||||||
|
|
||||||
**Producción:**
|
**Producción:**
|
||||||
```bash
|
```bash
|
||||||
BARK_API_URL=http://t-800.norteamericano.cl:8000
|
|
||||||
OLLAMA_URL=http://t-800.norteamericano.cl:11434
|
OLLAMA_URL=http://t-800.norteamericano.cl:11434
|
||||||
WHISPER_URL=http://t-800.norteamericano.cl:9000
|
WHISPER_URL=http://t-800.norteamericano.cl:9000
|
||||||
```
|
```
|
||||||
@@ -147,8 +109,8 @@ GET /question-bank/{id} # Obtener pregunta
|
|||||||
PUT /question-bank/{id} # Actualizar pregunta
|
PUT /question-bank/{id} # Actualizar pregunta
|
||||||
DELETE /question-bank/{id} # Eliminar pregunta
|
DELETE /question-bank/{id} # Eliminar pregunta
|
||||||
POST /question-bank/import-mysql # Importar desde MySQL
|
POST /question-bank/import-mysql # Importar desde MySQL
|
||||||
POST /question-bank/{id}/generate-audio # Generar audio Bark
|
|
||||||
GET /question-bank/mysql-courses # Listar cursos MySQL
|
GET /question-bank/mysql-courses # Listar cursos MySQL
|
||||||
|
POST /question-bank/import-mysql-all # Importar todo desde MySQL
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Templates (actualizado)
|
### Test Templates (actualizado)
|
||||||
@@ -175,12 +137,6 @@ npm run type-check
|
|||||||
# ⚠️ 3 errores menores en admin (no afectan Question Bank)
|
# ⚠️ 3 errores menores en admin (no afectan Question Bank)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Bark (después de instalar)
|
|
||||||
```bash
|
|
||||||
curl http://t-800:8000/health
|
|
||||||
# Expected: {"status":"healthy","service":"bark-tts"}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎨 UI Features
|
## 🎨 UI Features
|
||||||
@@ -201,25 +157,20 @@ curl http://t-800:8000/health
|
|||||||
- 10 tipos de preguntas
|
- 10 tipos de preguntas
|
||||||
- Generación IA con skills
|
- Generación IA con skills
|
||||||
- Tags automáticos
|
- Tags automáticos
|
||||||
- Audio generation checkbox
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Próximos Pasos (Opcionales)
|
## 📝 Próximos Pasos (Opcionales)
|
||||||
|
|
||||||
1. **Desplegar Bark en t-800**
|
1. **Filtrar errores de admin** (no críticos)
|
||||||
- Ejecutar `./scripts/deploy_to_t800.sh`
|
|
||||||
- O seguir `docs/BARK_MANUAL_INSTALL.md`
|
|
||||||
|
|
||||||
2. **Filtrar errores de admin** (no críticos)
|
|
||||||
- `getOrganizations` no existe
|
- `getOrganizations` no existe
|
||||||
- `BrandingContext` type error
|
- `BrandingContext` type error
|
||||||
|
|
||||||
3. **Integración con Test Templates**
|
2. **Integración con Test Templates**
|
||||||
- Selector de preguntas desde banco
|
- Selector de preguntas desde banco
|
||||||
- Bulk selection
|
- Bulk selection
|
||||||
|
|
||||||
4. **Analytics de Skills**
|
3. **Analytics de Skills**
|
||||||
- Dashboard de distribución de skills
|
- Dashboard de distribución de skills
|
||||||
- Reportes por habilidad
|
- Reportes por habilidad
|
||||||
|
|
||||||
@@ -231,10 +182,9 @@ curl http://t-800:8000/health
|
|||||||
|------------|--------|-------|
|
|------------|--------|-------|
|
||||||
| Backend Question Bank | ✅ 100% | Compila exitosamente |
|
| Backend Question Bank | ✅ 100% | Compila exitosamente |
|
||||||
| Frontend Question Bank | ✅ 95% | UI completa, 3 errores admin menores |
|
| Frontend Question Bank | ✅ 95% | UI completa, 3 errores admin menores |
|
||||||
| Bark Scripts | ✅ 100% | Listos para desplegar |
|
|
||||||
| install.sh | ✅ 100% | Detecta dev/prod automáticamente |
|
| install.sh | ✅ 100% | Detecta dev/prod automáticamente |
|
||||||
| Skills Verification | ✅ 100% | Implementado en IA y BD |
|
| Skills Verification | ✅ 100% | Implementado en IA y BD |
|
||||||
| Documentación | ✅ 100% | 4 archivos docs completos |
|
| Documentación | ✅ 100% | Archivos docs completos |
|
||||||
|
|
||||||
**Progreso Total: 98%** 🎉
|
**Progreso Total: 98%** 🎉
|
||||||
|
|
||||||
@@ -242,7 +192,5 @@ curl http://t-800:8000/health
|
|||||||
|
|
||||||
## 📞 Soporte
|
## 📞 Soporte
|
||||||
|
|
||||||
- **Bark Installation**: `docs/BARK_MANUAL_INSTALL.md`
|
|
||||||
- **UI Usage**: `docs/QUESTION_BANK_UI.md`
|
- **UI Usage**: `docs/QUESTION_BANK_UI.md`
|
||||||
- **Bark API**: `docs/BARK_TTS_GUIDE.md`
|
|
||||||
- **General**: `README.md` del proyecto
|
- **General**: `README.md` del proyecto
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
# Instalación Manual de Bark TTS en t-800
|
|
||||||
|
|
||||||
## Opción A: Instalación Automática (Recomendada)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Copiar script a t-800
|
|
||||||
scp scripts/install_bark_tts.sh juan@t-800:/tmp/install_bark_tts.sh
|
|
||||||
|
|
||||||
# 2. Conectarse a t-800
|
|
||||||
ssh juan@t-800
|
|
||||||
|
|
||||||
# 3. Ejecutar instalación
|
|
||||||
chmod +x /tmp/install_bark_tts.sh
|
|
||||||
sudo /tmp/install_bark_tts.sh
|
|
||||||
|
|
||||||
# 4. Verificar instalación
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
## Opción B: Instalación Paso a Paso
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Conectarse a t-800
|
|
||||||
ssh juan@t-800
|
|
||||||
# Contraseña: apoca11
|
|
||||||
|
|
||||||
# Actualizar sistema
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y python3 python3-pip python3-venv git ffmpeg curl
|
|
||||||
|
|
||||||
# Crear directorio
|
|
||||||
sudo mkdir -p /opt/bark
|
|
||||||
sudo chown juan:juan /opt/bark
|
|
||||||
cd /opt/bark
|
|
||||||
|
|
||||||
# Clonar Bark
|
|
||||||
git clone https://github.com/suno-ai/bark.git
|
|
||||||
cd bark
|
|
||||||
|
|
||||||
# Crear entorno virtual
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Instalar dependencias
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -e .
|
|
||||||
pip install fastapi uvicorn[standard] python-multipart numpy scipy
|
|
||||||
|
|
||||||
# Crear archivo de API
|
|
||||||
cat > bark_api.py << 'PYEOF'
|
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from bark import SAMPLE_RATE, generate_audio, preload_models
|
|
||||||
from scipy.io.wavfile import write as write_wav
|
|
||||||
import numpy as np
|
|
||||||
import io
|
|
||||||
|
|
||||||
app = FastAPI(title="Bark TTS API", version="1.0.0")
|
|
||||||
|
|
||||||
print("Preloading Bark models...")
|
|
||||||
preload_models()
|
|
||||||
print("Models loaded!")
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
return {"status": "healthy", "service": "bark-tts"}
|
|
||||||
|
|
||||||
@app.get("/api/voices")
|
|
||||||
async def list_voices():
|
|
||||||
return {
|
|
||||||
"voices": [
|
|
||||||
{"id": "v2/en_speaker_0", "name": "English Speaker 0", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_1", "name": "English Speaker 1", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_6", "name": "English Speaker 6", "language": "en"},
|
|
||||||
{"id": "v2/es_speaker_0", "name": "Spanish Speaker 0", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_1", "name": "Spanish Speaker 1", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_3", "name": "Spanish Speaker 3", "language": "es"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.post("/api/generate")
|
|
||||||
async def generate_speech(
|
|
||||||
text: str = Query(..., min_length=1, max_length=500),
|
|
||||||
voice: str = Query(default="v2/en_speaker_1"),
|
|
||||||
speed: float = Query(default=1.0, ge=0.5, le=2.0),
|
|
||||||
output_format: str = Query(default="wav", regex="^(mp3|wav|ogg)$")
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
audio_array = generate_audio(text, history_prompt=voice)
|
|
||||||
|
|
||||||
if speed != 1.0:
|
|
||||||
new_length = int(len(audio_array) / speed)
|
|
||||||
audio_array = audio_array[:new_length]
|
|
||||||
|
|
||||||
audio_buffer = io.BytesIO()
|
|
||||||
write_wav(audio_buffer, SAMPLE_RATE, audio_array)
|
|
||||||
audio_buffer.seek(0)
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
audio_buffer,
|
|
||||||
media_type="audio/wav",
|
|
||||||
headers={"Content-Disposition": f"attachment; filename=speech.wav"}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
# Crear servicio systemd
|
|
||||||
cat > /tmp/bark-tts.service << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Bark TTS API Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=juan
|
|
||||||
Group=juan
|
|
||||||
WorkingDirectory=/opt/bark/bark
|
|
||||||
Environment="PATH=/opt/bark/bark/venv/bin"
|
|
||||||
ExecStart=/opt/bark/bark/venv/bin/uvicorn bark_api:app --host 0.0.0.0 --port 8000 --workers 1
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
MemoryMax=4G
|
|
||||||
MemoryHigh=3G
|
|
||||||
CPUQuota=80%
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo mv /tmp/bark-tts.service /etc/systemd/system/
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable bark-tts
|
|
||||||
sudo systemctl start bark-tts
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
sudo systemctl status bark-tts
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prueba de Funcionamiento
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test básico
|
|
||||||
curl "http://localhost:8000/api/generate?text=Hello%20World&voice=v2/en_speaker_1" -o test.wav
|
|
||||||
|
|
||||||
# Test desde OpenCCB
|
|
||||||
curl http://t-800:8000/health
|
|
||||||
|
|
||||||
# Ver logs
|
|
||||||
sudo journalctl -u bark-tts -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuración en OpenCCB
|
|
||||||
|
|
||||||
Agregar a `.env`:
|
|
||||||
```bash
|
|
||||||
BARK_API_URL=http://t-800:8000
|
|
||||||
# O para producción:
|
|
||||||
# BARK_API_URL=http://t-800.norteamericano.cl:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
## Solución de Problemas
|
|
||||||
|
|
||||||
### Error: "Out of Memory"
|
|
||||||
```bash
|
|
||||||
# Reducir límite de memoria en systemd
|
|
||||||
sudo systemctl edit bark-tts
|
|
||||||
# Agregar:
|
|
||||||
# [Service]
|
|
||||||
# MemoryMax=2G
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error: "Model not found"
|
|
||||||
```bash
|
|
||||||
# Reinstalar modelos
|
|
||||||
cd /opt/bark/bark
|
|
||||||
source venv/bin/activate
|
|
||||||
python -c "from bark import preload_models; preload_models()"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Servicio no inicia
|
|
||||||
```bash
|
|
||||||
# Ver logs
|
|
||||||
sudo journalctl -u bark-tts -n 50
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
sudo systemctl restart bark-tts
|
|
||||||
```
|
|
||||||
|
|
||||||
## URLs de Acceso
|
|
||||||
|
|
||||||
- **Health**: http://t-800:8000/health
|
|
||||||
- **Voices**: http://t-800:8000/api/voices
|
|
||||||
- **Generate**: http://t-800:8000/api/generate?text=Hello&voice=v2/en_speaker_1
|
|
||||||
|
|
||||||
## Producción (t-800.norteamericano.cl)
|
|
||||||
|
|
||||||
Para producción, asegurar que:
|
|
||||||
1. El puerto 8000 esté abierto en el firewall
|
|
||||||
2. El dominio t-800.norteamericano.cl apunte a la IP correcta
|
|
||||||
3. Usar BARK_API_URL=http://t-800.norteamericano.cl:8000
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
# Bark TTS Integration Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
OpenCCB now integrates with **Suno AI's Bark** text-to-speech system for generating audio versions of questions. This allows students to listen to questions instead of just reading them, improving accessibility and supporting different learning styles.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────┐ HTTP ┌─────────────────┐
|
|
||||||
│ OpenCCB CMS │ ────────────> │ Bark TTS API │
|
|
||||||
│ (PostgreSQL) │ <──────────── │ (Server t-800)│
|
|
||||||
│ │ Audio │ │
|
|
||||||
└─────────────────┘ └─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment to t-800 Server
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- SSH access to t-800 server
|
|
||||||
- At least 8GB RAM recommended (Bark loads large models)
|
|
||||||
- 10GB free disk space
|
|
||||||
- Python 3.8+
|
|
||||||
- GPU optional (CUDA support for faster generation)
|
|
||||||
|
|
||||||
### Quick Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From your local machine
|
|
||||||
cd /home/juan/dev/openccb
|
|
||||||
./scripts/deploy_to_t800.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will:
|
|
||||||
1. SSH into t-800
|
|
||||||
2. Install Python dependencies
|
|
||||||
3. Clone Bark repository
|
|
||||||
4. Set up systemd service
|
|
||||||
5. Start the API server
|
|
||||||
|
|
||||||
### Manual Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SSH into t-800
|
|
||||||
ssh juan@t-800
|
|
||||||
|
|
||||||
# Run installation script
|
|
||||||
wget https://raw.githubusercontent.com/suno-ai/bark/main/scripts/install.sh
|
|
||||||
sudo bash install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
Once deployed, Bark API is available at `http://t-800:8000`
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
```bash
|
|
||||||
curl http://t-800:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### List Available Voices
|
|
||||||
```bash
|
|
||||||
curl http://t-800:8000/api/voices
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generate Speech
|
|
||||||
```bash
|
|
||||||
# Basic usage
|
|
||||||
curl "http://t-800:8000/api/generate?text=What%20color%20is%20the%20sky%3F" \
|
|
||||||
-o question.wav
|
|
||||||
|
|
||||||
# With specific voice and speed
|
|
||||||
curl "http://t-800:8000/api/generate?text=Hello%20World&voice=v2/en_speaker_6&speed=1.2" \
|
|
||||||
-o greeting.wav
|
|
||||||
|
|
||||||
# Spanish voice
|
|
||||||
curl "http://t-800:8000/api/generate?text=Hola%20mundo&voice=v2/es_speaker_0" \
|
|
||||||
-o saludo.wav
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Voices
|
|
||||||
|
|
||||||
### English Voices
|
|
||||||
- `v2/en_speaker_0` through `v2/en_speaker_9`
|
|
||||||
|
|
||||||
### Spanish Voices
|
|
||||||
- `v2/es_speaker_0` through `v2/es_speaker_9`
|
|
||||||
|
|
||||||
## Integration with OpenCCB
|
|
||||||
|
|
||||||
### Generate Audio for a Question
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Via API
|
|
||||||
curl -X POST "http://localhost:3001/question-bank/{question_id}/generate-audio" \
|
|
||||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"text": "What color is the sky?",
|
|
||||||
"voice": "v2/en_speaker_1",
|
|
||||||
"speed": 1.0
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automatic Audio Generation
|
|
||||||
|
|
||||||
When creating a question:
|
|
||||||
|
|
||||||
```json
|
|
||||||
POST /question-bank
|
|
||||||
{
|
|
||||||
"question_text": "What is the capital of France?",
|
|
||||||
"question_type": "multiple-choice",
|
|
||||||
"options": ["Paris", "London", "Berlin", "Madrid"],
|
|
||||||
"correct_answer": 0,
|
|
||||||
"explanation": "Paris is the capital of France.",
|
|
||||||
"generate_audio": true // Triggers async audio generation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Add to your `.env` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Bark TTS API URL
|
|
||||||
BARK_API_URL=http://t-800:8000
|
|
||||||
|
|
||||||
# Optional: Default voice for audio generation
|
|
||||||
BARK_DEFAULT_VOICE=v2/en_speaker_1
|
|
||||||
|
|
||||||
# Optional: Default speed
|
|
||||||
BARK_DEFAULT_SPEED=1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Optimization
|
|
||||||
|
|
||||||
### Model Preloading
|
|
||||||
|
|
||||||
Bark preloads models on startup (takes ~30 seconds). The systemd service handles this automatically.
|
|
||||||
|
|
||||||
### Memory Management
|
|
||||||
|
|
||||||
The systemd service includes memory limits:
|
|
||||||
```ini
|
|
||||||
MemoryMax=4G
|
|
||||||
MemoryHigh=3G
|
|
||||||
```
|
|
||||||
|
|
||||||
Adjust based on your server's capacity.
|
|
||||||
|
|
||||||
### Batch Generation
|
|
||||||
|
|
||||||
For importing many questions:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate audio for multiple questions
|
|
||||||
curl "http://t-800:8000/api/generate/batch?texts=Question%201&texts=Question%202&voice=v2/en_speaker_1"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Service Not Starting
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check status
|
|
||||||
sudo systemctl status bark-tts
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
sudo journalctl -u bark-tts -f
|
|
||||||
|
|
||||||
# Restart service
|
|
||||||
sudo systemctl restart bark-tts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Out of Memory
|
|
||||||
|
|
||||||
If Bark crashes due to memory:
|
|
||||||
1. Reduce `MemoryMax` in systemd service
|
|
||||||
2. Use smaller models: `suno/bark-small`
|
|
||||||
3. Process questions one at a time
|
|
||||||
|
|
||||||
### Slow Generation
|
|
||||||
|
|
||||||
- GPU acceleration: Install CUDA-enabled PyTorch
|
|
||||||
- Reduce audio quality settings
|
|
||||||
- Use shorter text segments
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test English voice
|
|
||||||
curl "http://t-800:8000/api/generate?text=The%20quick%20brown%20fox&voice=v2/en_speaker_1" | play -
|
|
||||||
|
|
||||||
# Test Spanish voice
|
|
||||||
curl "http://t-800:8000/api/generate?text=El%20rápido%20zorro%20marrón&voice=v2/es_speaker_0" | play -
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
- Bark API runs on internal network only
|
|
||||||
- No authentication required (assumes trusted network)
|
|
||||||
- Rate limiting handled by OpenCCB
|
|
||||||
- Audio files stored in `uploads/audio/` directory
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- [ ] Add authentication to Bark API
|
|
||||||
- [ ] Support for custom voice cloning
|
|
||||||
- [ ] Audio preprocessing (noise reduction, normalization)
|
|
||||||
- [ ] Caching layer for repeated requests
|
|
||||||
- [ ] WebSocket support for streaming audio
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Bark GitHub](https://github.com/suno-ai/bark)
|
|
||||||
- [Bark Hugging Face](https://huggingface.co/suno/bark)
|
|
||||||
- [OpenCCB Question Bank Documentation](../docs/question-bank.md)
|
|
||||||
@@ -101,40 +101,6 @@ Modal para importar preguntas desde MySQL:
|
|||||||
- Progreso de importación
|
- Progreso de importación
|
||||||
- Resultado (éxito/error)
|
- Resultado (éxito/error)
|
||||||
|
|
||||||
### 5. AudioGeneratorModal (`AudioGeneratorModal.tsx`)
|
|
||||||
|
|
||||||
Modal para generar audio con Bark TTS:
|
|
||||||
|
|
||||||
**Configuración:**
|
|
||||||
- Vista previa del texto de la pregunta
|
|
||||||
- Texto personalizable (opcional)
|
|
||||||
- Selector de voz (6 opciones: 3 inglés, 3 español)
|
|
||||||
- Control de velocidad (0.5x - 2.0x)
|
|
||||||
|
|
||||||
**Voces disponibles:**
|
|
||||||
```
|
|
||||||
Inglés:
|
|
||||||
- v2/en_speaker_0 (English Speaker 0)
|
|
||||||
- v2/en_speaker_1 (English Speaker 1) ← default
|
|
||||||
- v2/en_speaker_6 (English Speaker 6)
|
|
||||||
|
|
||||||
Español:
|
|
||||||
- v2/es_speaker_0 (Spanish Speaker 0)
|
|
||||||
- v2/es_speaker_1 (Spanish Speaker 1)
|
|
||||||
- v2/es_speaker_3 (Spanish Speaker 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Estados:**
|
|
||||||
- ⏳ Generando... (polling cada 1s, max 30s)
|
|
||||||
- ✅ Audio generado (con preview play/pause)
|
|
||||||
- ❌ Error (mensaje descriptivo)
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Polling automático para verificar estado
|
|
||||||
- Reproductor de audio integrado
|
|
||||||
- Botón Play/Pause
|
|
||||||
- Indicador visual de estado
|
|
||||||
|
|
||||||
## Flujos de Usuario
|
## Flujos de Usuario
|
||||||
|
|
||||||
### Crear Pregunta Manualmente
|
### Crear Pregunta Manualmente
|
||||||
@@ -288,11 +254,6 @@ GET /question-bank/mysql-courses
|
|||||||
- Verificar que `MYSQL_DATABASE_URL` esté configurado en `.env`
|
- Verificar que `MYSQL_DATABASE_URL` esté configurado en `.env`
|
||||||
- Verificar conectividad al servidor MySQL
|
- Verificar conectividad al servidor MySQL
|
||||||
|
|
||||||
**Error: "Error al generar audio"**
|
|
||||||
- Verificar que Bark TTS esté corriendo en t-800
|
|
||||||
- Verificar que `BARK_API_URL` esté configurado
|
|
||||||
- Revisar logs de Bark: `ssh juan@t-800 && journalctl -u bark-tts -f`
|
|
||||||
|
|
||||||
**Audio no se reproduce**
|
**Audio no se reproduce**
|
||||||
- Verificar formato de audio (WAV soportado)
|
- Verificar formato de audio (WAV soportado)
|
||||||
- Verificar permisos del navegador
|
- Verificar permisos del navegador
|
||||||
@@ -301,5 +262,4 @@ GET /question-bank/mysql-courses
|
|||||||
## Referencias
|
## Referencias
|
||||||
|
|
||||||
- [Question Bank Backend](../../services/cms-service/src/handlers_question_bank.rs)
|
- [Question Bank Backend](../../services/cms-service/src/handlers_question_bank.rs)
|
||||||
- [Bark TTS Guide](../../docs/BARK_TTS_GUIDE.md)
|
|
||||||
- [Test Templates UI](./TestTemplates/)
|
- [Test Templates UI](./TestTemplates/)
|
||||||
|
|||||||
@@ -120,11 +120,9 @@ echo "🔍 Configurando Servicios de IA Remota ($ENV_CHOICE)..."
|
|||||||
if [ "$ENV_CHOICE" == "dev" ]; then
|
if [ "$ENV_CHOICE" == "dev" ]; then
|
||||||
DEFAULT_OLLAMA="http://t-800:11434"
|
DEFAULT_OLLAMA="http://t-800:11434"
|
||||||
DEFAULT_WHISPER="http://t-800:9000"
|
DEFAULT_WHISPER="http://t-800:9000"
|
||||||
DEFAULT_BARK="http://t-800:8000"
|
|
||||||
else
|
else
|
||||||
DEFAULT_OLLAMA="http://t-800.norteamericano.cl:11434"
|
DEFAULT_OLLAMA="http://t-800.norteamericano.cl:11434"
|
||||||
DEFAULT_WHISPER="http://t-800.norteamericano.cl:9000"
|
DEFAULT_WHISPER="http://t-800.norteamericano.cl:9000"
|
||||||
DEFAULT_BARK="http://t-800.norteamericano.cl:8000"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -p "Ingrese la URL de Ollama Remoto [$DEFAULT_OLLAMA]: " REMOTE_OLLAMA_URL
|
read -p "Ingrese la URL de Ollama Remoto [$DEFAULT_OLLAMA]: " REMOTE_OLLAMA_URL
|
||||||
@@ -133,27 +131,22 @@ read -p "Ingrese la URL de Whisper Remoto [$DEFAULT_WHISPER]: " REMOTE_WHISPER_U
|
|||||||
REMOTE_WHISPER_URL=${REMOTE_WHISPER_URL:-$DEFAULT_WHISPER}
|
REMOTE_WHISPER_URL=${REMOTE_WHISPER_URL:-$DEFAULT_WHISPER}
|
||||||
read -p "Ingrese la URL del Image Bridge Remoto [http://t-800:8080]: " REMOTE_IMAGE_URL
|
read -p "Ingrese la URL del Image Bridge Remoto [http://t-800:8080]: " REMOTE_IMAGE_URL
|
||||||
REMOTE_IMAGE_URL=${REMOTE_IMAGE_URL:-"http://t-800:8080"}
|
REMOTE_IMAGE_URL=${REMOTE_IMAGE_URL:-"http://t-800:8080"}
|
||||||
read -p "Ingrese la URL de Bark TTS Remoto [$DEFAULT_BARK]: " REMOTE_BARK_URL
|
|
||||||
REMOTE_BARK_URL=${REMOTE_BARK_URL:-$DEFAULT_BARK}
|
|
||||||
read -p "Ingrese el nombre del Modelo (en el servidor remoto) [llama3.2:3b]: " LLM_MODEL
|
read -p "Ingrese el nombre del Modelo (en el servidor remoto) [llama3.2:3b]: " LLM_MODEL
|
||||||
LLM_MODEL=${LLM_MODEL:-llama3.2:3b}
|
LLM_MODEL=${LLM_MODEL:-llama3.2:3b}
|
||||||
|
|
||||||
update_env "AI_PROVIDER" "local"
|
update_env "AI_PROVIDER" "local"
|
||||||
update_env "LOCAL_LLM_MODEL" "$LLM_MODEL"
|
update_env "LOCAL_LLM_MODEL" "$LLM_MODEL"
|
||||||
update_env "LOCAL_VIDEO_BRIDGE_URL" "$REMOTE_IMAGE_URL"
|
update_env "LOCAL_VIDEO_BRIDGE_URL" "$REMOTE_IMAGE_URL"
|
||||||
update_env "BARK_API_URL" "$REMOTE_BARK_URL"
|
|
||||||
|
|
||||||
if [ "$ENV_CHOICE" == "dev" ]; then
|
if [ "$ENV_CHOICE" == "dev" ]; then
|
||||||
update_env "DEV_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
update_env "DEV_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
||||||
update_env "DEV_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
update_env "DEV_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
||||||
update_env "DEV_BARK_URL" "$REMOTE_BARK_URL"
|
|
||||||
# Portavilidad: set base URLs too
|
# Portavilidad: set base URLs too
|
||||||
update_env "LOCAL_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
update_env "LOCAL_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
||||||
update_env "LOCAL_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
update_env "LOCAL_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
||||||
else
|
else
|
||||||
update_env "PROD_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
update_env "PROD_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
||||||
update_env "PROD_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
update_env "PROD_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
||||||
update_env "PROD_BARK_URL" "$REMOTE_BARK_URL"
|
|
||||||
# Portavilidad: set base URLs too
|
# Portavilidad: set base URLs too
|
||||||
update_env "LOCAL_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
update_env "LOCAL_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
||||||
update_env "LOCAL_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
update_env "LOCAL_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Check and start Bark TTS service on t-800
|
|
||||||
|
|
||||||
echo "=== Checking Bark TTS Status on t-800 ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if systemd service exists
|
|
||||||
if systemctl list-unit-files | grep -q bark-tts; then
|
|
||||||
echo "✅ Bark systemd service found"
|
|
||||||
|
|
||||||
# Check service status
|
|
||||||
echo ""
|
|
||||||
echo "Service Status:"
|
|
||||||
sudo systemctl status bark-tts --no-pager
|
|
||||||
|
|
||||||
# If not running, try to start
|
|
||||||
if ! systemctl is-active --quiet bark-tts; then
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ Service is not running. Attempting to start..."
|
|
||||||
sudo systemctl start bark-tts
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
if systemctl is-active --quiet bark-tts; then
|
|
||||||
echo "✅ Service started successfully!"
|
|
||||||
else
|
|
||||||
echo "❌ Failed to start service. Checking logs..."
|
|
||||||
echo ""
|
|
||||||
echo "Recent logs:"
|
|
||||||
sudo journalctl -u bark-tts -n 20 --no-pager
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "✅ Service is running"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "❌ Bark systemd service not found"
|
|
||||||
echo ""
|
|
||||||
echo "The installation may not have completed successfully."
|
|
||||||
echo "Check if Bark is installed manually:"
|
|
||||||
echo ""
|
|
||||||
echo " ls -la /opt/bark/bark/"
|
|
||||||
echo " ps aux | grep uvicorn"
|
|
||||||
echo ""
|
|
||||||
echo "To install manually, run:"
|
|
||||||
echo " ssh juan@t-800"
|
|
||||||
echo " sudo /tmp/install_bark_tts.sh (if script exists)"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test API if service is running
|
|
||||||
if systemctl is-active --quiet bark-tts; then
|
|
||||||
echo ""
|
|
||||||
echo "=== Testing Bark API ==="
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
echo "Health endpoint:"
|
|
||||||
curl -s http://localhost:8443/health | head -5
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo ""
|
|
||||||
echo "Voices endpoint:"
|
|
||||||
curl -s http://localhost:8443/api/voices | head -10
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo ""
|
|
||||||
echo "=== API is accessible ==="
|
|
||||||
echo "You can now generate audio with:"
|
|
||||||
echo " curl 'http://localhost:8443/api/generate?text=Hello%20World&voice=v2/en_speaker_1' -o test.wav"
|
|
||||||
fi
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Bark TTS Cleanup Script for t-800
|
|
||||||
# This script removes all Bark TTS components from the server
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
T800_HOST="t-800"
|
|
||||||
T800_USER="juan"
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Bark TTS Cleanup for t-800"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
echo "This script will completely remove Bark TTS from t-800"
|
|
||||||
echo "Including:"
|
|
||||||
echo " - Systemd service"
|
|
||||||
echo " - Installation directory (/opt/bark)"
|
|
||||||
echo " - User account (bark)"
|
|
||||||
echo " - All cached models (~3.6 GB)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
read -p "Continue? [y/N]: " confirm
|
|
||||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
|
||||||
echo "❌ Cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "📤 Copying cleanup script to t-800..."
|
|
||||||
|
|
||||||
# Create cleanup script
|
|
||||||
cat > /tmp/cleanup_bark_remote.sh << 'INNEREOF'
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Stopping Bark Service ==="
|
|
||||||
sudo systemctl stop bark-tts 2>/dev/null && echo "✅ Service stopped" || echo "⚠️ Service not running"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Disabling Bark Service ==="
|
|
||||||
sudo systemctl disable bark-tts 2>/dev/null && echo "✅ Service disabled" || echo "⚠️ Service not enabled"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Removing Systemd Service ==="
|
|
||||||
if [ -f /etc/systemd/system/bark-tts.service ]; then
|
|
||||||
sudo rm -f /etc/systemd/system/bark-tts.service
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
echo "✅ Systemd service removed"
|
|
||||||
else
|
|
||||||
echo "⚠️ Systemd service not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Removing Installation Directory ==="
|
|
||||||
if [ -d /opt/bark ]; then
|
|
||||||
SIZE=$(du -sh /opt/bark 2>/dev/null | cut -f1)
|
|
||||||
echo "📊 Directory size: $SIZE"
|
|
||||||
sudo rm -rf /opt/bark
|
|
||||||
echo "✅ Directory removed"
|
|
||||||
else
|
|
||||||
echo "⚠️ Directory /opt/bark not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Removing User Account ==="
|
|
||||||
if id bark &>/dev/null; then
|
|
||||||
sudo userdel -r bark 2>/dev/null && echo "✅ User removed" || echo "⚠️ Could not remove user"
|
|
||||||
else
|
|
||||||
echo "⚠️ User 'bark' does not exist"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Cleaning Python Cache ==="
|
|
||||||
sudo rm -rf /root/.cache/pip 2>/dev/null || true
|
|
||||||
echo "✅ Cache cleaned"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Verification"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Services:"
|
|
||||||
if systemctl list-unit-files 2>/dev/null | grep -q bark; then
|
|
||||||
echo "❌ Bark services still exist:"
|
|
||||||
systemctl list-unit-files 2>/dev/null | grep bark
|
|
||||||
else
|
|
||||||
echo "✅ No Bark services found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Directories:"
|
|
||||||
if [ -d /opt/bark ]; then
|
|
||||||
echo "❌ Directory still exists: /opt/bark"
|
|
||||||
else
|
|
||||||
echo "✅ No Bark directories found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Users:"
|
|
||||||
if id bark &>/dev/null 2>&1; then
|
|
||||||
echo "❌ User 'bark' still exists"
|
|
||||||
else
|
|
||||||
echo "✅ User 'bark' removed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Processes:"
|
|
||||||
if ps aux | grep -v grep | grep -q "bark_api"; then
|
|
||||||
echo "❌ Bark processes still running"
|
|
||||||
ps aux | grep -v grep | grep "bark_api"
|
|
||||||
else
|
|
||||||
echo "✅ No Bark processes running"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Ports:"
|
|
||||||
if sudo netstat -tlnp 2>/dev/null | grep -q 8443; then
|
|
||||||
echo "❌ Port 8443 still in use"
|
|
||||||
sudo netstat -tlnp 2>/dev/null | grep 8443
|
|
||||||
else
|
|
||||||
echo "✅ Port 8443 is free"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Disk Space Recovered:"
|
|
||||||
echo "Previous: $(df -h /opt 2>/dev/null | tail -1 | awk '{print $4}') available"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " ✅ Cleanup Complete!"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo " 1. Verify disk space: df -h"
|
|
||||||
echo " 2. Check available ports: sudo netstat -tlnp"
|
|
||||||
echo " 3. Continue with OpenCCB features"
|
|
||||||
echo ""
|
|
||||||
INNEREOF
|
|
||||||
|
|
||||||
# Copy to t-800
|
|
||||||
scp /tmp/cleanup_bark_remote.sh ${T800_USER}@${T800_HOST}:/tmp/cleanup_bark_remote.sh
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🔌 Connecting to t-800 and running cleanup..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Execute on t-800
|
|
||||||
ssh -t ${T800_USER}@${T800_HOST} << 'ENDSSH'
|
|
||||||
chmod +x /tmp/cleanup_bark_remote.sh
|
|
||||||
sudo /tmp/cleanup_bark_remote.sh
|
|
||||||
|
|
||||||
# Clean up temp file
|
|
||||||
rm /tmp/cleanup_bark_remote.sh
|
|
||||||
ENDSSH
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Local Cleanup Complete"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Clean up local temp file
|
|
||||||
rm -f /tmp/cleanup_bark_remote.sh
|
|
||||||
|
|
||||||
echo "✅ All Bark TTS components have been removed from t-800"
|
|
||||||
echo ""
|
|
||||||
echo "📊 Next Steps - Choose What to Do Next:"
|
|
||||||
echo ""
|
|
||||||
echo " 1️⃣ Question Bank (sin audio)"
|
|
||||||
echo " - Crear preguntas manualmente"
|
|
||||||
echo " - Importar desde MySQL"
|
|
||||||
echo " - Generar con IA"
|
|
||||||
echo " - Acceder: /question-bank"
|
|
||||||
echo ""
|
|
||||||
echo " 2️⃣ Token Usage Dashboard"
|
|
||||||
echo " - Ver consumo de IA por usuario"
|
|
||||||
echo " - Monitorear costos"
|
|
||||||
echo " - Detectar alto consumo"
|
|
||||||
echo " - Acceder: /admin/token-usage"
|
|
||||||
echo ""
|
|
||||||
echo " 3️⃣ Importar Preguntas desde MySQL"
|
|
||||||
echo " - Traer preguntas del sistema legacy"
|
|
||||||
echo " - Marcar para no duplicar"
|
|
||||||
echo " - Asignar skills automáticamente"
|
|
||||||
echo ""
|
|
||||||
echo "💡 Recommended: Try all three!"
|
|
||||||
echo ""
|
|
||||||
echo " cd /home/juan/dev/openccb"
|
|
||||||
echo " # Start Studio"
|
|
||||||
echo " cd web/studio && npm run dev"
|
|
||||||
echo ""
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Deploy Bark TTS to t-800 server
|
|
||||||
# Usage: ./deploy_to_t800.sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
T800_HOST="t-800"
|
|
||||||
T800_USER="juan"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Deploying Bark TTS to t-800"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if SSH key exists
|
|
||||||
if [ ! -f ~/.ssh/id_rsa.pub ]; then
|
|
||||||
echo "SSH key not found. Generating one..."
|
|
||||||
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N "" -C "openccb_bark_deployment"
|
|
||||||
echo ""
|
|
||||||
echo "Now copy your SSH key to t-800:"
|
|
||||||
echo " ssh-copy-id ${T800_USER}@${T800_HOST}"
|
|
||||||
echo ""
|
|
||||||
read -p "Press Enter after copying the key..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy installation script to t-800
|
|
||||||
echo "Copying installation script to t-800..."
|
|
||||||
scp "${SCRIPT_DIR}/install_bark_tts.sh" ${T800_USER}@${T800_HOST}:/tmp/install_bark_tts.sh
|
|
||||||
|
|
||||||
# Execute installation on t-800 with pseudo-terminal
|
|
||||||
echo ""
|
|
||||||
echo "Connecting to t-800 and installing Bark TTS..."
|
|
||||||
echo "This may take 10-15 minutes depending on internet speed..."
|
|
||||||
echo "You'll be prompted for your password..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
ssh -t ${T800_USER}@${T800_HOST} << 'ENDSSH'
|
|
||||||
echo "Connected to t-800"
|
|
||||||
echo "Hostname: $(hostname)"
|
|
||||||
echo "Memory: $(free -h | grep Mem | awk '{print $2}')"
|
|
||||||
echo "Disk: $(df -h / | tail -1 | awk '{print $4}') available"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Install jq if not present
|
|
||||||
if ! command -v jq &> /dev/null; then
|
|
||||||
echo "Installing jq..."
|
|
||||||
sudo apt-get update && sudo apt-get install -y jq
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make script executable and run
|
|
||||||
chmod +x /tmp/install_bark_tts.sh
|
|
||||||
echo "Running Bark installation..."
|
|
||||||
sudo /tmp/install_bark_tts.sh
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
rm /tmp/install_bark_tts.sh
|
|
||||||
|
|
||||||
# Wait for service to be ready
|
|
||||||
echo ""
|
|
||||||
echo "Waiting for Bark API to be ready..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Test the API
|
|
||||||
echo "Testing Bark API..."
|
|
||||||
if curl -s http://localhost:8443/health | jq . > /dev/null 2>&1; then
|
|
||||||
echo "✅ Bark API is running!"
|
|
||||||
curl -s http://localhost:8443/health | jq .
|
|
||||||
else
|
|
||||||
echo "⚠️ API may still be starting up..."
|
|
||||||
echo "Check status with: sudo systemctl status bark-tts"
|
|
||||||
echo "View logs with: sudo journalctl -u bark-tts -f"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Bark TTS installation complete on t-800!"
|
|
||||||
ENDSSH
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Deployment Complete!"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo "1. Add BARK_API_URL to your .env file:"
|
|
||||||
echo " BARK_API_URL=http://t-800:8443"
|
|
||||||
echo ""
|
|
||||||
echo "2. Test the API:"
|
|
||||||
echo " curl 'http://t-800:8443/api/generate?text=Hello%20World&voice=v2/en_speaker_1' -o test.wav"
|
|
||||||
echo ""
|
|
||||||
echo "3. Generate audio for questions in OpenCCB:"
|
|
||||||
echo " POST /question-bank/{id}/generate-audio"
|
|
||||||
echo ""
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Fix Bark PyTorch 2.6+ compatibility issue
|
|
||||||
# Run this on t-800
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== Fixing Bark PyTorch 2.6+ Compatibility ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
BARK_DIR="/opt/bark/bark"
|
|
||||||
|
|
||||||
# Backup original generation.py
|
|
||||||
echo "[1/3] Backing up original generation.py..."
|
|
||||||
cp $BARK_DIR/bark/generation.py $BARK_DIR/bark/generation.py.backup
|
|
||||||
|
|
||||||
# Patch generation.py to use weights_only=False
|
|
||||||
echo "[2/3] Patching generation.py..."
|
|
||||||
sed -i 's/torch.load(ckpt_path, map_location=device)/torch.load(ckpt_path, map_location=device, weights_only=False)/g' $BARK_DIR/bark/generation.py
|
|
||||||
|
|
||||||
# Create fixed bark_api.py
|
|
||||||
echo "[3/3] Creating fixed bark_api.py..."
|
|
||||||
cat > $BARK_DIR/bark/bark_api.py << 'PYEOF'
|
|
||||||
"""
|
|
||||||
Bark TTS API Server - Fixed for PyTorch 2.6+
|
|
||||||
Simple FastAPI wrapper for Bark text-to-speech
|
|
||||||
"""
|
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from bark import SAMPLE_RATE, generate_audio, preload_models
|
|
||||||
from scipy.io.wavfile import write as write_wav
|
|
||||||
import numpy as np
|
|
||||||
import io
|
|
||||||
import torch
|
|
||||||
|
|
||||||
# Fix PyTorch 2.6+ weights_only issue
|
|
||||||
# This must be done BEFORE preload_models()
|
|
||||||
print("Configuring PyTorch for Bark compatibility...")
|
|
||||||
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bark TTS API",
|
|
||||||
description="Text-to-Speech API using Suno AI's Bark",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Preload models on startup (with warm-up)
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
print("Preloading Bark models...")
|
|
||||||
try:
|
|
||||||
preload_models()
|
|
||||||
print("Models loaded successfully!")
|
|
||||||
|
|
||||||
# Warm-up with a short generation
|
|
||||||
print("Warming up models with short generation...")
|
|
||||||
from bark import generate_text_semantic
|
|
||||||
text_semantic = generate_text_semantic("Hi", temp=0.7)
|
|
||||||
print("Warm-up complete! API ready.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR loading models: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
return {"status": "healthy", "service": "bark-tts"}
|
|
||||||
|
|
||||||
@app.get("/api/voices")
|
|
||||||
async def list_voices():
|
|
||||||
"""List available voice presets"""
|
|
||||||
return {
|
|
||||||
"voices": [
|
|
||||||
{"id": "v2/en_speaker_0", "name": "English Speaker 0", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_1", "name": "English Speaker 1", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_6", "name": "English Speaker 6", "language": "en"},
|
|
||||||
{"id": "v2/es_speaker_0", "name": "Spanish Speaker 0", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_1", "name": "Spanish Speaker 1", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_3", "name": "Spanish Speaker 3", "language": "es"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.post("/api/generate")
|
|
||||||
async def generate_speech(
|
|
||||||
text: str = Query(..., min_length=1, max_length=500, description="Text to convert to speech"),
|
|
||||||
voice: str = Query(default="v2/en_speaker_1", description="Voice preset to use"),
|
|
||||||
speed: float = Query(default=1.0, ge=0.5, le=2.0, description="Speech speed multiplier"),
|
|
||||||
output_format: str = Query(default="wav", regex="^(mp3|wav|ogg)$", description="Output audio format")
|
|
||||||
):
|
|
||||||
"""Generate speech from text using Bark TTS"""
|
|
||||||
try:
|
|
||||||
# Generate audio
|
|
||||||
audio_array = generate_audio(text, history_prompt=voice)
|
|
||||||
|
|
||||||
# Apply speed adjustment if needed
|
|
||||||
if speed != 1.0:
|
|
||||||
new_length = int(len(audio_array) / speed)
|
|
||||||
audio_array = audio_array[:new_length]
|
|
||||||
|
|
||||||
# Convert to bytes
|
|
||||||
audio_buffer = io.BytesIO()
|
|
||||||
write_wav(audio_buffer, SAMPLE_RATE, audio_array)
|
|
||||||
audio_buffer.seek(0)
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
audio_buffer,
|
|
||||||
media_type="audio/wav",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": f"attachment; filename=speech.wav",
|
|
||||||
"X-Voice-Used": voice,
|
|
||||||
"X-Speed": str(speed),
|
|
||||||
"X-Duration-Seconds": str(len(audio_array) / SAMPLE_RATE)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Generation error: {e}")
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/generate/batch")
|
|
||||||
async def generate_batch_speech(
|
|
||||||
texts: list[str] = Query(..., description="List of texts to convert"),
|
|
||||||
voice: str = Query(default="v2/en_speaker_1", description="Voice preset"),
|
|
||||||
speed: float = Query(default=1.0, description="Speech speed")
|
|
||||||
):
|
|
||||||
"""Generate multiple audio files in batch"""
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for i, text in enumerate(texts):
|
|
||||||
try:
|
|
||||||
audio_array = generate_audio(text, history_prompt=voice)
|
|
||||||
|
|
||||||
# Save to temp file
|
|
||||||
import tempfile
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
|
||||||
write_wav(f.name, SAMPLE_RATE, audio_array)
|
|
||||||
results.append({
|
|
||||||
"index": i,
|
|
||||||
"text": text,
|
|
||||||
"duration_seconds": len(audio_array) / SAMPLE_RATE,
|
|
||||||
"status": "success"
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
results.append({
|
|
||||||
"index": i,
|
|
||||||
"text": text,
|
|
||||||
"error": str(e),
|
|
||||||
"status": "failed"
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"results": results}
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
# Fix ownership
|
|
||||||
chown bark:bark $BARK_DIR/bark/generation.py
|
|
||||||
chown bark:bark $BARK_DIR/bark/bark_api.py
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Patch Applied ==="
|
|
||||||
echo ""
|
|
||||||
echo "Restarting Bark service..."
|
|
||||||
systemctl restart bark-tts
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Waiting for models to load (this takes 1-2 minutes)..."
|
|
||||||
sleep 30
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Checking service status..."
|
|
||||||
systemctl status bark-tts --no-pager
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Testing API..."
|
|
||||||
if curl -s http://localhost:8443/health | jq . > /dev/null 2>&1; then
|
|
||||||
echo "✅ Bark API is running!"
|
|
||||||
curl -s http://localhost:8443/health | jq .
|
|
||||||
else
|
|
||||||
echo "⚠️ API is still loading models..."
|
|
||||||
echo "Check logs with: journalctl -u bark-tts -f"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Fix Complete ==="
|
|
||||||
echo ""
|
|
||||||
echo "If you see errors, check:"
|
|
||||||
echo " 1. journalctl -u bark-tts -f"
|
|
||||||
echo " 2. systemctl status bark-tts"
|
|
||||||
echo ""
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Manual Bark TTS Installation for t-800
|
|
||||||
# Run this ONCE on t-800 server
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Manual Bark TTS Installation"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if running as root or with sudo
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
echo "Please run with: sudo ./install_bark_manual.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[1/6] Installing system dependencies..."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y python3 python3-pip python3-venv git ffmpeg curl jq
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[2/6] Creating bark user..."
|
|
||||||
if ! id -u bark > /dev/null 2>&1; then
|
|
||||||
useradd -r -m -s /bin/bash bark
|
|
||||||
echo "User 'bark' created"
|
|
||||||
else
|
|
||||||
echo "User 'bark' already exists"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[3/6] Setting up application directory..."
|
|
||||||
BARK_DIR="/opt/bark"
|
|
||||||
mkdir -p $BARK_DIR
|
|
||||||
chown bark:bark $BARK_DIR
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[4/6] Cloning Bark repository..."
|
|
||||||
cd $BARK_DIR
|
|
||||||
su - bark -c "cd $BARK_DIR && git clone https://github.com/suno-ai/bark.git"
|
|
||||||
chown -R bark:bark $BARK_DIR/bark
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[5/6] Creating Python virtual environment and installing dependencies..."
|
|
||||||
cd $BARK_DIR/bark
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && python3 -m venv venv"
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && source venv/bin/activate && pip install --upgrade pip"
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && source venv/bin/activate && pip install -e ."
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && source venv/bin/activate && pip install fastapi uvicorn[standard] python-multipart numpy scipy"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[6/6] Creating systemd service..."
|
|
||||||
cat > /etc/systemd/system/bark-tts.service << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Bark TTS API Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=bark
|
|
||||||
Group=bark
|
|
||||||
WorkingDirectory=$BARK_DIR/bark
|
|
||||||
Environment="PATH=$BARK_DIR/bark/venv/bin"
|
|
||||||
ExecStart=$BARK_DIR/bark/venv/bin/uvicorn bark_api:app --host 0.0.0.0 --port 8443 --workers 1
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
# Memory limits
|
|
||||||
MemoryMax=4G
|
|
||||||
MemoryHigh=3G
|
|
||||||
|
|
||||||
# CPU limits
|
|
||||||
CPUQuota=80%
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create Bark API wrapper
|
|
||||||
cat > $BARK_DIR/bark/bark_api.py << 'PYEOF'
|
|
||||||
"""
|
|
||||||
Bark TTS API Server
|
|
||||||
Simple FastAPI wrapper for Bark text-to-speech
|
|
||||||
"""
|
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from bark import SAMPLE_RATE, generate_audio, preload_models
|
|
||||||
from scipy.io.wavfile import write as write_wav
|
|
||||||
import numpy as np
|
|
||||||
import io
|
|
||||||
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bark TTS API",
|
|
||||||
description="Text-to-Speech API using Suno AI's Bark",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Preload models on startup
|
|
||||||
print("Preloading Bark models...")
|
|
||||||
preload_models()
|
|
||||||
print("Models loaded!")
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
return {"status": "healthy", "service": "bark-tts"}
|
|
||||||
|
|
||||||
@app.get("/api/voices")
|
|
||||||
async def list_voices():
|
|
||||||
"""List available voice presets"""
|
|
||||||
return {
|
|
||||||
"voices": [
|
|
||||||
{"id": "v2/en_speaker_0", "name": "English Speaker 0", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_1", "name": "English Speaker 1", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_6", "name": "English Speaker 6", "language": "en"},
|
|
||||||
{"id": "v2/es_speaker_0", "name": "Spanish Speaker 0", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_1", "name": "Spanish Speaker 1", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_3", "name": "Spanish Speaker 3", "language": "es"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.post("/api/generate")
|
|
||||||
async def generate_speech(
|
|
||||||
text: str = Query(..., min_length=1, max_length=500, description="Text to convert to speech"),
|
|
||||||
voice: str = Query(default="v2/en_speaker_1", description="Voice preset to use"),
|
|
||||||
speed: float = Query(default=1.0, ge=0.5, le=2.0, description="Speech speed multiplier"),
|
|
||||||
output_format: str = Query(default="wav", regex="^(mp3|wav|ogg)$", description="Output audio format")
|
|
||||||
):
|
|
||||||
"""Generate speech from text using Bark TTS"""
|
|
||||||
try:
|
|
||||||
# Generate audio
|
|
||||||
audio_array = generate_audio(text, history_prompt=voice)
|
|
||||||
|
|
||||||
# Apply speed adjustment if needed
|
|
||||||
if speed != 1.0:
|
|
||||||
new_length = int(len(audio_array) / speed)
|
|
||||||
audio_array = audio_array[:new_length]
|
|
||||||
|
|
||||||
# Convert to bytes
|
|
||||||
audio_buffer = io.BytesIO()
|
|
||||||
write_wav(audio_buffer, SAMPLE_RATE, audio_array)
|
|
||||||
audio_buffer.seek(0)
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
audio_buffer,
|
|
||||||
media_type="audio/wav",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": f"attachment; filename=speech.wav",
|
|
||||||
"X-Voice-Used": voice,
|
|
||||||
"X-Speed": str(speed),
|
|
||||||
"X-Duration-Seconds": str(len(audio_array) / SAMPLE_RATE)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
chown bark:bark $BARK_DIR/bark/bark_api.py
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Enabling and starting service..."
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable bark-tts
|
|
||||||
systemctl start bark-tts
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Installation Complete!"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
echo "Service Status:"
|
|
||||||
systemctl status bark-tts --no-pager
|
|
||||||
echo ""
|
|
||||||
echo "Waiting for Bark to preload models (this takes 1-2 minutes)..."
|
|
||||||
sleep 30
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Testing API..."
|
|
||||||
if curl -s http://localhost:8000/health | jq . > /dev/null 2>&1; then
|
|
||||||
echo "✅ Bark API is running!"
|
|
||||||
curl -s http://localhost:8000/health | jq .
|
|
||||||
else
|
|
||||||
echo "⚠️ API is starting up, models are loading..."
|
|
||||||
echo "Check status with: systemctl status bark-tts"
|
|
||||||
echo "View logs with: journalctl -u bark-tts -f"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Next Steps"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
echo "1. Test the API:"
|
|
||||||
echo " curl 'http://localhost:8000/api/generate?text=Hello%20World&voice=v2/en_speaker_1' -o test.wav"
|
|
||||||
echo ""
|
|
||||||
echo "2. Check service status anytime:"
|
|
||||||
echo " systemctl status bark-tts"
|
|
||||||
echo ""
|
|
||||||
echo "3. View logs:"
|
|
||||||
echo " journalctl -u bark-tts -f"
|
|
||||||
echo ""
|
|
||||||
echo "4. Restart service if needed:"
|
|
||||||
echo " systemctl restart bark-tts"
|
|
||||||
echo ""
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Bark TTS Installation Script for t-800 server
|
|
||||||
# This script installs Suno AI's Bark text-to-speech system
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Bark TTS Installation - Server t-800"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
# Check if running as root
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
|
||||||
echo "Please run as root (sudo ./install_bark.sh)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# System requirements check
|
|
||||||
echo "[1/8] Checking system requirements..."
|
|
||||||
REQUIRED_RAM=8
|
|
||||||
AVAILABLE_RAM=$(free -g | awk '/^Mem:/{print $2}')
|
|
||||||
|
|
||||||
if [ $AVAILABLE_RAM -lt $REQUIRED_RAM ]; then
|
|
||||||
echo "WARNING: Bark requires at least ${REQUIRED_RAM}GB RAM (found: ${AVAILABLE_RAM}GB)"
|
|
||||||
echo "Continuing anyway, but performance may be poor..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update system packages
|
|
||||||
echo "[2/8] Updating system packages..."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y python3 python3-pip python3-venv git ffmpeg curl
|
|
||||||
|
|
||||||
# Create bark user
|
|
||||||
echo "[3/8] Creating bark user..."
|
|
||||||
if ! id -u bark > /dev/null 2>&1; then
|
|
||||||
useradd -r -m -s /bin/bash bark
|
|
||||||
echo "User 'bark' created"
|
|
||||||
else
|
|
||||||
echo "User 'bark' already exists"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create application directory
|
|
||||||
echo "[4/8] Setting up application directory..."
|
|
||||||
BARK_DIR="/opt/bark"
|
|
||||||
mkdir -p $BARK_DIR
|
|
||||||
chown bark:bark $BARK_DIR
|
|
||||||
|
|
||||||
# Clone Bark repository
|
|
||||||
echo "[5/8] Cloning Bark repository..."
|
|
||||||
cd $BARK_DIR
|
|
||||||
if [ ! -d "bark" ]; then
|
|
||||||
su - bark -c "cd $BARK_DIR && git clone https://github.com/suno-ai/bark.git"
|
|
||||||
chown -R bark:bark $BARK_DIR/bark
|
|
||||||
else
|
|
||||||
echo "Bark repository already exists, updating..."
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && git pull"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create virtual environment
|
|
||||||
echo "[6/8] Creating Python virtual environment..."
|
|
||||||
cd $BARK_DIR/bark
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && python3 -m venv venv"
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
echo "[7/8] Installing Python dependencies..."
|
|
||||||
su - bark -c "cd $BARK_DIR/bark && source venv/bin/activate && pip install --upgrade pip && pip install -e ."
|
|
||||||
|
|
||||||
# Additional dependencies for API server
|
|
||||||
su - bark -c "source $BARK_DIR/bark/venv/bin/activate && pip install fastapi uvicorn[standard] python-multipart numpy scipy"
|
|
||||||
|
|
||||||
# Create systemd service
|
|
||||||
echo "[8/8] Creating systemd service..."
|
|
||||||
cat > /etc/systemd/system/bark-tts.service << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Bark TTS API Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=bark
|
|
||||||
Group=bark
|
|
||||||
WorkingDirectory=$BARK_DIR/bark
|
|
||||||
Environment="PATH=$BARK_DIR/bark/venv/bin"
|
|
||||||
ExecStart=$BARK_DIR/bark/venv/bin/uvicorn bark_api:app --host 0.0.0.0 --port 8000 --workers 1
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
# Memory limits
|
|
||||||
MemoryMax=4G
|
|
||||||
MemoryHigh=3G
|
|
||||||
|
|
||||||
# CPU limits
|
|
||||||
CPUQuota=80%
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create Bark API wrapper
|
|
||||||
cat > $BARK_DIR/bark/bark_api.py << 'PYEOF'
|
|
||||||
"""
|
|
||||||
Bark TTS API Server
|
|
||||||
Simple FastAPI wrapper for Bark text-to-speech
|
|
||||||
"""
|
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
|
||||||
from fastapi.responses import StreamingResponse, JSONResponse
|
|
||||||
from bark import SAMPLE_RATE, generate_audio, preload_models
|
|
||||||
from scipy.io.wavfile import write as write_wav
|
|
||||||
import numpy as np
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bark TTS API",
|
|
||||||
description="Text-to-Speech API using Suno AI's Bark",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Preload models on startup
|
|
||||||
print("Preloading Bark models...")
|
|
||||||
preload_models()
|
|
||||||
print("Models loaded!")
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
return {"status": "healthy", "service": "bark-tts"}
|
|
||||||
|
|
||||||
@app.get("/api/voices")
|
|
||||||
async def list_voices():
|
|
||||||
"""List available voice presets"""
|
|
||||||
return {
|
|
||||||
"voices": [
|
|
||||||
{"id": "v2/en_speaker_0", "name": "English Speaker 0", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_1", "name": "English Speaker 1", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_2", "name": "English Speaker 2", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_3", "name": "English Speaker 3", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_4", "name": "English Speaker 4", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_5", "name": "English Speaker 5", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_6", "name": "English Speaker 6", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_7", "name": "English Speaker 7", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_8", "name": "English Speaker 8", "language": "en"},
|
|
||||||
{"id": "v2/en_speaker_9", "name": "English Speaker 9", "language": "en"},
|
|
||||||
{"id": "v2/es_speaker_0", "name": "Spanish Speaker 0", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_1", "name": "Spanish Speaker 1", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_2", "name": "Spanish Speaker 2", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_3", "name": "Spanish Speaker 3", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_4", "name": "Spanish Speaker 4", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_5", "name": "Spanish Speaker 5", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_6", "name": "Spanish Speaker 6", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_7", "name": "Spanish Speaker 7", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_8", "name": "Spanish Speaker 8", "language": "es"},
|
|
||||||
{"id": "v2/es_speaker_9", "name": "Spanish Speaker 9", "language": "es"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.post("/api/generate")
|
|
||||||
async def generate_speech(
|
|
||||||
text: str = Query(..., min_length=1, max_length=500, description="Text to convert to speech"),
|
|
||||||
voice: str = Query(default="v2/en_speaker_1", description="Voice preset to use"),
|
|
||||||
speed: float = Query(default=1.0, ge=0.5, le=2.0, description="Speech speed multiplier"),
|
|
||||||
output_format: str = Query(default="mp3", regex="^(mp3|wav|ogg)$", description="Output audio format")
|
|
||||||
):
|
|
||||||
"""Generate speech from text using Bark TTS"""
|
|
||||||
try:
|
|
||||||
# Extract speaker number from voice preset
|
|
||||||
parts = voice.split("_")
|
|
||||||
if len(parts) >= 3:
|
|
||||||
speaker = f"{parts[0]}_{parts[1]}_{parts[2]}"
|
|
||||||
else:
|
|
||||||
speaker = "v2/en_speaker_1"
|
|
||||||
|
|
||||||
# Generate audio
|
|
||||||
audio_array = generate_audio(text, history_prompt=speaker)
|
|
||||||
|
|
||||||
# Apply speed adjustment if needed
|
|
||||||
if speed != 1.0:
|
|
||||||
# Simple speed adjustment by resampling
|
|
||||||
new_length = int(len(audio_array) / speed)
|
|
||||||
audio_array = audio_array[:new_length]
|
|
||||||
|
|
||||||
# Convert to bytes
|
|
||||||
audio_buffer = io.BytesIO()
|
|
||||||
write_wav(audio_buffer, SAMPLE_RATE, audio_array)
|
|
||||||
audio_buffer.seek(0)
|
|
||||||
|
|
||||||
# For MP3 output, we'd need to add pydub/ffmpeg
|
|
||||||
# For now, return WAV
|
|
||||||
media_type = "audio/wav"
|
|
||||||
filename = f"speech_{voice}.{output_format}"
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
audio_buffer,
|
|
||||||
media_type=media_type,
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": f"attachment; filename={filename}",
|
|
||||||
"X-Voice-Used": voice,
|
|
||||||
"X-Speed": str(speed),
|
|
||||||
"X-Duration-Seconds": str(len(audio_array) / SAMPLE_RATE)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/api/generate/batch")
|
|
||||||
async def generate_batch_speech(
|
|
||||||
texts: list[str] = Query(..., description="List of texts to convert"),
|
|
||||||
voice: str = Query(default="v2/en_speaker_1", description="Voice preset"),
|
|
||||||
speed: float = Query(default=1.0, description="Speech speed")
|
|
||||||
):
|
|
||||||
"""Generate multiple audio files in batch"""
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for i, text in enumerate(texts):
|
|
||||||
try:
|
|
||||||
audio_array = generate_audio(text, history_prompt=voice)
|
|
||||||
|
|
||||||
# Save to temp file
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
|
||||||
write_wav(f.name, SAMPLE_RATE, audio_array)
|
|
||||||
results.append({
|
|
||||||
"index": i,
|
|
||||||
"text": text,
|
|
||||||
"duration_seconds": len(audio_array) / SAMPLE_RATE,
|
|
||||||
"status": "success"
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
results.append({
|
|
||||||
"index": i,
|
|
||||||
"text": text,
|
|
||||||
"error": str(e),
|
|
||||||
"status": "failed"
|
|
||||||
})
|
|
||||||
|
|
||||||
return JSONResponse(content={"results": results})
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
chown bark:bark $BARK_DIR/bark/bark_api.py
|
|
||||||
|
|
||||||
# Enable and start service
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable bark-tts
|
|
||||||
systemctl start bark-tts
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Installation Complete!"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
echo "Service Status:"
|
|
||||||
systemctl status bark-tts --no-pager
|
|
||||||
echo ""
|
|
||||||
echo "API Endpoints:"
|
|
||||||
echo " - Health: http://localhost:8000/health"
|
|
||||||
echo " - Voices: http://localhost:8000/api/voices"
|
|
||||||
echo " - Generate: http://localhost:8000/api/generate?text=Hello&voice=v2/en_speaker_1"
|
|
||||||
echo ""
|
|
||||||
echo "Usage Example:"
|
|
||||||
echo " curl 'http://localhost:8000/api/generate?text=What%20color%20is%20the%20sky%3F&voice=v2/en_speaker_1' -o question.wav"
|
|
||||||
echo ""
|
|
||||||
echo "Logs: journalctl -u bark-tts -f"
|
|
||||||
echo ""
|
|
||||||
@@ -43,19 +43,11 @@ pub async fn create_question(
|
|||||||
.bind(payload.difficulty.as_deref().unwrap_or("medium"))
|
.bind(payload.difficulty.as_deref().unwrap_or("medium"))
|
||||||
.bind(payload.tags.as_deref())
|
.bind(payload.tags.as_deref())
|
||||||
.bind(payload.skill_assessed.as_deref())
|
.bind(payload.skill_assessed.as_deref())
|
||||||
.bind(payload.media_url.as_deref())
|
|
||||||
.bind(payload.media_type.as_deref())
|
.bind(payload.media_type.as_deref())
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// If audio generation requested, trigger it asynchronously
|
|
||||||
if payload.generate_audio.unwrap_or(false) {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = generate_audio_for_question(question.id, pool.clone()).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(question))
|
Ok(Json(question))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,129 +429,6 @@ pub async fn import_from_mysql(
|
|||||||
Ok(Json(imported_questions))
|
Ok(Json(imported_questions))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Audio Generation ====================
|
|
||||||
|
|
||||||
/// POST /api/question-bank/{id}/generate-audio - Generate audio for a question using Bark
|
|
||||||
pub async fn generate_audio(
|
|
||||||
Org(org_ctx): Org,
|
|
||||||
Path(id): Path<Uuid>,
|
|
||||||
State(pool): State<PgPool>,
|
|
||||||
payload: Option<Json<common::models::GenerateAudioPayload>>,
|
|
||||||
) -> Result<StatusCode, (StatusCode, String)> {
|
|
||||||
// Get question
|
|
||||||
let question: QuestionBank = sqlx::query_as(
|
|
||||||
"SELECT * FROM question_bank WHERE id = $1 AND organization_id = $2"
|
|
||||||
)
|
|
||||||
.bind(id)
|
|
||||||
.bind(org_ctx.id)
|
|
||||||
.fetch_one(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, "Question not found".to_string()),
|
|
||||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Spawn async task for audio generation
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = generate_audio_for_question_with_params(id, pool, payload.map(|p| p.0)).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(StatusCode::ACCEPTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn generate_audio_for_question(
|
|
||||||
question_id: Uuid,
|
|
||||||
pool: PgPool,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
generate_audio_for_question_with_params(question_id, pool, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn generate_audio_for_question_with_params(
|
|
||||||
question_id: Uuid,
|
|
||||||
pool: PgPool,
|
|
||||||
payload: Option<common::models::GenerateAudioPayload>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
use reqwest::Client;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
// Get question text
|
|
||||||
let question_text: String = sqlx::query_scalar("SELECT audio_text FROM question_bank WHERE id = $1")
|
|
||||||
.bind(question_id)
|
|
||||||
.fetch_optional(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to get question: {}", e))?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let text = payload.as_ref().map(|p| p.text.clone()).unwrap_or(question_text);
|
|
||||||
|
|
||||||
// Update status to generating
|
|
||||||
sqlx::query("UPDATE question_bank SET audio_status = 'generating' WHERE id = $1")
|
|
||||||
.bind(question_id)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to update status: {}", e))?;
|
|
||||||
|
|
||||||
// Call Bark TTS API
|
|
||||||
let bark_url = std::env::var("BARK_API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
|
|
||||||
let client = Client::new();
|
|
||||||
|
|
||||||
let voice = payload.as_ref().and_then(|p| p.voice.clone()).unwrap_or_else(|| "v2/en_speaker_1".to_string());
|
|
||||||
let speed = payload.as_ref().and_then(|p| p.speed).unwrap_or(1.0);
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.post(&format!("{}/api/generate", bark_url))
|
|
||||||
.json(&json!({
|
|
||||||
"text": text,
|
|
||||||
"voice": voice,
|
|
||||||
"speed": speed,
|
|
||||||
"output_format": "mp3"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Bark API request failed: {}", e))?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
sqlx::query("UPDATE question_bank SET audio_status = 'failed' WHERE id = $1")
|
|
||||||
.bind(question_id)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|_| "Failed to update status".to_string())?;
|
|
||||||
|
|
||||||
return Err(format!("Bark API returned error: {}", response.status()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save audio file
|
|
||||||
let audio_bytes = response.bytes().await.map_err(|e| format!("Failed to get audio bytes: {}", e))?;
|
|
||||||
|
|
||||||
// Save to uploads directory
|
|
||||||
let filename = format!("question_{}.mp3", question_id);
|
|
||||||
let file_path = format!("uploads/audio/{}", filename);
|
|
||||||
|
|
||||||
std::fs::create_dir_all("uploads/audio").map_err(|e| format!("Failed to create directory: {}", e))?;
|
|
||||||
std::fs::write(&file_path, &audio_bytes).map_err(|e| format!("Failed to save audio: {}", e))?;
|
|
||||||
|
|
||||||
// Update question with audio URL
|
|
||||||
let audio_url = format!("/audio/{}", filename);
|
|
||||||
sqlx::query(
|
|
||||||
"UPDATE question_bank SET audio_url = $1, audio_status = 'ready', audio_metadata = $2 WHERE id = $3"
|
|
||||||
)
|
|
||||||
.bind(&audio_url)
|
|
||||||
.bind(&json!({
|
|
||||||
"voice": voice,
|
|
||||||
"speed": speed,
|
|
||||||
"generated_at": chrono::Utc::now().to_rfc3339(),
|
|
||||||
"file_size": audio_bytes.len(),
|
|
||||||
}))
|
|
||||||
.bind(question_id)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to update question: {}", e))?;
|
|
||||||
|
|
||||||
tracing::info!("Generated audio for question {}", question_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Helpers ====================
|
// ==================== Helpers ====================
|
||||||
|
|
||||||
fn map_mysql_question_type(mysql_type: i32) -> QuestionBankType {
|
fn map_mysql_question_type(mysql_type: i32) -> QuestionBankType {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin("http://localhost:3000".parse::<http::HeaderValue>().unwrap())
|
||||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS, Method::PATCH])
|
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS, Method::PATCH])
|
||||||
.allow_headers([
|
.allow_headers([
|
||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
@@ -104,7 +104,7 @@ async fn main() {
|
|||||||
header::HeaderName::from_static("x-requested-with"),
|
header::HeaderName::from_static("x-requested-with"),
|
||||||
header::HeaderName::from_static("x-organization-id"),
|
header::HeaderName::from_static("x-organization-id"),
|
||||||
])
|
])
|
||||||
.expose_headers([header::CONTENT_LENGTH]);
|
.expose_headers([header::CONTENT_LENGTH, header::CONTENT_TYPE]);
|
||||||
|
|
||||||
// Rate limiting: Deshabilitado temporalmente por problemas de compatibilidad con tower-governor
|
// Rate limiting: Deshabilitado temporalmente por problemas de compatibilidad con tower-governor
|
||||||
// Para habilitar en producción, configurar con GovernorLayer y ajustar los límites apropiadamente
|
// Para habilitar en producción, configurar con GovernorLayer y ajustar los límites apropiadamente
|
||||||
@@ -343,10 +343,6 @@ async fn main() {
|
|||||||
"/question-bank/import-mysql",
|
"/question-bank/import-mysql",
|
||||||
post(handlers_question_bank::import_from_mysql),
|
post(handlers_question_bank::import_from_mysql),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/question-bank/{id}/generate-audio",
|
|
||||||
post(handlers_question_bank::generate_audio),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/question-bank/mysql-courses",
|
"/question-bank/mysql-courses",
|
||||||
get(handlers_question_bank::list_mysql_courses),
|
get(handlers_question_bank::list_mysql_courses),
|
||||||
@@ -392,10 +388,6 @@ async fn main() {
|
|||||||
.route(
|
.route(
|
||||||
"/branding",
|
"/branding",
|
||||||
get(handlers_branding::get_organization_branding),
|
get(handlers_branding::get_organization_branding),
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/organization",
|
|
||||||
get(handlers::get_public_organization),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
|
|||||||
@@ -1408,7 +1408,6 @@ pub struct CreateQuestionBankPayload {
|
|||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
pub media_url: Option<String>,
|
pub media_url: Option<String>,
|
||||||
pub media_type: Option<String>,
|
pub media_type: Option<String>,
|
||||||
pub generate_audio: Option<bool>,
|
|
||||||
pub skill_assessed: Option<String>, // reading, listening, speaking, writing
|
pub skill_assessed: Option<String>, // reading, listening, speaking, writing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1433,13 +1432,6 @@ pub struct ImportQuestionFromMySQLPayload {
|
|||||||
pub import_all: Option<bool>, // Import all questions from MySQL
|
pub import_all: Option<bool>, // Import all questions from MySQL
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct GenerateAudioPayload {
|
|
||||||
pub text: String,
|
|
||||||
pub voice: Option<String>, // Bark voice preset
|
|
||||||
pub speed: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct QuestionBankFilters {
|
pub struct QuestionBankFilters {
|
||||||
pub question_type: Option<QuestionBankType>,
|
pub question_type: Option<QuestionBankType>,
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ export default function AdminDashboard() {
|
|||||||
try {
|
try {
|
||||||
// In a real app we'd have a specific stats endpoint,
|
// In a real app we'd have a specific stats endpoint,
|
||||||
// but for now we'll calculate from lists
|
// but for now we'll calculate from lists
|
||||||
const [orgs, users] = await Promise.all([
|
const [org, users] = await Promise.all([
|
||||||
cmsApi.getOrganizations(),
|
cmsApi.getOrganization(),
|
||||||
cmsApi.getAllUsers()
|
cmsApi.getAllUsers()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
orgs: orgs.length,
|
orgs: 1, // Single tenant architecture
|
||||||
users: users.length,
|
users: users.length,
|
||||||
courses: 0 // We'd need a global courses count
|
courses: 0 // We'd need a global courses count
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ export default function UsersPage() {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const [usersData, orgsData] = await Promise.all([
|
const [usersData, orgData] = await Promise.all([
|
||||||
cmsApi.getAllUsers(),
|
cmsApi.getAllUsers(),
|
||||||
cmsApi.getOrganizations()
|
cmsApi.getOrganization()
|
||||||
]);
|
]);
|
||||||
setUsers(usersData);
|
setUsers(usersData);
|
||||||
setOrganizations(orgsData);
|
setOrganizations([orgData]); // Single tenant - wrap in array for compatibility
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load data', error);
|
console.error('Failed to load data', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { questionBankApi, QuestionBank, QuestionBankFilters, QuestionBankType } from '@/lib/api';
|
import { questionBankApi, QuestionBank, QuestionBankFilters, QuestionBankType } from '@/lib/api';
|
||||||
import {
|
import {
|
||||||
Plus, Search, Filter, Edit2, Trash2, Volume2, VolumeX, Download,
|
Plus, Search, Filter, Edit2, Trash2, Download,
|
||||||
Upload, Sparkles, ChevronDown, ChevronUp, X, Check, AlertCircle,
|
Upload, Sparkles, ChevronDown, ChevronUp, X, Check, AlertCircle,
|
||||||
Headphones, BookOpen, Tag, Hash, Globe
|
Headphones, BookOpen, Tag, Hash, Globe
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import QuestionBankEditor from '@/components/QuestionBank/QuestionBankEditor';
|
import QuestionBankEditor from '@/components/QuestionBank/QuestionBankEditor';
|
||||||
import QuestionBankCard from '@/components/QuestionBank/QuestionBankCard';
|
import QuestionBankCard from '@/components/QuestionBank/QuestionBankCard';
|
||||||
import MySQLImportModal from '@/components/QuestionBank/MySQLImportModal';
|
import MySQLImportModal from '@/components/QuestionBank/MySQLImportModal';
|
||||||
import AudioGeneratorModal from '@/components/QuestionBank/AudioGeneratorModal';
|
|
||||||
|
|
||||||
export default function QuestionBankPage() {
|
export default function QuestionBankPage() {
|
||||||
const [questions, setQuestions] = useState<QuestionBank[]>([]);
|
const [questions, setQuestions] = useState<QuestionBank[]>([]);
|
||||||
@@ -21,8 +20,6 @@ export default function QuestionBankPage() {
|
|||||||
const [showEditor, setShowEditor] = useState(false);
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
const [editingQuestion, setEditingQuestion] = useState<QuestionBank | null>(null);
|
const [editingQuestion, setEditingQuestion] = useState<QuestionBank | null>(null);
|
||||||
const [showImportModal, setShowImportModal] = useState(false);
|
const [showImportModal, setShowImportModal] = useState(false);
|
||||||
const [showAudioModal, setShowAudioModal] = useState(false);
|
|
||||||
const [selectedForAudio, setSelectedForAudio] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const loadQuestions = async () => {
|
const loadQuestions = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -67,17 +64,6 @@ export default function QuestionBankPage() {
|
|||||||
await loadQuestions();
|
await loadQuestions();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAudioGenerate = (questionId: string) => {
|
|
||||||
setSelectedForAudio(questionId);
|
|
||||||
setShowAudioModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAudioSuccess = async () => {
|
|
||||||
setShowAudioModal(false);
|
|
||||||
setSelectedForAudio(null);
|
|
||||||
await loadQuestions();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getQuestionTypeLabel = (type: QuestionBankType) => {
|
const getQuestionTypeLabel = (type: QuestionBankType) => {
|
||||||
const labels: Record<QuestionBankType, string> = {
|
const labels: Record<QuestionBankType, string> = {
|
||||||
'multiple-choice': 'Opción Múltiple',
|
'multiple-choice': 'Opción Múltiple',
|
||||||
@@ -311,7 +297,6 @@ export default function QuestionBankPage() {
|
|||||||
question={question}
|
question={question}
|
||||||
onEdit={() => handleEdit(question)}
|
onEdit={() => handleEdit(question)}
|
||||||
onDelete={() => handleDelete(question.id)}
|
onDelete={() => handleDelete(question.id)}
|
||||||
onGenerateAudio={() => handleAudioGenerate(question.id)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -337,18 +322,6 @@ export default function QuestionBankPage() {
|
|||||||
onCancel={() => setShowImportModal(false)}
|
onCancel={() => setShowImportModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Audio Generator Modal */}
|
|
||||||
{showAudioModal && selectedForAudio && (
|
|
||||||
<AudioGeneratorModal
|
|
||||||
questionId={selectedForAudio}
|
|
||||||
onSuccess={handleAudioSuccess}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowAudioModal(false);
|
|
||||||
setSelectedForAudio(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,320 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { questionBankApi, QuestionBank } from '@/lib/api';
|
|
||||||
import { X, Volume2, Check, AlertCircle, Play, Pause } from 'lucide-react';
|
|
||||||
|
|
||||||
interface AudioGeneratorModalProps {
|
|
||||||
questionId: string;
|
|
||||||
onSuccess?: () => void;
|
|
||||||
onCancel?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AudioGeneratorModal({ questionId, onSuccess, onCancel }: AudioGeneratorModalProps) {
|
|
||||||
const [question, setQuestion] = useState<QuestionBank | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [generating, setGenerating] = useState(false);
|
|
||||||
const [generated, setGenerated] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
|
||||||
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
|
||||||
|
|
||||||
const [voice, setVoice] = useState('v2/en_speaker_1');
|
|
||||||
const [speed, setSpeed] = useState(1.0);
|
|
||||||
const [customText, setCustomText] = useState('');
|
|
||||||
|
|
||||||
const voices = [
|
|
||||||
{ id: 'v2/en_speaker_0', name: 'English Speaker 0', language: 'en' },
|
|
||||||
{ id: 'v2/en_speaker_1', name: 'English Speaker 1', language: 'en' },
|
|
||||||
{ id: 'v2/en_speaker_6', name: 'English Speaker 6', language: 'en' },
|
|
||||||
{ id: 'v2/es_speaker_0', name: 'Spanish Speaker 0', language: 'es' },
|
|
||||||
{ id: 'v2/es_speaker_1', name: 'Spanish Speaker 1', language: 'es' },
|
|
||||||
{ id: 'v2/es_speaker_3', name: 'Spanish Speaker 3', language: 'es' },
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadQuestion();
|
|
||||||
}, [questionId]);
|
|
||||||
|
|
||||||
const loadQuestion = async () => {
|
|
||||||
try {
|
|
||||||
const data = await questionBankApi.get(questionId);
|
|
||||||
setQuestion(data);
|
|
||||||
setCustomText(data.question_text);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load question:', error);
|
|
||||||
setError('No se pudo cargar la pregunta');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
|
||||||
try {
|
|
||||||
setGenerating(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
await questionBankApi.generateAudio(questionId, customText, voice, speed);
|
|
||||||
|
|
||||||
// Poll for completion
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 30; // 30 seconds max
|
|
||||||
|
|
||||||
while (attempts < maxAttempts) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
const updated = await questionBankApi.get(questionId);
|
|
||||||
|
|
||||||
if (updated.audio_status === 'ready') {
|
|
||||||
setGenerated(true);
|
|
||||||
setQuestion(updated);
|
|
||||||
break;
|
|
||||||
} else if (updated.audio_status === 'failed') {
|
|
||||||
setError('Error al generar el audio');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempts >= maxAttempts) {
|
|
||||||
setError('Tiempo de espera agotado. El audio puede estar generándose aún.');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Audio generation failed:', error);
|
|
||||||
setError(error.message || 'Error al generar audio');
|
|
||||||
} finally {
|
|
||||||
setGenerating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlay = () => {
|
|
||||||
if (!question?.audio_url) return;
|
|
||||||
|
|
||||||
if (audio) {
|
|
||||||
audio.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioEl = new Audio(question.audio_url);
|
|
||||||
audioEl.onended = () => setIsPlaying(false);
|
|
||||||
audioEl.onerror = () => {
|
|
||||||
setIsPlaying(false);
|
|
||||||
alert('Error al reproducir el audio');
|
|
||||||
};
|
|
||||||
|
|
||||||
setAudio(audioEl);
|
|
||||||
setIsPlaying(true);
|
|
||||||
audioEl.play();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStop = () => {
|
|
||||||
if (audio) {
|
|
||||||
audio.pause();
|
|
||||||
audio.currentTime = 0;
|
|
||||||
}
|
|
||||||
setIsPlaying(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-lg w-full p-8 text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Cargando pregunta...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-2xl w-full">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Volume2 className="w-6 h-6 text-blue-600" />
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
|
||||||
Generar Audio con Bark
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Convierte el texto de la pregunta a audio
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onCancel}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
{/* Question Preview */}
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
|
||||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
|
||||||
Texto de la pregunta
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-900 dark:text-white text-sm">
|
|
||||||
{question?.question_text}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom Text */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Texto para audio (opcional)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={customText}
|
|
||||||
onChange={(e) => setCustomText(e.target.value)}
|
|
||||||
rows={2}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
placeholder="Deja en blanco para usar el texto de la pregunta"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
Puedes personalizar el texto si quieres una pronunciación diferente
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Voice Selection */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Voz
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={voice}
|
|
||||||
onChange={(e) => setVoice(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
|
||||||
>
|
|
||||||
{voices.map((v) => (
|
|
||||||
<option key={v.id} value={v.id}>
|
|
||||||
{v.name} ({v.language === 'en' ? 'Inglés' : 'Español'})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Speed */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Velocidad
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.5"
|
|
||||||
max="2.0"
|
|
||||||
step="0.1"
|
|
||||||
value={speed}
|
|
||||||
onChange={(e) => setSpeed(parseFloat(e.target.value))}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 w-12 text-right">
|
|
||||||
{speed.toFixed(1)}x
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Audio Preview */}
|
|
||||||
{question?.audio_status === 'ready' && question.audio_url && (
|
|
||||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-green-100 dark:bg-green-900/40 rounded-full flex items-center justify-center">
|
|
||||||
<Volume2 className="w-5 h-5 text-green-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-green-900 dark:text-green-100">
|
|
||||||
Audio generado
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-green-700 dark:text-green-300">
|
|
||||||
Haz click para escuchar
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={isPlaying ? handleStop : handlePlay}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
|
||||||
>
|
|
||||||
{isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
|
||||||
{isPlaying ? 'Detener' : 'Reproducir'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Generating Status */}
|
|
||||||
{generating && (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
|
||||||
Generando audio...
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
|
||||||
Esto puede tomar unos segundos
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-center gap-3">
|
|
||||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-red-900 dark:text-red-100">
|
|
||||||
Error
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-red-700 dark:text-red-300">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Success Message */}
|
|
||||||
{generated && (
|
|
||||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 flex items-center gap-3">
|
|
||||||
<Check className="w-5 h-5 text-green-600" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-green-900 dark:text-green-100">
|
|
||||||
¡Audio generado exitosamente!
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-green-700 dark:text-green-300">
|
|
||||||
El audio está disponible para los estudiantes
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex items-center justify-end gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={generating}
|
|
||||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{generated ? 'Cerrar' : 'Cancelar'}
|
|
||||||
</button>
|
|
||||||
{!question?.audio_url && (
|
|
||||||
<button
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={generating || generated}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Volume2 className="w-4 h-4" />
|
|
||||||
{generating ? 'Generando...' : 'Generar Audio'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, Download, Database, Check, AlertCircle, Upload, FileSpreadsheet } from 'lucide-react';
|
import { X, Download, Database, Check, AlertCircle, Upload, FileSpreadsheet } from 'lucide-react';
|
||||||
|
import { apiFetch } from '@/lib/api';
|
||||||
import ExcelImportModal from './ExcelImportModal';
|
import ExcelImportModal from './ExcelImportModal';
|
||||||
|
|
||||||
interface MySQLImportModalProps {
|
interface MySQLImportModalProps {
|
||||||
@@ -24,13 +25,9 @@ export default function MySQLImportModal({ onSuccess, onCancel }: MySQLImportMod
|
|||||||
setImporting(true);
|
setImporting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const result = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/question-bank/import-mysql-all`, {
|
const result = await apiFetch('/question-bank/import-mysql-all', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
});
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}).then(r => r.json());
|
|
||||||
|
|
||||||
setImportResult(result);
|
setImportResult(result);
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { QuestionBank } from '@/lib/api';
|
import { QuestionBank } from '@/lib/api';
|
||||||
import { Edit2, Trash2, Volume2, VolumeX, Sparkles, Globe, MoreVertical, Play, Pause } from 'lucide-react';
|
import { Edit2, Trash2, Volume2, Sparkles, Globe } from 'lucide-react';
|
||||||
|
|
||||||
interface QuestionBankCardProps {
|
interface QuestionBankCardProps {
|
||||||
question: QuestionBank;
|
question: QuestionBank;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onGenerateAudio: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuestionBankCard({ question, onEdit, onDelete, onGenerateAudio }: QuestionBankCardProps) {
|
export default function QuestionBankCard({ question, onEdit, onDelete }: QuestionBankCardProps) {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
@@ -87,21 +86,13 @@ export default function QuestionBankCard({ question, onEdit, onDelete, onGenerat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{question.audio_status === 'ready' ? (
|
{question.audio_url && (
|
||||||
<button
|
<button
|
||||||
onClick={isPlaying ? handleStopAudio : handlePlayAudio}
|
onClick={isPlaying ? handleStopAudio : handlePlayAudio}
|
||||||
className="p-1.5 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded transition-colors"
|
className="p-1.5 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/20 rounded transition-colors"
|
||||||
title={isPlaying ? 'Detener audio' : 'Reproducir audio'}
|
title={isPlaying ? 'Detener audio' : 'Reproducir audio'}
|
||||||
>
|
>
|
||||||
{isPlaying ? <Pause className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
|
<Volume2 className="w-4 h-4" />
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={onGenerateAudio}
|
|
||||||
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
|
|
||||||
title="Generar audio"
|
|
||||||
>
|
|
||||||
<VolumeX className="w-4 h-4" />
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -175,14 +166,6 @@ export default function QuestionBankCard({ question, onEdit, onDelete, onGenerat
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{question.audio_status === 'ready' && (
|
|
||||||
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
|
|
||||||
<Volume2 className="w-3 h-3" /> Audio listo
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{question.audio_status === 'generating' && (
|
|
||||||
<span className="text-xs text-yellow-600 dark:text-yellow-400">Generando audio...</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Usage Stats */}
|
{/* Usage Stats */}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export default function QuestionBankEditor({ question, onSuccess, onCancel }: Qu
|
|||||||
tags: question?.tags || [],
|
tags: question?.tags || [],
|
||||||
media_url: question?.media_url,
|
media_url: question?.media_url,
|
||||||
media_type: question?.media_type,
|
media_type: question?.media_type,
|
||||||
generate_audio: false,
|
|
||||||
skill_assessed: question?.skill_assessed,
|
skill_assessed: question?.skill_assessed,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -499,21 +498,6 @@ export default function QuestionBankEditor({ question, onSuccess, onCancel }: Qu
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Audio Generation Option */}
|
|
||||||
<div className="flex items-center gap-3 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="generate_audio"
|
|
||||||
checked={formData.generate_audio}
|
|
||||||
onChange={(e) => setFormData({ ...formData, generate_audio: e.target.checked })}
|
|
||||||
className="w-4 h-4 text-green-600 rounded"
|
|
||||||
/>
|
|
||||||
<label htmlFor="generate_audio" className="flex items-center gap-2 text-sm text-green-800 dark:text-green-200">
|
|
||||||
<Volume2 className="w-4 h-4" />
|
|
||||||
Generar audio automáticamente con Bark después de guardar
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700 sticky bottom-0 bg-white dark:bg-gray-800 -mx-6 px-6 py-4">
|
<div className="flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700 sticky bottom-0 bg-white dark:bg-gray-800 -mx-6 px-6 py-4">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -594,7 +594,7 @@ interface ApiFetchOptions extends RequestInit {
|
|||||||
query?: Record<string, string | number | boolean | undefined | null>;
|
query?: Record<string, string | number | boolean | undefined | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiFetch = (url: string, options: ApiFetchOptions = {}, isLms: boolean = false) => {
|
export const apiFetch = (url: string, options: ApiFetchOptions = {}, isLms: boolean = false) => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const selectedOrgId = getSelectedOrgId();
|
const selectedOrgId = getSelectedOrgId();
|
||||||
const baseUrl = isLms ? LMS_API_BASE_URL : API_BASE_URL;
|
const baseUrl = isLms ? LMS_API_BASE_URL : API_BASE_URL;
|
||||||
@@ -654,6 +654,7 @@ export const cmsApi = {
|
|||||||
},
|
},
|
||||||
getSSOConfig: (): Promise<OrganizationSSOConfig> => apiFetch('/organization/sso'),
|
getSSOConfig: (): Promise<OrganizationSSOConfig> => apiFetch('/organization/sso'),
|
||||||
updateSSOConfig: (payload: Partial<OrganizationSSOConfig>): Promise<void> => apiFetch('/organization/sso', { method: 'PUT', body: JSON.stringify(payload) }),
|
updateSSOConfig: (payload: Partial<OrganizationSSOConfig>): Promise<void> => apiFetch('/organization/sso', { method: 'PUT', body: JSON.stringify(payload) }),
|
||||||
|
getOrganization: (): Promise<Organization> => apiFetch('/organization'),
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
register: (payload: AuthPayload): Promise<AuthResponse> => apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }),
|
register: (payload: AuthPayload): Promise<AuthResponse> => apiFetch('/auth/register', { method: 'POST', body: JSON.stringify(payload) }),
|
||||||
@@ -980,7 +981,6 @@ export interface CreateQuestionBankPayload {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
media_url?: string;
|
media_url?: string;
|
||||||
media_type?: string;
|
media_type?: string;
|
||||||
generate_audio?: boolean;
|
|
||||||
skill_assessed?: string;
|
skill_assessed?: string;
|
||||||
audio_url?: string;
|
audio_url?: string;
|
||||||
audio_text?: string;
|
audio_text?: string;
|
||||||
@@ -1015,8 +1015,6 @@ export const questionBankApi = {
|
|||||||
apiFetch(`/question-bank/${id}`, { method: 'DELETE' }, false),
|
apiFetch(`/question-bank/${id}`, { method: 'DELETE' }, false),
|
||||||
importFromMySQL: (courseId?: number, questionIds?: number[], importAll?: boolean): Promise<QuestionBank[]> =>
|
importFromMySQL: (courseId?: number, questionIds?: number[], importAll?: boolean): Promise<QuestionBank[]> =>
|
||||||
apiFetch('/question-bank/import-mysql', { method: 'POST', body: JSON.stringify({ mysql_course_id: courseId, question_ids: questionIds, import_all: importAll }) }, false),
|
apiFetch('/question-bank/import-mysql', { method: 'POST', body: JSON.stringify({ mysql_course_id: courseId, question_ids: questionIds, import_all: importAll }) }, false),
|
||||||
generateAudio: (id: string, text?: string, voice?: string, speed?: number): Promise<void> =>
|
|
||||||
apiFetch(`/question-bank/${id}/generate-audio`, { method: 'POST', body: JSON.stringify({ text, voice, speed }) }, false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lmsApi = {
|
export const lmsApi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user