delete liblary tts
This commit is contained in:
parent
157cc6eed8
commit
2999084323
|
@ -1,323 +0,0 @@
|
|||
# Indonesian TTS Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Repository ini mengintegrasikan [Indonesian TTS](https://github.com/Wikidepia/indonesian-tts) yang menggunakan Coqui TTS untuk menghasilkan suara Indonesia yang lebih natural dan akurat. Indonesian TTS ini cocok untuk sistem antrian Puskesmas karena:
|
||||
|
||||
- **Suara Natural**: Menggunakan model yang dilatih khusus untuk bahasa Indonesia
|
||||
- **Pengucapan Akurat**: Menggunakan g2p-id untuk konversi grapheme ke phoneme
|
||||
- **Multiple Speakers**: Tersedia 80+ speaker dengan berbagai karakteristik suara
|
||||
- **Offline Capability**: Dapat berjalan tanpa internet setelah model diinstall
|
||||
|
||||
## Fitur Utama
|
||||
|
||||
### 1. Indonesian Pronunciation
|
||||
- Konversi otomatis nomor antrian ke pengucapan Indonesia
|
||||
- Contoh: "U5" → "U Lima", "A10" → "A Sepuluh"
|
||||
- Mendukung angka 0-100 dengan pengucapan yang benar
|
||||
|
||||
### 2. Multiple TTS Options
|
||||
- **Indonesian TTS** (Prioritas): Menggunakan model Coqui TTS Indonesia
|
||||
- **Google TTS** (Fallback): Menggunakan Google Cloud TTS API
|
||||
- **Browser TTS** (Fallback): Menggunakan Web Speech API browser
|
||||
|
||||
### 3. Audio Sequence Management
|
||||
- Urutan audio: Attention Sound → TTS → Attention Sound
|
||||
- Queue management untuk mencegah overlap
|
||||
- Cross-page communication untuk admin → display
|
||||
|
||||
## Instalasi
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Python 3.8+**
|
||||
2. **pip** (Python package manager)
|
||||
3. **Laravel 8+** dengan PHP 8.0+
|
||||
|
||||
### Step 1: Install Coqui TTS
|
||||
|
||||
```bash
|
||||
# Install Coqui TTS
|
||||
pip install TTS
|
||||
|
||||
# Verify installation
|
||||
tts --version
|
||||
```
|
||||
|
||||
### Step 2: Download Model Files
|
||||
|
||||
1. Kunjungi [Indonesian TTS Releases](https://github.com/Wikidepia/indonesian-tts/releases)
|
||||
2. Download file:
|
||||
- `checkpoint.pth` (model file)
|
||||
- `config.json` (configuration file)
|
||||
3. Buat folder: `storage/app/tts/models/`
|
||||
4. Simpan file di folder tersebut
|
||||
|
||||
### Step 3: Install g2p-id (Optional)
|
||||
|
||||
```bash
|
||||
# Install g2p-id for better pronunciation
|
||||
pip install g2p-id
|
||||
|
||||
# Verify installation
|
||||
g2p-id --help
|
||||
```
|
||||
|
||||
### Step 4: Test Installation
|
||||
|
||||
```bash
|
||||
# Test TTS with sample text
|
||||
tts --text "Halo dunia" \
|
||||
--model_path storage/app/tts/models/checkpoint.pth \
|
||||
--config_path storage/app/tts/models/config.json \
|
||||
--speaker_idx wibowo \
|
||||
--out_path test.wav
|
||||
```
|
||||
|
||||
## Konfigurasi
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Tambahkan ke file `.env`:
|
||||
|
||||
```env
|
||||
# Indonesian TTS Configuration
|
||||
INDONESIAN_TTS_ENABLED=true
|
||||
INDONESIAN_TTS_MODEL_PATH=storage/app/tts/models/checkpoint.pth
|
||||
INDONESIAN_TTS_CONFIG_PATH=storage/app/tts/models/config.json
|
||||
INDONESIAN_TTS_DEFAULT_SPEAKER=wibowo
|
||||
|
||||
# Google TTS (Fallback)
|
||||
GOOGLE_TTS_API_KEY=your_google_tts_api_key
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
storage/
|
||||
├── app/
|
||||
│ └── tts/
|
||||
│ ├── models/
|
||||
│ │ ├── checkpoint.pth
|
||||
│ │ └── config.json
|
||||
│ └── g2p-id
|
||||
└── public/
|
||||
└── audio/
|
||||
└── queue_calls/
|
||||
└── indonesian_tts_*.wav
|
||||
```
|
||||
|
||||
## Penggunaan
|
||||
|
||||
### 1. Admin Panel
|
||||
|
||||
Akses halaman Indonesian TTS Settings:
|
||||
```
|
||||
/admin/indonesian-tts
|
||||
```
|
||||
|
||||
Fitur yang tersedia:
|
||||
- Status monitoring (Indonesian TTS, Coqui TTS, Model Files, Speakers)
|
||||
- Test TTS dengan text custom
|
||||
- Installation instructions
|
||||
- Download model files
|
||||
|
||||
### 2. API Endpoints
|
||||
|
||||
```php
|
||||
// Generate TTS audio
|
||||
POST /admin/indonesian-tts/generate
|
||||
{
|
||||
"poli_name": "Poli Umum",
|
||||
"queue_number": "U5"
|
||||
}
|
||||
|
||||
// Create complete audio sequence
|
||||
POST /admin/indonesian-tts/audio-sequence
|
||||
{
|
||||
"poli_name": "Poli Umum",
|
||||
"queue_number": "U5"
|
||||
}
|
||||
|
||||
// Check status
|
||||
GET /admin/indonesian-tts/status
|
||||
|
||||
// Test TTS
|
||||
POST /admin/indonesian-tts/test
|
||||
{
|
||||
"text": "Nomor antrian U Lima, silakan menuju ke Poli Umum"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Service Usage
|
||||
|
||||
```php
|
||||
use App\Services\IndonesianTTSService;
|
||||
|
||||
$ttsService = new IndonesianTTSService();
|
||||
|
||||
// Generate TTS
|
||||
$result = $ttsService->generateQueueCall('Poli Umum', 'U5');
|
||||
|
||||
// Create audio sequence
|
||||
$sequence = $ttsService->createCompleteAudioSequence('Poli Umum', 'U5');
|
||||
|
||||
// Check status
|
||||
$isAvailable = $ttsService->isIndonesianTTSAvailable();
|
||||
$speakers = $ttsService->getAvailableSpeakers();
|
||||
```
|
||||
|
||||
## Available Speakers
|
||||
|
||||
Model Indonesian TTS menyediakan 80+ speaker dengan karakteristik berbeda:
|
||||
|
||||
### Male Speakers
|
||||
- `wibowo`: Suara pria natural, cocok untuk announcement
|
||||
- `ardi`: Suara pria formal
|
||||
- `budi`: Suara pria ramah
|
||||
|
||||
### Female Speakers
|
||||
- `gadis`: Suara wanita natural, cocok untuk announcement
|
||||
- `sari`: Suara wanita formal
|
||||
- `rini`: Suara wanita ramah
|
||||
|
||||
### Regional Speakers
|
||||
- `javanese_*`: Speaker dengan aksen Jawa
|
||||
- `sundanese_*`: Speaker dengan aksen Sunda
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"tts command not found"**
|
||||
```bash
|
||||
# Reinstall TTS
|
||||
pip uninstall TTS
|
||||
pip install TTS
|
||||
```
|
||||
|
||||
2. **"Model files not found"**
|
||||
```bash
|
||||
# Check file permissions
|
||||
ls -la storage/app/tts/models/
|
||||
chmod 644 storage/app/tts/models/*
|
||||
```
|
||||
|
||||
3. **"Permission denied"**
|
||||
```bash
|
||||
# Fix directory permissions
|
||||
chmod -R 755 storage/app/tts/
|
||||
chown -R www-data:www-data storage/app/tts/
|
||||
```
|
||||
|
||||
4. **"Audio generation failed"**
|
||||
```bash
|
||||
# Check Python environment
|
||||
which python
|
||||
which tts
|
||||
|
||||
# Test with simple command
|
||||
tts --text "test" --model_path storage/app/tts/models/checkpoint.pth --config_path storage/app/tts/models/config.json --out_path test.wav
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Aktifkan debug mode di `.env`:
|
||||
|
||||
```env
|
||||
APP_DEBUG=true
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
Cek log Laravel:
|
||||
```bash
|
||||
tail -f storage/logs/laravel.log
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Model Caching
|
||||
```php
|
||||
// Cache model loading
|
||||
$ttsService = app(IndonesianTTSService::class);
|
||||
```
|
||||
|
||||
### 2. Audio File Management
|
||||
```bash
|
||||
# Clean old audio files (older than 7 days)
|
||||
find storage/app/public/audio/queue_calls/ -name "*.wav" -mtime +7 -delete
|
||||
```
|
||||
|
||||
### 3. Memory Management
|
||||
```php
|
||||
// Limit concurrent TTS processes
|
||||
$maxProcesses = 3;
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **File Permissions**: Pastikan model files tidak dapat diakses publik
|
||||
2. **Input Validation**: Validasi input text untuk mencegah injection
|
||||
3. **Rate Limiting**: Batasi jumlah request TTS per menit
|
||||
4. **Audio Sanitization**: Bersihkan nama file audio
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
```php
|
||||
// Check TTS health
|
||||
$health = [
|
||||
'indonesian_tts' => $ttsService->isIndonesianTTSAvailable(),
|
||||
'coqui_tts' => $ttsService->isCoquiTTSInstalled(),
|
||||
'model_files' => file_exists($modelPath),
|
||||
'speakers' => count($ttsService->getAvailableSpeakers())
|
||||
];
|
||||
```
|
||||
|
||||
### Metrics
|
||||
|
||||
- TTS generation success rate
|
||||
- Audio file size and duration
|
||||
- Speaker usage statistics
|
||||
- Error rates and types
|
||||
|
||||
## Migration from Google TTS
|
||||
|
||||
Jika ingin migrasi dari Google TTS ke Indonesian TTS:
|
||||
|
||||
1. **Backup existing TTS configuration**
|
||||
2. **Install Indonesian TTS** (ikuti guide di atas)
|
||||
3. **Update service configuration**:
|
||||
```php
|
||||
// In TTSService.php
|
||||
public function generateQueueCall($poliName, $queueNumber)
|
||||
{
|
||||
// Try Indonesian TTS first
|
||||
$indonesianTTS = new IndonesianTTSService();
|
||||
if ($indonesianTTS->isIndonesianTTSAvailable()) {
|
||||
return $indonesianTTS->generateQueueCall($poliName, $queueNumber);
|
||||
}
|
||||
|
||||
// Fallback to Google TTS
|
||||
return parent::generateQueueCall($poliName, $queueNumber);
|
||||
}
|
||||
```
|
||||
4. **Test thoroughly** dengan berbagai nomor antrian
|
||||
5. **Monitor performance** dan error rates
|
||||
|
||||
## Support
|
||||
|
||||
Untuk bantuan lebih lanjut:
|
||||
|
||||
1. **Documentation**: [Indonesian TTS GitHub](https://github.com/Wikidepia/indonesian-tts)
|
||||
2. **Issues**: Buat issue di repository project ini
|
||||
3. **Community**: Coqui TTS Discord/Forum
|
||||
|
||||
## License
|
||||
|
||||
Indonesian TTS model memiliki lisensi terpisah. Pastikan untuk membaca dan mematuhi lisensi yang berlaku sebelum menggunakan untuk tujuan komersial.
|
||||
|
||||
---
|
||||
|
||||
**Note**: Indonesian TTS memerlukan resource yang cukup (RAM, CPU) untuk berjalan optimal. Pastikan server memiliki spesifikasi yang memadai.
|
369
TTS_README.md
369
TTS_README.md
|
@ -1,369 +0,0 @@
|
|||
# 🎤 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
|
|
@ -12,7 +12,7 @@
|
|||
use App\Models\Antrian;
|
||||
use App\Models\Poli;
|
||||
use App\Models\RiwayatPanggilan;
|
||||
use App\Services\TTSService;
|
||||
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\IndonesianTTSService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class IndonesianTTSController extends Controller
|
||||
{
|
||||
private $indonesianTTSService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->indonesianTTSService = new IndonesianTTSService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Indonesian TTS settings page
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.indonesian-tts.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TTS audio for queue call
|
||||
*/
|
||||
public function generateQueueCall(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'poli_name' => 'required|string',
|
||||
'queue_number' => 'required|string'
|
||||
]);
|
||||
|
||||
$poliName = $request->input('poli_name');
|
||||
$queueNumber = $request->input('queue_number');
|
||||
|
||||
$result = $this->indonesianTTSService->generateQueueCall($poliName, $queueNumber);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create complete audio sequence for queue call
|
||||
*/
|
||||
public function createAudioSequence(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'poli_name' => 'required|string',
|
||||
'queue_number' => 'required|string'
|
||||
]);
|
||||
|
||||
$poliName = $request->input('poli_name');
|
||||
$queueNumber = $request->input('queue_number');
|
||||
|
||||
$audioSequence = $this->indonesianTTSService->createCompleteAudioSequence($poliName, $queueNumber);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'audio_sequence' => $audioSequence,
|
||||
'poli_name' => $poliName,
|
||||
'queue_number' => $queueNumber
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Indonesian TTS status
|
||||
*/
|
||||
public function checkStatus(): JsonResponse
|
||||
{
|
||||
$status = [
|
||||
'indonesian_tts_available' => $this->indonesianTTSService->isIndonesianTTSAvailable(),
|
||||
'coqui_tts_installed' => $this->indonesianTTSService->isCoquiTTSInstalled(),
|
||||
'model_files_exist' => file_exists(storage_path('app/tts/models/checkpoint.pth')) &&
|
||||
file_exists(storage_path('app/tts/models/config.json')),
|
||||
'available_speakers' => $this->indonesianTTSService->getAvailableSpeakers(),
|
||||
'installation_instructions' => $this->indonesianTTSService->installIndonesianTTS()
|
||||
];
|
||||
|
||||
return response()->json($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Indonesian TTS with sample text
|
||||
*/
|
||||
public function testTTS(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'text' => 'required|string|max:500'
|
||||
]);
|
||||
|
||||
$text = $request->input('text');
|
||||
|
||||
try {
|
||||
$result = $this->indonesianTTSService->generateWithIndonesianTTS($text, 'test', 'Test');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'TTS test berhasil',
|
||||
'audio_url' => $result['audio_url'] ?? null,
|
||||
'tts_type' => $result['tts_type'] ?? 'fallback'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'TTS test gagal: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installation instructions
|
||||
*/
|
||||
public function getInstallationInstructions(): JsonResponse
|
||||
{
|
||||
$instructions = $this->indonesianTTSService->installIndonesianTTS();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'instructions' => $instructions
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download Indonesian TTS model files
|
||||
*/
|
||||
public function downloadModelFiles(): JsonResponse
|
||||
{
|
||||
$modelUrls = [
|
||||
'checkpoint.pth' => 'https://github.com/Wikidepia/indonesian-tts/releases/download/v1.2/checkpoint.pth',
|
||||
'config.json' => 'https://github.com/Wikidepia/indonesian-tts/releases/download/v1.2/config.json'
|
||||
];
|
||||
|
||||
$downloadInfo = [
|
||||
'title' => 'Download Indonesian TTS Model Files',
|
||||
'description' => 'Download file model yang diperlukan untuk Indonesian TTS',
|
||||
'files' => $modelUrls,
|
||||
'manual_steps' => [
|
||||
'1. Kunjungi: https://github.com/Wikidepia/indonesian-tts/releases',
|
||||
'2. Download file checkpoint.pth dan config.json',
|
||||
'3. Buat folder: ' . storage_path('app/tts/models/'),
|
||||
'4. Simpan file di folder tersebut',
|
||||
'5. Pastikan permission file dapat dibaca oleh web server'
|
||||
]
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'download_info' => $downloadInfo
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\SimpleTTSService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TTSController extends Controller
|
||||
{
|
||||
protected $ttsService;
|
||||
|
||||
public function __construct(SimpleTTSService $ttsService)
|
||||
{
|
||||
$this->ttsService = $ttsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show TTS management page
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('admin.tts.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TTS untuk nomor antrian
|
||||
*/
|
||||
public function generateQueueTTS(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'queue_number' => 'required|string',
|
||||
'service_name' => 'nullable|string'
|
||||
]);
|
||||
|
||||
try {
|
||||
$queueNumber = $request->input('queue_number');
|
||||
$serviceName = $request->input('service_name', '');
|
||||
|
||||
// Generate TTS audio
|
||||
$audioPath = $this->ttsService->generateQueueNumberTTS($queueNumber, $serviceName);
|
||||
|
||||
if (!$audioPath) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal generate TTS audio'
|
||||
], 500);
|
||||
}
|
||||
|
||||
// Get file info
|
||||
$fileName = basename($audioPath);
|
||||
$fileSize = filesize($audioPath);
|
||||
$fileType = pathinfo($audioPath, PATHINFO_EXTENSION) === 'wav' ? 'audio/wav' : 'audio/mpeg';
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'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) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'poli_name' => 'required|string',
|
||||
'queue_number' => 'required|string'
|
||||
]);
|
||||
|
||||
$poliName = $request->input('poli_name');
|
||||
$queueNumber = $request->input('queue_number');
|
||||
|
||||
// Generate TTS audio using our new service
|
||||
$audioPath = $this->ttsService->generateQueueNumberTTS($queueNumber, $poliName);
|
||||
|
||||
if (!$audioPath || !file_exists($audioPath)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,385 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class IndonesianTTSService
|
||||
{
|
||||
private $modelPath;
|
||||
private $configPath;
|
||||
private $g2pPath;
|
||||
private $outputPath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Path untuk model dan konfigurasi Indonesian TTS
|
||||
$this->modelPath = storage_path('app/tts/models/checkpoint.pth');
|
||||
$this->configPath = storage_path('app/tts/models/config.json');
|
||||
$this->g2pPath = storage_path('app/tts/g2p-id');
|
||||
$this->outputPath = storage_path('app/public/audio/queue_calls');
|
||||
|
||||
// Buat direktori jika belum ada
|
||||
if (!file_exists($this->outputPath)) {
|
||||
mkdir($this->outputPath, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TTS audio using Indonesian TTS model
|
||||
*/
|
||||
public function generateQueueCall($poliName, $queueNumber)
|
||||
{
|
||||
try {
|
||||
// Convert queue number to Indonesian pronunciation
|
||||
$indonesianQueueNumber = $this->convertQueueNumberToIndonesian($queueNumber);
|
||||
|
||||
// Create the text to be spoken
|
||||
$text = "Nomor antrian {$indonesianQueueNumber}, silakan menuju ke {$poliName}";
|
||||
|
||||
// Check if Indonesian TTS model is available
|
||||
if ($this->isIndonesianTTSAvailable()) {
|
||||
return $this->generateWithIndonesianTTS($text, $queueNumber, $poliName);
|
||||
} else {
|
||||
// Fallback to Google TTS or browser TTS
|
||||
return $this->generateWithFallbackTTS($text);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error generating Indonesian TTS: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TTS using Indonesian TTS model
|
||||
*/
|
||||
public function generateWithIndonesianTTS($text, $queueNumber, $poliName)
|
||||
{
|
||||
try {
|
||||
// Generate filename
|
||||
$filename = "indonesian_tts_{$queueNumber}_{$poliName}_" . time() . ".wav";
|
||||
$filepath = $this->outputPath . '/' . $filename;
|
||||
|
||||
// Prepare text for TTS (convert to phonemes if g2p-id is available)
|
||||
$phonemeText = $this->convertToPhonemes($text);
|
||||
|
||||
// Run Coqui TTS command
|
||||
$command = $this->buildTTSCmd($phonemeText, $filepath);
|
||||
|
||||
$result = Process::run($command);
|
||||
|
||||
if ($result->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'audio_url' => asset('storage/audio/queue_calls/' . $filename),
|
||||
'filename' => $filename,
|
||||
'tts_type' => 'indonesian_tts'
|
||||
];
|
||||
} else {
|
||||
throw new \Exception('TTS generation failed: ' . $result->errorOutput());
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Fallback to other TTS methods
|
||||
return $this->generateWithFallbackTTS($text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build TTS command for Coqui TTS
|
||||
*/
|
||||
private function buildTTSCmd($text, $outputPath)
|
||||
{
|
||||
$speaker = 'wibowo'; // Default speaker, can be made configurable
|
||||
|
||||
return [
|
||||
'tts',
|
||||
'--text',
|
||||
$text,
|
||||
'--model_path',
|
||||
$this->modelPath,
|
||||
'--config_path',
|
||||
$this->configPath,
|
||||
'--speaker_idx',
|
||||
$speaker,
|
||||
'--out_path',
|
||||
$outputPath
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert text to phonemes using g2p-id
|
||||
*/
|
||||
private function convertToPhonemes($text)
|
||||
{
|
||||
// If g2p-id is available, use it to convert to phonemes
|
||||
if (file_exists($this->g2pPath)) {
|
||||
try {
|
||||
$result = Process::run([$this->g2pPath, $text]);
|
||||
if ($result->successful()) {
|
||||
return trim($result->output());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If g2p conversion fails, return original text
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
// Return original text if g2p-id is not available
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Indonesian TTS model is available
|
||||
*/
|
||||
public function isIndonesianTTSAvailable()
|
||||
{
|
||||
return file_exists($this->modelPath) &&
|
||||
file_exists($this->configPath) &&
|
||||
$this->isCoquiTTSInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Coqui TTS is installed
|
||||
*/
|
||||
public function isCoquiTTSInstalled()
|
||||
{
|
||||
try {
|
||||
$result = Process::run(['tts', '--version']);
|
||||
return $result->successful();
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback to Google TTS or browser TTS
|
||||
*/
|
||||
private function generateWithFallbackTTS($text)
|
||||
{
|
||||
// Use existing TTSService as fallback
|
||||
$ttsService = new TTSService();
|
||||
|
||||
// Extract poli name and queue number from text for fallback
|
||||
preg_match('/Nomor antrian (.+), silakan menuju ke ruang (.+)/', $text, $matches);
|
||||
$queueNumber = $matches[1] ?? '';
|
||||
$poliName = $matches[2] ?? '';
|
||||
|
||||
return $ttsService->generateQueueCall($poliName, $queueNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert alphanumeric queue number to Indonesian pronunciation
|
||||
* Example: "U5" becomes "U Lima", "A10" becomes "A Sepuluh"
|
||||
*/
|
||||
private function convertQueueNumberToIndonesian($queueNumber)
|
||||
{
|
||||
// Indonesian number words
|
||||
$indonesianNumbers = [
|
||||
'0' => 'Nol',
|
||||
'1' => 'Satu',
|
||||
'2' => 'Dua',
|
||||
'3' => 'Tiga',
|
||||
'4' => 'Empat',
|
||||
'5' => 'Lima',
|
||||
'6' => 'Enam',
|
||||
'7' => 'Tujuh',
|
||||
'8' => 'Delapan',
|
||||
'9' => 'Sembilan',
|
||||
'10' => 'Sepuluh',
|
||||
'11' => 'Sebelas',
|
||||
'12' => 'Dua Belas',
|
||||
'13' => 'Tiga Belas',
|
||||
'14' => 'Empat Belas',
|
||||
'15' => 'Lima Belas',
|
||||
'16' => 'Enam Belas',
|
||||
'17' => 'Tujuh Belas',
|
||||
'18' => 'Delapan Belas',
|
||||
'19' => 'Sembilan Belas',
|
||||
'20' => 'Dua Puluh',
|
||||
'30' => 'Tiga Puluh',
|
||||
'40' => 'Empat Puluh',
|
||||
'50' => 'Lima Puluh',
|
||||
'60' => 'Enam Puluh',
|
||||
'70' => 'Tujuh Puluh',
|
||||
'80' => 'Delapan Puluh',
|
||||
'90' => 'Sembilan Puluh',
|
||||
'100' => 'Seratus'
|
||||
];
|
||||
|
||||
// If it's a pure number, convert it
|
||||
if (is_numeric($queueNumber)) {
|
||||
$number = (int) $queueNumber;
|
||||
if (isset($indonesianNumbers[$number])) {
|
||||
return $indonesianNumbers[$number];
|
||||
} else {
|
||||
// For numbers > 100, build the pronunciation
|
||||
if ($number < 100) {
|
||||
$tens = floor($number / 10) * 10;
|
||||
$ones = $number % 10;
|
||||
if ($ones == 0) {
|
||||
return $indonesianNumbers[$tens];
|
||||
} else {
|
||||
return $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
|
||||
}
|
||||
} else {
|
||||
return $number; // Fallback for large numbers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For alphanumeric (like "U5", "A10"), convert the numeric part
|
||||
$letters = '';
|
||||
$numbers = '';
|
||||
|
||||
// Split into letters and numbers
|
||||
for ($i = 0; $i < strlen($queueNumber); $i++) {
|
||||
$char = $queueNumber[$i];
|
||||
if (is_numeric($char)) {
|
||||
$numbers .= $char;
|
||||
} else {
|
||||
$letters .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have both letters and numbers
|
||||
if ($letters && $numbers) {
|
||||
$numberValue = (int) $numbers;
|
||||
if (isset($indonesianNumbers[$numberValue])) {
|
||||
return $letters . ' ' . $indonesianNumbers[$numberValue];
|
||||
} else {
|
||||
// For numbers > 100, build the pronunciation
|
||||
if ($numberValue < 100) {
|
||||
$tens = floor($numberValue / 10) * 10;
|
||||
$ones = $numberValue % 10;
|
||||
if ($ones == 0) {
|
||||
return $letters . ' ' . $indonesianNumbers[$tens];
|
||||
} else {
|
||||
return $letters . ' ' . $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
|
||||
}
|
||||
} else {
|
||||
return $queueNumber; // Fallback for large numbers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no conversion needed, return as is
|
||||
return $queueNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create complete audio sequence for queue call
|
||||
*/
|
||||
public function createCompleteAudioSequence($poliName, $queueNumber)
|
||||
{
|
||||
$audioFiles = [];
|
||||
|
||||
// 1. Attention sound (4 seconds - actual file duration)
|
||||
$attentionSound = asset('assets/music/call-to-attention-123107.mp3');
|
||||
$audioFiles[] = [
|
||||
'type' => 'attention',
|
||||
'url' => $attentionSound,
|
||||
'duration' => 4000 // 4 seconds - actual file duration
|
||||
];
|
||||
|
||||
// 2. TTS for poli name and number (no final attention sound)
|
||||
$ttsResult = $this->generateQueueCall($poliName, $queueNumber);
|
||||
if ($ttsResult['success']) {
|
||||
if (isset($ttsResult['use_browser_tts']) && $ttsResult['use_browser_tts']) {
|
||||
// Use browser TTS
|
||||
$audioFiles[] = [
|
||||
'type' => 'browser_tts',
|
||||
'text' => $ttsResult['text'],
|
||||
'duration' => 8000 // 8 seconds - longer for natural speech
|
||||
];
|
||||
} else {
|
||||
// Use generated audio file
|
||||
$audioFiles[] = [
|
||||
'type' => 'tts',
|
||||
'url' => $ttsResult['audio_url'],
|
||||
'duration' => 8000 // 8 seconds - longer for natural speech
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $audioFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available speakers from Indonesian TTS model
|
||||
*/
|
||||
public function getAvailableSpeakers()
|
||||
{
|
||||
if (!$this->isIndonesianTTSAvailable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$result = Process::run([
|
||||
'tts',
|
||||
'--model_path',
|
||||
$this->modelPath,
|
||||
'--config_path',
|
||||
$this->configPath,
|
||||
'--list_speaker_idxs'
|
||||
]);
|
||||
|
||||
if ($result->successful()) {
|
||||
$output = $result->output();
|
||||
// Parse speaker list from output
|
||||
$speakers = [];
|
||||
$lines = explode("\n", $output);
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^(\d+):\s*(.+)$/', $line, $matches)) {
|
||||
$speakers[$matches[1]] = trim($matches[2]);
|
||||
}
|
||||
}
|
||||
return $speakers;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Return default speakers if listing fails
|
||||
return [
|
||||
'wibowo' => 'Wibowo (Male)',
|
||||
'ardi' => 'Ardi (Male)',
|
||||
'gadis' => 'Gadis (Female)'
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Indonesian TTS model
|
||||
*/
|
||||
public function installIndonesianTTS()
|
||||
{
|
||||
$instructions = [
|
||||
'title' => 'Instalasi Indonesian TTS',
|
||||
'steps' => [
|
||||
'1. Install Coqui TTS:',
|
||||
' pip install TTS',
|
||||
'',
|
||||
'2. Download model dari GitHub:',
|
||||
' - Kunjungi: https://github.com/Wikidepia/indonesian-tts/releases',
|
||||
' - Download file checkpoint.pth dan config.json',
|
||||
' - Simpan di: ' . storage_path('app/tts/models/'),
|
||||
'',
|
||||
'3. Install g2p-id (opsional):',
|
||||
' pip install g2p-id',
|
||||
'',
|
||||
'4. Test instalasi:',
|
||||
' tts --version',
|
||||
'',
|
||||
'5. Test model:',
|
||||
' tts --text "Halo dunia" --model_path ' . $this->modelPath . ' --config_path ' . $this->configPath . ' --out_path test.wav'
|
||||
]
|
||||
];
|
||||
|
||||
return $instructions;
|
||||
}
|
||||
}
|
|
@ -1,283 +0,0 @@
|
|||
<?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,243 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class TTSService
|
||||
{
|
||||
private $apiKey;
|
||||
private $baseUrl = 'https://texttospeech.googleapis.com/v1/text:synthesize';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.google.tts_api_key');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert alphanumeric queue number to Indonesian pronunciation
|
||||
* Example: "U5" becomes "U Lima", "A10" becomes "A Sepuluh"
|
||||
*/
|
||||
private function convertQueueNumberToIndonesian($queueNumber)
|
||||
{
|
||||
// Indonesian number words
|
||||
$indonesianNumbers = [
|
||||
'0' => 'Nol',
|
||||
'1' => 'Satu',
|
||||
'2' => 'Dua',
|
||||
'3' => 'Tiga',
|
||||
'4' => 'Empat',
|
||||
'5' => 'Lima',
|
||||
'6' => 'Enam',
|
||||
'7' => 'Tujuh',
|
||||
'8' => 'Delapan',
|
||||
'9' => 'Sembilan',
|
||||
'10' => 'Sepuluh',
|
||||
'11' => 'Sebelas',
|
||||
'12' => 'Dua Belas',
|
||||
'13' => 'Tiga Belas',
|
||||
'14' => 'Empat Belas',
|
||||
'15' => 'Lima Belas',
|
||||
'16' => 'Enam Belas',
|
||||
'17' => 'Tujuh Belas',
|
||||
'18' => 'Delapan Belas',
|
||||
'19' => 'Sembilan Belas',
|
||||
'20' => 'Dua Puluh',
|
||||
'30' => 'Tiga Puluh',
|
||||
'40' => 'Empat Puluh',
|
||||
'50' => 'Lima Puluh',
|
||||
'60' => 'Enam Puluh',
|
||||
'70' => 'Tujuh Puluh',
|
||||
'80' => 'Delapan Puluh',
|
||||
'90' => 'Sembilan Puluh',
|
||||
'100' => 'Seratus'
|
||||
];
|
||||
|
||||
// If it's a pure number, convert it
|
||||
if (is_numeric($queueNumber)) {
|
||||
$number = (int) $queueNumber;
|
||||
if (isset($indonesianNumbers[$number])) {
|
||||
return $indonesianNumbers[$number];
|
||||
} else {
|
||||
// For numbers > 100, build the pronunciation
|
||||
if ($number < 100) {
|
||||
$tens = floor($number / 10) * 10;
|
||||
$ones = $number % 10;
|
||||
if ($ones == 0) {
|
||||
return $indonesianNumbers[$tens];
|
||||
} else {
|
||||
return $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
|
||||
}
|
||||
} else {
|
||||
return $number; // Fallback for large numbers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For alphanumeric (like "U5", "A10"), convert the numeric part
|
||||
$letters = '';
|
||||
$numbers = '';
|
||||
|
||||
// Split into letters and numbers
|
||||
for ($i = 0; $i < strlen($queueNumber); $i++) {
|
||||
$char = $queueNumber[$i];
|
||||
if (is_numeric($char)) {
|
||||
$numbers .= $char;
|
||||
} else {
|
||||
$letters .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have both letters and numbers
|
||||
if ($letters && $numbers) {
|
||||
$numberValue = (int) $numbers;
|
||||
if (isset($indonesianNumbers[$numberValue])) {
|
||||
return $letters . ' ' . $indonesianNumbers[$numberValue];
|
||||
} else {
|
||||
// For numbers > 100, build the pronunciation
|
||||
if ($numberValue < 100) {
|
||||
$tens = floor($numberValue / 10) * 10;
|
||||
$ones = $numberValue % 10;
|
||||
if ($ones == 0) {
|
||||
return $letters . ' ' . $indonesianNumbers[$tens];
|
||||
} else {
|
||||
return $letters . ' ' . $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
|
||||
}
|
||||
} else {
|
||||
return $queueNumber; // Fallback for large numbers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no conversion needed, return as is
|
||||
return $queueNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TTS audio for queue call
|
||||
*/
|
||||
public function generateQueueCall($poliName, $queueNumber)
|
||||
{
|
||||
try {
|
||||
// Convert queue number to Indonesian pronunciation
|
||||
$indonesianQueueNumber = $this->convertQueueNumberToIndonesian($queueNumber);
|
||||
|
||||
// Create the text to be spoken
|
||||
$text = "antrian selanjutnya poli {$poliName} nomor {$indonesianQueueNumber}";
|
||||
|
||||
// Generate TTS audio
|
||||
$audioContent = $this->synthesizeSpeech($text);
|
||||
|
||||
if ($audioContent) {
|
||||
// Save audio file
|
||||
$filename = "queue_call_{$queueNumber}_{$poliName}_" . time() . ".mp3";
|
||||
$filepath = "audio/queue_calls/" . $filename;
|
||||
|
||||
Storage::disk('public')->put($filepath, base64_decode($audioContent));
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'audio_url' => asset('storage/' . $filepath),
|
||||
'filename' => $filename
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback: return browser TTS info
|
||||
return [
|
||||
'success' => true,
|
||||
'audio_url' => null,
|
||||
'filename' => null,
|
||||
'text' => $text,
|
||||
'use_browser_tts' => true
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error generating TTS: ' . $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize speech using Google TTS API
|
||||
*/
|
||||
private function synthesizeSpeech($text)
|
||||
{
|
||||
if (!$this->apiKey) {
|
||||
// Fallback: use browser's built-in TTS
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestData = [
|
||||
'input' => [
|
||||
'text' => $text
|
||||
],
|
||||
'voice' => [
|
||||
'languageCode' => 'id-ID',
|
||||
'name' => 'id-ID-Wavenet-A', // More natural and fluent Indonesian female voice
|
||||
'ssmlGender' => 'FEMALE'
|
||||
],
|
||||
'audioConfig' => [
|
||||
'audioEncoding' => 'MP3',
|
||||
'speakingRate' => 0.85, // Slightly faster for more natural flow
|
||||
'pitch' => 0,
|
||||
'volumeGainDb' => 0
|
||||
]
|
||||
];
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
])->post($this->baseUrl . '?key=' . $this->apiKey, $requestData);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
return $data['audioContent'] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create complete audio sequence for queue call
|
||||
*/
|
||||
public function createCompleteAudioSequence($poliName, $queueNumber)
|
||||
{
|
||||
$audioFiles = [];
|
||||
|
||||
// 1. Attention sound (4 seconds - actual file duration)
|
||||
$attentionSound = asset('assets/music/call-to-attention-123107.mp3');
|
||||
$audioFiles[] = [
|
||||
'type' => 'attention',
|
||||
'url' => $attentionSound,
|
||||
'duration' => 4000 // 4 seconds - actual file duration
|
||||
];
|
||||
|
||||
// 2. TTS for poli name and number (no final attention sound)
|
||||
$ttsResult = $this->generateQueueCall($poliName, $queueNumber);
|
||||
if ($ttsResult['success']) {
|
||||
if ($ttsResult['use_browser_tts']) {
|
||||
// Use browser TTS
|
||||
$audioFiles[] = [
|
||||
'type' => 'browser_tts',
|
||||
'text' => $ttsResult['text'],
|
||||
'duration' => 8000 // 8 seconds - longer for natural speech
|
||||
];
|
||||
} else {
|
||||
// Use generated audio file
|
||||
$audioFiles[] = [
|
||||
'type' => 'tts',
|
||||
'url' => $ttsResult['audio_url'],
|
||||
'duration' => 8000 // 8 seconds - longer for natural speech
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $audioFiles;
|
||||
}
|
||||
}
|
|
@ -289,33 +289,32 @@
|
|||
},
|
||||
{
|
||||
"name": "doctrine/inflector",
|
||||
"version": "2.0.10",
|
||||
"version": "2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/inflector.git",
|
||||
"reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
|
||||
"reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
|
||||
"reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
|
||||
"url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
|
||||
"reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^11.0",
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"phpstan/phpstan-phpunit": "^1.1",
|
||||
"phpstan/phpstan-strict-rules": "^1.3",
|
||||
"phpunit/phpunit": "^8.5 || ^9.5",
|
||||
"vimeo/psalm": "^4.25 || ^5.4"
|
||||
"doctrine/coding-standard": "^12.0 || ^13.0",
|
||||
"phpstan/phpstan": "^1.12 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.4 || ^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
|
||||
"phpunit/phpunit": "^8.5 || ^12.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
|
||||
"Doctrine\\Inflector\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
|
@ -360,7 +359,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/inflector/issues",
|
||||
"source": "https://github.com/doctrine/inflector/tree/2.0.10"
|
||||
"source": "https://github.com/doctrine/inflector/tree/2.1.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -376,7 +375,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-02-18T20:23:39+00:00"
|
||||
"time": "2025-08-10T19:31:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
|
@ -1288,16 +1287,16 @@
|
|||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v12.22.0",
|
||||
"version": "v12.22.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "6ab00c913ef6ec6fad0bd506f7452c0bb9e792c3"
|
||||
"reference": "d33ee45184126f32f593d4b809a846ed88a1dc43"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/6ab00c913ef6ec6fad0bd506f7452c0bb9e792c3",
|
||||
"reference": "6ab00c913ef6ec6fad0bd506f7452c0bb9e792c3",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/d33ee45184126f32f593d4b809a846ed88a1dc43",
|
||||
"reference": "d33ee45184126f32f593d4b809a846ed88a1dc43",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1499,7 +1498,7 @@
|
|||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-08-07T13:49:53+00:00"
|
||||
"time": "2025-08-08T13:58:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
|
@ -6470,16 +6469,16 @@
|
|||
},
|
||||
{
|
||||
"name": "filp/whoops",
|
||||
"version": "2.18.3",
|
||||
"version": "2.18.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filp/whoops.git",
|
||||
"reference": "59a123a3d459c5a23055802237cb317f609867e5"
|
||||
"reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5",
|
||||
"reference": "59a123a3d459c5a23055802237cb317f609867e5",
|
||||
"url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d",
|
||||
"reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -6529,7 +6528,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/filp/whoops/issues",
|
||||
"source": "https://github.com/filp/whoops/tree/2.18.3"
|
||||
"source": "https://github.com/filp/whoops/tree/2.18.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6537,7 +6536,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-06-16T00:02:10+00:00"
|
||||
"time": "2025-08-08T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "hamcrest/hamcrest-php",
|
||||
|
@ -7498,16 +7497,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.5.28",
|
||||
"version": "11.5.31",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "93f30aa3889e785ac63493d4976df0ae9fdecb60"
|
||||
"reference": "fc44414e0779e94640663b809557b0b599548260"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/93f30aa3889e785ac63493d4976df0ae9fdecb60",
|
||||
"reference": "93f30aa3889e785ac63493d4976df0ae9fdecb60",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc44414e0779e94640663b809557b0b599548260",
|
||||
"reference": "fc44414e0779e94640663b809557b0b599548260",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -7517,7 +7516,7 @@
|
|||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"myclabs/deep-copy": "^1.13.3",
|
||||
"myclabs/deep-copy": "^1.13.4",
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.2",
|
||||
|
@ -7528,13 +7527,13 @@
|
|||
"phpunit/php-timer": "^7.0.1",
|
||||
"sebastian/cli-parser": "^3.0.2",
|
||||
"sebastian/code-unit": "^3.0.3",
|
||||
"sebastian/comparator": "^6.3.1",
|
||||
"sebastian/comparator": "^6.3.2",
|
||||
"sebastian/diff": "^6.0.2",
|
||||
"sebastian/environment": "^7.2.1",
|
||||
"sebastian/exporter": "^6.3.0",
|
||||
"sebastian/global-state": "^7.0.2",
|
||||
"sebastian/object-enumerator": "^6.0.1",
|
||||
"sebastian/type": "^5.1.2",
|
||||
"sebastian/type": "^5.1.3",
|
||||
"sebastian/version": "^5.0.2",
|
||||
"staabm/side-effects-detector": "^1.0.5"
|
||||
},
|
||||
|
@ -7579,7 +7578,7 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.28"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.31"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -7603,7 +7602,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-07-31T07:10:28+00:00"
|
||||
"time": "2025-08-11T05:27:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
@ -7777,16 +7776,16 @@
|
|||
},
|
||||
{
|
||||
"name": "sebastian/comparator",
|
||||
"version": "6.3.1",
|
||||
"version": "6.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||
"reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959"
|
||||
"reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959",
|
||||
"reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
|
||||
"reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -7845,15 +7844,27 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1"
|
||||
"source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-07T06:57:01+00:00"
|
||||
"time": "2025-08-10T08:07:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/complexity",
|
||||
|
@ -8434,16 +8445,16 @@
|
|||
},
|
||||
{
|
||||
"name": "sebastian/type",
|
||||
"version": "5.1.2",
|
||||
"version": "5.1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/type.git",
|
||||
"reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e"
|
||||
"reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e",
|
||||
"reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
|
||||
"reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -8479,15 +8490,27 @@
|
|||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/type/issues",
|
||||
"security": "https://github.com/sebastianbergmann/type/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/type/tree/5.1.2"
|
||||
"source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/type",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-18T13:35:50+00:00"
|
||||
"time": "2025-08-09T06:55:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/version",
|
||||
|
@ -8724,12 +8747,12 @@
|
|||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
@ -1,267 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 🎤 Indonesian TTS Installation Script untuk Puskesmas
|
||||
# Script ini akan menginstall semua dependencies yang diperlukan untuk Indonesian TTS
|
||||
|
||||
echo "🚀 Memulai instalasi Indonesian TTS System..."
|
||||
echo "================================================"
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "❌ Jangan jalankan script ini sebagai root/sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Python version
|
||||
echo "🔍 Memeriksa Python..."
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_CMD="python3"
|
||||
echo "✅ Python3 ditemukan: $(python3 --version)"
|
||||
elif command -v python &> /dev/null; then
|
||||
PYTHON_CMD="python"
|
||||
echo "✅ Python ditemukan: $(python --version)"
|
||||
else
|
||||
echo "❌ Python tidak ditemukan. Silakan install Python 3.7+ terlebih dahulu."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check pip
|
||||
echo "🔍 Memeriksa pip..."
|
||||
if ! command -v pip3 &> /dev/null && ! command -v pip &> /dev/null; then
|
||||
echo "❌ pip tidak ditemukan. Silakan install pip terlebih dahulu."
|
||||
exit 1
|
||||
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
|
||||
echo "📁 Membuat direktori yang diperlukan..."
|
||||
mkdir -p storage/app/tts_scripts
|
||||
mkdir -p storage/app/tts/models
|
||||
mkdir -p storage/app/public/audio/queue_calls
|
||||
|
||||
# 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 Indonesian TTS models (if available)
|
||||
echo "📥 Downloading Indonesian TTS models..."
|
||||
MODEL_DIR="storage/app/tts/models"
|
||||
|
||||
# Check if models already exist
|
||||
if [ -f "$MODEL_DIR/checkpoint.pth" ] && [ -f "$MODEL_DIR/config.json" ]; then
|
||||
echo "✅ Model files sudah ada"
|
||||
else
|
||||
echo "⚠️ Model files belum ada. Silakan download manual dari:"
|
||||
echo " https://huggingface.co/coqui/Indonesian-TTS"
|
||||
echo " Atau gunakan script download terpisah"
|
||||
fi
|
||||
|
||||
# Create test script
|
||||
echo "🧪 Membuat test script..."
|
||||
cat > storage/app/tts_scripts/test_indonesian_tts.py << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script untuk Indonesian TTS
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def test_pyttsx3():
|
||||
try:
|
||||
import pyttsx3
|
||||
print("✅ pyttsx3: OK")
|
||||
return True
|
||||
except ImportError:
|
||||
print("❌ pyttsx3: NOT FOUND")
|
||||
return False
|
||||
|
||||
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
|
||||
echo "🧪 Testing installation..."
|
||||
$PYTHON_CMD storage/app/tts_scripts/test_indonesian_tts.py
|
||||
|
||||
# Create README for Indonesian TTS
|
||||
echo "📖 Membuat README Indonesian TTS..."
|
||||
cat > README_INDONESIAN_TTS.md << 'EOF'
|
||||
# 🎤 Indonesian TTS System untuk Puskesmas
|
||||
|
||||
## 📋 Overview
|
||||
Sistem Indonesian TTS menggunakan model Coqui TTS yang dioptimalkan untuk bahasa Indonesia.
|
||||
|
||||
## 🚀 Fitur
|
||||
- Model TTS khusus bahasa Indonesia
|
||||
- Pengucapan natural dan akurat
|
||||
- Support multiple speakers
|
||||
- Offline processing
|
||||
- High quality audio output
|
||||
|
||||
## 📁 File Structure
|
||||
```
|
||||
storage/app/tts/
|
||||
├── models/
|
||||
│ ├── checkpoint.pth # Model checkpoint
|
||||
│ └── config.json # Model configuration
|
||||
└── scripts/
|
||||
├── test_indonesian_tts.py
|
||||
└── indonesian_tts_generator.py
|
||||
```
|
||||
|
||||
## 🔧 Usage
|
||||
```bash
|
||||
# Test dependencies
|
||||
python storage/app/tts_scripts/test_indonesian_tts.py
|
||||
|
||||
# Generate TTS
|
||||
python storage/app/tts_scripts/indonesian_tts_generator.py "Nomor antrian 001" output.wav
|
||||
```
|
||||
|
||||
## 📥 Model Download
|
||||
Download model files dari: https://huggingface.co/coqui/Indonesian-TTS
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
- Pastikan Python 3.7+ terinstall
|
||||
- Pastikan semua dependencies terinstall
|
||||
- Pastikan model files ada di direktori yang benar
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "🎉 Instalasi Indonesian TTS selesai!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
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 "📖 Dokumentasi lengkap ada di: README_INDONESIAN_TTS.md"
|
||||
echo ""
|
||||
echo "🚀 Selamat menggunakan Indonesian TTS System!"
|
|
@ -1,450 +0,0 @@
|
|||
@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,31 +99,7 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.lapor
|
|||
</a>
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,510 +0,0 @@
|
|||
@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
|
|
@ -129,18 +129,7 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.lapor
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Indonesian TTS -->
|
||||
<div>
|
||||
<a href="{{ route('admin.indonesian-tts.index') }}"
|
||||
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.indonesian-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">Indonesian TTS</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
|
|
@ -184,183 +184,9 @@ class="text-lg md:text-xl font-bold text-gray-900">{{ $antrian->no_antrian }}</s
|
|||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// TTS Audio Player
|
||||
class TTSAudioPlayer {
|
||||
constructor() {
|
||||
this.audioQueue = [];
|
||||
this.isPlaying = false;
|
||||
this.currentAudio = null;
|
||||
}
|
||||
|
||||
// Play complete audio sequence
|
||||
async playAudioSequence(audioSequence) {
|
||||
if (this.isPlaying) {
|
||||
console.log('Audio already playing, queueing...');
|
||||
this.audioQueue.push(audioSequence);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPlaying = true;
|
||||
|
||||
for (let i = 0; i < audioSequence.length; i++) {
|
||||
const audioItem = audioSequence[i];
|
||||
await this.playAudioItem(audioItem);
|
||||
|
||||
// Wait between audio items (1 second gap)
|
||||
if (i < audioSequence.length - 1) {
|
||||
await this.delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
this.isPlaying = false;
|
||||
|
||||
// Play next in queue if available
|
||||
if (this.audioQueue.length > 0) {
|
||||
const nextSequence = this.audioQueue.shift();
|
||||
this.playAudioSequence(nextSequence);
|
||||
}
|
||||
}
|
||||
|
||||
// Play single audio item
|
||||
playAudioItem(audioItem) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (audioItem.type === 'browser_tts') {
|
||||
// Use browser TTS
|
||||
if ('speechSynthesis' in window) {
|
||||
const utterance = new SpeechSynthesisUtterance(audioItem.text);
|
||||
utterance.lang = 'id-ID';
|
||||
utterance.rate = 0.85; // Slightly faster for more natural flow
|
||||
utterance.volume = 1.0;
|
||||
|
||||
// Try to select a female Indonesian voice if available
|
||||
const voices = speechSynthesis.getVoices();
|
||||
const indonesianVoice = voices.find(voice =>
|
||||
voice.lang === 'id-ID' &&
|
||||
voice.name.toLowerCase().includes('female')
|
||||
) || voices.find(voice => voice.lang === 'id-ID');
|
||||
|
||||
if (indonesianVoice) {
|
||||
utterance.voice = indonesianVoice;
|
||||
}
|
||||
|
||||
utterance.addEventListener('end', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
utterance.addEventListener('error', (error) => {
|
||||
console.error('TTS error:', error);
|
||||
resolve();
|
||||
});
|
||||
|
||||
speechSynthesis.speak(utterance);
|
||||
|
||||
// Fallback timeout
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, audioItem.duration || 8000);
|
||||
} else {
|
||||
console.warn('Speech synthesis not supported');
|
||||
resolve();
|
||||
}
|
||||
} else if (audioItem.type === 'audio_file') {
|
||||
// Play audio file from our TTS system
|
||||
const audio = new Audio(audioItem.url);
|
||||
|
||||
audio.addEventListener('loadeddata', () => {
|
||||
console.log('Audio loaded, playing...');
|
||||
audio.play().catch(error => {
|
||||
console.error('Audio play error:', error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
console.log('Audio ended');
|
||||
resolve();
|
||||
});
|
||||
|
||||
audio.addEventListener('error', (error) => {
|
||||
console.error('Audio playback error:', error);
|
||||
// Fallback to browser TTS if audio file fails
|
||||
this.fallbackToBrowserTTS(audioItem.text, resolve);
|
||||
});
|
||||
|
||||
// Fallback timeout
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 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
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Play TTS for queue call
|
||||
async playQueueCall(poliName, queueNumber) {
|
||||
try {
|
||||
console.log('Playing TTS for:', poliName, queueNumber);
|
||||
|
||||
const response = await fetch('{{ route('tts.play-sequence') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
|
||||
},
|
||||
body: JSON.stringify({
|
||||
poli_name: poliName,
|
||||
queue_number: queueNumber
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.audio_sequence) {
|
||||
console.log('Audio sequence received:', data.audio_sequence);
|
||||
await this.playAudioSequence(data.audio_sequence);
|
||||
} else {
|
||||
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) {
|
||||
console.error('Error playing TTS:', error);
|
||||
// Fallback to browser TTS
|
||||
this.fallbackToBrowserTTS(`Nomor antrian ${queueNumber} untuk ${poliName}. Silakan menuju ke loket yang tersedia.`, () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize TTS Audio Player
|
||||
const ttsPlayer = new TTSAudioPlayer();
|
||||
|
||||
// Show SweetAlert2 for success messages
|
||||
@if (session('success'))
|
||||
|
@ -466,8 +292,7 @@ function checkForNewCalls() {
|
|||
if (data.has_new_call && data.antrian) {
|
||||
console.log('New call detected:', data.antrian);
|
||||
|
||||
// Play TTS for the called queue
|
||||
ttsPlayer.playQueueCall(data.antrian.poli_name, data.antrian.queue_number);
|
||||
|
||||
|
||||
// Show notification
|
||||
showNewCallNotification(data.antrian.poli_name, data.antrian.queue_number);
|
||||
|
@ -547,16 +372,7 @@ function updateNextQueue(containerId, nextQueues) {
|
|||
// Update display data every 10 seconds
|
||||
setInterval(updateDisplayData, 10000);
|
||||
|
||||
// Fallback: Listen for TTS events from admin panel (if broadcast not available)
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'TTS_CALL') {
|
||||
const {
|
||||
poliName,
|
||||
queueNumber
|
||||
} = event.data;
|
||||
ttsPlayer.playQueueCall(poliName, queueNumber);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\DisplayController;
|
||||
use App\Http\Controllers\AdminController;
|
||||
use App\Http\Controllers\TTSController;
|
||||
use App\Http\Controllers\IndonesianTTSController;
|
||||
|
||||
|
||||
// Landing Page
|
||||
Route::get('/', [LandingController::class, 'index'])->name('landing');
|
||||
|
@ -85,34 +84,10 @@
|
|||
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');
|
||||
|
||||
// Simple TTS Routes (Windows Compatible)
|
||||
Route::get('/admin/tts', [TTSController::class, 'index'])->name('admin.tts.index');
|
||||
Route::post('/admin/tts/generate', [TTSController::class, 'generateQueueTTS'])->name('admin.tts.generate');
|
||||
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
|
||||
Route::post('/admin/indonesian-tts/generate', [IndonesianTTSController::class, 'generateQueueCall'])->name('admin.indonesian-tts.generate');
|
||||
Route::post('/admin/indonesian-tts/audio-sequence', [IndonesianTTSController::class, 'createAudioSequence'])->name('admin.indonesian-tts.audio-sequence');
|
||||
Route::get('/admin/indonesian-tts/status', [IndonesianTTSController::class, 'checkStatus'])->name('admin.indonesian-tts.status');
|
||||
Route::post('/admin/indonesian-tts/test', [IndonesianTTSController::class, 'testTTS'])->name('admin.indonesian-tts.test');
|
||||
Route::get('/admin/indonesian-tts/install', [IndonesianTTSController::class, 'getInstallationInstructions'])->name('admin.indonesian-tts.install');
|
||||
Route::get('/admin/indonesian-tts/download', [IndonesianTTSController::class, 'downloadModelFiles'])->name('admin.indonesian-tts.download');
|
||||
Route::get('/admin/indonesian-tts', [IndonesianTTSController::class, 'index'])->name('admin.indonesian-tts.index');
|
||||
});
|
||||
|
||||
// Public TTS Routes (for display)
|
||||
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
|
||||
Route::get('/api/check-new-calls', [DisplayController::class, 'checkNewCalls'])->name('api.check-new-calls');
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Test Audio Sequence - Puskesmas TTS System
|
||||
*
|
||||
* File ini untuk testing audio sequence yang sudah disesuaikan:
|
||||
* 1. Attention Sound (4 detik)
|
||||
* 2. Jeda (1 detik)
|
||||
* 3. TTS (8 detik)
|
||||
* 4. Selesai
|
||||
*
|
||||
* Total: 13 detik
|
||||
*/
|
||||
|
||||
// Test function untuk memverifikasi timing
|
||||
function testAudioSequence()
|
||||
{
|
||||
echo "=== TEST AUDIO SEQUENCE ===\n";
|
||||
echo "Urutan yang diharapkan:\n";
|
||||
echo "1. Attention Sound: 4 detik\n";
|
||||
echo "2. Jeda: 1 detik\n";
|
||||
echo "3. TTS: 8 detik\n";
|
||||
echo "4. Selesai\n";
|
||||
echo "Total: 13 detik\n\n";
|
||||
|
||||
echo "Status: Audio sequence sudah disesuaikan\n";
|
||||
echo "- Attention sound kedua dihilangkan\n";
|
||||
echo "- TTS durasi 8 detik untuk memastikan selesai\n";
|
||||
echo "- Jeda antar audio 1 detik\n\n";
|
||||
|
||||
echo "Untuk test:\n";
|
||||
echo "1. Buka halaman admin\n";
|
||||
echo "2. Klik tombol 'Panggil' pada antrian\n";
|
||||
echo "3. Dengarkan di halaman display\n";
|
||||
echo "4. Pastikan urutan: Attention → Jeda → TTS → Selesai\n";
|
||||
}
|
||||
|
||||
// Jalankan test
|
||||
testAudioSequence();
|
||||
|
||||
echo "\n=== DETAIL PERUBAHAN ===\n";
|
||||
echo "File yang diubah:\n";
|
||||
echo "- app/Services/TTSService.php\n";
|
||||
echo "- app/Services/IndonesianTTSService.php\n";
|
||||
echo "- resources/views/display/index.blade.php\n";
|
||||
echo "- AUDIO_TIMING_GUIDE.md\n\n";
|
||||
|
||||
echo "Perubahan utama:\n";
|
||||
echo "1. Menghilangkan attention sound kedua\n";
|
||||
echo "2. Menambah durasi TTS dari 6 detik menjadi 8 detik\n";
|
||||
echo "3. Menambah jeda antar audio dari 500ms menjadi 1000ms\n";
|
||||
echo "4. Update dokumentasi timing\n";
|
||||
|
||||
?>
|
|
@ -1,169 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Test Indonesian Pronunciation Conversion
|
||||
*
|
||||
* This file tests the Indonesian pronunciation conversion logic
|
||||
* that converts alphanumeric queue numbers to Indonesian pronunciation.
|
||||
*/
|
||||
|
||||
// Indonesian number words (same as in TTSService)
|
||||
$indonesianNumbers = [
|
||||
'0' => 'Nol',
|
||||
'1' => 'Satu',
|
||||
'2' => 'Dua',
|
||||
'3' => 'Tiga',
|
||||
'4' => 'Empat',
|
||||
'5' => 'Lima',
|
||||
'6' => 'Enam',
|
||||
'7' => 'Tujuh',
|
||||
'8' => 'Delapan',
|
||||
'9' => 'Sembilan',
|
||||
'10' => 'Sepuluh',
|
||||
'11' => 'Sebelas',
|
||||
'12' => 'Dua Belas',
|
||||
'13' => 'Tiga Belas',
|
||||
'14' => 'Empat Belas',
|
||||
'15' => 'Lima Belas',
|
||||
'16' => 'Enam Belas',
|
||||
'17' => 'Tujuh Belas',
|
||||
'18' => 'Delapan Belas',
|
||||
'19' => 'Sembilan Belas',
|
||||
'20' => 'Dua Puluh',
|
||||
'30' => 'Tiga Puluh',
|
||||
'40' => 'Empat Puluh',
|
||||
'50' => 'Lima Puluh',
|
||||
'60' => 'Enam Puluh',
|
||||
'70' => 'Tujuh Puluh',
|
||||
'80' => 'Delapan Puluh',
|
||||
'90' => 'Sembilan Puluh',
|
||||
'100' => 'Seratus'
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert alphanumeric queue number to Indonesian pronunciation
|
||||
* Example: "U5" becomes "U Lima", "A10" becomes "A Sepuluh"
|
||||
*/
|
||||
function convertQueueNumberToIndonesian($queueNumber) {
|
||||
global $indonesianNumbers;
|
||||
|
||||
// If it's a pure number, convert it
|
||||
if (is_numeric($queueNumber)) {
|
||||
$number = (int)$queueNumber;
|
||||
if (isset($indonesianNumbers[$number])) {
|
||||
return $indonesianNumbers[$number];
|
||||
} else {
|
||||
// For numbers > 100, build the pronunciation
|
||||
if ($number < 100) {
|
||||
$tens = floor($number / 10) * 10;
|
||||
$ones = $number % 10;
|
||||
if ($ones == 0) {
|
||||
return $indonesianNumbers[$tens];
|
||||
} else {
|
||||
return $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
|
||||
}
|
||||
} else {
|
||||
return $number; // Fallback for large numbers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For alphanumeric (like "U5", "A10"), convert the numeric part
|
||||
$letters = '';
|
||||
$numbers = '';
|
||||
|
||||
// Split into letters and numbers
|
||||
for ($i = 0; $i < strlen($queueNumber); $i++) {
|
||||
$char = $queueNumber[$i];
|
||||
if (is_numeric($char)) {
|
||||
$numbers .= $char;
|
||||
} else {
|
||||
$letters .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have both letters and numbers
|
||||
if ($letters && $numbers) {
|
||||
$numberValue = (int)$numbers;
|
||||
if (isset($indonesianNumbers[$numberValue])) {
|
||||
return $letters . ' ' . $indonesianNumbers[$numberValue];
|
||||
} else {
|
||||
// For numbers > 100, build the pronunciation
|
||||
if ($numberValue < 100) {
|
||||
$tens = floor($numberValue / 10) * 10;
|
||||
$ones = $numberValue % 10;
|
||||
if ($ones == 0) {
|
||||
return $letters . ' ' . $indonesianNumbers[$tens];
|
||||
} else {
|
||||
return $letters . ' ' . $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
|
||||
}
|
||||
} else {
|
||||
return $queueNumber; // Fallback for large numbers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no conversion needed, return as is
|
||||
return $queueNumber;
|
||||
}
|
||||
|
||||
// Test cases
|
||||
echo "=== Indonesian Pronunciation Conversion Test ===\n\n";
|
||||
|
||||
$testCases = [
|
||||
// Pure numbers
|
||||
'1' => 'Satu',
|
||||
'5' => 'Lima',
|
||||
'10' => 'Sepuluh',
|
||||
'15' => 'Lima Belas',
|
||||
'25' => 'Dua Puluh Lima',
|
||||
'100' => 'Seratus',
|
||||
|
||||
// Alphanumeric queue numbers
|
||||
'U5' => 'U Lima',
|
||||
'A10' => 'A Sepuluh',
|
||||
'B15' => 'B Lima Belas',
|
||||
'C25' => 'C Dua Puluh Lima',
|
||||
'D100' => 'D Seratus',
|
||||
|
||||
// Edge cases
|
||||
'ABC123' => 'ABC Seratus Dua Puluh Tiga',
|
||||
'X1' => 'X Satu',
|
||||
'Y0' => 'Y Nol',
|
||||
'Z99' => 'Z Sembilan Puluh Sembilan',
|
||||
|
||||
// Non-alphanumeric (should remain unchanged)
|
||||
'POLI-UMUM' => 'POLI-UMUM',
|
||||
'ANTRIAN-1' => 'ANTRIAN-1', // Mixed with dash
|
||||
];
|
||||
|
||||
echo "Test Results:\n";
|
||||
echo str_repeat('-', 50) . "\n";
|
||||
|
||||
foreach ($testCases as $input => $expected) {
|
||||
$result = convertQueueNumberToIndonesian($input);
|
||||
$status = ($result === $expected) ? '✓ PASS' : '✗ FAIL';
|
||||
echo sprintf("%-15s → %-25s [%s]\n", $input, $result, $status);
|
||||
|
||||
if ($result !== $expected) {
|
||||
echo " Expected: $expected\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n" . str_repeat('-', 50) . "\n";
|
||||
echo "Test completed!\n\n";
|
||||
|
||||
// Additional examples for user verification
|
||||
echo "=== Examples for User Verification ===\n";
|
||||
echo "Queue Number → Indonesian Pronunciation\n";
|
||||
echo str_repeat('-', 40) . "\n";
|
||||
|
||||
$examples = ['U5', 'A10', 'B15', 'C25', 'D100', 'X1', 'Y0', 'Z99'];
|
||||
|
||||
foreach ($examples as $example) {
|
||||
$pronunciation = convertQueueNumberToIndonesian($example);
|
||||
echo "$example → $pronunciation\n";
|
||||
}
|
||||
|
||||
echo "\nThese examples show how alphanumeric queue numbers\n";
|
||||
echo "are converted to Indonesian pronunciation for TTS.\n";
|
||||
echo "For example, 'U5' becomes 'U Lima' instead of 'U five'.\n";
|
Loading…
Reference in New Issue