// OpenAI Realtime two-presenter audio backend; no external WebSocket library required. if (!class_exists('SV_OpenAI_Realtime_Podcast')) { class SV_OpenAI_Realtime_Podcast { private static function readn($s,$n){$b='';while(strlen($b)<$n&&!feof($s)){$p=fread($s,$n-strlen($b));if($p===false||$p==='')break;$b.=$p;}return strlen($b)===$n?$b:false;} private static function send($s,$data){$n=strlen($data);$h=chr(0x81);if($n<126)$h.=chr(0x80|$n);elseif($n<65536)$h.=chr(0xfe).pack('n',$n);else $h.=chr(0xff).pack('NN',0,$n);$mask=random_bytes(4);$p='';for($i=0;$i<$n;$i++)$p.=$data[$i]^$mask[$i%4];$frame=$h.$mask.$p;while($frame!==''){$w=fwrite($s,$frame);if(!$w)return false;$frame=substr($frame,$w);}return true;} private static function frame($s){$h=self::readn($s,2);if($h===false)return false;$a=ord($h[0]);$b=ord($h[1]);$n=$b&127;if($n===126){$v=self::readn($s,2);if(!$v)return false;$n=unpack('n',$v)[1];}elseif($n===127){$v=self::readn($s,8);if(!$v)return false;$a64=unpack('Nhi/Nlo',$v);$n=$a64['hi']*4294967296+$a64['lo'];}if($n>12000000)return false;$mask=($b&128)?self::readn($s,4):'';$p=self::readn($s,(int)$n);if($p===false)return false;if($mask)for($i=0;$i<$n;$i++)$p[$i]=$p[$i]^$mask[$i%4];return ['opcode'=>$a&15,'data'=>$p];} private static function pcm($key,$model,$voice,$text){ $host='api.openai.com';$path='/v1/realtime?model='.rawurlencode($model); $ctx=stream_context_create(['ssl'=>['verify_peer'=>true,'verify_peer_name'=>true],'socket'=>['tcp_nodelay'=>true]]); $s=@stream_socket_client('tls://'.$host.':443',$errno,$errstr,15,STREAM_CLIENT_CONNECT,$ctx); if(!$s){error_log('[SV Realtime] connect '.$errstr);return false;} stream_set_timeout($s,35);$nonce=base64_encode(random_bytes(16)); $req="GET $path HTTP/1.1\r\nHost: $host\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: $nonce\r\nSec-WebSocket-Version: 13\r\nAuthorization: Bearer $key\r\nOpenAI-Beta: realtime=v1\r\n\r\n"; fwrite($s,$req);$headers='';while(strpos($headers,"\r\n\r\n")===false&&strlen($headers)<16000){$line=fgets($s,4096);if($line===false)break;$headers.=$line;} if(!preg_match('~^HTTP/1\\.[01] 101\\b~',$headers)){error_log('[SV Realtime] handshake '.substr($headers,0,90));fclose($s);return false;} // Separate response for every presenter; no conversation content is generated or altered. $session=['type'=>'session.update','session'=>['instructions'=>'Sei un doppiatore italiano. Parla ESCLUSIVAMENTE in italiano standard (it-IT) con cadenza giornalistica naturale. Leggi esattamente il testo richiesto, senza traduzioni o aggiunte. Non pronunciare istruzioni, etichette o nomi dei presentatori.','voice'=>$voice,'output_audio_format'=>'pcm16']]; $item=['type'=>'conversation.item.create','item'=>['type'=>'message','role'=>'user','content'=>[['type'=>'input_text','text'=>'Leggi ad alta voce ESATTAMENTE questo testo in italiano, senza cambiare parole e senza introduzioni: '.mb_substr($text,0,3500)]]]]; $response=['type'=>'response.create','response'=>['modalities'=>['audio'],'instructions'=>'Leggi SOLO la battuta specificata, in italiano standard, senza aggiungere parole.']]; foreach([$session,$item,$response] as $event)if(!self::send($s,wp_json_encode($event))){fclose($s);return false;} $pcm='';$complete=false;$start=microtime(true);$error=''; while(microtime(true)-$start<105){$f=self::frame($s);if(!$f)break;if($f['opcode']===9){self::send($s,$f['data']);continue;}if($f['opcode']===8)break;if($f['opcode']!==1)continue;$e=json_decode($f['data'],true);if(!is_array($e))continue; $type=$e['type']??''; if($type==='response.audio.delta'||$type==='response.output_audio.delta'){$b=base64_decode($e['delta']??'',true);if($b!==false)$pcm.=$b;} if($type==='response.done'){$complete=($e['response']['status']??'')==='completed';if(!$complete)$error=wp_json_encode($e['response']['status_details']??[]);break;} if($type==='error'){$error=(string)($e['error']['message']??'error');break;} } fclose($s); if(!$complete||strlen($pcm)<2000){error_log('[SV Realtime] failed '.$error);return false;} return $pcm; } public static function synthesize($text,$key,$model,$voice,$mp3,$ffmpeg='ffmpeg'){ if(!in_array($model,['gpt-realtime-2.1-mini','gpt-realtime-2.1'],true))return false; $pcm=self::pcm($key,$model,$voice,$text);if(!$pcm)return false; $temp=tempnam(sys_get_temp_dir(),'svrtpcm');if(!$temp)return false;file_put_contents($temp,$pcm); $cmd=escapeshellcmd($ffmpeg).' -nostdin -y -loglevel error -f s16le -ar 24000 -ac 1 -i '.escapeshellarg($temp).' -c:a libmp3lame -b:a 128k '.escapeshellarg($mp3).' 2>&1'; $o=[];$rc=1;exec($cmd,$o,$rc);@unlink($temp); return $rc===0&&is_file($mp3)&&filesize($mp3)>1000; } }}