From 879ee41c7689d44a9a68885877a2b3e4f5535c4e Mon Sep 17 00:00:00 2001 From: Endyfadlullah Date: Mon, 11 Aug 2025 02:17:15 +0700 Subject: [PATCH] install TTS dan fix timing pada waktu pemanggilan pada laporan, menambahkan fitur tambahkan user pada admin, kemudian handle ambil antran berulang pada user --- README_INDONESIAN_TTS.md | 323 +++++++++++ README_TTS.md | 194 ------- README_TTS_VOICES.md | 166 ------ TTS_CONFIG.md | 63 -- app/Events/AntrianDipanggil.php | 51 ++ app/Http/Controllers/AdminController.php | 62 +- app/Http/Controllers/DashboardController.php | 51 +- app/Http/Controllers/DisplayController.php | 96 ++++ .../Controllers/IndonesianTTSController.php | 154 +++++ app/Http/Controllers/TTSController.php | 13 +- app/Models/Antrian.php | 18 + app/Services/IndonesianTTSService.php | 385 +++++++++++++ app/Services/TTSService.php | 121 +++- composer.json | 3 +- composer.lock | 154 ++++- .../2025_08_10_160027_create_jobs_table.php | 32 ++ install_indonesian_tts.sh | 237 ++++++++ resources/views/admin/dashboard.blade.php | 119 +++- .../admin/indonesian-tts/index.blade.php | 536 ++++++++++++++++++ resources/views/admin/laporan/index.blade.php | 174 +++++- resources/views/admin/poli/index.blade.php | 290 +++++++++- resources/views/admin/users/create.blade.php | 329 +++++++++++ resources/views/admin/users/index.blade.php | 125 +++- resources/views/dashboard/index.blade.php | 314 ++++++++-- resources/views/display/index.blade.php | 116 +++- routes/web.php | 16 + test_audio_sequence.php | 54 ++ test_indonesian_pronunciation.php | 169 ++++++ test_riwayat.php | 1 - test_tts.php | 35 -- test_tts_simple.php | 53 -- test_voice_config.php | 80 --- 32 files changed, 3801 insertions(+), 733 deletions(-) create mode 100644 README_INDONESIAN_TTS.md delete mode 100644 README_TTS.md delete mode 100644 README_TTS_VOICES.md delete mode 100644 TTS_CONFIG.md create mode 100644 app/Events/AntrianDipanggil.php create mode 100644 app/Http/Controllers/IndonesianTTSController.php create mode 100644 app/Services/IndonesianTTSService.php create mode 100644 database/migrations/2025_08_10_160027_create_jobs_table.php create mode 100644 install_indonesian_tts.sh create mode 100644 resources/views/admin/indonesian-tts/index.blade.php create mode 100644 resources/views/admin/users/create.blade.php create mode 100644 test_audio_sequence.php create mode 100644 test_indonesian_pronunciation.php delete mode 100644 test_riwayat.php delete mode 100644 test_tts.php delete mode 100644 test_tts_simple.php delete mode 100644 test_voice_config.php diff --git a/README_INDONESIAN_TTS.md b/README_INDONESIAN_TTS.md new file mode 100644 index 0000000..e2f9672 --- /dev/null +++ b/README_INDONESIAN_TTS.md @@ -0,0 +1,323 @@ +# 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. diff --git a/README_TTS.md b/README_TTS.md deleted file mode 100644 index 66db1ac..0000000 --- a/README_TTS.md +++ /dev/null @@ -1,194 +0,0 @@ -# Fitur TTS (Text-to-Speech) untuk Sistem Antrian Puskesmas - -## Overview - -Fitur TTS telah ditambahkan ke sistem antrian puskesmas untuk memanggil antrian secara otomatis dengan suara. Ketika admin menekan tombol "Panggil", sistem akan memainkan urutan audio berikut: - -1. **Attention Sound** - Bunyi perhatian -2. **TTS Announcement** - "Nomor antrian X, silakan menuju ke [nama poli]" -3. **Attention Sound** - Bunyi perhatian lagi - -## Cara Kerja - -### 1. Admin Panel - -- Admin membuka halaman poli (Umum, Gigi, Jiwa, Tradisional) -- Klik tombol "Panggil" pada antrian yang menunggu -- Sistem akan: - - Update status antrian menjadi "dipanggil" - - Generate audio TTS sequence - - Kirim pesan ke halaman display (jika terbuka) - - Mainkan audio di browser admin sebagai fallback - -### 2. Display Page - -- Halaman display akan menerima pesan TTS -- Audio sequence akan diputar secara otomatis -- Urutan: Attention Sound → TTS → Attention Sound - -## Komponen yang Ditambahkan - -### 1. Service - -- `app/Services/TTSService.php` - Service untuk mengelola TTS - -### 2. Controller - -- `app/Http/Controllers/TTSController.php` - Controller untuk endpoint TTS - -### 3. Routes - -```php -// Admin TTS Routes -Route::post('/admin/tts/generate', [TTSController::class, 'generateQueueCall']); -Route::post('/admin/tts/audio-sequence', [TTSController::class, 'getAudioSequence']); -Route::post('/admin/tts/play-sequence', [TTSController::class, 'playAudioSequence']); - -// Public TTS Routes -Route::post('/tts/play-sequence', [TTSController::class, 'playAudioSequence']); -``` - -### 4. JavaScript - -- TTS Audio Player class di halaman display -- Browser TTS fallback di halaman admin - -## Konfigurasi - -### 1. Google TTS API (Opsional) - -Untuk kualitas TTS yang lebih baik, Anda bisa menggunakan Google TTS API: - -1. Dapatkan API Key dari Google Cloud Console -2. Tambahkan ke file `.env`: - -```env -GOOGLE_TTS_API_KEY=your_api_key_here -``` - -### 2. Browser TTS (Default) - -Jika Google TTS API tidak tersedia, sistem akan menggunakan browser's built-in Speech Synthesis API. - -## Testing - -### 1. Tanpa Google TTS API - -1. Biarkan `GOOGLE_TTS_API_KEY` kosong di `.env` -2. Sistem akan menggunakan browser TTS -3. Buka halaman admin dan klik "Panggil" -4. Audio akan diputar di browser - -### 2. Dengan Google TTS API - -1. Set `GOOGLE_TTS_API_KEY` di `.env` -2. Sistem akan generate file audio MP3 -3. File disimpan di `public/storage/audio/queue_calls/` - -## File Audio - -### 1. Attention Sound - -- Lokasi: `public/assets/music/call-to-attention-123107.mp3` -- Digunakan di awal dan akhir sequence - -### 2. TTS Audio Files - -- Lokasi: `public/storage/audio/queue_calls/` -- Format: `queue_call_{number}_{poli}_{timestamp}.mp3` -- Dibuat otomatis saat menggunakan Google TTS API - -## Troubleshooting - -### 1. Audio tidak diputar - -- Periksa console browser untuk error -- Pastikan file attention sound ada -- Periksa permission folder storage - -### 2. TTS tidak berfungsi - -- Pastikan browser mendukung Speech Synthesis -- Periksa CSRF token untuk request AJAX -- Coba refresh halaman - -### 3. Google TTS API error - -- Periksa API key valid -- Pastikan Cloud Text-to-Speech API aktif -- Cek quota dan billing - -## Fitur Tambahan - -### 1. Audio Queue - -- Multiple panggilan akan di-queue -- Tidak ada overlap audio - -### 2. Cross-page Communication - -- Admin page bisa kirim pesan ke display page -- Menggunakan `postMessage` API - -### 3. Fallback System - -- Google TTS → Browser TTS → Silent -- Memastikan sistem tetap berfungsi - -## Keamanan - -### 1. CSRF Protection - -- Semua request TTS dilindungi CSRF token -- Validasi input untuk mencegah injection - -### 2. File Storage - -- Audio files disimpan di public storage -- Nama file menggunakan timestamp untuk uniqueness - -## Performance - -### 1. Audio Caching - -- Browser akan cache audio files -- Mengurangi bandwidth usage - -### 2. Async Processing - -- TTS generation tidak blocking -- Audio playback asynchronous - -## Monitoring - -### 1. Console Logs - -- Error dan warning di console browser -- Debug info untuk troubleshooting - -### 2. Network Tab - -- Monitor request ke TTS endpoints -- Check audio file downloads - -## Future Enhancements - -### 1. Multiple Languages - -- Support untuk bahasa lain -- Configurable voice settings - -### 2. Custom Audio - -- Upload custom attention sounds -- Per-poli audio customization - -### 3. Volume Control - -- User-adjustable volume -- Per-device audio settings - -### 4. Audio Analytics - -- Track audio playback success -- Monitor TTS usage statistics diff --git a/README_TTS_VOICES.md b/README_TTS_VOICES.md deleted file mode 100644 index c5a3b6d..0000000 --- a/README_TTS_VOICES.md +++ /dev/null @@ -1,166 +0,0 @@ -# TTS Voice Configuration untuk Bahasa Indonesia - -## Google Cloud Text-to-Speech API - -### Suara Indonesia yang Tersedia - -Google TTS API menyediakan beberapa opsi suara untuk bahasa Indonesia: - -#### 1. **Wavenet Voices (Lebih Natural)** -- `id-ID-Wavenet-A` - Suara wanita Indonesia yang natural dan fasih ⭐ **DIGUNAKAN SAAT INI** -- `id-ID-Wavenet-B` - Suara pria Indonesia yang natural -- `id-ID-Wavenet-C` - Suara wanita Indonesia alternatif -- `id-ID-Wavenet-D` - Suara pria Indonesia alternatif - -#### 2. **Standard Voices (Lebih Cepat)** -- `id-ID-Standard-A` - Suara wanita Indonesia standar -- `id-ID-Standard-B` - Suara pria Indonesia standar -- `id-ID-Standard-C` - Suara wanita Indonesia alternatif -- `id-ID-Standard-D` - Suara pria Indonesia alternatif - -### Konfigurasi Saat Ini - -```php -'voice' => [ - 'languageCode' => 'id-ID', - 'name' => 'id-ID-Wavenet-A', // Suara wanita Indonesia yang natural - 'ssmlGender' => 'FEMALE' -], -'audioConfig' => [ - 'audioEncoding' => 'MP3', - 'speakingRate' => 0.85, // Sedikit lebih cepat untuk alur yang natural - 'pitch' => 0, - 'volumeGainDb' => 0 -] -``` - -### Keunggulan Wavenet vs Standard - -**Wavenet Voices:** -- ✅ Suara lebih natural dan manusiawi -- ✅ Intonasi yang lebih baik -- ✅ Pengucapan yang lebih akurat -- ⚠️ Lebih lambat dalam generate -- ⚠️ Lebih mahal (2x lipat) - -**Standard Voices:** -- ✅ Lebih cepat dalam generate -- ✅ Lebih murah -- ⚠️ Suara lebih robotik -- ⚠️ Intonasi kurang natural - -## Browser Speech Synthesis API - -### Konfigurasi Saat Ini - -```javascript -utterance.lang = 'id-ID'; -utterance.rate = 0.85; // Sedikit lebih cepat untuk alur yang natural -utterance.volume = 1.0; - -// Mencoba memilih suara wanita Indonesia jika tersedia -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; -} -``` - -### Suara Browser yang Tersedia - -Browser TTS bergantung pada sistem operasi: - -**Windows:** -- Microsoft Zira Desktop (English, tapi bisa digunakan) -- Microsoft David Desktop (English, tapi bisa digunakan) - -**macOS:** -- Siri (Female) -- Tom (Male) - -**Linux:** -- Festival voices -- eSpeak voices - -## Cara Mengubah Suara - -### 1. Mengubah Google TTS Voice - -Edit file `app/Services/TTSService.php`: - -```php -'voice' => [ - 'languageCode' => 'id-ID', - 'name' => 'id-ID-Wavenet-B', // Ganti dengan suara yang diinginkan - 'ssmlGender' => 'FEMALE' -], -``` - -### 2. Mengubah Browser TTS Voice - -Edit file `resources/views/display/index.blade.php` dan `resources/views/admin/poli/index.blade.php`: - -```javascript -// Untuk memilih suara tertentu -const voices = speechSynthesis.getVoices(); -const specificVoice = voices.find(voice => voice.name === 'Nama Suara Spesifik'); -if (specificVoice) { - utterance.voice = specificVoice; -} -``` - -## Testing Suara - -### 1. Test Google TTS - -```bash -# Pastikan API key sudah diset -echo "GOOGLE_TTS_API_KEY=your_api_key_here" >> .env - -# Test via artisan command (buat sendiri) -php artisan tts:test "Nomor antrian 001, silakan menuju ke Poli Umum" -``` - -### 2. Test Browser TTS - -Buka browser console dan jalankan: - -```javascript -// Test browser TTS -const utterance = new SpeechSynthesisUtterance("Nomor antrian 001, silakan menuju ke Poli Umum"); -utterance.lang = 'id-ID'; -utterance.rate = 0.85; -speechSynthesis.speak(utterance); - -// Lihat suara yang tersedia -speechSynthesis.getVoices().forEach(voice => { - console.log(`${voice.name} - ${voice.lang}`); -}); -``` - -## Rekomendasi - -1. **Untuk Produksi:** Gunakan `id-ID-Wavenet-A` (suara wanita natural) -2. **Untuk Testing:** Gunakan `id-ID-Standard-A` (lebih cepat dan murah) -3. **Fallback:** Browser TTS dengan konfigurasi yang sudah dioptimalkan - -## Troubleshooting - -### Suara Google TTS Tidak Berfungsi -1. Periksa API key di `.env` -2. Pastikan billing Google Cloud aktif -3. Periksa quota API - -### Suara Browser TTS Tidak Berfungsi -1. Periksa browser support untuk `speechSynthesis` -2. Pastikan sistem operasi memiliki TTS engine -3. Coba browser berbeda (Chrome, Firefox, Safari) - -### Suara Terdengar Robotik -1. Gunakan Wavenet voices untuk Google TTS -2. Sesuaikan `speakingRate` (0.8 - 1.0) -3. Pastikan teks menggunakan bahasa Indonesia yang benar diff --git a/TTS_CONFIG.md b/TTS_CONFIG.md deleted file mode 100644 index 6590e92..0000000 --- a/TTS_CONFIG.md +++ /dev/null @@ -1,63 +0,0 @@ -# Konfigurasi TTS (Text-to-Speech) - -## Setup Google Text-to-Speech API - -Untuk menggunakan fitur TTS, Anda perlu mengatur Google Text-to-Speech API: - -### 1. Dapatkan API Key -1. Kunjungi [Google Cloud Console](https://console.cloud.google.com/) -2. Buat project baru atau pilih project yang ada -3. Aktifkan Cloud Text-to-Speech API -4. Buat credentials (API Key) -5. Salin API Key - -### 2. Tambahkan ke .env -Tambahkan baris berikut ke file `.env`: - -```env -GOOGLE_TTS_API_KEY=your_google_tts_api_key_here -``` - -### 3. Fitur TTS - -Fitur TTS akan memainkan urutan audio berikut: -1. **Attention Sound** - `call-to-attention-123107.mp3` -2. **TTS Poli** - "Nomor antrian X, silakan menuju ke [nama poli]" -3. **TTS Nomor** - "Nomor antrian X" -4. **Attention Sound** - `call-to-attention-123107.mp3` - -### 4. Fallback TTS - -Jika Google TTS API tidak tersedia, sistem akan menggunakan: -- Browser's built-in Speech Synthesis API -- File audio attention sound yang sudah ada - -### 5. Cara Kerja - -1. Admin klik button "Panggil" di halaman admin -2. Sistem mengupdate status antrian menjadi "dipanggil" -3. Sistem generate audio TTS sequence -4. Audio diputar di halaman display -5. Jika display page tidak terbuka, audio diputar di browser admin - -### 6. File Audio - -File audio TTS akan disimpan di: -``` -public/storage/audio/queue_calls/ -``` - -### 7. Testing - -Untuk testing tanpa Google TTS API: -1. Biarkan `GOOGLE_TTS_API_KEY` kosong di .env -2. Sistem akan menggunakan browser TTS sebagai fallback -3. Audio attention sound tetap akan diputar - -### 8. Troubleshooting - -Jika TTS tidak berfungsi: -1. Periksa console browser untuk error -2. Pastikan file audio attention sound ada di `public/assets/music/` -3. Periksa permission folder `public/storage/audio/queue_calls/` -4. Pastikan CSRF token valid untuk request AJAX diff --git a/app/Events/AntrianDipanggil.php b/app/Events/AntrianDipanggil.php new file mode 100644 index 0000000..56d5a21 --- /dev/null +++ b/app/Events/AntrianDipanggil.php @@ -0,0 +1,51 @@ +antrian = $antrian; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new Channel('antrian-display'), + ]; + } + + /** + * Get the data to broadcast. + */ + public function broadcastWith(): array + { + return [ + 'poli_name' => $this->antrian->poli->nama_poli, + 'queue_number' => $this->antrian->no_antrian, + 'antrian_id' => $this->antrian->id + ]; + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index b9831bb..c651d53 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -86,6 +86,45 @@ public function showUser(User $user) return view('admin.users.show', compact('user')); } + public function createUser() + { + return view('admin.users.create'); + } + + public function storeUser(Request $request) + { + $request->validate([ + 'nama' => 'required|string|max:255', + 'alamat' => 'required|string', + 'jenis_kelamin' => 'required|in:laki-laki,perempuan', + 'no_hp' => 'required|string|max:20', + 'no_ktp' => 'required|string|size:16|unique:users|regex:/^[0-9]+$/', + 'pekerjaan' => 'required|string|max:100', + 'password' => 'required|string|min:8|confirmed', + ], [ + 'no_ktp.size' => 'Nomor KTP harus tepat 16 digit.', + 'no_ktp.regex' => 'Nomor KTP hanya boleh berisi angka.', + 'no_ktp.unique' => 'Nomor KTP sudah terdaftar.', + 'password.confirmed' => 'Konfirmasi password tidak cocok.', + ]); + + try { + $user = User::create([ + 'nama' => $request->nama, + 'alamat' => $request->alamat, + 'jenis_kelamin' => $request->jenis_kelamin, + 'no_hp' => $request->no_hp, + 'no_ktp' => $request->no_ktp, + 'pekerjaan' => $request->pekerjaan, + 'password' => Hash::make($request->password), + ]); + + return redirect()->route('admin.users.index')->with('success', 'User berhasil ditambahkan!'); + } catch (\Exception $e) { + return back()->withErrors(['error' => 'Terjadi kesalahan saat menambahkan user.'])->withInput(); + } + } + public function updateUser(Request $request, User $user) { $request->validate([ @@ -363,8 +402,11 @@ public function panggilAntrian(Request $request) ]); } - // Update status to 'dipanggil' - $antrian->update(['status' => 'dipanggil']); + // Update status to 'dipanggil' and set call time + $antrian->update([ + 'status' => 'dipanggil', + 'waktu_panggil' => now() + ]); // Record call history RiwayatPanggilan::create([ @@ -394,8 +436,11 @@ public function panggilAntrianById(Antrian $antrian) ]); } - // Update status to 'dipanggil' - $antrian->update(['status' => 'dipanggil']); + // Update status to 'dipanggil' and set call time + $antrian->update([ + 'status' => 'dipanggil', + 'waktu_panggil' => now() + ]); // Record call history RiwayatPanggilan::create([ @@ -403,17 +448,12 @@ public function panggilAntrianById(Antrian $antrian) 'waktu_panggilan' => now() ]); - // Generate TTS audio sequence - $ttsService = new TTSService(); - $audioSequence = $ttsService->createCompleteAudioSequence( - $antrian->poli->nama_poli, - $antrian->no_antrian - ); + // Broadcast event for display page + event(new \App\Events\AntrianDipanggil($antrian)); return response()->json([ 'success' => true, 'message' => 'Antrian ' . $antrian->no_antrian . ' dipanggil', - 'audio_sequence' => $audioSequence, 'poli_name' => $antrian->poli->nama_poli, 'queue_number' => $antrian->no_antrian ]); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 17d82cf..1721c25 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -33,6 +33,8 @@ public function addQueue(Request $request) DB::beginTransaction(); $user = Auth::user(); + $poli = Poli::find($request->poli_id); + $poliName = $poli->nama_poli; // Check if user already has a queue today for the same poli $existingQueue = Antrian::where('user_id', $user->id) @@ -42,13 +44,31 @@ public function addQueue(Request $request) ->first(); if ($existingQueue) { - // Get poli name for error message - $poliName = Poli::find($request->poli_id)->nama_poli; + // Check if request wants JSON response + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'type' => 'existing_queue', + 'message' => 'Anda sudah memiliki antrian di ' . $poliName . ' hari ini.', + 'existing_queue' => [ + 'id' => $existingQueue->id, + 'no_antrian' => $existingQueue->no_antrian, + 'status' => $existingQueue->status, + 'created_at' => $existingQueue->created_at->format('H:i'), + 'poli_name' => $poliName + ] + ]); + } + + // Fallback to redirect for non-AJAX requests return redirect()->back()->with('error', 'Anda sudah memiliki antrian di ' . $poliName . ' hari ini.'); } + // Note: User CAN have multiple queues in different polis + // We only prevent duplicate queues in the same poli (checked above) + // Multi-queue cross-poli is allowed and encouraged + // Get poli info for prefix - $poli = Poli::find($request->poli_id); $prefix = $this->getPoliPrefix($poli->nama_poli); // Get next queue number for the poli @@ -76,12 +96,31 @@ public function addQueue(Request $request) DB::commit(); - // Get poli name for success message - $poliName = $poli->nama_poli; + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'message' => 'Antrian berhasil diambil! Nomor antrian Anda di ' . $poliName . ': ' . $nextQueueNumber, + 'antrian' => [ + 'id' => $antrian->id, + 'no_antrian' => $nextQueueNumber, + 'poli_name' => $poliName, + 'status' => 'menunggu' + ] + ]); + } + return redirect()->back()->with('success', 'Antrian berhasil diambil! Nomor antrian Anda di ' . $poliName . ': ' . $nextQueueNumber); } catch (\Exception $e) { DB::rollback(); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Terjadi kesalahan saat mengambil antrian: ' . $e->getMessage() + ], 500); + } + return redirect()->back()->with('error', 'Terjadi kesalahan saat mengambil antrian: ' . $e->getMessage()); } } @@ -202,7 +241,7 @@ public function cetakAntrian(Antrian $antrian) } $antrian->load(['user', 'poli']); - + $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('user.antrian.print', compact('antrian')); return $pdf->stream('antrian-' . $antrian->no_antrian . '.pdf'); } catch (\Exception $e) { diff --git a/app/Http/Controllers/DisplayController.php b/app/Http/Controllers/DisplayController.php index 4e43f83..3a7949b 100644 --- a/app/Http/Controllers/DisplayController.php +++ b/app/Http/Controllers/DisplayController.php @@ -75,4 +75,100 @@ public function index() 'poliTradisionalNext' )); } + + public function checkNewCalls(Request $request) + { + $lastCheck = $request->get('last_check', 0); + $lastCheckTime = date('Y-m-d H:i:s', $lastCheck / 1000); + + // Check for new calls since last check + $newCall = \App\Models\RiwayatPanggilan::with(['antrian.poli']) + ->where('waktu_panggilan', '>', $lastCheckTime) + ->whereDate('waktu_panggilan', today()) + ->orderBy('waktu_panggilan', 'desc') + ->first(); + + if ($newCall) { + return response()->json([ + 'has_new_call' => true, + 'antrian' => [ + 'poli_name' => $newCall->antrian->poli->nama_poli, + 'queue_number' => $newCall->antrian->no_antrian, + 'antrian_id' => $newCall->antrian->id + ] + ]); + } + + return response()->json([ + 'has_new_call' => false + ]); + } + + public function getDisplayData() + { + // Current: sedang dipanggil per poli + $poliUmumCurrent = Antrian::where('poli_id', 1) + ->where('status', 'dipanggil') + ->whereDate('created_at', today()) + ->orderByDesc('updated_at') + ->first(); + + $poliGigiCurrent = Antrian::where('poli_id', 2) + ->where('status', 'dipanggil') + ->whereDate('created_at', today()) + ->orderByDesc('updated_at') + ->first(); + + $poliJiwaCurrent = Antrian::where('poli_id', 3) + ->where('status', 'dipanggil') + ->whereDate('created_at', today()) + ->orderByDesc('updated_at') + ->first(); + + $poliTradisionalCurrent = Antrian::where('poli_id', 4) + ->where('status', 'dipanggil') + ->whereDate('created_at', today()) + ->orderByDesc('updated_at') + ->first(); + + // Next: menunggu per poli (maks 3) + $poliUmumNext = Antrian::where('poli_id', 1) + ->where('status', 'menunggu') + ->whereDate('created_at', today()) + ->orderBy('created_at', 'asc') + ->take(3) + ->get(); + + $poliGigiNext = Antrian::where('poli_id', 2) + ->where('status', 'menunggu') + ->whereDate('created_at', today()) + ->orderBy('created_at', 'asc') + ->take(3) + ->get(); + + $poliJiwaNext = Antrian::where('poli_id', 3) + ->where('status', 'menunggu') + ->whereDate('created_at', today()) + ->orderBy('created_at', 'asc') + ->take(3) + ->get(); + + $poliTradisionalNext = Antrian::where('poli_id', 4) + ->where('status', 'menunggu') + ->whereDate('created_at', today()) + ->orderBy('created_at', 'asc') + ->take(3) + ->get(); + + return response()->json([ + 'poliUmumCurrent' => $poliUmumCurrent, + 'poliGigiCurrent' => $poliGigiCurrent, + 'poliJiwaCurrent' => $poliJiwaCurrent, + 'poliTradisionalCurrent' => $poliTradisionalCurrent, + 'poliUmumNext' => $poliUmumNext, + 'poliGigiNext' => $poliGigiNext, + 'poliJiwaNext' => $poliJiwaNext, + 'poliTradisionalNext' => $poliTradisionalNext + ]); + } } diff --git a/app/Http/Controllers/IndonesianTTSController.php b/app/Http/Controllers/IndonesianTTSController.php new file mode 100644 index 0000000..1a31be8 --- /dev/null +++ b/app/Http/Controllers/IndonesianTTSController.php @@ -0,0 +1,154 @@ +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 + ]); + } +} diff --git a/app/Http/Controllers/TTSController.php b/app/Http/Controllers/TTSController.php index 316ade9..f894d82 100644 --- a/app/Http/Controllers/TTSController.php +++ b/app/Http/Controllers/TTSController.php @@ -74,22 +74,21 @@ public function getAudioSequence(Request $request) public function playAudioSequence(Request $request) { $request->validate([ - 'antrian_id' => 'required|exists:antrians,id' + 'poli_name' => 'required|string', + 'queue_number' => 'required|string' ]); try { - $antrian = Antrian::with('poli')->findOrFail($request->antrian_id); - $audioSequence = $this->ttsService->createCompleteAudioSequence( - $antrian->poli->nama_poli, - $antrian->no_antrian + $request->poli_name, + $request->queue_number ); return response()->json([ 'success' => true, 'audio_sequence' => $audioSequence, - 'poli_name' => $antrian->poli->nama_poli, - 'queue_number' => $antrian->no_antrian + 'poli_name' => $request->poli_name, + 'queue_number' => $request->queue_number ]); } catch (\Exception $e) { return response()->json([ diff --git a/app/Models/Antrian.php b/app/Models/Antrian.php index 86b220e..3861e7f 100644 --- a/app/Models/Antrian.php +++ b/app/Models/Antrian.php @@ -46,4 +46,22 @@ public function riwayatPanggilan() { return $this->hasMany(RiwayatPanggilan::class); } + + public function getWaktuPanggilAttribute($value) + { + // If waktu_panggil is not set but we have call history, get it from there + if (!$value && $this->status === 'dipanggil') { + $latestCall = $this->riwayatPanggilan()->latest('waktu_panggilan')->first(); + if ($latestCall) { + return $latestCall->waktu_panggilan; + } + } + + // Ensure we return a Carbon instance for proper formatting + if ($value) { + return \Carbon\Carbon::parse($value); + } + + return null; + } } diff --git a/app/Services/IndonesianTTSService.php b/app/Services/IndonesianTTSService.php new file mode 100644 index 0000000..fb89530 --- /dev/null +++ b/app/Services/IndonesianTTSService.php @@ -0,0 +1,385 @@ +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; + } +} diff --git a/app/Services/TTSService.php b/app/Services/TTSService.php index 91dbf06..f9745ff 100644 --- a/app/Services/TTSService.php +++ b/app/Services/TTSService.php @@ -15,14 +15,116 @@ 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 = "Nomor antrian {$queueNumber}, silakan menuju ke {$poliName}"; + $text = "antrian selanjutnya poli {$poliName} nomor {$indonesianQueueNumber}"; // Generate TTS audio $audioContent = $this->synthesizeSpeech($text); @@ -108,15 +210,15 @@ public function createCompleteAudioSequence($poliName, $queueNumber) { $audioFiles = []; - // 1. Attention sound + // 1. Attention sound (4 seconds - actual file duration) $attentionSound = asset('assets/music/call-to-attention-123107.mp3'); $audioFiles[] = [ 'type' => 'attention', 'url' => $attentionSound, - 'duration' => 2000 // 2 seconds + 'duration' => 4000 // 4 seconds - actual file duration ]; - // 2. TTS for poli name and number + // 2. TTS for poli name and number (no final attention sound) $ttsResult = $this->generateQueueCall($poliName, $queueNumber); if ($ttsResult['success']) { if ($ttsResult['use_browser_tts']) { @@ -124,25 +226,18 @@ public function createCompleteAudioSequence($poliName, $queueNumber) $audioFiles[] = [ 'type' => 'browser_tts', 'text' => $ttsResult['text'], - 'duration' => 4000 // 4 seconds + 'duration' => 8000 // 8 seconds - longer for natural speech ]; } else { // Use generated audio file $audioFiles[] = [ 'type' => 'tts', 'url' => $ttsResult['audio_url'], - 'duration' => 4000 // 4 seconds + 'duration' => 8000 // 8 seconds - longer for natural speech ]; } } - // 3. Final attention sound - $audioFiles[] = [ - 'type' => 'attention', - 'url' => $attentionSound, - 'duration' => 2000 // 2 seconds - ]; - return $audioFiles; } } diff --git a/composer.json b/composer.json index f5fae8e..281564c 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "barryvdh/laravel-dompdf": "^3.1", "laravel/framework": "^12.0", "laravel/sanctum": "^4.2", - "laravel/tinker": "^2.10.1" + "laravel/tinker": "^2.10.1", + "pusher/pusher-php-server": "^7.2" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 7aefc2c..2f24f1d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "efff1cd36f67794eb042a3b6c431b4a9", + "content-hash": "6a7319662ab12a24a54850a3e8a7b468", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -2873,6 +2873,97 @@ ], "time": "2025-05-08T08:14:37+00:00" }, + { + "name": "paragonie/sodium_compat", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", + "shasum": "" + }, + "require": { + "php": "^8.1", + "php-64bit": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8|^9", + "vimeo/psalm": "^4|^5" + }, + "suggest": { + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" + }, + "time": "2024-09-04T12:51:01+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -3438,6 +3529,67 @@ }, "time": "2025-08-04T12:39:37+00:00" }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.7", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "support": { + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + }, + "time": "2025-01-06T10:56:20+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", diff --git a/database/migrations/2025_08_10_160027_create_jobs_table.php b/database/migrations/2025_08_10_160027_create_jobs_table.php new file mode 100644 index 0000000..6098d9b --- /dev/null +++ b/database/migrations/2025_08_10_160027_create_jobs_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + } +}; diff --git a/install_indonesian_tts.sh b/install_indonesian_tts.sh new file mode 100644 index 0000000..ef5bf01 --- /dev/null +++ b/install_indonesian_tts.sh @@ -0,0 +1,237 @@ +#!/bin/bash + +# Indonesian TTS Installation Script +# This script automates the installation of Indonesian TTS for Puskesmas system + +set -e + +echo "🏥 Indonesian TTS Installation for Puskesmas System" +echo "==================================================" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + print_error "This script should not be run as root" + exit 1 +fi + +# Check if Python is installed +print_status "Checking Python installation..." +if ! command -v python3 &> /dev/null; then + print_error "Python 3 is not installed. Please install Python 3.8+ first." + exit 1 +fi + +PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) +print_success "Python $PYTHON_VERSION found" + +# Check if pip is installed +print_status "Checking pip installation..." +if ! command -v pip3 &> /dev/null; then + print_error "pip3 is not installed. Please install pip first." + exit 1 +fi + +print_success "pip3 found" + +# Install Coqui TTS +print_status "Installing Coqui TTS..." +if pip3 install TTS; then + print_success "Coqui TTS installed successfully" +else + print_error "Failed to install Coqui TTS" + exit 1 +fi + +# Verify TTS installation +print_status "Verifying TTS installation..." +if tts --version &> /dev/null; then + print_success "TTS command available" +else + print_error "TTS command not found. Installation may have failed." + exit 1 +fi + +# Create necessary directories +print_status "Creating directories..." +mkdir -p storage/app/tts/models +mkdir -p storage/app/public/audio/queue_calls + +print_success "Directories created" + +# Download model files +print_status "Downloading Indonesian TTS model files..." + +MODEL_URL="https://github.com/Wikidepia/indonesian-tts/releases/download/v1.2" +CHECKPOINT_URL="$MODEL_URL/checkpoint.pth" +CONFIG_URL="$MODEL_URL/config.json" + +# Download checkpoint.pth +print_status "Downloading checkpoint.pth..." +if curl -L -o storage/app/tts/models/checkpoint.pth "$CHECKPOINT_URL"; then + print_success "checkpoint.pth downloaded" +else + print_warning "Failed to download checkpoint.pth automatically" + print_status "Please download manually from: $CHECKPOINT_URL" + print_status "And save to: storage/app/tts/models/checkpoint.pth" +fi + +# Download config.json +print_status "Downloading config.json..." +if curl -L -o storage/app/tts/models/config.json "$CONFIG_URL"; then + print_success "config.json downloaded" +else + print_warning "Failed to download config.json automatically" + print_status "Please download manually from: $CONFIG_URL" + print_status "And save to: storage/app/tts/models/config.json" +fi + +# Install g2p-id (optional) +print_status "Installing g2p-id for better pronunciation..." +if pip3 install g2p-id; then + print_success "g2p-id installed successfully" +else + print_warning "Failed to install g2p-id. This is optional but recommended." +fi + +# Set proper permissions +print_status "Setting file permissions..." +chmod -R 755 storage/app/tts/ +chmod 644 storage/app/tts/models/* 2>/dev/null || true + +print_success "Permissions set" + +# Test the installation +print_status "Testing Indonesian TTS installation..." + +TEST_TEXT="Halo dunia" +TEST_OUTPUT="test_indonesian_tts.wav" + +if tts --text "$TEST_TEXT" \ + --model_path storage/app/tts/models/checkpoint.pth \ + --config_path storage/app/tts/models/config.json \ + --speaker_idx wibowo \ + --out_path "$TEST_OUTPUT" 2>/dev/null; then + + print_success "Indonesian TTS test successful!" + + # Check if audio file was created + if [ -f "$TEST_OUTPUT" ]; then + FILE_SIZE=$(du -h "$TEST_OUTPUT" | cut -f1) + print_success "Test audio file created: $TEST_OUTPUT ($FILE_SIZE)" + + # Clean up test file + rm "$TEST_OUTPUT" + print_status "Test file cleaned up" + fi +else + print_warning "Indonesian TTS test failed. Please check the installation manually." +fi + +# Create symbolic link for public access +print_status "Creating symbolic link for public access..." +if [ ! -L "public/storage" ]; then + php artisan storage:link + print_success "Storage link created" +else + print_status "Storage link already exists" +fi + +# Update .env file +print_status "Updating environment configuration..." + +# Check if .env exists +if [ -f ".env" ]; then + # Add Indonesian TTS configuration if not exists + if ! grep -q "INDONESIAN_TTS_ENABLED" .env; then + echo "" >> .env + echo "# Indonesian TTS Configuration" >> .env + echo "INDONESIAN_TTS_ENABLED=true" >> .env + echo "INDONESIAN_TTS_MODEL_PATH=storage/app/tts/models/checkpoint.pth" >> .env + echo "INDONESIAN_TTS_CONFIG_PATH=storage/app/tts/models/config.json" >> .env + echo "INDONESIAN_TTS_DEFAULT_SPEAKER=wibowo" >> .env + print_success "Environment variables added to .env" + else + print_status "Indonesian TTS environment variables already exist" + fi +else + print_warning ".env file not found. Please add Indonesian TTS configuration manually." +fi + +# Final status check +print_status "Performing final status check..." + +echo "" +echo "📋 Installation Summary:" +echo "========================" + +# Check Python +if command -v python3 &> /dev/null; then + echo -e "✅ Python: $(python3 --version)" +else + echo -e "❌ Python: Not found" +fi + +# Check TTS +if command -v tts &> /dev/null; then + echo -e "✅ Coqui TTS: $(tts --version 2>/dev/null | head -n1 || echo 'Installed')" +else + echo -e "❌ Coqui TTS: Not found" +fi + +# Check model files +if [ -f "storage/app/tts/models/checkpoint.pth" ]; then + echo -e "✅ Model file: checkpoint.pth" +else + echo -e "❌ Model file: checkpoint.pth (missing)" +fi + +if [ -f "storage/app/tts/models/config.json" ]; then + echo -e "✅ Config file: config.json" +else + echo -e "❌ Config file: config.json (missing)" +fi + +# Check g2p-id +if command -v g2p-id &> /dev/null; then + echo -e "✅ g2p-id: Installed" +else + echo -e "⚠️ g2p-id: Not installed (optional)" +fi + +echo "" +echo "🎉 Installation completed!" +echo "" +echo "📖 Next steps:" +echo "1. Access Indonesian TTS settings at: /admin/indonesian-tts" +echo "2. Test the TTS functionality" +echo "3. Configure speakers and preferences" +echo "" +echo "📚 Documentation: README_INDONESIAN_TTS.md" +echo "🐛 Troubleshooting: Check the documentation for common issues" +echo "" +echo "Happy coding! 🚀" diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 08d0560..037f320 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -223,7 +223,9 @@ class="flex items-center px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 tra

