delete liblary tts

This commit is contained in:
Vckynando12 2025-08-12 20:58:12 +07:00
parent 157cc6eed8
commit 2999084323
18 changed files with 75 additions and 3920 deletions

View File

@ -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.

View File

@ -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

View File

@ -12,7 +12,7 @@
use App\Models\Antrian; use App\Models\Antrian;
use App\Models\Poli; use App\Models\Poli;
use App\Models\RiwayatPanggilan; use App\Models\RiwayatPanggilan;
use App\Services\TTSService;
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;

View File

@ -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
]);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

117
composer.lock generated
View File

@ -289,33 +289,32 @@
}, },
{ {
"name": "doctrine/inflector", "name": "doctrine/inflector",
"version": "2.0.10", "version": "2.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/inflector.git", "url": "https://github.com/doctrine/inflector.git",
"reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2 || ^8.0" "php": "^7.2 || ^8.0"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^11.0", "doctrine/coding-standard": "^12.0 || ^13.0",
"phpstan/phpstan": "^1.8", "phpstan/phpstan": "^1.12 || ^2.0",
"phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
"phpstan/phpstan-strict-rules": "^1.3", "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
"phpunit/phpunit": "^8.5 || ^9.5", "phpunit/phpunit": "^8.5 || ^12.2"
"vimeo/psalm": "^4.25 || ^5.4"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Doctrine\\Inflector\\": "lib/Doctrine/Inflector" "Doctrine\\Inflector\\": "src"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@ -360,7 +359,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/inflector/issues", "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": [ "funding": [
{ {
@ -376,7 +375,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-02-18T20:23:39+00:00" "time": "2025-08-10T19:31:58+00:00"
}, },
{ {
"name": "doctrine/lexer", "name": "doctrine/lexer",
@ -1288,16 +1287,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v12.22.0", "version": "v12.22.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "6ab00c913ef6ec6fad0bd506f7452c0bb9e792c3" "reference": "d33ee45184126f32f593d4b809a846ed88a1dc43"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/6ab00c913ef6ec6fad0bd506f7452c0bb9e792c3", "url": "https://api.github.com/repos/laravel/framework/zipball/d33ee45184126f32f593d4b809a846ed88a1dc43",
"reference": "6ab00c913ef6ec6fad0bd506f7452c0bb9e792c3", "reference": "d33ee45184126f32f593d4b809a846ed88a1dc43",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1499,7 +1498,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "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", "name": "laravel/prompts",
@ -6470,16 +6469,16 @@
}, },
{ {
"name": "filp/whoops", "name": "filp/whoops",
"version": "2.18.3", "version": "2.18.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/filp/whoops.git", "url": "https://github.com/filp/whoops.git",
"reference": "59a123a3d459c5a23055802237cb317f609867e5" "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d",
"reference": "59a123a3d459c5a23055802237cb317f609867e5", "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6529,7 +6528,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/filp/whoops/issues", "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": [ "funding": [
{ {
@ -6537,7 +6536,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-06-16T00:02:10+00:00" "time": "2025-08-08T12:00:00+00:00"
}, },
{ {
"name": "hamcrest/hamcrest-php", "name": "hamcrest/hamcrest-php",
@ -7498,16 +7497,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.5.28", "version": "11.5.31",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "93f30aa3889e785ac63493d4976df0ae9fdecb60" "reference": "fc44414e0779e94640663b809557b0b599548260"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/93f30aa3889e785ac63493d4976df0ae9fdecb60", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc44414e0779e94640663b809557b0b599548260",
"reference": "93f30aa3889e785ac63493d4976df0ae9fdecb60", "reference": "fc44414e0779e94640663b809557b0b599548260",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7517,7 +7516,7 @@
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-xml": "*", "ext-xml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.13.3", "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4", "phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1", "phar-io/version": "^3.2.1",
"php": ">=8.2", "php": ">=8.2",
@ -7528,13 +7527,13 @@
"phpunit/php-timer": "^7.0.1", "phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2", "sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.3", "sebastian/code-unit": "^3.0.3",
"sebastian/comparator": "^6.3.1", "sebastian/comparator": "^6.3.2",
"sebastian/diff": "^6.0.2", "sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.1", "sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.0", "sebastian/exporter": "^6.3.0",
"sebastian/global-state": "^7.0.2", "sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1", "sebastian/object-enumerator": "^6.0.1",
"sebastian/type": "^5.1.2", "sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2", "sebastian/version": "^5.0.2",
"staabm/side-effects-detector": "^1.0.5" "staabm/side-effects-detector": "^1.0.5"
}, },
@ -7579,7 +7578,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "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": [ "funding": [
{ {
@ -7603,7 +7602,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-31T07:10:28+00:00" "time": "2025-08-11T05:27:39+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@ -7777,16 +7776,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "6.3.1", "version": "6.3.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
"reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7845,15 +7844,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "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": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "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", "name": "sebastian/complexity",
@ -8434,16 +8445,16 @@
}, },
{ {
"name": "sebastian/type", "name": "sebastian/type",
"version": "5.1.2", "version": "5.1.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/type.git", "url": "https://github.com/sebastianbergmann/type.git",
"reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
"reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8479,15 +8490,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/type/issues", "issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy", "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": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "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", "name": "sebastian/version",
@ -8724,12 +8747,12 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"
} }

View File

@ -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!"

View File

@ -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

View File

@ -99,31 +99,7 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.lapor
</a> </a>
</div> </div>
<!-- TTS Management -->
<div>
<a href="{{ route('admin.tts.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.tts.*') ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z">
</path>
</svg>
<span class="font-medium">TTS Management</span>
</a>
</div>
<!-- Indonesian TTS Management -->
<div>
<a href="{{ route('admin.indonesian-tts.index') }}"
class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.indonesian-tts.*') ? 'bg-green-50 text-green-700 border border-green-200' : 'text-gray-700 hover:bg-gray-50' }} transition duration-200">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z">
</path>
</svg>
<span class="font-medium">Indonesian TTS</span>
</a>
</div>

View File

@ -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

View File

@ -129,18 +129,7 @@ class="flex items-center px-4 py-3 rounded-xl {{ request()->routeIs('admin.lapor
</a> </a>
</div> </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> </div>
</nav> </nav>
</aside> </aside>

View File

@ -184,183 +184,9 @@ class="text-lg md:text-xl font-bold text-gray-900">{{ $antrian->no_antrian }}</s
@push('scripts') @push('scripts')
<script> <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 // Show SweetAlert2 for success messages
@if (session('success')) @if (session('success'))
@ -466,8 +292,7 @@ function checkForNewCalls() {
if (data.has_new_call && data.antrian) { if (data.has_new_call && data.antrian) {
console.log('New call detected:', 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 // Show notification
showNewCallNotification(data.antrian.poli_name, data.antrian.queue_number); showNewCallNotification(data.antrian.poli_name, data.antrian.queue_number);
@ -547,16 +372,7 @@ function updateNextQueue(containerId, nextQueues) {
// Update display data every 10 seconds // Update display data every 10 seconds
setInterval(updateDisplayData, 10000); 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> </script>
@endpush @endpush
@endsection @endsection

View File

@ -6,8 +6,7 @@
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\DisplayController; use App\Http\Controllers\DisplayController;
use App\Http\Controllers\AdminController; use App\Http\Controllers\AdminController;
use App\Http\Controllers\TTSController;
use App\Http\Controllers\IndonesianTTSController;
// Landing Page // Landing Page
Route::get('/', [LandingController::class, 'index'])->name('landing'); Route::get('/', [LandingController::class, 'index'])->name('landing');
@ -85,34 +84,10 @@
Route::post('/admin/antrian/store', [AdminController::class, 'storeAntrianAdmin'])->name('admin.antrian.store'); Route::post('/admin/antrian/store', [AdminController::class, 'storeAntrianAdmin'])->name('admin.antrian.store');
Route::get('/admin/antrian/{antrian}/cetak', [AdminController::class, 'cetakAntrian'])->name('admin.antrian.cetak'); Route::get('/admin/antrian/{antrian}/cetak', [AdminController::class, 'cetakAntrian'])->name('admin.antrian.cetak');
// 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 // API Routes for display
Route::get('/api/check-new-calls', [DisplayController::class, 'checkNewCalls'])->name('api.check-new-calls'); Route::get('/api/check-new-calls', [DisplayController::class, 'checkNewCalls'])->name('api.check-new-calls');

View File

@ -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";
?>

View File

@ -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";