Files
twilio-wp-plugin/includes/class-twp-tts-helper.php
jknapp ae92ea2c81 Fix hold/resume functionality and customer number detection
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>
2025-09-01 09:34:07 -07:00

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