Antrian Terbaru

-
+ + + + + +
+ @forelse($antrianTerbaru ?? [] as $antrian) +
+ +
+
+

No. + Antrian

+ {{ $antrian->no_antrian ?? 'N/A' }} +
+
+

Nama

+
+ {{ $antrian->user?->nama ?? 'N/A' }}
+
+
+

Poli

+ + {{ $antrian->poli->nama_poli ?? 'N/A' }} + +
+
+

Status +

+ @if (($antrian->status ?? '') == 'menunggu') + + Menunggu + + @elseif(($antrian->status ?? '') == 'dipanggil') + + Dipanggil + + @elseif(($antrian->status ?? '') == 'selesai') + + Selesai + + @else + + Batal + + @endif +
+
+ + +
+ + + + +
+
+ @empty +
+ + + + +

Belum ada antrian hari ini

+

Antrian akan muncul di sini setelah ada + pendaftaran

+
+ @endforelse +
@@ -406,6 +504,25 @@ function confirmLogout() { } }); } + + // Function to toggle dashboard details + function toggleDashboardDetails(antrianId) { + const detailsElement = document.getElementById(`dashboard-details-${antrianId}`); + const buttonTextElement = document.getElementById(`dashboard-btn-text-${antrianId}`); + const iconElement = document.getElementById(`dashboard-icon-${antrianId}`); + + if (detailsElement.classList.contains('hidden')) { + detailsElement.classList.remove('hidden'); + buttonTextElement.textContent = 'Sembunyikan'; + iconElement.classList.remove('transform', 'rotate-180'); + iconElement.classList.add('transform', 'rotate-0'); + } else { + detailsElement.classList.add('hidden'); + buttonTextElement.textContent = 'Selengkapnya'; + iconElement.classList.remove('transform', 'rotate-0'); + iconElement.classList.add('transform', 'rotate-180'); + } + } @endpush @endsection diff --git a/resources/views/admin/indonesian-tts/index.blade.php b/resources/views/admin/indonesian-tts/index.blade.php new file mode 100644 index 0000000..f204c9d --- /dev/null +++ b/resources/views/admin/indonesian-tts/index.blade.php @@ -0,0 +1,536 @@ +@extends('layouts.app') + +@section('title', 'Indonesian TTS Settings') + +@section('content') +
+ + + +
+ + + + + + + +
+
+ +
+

