CRITICAL FIXES: - Fixed hold/resume functions to use proper Hold Queue system instead of empty TwiML - Enhanced customer number detection for voicemail and call recording interfaces - Resolved customer disconnections during hold/resume operations on outbound calls Hold/Resume System Improvements: - Updated ajax_toggle_hold() to use TWP_User_Queue_Manager for proper queue management - Fixed resume function to redirect calls back to appropriate queues (personal/shared) - Added comprehensive logging for hold queue operations and call leg detection - Enhanced hold experience with proper TTS messages and queue tracking Customer Number Detection Enhancements: - Enhanced handle_voicemail_callback() with fallback logic for missing From parameters - Added browser phone detection to identify client: calls and find real customer numbers - Enhanced call recording to use find_customer_call_leg() for proper customer identification - Fixed admin interfaces to show actual phone numbers instead of "client:agentname" Technical Improvements: - Integrated Hold Queue system with existing call leg detection infrastructure - Added proper TwiML generation for hold/resume operations - Enhanced error handling and logging for debugging complex call topologies - Maintains database consistency with queue position tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
220 lines
6.9 KiB
PHP
220 lines
6.9 KiB
PHP
<?php
|
|
/**
|
|
* TTS Helper class to handle Text-to-Speech with ElevenLabs or Twilio
|
|
*/
|
|
class TWP_TTS_Helper {
|
|
|
|
private static $instance = null;
|
|
private $elevenlabs_api = null;
|
|
private $use_elevenlabs = false;
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
public static function get_instance() {
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
// Check if ElevenLabs is configured
|
|
$api_key = get_option('twp_elevenlabs_api_key');
|
|
$voice_id = get_option('twp_elevenlabs_voice_id');
|
|
|
|
if (!empty($api_key) && !empty($voice_id)) {
|
|
$this->use_elevenlabs = true;
|
|
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-elevenlabs-api.php';
|
|
$this->elevenlabs_api = new TWP_ElevenLabs_API();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get cache key for text
|
|
*/
|
|
private function get_cache_key($text) {
|
|
$voice_id = get_option('twp_elevenlabs_voice_id');
|
|
$model_id = get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2');
|
|
return 'twp_tts_' . md5($text . $voice_id . $model_id);
|
|
}
|
|
|
|
/**
|
|
* Get cached audio URL if exists
|
|
*/
|
|
private function get_cached_audio($text) {
|
|
$cache_key = $this->get_cache_key($text);
|
|
$cached_data = get_transient($cache_key);
|
|
|
|
if ($cached_data !== false) {
|
|
// Verify the file still exists
|
|
if (file_exists($cached_data['file_path'])) {
|
|
error_log("TWP TTS: Using cached audio for text: " . substr($text, 0, 50) . "...");
|
|
return $cached_data;
|
|
} else {
|
|
// File was deleted, remove from cache
|
|
delete_transient($cache_key);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Save audio to cache
|
|
*/
|
|
private function cache_audio($text, $audio_data) {
|
|
$cache_key = $this->get_cache_key($text);
|
|
// Cache for 30 days
|
|
set_transient($cache_key, $audio_data, 30 * DAY_IN_SECONDS);
|
|
}
|
|
|
|
/**
|
|
* Add TTS to TwiML Response
|
|
*
|
|
* @param \Twilio\TwiML\VoiceResponse $twiml The TwiML response object
|
|
* @param string $text The text to speak
|
|
* @param array $options Options for voice settings (used for Twilio fallback)
|
|
* @return bool Success status
|
|
*/
|
|
public function add_tts_to_twiml($twiml, $text, $options = []) {
|
|
// Default Twilio voice options
|
|
$default_options = [
|
|
'voice' => 'alice',
|
|
'language' => 'en-US'
|
|
];
|
|
$options = array_merge($default_options, $options);
|
|
|
|
if ($this->use_elevenlabs) {
|
|
// First check cache
|
|
$cached_audio = $this->get_cached_audio($text);
|
|
|
|
if ($cached_audio !== false) {
|
|
$twiml->play($cached_audio['file_url']);
|
|
return true;
|
|
}
|
|
|
|
// Not in cache, generate new audio
|
|
$audio_result = $this->elevenlabs_api->text_to_speech($text);
|
|
|
|
if ($audio_result && isset($audio_result['success']) && $audio_result['success']) {
|
|
// Cache the result
|
|
$this->cache_audio($text, [
|
|
'file_url' => $audio_result['file_url'],
|
|
'file_path' => $audio_result['file_path']
|
|
]);
|
|
|
|
// Use the generated audio file
|
|
$twiml->play($audio_result['file_url']);
|
|
error_log("TWP TTS: Generated new ElevenLabs audio for text: " . substr($text, 0, 50) . "...");
|
|
return true;
|
|
} else {
|
|
// Log the failure and fall back to Twilio
|
|
error_log("TWP TTS: ElevenLabs failed, falling back to Twilio. Error: " .
|
|
(isset($audio_result['error']) ? $audio_result['error'] : 'Unknown error'));
|
|
}
|
|
}
|
|
|
|
// Fall back to Twilio's built-in TTS
|
|
$twiml->say($text, $options);
|
|
error_log("TWP TTS: Using Twilio voice for text: " . substr($text, 0, 50) . "...");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate TTS audio file (for pre-generation)
|
|
*
|
|
* @param string $text The text to convert
|
|
* @return array|false Array with file_url on success, false on failure
|
|
*/
|
|
public function generate_tts_audio($text) {
|
|
if (!$this->use_elevenlabs) {
|
|
return false;
|
|
}
|
|
|
|
// First check cache
|
|
$cached_audio = $this->get_cached_audio($text);
|
|
if ($cached_audio !== false) {
|
|
return [
|
|
'success' => true,
|
|
'file_url' => $cached_audio['file_url'],
|
|
'file_path' => $cached_audio['file_path'],
|
|
'cached' => true
|
|
];
|
|
}
|
|
|
|
// Not in cache, generate new audio
|
|
$result = $this->elevenlabs_api->text_to_speech($text);
|
|
|
|
if ($result && isset($result['success']) && $result['success']) {
|
|
// Cache the result
|
|
$this->cache_audio($text, [
|
|
'file_url' => $result['file_url'],
|
|
'file_path' => $result['file_path']
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'file_url' => $result['file_url'],
|
|
'file_path' => $result['file_path'],
|
|
'cached' => false
|
|
];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if ElevenLabs is configured and available
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_elevenlabs_available() {
|
|
return $this->use_elevenlabs;
|
|
}
|
|
|
|
/**
|
|
* Get configured voice info
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_voice_info() {
|
|
if ($this->use_elevenlabs) {
|
|
return [
|
|
'provider' => 'elevenlabs',
|
|
'voice_id' => get_option('twp_elevenlabs_voice_id'),
|
|
'model_id' => get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2')
|
|
];
|
|
}
|
|
|
|
return [
|
|
'provider' => 'twilio',
|
|
'voice' => 'alice',
|
|
'language' => 'en-US'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Clean up old TTS files (maintenance)
|
|
*/
|
|
public function cleanup_old_files($hours = 24) {
|
|
$upload_dir = wp_upload_dir();
|
|
$path = $upload_dir['path'];
|
|
|
|
// Only clean up TTS files older than specified hours
|
|
$expire_time = time() - ($hours * 3600);
|
|
|
|
$files = glob($path . '/tts_*.mp3');
|
|
if ($files) {
|
|
foreach ($files as $file) {
|
|
if (filemtime($file) < $expire_time) {
|
|
unlink($file);
|
|
error_log("TWP TTS: Cleaned up old file: " . basename($file));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |