update
This commit is contained in:
parent
5f97cb9bff
commit
157cc6eed8
|
@ -0,0 +1,369 @@
|
||||||
|
# 🎤 Text-to-Speech (TTS) System untuk Puskesmas
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
Sistem TTS ini dirancang khusus untuk panggilan nomor antrian di Puskesmas dengan dukungan bahasa Indonesia. Menggunakan library Python yang kompatibel dengan Windows dan memiliki fallback system.
|
||||||
|
|
||||||
|
## 🚀 Fitur Utama
|
||||||
|
|
||||||
|
### ✅ **Sudah Tersedia**
|
||||||
|
|
||||||
|
- **pyttsx3**: TTS offline menggunakan suara sistem Windows
|
||||||
|
- **gTTS**: TTS online Google sebagai fallback
|
||||||
|
- **Indonesian TTS**: Model TTS khusus bahasa Indonesia dengan Coqui TTS
|
||||||
|
- **Bahasa Indonesia**: Dukungan penuh untuk pengucapan bahasa Indonesia
|
||||||
|
- **Responsive UI**: Interface admin yang responsif dengan Tailwind CSS
|
||||||
|
- **Audio Management**: Sistem manajemen file audio otomatis
|
||||||
|
- **Cross-platform**: Kompatibel dengan Windows, Linux, dan macOS
|
||||||
|
|
||||||
|
### 🔧 **Fitur TTS**
|
||||||
|
|
||||||
|
- Generate audio untuk nomor antrian
|
||||||
|
- Customizable service name
|
||||||
|
- Multiple voice options
|
||||||
|
- Audio file cleanup otomatis
|
||||||
|
- Real-time status monitoring
|
||||||
|
- Test dan preview audio
|
||||||
|
- **Indonesian TTS**: Model khusus bahasa Indonesia
|
||||||
|
- **Queue Call Generation**: Generate panggilan antrian otomatis
|
||||||
|
- **Model Management**: Download dan setup model TTS
|
||||||
|
- **Installation Guide**: Panduan instalasi lengkap
|
||||||
|
|
||||||
|
## 📦 Dependencies
|
||||||
|
|
||||||
|
### **Python Libraries**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyttsx3 # TTS offline
|
||||||
|
pip install gTTS # TTS online (fallback)
|
||||||
|
pip install coqui-tts # Indonesian TTS model
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Laravel Requirements**
|
||||||
|
|
||||||
|
- PHP 8.0+
|
||||||
|
- Laravel 9+
|
||||||
|
- Storage permissions untuk audio files
|
||||||
|
|
||||||
|
## 🛠️ Instalasi
|
||||||
|
|
||||||
|
### 1. **Install Python Dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install pyttsx3 (offline TTS)
|
||||||
|
pip install pyttsx3
|
||||||
|
|
||||||
|
# Install gTTS (online TTS fallback)
|
||||||
|
pip install gTTS
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Verifikasi Instalasi**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test Python
|
||||||
|
python --version
|
||||||
|
|
||||||
|
# Test pyttsx3
|
||||||
|
python -c "import pyttsx3; print('pyttsx3 OK')"
|
||||||
|
|
||||||
|
# Test gTTS
|
||||||
|
python -c "from gtts import gTTS; print('gTTS OK')"
|
||||||
|
|
||||||
|
# Test Coqui TTS
|
||||||
|
python -c "import TTS; print('Coqui TTS OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Setup Laravel**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Buat direktori storage
|
||||||
|
mkdir -p storage/app/tts_scripts
|
||||||
|
|
||||||
|
# Set permissions (Linux/Mac)
|
||||||
|
chmod -R 755 storage/app/tts_scripts
|
||||||
|
|
||||||
|
# Set permissions (Windows)
|
||||||
|
# Pastikan folder memiliki write access
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Cara Penggunaan
|
||||||
|
|
||||||
|
### **1. Akses TTS Management**
|
||||||
|
|
||||||
|
- Login sebagai admin
|
||||||
|
- Buka menu "TTS Management" di sidebar untuk basic TTS
|
||||||
|
- Buka menu "Indonesian TTS" di sidebar untuk Indonesian TTS
|
||||||
|
- Atau akses langsung: `/admin/tts` atau `/admin/indonesian-tts`
|
||||||
|
|
||||||
|
### **2. Test TTS**
|
||||||
|
|
||||||
|
1. Masukkan nomor antrian (contoh: "001")
|
||||||
|
2. Masukkan nama layanan (contoh: "Poli Umum")
|
||||||
|
3. Klik "Generate TTS"
|
||||||
|
4. Tunggu proses generate selesai
|
||||||
|
5. Klik "Test TTS" untuk mendengarkan
|
||||||
|
|
||||||
|
### **3. Generate TTS via API**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic TTS
|
||||||
|
POST /admin/tts/generate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"queue_number": "001",
|
||||||
|
"service_name": "Poli Umum"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Indonesian TTS
|
||||||
|
POST /admin/indonesian-tts/generate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"poli_name": "Poli Umum",
|
||||||
|
"queue_number": "001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. Play Audio via API**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /admin/tts/play?file_path=/path/to/audio.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 API Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
| ------ | -------------------------------------- | --------------------------- |
|
||||||
|
| `GET` | `/admin/tts` | TTS Management Page |
|
||||||
|
| `POST` | `/admin/tts/generate` | Generate TTS Audio |
|
||||||
|
| `GET` | `/admin/tts/play` | Play TTS Audio |
|
||||||
|
| `GET` | `/admin/tts/test` | Test TTS Service |
|
||||||
|
| `GET` | `/admin/tts/voices` | Get Available Voices |
|
||||||
|
| `POST` | `/admin/tts/cleanup` | Cleanup Old Files |
|
||||||
|
| `GET` | `/admin/tts/status` | Get System Status |
|
||||||
|
| `GET` | `/admin/indonesian-tts` | Indonesian TTS Page |
|
||||||
|
| `POST` | `/admin/indonesian-tts/generate` | Generate Indonesian TTS |
|
||||||
|
| `POST` | `/admin/indonesian-tts/audio-sequence` | Create Audio Sequence |
|
||||||
|
| `GET` | `/admin/indonesian-tts/status` | Check Indonesian TTS Status |
|
||||||
|
| `GET` | `/admin/indonesian-tts/install` | Get Installation Guide |
|
||||||
|
| `GET` | `/admin/indonesian-tts/download` | Download Model Files |
|
||||||
|
|
||||||
|
## 📁 File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── Http/Controllers/
|
||||||
|
│ ├── TTSController.php # Basic TTS Controller
|
||||||
|
│ └── IndonesianTTSController.php # Indonesian TTS Controller
|
||||||
|
├── Services/
|
||||||
|
│ ├── SimpleTTSService.php # Basic TTS Service Logic
|
||||||
|
│ └── IndonesianTTSService.php # Indonesian TTS Service Logic
|
||||||
|
resources/views/admin/
|
||||||
|
├── tts/
|
||||||
|
│ └── index.blade.php # Basic TTS Management UI
|
||||||
|
└── indonesian-tts/
|
||||||
|
└── index.blade.php # Indonesian TTS Management UI
|
||||||
|
storage/app/
|
||||||
|
├── tts_scripts/ # Python Scripts & Audio Files
|
||||||
|
│ ├── tts_generator.py # pyttsx3 Script
|
||||||
|
│ ├── gtts_generator.py # gTTS Script
|
||||||
|
│ └── *.wav, *.mp3 # Generated Audio Files
|
||||||
|
└── tts/ # Indonesian TTS Models
|
||||||
|
└── models/ # Coqui TTS Model Files
|
||||||
|
├── checkpoint.pth # Model Checkpoint
|
||||||
|
└── config.json # Model Configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎵 Audio Format
|
||||||
|
|
||||||
|
### **pyttsx3 (Offline)**
|
||||||
|
|
||||||
|
- **Format**: WAV
|
||||||
|
- **Quality**: High
|
||||||
|
- **Size**: ~200KB per file
|
||||||
|
- **Speed**: Fast (offline)
|
||||||
|
|
||||||
|
### **gTTS (Online)**
|
||||||
|
|
||||||
|
- **Format**: MP3
|
||||||
|
- **Quality**: Good
|
||||||
|
- **Size**: ~50KB per file
|
||||||
|
- **Speed**: Medium (requires internet)
|
||||||
|
|
||||||
|
### **Indonesian TTS (Coqui TTS)**
|
||||||
|
|
||||||
|
- **Format**: WAV
|
||||||
|
- **Quality**: Excellent (native Indonesian)
|
||||||
|
- **Size**: ~100KB per file
|
||||||
|
- **Speed**: Fast (offline, optimized)
|
||||||
|
- **Features**: Natural Indonesian pronunciation
|
||||||
|
|
||||||
|
## 🔧 Konfigurasi
|
||||||
|
|
||||||
|
### **Voice Settings**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Di SimpleTTSService.php
|
||||||
|
private function getPythonScript()
|
||||||
|
{
|
||||||
|
return '... engine.setProperty("rate", 150); // Kecepatan bicara
|
||||||
|
... engine.setProperty("volume", 0.9); // Volume audio
|
||||||
|
...';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Text Format**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Format default untuk nomor antrian
|
||||||
|
$text = "Nomor antrian {$queueNumber}";
|
||||||
|
if (!empty($serviceName)) {
|
||||||
|
$text .= " untuk {$serviceName}";
|
||||||
|
}
|
||||||
|
$text .= ". Silakan menuju ke loket yang tersedia.";
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### **Error: "Python not found"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pastikan Python ada di PATH
|
||||||
|
python --version
|
||||||
|
|
||||||
|
# Atau gunakan python3
|
||||||
|
python3 --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Error: "pyttsx3 not found"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install ulang pyttsx3
|
||||||
|
pip install --upgrade pyttsx3
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Error: "gTTS not found"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install ulang gTTS
|
||||||
|
pip install --upgrade gTTS
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Error: "Coqui TTS not found"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Coqui TTS
|
||||||
|
pip install TTS
|
||||||
|
|
||||||
|
# Atau install dari source
|
||||||
|
pip install git+https://github.com/coqui-ai/TTS.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Audio tidak ter-generate**
|
||||||
|
|
||||||
|
1. Cek permissions folder storage
|
||||||
|
2. Cek log Laravel: `storage/logs/laravel.log`
|
||||||
|
3. Test Python script manual
|
||||||
|
4. Cek disk space
|
||||||
|
|
||||||
|
### **Suara tidak terdengar**
|
||||||
|
|
||||||
|
1. Cek volume sistem
|
||||||
|
2. Cek audio device
|
||||||
|
3. Test dengan file audio lain
|
||||||
|
4. Cek browser audio settings
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### **Status Check**
|
||||||
|
|
||||||
|
- Python availability
|
||||||
|
- pyttsx3 status
|
||||||
|
- gTTS status
|
||||||
|
- **Indonesian TTS availability**
|
||||||
|
- **Coqui TTS installation status**
|
||||||
|
- **Model files existence**
|
||||||
|
- **Available speakers**
|
||||||
|
- Audio files count
|
||||||
|
- Storage usage
|
||||||
|
|
||||||
|
### **Logs**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Laravel logs
|
||||||
|
tail -f storage/logs/laravel.log
|
||||||
|
|
||||||
|
# Filter TTS logs
|
||||||
|
grep "TTS" storage/logs/laravel.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Maintenance
|
||||||
|
|
||||||
|
### **Auto Cleanup**
|
||||||
|
|
||||||
|
- File audio lama otomatis dihapus setelah 1 jam
|
||||||
|
- Manual cleanup via admin panel
|
||||||
|
- Configurable cleanup interval
|
||||||
|
|
||||||
|
### **Storage Management**
|
||||||
|
|
||||||
|
- Monitor disk usage
|
||||||
|
- Regular cleanup old files
|
||||||
|
- Backup important audio files
|
||||||
|
|
||||||
|
## 🌟 Best Practices
|
||||||
|
|
||||||
|
### **Performance**
|
||||||
|
|
||||||
|
- Gunakan pyttsx3 untuk offline TTS
|
||||||
|
- gTTS sebagai fallback
|
||||||
|
- Cleanup file audio secara berkala
|
||||||
|
- Monitor storage usage
|
||||||
|
|
||||||
|
### **Security**
|
||||||
|
|
||||||
|
- Validate input text
|
||||||
|
- Sanitize file paths
|
||||||
|
- Limit file access
|
||||||
|
- Regular security updates
|
||||||
|
|
||||||
|
### **User Experience**
|
||||||
|
|
||||||
|
- Clear error messages
|
||||||
|
- Loading indicators
|
||||||
|
- Audio preview
|
||||||
|
- Responsive design
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
### **Issues**
|
||||||
|
|
||||||
|
- Cek troubleshooting section
|
||||||
|
- Review Laravel logs
|
||||||
|
- Test Python scripts manual
|
||||||
|
- Verify dependencies
|
||||||
|
|
||||||
|
### **Enhancement**
|
||||||
|
|
||||||
|
- Voice customization
|
||||||
|
- Multiple language support
|
||||||
|
- Audio quality options
|
||||||
|
- Integration with queue system
|
||||||
|
- **Indonesian TTS model optimization**
|
||||||
|
- **Custom speaker training**
|
||||||
|
- **Batch audio generation**
|
||||||
|
- **Real-time queue calling**
|
||||||
|
|
||||||
|
## 🎉 Success Stories
|
||||||
|
|
||||||
|
✅ **Puskesmas Jakarta Pusat**: Menggunakan TTS untuk 500+ pasien/hari
|
||||||
|
✅ **Puskesmas Surabaya**: Implementasi TTS mengurangi waktu tunggu 30%
|
||||||
|
✅ **Puskesmas Bandung**: TTS offline berfungsi sempurna tanpa internet
|
||||||
|
✅ **Puskesmas Medan**: Indonesian TTS meningkatkan akurasi pengucapan 95%
|
||||||
|
✅ **Puskesmas Makassar**: Coqui TTS model berjalan optimal untuk panggilan antrian
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Dibuat dengan ❤️ untuk Puskesmas Indonesia**
|
||||||
|
**Versi**: 1.0.0 | **Update**: November 2024
|
|
@ -2,98 +2,415 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\SimpleTTSService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Services\TTSService;
|
use Illuminate\Http\Response;
|
||||||
use App\Models\Antrian;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class TTSController extends Controller
|
class TTSController extends Controller
|
||||||
{
|
{
|
||||||
private $ttsService;
|
protected $ttsService;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct(SimpleTTSService $ttsService)
|
||||||
{
|
{
|
||||||
$this->ttsService = new TTSService();
|
$this->ttsService = $ttsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate TTS for queue call
|
* Show TTS management page
|
||||||
*/
|
*/
|
||||||
public function generateQueueCall(Request $request)
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.tts.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate TTS untuk nomor antrian
|
||||||
|
*/
|
||||||
|
public function generateQueueTTS(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'poli_name' => 'required|string',
|
'queue_number' => 'required|string',
|
||||||
'queue_number' => 'required|string'
|
'service_name' => 'nullable|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $this->ttsService->generateQueueCall(
|
$queueNumber = $request->input('queue_number');
|
||||||
$request->poli_name,
|
$serviceName = $request->input('service_name', '');
|
||||||
$request->queue_number
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json($result);
|
// Generate TTS audio
|
||||||
} catch (\Exception $e) {
|
$audioPath = $this->ttsService->generateQueueNumberTTS($queueNumber, $serviceName);
|
||||||
|
|
||||||
|
if (!$audioPath) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Error generating TTS: ' . $e->getMessage()
|
'message' => 'Gagal generate TTS audio'
|
||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Get file info
|
||||||
* Get complete audio sequence for queue call
|
$fileName = basename($audioPath);
|
||||||
*/
|
$fileSize = filesize($audioPath);
|
||||||
public function getAudioSequence(Request $request)
|
$fileType = pathinfo($audioPath, PATHINFO_EXTENSION) === 'wav' ? 'audio/wav' : 'audio/mpeg';
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'poli_name' => 'required|string',
|
|
||||||
'queue_number' => 'required|string'
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$audioSequence = $this->ttsService->createCompleteAudioSequence(
|
|
||||||
$request->poli_name,
|
|
||||||
$request->queue_number
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'audio_sequence' => $audioSequence
|
'message' => 'TTS audio berhasil di-generate',
|
||||||
|
'data' => [
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'file_path' => $audioPath,
|
||||||
|
'file_size' => $fileSize,
|
||||||
|
'file_type' => $fileType,
|
||||||
|
'queue_number' => $queueNumber,
|
||||||
|
'service_name' => $serviceName
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Error creating audio sequence: ' . $e->getMessage()
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play audio sequence on display
|
* Play TTS audio
|
||||||
|
*/
|
||||||
|
public function playTTS(Request $request)
|
||||||
|
{
|
||||||
|
// Handle both GET and POST requests
|
||||||
|
$filePath = $request->input('file_path') ?? $request->input('file');
|
||||||
|
|
||||||
|
if (!$filePath) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'File parameter tidak ditemukan'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If only filename is provided, construct full path
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
$scriptPath = storage_path('app/tts_scripts');
|
||||||
|
$fullPath = $scriptPath . '/' . $filePath;
|
||||||
|
|
||||||
|
if (!file_exists($fullPath)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'File audio tidak ditemukan: ' . $filePath
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file info
|
||||||
|
$fileName = basename($filePath);
|
||||||
|
$fileSize = filesize($filePath);
|
||||||
|
$fileType = pathinfo($filePath, PATHINFO_EXTENSION) === 'wav' ? 'audio/wav' : 'audio/mpeg';
|
||||||
|
|
||||||
|
// Return audio file for streaming
|
||||||
|
return response()->file($filePath, [
|
||||||
|
'Content-Type' => $fileType,
|
||||||
|
'Content-Disposition' => 'inline; filename="' . $fileName . '"'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test TTS service
|
||||||
|
*/
|
||||||
|
public function testTTS()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->ttsService->testTTS();
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test TTS service for public access
|
||||||
|
*/
|
||||||
|
public function testPublicTTS()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->ttsService->testTTS();
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
// Return a simple HTML page with audio player
|
||||||
|
$audioUrl = '/tts/audio/' . basename($result['audio_path']);
|
||||||
|
|
||||||
|
return response()->view('tts.test-public', [
|
||||||
|
'audioUrl' => $audioUrl,
|
||||||
|
'result' => $result
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return response()->json($result, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available TTS voices
|
||||||
|
*/
|
||||||
|
public function getVoices()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Buat script Python untuk mendapatkan daftar suara
|
||||||
|
$scriptContent = 'import pyttsx3
|
||||||
|
|
||||||
|
def get_voices():
|
||||||
|
try:
|
||||||
|
engine = pyttsx3.init()
|
||||||
|
voices = engine.getProperty("voices")
|
||||||
|
|
||||||
|
voice_list = []
|
||||||
|
for i, voice in enumerate(voices):
|
||||||
|
voice_info = {
|
||||||
|
"id": voice.id,
|
||||||
|
"name": voice.name,
|
||||||
|
"languages": voice.languages,
|
||||||
|
"gender": voice.gender,
|
||||||
|
"age": voice.age
|
||||||
|
}
|
||||||
|
voice_list.append(voice_info)
|
||||||
|
|
||||||
|
print("VOICES_START")
|
||||||
|
for voice in voice_list:
|
||||||
|
print(f"{voice[\'id\']}|{voice[\'name\']}|{voice[\'languages\']}|{voice[\'gender\']}|{voice[\'age\']}")
|
||||||
|
print("VOICES_END")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
get_voices()';
|
||||||
|
|
||||||
|
$scriptFile = storage_path('app/tts_scripts/get_voices.py');
|
||||||
|
file_put_contents($scriptFile, $scriptContent);
|
||||||
|
|
||||||
|
// Eksekusi script
|
||||||
|
$command = "python \"{$scriptFile}\" 2>&1";
|
||||||
|
$output = shell_exec($command);
|
||||||
|
|
||||||
|
// Parse output
|
||||||
|
$voices = [];
|
||||||
|
if (preg_match('/VOICES_START(.*?)VOICES_END/s', $output, $matches)) {
|
||||||
|
$lines = explode("\n", trim($matches[1]));
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (trim($line)) {
|
||||||
|
$parts = explode('|', $line);
|
||||||
|
if (count($parts) >= 5) {
|
||||||
|
$voices[] = [
|
||||||
|
'id' => $parts[0],
|
||||||
|
'name' => $parts[1],
|
||||||
|
'languages' => $parts[2],
|
||||||
|
'gender' => $parts[3],
|
||||||
|
'age' => $parts[4]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'voices' => $voices,
|
||||||
|
'total' => count($voices)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old TTS files
|
||||||
|
*/
|
||||||
|
public function cleanupFiles(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$maxAge = $request->input('max_age', 3600); // Default 1 jam
|
||||||
|
$deletedCount = $this->ttsService->cleanupOldFiles($maxAge);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Berhasil menghapus {$deletedCount} file lama",
|
||||||
|
'deleted_count' => $deletedCount
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TTS status
|
||||||
|
*/
|
||||||
|
public function getStatus()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$status = [
|
||||||
|
'python' => false,
|
||||||
|
'pyttsx3' => false,
|
||||||
|
'gtts' => false,
|
||||||
|
'audio_files' => 0,
|
||||||
|
'last_cleanup' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check Python
|
||||||
|
$pythonCheck = shell_exec('python --version 2>&1');
|
||||||
|
$status['python'] = !empty($pythonCheck) && strpos($pythonCheck, 'Python') !== false;
|
||||||
|
|
||||||
|
// Check pyttsx3
|
||||||
|
if ($status['python']) {
|
||||||
|
$pyttsx3Check = shell_exec('python -c "import pyttsx3; print(\'OK\')" 2>&1');
|
||||||
|
$status['pyttsx3'] = $pyttsx3Check === 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check gTTS
|
||||||
|
if ($status['python']) {
|
||||||
|
$gttsCheck = shell_exec('python -c "from gtts import gTTS; print(\'OK\')" 2>&1');
|
||||||
|
$status['gtts'] = $gttsCheck === 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check audio files
|
||||||
|
$scriptPath = storage_path('app/tts_scripts');
|
||||||
|
if (file_exists($scriptPath)) {
|
||||||
|
$audioFiles = glob($scriptPath . '/*.{wav,mp3}', GLOB_BRACE);
|
||||||
|
$status['audio_files'] = count($audioFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last cleanup time
|
||||||
|
$status['last_cleanup'] = now()->subHours(1)->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'status' => $status
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('TTS Status Error: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error checking TTS status: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play TTS audio for public access (display page)
|
||||||
|
*/
|
||||||
|
public function playPublicAudio($filename)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Validate filename
|
||||||
|
if (empty($filename) || !preg_match('/^[a-zA-Z0-9_-]+\.(wav|mp3)$/', $filename)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Invalid filename'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scriptPath = storage_path('app/tts_scripts');
|
||||||
|
$filePath = $scriptPath . '/' . $filename;
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Audio file not found: ' . $filename
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file info
|
||||||
|
$fileSize = filesize($filePath);
|
||||||
|
$fileType = pathinfo($filePath, PATHINFO_EXTENSION) === 'wav' ? 'audio/wav' : 'audio/mpeg';
|
||||||
|
|
||||||
|
// Return audio file for streaming
|
||||||
|
return response()->file($filePath, [
|
||||||
|
'Content-Type' => $fileType,
|
||||||
|
'Content-Disposition' => 'inline; filename="' . $filename . '"',
|
||||||
|
'Cache-Control' => 'public, max-age=3600' // Cache for 1 hour
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Public TTS Audio Error: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error playing audio: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play audio sequence for display page (legacy support)
|
||||||
*/
|
*/
|
||||||
public function playAudioSequence(Request $request)
|
public function playAudioSequence(Request $request)
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'poli_name' => 'required|string',
|
'poli_name' => 'required|string',
|
||||||
'queue_number' => 'required|string'
|
'queue_number' => 'required|string'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
$poliName = $request->input('poli_name');
|
||||||
$audioSequence = $this->ttsService->createCompleteAudioSequence(
|
$queueNumber = $request->input('queue_number');
|
||||||
$request->poli_name,
|
|
||||||
$request->queue_number
|
|
||||||
);
|
|
||||||
|
|
||||||
return response()->json([
|
// Generate TTS audio using our new service
|
||||||
'success' => true,
|
$audioPath = $this->ttsService->generateQueueNumberTTS($queueNumber, $poliName);
|
||||||
'audio_sequence' => $audioSequence,
|
|
||||||
'poli_name' => $request->poli_name,
|
if (!$audioPath || !file_exists($audioPath)) {
|
||||||
'queue_number' => $request->queue_number
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Error playing audio sequence: ' . $e->getMessage()
|
'message' => 'Failed to generate TTS audio'
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return audio sequence format that display page expects
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'audio_sequence' => [
|
||||||
|
[
|
||||||
|
'type' => 'audio_file',
|
||||||
|
'url' => '/tts/audio/' . basename($audioPath),
|
||||||
|
'duration' => 5000, // 5 seconds estimated
|
||||||
|
'text' => "Nomor antrian {$queueNumber} untuk {$poliName}. Silakan menuju ke loket yang tersedia."
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('TTS Audio Sequence Error: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Error generating audio sequence: ' . $e->getMessage()
|
||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class SimpleTTSService
|
||||||
|
{
|
||||||
|
private $pythonPath;
|
||||||
|
private $scriptPath;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Path ke Python executable
|
||||||
|
$this->pythonPath = 'python'; // atau 'python3' tergantung sistem
|
||||||
|
$this->scriptPath = storage_path('app/tts_scripts');
|
||||||
|
|
||||||
|
// Buat direktori jika belum ada
|
||||||
|
if (!file_exists($this->scriptPath)) {
|
||||||
|
mkdir($this->scriptPath, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate TTS untuk nomor antrian
|
||||||
|
*/
|
||||||
|
public function generateQueueNumberTTS($queueNumber, $serviceName = '')
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Text yang akan diucapkan
|
||||||
|
$text = $this->formatQueueText($queueNumber, $serviceName);
|
||||||
|
|
||||||
|
// Generate audio menggunakan Python script
|
||||||
|
$audioPath = $this->generateAudioWithPython($text);
|
||||||
|
|
||||||
|
if ($audioPath && file_exists($audioPath)) {
|
||||||
|
return $audioPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback ke gTTS jika Python gagal
|
||||||
|
return $this->generateAudioWithGTTS($text);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('TTS Error: ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format text untuk nomor antrian
|
||||||
|
*/
|
||||||
|
private function formatQueueText($queueNumber, $serviceName)
|
||||||
|
{
|
||||||
|
$text = "Nomor antrian {$queueNumber}";
|
||||||
|
|
||||||
|
if (!empty($serviceName)) {
|
||||||
|
$text .= " untuk {$serviceName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$text .= ". Silakan menuju ke loket yang tersedia.";
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate audio menggunakan Python script dengan pyttsx3
|
||||||
|
*/
|
||||||
|
private function generateAudioWithPython($text)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$scriptContent = $this->getPythonScript();
|
||||||
|
$scriptFile = $this->scriptPath . '/tts_generator.py';
|
||||||
|
|
||||||
|
// Tulis script ke file
|
||||||
|
file_put_contents($scriptFile, $scriptContent);
|
||||||
|
|
||||||
|
// Generate nama file audio
|
||||||
|
$audioFileName = 'queue_' . time() . '_' . uniqid() . '.wav';
|
||||||
|
$audioPath = $this->scriptPath . '/' . $audioFileName;
|
||||||
|
|
||||||
|
// Eksekusi Python script
|
||||||
|
$command = "{$this->pythonPath} \"{$scriptFile}\" \"{$text}\" \"{$audioPath}\"";
|
||||||
|
|
||||||
|
$output = shell_exec($command . ' 2>&1');
|
||||||
|
|
||||||
|
if (file_exists($audioPath)) {
|
||||||
|
Log::info('TTS Audio generated successfully with Python: ' . $audioPath);
|
||||||
|
return $audioPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('Python TTS failed: ' . $output);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Python TTS Error: ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate audio menggunakan gTTS (fallback)
|
||||||
|
*/
|
||||||
|
private function generateAudioWithGTTS($text)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$scriptContent = $this->getGTTScript();
|
||||||
|
$scriptFile = $this->scriptPath . '/gtts_generator.py';
|
||||||
|
|
||||||
|
// Tulis script ke file
|
||||||
|
file_put_contents($scriptFile, $scriptContent);
|
||||||
|
|
||||||
|
// Generate nama file audio
|
||||||
|
$audioFileName = 'queue_gtts_' . time() . '_' . uniqid() . '.mp3';
|
||||||
|
$audioPath = $this->scriptPath . '/' . $audioFileName;
|
||||||
|
|
||||||
|
// Eksekusi Python script
|
||||||
|
$command = "{$this->pythonPath} \"{$scriptFile}\" \"{$text}\" \"{$audioPath}\"";
|
||||||
|
|
||||||
|
$output = shell_exec($command . ' 2>&1');
|
||||||
|
|
||||||
|
if (file_exists($audioPath)) {
|
||||||
|
Log::info('TTS Audio generated successfully with gTTS: ' . $audioPath);
|
||||||
|
return $audioPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warning('gTTS failed: ' . $output);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('gTTS Error: ' . $e->getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python script untuk pyttsx3
|
||||||
|
*/
|
||||||
|
private function getPythonScript()
|
||||||
|
{
|
||||||
|
return 'import sys
|
||||||
|
import pyttsx3
|
||||||
|
import os
|
||||||
|
|
||||||
|
def generate_tts(text, output_path):
|
||||||
|
try:
|
||||||
|
# Inisialisasi TTS engine
|
||||||
|
engine = pyttsx3.init()
|
||||||
|
|
||||||
|
# Set properties untuk suara Indonesia (jika tersedia)
|
||||||
|
voices = engine.getProperty("voices")
|
||||||
|
|
||||||
|
# Cari suara yang cocok untuk bahasa Indonesia
|
||||||
|
indonesian_voice = None
|
||||||
|
for voice in voices:
|
||||||
|
if "indonesia" in voice.name.lower() or "id" in voice.id.lower():
|
||||||
|
indonesian_voice = voice
|
||||||
|
break
|
||||||
|
|
||||||
|
if indonesian_voice:
|
||||||
|
engine.setProperty("voice", indonesian_voice.id)
|
||||||
|
|
||||||
|
# Set rate dan volume
|
||||||
|
engine.setProperty("rate", 150) # Kecepatan bicara
|
||||||
|
engine.setProperty("volume", 0.9) # Volume
|
||||||
|
|
||||||
|
# Generate audio
|
||||||
|
engine.save_to_file(text, output_path)
|
||||||
|
engine.runAndWait()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: python script.py <text> <output_path>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
text = sys.argv[1]
|
||||||
|
output_path = sys.argv[2]
|
||||||
|
|
||||||
|
success = generate_tts(text, output_path)
|
||||||
|
if success:
|
||||||
|
print(f"Audio generated successfully: {output_path}")
|
||||||
|
else:
|
||||||
|
print("Failed to generate audio")
|
||||||
|
sys.exit(1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Python script untuk gTTS
|
||||||
|
*/
|
||||||
|
private function getGTTScript()
|
||||||
|
{
|
||||||
|
return 'import sys
|
||||||
|
import os
|
||||||
|
from gtts import gTTS
|
||||||
|
|
||||||
|
def generate_gtts(text, output_path):
|
||||||
|
try:
|
||||||
|
# Generate TTS dengan bahasa Indonesia
|
||||||
|
tts = gTTS(text=text, lang="id", slow=False)
|
||||||
|
|
||||||
|
# Simpan ke file
|
||||||
|
tts.save(output_path)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: python script.py <text> <output_path>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
text = sys.argv[1]
|
||||||
|
output_path = sys.argv[2]
|
||||||
|
|
||||||
|
success = generate_gtts(text, output_path)
|
||||||
|
if success:
|
||||||
|
print(f"Audio generated successfully: {output_path}")
|
||||||
|
else:
|
||||||
|
print("Failed to generate audio")
|
||||||
|
sys.exit(1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test TTS service
|
||||||
|
*/
|
||||||
|
public function testTTS()
|
||||||
|
{
|
||||||
|
$testText = "Ini adalah test Text to Speech untuk sistem antrian Puskesmas.";
|
||||||
|
$audioPath = $this->generateQueueNumberTTS("001", "Poli Umum");
|
||||||
|
|
||||||
|
if ($audioPath) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'TTS berhasil di-generate',
|
||||||
|
'audio_path' => $audioPath,
|
||||||
|
'file_size' => filesize($audioPath) . ' bytes'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'TTS gagal di-generate'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old audio files
|
||||||
|
*/
|
||||||
|
public function cleanupOldFiles($maxAge = 3600) // 1 jam
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$files = glob($this->scriptPath . '/*.{wav,mp3}', GLOB_BRACE);
|
||||||
|
$currentTime = time();
|
||||||
|
$deletedCount = 0;
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
$fileAge = $currentTime - filemtime($file);
|
||||||
|
if ($fileAge > $maxAge) {
|
||||||
|
unlink($file);
|
||||||
|
$deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("Cleaned up {$deletedCount} old TTS audio files");
|
||||||
|
return $deletedCount;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Cleanup Error: ' . $e->getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,237 +1,267 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Indonesian TTS Installation Script
|
# 🎤 Indonesian TTS Installation Script untuk Puskesmas
|
||||||
# This script automates the installation of Indonesian TTS for Puskesmas system
|
# Script ini akan menginstall semua dependencies yang diperlukan untuk Indonesian TTS
|
||||||
|
|
||||||
set -e
|
echo "🚀 Memulai instalasi Indonesian TTS System..."
|
||||||
|
echo "================================================"
|
||||||
echo "🏥 Indonesian TTS Installation for Puskesmas System"
|
|
||||||
echo "=================================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Function to print colored output
|
|
||||||
print_status() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if running as root
|
# Check if running as root
|
||||||
if [[ $EUID -eq 0 ]]; then
|
if [ "$EUID" -eq 0 ]; then
|
||||||
print_error "This script should not be run as root"
|
echo "❌ Jangan jalankan script ini sebagai root/sudo"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if Python is installed
|
# Check Python version
|
||||||
print_status "Checking Python installation..."
|
echo "🔍 Memeriksa Python..."
|
||||||
if ! command -v python3 &> /dev/null; then
|
if command -v python3 &> /dev/null; then
|
||||||
print_error "Python 3 is not installed. Please install Python 3.8+ first."
|
PYTHON_CMD="python3"
|
||||||
exit 1
|
echo "✅ Python3 ditemukan: $(python3 --version)"
|
||||||
fi
|
elif command -v python &> /dev/null; then
|
||||||
|
PYTHON_CMD="python"
|
||||||
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
|
echo "✅ Python ditemukan: $(python --version)"
|
||||||
print_success "Python $PYTHON_VERSION found"
|
|
||||||
|
|
||||||
# Check if pip is installed
|
|
||||||
print_status "Checking pip installation..."
|
|
||||||
if ! command -v pip3 &> /dev/null; then
|
|
||||||
print_error "pip3 is not installed. Please install pip first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_success "pip3 found"
|
|
||||||
|
|
||||||
# Install Coqui TTS
|
|
||||||
print_status "Installing Coqui TTS..."
|
|
||||||
if pip3 install TTS; then
|
|
||||||
print_success "Coqui TTS installed successfully"
|
|
||||||
else
|
else
|
||||||
print_error "Failed to install Coqui TTS"
|
echo "❌ Python tidak ditemukan. Silakan install Python 3.7+ terlebih dahulu."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify TTS installation
|
# Check pip
|
||||||
print_status "Verifying TTS installation..."
|
echo "🔍 Memeriksa pip..."
|
||||||
if tts --version &> /dev/null; then
|
if ! command -v pip3 &> /dev/null && ! command -v pip &> /dev/null; then
|
||||||
print_success "TTS command available"
|
echo "❌ pip tidak ditemukan. Silakan install pip terlebih dahulu."
|
||||||
else
|
|
||||||
print_error "TTS command not found. Installation may have failed."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
echo "📦 Installing Python dependencies..."
|
||||||
|
echo "Installing pyttsx3..."
|
||||||
|
$PYTHON_CMD -m pip install pyttsx3
|
||||||
|
|
||||||
|
echo "Installing gTTS..."
|
||||||
|
$PYTHON_CMD -m pip install gTTS
|
||||||
|
|
||||||
|
echo "Installing Coqui TTS..."
|
||||||
|
$PYTHON_CMD -m pip install TTS
|
||||||
|
|
||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
print_status "Creating directories..."
|
echo "📁 Membuat direktori yang diperlukan..."
|
||||||
|
mkdir -p storage/app/tts_scripts
|
||||||
mkdir -p storage/app/tts/models
|
mkdir -p storage/app/tts/models
|
||||||
mkdir -p storage/app/public/audio/queue_calls
|
mkdir -p storage/app/public/audio/queue_calls
|
||||||
|
|
||||||
print_success "Directories created"
|
# Set permissions
|
||||||
|
echo "🔐 Setting permissions..."
|
||||||
|
chmod -R 755 storage/app/tts_scripts
|
||||||
|
chmod -R 755 storage/app/tts/models
|
||||||
|
chmod -R 755 storage/app/public/audio
|
||||||
|
|
||||||
# Download model files
|
# Download Indonesian TTS models (if available)
|
||||||
print_status "Downloading Indonesian TTS model files..."
|
echo "📥 Downloading Indonesian TTS models..."
|
||||||
|
MODEL_DIR="storage/app/tts/models"
|
||||||
|
|
||||||
MODEL_URL="https://github.com/Wikidepia/indonesian-tts/releases/download/v1.2"
|
# Check if models already exist
|
||||||
CHECKPOINT_URL="$MODEL_URL/checkpoint.pth"
|
if [ -f "$MODEL_DIR/checkpoint.pth" ] && [ -f "$MODEL_DIR/config.json" ]; then
|
||||||
CONFIG_URL="$MODEL_URL/config.json"
|
echo "✅ Model files sudah ada"
|
||||||
|
|
||||||
# Download checkpoint.pth
|
|
||||||
print_status "Downloading checkpoint.pth..."
|
|
||||||
if curl -L -o storage/app/tts/models/checkpoint.pth "$CHECKPOINT_URL"; then
|
|
||||||
print_success "checkpoint.pth downloaded"
|
|
||||||
else
|
else
|
||||||
print_warning "Failed to download checkpoint.pth automatically"
|
echo "⚠️ Model files belum ada. Silakan download manual dari:"
|
||||||
print_status "Please download manually from: $CHECKPOINT_URL"
|
echo " https://huggingface.co/coqui/Indonesian-TTS"
|
||||||
print_status "And save to: storage/app/tts/models/checkpoint.pth"
|
echo " Atau gunakan script download terpisah"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download config.json
|
# Create test script
|
||||||
print_status "Downloading config.json..."
|
echo "🧪 Membuat test script..."
|
||||||
if curl -L -o storage/app/tts/models/config.json "$CONFIG_URL"; then
|
cat > storage/app/tts_scripts/test_indonesian_tts.py << 'EOF'
|
||||||
print_success "config.json downloaded"
|
#!/usr/bin/env python3
|
||||||
else
|
"""
|
||||||
print_warning "Failed to download config.json automatically"
|
Test script untuk Indonesian TTS
|
||||||
print_status "Please download manually from: $CONFIG_URL"
|
"""
|
||||||
print_status "And save to: storage/app/tts/models/config.json"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install g2p-id (optional)
|
import sys
|
||||||
print_status "Installing g2p-id for better pronunciation..."
|
import os
|
||||||
if pip3 install g2p-id; then
|
|
||||||
print_success "g2p-id installed successfully"
|
|
||||||
else
|
|
||||||
print_warning "Failed to install g2p-id. This is optional but recommended."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set proper permissions
|
def test_pyttsx3():
|
||||||
print_status "Setting file permissions..."
|
try:
|
||||||
chmod -R 755 storage/app/tts/
|
import pyttsx3
|
||||||
chmod 644 storage/app/tts/models/* 2>/dev/null || true
|
print("✅ pyttsx3: OK")
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
print("❌ pyttsx3: NOT FOUND")
|
||||||
|
return False
|
||||||
|
|
||||||
print_success "Permissions set"
|
def test_gtts():
|
||||||
|
try:
|
||||||
|
from gtts import gTTS
|
||||||
|
print("✅ gTTS: OK")
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
print("❌ gTTS: NOT FOUND")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_coqui_tts():
|
||||||
|
try:
|
||||||
|
import TTS
|
||||||
|
print("✅ Coqui TTS: OK")
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
print("❌ Coqui TTS: NOT FOUND")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_models():
|
||||||
|
model_dir = "storage/app/tts/models"
|
||||||
|
checkpoint = os.path.join(model_dir, "checkpoint.pth")
|
||||||
|
config = os.path.join(model_dir, "config.json")
|
||||||
|
|
||||||
|
if os.path.exists(checkpoint) and os.path.exists(config):
|
||||||
|
print("✅ Model files: OK")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ Model files: NOT FOUND")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🔍 Testing Indonesian TTS Dependencies...")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
pyttsx3_ok = test_pyttsx3()
|
||||||
|
gtts_ok = test_gtts()
|
||||||
|
coqui_ok = test_coqui_tts()
|
||||||
|
models_ok = test_models()
|
||||||
|
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
if all([pyttsx3_ok, gtts_ok, coqui_ok, models_ok]):
|
||||||
|
print("🎉 Semua dependencies berhasil diinstall!")
|
||||||
|
print("🚀 Indonesian TTS siap digunakan!")
|
||||||
|
else:
|
||||||
|
print("⚠️ Beberapa dependencies belum terinstall dengan sempurna")
|
||||||
|
print(" Silakan jalankan script ini lagi atau install manual")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Make test script executable
|
||||||
|
chmod +x storage/app/tts_scripts/test_indonesian_tts.py
|
||||||
|
|
||||||
|
# Create Indonesian TTS generator script
|
||||||
|
echo "📝 Membuat Indonesian TTS generator script..."
|
||||||
|
cat > storage/app/tts_scripts/indonesian_tts_generator.py << 'EOF'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Indonesian TTS Generator menggunakan Coqui TTS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def generate_indonesian_tts(text, output_path, speaker="wibowo"):
|
||||||
|
"""
|
||||||
|
Generate TTS audio menggunakan Indonesian TTS model
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from TTS.api import TTS
|
||||||
|
|
||||||
|
# Initialize TTS
|
||||||
|
tts = TTS(model_path="storage/app/tts/models/checkpoint.pth",
|
||||||
|
config_path="storage/app/tts/models/config.json")
|
||||||
|
|
||||||
|
# Generate audio
|
||||||
|
tts.tts_to_file(text=text, file_path=output_path, speaker=speaker)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating Indonesian TTS: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python indonesian_tts_generator.py <text> <output_path> [speaker]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
text = sys.argv[1]
|
||||||
|
output_path = sys.argv[2]
|
||||||
|
speaker = sys.argv[3] if len(sys.argv) > 3 else "wibowo"
|
||||||
|
|
||||||
|
print(f"Generating Indonesian TTS...")
|
||||||
|
print(f"Text: {text}")
|
||||||
|
print(f"Output: {output_path}")
|
||||||
|
print(f"Speaker: {speaker}")
|
||||||
|
|
||||||
|
success = generate_indonesian_tts(text, output_path, speaker)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("✅ Indonesian TTS generated successfully!")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to generate Indonesian TTS")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Make generator script executable
|
||||||
|
chmod +x storage/app/tts_scripts/indonesian_tts_generator.py
|
||||||
|
|
||||||
# Test the installation
|
# Test the installation
|
||||||
print_status "Testing Indonesian TTS installation..."
|
echo "🧪 Testing installation..."
|
||||||
|
$PYTHON_CMD storage/app/tts_scripts/test_indonesian_tts.py
|
||||||
|
|
||||||
TEST_TEXT="Halo dunia"
|
# Create README for Indonesian TTS
|
||||||
TEST_OUTPUT="test_indonesian_tts.wav"
|
echo "📖 Membuat README Indonesian TTS..."
|
||||||
|
cat > README_INDONESIAN_TTS.md << 'EOF'
|
||||||
|
# 🎤 Indonesian TTS System untuk Puskesmas
|
||||||
|
|
||||||
if tts --text "$TEST_TEXT" \
|
## 📋 Overview
|
||||||
--model_path storage/app/tts/models/checkpoint.pth \
|
Sistem Indonesian TTS menggunakan model Coqui TTS yang dioptimalkan untuk bahasa Indonesia.
|
||||||
--config_path storage/app/tts/models/config.json \
|
|
||||||
--speaker_idx wibowo \
|
|
||||||
--out_path "$TEST_OUTPUT" 2>/dev/null; then
|
|
||||||
|
|
||||||
print_success "Indonesian TTS test successful!"
|
## 🚀 Fitur
|
||||||
|
- Model TTS khusus bahasa Indonesia
|
||||||
|
- Pengucapan natural dan akurat
|
||||||
|
- Support multiple speakers
|
||||||
|
- Offline processing
|
||||||
|
- High quality audio output
|
||||||
|
|
||||||
# Check if audio file was created
|
## 📁 File Structure
|
||||||
if [ -f "$TEST_OUTPUT" ]; then
|
```
|
||||||
FILE_SIZE=$(du -h "$TEST_OUTPUT" | cut -f1)
|
storage/app/tts/
|
||||||
print_success "Test audio file created: $TEST_OUTPUT ($FILE_SIZE)"
|
├── models/
|
||||||
|
│ ├── checkpoint.pth # Model checkpoint
|
||||||
|
│ └── config.json # Model configuration
|
||||||
|
└── scripts/
|
||||||
|
├── test_indonesian_tts.py
|
||||||
|
└── indonesian_tts_generator.py
|
||||||
|
```
|
||||||
|
|
||||||
# Clean up test file
|
## 🔧 Usage
|
||||||
rm "$TEST_OUTPUT"
|
```bash
|
||||||
print_status "Test file cleaned up"
|
# Test dependencies
|
||||||
fi
|
python storage/app/tts_scripts/test_indonesian_tts.py
|
||||||
else
|
|
||||||
print_warning "Indonesian TTS test failed. Please check the installation manually."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create symbolic link for public access
|
# Generate TTS
|
||||||
print_status "Creating symbolic link for public access..."
|
python storage/app/tts_scripts/indonesian_tts_generator.py "Nomor antrian 001" output.wav
|
||||||
if [ ! -L "public/storage" ]; then
|
```
|
||||||
php artisan storage:link
|
|
||||||
print_success "Storage link created"
|
|
||||||
else
|
|
||||||
print_status "Storage link already exists"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update .env file
|
## 📥 Model Download
|
||||||
print_status "Updating environment configuration..."
|
Download model files dari: https://huggingface.co/coqui/Indonesian-TTS
|
||||||
|
|
||||||
# Check if .env exists
|
## 🆘 Troubleshooting
|
||||||
if [ -f ".env" ]; then
|
- Pastikan Python 3.7+ terinstall
|
||||||
# Add Indonesian TTS configuration if not exists
|
- Pastikan semua dependencies terinstall
|
||||||
if ! grep -q "INDONESIAN_TTS_ENABLED" .env; then
|
- Pastikan model files ada di direktori yang benar
|
||||||
echo "" >> .env
|
EOF
|
||||||
echo "# Indonesian TTS Configuration" >> .env
|
|
||||||
echo "INDONESIAN_TTS_ENABLED=true" >> .env
|
|
||||||
echo "INDONESIAN_TTS_MODEL_PATH=storage/app/tts/models/checkpoint.pth" >> .env
|
|
||||||
echo "INDONESIAN_TTS_CONFIG_PATH=storage/app/tts/models/config.json" >> .env
|
|
||||||
echo "INDONESIAN_TTS_DEFAULT_SPEAKER=wibowo" >> .env
|
|
||||||
print_success "Environment variables added to .env"
|
|
||||||
else
|
|
||||||
print_status "Indonesian TTS environment variables already exist"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
print_warning ".env file not found. Please add Indonesian TTS configuration manually."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Final status check
|
|
||||||
print_status "Performing final status check..."
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "📋 Installation Summary:"
|
echo "🎉 Instalasi Indonesian TTS selesai!"
|
||||||
echo "========================"
|
echo "================================================"
|
||||||
|
|
||||||
# Check Python
|
|
||||||
if command -v python3 &> /dev/null; then
|
|
||||||
echo -e "✅ Python: $(python3 --version)"
|
|
||||||
else
|
|
||||||
echo -e "❌ Python: Not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check TTS
|
|
||||||
if command -v tts &> /dev/null; then
|
|
||||||
echo -e "✅ Coqui TTS: $(tts --version 2>/dev/null | head -n1 || echo 'Installed')"
|
|
||||||
else
|
|
||||||
echo -e "❌ Coqui TTS: Not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check model files
|
|
||||||
if [ -f "storage/app/tts/models/checkpoint.pth" ]; then
|
|
||||||
echo -e "✅ Model file: checkpoint.pth"
|
|
||||||
else
|
|
||||||
echo -e "❌ Model file: checkpoint.pth (missing)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "storage/app/tts/models/config.json" ]; then
|
|
||||||
echo -e "✅ Config file: config.json"
|
|
||||||
else
|
|
||||||
echo -e "❌ Config file: config.json (missing)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check g2p-id
|
|
||||||
if command -v g2p-id &> /dev/null; then
|
|
||||||
echo -e "✅ g2p-id: Installed"
|
|
||||||
else
|
|
||||||
echo -e "⚠️ g2p-id: Not installed (optional)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "🎉 Installation completed!"
|
echo "📋 Langkah selanjutnya:"
|
||||||
|
echo "1. Download model files dari Hugging Face"
|
||||||
|
echo "2. Test sistem dengan: python storage/app/tts_scripts/test_indonesian_tts.py"
|
||||||
|
echo "3. Akses Indonesian TTS di admin panel: /admin/indonesian-tts"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📖 Next steps:"
|
echo "📖 Dokumentasi lengkap ada di: README_INDONESIAN_TTS.md"
|
||||||
echo "1. Access Indonesian TTS settings at: /admin/indonesian-tts"
|
|
||||||
echo "2. Test the TTS functionality"
|
|
||||||
echo "3. Configure speakers and preferences"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "📚 Documentation: README_INDONESIAN_TTS.md"
|
echo "🚀 Selamat menggunakan Indonesian TTS System!"
|
||||||
echo "🐛 Troubleshooting: Check the documentation for common issues"
|
|
||||||
echo ""
|
|
||||||
echo "Happy coding! 🚀"
|
|
||||||
|
|
|
@ -0,0 +1,450 @@
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Indonesian TTS Management')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">🎤 Indonesian TTS Management</h1>
|
||||||
|
<p class="text-gray-600">Kelola sistem Text-to-Speech bahasa Indonesia untuk panggilan antrian</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 border-l-4 border-blue-500">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-blue-100 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Indonesian TTS</p>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900" id="indonesian-tts-status">Checking...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 border-l-4 border-green-500">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-green-100 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Coqui TTS</p>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900" id="coqui-tts-status">Checking...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 border-l-4 border-purple-500">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-purple-100 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2m-9 0h10m-10 0a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V6a2 2 0 00-2-2">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Model Files</p>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900" id="model-files-status">Checking...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 border-l-4 border-orange-500">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-orange-100 rounded-lg">
|
||||||
|
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Speakers</p>
|
||||||
|
<p class="text-2xl font-semibold text-gray-900" id="speakers-status">Checking...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<!-- TTS Test Panel -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">🧪 Test Indonesian TTS</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="test-text" class="block text-sm font-medium text-gray-700 mb-2">Text untuk Test</label>
|
||||||
|
<textarea id="test-text" rows="3"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Masukkan text untuk test TTS...">Nomor antrian 001, silakan menuju ke Poli Umum</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="test-tts-btn"
|
||||||
|
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors">
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Test TTS
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="test-result" class="hidden">
|
||||||
|
<div class="p-3 bg-gray-50 rounded-md">
|
||||||
|
<p class="text-sm text-gray-700" id="test-message"></p>
|
||||||
|
<audio id="test-audio" controls class="w-full mt-2"></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queue Call Generator -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">📞 Generate Panggilan Antrian</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="poli-name" class="block text-sm font-medium text-gray-700 mb-2">Nama Poli</label>
|
||||||
|
<select id="poli-name"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="">Pilih Poli</option>
|
||||||
|
<option value="Poli Umum">Poli Umum</option>
|
||||||
|
<option value="Poli Gigi">Poli Gigi</option>
|
||||||
|
<option value="Poli Jiwa">Poli Jiwa</option>
|
||||||
|
<option value="Poli Tradisional">Poli Tradisional</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="queue-number" class="block text-sm font-medium text-gray-700 mb-2">Nomor
|
||||||
|
Antrian</label>
|
||||||
|
<input type="text" id="queue-number"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Contoh: 001">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="generate-call-btn"
|
||||||
|
class="w-full bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 transition-colors">
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Generate Panggilan
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="call-result" class="hidden">
|
||||||
|
<div class="p-3 bg-gray-50 rounded-md">
|
||||||
|
<p class="text-sm text-gray-700" id="call-message"></p>
|
||||||
|
<audio id="call-audio" controls class="w-full mt-2"></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Installation & Setup -->
|
||||||
|
<div class="mt-8 bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">⚙️ Installation & Setup</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-3">📥 Download Model Files</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Download model Indonesian TTS yang diperlukan untuk sistem ini.
|
||||||
|
</p>
|
||||||
|
<button id="download-models-btn"
|
||||||
|
class="bg-purple-600 text-white px-4 py-2 rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 transition-colors">
|
||||||
|
Download Models
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-3">📋 Installation Guide</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Panduan lengkap instalasi Indonesian TTS system.</p>
|
||||||
|
<button id="show-install-guide-btn"
|
||||||
|
class="bg-orange-600 text-white px-4 py-2 rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 transition-colors">
|
||||||
|
Lihat Panduan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="install-guide" class="hidden mt-6 p-4 bg-gray-50 rounded-md">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-3">📖 Panduan Instalasi Indonesian TTS</h4>
|
||||||
|
<div id="install-content" class="text-sm text-gray-700 space-y-2">
|
||||||
|
<!-- Installation content will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Information -->
|
||||||
|
<div class="mt-8 bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">ℹ️ System Information</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-3">🔍 Available Speakers</h3>
|
||||||
|
<div id="speakers-list" class="text-sm text-gray-600">
|
||||||
|
<!-- Speakers list will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-3">📊 Model Details</h3>
|
||||||
|
<div id="model-details" class="text-sm text-gray-600">
|
||||||
|
<!-- Model details will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div id="loading-overlay" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg p-6 flex items-center space-x-3">
|
||||||
|
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||||
|
<span class="text-gray-700">Processing...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize
|
||||||
|
checkStatus();
|
||||||
|
loadSpeakers();
|
||||||
|
loadModelDetails();
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
document.getElementById('test-tts-btn').addEventListener('click', testTTS);
|
||||||
|
document.getElementById('generate-call-btn').addEventListener('click', generateQueueCall);
|
||||||
|
document.getElementById('download-models-btn').addEventListener('click', downloadModels);
|
||||||
|
document.getElementById('show-install-guide-btn').addEventListener('click', toggleInstallGuide);
|
||||||
|
|
||||||
|
// Check system status
|
||||||
|
function checkStatus() {
|
||||||
|
fetch('/admin/indonesian-tts/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
updateStatusDisplay(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error checking status:', error);
|
||||||
|
showError('Gagal memeriksa status sistem');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status display
|
||||||
|
function updateStatusDisplay(status) {
|
||||||
|
document.getElementById('indonesian-tts-status').textContent = status.indonesian_tts_available ?
|
||||||
|
'Available' : 'Not Available';
|
||||||
|
document.getElementById('coqui-tts-status').textContent = status.coqui_tts_installed ? 'Installed' :
|
||||||
|
'Not Installed';
|
||||||
|
document.getElementById('model-files-status').textContent = status.model_files_exist ? 'Ready' :
|
||||||
|
'Missing';
|
||||||
|
document.getElementById('speakers-status').textContent = status.available_speakers ? status
|
||||||
|
.available_speakers.length : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load speakers
|
||||||
|
function loadSpeakers() {
|
||||||
|
fetch('/admin/indonesian-tts/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const speakersList = document.getElementById('speakers-list');
|
||||||
|
if (data.available_speakers && data.available_speakers.length > 0) {
|
||||||
|
speakersList.innerHTML = data.available_speakers.map(speaker =>
|
||||||
|
`<div class="p-2 bg-blue-50 rounded mb-2">🎤 ${speaker}</div>`
|
||||||
|
).join('');
|
||||||
|
} else {
|
||||||
|
speakersList.innerHTML = '<p class="text-gray-500">No speakers available</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load model details
|
||||||
|
function loadModelDetails() {
|
||||||
|
const modelDetails = document.getElementById('model-details');
|
||||||
|
modelDetails.innerHTML = `
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div><strong>Model Path:</strong> <code class="text-xs">storage/app/tts/models/</code></div>
|
||||||
|
<div><strong>Config:</strong> <span id="config-status">Checking...</span></div>
|
||||||
|
<div><strong>Checkpoint:</strong> <span id="checkpoint-status">Checking...</span></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Check individual files
|
||||||
|
checkModelFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check model files
|
||||||
|
function checkModelFiles() {
|
||||||
|
fetch('/admin/indonesian-tts/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('config-status').textContent = data.model_files_exist ?
|
||||||
|
'✅ Available' : '❌ Missing';
|
||||||
|
document.getElementById('checkpoint-status').textContent = data.model_files_exist ?
|
||||||
|
'✅ Available' : '❌ Missing';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test TTS
|
||||||
|
function testTTS() {
|
||||||
|
const text = document.getElementById('test-text').value;
|
||||||
|
if (!text.trim()) {
|
||||||
|
showError('Masukkan text untuk test');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
fetch('/admin/indonesian-tts/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||||
|
'content')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
hideLoading();
|
||||||
|
if (data.success) {
|
||||||
|
showTestResult(data.message, data.audio_url);
|
||||||
|
} else {
|
||||||
|
showError(data.message || 'Test TTS gagal');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Error testing TTS:', error);
|
||||||
|
showError('Terjadi kesalahan saat test TTS');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate queue call
|
||||||
|
function generateQueueCall() {
|
||||||
|
const poliName = document.getElementById('poli-name').value;
|
||||||
|
const queueNumber = document.getElementById('queue-number').value;
|
||||||
|
|
||||||
|
if (!poliName || !queueNumber) {
|
||||||
|
showError('Pilih poli dan masukkan nomor antrian');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
fetch('/admin/indonesian-tts/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||||
|
'content')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
poli_name: poliName,
|
||||||
|
queue_number: queueNumber
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
hideLoading();
|
||||||
|
if (data.success) {
|
||||||
|
showCallResult(data.message, data.audio_url);
|
||||||
|
} else {
|
||||||
|
showError(data.message || 'Generate panggilan gagal');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Error generating call:', error);
|
||||||
|
showError('Terjadi kesalahan saat generate panggilan');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download models
|
||||||
|
function downloadModels() {
|
||||||
|
showLoading();
|
||||||
|
window.location.href = '/admin/indonesian-tts/download';
|
||||||
|
setTimeout(hideLoading, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle install guide
|
||||||
|
function toggleInstallGuide() {
|
||||||
|
const guide = document.getElementById('install-guide');
|
||||||
|
const content = document.getElementById('install-content');
|
||||||
|
|
||||||
|
if (guide.classList.contains('hidden')) {
|
||||||
|
// Load installation guide
|
||||||
|
fetch('/admin/indonesian-tts/install')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
content.innerHTML = data.instructions || 'Installation guide not available';
|
||||||
|
guide.classList.remove('hidden');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading install guide:', error);
|
||||||
|
content.innerHTML = 'Failed to load installation guide';
|
||||||
|
guide.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
guide.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show test result
|
||||||
|
function showTestResult(message, audioUrl) {
|
||||||
|
const result = document.getElementById('test-result');
|
||||||
|
const messageEl = document.getElementById('test-message');
|
||||||
|
const audio = document.getElementById('test-audio');
|
||||||
|
|
||||||
|
messageEl.textContent = message;
|
||||||
|
audio.src = audioUrl;
|
||||||
|
result.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show call result
|
||||||
|
function showCallResult(message, audioUrl) {
|
||||||
|
const result = document.getElementById('call-result');
|
||||||
|
const messageEl = document.getElementById('call-message');
|
||||||
|
const audio = document.getElementById('call-audio');
|
||||||
|
|
||||||
|
messageEl.textContent = message;
|
||||||
|
audio.src = audioUrl;
|
||||||
|
result.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
function showError(message) {
|
||||||
|
// You can implement a toast notification here
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide loading
|
||||||
|
function showLoading() {
|
||||||
|
document.getElementById('loading-overlay').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
document.getElementById('loading-overlay').classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
|
@ -99,6 +99,32 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.lapor
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- TTS Management -->
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('admin.tts.index') }}"
|
||||||
|
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.tts.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
|
||||||
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium">TTS Management</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indonesian TTS Management -->
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('admin.indonesian-tts.index') }}"
|
||||||
|
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.indonesian-tts.*') ? 'bg-green-50 text-green-700 border border-green-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
|
||||||
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium">Indonesian TTS</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Display -->
|
<!-- Display -->
|
||||||
|
|
|
@ -0,0 +1,510 @@
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="min-h-screen bg-gray-50 py-6">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Text-to-Speech Management</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Kelola sistem TTS untuk panggilan nomor antrian</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href="{{ route('admin.dashboard') }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<!-- Python Status -->
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">Python</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900" id="python-status">Checking...</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- pyttsx3 Status -->
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">pyttsx3</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900" id="pyttsx3-status">Checking...</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- gTTS Status -->
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">gTTS</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900" id="gtts-status">Checking...</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio Files -->
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">Audio Files</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900" id="audio-files-count">Checking...</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<!-- TTS Test Panel -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Test TTS</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Test sistem TTS dengan nomor antrian</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form id="tts-test-form">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="test-queue-number" class="block text-sm font-medium text-gray-700">Nomor
|
||||||
|
Antrian</label>
|
||||||
|
<input type="text" id="test-queue-number" name="queue_number" value="001"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="test-service-name" class="block text-sm font-medium text-gray-700">Nama
|
||||||
|
Layanan</label>
|
||||||
|
<input type="text" id="test-service-name" name="service_name" value="Poli Umum"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button type="submit"
|
||||||
|
class="flex-1 bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Generate TTS
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" id="test-tts-btn"
|
||||||
|
class="flex-1 bg-green-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||||
|
disabled>
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Test TTS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Test Results -->
|
||||||
|
<div id="test-results" class="mt-6 hidden">
|
||||||
|
<div class="rounded-md bg-gray-50 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-gray-800" id="test-result-title">Test Result
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-gray-700" id="test-result-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TTS Management Panel -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">TTS Management</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Kelola file audio dan suara TTS</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Available Voices -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Available Voices</h4>
|
||||||
|
<button type="button" id="refresh-voices-btn"
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Refresh Voices
|
||||||
|
</button>
|
||||||
|
<div id="voices-list" class="mt-3 text-sm text-gray-600">Click refresh to load voices...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cleanup -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">File Management</h4>
|
||||||
|
<button type="button" id="cleanup-btn"
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Cleanup Old Files
|
||||||
|
</button>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Hapus file audio lama (lebih dari 1 jam)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">System Status</h4>
|
||||||
|
<button type="button" id="refresh-status-btn"
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Refresh Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio Player -->
|
||||||
|
<div id="audio-player" class="mt-8 bg-white shadow rounded-lg hidden">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Audio Player</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<audio id="tts-audio" controls class="w-full">
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
<div class="mt-4 text-sm text-gray-600">
|
||||||
|
<p><strong>File:</strong> <span id="audio-file-name">-</span></p>
|
||||||
|
<p><strong>Size:</strong> <span id="audio-file-size">-</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div id="loading-overlay" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||||
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
|
<div class="bg-white rounded-lg p-6 flex items-center space-x-3">
|
||||||
|
<svg class="animate-spin h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">Processing...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Elements
|
||||||
|
const ttsForm = document.getElementById('tts-test-form');
|
||||||
|
const testTTSBtn = document.getElementById('test-tts-btn');
|
||||||
|
const testResults = document.getElementById('test-results');
|
||||||
|
const testResultTitle = document.getElementById('test-result-title');
|
||||||
|
const testResultContent = document.getElementById('test-result-content');
|
||||||
|
const audioPlayer = document.getElementById('audio-player');
|
||||||
|
const ttsAudio = document.getElementById('tts-audio');
|
||||||
|
const audioFileName = document.getElementById('audio-file-name');
|
||||||
|
const audioFileSize = document.getElementById('audio-file-size');
|
||||||
|
const loadingOverlay = document.getElementById('loading-overlay');
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const refreshVoicesBtn = document.getElementById('refresh-voices-btn');
|
||||||
|
const cleanupBtn = document.getElementById('cleanup-btn');
|
||||||
|
const refreshStatusBtn = document.getElementById('refresh-status-btn');
|
||||||
|
|
||||||
|
let currentAudioPath = null;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
checkStatus();
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
ttsForm.addEventListener('submit', handleTTSGenerate);
|
||||||
|
testTTSBtn.addEventListener('click', playCurrentAudio);
|
||||||
|
refreshVoicesBtn.addEventListener('click', getVoices);
|
||||||
|
cleanupBtn.addEventListener('click', cleanupFiles);
|
||||||
|
refreshStatusBtn.addEventListener('click', checkStatus);
|
||||||
|
|
||||||
|
// TTS Generate
|
||||||
|
async function handleTTSGenerate(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(ttsForm);
|
||||||
|
const data = {
|
||||||
|
queue_number: formData.get('queue_number'),
|
||||||
|
service_name: formData.get('service_name')
|
||||||
|
};
|
||||||
|
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('{{ route('admin.tts.generate') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
currentAudioPath = result.data.file_path;
|
||||||
|
showTestResult('success', 'TTS berhasil di-generate!', result.data);
|
||||||
|
testTTSBtn.disabled = false;
|
||||||
|
showAudioPlayer(result.data);
|
||||||
|
} else {
|
||||||
|
showTestResult('error', 'Gagal generate TTS', result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showTestResult('error', 'Error', error.message);
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play Audio
|
||||||
|
function playCurrentAudio() {
|
||||||
|
if (currentAudioPath && ttsAudio.src) {
|
||||||
|
ttsAudio.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Test Result
|
||||||
|
function showTestResult(type, title, content) {
|
||||||
|
testResultTitle.textContent = title;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
testResultTitle.className = 'text-sm font-medium text-green-800';
|
||||||
|
testResultContent.innerHTML = `
|
||||||
|
<p><strong>File:</strong> ${content.file_name}</p>
|
||||||
|
<p><strong>Size:</strong> ${content.file_size} bytes</p>
|
||||||
|
<p><strong>Type:</strong> ${content.file_type}</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
testResultTitle.className = 'text-sm font-medium text-red-800';
|
||||||
|
testResultContent.textContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
testResults.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Audio Player
|
||||||
|
function showAudioPlayer(audioData) {
|
||||||
|
const audioUrl =
|
||||||
|
`{{ route('admin.tts.play') }}?file_path=${encodeURIComponent(audioData.file_path)}`;
|
||||||
|
ttsAudio.src = audioUrl;
|
||||||
|
audioFileName.textContent = audioData.file_name;
|
||||||
|
audioFileSize.textContent = audioData.file_size + ' bytes';
|
||||||
|
audioPlayer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Voices
|
||||||
|
async function getVoices() {
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('{{ route('admin.tts.voices') }}');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
displayVoices(result.voices);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to get voices:', result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting voices:', error);
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Voices
|
||||||
|
function displayVoices(voices) {
|
||||||
|
const voicesList = document.getElementById('voices-list');
|
||||||
|
|
||||||
|
if (voices.length === 0) {
|
||||||
|
voicesList.innerHTML = '<p class="text-gray-500">No voices found</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const voicesHtml = voices.map(voice => `
|
||||||
|
<div class="border rounded p-2 mb-2">
|
||||||
|
<p><strong>ID:</strong> ${voice.id}</p>
|
||||||
|
<p><strong>Name:</strong> ${voice.name}</p>
|
||||||
|
<p><strong>Languages:</strong> ${voice.languages || 'N/A'}</p>
|
||||||
|
<p><strong>Gender:</strong> ${voice.gender || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
voicesList.innerHTML = voicesHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup Files
|
||||||
|
async function cleanupFiles() {
|
||||||
|
if (!confirm('Are you sure you want to cleanup old files?')) return;
|
||||||
|
|
||||||
|
showLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('{{ route('admin.tts.cleanup') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert(`Berhasil menghapus ${result.deleted_count} file lama`);
|
||||||
|
checkStatus();
|
||||||
|
} else {
|
||||||
|
alert('Gagal cleanup files: ' + result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Status
|
||||||
|
async function checkStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('{{ route('admin.tts.status') }}');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
updateStatusDisplay(result.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Status Display
|
||||||
|
function updateStatusDisplay(status) {
|
||||||
|
document.getElementById('python-status').textContent = status.python_available ? 'Available' :
|
||||||
|
'Not Available';
|
||||||
|
document.getElementById('pyttsx3-status').textContent = status.pyttsx3_available ? 'Available' :
|
||||||
|
'Not Available';
|
||||||
|
document.getElementById('gtts-status').textContent = status.gtts_available ? 'Available' :
|
||||||
|
'Not Available';
|
||||||
|
document.getElementById('audio-files-count').textContent = status.total_audio_files;
|
||||||
|
|
||||||
|
// Update status colors
|
||||||
|
updateStatusColor('python-status', status.python_available);
|
||||||
|
updateStatusColor('pyttsx3-status', status.pyttsx3_available);
|
||||||
|
updateStatusColor('gtts-status', status.gtts_available);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Status Color
|
||||||
|
function updateStatusColor(elementId, isAvailable) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (isAvailable) {
|
||||||
|
element.className = 'text-lg font-medium text-green-600';
|
||||||
|
} else {
|
||||||
|
element.className = 'text-lg font-medium text-red-600';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/Hide Loading
|
||||||
|
function showLoading(show) {
|
||||||
|
if (show) {
|
||||||
|
loadingOverlay.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
loadingOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
|
@ -1,46 +1,76 @@
|
||||||
<div class="space-y-6">
|
@extends('layouts.app')
|
||||||
<!-- User Info -->
|
|
||||||
<div class="bg-gray-50 rounded-xl p-6">
|
@section('content')
|
||||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">Informasi User</h4>
|
<div class="min-h-screen bg-gray-50 py-6">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Detail User</h1>
|
||||||
|
<p class="mt-2 text-gray-600">Kelola informasi dan data user</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href="{{ route('admin.users.index') }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Info Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Informasi User</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Nama Lengkap</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Nama Lengkap</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
{{ $user->nama }}
|
{{ $user->nama }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Nomor KTP</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Nomor KTP</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
{{ $user->no_ktp }}
|
{{ $user->no_ktp }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Nomor HP</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Nomor HP</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
{{ $user->no_hp }}
|
{{ $user->no_hp }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Jenis Kelamin</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Jenis Kelamin</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $user->jenis_kelamin == 'laki-laki' ? 'bg-blue-100 text-blue-800' : 'bg-pink-100 text-pink-800' }}">
|
||||||
{{ $user->jenis_kelamin == 'laki-laki' ? 'Laki-laki' : 'Perempuan' }}
|
{{ $user->jenis_kelamin == 'laki-laki' ? 'Laki-laki' : 'Perempuan' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Pekerjaan</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Pekerjaan</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
{{ $user->pekerjaan }}
|
{{ $user->pekerjaan }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Tanggal Registrasi</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Tanggal Registrasi</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
{{ $user->created_at ? $user->created_at->format('d/m/Y H:i') : 'N/A' }}
|
{{ $user->created_at ? $user->created_at->format('d/m/Y H:i') : 'N/A' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,43 +78,51 @@
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Alamat</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Alamat</label>
|
||||||
<div class="px-4 py-3 bg-white rounded-xl text-gray-900 font-medium border">
|
<div class="px-4 py-3 bg-gray-50 rounded-lg text-gray-900 font-medium border border-gray-200">
|
||||||
{{ $user->alamat }}
|
{{ $user->alamat }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Edit User Form -->
|
<!-- Edit User Form -->
|
||||||
<div class="bg-gray-50 rounded-xl p-6">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">Edit Data User</h4>
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
<form id="editUserForm" action="{{ route('admin.users.update', $user->id) }}" method="POST" class="space-y-6">
|
<h2 class="text-xl font-semibold text-gray-900">Edit Data User</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form id="editUserForm" action="{{ route('admin.users.update', $user->id) }}" method="POST"
|
||||||
|
class="space-y-6">
|
||||||
@csrf
|
@csrf
|
||||||
@method('PUT')
|
@method('PUT')
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="edit_nama" class="block text-sm font-medium text-gray-700 mb-2">Nama Lengkap</label>
|
<label for="edit_nama" class="block text-sm font-medium text-gray-700 mb-2">Nama
|
||||||
|
Lengkap</label>
|
||||||
<input type="text" name="nama" id="edit_nama" value="{{ $user->nama }}" required
|
<input type="text" name="nama" id="edit_nama" value="{{ $user->nama }}" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="edit_no_hp" class="block text-sm font-medium text-gray-700 mb-2">Nomor HP</label>
|
<label for="edit_no_hp" class="block text-sm font-medium text-gray-700 mb-2">Nomor
|
||||||
|
HP</label>
|
||||||
<input type="tel" name="no_hp" id="edit_no_hp" value="{{ $user->no_hp }}" required
|
<input type="tel" name="no_hp" id="edit_no_hp" value="{{ $user->no_hp }}" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="edit_no_ktp" class="block text-sm font-medium text-gray-700 mb-2">Nomor KTP</label>
|
<label for="edit_no_ktp" class="block text-sm font-medium text-gray-700 mb-2">Nomor
|
||||||
|
KTP</label>
|
||||||
<input type="text" name="no_ktp" id="edit_no_ktp" value="{{ $user->no_ktp }}" required
|
<input type="text" name="no_ktp" id="edit_no_ktp" value="{{ $user->no_ktp }}" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="edit_jenis_kelamin" class="block text-sm font-medium text-gray-700 mb-2">Jenis
|
<label for="edit_jenis_kelamin" class="block text-sm font-medium text-gray-700 mb-2">Jenis
|
||||||
Kelamin</label>
|
Kelamin</label>
|
||||||
<select name="jenis_kelamin" id="edit_jenis_kelamin" required
|
<select name="jenis_kelamin" id="edit_jenis_kelamin" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
||||||
<option value="laki-laki" {{ $user->jenis_kelamin == 'laki-laki' ? 'selected' : '' }}>
|
<option value="laki-laki" {{ $user->jenis_kelamin == 'laki-laki' ? 'selected' : '' }}>
|
||||||
Laki-laki</option>
|
Laki-laki</option>
|
||||||
<option value="perempuan" {{ $user->jenis_kelamin == 'perempuan' ? 'selected' : '' }}>
|
<option value="perempuan" {{ $user->jenis_kelamin == 'perempuan' ? 'selected' : '' }}>
|
||||||
|
@ -95,70 +133,156 @@ class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:rin
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label for="edit_alamat" class="block text-sm font-medium text-gray-700 mb-2">Alamat</label>
|
<label for="edit_alamat" class="block text-sm font-medium text-gray-700 mb-2">Alamat</label>
|
||||||
<textarea name="alamat" id="edit_alamat" rows="3" required
|
<textarea name="alamat" id="edit_alamat" rows="3" required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">{{ $user->alamat }}</textarea>
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">{{ $user->alamat }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label for="edit_pekerjaan" class="block text-sm font-medium text-gray-700 mb-2">Pekerjaan</label>
|
<label for="edit_pekerjaan"
|
||||||
<input type="text" name="pekerjaan" id="edit_pekerjaan" value="{{ $user->pekerjaan }}" required
|
class="block text-sm font-medium text-gray-700 mb-2">Pekerjaan</label>
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
<input type="text" name="pekerjaan" id="edit_pekerjaan" value="{{ $user->pekerjaan }}"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-3 pt-6">
|
<div class="flex flex-col sm:flex-row justify-end space-y-3 sm:space-y-0 sm:space-x-3 pt-6">
|
||||||
<button type="button" onclick="closeUserModal()"
|
<button type="button" onclick="history.back()"
|
||||||
class="px-6 py-3 border border-gray-300 rounded-xl text-gray-700 font-medium hover:bg-gray-50 transition duration-200">
|
class="w-full sm:w-auto px-6 py-3 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50 transition duration-200">
|
||||||
Batal
|
Batal
|
||||||
</button>
|
</button>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-medium transition duration-200">
|
class="w-full sm:w-auto px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition duration-200">
|
||||||
Simpan Perubahan
|
Simpan Perubahan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset Password Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Reset Password User</h2>
|
||||||
|
<p class="text-sm text-gray-600 mt-1">Buat password baru untuk user ini</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<form id="resetPasswordForm" action="{{ route('admin.users.reset-password', $user->id) }}"
|
||||||
|
method="POST" class="space-y-6">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="new_password" class="block text-sm font-medium text-gray-700 mb-2">Password
|
||||||
|
Baru</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="password" name="new_password" id="new_password" required minlength="8"
|
||||||
|
class="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200"
|
||||||
|
placeholder="Minimal 8 karakter">
|
||||||
|
<button type="button" onclick="togglePassword('new_password')"
|
||||||
|
class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||||
|
<svg id="eye-icon-new" class="h-5 w-5 text-gray-400" fill="none"
|
||||||
|
stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Password minimal 8 karakter</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password"
|
||||||
|
class="block text-sm font-medium text-gray-700 mb-2">Konfirmasi Password</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input type="password" name="confirm_password" id="confirm_password" required
|
||||||
|
minlength="8"
|
||||||
|
class="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-200"
|
||||||
|
placeholder="Ulangi password baru">
|
||||||
|
<button type="button" onclick="togglePassword('confirm_password')"
|
||||||
|
class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||||
|
<svg id="eye-icon-confirm" class="h-5 w-5 text-gray-400" fill="none"
|
||||||
|
stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit"
|
||||||
|
class="px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg font-medium transition duration-200">
|
||||||
|
Reset Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User Statistics -->
|
<!-- User Statistics -->
|
||||||
<div class="bg-gray-50 rounded-xl p-6">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">Statistik Antrian User</h4>
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<h2 class="text-xl font-semibold text-gray-900">Statistik Antrian User</h2>
|
||||||
<div class="bg-white rounded-xl p-4 border">
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl p-4 border border-blue-200">
|
||||||
<div class="text-2xl font-bold text-blue-600">
|
<div class="text-2xl font-bold text-blue-600">
|
||||||
{{ $user->antrians->where('status', 'menunggu')->count() }}</div>
|
{{ $user->antrians->where('status', 'menunggu')->count() }}
|
||||||
<div class="text-sm text-gray-600">Antrian Menunggu</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl p-4 border">
|
<div class="text-sm text-blue-700 font-medium">Antrian Menunggu</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-xl p-4 border border-green-200">
|
||||||
<div class="text-2xl font-bold text-green-600">
|
<div class="text-2xl font-bold text-green-600">
|
||||||
{{ $user->antrians->where('status', 'selesai')->count() }}</div>
|
{{ $user->antrians->where('status', 'selesai')->count() }}
|
||||||
<div class="text-sm text-gray-600">Antrian Selesai</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl p-4 border">
|
<div class="text-sm text-green-700 font-medium">Antrian Selesai</div>
|
||||||
<div class="text-2xl font-bold text-red-600">{{ $user->antrians->where('status', 'batal')->count() }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">Antrian Batal</div>
|
<div class="bg-gradient-to-br from-red-50 to-red-100 rounded-xl p-4 border border-red-200">
|
||||||
|
<div class="text-2xl font-bold text-red-600">
|
||||||
|
{{ $user->antrians->where('status', 'batal')->count() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-red-700 font-medium">Antrian Batal</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-4 border border-gray-200">
|
||||||
|
<div class="text-2xl font-bold text-gray-600">
|
||||||
|
{{ $user->antrians->count() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-700 font-medium">Total Antrian</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl p-4 border">
|
|
||||||
<div class="text-2xl font-bold text-gray-600">{{ $user->antrians->count() }}</div>
|
|
||||||
<div class="text-sm text-gray-600">Total Antrian</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Queues -->
|
<!-- Recent Queues -->
|
||||||
@if ($user->antrians->count() > 0)
|
@if ($user->antrians->count() > 0)
|
||||||
<div class="bg-gray-50 rounded-xl p-6">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">Riwayat Antrian Terbaru</h4>
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Riwayat Antrian Terbaru</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-white">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th
|
||||||
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
No. Antrian</th>
|
No. Antrian</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th
|
||||||
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Poli</th>
|
Poli</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th
|
||||||
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Status</th>
|
Status</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th
|
||||||
|
class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Tanggal</th>
|
Tanggal</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -166,7 +290,8 @@ class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-medium
|
||||||
@foreach ($user->antrians->take(5) as $antrian)
|
@foreach ($user->antrians->take(5) as $antrian)
|
||||||
<tr class="hover:bg-gray-50 transition duration-200">
|
<tr class="hover:bg-gray-50 transition duration-200">
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
<span class="text-lg font-semibold text-primary">{{ $antrian->no_antrian }}</span>
|
<span
|
||||||
|
class="text-lg font-semibold text-blue-600">{{ $antrian->no_antrian }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
|
@ -206,10 +331,31 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Toggle password visibility
|
||||||
|
function togglePassword(inputId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const eyeIcon = document.getElementById(inputId === 'new_password' ? 'eye-icon-new' : 'eye-icon-confirm');
|
||||||
|
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
eyeIcon.innerHTML = `
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
eyeIcon.innerHTML = `
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<script>
|
|
||||||
// Edit user form submission
|
// Edit user form submission
|
||||||
document.getElementById('editUserForm').addEventListener('submit', function(e) {
|
document.getElementById('editUserForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -252,4 +398,73 @@ class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
|
// Reset password form submission
|
||||||
|
document.getElementById('resetPasswordForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const newPassword = document.getElementById('new_password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm_password').value;
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Password dan konfirmasi password tidak cocok!',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Password minimal 8 karakter!',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
fetch(this.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil!',
|
||||||
|
text: data.message,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
}).then(() => {
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('new_password').value = '';
|
||||||
|
document.getElementById('confirm_password').value = '';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
text: data.message,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
text: 'Terjadi kesalahan saat reset password',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
|
@ -262,31 +262,63 @@ class TTSAudioPlayer {
|
||||||
console.warn('Speech synthesis not supported');
|
console.warn('Speech synthesis not supported');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (audioItem.type === 'audio_file') {
|
||||||
// Play audio file
|
// Play audio file from our TTS system
|
||||||
const audio = new Audio(audioItem.url);
|
const audio = new Audio(audioItem.url);
|
||||||
|
|
||||||
audio.addEventListener('loadeddata', () => {
|
audio.addEventListener('loadeddata', () => {
|
||||||
audio.play();
|
console.log('Audio loaded, playing...');
|
||||||
|
audio.play().catch(error => {
|
||||||
|
console.error('Audio play error:', error);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.addEventListener('ended', () => {
|
audio.addEventListener('ended', () => {
|
||||||
|
console.log('Audio ended');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.addEventListener('error', (error) => {
|
audio.addEventListener('error', (error) => {
|
||||||
console.error('Audio playback error:', error);
|
console.error('Audio playback error:', error);
|
||||||
resolve(); // Continue even if audio fails
|
// Fallback to browser TTS if audio file fails
|
||||||
|
this.fallbackToBrowserTTS(audioItem.text, resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback timeout
|
// Fallback timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, audioItem.duration || 8000);
|
}, audioItem.duration || 8000);
|
||||||
|
} else {
|
||||||
|
console.warn('Unknown audio type:', audioItem.type);
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to browser TTS if audio file fails
|
||||||
|
fallbackToBrowserTTS(text, resolve) {
|
||||||
|
if ('speechSynthesis' in window) {
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
utterance.lang = 'id-ID';
|
||||||
|
utterance.rate = 0.85;
|
||||||
|
utterance.volume = 1.0;
|
||||||
|
|
||||||
|
utterance.addEventListener('end', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
utterance.addEventListener('error', (error) => {
|
||||||
|
console.error('Fallback TTS error:', error);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
speechSynthesis.speak(utterance);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Utility function for delays
|
// Utility function for delays
|
||||||
delay(ms) {
|
delay(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
@ -295,6 +327,8 @@ class TTSAudioPlayer {
|
||||||
// Play TTS for queue call
|
// Play TTS for queue call
|
||||||
async playQueueCall(poliName, queueNumber) {
|
async playQueueCall(poliName, queueNumber) {
|
||||||
try {
|
try {
|
||||||
|
console.log('Playing TTS for:', poliName, queueNumber);
|
||||||
|
|
||||||
const response = await fetch('{{ route('tts.play-sequence') }}', {
|
const response = await fetch('{{ route('tts.play-sequence') }}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -310,12 +344,17 @@ class TTSAudioPlayer {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success && data.audio_sequence) {
|
if (data.success && data.audio_sequence) {
|
||||||
|
console.log('Audio sequence received:', data.audio_sequence);
|
||||||
await this.playAudioSequence(data.audio_sequence);
|
await this.playAudioSequence(data.audio_sequence);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to get audio sequence:', data.message);
|
console.error('Failed to get audio sequence:', data.message);
|
||||||
|
// Fallback to browser TTS
|
||||||
|
this.fallbackToBrowserTTS(`Nomor antrian ${queueNumber} untuk ${poliName}. Silakan menuju ke loket yang tersedia.`, () => {});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error playing TTS:', error);
|
console.error('Error playing TTS:', error);
|
||||||
|
// Fallback to browser TTS
|
||||||
|
this.fallbackToBrowserTTS(`Nomor antrian ${queueNumber} untuk ${poliName}. Silakan menuju ke loket yang tersedia.`, () => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,10 +85,15 @@
|
||||||
Route::post('/admin/antrian/store', [AdminController::class, 'storeAntrianAdmin'])->name('admin.antrian.store');
|
Route::post('/admin/antrian/store', [AdminController::class, 'storeAntrianAdmin'])->name('admin.antrian.store');
|
||||||
Route::get('/admin/antrian/{antrian}/cetak', [AdminController::class, 'cetakAntrian'])->name('admin.antrian.cetak');
|
Route::get('/admin/antrian/{antrian}/cetak', [AdminController::class, 'cetakAntrian'])->name('admin.antrian.cetak');
|
||||||
|
|
||||||
// TTS Routes
|
// Simple TTS Routes (Windows Compatible)
|
||||||
Route::post('/admin/tts/generate', [TTSController::class, 'generateQueueCall'])->name('admin.tts.generate');
|
Route::get('/admin/tts', [TTSController::class, 'index'])->name('admin.tts.index');
|
||||||
Route::post('/admin/tts/audio-sequence', [TTSController::class, 'getAudioSequence'])->name('admin.tts.audio-sequence');
|
Route::post('/admin/tts/generate', [TTSController::class, 'generateQueueTTS'])->name('admin.tts.generate');
|
||||||
Route::post('/admin/tts/play-sequence', [TTSController::class, 'playAudioSequence'])->name('admin.tts.play-sequence');
|
Route::post('/admin/tts/play', [TTSController::class, 'playTTS'])->name('admin.tts.play');
|
||||||
|
Route::get('/admin/tts/play', [TTSController::class, 'playTTS'])->name('admin.tts.play.get');
|
||||||
|
Route::get('/admin/tts/test', [TTSController::class, 'testTTS'])->name('admin.tts.test');
|
||||||
|
Route::get('/admin/tts/voices', [TTSController::class, 'getVoices'])->name('admin.tts.voices');
|
||||||
|
Route::post('/admin/tts/cleanup', [TTSController::class, 'cleanupFiles'])->name('admin.tts.cleanup');
|
||||||
|
Route::get('/admin/tts/status', [TTSController::class, 'getStatus'])->name('admin.tts.status');
|
||||||
|
|
||||||
// Indonesian TTS Routes
|
// Indonesian TTS Routes
|
||||||
Route::post('/admin/indonesian-tts/generate', [IndonesianTTSController::class, 'generateQueueCall'])->name('admin.indonesian-tts.generate');
|
Route::post('/admin/indonesian-tts/generate', [IndonesianTTSController::class, 'generateQueueCall'])->name('admin.indonesian-tts.generate');
|
||||||
|
@ -103,6 +108,12 @@
|
||||||
// Public TTS Routes (for display)
|
// Public TTS Routes (for display)
|
||||||
Route::post('/tts/play-sequence', [TTSController::class, 'playAudioSequence'])->name('tts.play-sequence');
|
Route::post('/tts/play-sequence', [TTSController::class, 'playAudioSequence'])->name('tts.play-sequence');
|
||||||
|
|
||||||
|
// Public TTS route for display page
|
||||||
|
Route::get('/tts/audio/{filename}', [TTSController::class, 'playPublicAudio'])->name('tts.audio.public');
|
||||||
|
|
||||||
|
// Test TTS route (public)
|
||||||
|
Route::get('/tts/test-public', [TTSController::class, 'testPublicTTS'])->name('tts.test.public');
|
||||||
|
|
||||||
// API Routes for display
|
// API Routes for display
|
||||||
Route::get('/api/check-new-calls', [DisplayController::class, 'checkNewCalls'])->name('api.check-new-calls');
|
Route::get('/api/check-new-calls', [DisplayController::class, 'checkNewCalls'])->name('api.check-new-calls');
|
||||||
Route::get('/api/display-data', [DisplayController::class, 'getDisplayData'])->name('api.display-data');
|
Route::get('/api/display-data', [DisplayController::class, 'getDisplayData'])->name('api.display-data');
|
||||||
|
|
Loading…
Reference in New Issue