Indonesian TTS Settings

+

Kelola pengaturan Text-to-Speech Indonesia

+
+ + +
+ +
+
+
+
+ + + +
+
+
+

Indonesian TTS

+

Checking...

+
+
+
+ + +
+
+
+
+ + + +
+
+
+

Coqui TTS

+

Checking...

+
+
+
+ + +
+
+
+
+ + + +
+
+
+

Model Files

+

Checking...

+
+
+
+ + +
+
+
+
+ + + +
+
+
+

Available Speakers

+

Checking...

+
+
+
+
+ + +
+

Test Indonesian TTS

+
+
+ + +
+
+ +
+
+ +
+ + +
+

Instalasi Indonesian TTS

+
+
+
+
+ + + + +
+
+

Perhatian

+
+

Indonesian TTS memerlukan instalasi Coqui TTS dan model files. Ikuti + langkah-langkah di bawah ini.

+
+
+
+
+ +
+ +
+
+
+ + +
+

Download Model Files

+
+ +
+
+
+
+
+
+ + @push('scripts') + + @endpush +@endsection diff --git a/resources/views/admin/laporan/index.blade.php b/resources/views/admin/laporan/index.blade.php index d4cebde..a60b97e 100644 --- a/resources/views/admin/laporan/index.blade.php +++ b/resources/views/admin/laporan/index.blade.php @@ -182,7 +182,8 @@ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
-
@@ -190,7 +191,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc
-
@@ -201,7 +203,8 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary"> @foreach ($polis as $poli) - @endforeach @@ -214,11 +217,15 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc @@ -229,9 +236,11 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none foc @@ -260,7 +269,8 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shad
- + @@ -334,23 +344,32 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shad

