MIF_E31222851/app/Services/IndonesianTTSService.php

386 lines
12 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Process;
class IndonesianTTSService
{
private $modelPath;
private $configPath;
private $g2pPath;
private $outputPath;
public function __construct()
{
// Path untuk model dan konfigurasi Indonesian TTS
$this->modelPath = storage_path('app/tts/models/checkpoint.pth');
$this->configPath = storage_path('app/tts/models/config.json');
$this->g2pPath = storage_path('app/tts/g2p-id');
$this->outputPath = storage_path('app/public/audio/queue_calls');
// Buat direktori jika belum ada
if (!file_exists($this->outputPath)) {
mkdir($this->outputPath, 0755, true);
}
}
/**
* Generate TTS audio using Indonesian TTS model
*/
public function generateQueueCall($poliName, $queueNumber)
{
try {
// Convert queue number to Indonesian pronunciation
$indonesianQueueNumber = $this->convertQueueNumberToIndonesian($queueNumber);
// Create the text to be spoken
$text = "Nomor antrian {$indonesianQueueNumber}, silakan menuju ke {$poliName}";
// Check if Indonesian TTS model is available
if ($this->isIndonesianTTSAvailable()) {
return $this->generateWithIndonesianTTS($text, $queueNumber, $poliName);
} else {
// Fallback to Google TTS or browser TTS
return $this->generateWithFallbackTTS($text);
}
} catch (\Exception $e) {
return [
'success' => false,
'message' => 'Error generating Indonesian TTS: ' . $e->getMessage()
];
}
}
/**
* Generate TTS using Indonesian TTS model
*/
public function generateWithIndonesianTTS($text, $queueNumber, $poliName)
{
try {
// Generate filename
$filename = "indonesian_tts_{$queueNumber}_{$poliName}_" . time() . ".wav";
$filepath = $this->outputPath . '/' . $filename;
// Prepare text for TTS (convert to phonemes if g2p-id is available)
$phonemeText = $this->convertToPhonemes($text);
// Run Coqui TTS command
$command = $this->buildTTSCmd($phonemeText, $filepath);
$result = Process::run($command);
if ($result->successful()) {
return [
'success' => true,
'audio_url' => asset('storage/audio/queue_calls/' . $filename),
'filename' => $filename,
'tts_type' => 'indonesian_tts'
];
} else {
throw new \Exception('TTS generation failed: ' . $result->errorOutput());
}
} catch (\Exception $e) {
// Fallback to other TTS methods
return $this->generateWithFallbackTTS($text);
}
}
/**
* Build TTS command for Coqui TTS
*/
private function buildTTSCmd($text, $outputPath)
{
$speaker = 'wibowo'; // Default speaker, can be made configurable
return [
'tts',
'--text',
$text,
'--model_path',
$this->modelPath,
'--config_path',
$this->configPath,
'--speaker_idx',
$speaker,
'--out_path',
$outputPath
];
}
/**
* Convert text to phonemes using g2p-id
*/
private function convertToPhonemes($text)
{
// If g2p-id is available, use it to convert to phonemes
if (file_exists($this->g2pPath)) {
try {
$result = Process::run([$this->g2pPath, $text]);
if ($result->successful()) {
return trim($result->output());
}
} catch (\Exception $e) {
// If g2p conversion fails, return original text
return $text;
}
}
// Return original text if g2p-id is not available
return $text;
}
/**
* Check if Indonesian TTS model is available
*/
public function isIndonesianTTSAvailable()
{
return file_exists($this->modelPath) &&
file_exists($this->configPath) &&
$this->isCoquiTTSInstalled();
}
/**
* Check if Coqui TTS is installed
*/
public function isCoquiTTSInstalled()
{
try {
$result = Process::run(['tts', '--version']);
return $result->successful();
} catch (\Exception $e) {
return false;
}
}
/**
* Fallback to Google TTS or browser TTS
*/
private function generateWithFallbackTTS($text)
{
// Use existing TTSService as fallback
$ttsService = new TTSService();
// Extract poli name and queue number from text for fallback
preg_match('/Nomor antrian (.+), silakan menuju ke ruang (.+)/', $text, $matches);
$queueNumber = $matches[1] ?? '';
$poliName = $matches[2] ?? '';
return $ttsService->generateQueueCall($poliName, $queueNumber);
}
/**
* Convert alphanumeric queue number to Indonesian pronunciation
* Example: "U5" becomes "U Lima", "A10" becomes "A Sepuluh"
*/
private function convertQueueNumberToIndonesian($queueNumber)
{
// Indonesian number words
$indonesianNumbers = [
'0' => 'Nol',
'1' => 'Satu',
'2' => 'Dua',
'3' => 'Tiga',
'4' => 'Empat',
'5' => 'Lima',
'6' => 'Enam',
'7' => 'Tujuh',
'8' => 'Delapan',
'9' => 'Sembilan',
'10' => 'Sepuluh',
'11' => 'Sebelas',
'12' => 'Dua Belas',
'13' => 'Tiga Belas',
'14' => 'Empat Belas',
'15' => 'Lima Belas',
'16' => 'Enam Belas',
'17' => 'Tujuh Belas',
'18' => 'Delapan Belas',
'19' => 'Sembilan Belas',
'20' => 'Dua Puluh',
'30' => 'Tiga Puluh',
'40' => 'Empat Puluh',
'50' => 'Lima Puluh',
'60' => 'Enam Puluh',
'70' => 'Tujuh Puluh',
'80' => 'Delapan Puluh',
'90' => 'Sembilan Puluh',
'100' => 'Seratus'
];
// If it's a pure number, convert it
if (is_numeric($queueNumber)) {
$number = (int) $queueNumber;
if (isset($indonesianNumbers[$number])) {
return $indonesianNumbers[$number];
} else {
// For numbers > 100, build the pronunciation
if ($number < 100) {
$tens = floor($number / 10) * 10;
$ones = $number % 10;
if ($ones == 0) {
return $indonesianNumbers[$tens];
} else {
return $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
}
} else {
return $number; // Fallback for large numbers
}
}
}
// For alphanumeric (like "U5", "A10"), convert the numeric part
$letters = '';
$numbers = '';
// Split into letters and numbers
for ($i = 0; $i < strlen($queueNumber); $i++) {
$char = $queueNumber[$i];
if (is_numeric($char)) {
$numbers .= $char;
} else {
$letters .= $char;
}
}
// If we have both letters and numbers
if ($letters && $numbers) {
$numberValue = (int) $numbers;
if (isset($indonesianNumbers[$numberValue])) {
return $letters . ' ' . $indonesianNumbers[$numberValue];
} else {
// For numbers > 100, build the pronunciation
if ($numberValue < 100) {
$tens = floor($numberValue / 10) * 10;
$ones = $numberValue % 10;
if ($ones == 0) {
return $letters . ' ' . $indonesianNumbers[$tens];
} else {
return $letters . ' ' . $indonesianNumbers[$tens] . ' ' . $indonesianNumbers[$ones];
}
} else {
return $queueNumber; // Fallback for large numbers
}
}
}
// If no conversion needed, return as is
return $queueNumber;
}
/**
* Create complete audio sequence for queue call
*/
public function createCompleteAudioSequence($poliName, $queueNumber)
{
$audioFiles = [];
// 1. Attention sound (4 seconds - actual file duration)
$attentionSound = asset('assets/music/call-to-attention-123107.mp3');
$audioFiles[] = [
'type' => 'attention',
'url' => $attentionSound,
'duration' => 4000 // 4 seconds - actual file duration
];
// 2. TTS for poli name and number (no final attention sound)
$ttsResult = $this->generateQueueCall($poliName, $queueNumber);
if ($ttsResult['success']) {
if (isset($ttsResult['use_browser_tts']) && $ttsResult['use_browser_tts']) {
// Use browser TTS
$audioFiles[] = [
'type' => 'browser_tts',
'text' => $ttsResult['text'],
'duration' => 8000 // 8 seconds - longer for natural speech
];
} else {
// Use generated audio file
$audioFiles[] = [
'type' => 'tts',
'url' => $ttsResult['audio_url'],
'duration' => 8000 // 8 seconds - longer for natural speech
];
}
}
return $audioFiles;
}
/**
* Get available speakers from Indonesian TTS model
*/
public function getAvailableSpeakers()
{
if (!$this->isIndonesianTTSAvailable()) {
return [];
}
try {
$result = Process::run([
'tts',
'--model_path',
$this->modelPath,
'--config_path',
$this->configPath,
'--list_speaker_idxs'
]);
if ($result->successful()) {
$output = $result->output();
// Parse speaker list from output
$speakers = [];
$lines = explode("\n", $output);
foreach ($lines as $line) {
if (preg_match('/^(\d+):\s*(.+)$/', $line, $matches)) {
$speakers[$matches[1]] = trim($matches[2]);
}
}
return $speakers;
}
} catch (\Exception $e) {
// Return default speakers if listing fails
return [
'wibowo' => 'Wibowo (Male)',
'ardi' => 'Ardi (Male)',
'gadis' => 'Gadis (Female)'
];
}
return [];
}
/**
* Install Indonesian TTS model
*/
public function installIndonesianTTS()
{
$instructions = [
'title' => 'Instalasi Indonesian TTS',
'steps' => [
'1. Install Coqui TTS:',
' pip install TTS',
'',
'2. Download model dari GitHub:',
' - Kunjungi: https://github.com/Wikidepia/indonesian-tts/releases',
' - Download file checkpoint.pth dan config.json',
' - Simpan di: ' . storage_path('app/tts/models/'),
'',
'3. Install g2p-id (opsional):',
' pip install g2p-id',
'',
'4. Test instalasi:',
' tts --version',
'',
'5. Test model:',
' tts --text "Halo dunia" --model_path ' . $this->modelPath . ' --config_path ' . $this->configPath . ' --out_path test.wav'
]
];
return $instructions;
}
}