Data Antrian

-
+ + + + + +
+ @forelse($antrian as $item) +
+ +
+
+

No + Antrian

+ + {{ $item->no_antrian }} + +
+
+

Nama + Pasien

+
{{ $item->user->nama }}
+
+
+

Poli

+ + {{ $item->poli->nama_poli }} + +
+
+

Status +

+ @if ($item->status == 'menunggu') + + Menunggu + + @elseif($item->status == 'sedang') + + Sedang + + @elseif($item->status == 'selesai') + + Selesai + + @else + + Batal + + @endif +
+
+ + +
+ + + + +
+
+ @empty +
+ Tidak ada data antrian yang ditemukan +
+ @endforelse +
@@ -492,6 +611,23 @@ function confirmLogout() { confirmButtonColor: '#EF4444' }); @endif + + // Function to toggle details visibility + function toggleLaporanDetails(id) { + const details = document.getElementById(`laporan-details-${id}`); + const buttonText = document.getElementById(`laporan-btn-text-${id}`); + const icon = document.getElementById(`laporan-icon-${id}`); + + if (details.classList.contains('hidden')) { + details.classList.remove('hidden'); + buttonText.textContent = 'Kurangi'; + icon.classList.remove('transform', 'rotate-180'); + } else { + details.classList.add('hidden'); + buttonText.textContent = 'Selengkapnya'; + icon.classList.add('transform', 'rotate-180'); + } + } @endpush @endsection diff --git a/resources/views/admin/poli/index.blade.php b/resources/views/admin/poli/index.blade.php index 854dd01..22c2e66 100644 --- a/resources/views/admin/poli/index.blade.php +++ b/resources/views/admin/poli/index.blade.php @@ -153,7 +153,8 @@ class="flex items-center px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 tra
-
+ + + + +
+ @forelse($antrians as $antrian) +
+ +
+
+

No + Antrian

+

{{ $antrian->no_antrian }}

+
+
+

Nama

+

{{ $antrian->user?->nama }}

+
+
+

Status +

+ @if ($antrian->status == 'menunggu') + + Menunggu + + @elseif($antrian->status == 'dipanggil') + + Dipanggil + + @elseif($antrian->status == 'selesai') + + Selesai + + @else + + Batal + + @endif +
+
+

Aksi

+ @if ($antrian->status == 'menunggu') + + @elseif($antrian->status == 'dipanggil') +
+ + +
+ @else + - + @endif +
+
+ + +
+ + + + +
+
+ @empty +
+ + + + +

Tidak ada antrian

+

Belum ada antrian yang terdaftar

+
+ @endforelse +
@@ -305,27 +423,30 @@ function panggil(url) { .then(r => r.json()) .then(d => { if (d.success) { - // Play TTS on display page - if (d.audio_sequence && d.poli_name && d.queue_number) { - // Send message to display page if it's open - if (window.opener && !window.opener.closed) { - window.opener.postMessage({ - type: 'TTS_CALL', - poliName: d.poli_name, - queueNumber: d.queue_number, - audioSequence: d.audio_sequence - }, '*'); - } - - // Also try to play locally if display is not open - playTTSLocally(d.poli_name, d.queue_number); - } - + // Show success alert with sound Swal.fire({ icon: 'success', - title: 'Berhasil', - text: d.message - }).then(() => location.reload()); + title: 'Antrian Dipanggil!', + text: `Antrian selanjutnya poli ${d.poli_name} nomor ${d.queue_number}`, + confirmButtonText: 'OK', + confirmButtonColor: '#10B981', + timer: 3000, + timerProgressBar: true, + showClass: { + popup: 'animate__animated animate__fadeInDown' + }, + hideClass: { + popup: 'animate__animated animate__fadeOutUp' + } + }); + + // Play notification sound + playNotificationSound(); + + // Reload page after delay + setTimeout(() => { + location.reload(); + }, 2000); } else { Swal.fire({ icon: 'warning', @@ -342,6 +463,109 @@ function panggil(url) { }); } + // Play notification sound + function playNotificationSound() { + const audio = new Audio( + 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmGgU7k9n1unEiBC13yO/eizEIHWq+8+OWT' + ); + audio.play(); + } + + // Function to convert queue number to Indonesian pronunciation + function convertQueueNumberToIndonesian(queueNumber) { + // Indonesian number words + const 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 (!isNaN(queueNumber)) { + const number = parseInt(queueNumber); + if (indonesianNumbers[number]) { + return indonesianNumbers[number]; + } else { + // For numbers > 100, build the pronunciation + if (number < 100) { + const tens = Math.floor(number / 10) * 10; + const ones = number % 10; + if (ones === 0) { + return indonesianNumbers[tens]; + } else { + return indonesianNumbers[tens] + ' ' + indonesianNumbers[ones]; + } + } else { + return queueNumber; // Fallback for large numbers + } + } + } + + // For alphanumeric (like "U5", "A10"), convert the numeric part + let letters = ''; + let numbers = ''; + + // Split into letters and numbers + for (let i = 0; i < queueNumber.length; i++) { + const char = queueNumber[i]; + if (!isNaN(char)) { + numbers += char; + } else { + letters += char; + } + } + + // If we have both letters and numbers + if (letters && numbers) { + const numberValue = parseInt(numbers); + if (indonesianNumbers[numberValue]) { + return letters + ' ' + indonesianNumbers[numberValue]; + } else { + // For numbers > 100, build the pronunciation + if (numberValue < 100) { + const tens = Math.floor(numberValue / 10) * 10; + const 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; + } + // Function to play TTS locally (fallback) function playTTSLocally(poliName, queueNumber) { // Create audio sequence manually @@ -353,7 +577,9 @@ function playTTSLocally(poliName, queueNumber) { // After attention sound, use browser TTS for poli and number setTimeout(() => { - const text = `Nomor antrian ${queueNumber}, silakan menuju ke ${poliName}`; + // Convert queue number to Indonesian pronunciation + const indonesianQueueNumber = convertQueueNumberToIndonesian(queueNumber); + const text = `Nomor antrian ${indonesianQueueNumber}, silakan menuju ke ${poliName}`; if ('speechSynthesis' in window) { const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'id-ID'; @@ -481,6 +707,22 @@ function confirmLogout() { } }); } + + function toggleDetails(antrianId) { + const details = document.getElementById(`details-${antrianId}`); + const buttonText = document.getElementById(`btn-text-${antrianId}`); + const icon = document.getElementById(`icon-${antrianId}`); + + if (details.classList.contains('hidden')) { + details.classList.remove('hidden'); + buttonText.textContent = 'Kurangi'; + icon.classList.remove('transform', 'rotate-180'); + } else { + details.classList.add('hidden'); + buttonText.textContent = 'Selengkapnya'; + icon.classList.add('transform', 'rotate-180'); + } + } @endpush @endsection diff --git a/resources/views/admin/users/create.blade.php b/resources/views/admin/users/create.blade.php new file mode 100644 index 0000000..71f0cfe --- /dev/null +++ b/resources/views/admin/users/create.blade.php @@ -0,0 +1,329 @@ +@extends('layouts.app') + +@section('title', 'Tambah User Baru - Admin Dashboard') + +@section('content') +
+ + + +
+ + + + +
+
+ +
+
+
+

Tambah User Baru

+

Tambahkan user baru ke sistem Puskesmas

+
+ + + + + Kembali + +
+
+ + +
+
+

Informasi User

+
+ +
+ @csrf + + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+ + +
+ + Batal + + +
+
+
+
+
+
+
+ + +@endsection diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php index b2b7685..9f9cfc3 100644 --- a/resources/views/admin/users/index.blade.php +++ b/resources/views/admin/users/index.blade.php @@ -155,16 +155,20 @@ class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden hidden transition dur
-
+
- +
- - + +
@@ -173,7 +177,8 @@ class="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 foc @@ -187,10 +192,20 @@ class="px-6 py-3 border border-gray-300 rounded-xl text-gray-700 font-medium hov
-
+ -
+ + + + + +
+ @forelse($users as $user) +
+ +
+
+

Nama

+

{{ $user->nama }}

+
+
+

No. KTP +

+

{{ $user->no_ktp }}

+
+
+

No. HP +

+

{{ $user->no_hp }}

+
+
+

Jenis + Kelamin

+ + {{ $user->jenis_kelamin == 'laki-laki' ? 'Laki-laki' : 'Perempuan' }} + +
+
+ + +
+ + + + +
+
+ @empty +
+ + + + +

Belum ada user

+

Tidak ada data user yang tersedia

+
+ @endforelse +
@@ -529,6 +623,23 @@ function confirmLogout() { timerProgressBar: true }); @endif + + // Function to toggle user details on mobile + function toggleUserDetails(userId) { + const detailsDiv = document.getElementById(`user-details-${userId}`); + const buttonText = document.getElementById(`user-btn-text-${userId}`); + const icon = document.getElementById(`user-icon-${userId}`); + + if (detailsDiv.classList.contains('hidden')) { + detailsDiv.classList.remove('hidden'); + buttonText.textContent = 'Lebih Sedikit'; + icon.classList.remove('transform', 'rotate-180'); + } else { + detailsDiv.classList.add('hidden'); + buttonText.textContent = 'Selengkapnya'; + icon.classList.add('transform', 'rotate-180'); + } + } @endpush @endsection diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php index e7d4df5..b7b321b 100644 --- a/resources/views/dashboard/index.blade.php +++ b/resources/views/dashboard/index.blade.php @@ -141,7 +141,7 @@ class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm fon
- + @csrf
@@ -158,7 +158,7 @@ class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:rin
-
-
+ + + + + +
+ @forelse($antrianSaya ?? [] as $antrian) +
+ +
+
+

No. Antrian

+ {{ $antrian->no_antrian ?? 'N/A' }} +
+
+

Nama

+
{{ $antrian->user->nama ?? 'N/A' }} +
+
+
+

Poli

+ + {{ $antrian->poli->nama_poli ?? 'N/A' }} + +
+
+

Status

+ @if (($antrian->status ?? '') == 'menunggu') + + Menunggu + + @elseif(($antrian->status ?? '') == 'dipanggil') + + Dipanggil + + @elseif(($antrian->status ?? '') == 'selesai') + + Selesai + + @else + + Batal + + @endif +
+
+ + +
+ + + + +
+
+ @empty +
+ + + + +

Belum ada antrian

+

Silakan ambil antrian baru di atas

+
+ @endforelse +
@@ -474,6 +598,105 @@ function closeEditModal() { }); @endif + // Handle form submission for adding queue + document.getElementById('addQueueForm').addEventListener('submit', function(e) { + e.preventDefault(); + + const submitBtn = document.getElementById('submitBtn'); + const originalText = submitBtn.innerHTML; + + // Disable button and show loading + submitBtn.disabled = true; + submitBtn.innerHTML = ` + + + + + Memproses... + `; + + const formData = new FormData(this); + formData.append('_token', '{{ csrf_token() }}'); + + fetch('{{ route('dashboard.add-queue') }}', { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Success case + Swal.fire({ + icon: 'success', + title: 'Berhasil!', + text: data.message, + confirmButtonText: 'OK', + confirmButtonColor: '#10B981' + }).then(() => { + location.reload(); + }); + } else { + // Handle different error types + if (data.type === 'existing_queue') { + // User already has queue in same poli + Swal.fire({ + icon: 'warning', + title: 'Antrian Sudah Ada!', + html: ` +
+

${data.message}

+
+
+ + + + Detail Antrian: +
+
+

No. Antrian: ${data.existing_queue.no_antrian}

+

Poli: ${data.existing_queue.poli_name}

+

Status: ${data.existing_queue.status === 'menunggu' ? 'Menunggu' : 'Dipanggil'}

+

Waktu Ambil: ${data.existing_queue.created_at}

+
+
+

Silakan tunggu hingga antrian Anda dipanggil atau batalkan antrian ini terlebih dahulu.

+
+ `, + confirmButtonText: 'OK', + confirmButtonColor: '#F59E0B' + }); + } else { + // Generic error + Swal.fire({ + icon: 'error', + title: 'Error!', + text: data.message, + confirmButtonText: 'OK', + confirmButtonColor: '#EF4444' + }); + } + } + }) + .catch(error => { + console.error('Error:', error); + Swal.fire({ + icon: 'error', + title: 'Error!', + text: 'Terjadi kesalahan saat mengambil antrian. Silakan coba lagi.', + confirmButtonText: 'OK', + confirmButtonColor: '#EF4444' + }); + }) + .finally(() => { + // Re-enable button and restore original text + submitBtn.disabled = false; + submitBtn.innerHTML = originalText; + }); + }); + // Function to confirm logout function confirmLogout() { Swal.fire({ @@ -521,40 +744,57 @@ function batalAntrian(antrianId) { formData.append('_token', document.querySelector('meta[name="csrf-token"]').content); fetch('/user/antrian/batal', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - Swal.fire({ - icon: 'success', - title: 'Berhasil!', - text: data.message, - confirmButtonText: 'OK' - }).then(() => { - location.reload(); - }); - } else { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + Swal.fire({ + icon: 'success', + title: 'Berhasil!', + text: data.message, + confirmButtonText: 'OK' + }).then(() => { + location.reload(); + }); + } else { + Swal.fire({ + icon: 'error', + title: 'Error!', + text: data.message, + confirmButtonText: 'OK' + }); + } + }) + .catch(error => { Swal.fire({ icon: 'error', title: 'Error!', - text: data.message, + text: 'Terjadi kesalahan saat membatalkan antrian', confirmButtonText: 'OK' }); - } - }) - .catch(error => { - Swal.fire({ - icon: 'error', - title: 'Error!', - text: 'Terjadi kesalahan saat membatalkan antrian', - confirmButtonText: 'OK' }); - }); } }); } + + // Function to toggle user dashboard details + function toggleUserDashboardDetails(antrianId) { + const detailsDiv = document.getElementById(`user-dashboard-details-${antrianId}`); + const buttonText = document.getElementById(`user-dashboard-btn-text-${antrianId}`); + const icon = document.getElementById(`user-dashboard-icon-${antrianId}`); + + if (detailsDiv.classList.contains('hidden')) { + detailsDiv.classList.remove('hidden'); + buttonText.textContent = 'Lebih Sedikit'; + icon.classList.remove('transform', 'rotate-180'); + } else { + detailsDiv.classList.add('hidden'); + buttonText.textContent = 'Selengkapnya'; + icon.classList.add('transform', 'rotate-180'); + } + } @endpush @endsection diff --git a/resources/views/display/index.blade.php b/resources/views/display/index.blade.php index a6161ad..7c62827 100644 --- a/resources/views/display/index.blade.php +++ b/resources/views/display/index.blade.php @@ -30,7 +30,7 @@

Antrian Berikutnya:

-
+
@forelse($poliUmumNext ?? [] as $antrian)
@@ -68,7 +68,7 @@ class="text-lg md:text-xl font-bold text-gray-900">{{ $antrian->no_antrian }}

Antrian Berikutnya:

-
+
@forelse($poliGigiNext ?? [] as $antrian)
@@ -106,7 +106,7 @@ class="text-lg md:text-xl font-bold text-gray-900">{{ $antrian->no_antrian }}

Antrian Berikutnya:

-
+
@forelse($poliJiwaNext ?? [] as $antrian)
@@ -145,7 +145,7 @@ class="text-lg md:text-xl font-bold text-gray-900">{{ $antrian->no_antrian }}

Antrian Berikutnya:

-
+
@forelse($poliTradisionalNext ?? [] as $antrian)
@@ -206,9 +206,9 @@ class TTSAudioPlayer { const audioItem = audioSequence[i]; await this.playAudioItem(audioItem); - // Wait between audio items + // Wait between audio items (1 second gap) if (i < audioSequence.length - 1) { - await this.delay(500); + await this.delay(1000); } } @@ -257,7 +257,7 @@ class TTSAudioPlayer { // Fallback timeout setTimeout(() => { resolve(); - }, audioItem.duration || 4000); + }, audioItem.duration || 8000); } else { console.warn('Speech synthesis not supported'); resolve(); @@ -282,7 +282,7 @@ class TTSAudioPlayer { // Fallback timeout setTimeout(() => { resolve(); - }, audioItem.duration || 3000); + }, audioItem.duration || 8000); } }); } @@ -362,10 +362,7 @@ class TTSAudioPlayer { }); @endif - // Auto refresh every 5 seconds - setInterval(function() { - location.reload(); - }, 5000); + // No more auto refresh - using real-time updates instead // Add sound effect for new calls function playNotificationSound() { @@ -386,7 +383,7 @@ function showNewCallNotification(poliName, number) { Swal.fire({ icon: 'info', title: 'Panggilan Baru!', - text: `Poli ${poliName} memanggil nomor ${number}`, + text: `Antrian selanjutnya poli ${poliName} nomor ${number}`, confirmButtonText: 'OK', confirmButtonColor: '#3B82F6', timer: 5000, @@ -420,7 +417,98 @@ function addPulseAnimation() { addPulseAnimation(); }); - // Listen for TTS events from admin panel + // Check for new calls using polling (fallback for broadcast) + let lastCallTime = new Date().getTime(); + + function checkForNewCalls() { + fetch('/api/check-new-calls?last_check=' + lastCallTime) + .then(response => response.json()) + .then(data => { + if (data.has_new_call && data.antrian) { + console.log('New call detected:', data.antrian); + + // Play TTS for the called queue + ttsPlayer.playQueueCall(data.antrian.poli_name, data.antrian.queue_number); + + // Show notification + showNewCallNotification(data.antrian.poli_name, data.antrian.queue_number); + + // Add pulse animation to the current number + addPulseAnimation(); + + // Update last check time + lastCallTime = new Date().getTime(); + + // Update display without refresh + updateDisplayData(); + } + }) + .catch(error => { + console.log('Error checking for new calls:', error); + }); + } + + // Function to update display data without refresh + function updateDisplayData() { + fetch('/api/display-data') + .then(response => response.json()) + .then(data => { + // Update current numbers + if (data.poliUmumCurrent) { + document.getElementById('poli-umum-current').textContent = data.poliUmumCurrent.no_antrian; + } + if (data.poliGigiCurrent) { + document.getElementById('poli-gigi-current').textContent = data.poliGigiCurrent.no_antrian; + } + if (data.poliJiwaCurrent) { + document.getElementById('poli-jiwa-current').textContent = data.poliJiwaCurrent.no_antrian; + } + if (data.poliTradisionalCurrent) { + document.getElementById('poli-tradisional-current').textContent = data.poliTradisionalCurrent + .no_antrian; + } + + // Update next queues + updateNextQueue('poli-umum-next', data.poliUmumNext); + updateNextQueue('poli-gigi-next', data.poliGigiNext); + updateNextQueue('poli-jiwa-next', data.poliJiwaNext); + updateNextQueue('poli-tradisional-next', data.poliTradisionalNext); + }) + .catch(error => { + console.log('Error updating display data:', error); + }); + } + + // Function to update next queue section + function updateNextQueue(containerId, nextQueues) { + const container = document.querySelector(`[data-next-queue="${containerId}"]`); + if (!container) return; + + if (nextQueues && nextQueues.length > 0) { + container.innerHTML = nextQueues.map(antrian => ` +
+ ${antrian.no_antrian} +
+ `).join(''); + } else { + container.innerHTML = ` +
+ + + +

Tidak ada antrian

+
+ `; + } + } + + // Check for new calls every 3 seconds + setInterval(checkForNewCalls, 3000); + + // Update display data every 10 seconds + setInterval(updateDisplayData, 10000); + + // Fallback: Listen for TTS events from admin panel (if broadcast not available) window.addEventListener('message', function(event) { if (event.data.type === 'TTS_CALL') { const { diff --git a/routes/web.php b/routes/web.php index b8a90e5..0e19f65 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\DisplayController; use App\Http\Controllers\AdminController; use App\Http\Controllers\TTSController; +use App\Http\Controllers\IndonesianTTSController; // Landing Page Route::get('/', [LandingController::class, 'index'])->name('landing'); @@ -67,6 +68,8 @@ // User Management Routes Route::get('/admin/users', [AdminController::class, 'manageUsers'])->name('admin.users.index'); + Route::get('/admin/users/create', [AdminController::class, 'createUser'])->name('admin.users.create'); + Route::post('/admin/users', [AdminController::class, 'storeUser'])->name('admin.users.store'); Route::get('/admin/users/{user}', [AdminController::class, 'showUser'])->name('admin.users.show'); Route::put('/admin/users/{user}', [AdminController::class, 'updateUser'])->name('admin.users.update'); Route::post('/admin/users/{user}/reset-password', [AdminController::class, 'resetUserPassword'])->name('admin.users.reset-password'); @@ -83,8 +86,21 @@ Route::post('/admin/tts/generate', [TTSController::class, 'generateQueueCall'])->name('admin.tts.generate'); Route::post('/admin/tts/audio-sequence', [TTSController::class, 'getAudioSequence'])->name('admin.tts.audio-sequence'); Route::post('/admin/tts/play-sequence', [TTSController::class, 'playAudioSequence'])->name('admin.tts.play-sequence'); + + // 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'); +// API Routes for display +Route::get('/api/check-new-calls', [DisplayController::class, 'checkNewCalls'])->name('api.check-new-calls'); +Route::get('/api/display-data', [DisplayController::class, 'getDisplayData'])->name('api.display-data'); + diff --git a/test_audio_sequence.php b/test_audio_sequence.php new file mode 100644 index 0000000..a3cd6e5 --- /dev/null +++ b/test_audio_sequence.php @@ -0,0 +1,54 @@ + \ No newline at end of file diff --git a/test_indonesian_pronunciation.php b/test_indonesian_pronunciation.php new file mode 100644 index 0000000..26a24fb --- /dev/null +++ b/test_indonesian_pronunciation.php @@ -0,0 +1,169 @@ + '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"; diff --git a/test_riwayat.php b/test_riwayat.php deleted file mode 100644 index 8b13789..0000000 --- a/test_riwayat.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test_tts.php b/test_tts.php deleted file mode 100644 index d23f6ea..0000000 --- a/test_tts.php +++ /dev/null @@ -1,35 +0,0 @@ -generateQueueCall('Poli Umum', 'A001'); - echo "Result: " . json_encode($result, JSON_PRETTY_PRINT) . "\n\n"; - - echo "2. Testing createCompleteAudioSequence...\n"; - $sequence = $ttsService->createCompleteAudioSequence('Poli Umum', 'A001'); - echo "Sequence: " . json_encode($sequence, JSON_PRETTY_PRINT) . "\n\n"; - - echo "3. Testing dengan poli berbeda...\n"; - $sequence2 = $ttsService->createCompleteAudioSequence('Poli Gigi', 'B002'); - echo "Sequence 2: " . json_encode($sequence2, JSON_PRETTY_PRINT) . "\n\n"; - - echo "✅ TTS Service berfungsi dengan baik!\n"; - -} catch (Exception $e) { - echo "❌ Error: " . $e->getMessage() . "\n"; - echo "Stack trace: " . $e->getTraceAsString() . "\n"; -} - -echo "\n=== Test Selesai ===\n"; diff --git a/test_tts_simple.php b/test_tts_simple.php deleted file mode 100644 index 9cc0d43..0000000 --- a/test_tts_simple.php +++ /dev/null @@ -1,53 +0,0 @@ -