.mp3"} */ use Google\Cloud\TextToSpeech\V1\Client\TextToSpeechClient; // microgenerator client use Google\Cloud\TextToSpeech\V1\AudioConfig; use Google\Cloud\TextToSpeech\V1\AudioEncoding; use Google\Cloud\TextToSpeech\V1\SynthesisInput; use Google\Cloud\TextToSpeech\V1\VoiceSelectionParams; use Google\Cloud\TextToSpeech\V1\SynthesizeSpeechRequest; header('Content-Type: application/json; charset=utf-8'); set_time_limit(25); /* ---- CONFIG ---- */ const GCP_CREDENTIALS = 'E:\\gkey\\tts-sa.json'; const OUT_DIR = __DIR__ . '/out'; const PUBLIC_URL_BASE = '/tts/out'; const QUEUE_FILE = __DIR__ . '/queue.jsonl'; const DEBUG_LOG_FILE = __DIR__ . '/tts-debug.jsonl'; const ENFORCE_AUTH = false; // flip to true to require X-API-Key const API_KEY = 'passTTS123'; const DEFAULT_LANG = 'ja-JP'; const DEFAULT_VOICE = 'ja-JP-Chirp3-HD-Sulafat'; const DEFAULT_RATE = 0.0; // 0.25–2.0 (non-Chirp only) const DEFAULT_PITCH = 5.0; // semitones, -20.0..+20.0 const DEFAULT_VOLUME_DB = 0.0; // dB, -96.0..+16.0 const DEFAULT_EFFECTS = []; // e.g., ['wearable-class-device'] const DEFAULT_FORMAT = 'MP3'; // MP3 | OGG | LINEAR16 /* ---- DEBUG LOGGING ---- */ function logj(string $event, array $data = []): void { $rec = $data + [ 'event' => $event, 'ts' => date('c'), ]; @file_put_contents( DEBUG_LOG_FILE, json_encode($rec, JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX ); } set_error_handler(function($sev, $msg, $file, $line){ logj('php_error', ['severity'=>$sev,'message'=>$msg,'file'=>$file,'line'=>$line]); return false; // also let PHP handle it normally }); set_exception_handler(function($e){ logj('php_exception', ['error'=>$e->getMessage(),'trace'=>$e->getTraceAsString()]); http_response_code(500); $payload = ['ok'=>false,'error'=>'Unhandled: '.$e->getMessage()]; echo json_encode($payload); logj('response', $payload); exit; }); /* ---- COMPOSER AUTOLOAD (local vendor only) ---- */ $autoload = __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; if (!is_file($autoload)) { http_response_code(500); $payload = ['ok'=>false,'error'=>'Composer autoload not found at '.$autoload]; echo json_encode($payload); logj('response', $payload); exit; } require $autoload; if (!class_exists(\Google\Cloud\TextToSpeech\V1\Client\TextToSpeechClient::class)) { http_response_code(500); $payload = ['ok'=>false,'error'=>'TextToSpeechClient class not found under V1\\Client.']; echo json_encode($payload); logj('response', $payload); exit; } /* ---- AUTH (optional) ---- */ if (ENFORCE_AUTH) { $hdrs = function_exists('getallheaders') ? (getallheaders() ?: []) : []; $clientKey = $hdrs['X-API-Key'] ?? $hdrs['x-api-key'] ?? ''; if ($clientKey !== API_KEY) { http_response_code(401); $payload = ['ok'=>false,'error'=>'Unauthorized']; echo json_encode($payload); logj('response', $payload); exit; } } /* ---- INPUT ---- */ $raw = file_get_contents('php://input') ?: ''; logj('request_raw', ['bytes'=>strlen($raw), 'preview'=>substr($raw,0,256)]); $body = json_decode($raw, true); if (!is_array($body)) { // allow plain text as a fallback $raw = trim($raw); if ($raw === '') { http_response_code(400); $payload = ['ok'=>false,'error'=>'Missing body']; echo json_encode($payload); logj('response', $payload); exit; } $body = ['text' => $raw]; } if (!isset($body['text']) || trim((string)$body['text']) === '') { http_response_code(400); $payload = ['ok'=>false,'error'=>'Missing "text"']; echo json_encode($payload); logj('response', $payload); exit; } $text = trim((string)$body['text']); $lang = (string)($body['lang'] ?? DEFAULT_LANG); $voice = trim((string)($body['voice'] ?? DEFAULT_VOICE)); $format = strtoupper((string)($body['audio_format'] ?? DEFAULT_FORMAT)); // Optional audio tuning from body $rate = isset($body['rate']) ? (float)$body['rate'] : DEFAULT_RATE; $pitch = isset($body['pitch']) ? (float)$body['pitch'] : DEFAULT_PITCH; $volumeDb = isset($body['volume_db']) ? (float)$body['volume_db'] : DEFAULT_VOLUME_DB; $effectsBody = $body['effects_profile'] ?? DEFAULT_EFFECTS; $effects = is_array($effectsBody) ? $effectsBody : (($effectsBody!=='') ? [$effectsBody] : []); logj('request_parsed', [ 'lang'=>$lang, 'voice'=>$voice, 'format'=>$format, 'rate'=>$rate, 'pitch'=>$pitch, 'volume_db'=>$volumeDb, 'effects_profile'=>$effects, 'text_len'=>strlen($text), 'text_preview'=>substr($text,0,160) ]); /* ---- PREP OUTPUT ---- */ @mkdir(OUT_DIR, 0775, true); $id = date('Ymd_His') . '_' . bin2hex(random_bytes(3)); $ext = map_ext($format); $absPath = sprintf('%s/%s.%s', OUT_DIR, $id, $ext); $url = sprintf('%s/%s.%s', PUBLIC_URL_BASE, $id, $ext); /* ---- HELPERS ---- */ function map_encoding(string $fmt): int { switch (strtoupper($fmt)) { case 'OGG': return AudioEncoding::OGG_OPUS; case 'LINEAR16': return AudioEncoding::LINEAR16; case 'MP3': default: return AudioEncoding::MP3; } } function map_ext(string $fmt): string { switch (strtoupper($fmt)) { case 'OGG': return 'ogg'; case 'LINEAR16': return 'ogg'; // (or 'oga' if you prefer) case 'LINEAR16': return 'wav'; case 'MP3': default: return 'mp3'; } } function ssml_escape_text(string $s): string { $s = str_replace(['&','<','>'], ['&','<','>'], $s); $s = preg_replace('/\s+/u', ' ', $s); return trim($s); } /** Simple JP container you already had; safe for non-Chirp voices. */ function build_basic_ssml(string $text): string { $t = ssml_escape_text($text); return <<

$t

SSML; } /** Chirp detector: “chirp” anywhere in the name. */ function is_chirp_voice(string $name): bool { return (strpos(strtolower($name), 'chirp') !== false); } function clampf(float $v, float $lo, float $hi): float { return max($lo, min($hi, $v)); } /* ---- BUILD REQUEST ---- */ $isChirp = is_chirp_voice($voice ?: DEFAULT_VOICE); // INPUT: Chirp must be plain text; others can be SSML if ($isChirp) { $input = (new SynthesisInput())->setText($text); } else { $input = (new SynthesisInput())->setSsml(build_basic_ssml($text)); } $voiceSel = (new VoiceSelectionParams())->setLanguageCode($lang); if ($voice !== '') { $voiceSel->setName($voice); } else { $voiceSel->setName(DEFAULT_VOICE); } $audioCfg = (new AudioConfig())->setAudioEncoding(map_encoding($format)); // Apply volume and effects for all voices $audioCfg->setVolumeGainDb(clampf($volumeDb, -96.0, 16.0)); if (!empty($effects)) { $audioCfg->setEffectsProfileId($effects); // array OK } // Rate/Pitch for non-Chirp voices if (!$isChirp) { $audioCfg->setSpeakingRate(clampf($rate, 0.25, 2.0)); // per docs $audioCfg->setPitch(clampf($pitch, -20.0, 20.0)); // semitones } logj('build_request', [ 'isChirp'=>$isChirp, 'voice'=>$voiceSel->getName(), 'lang'=>$voiceSel->getLanguageCode(), 'format'=>$format, 'out'=>$absPath ]); logj('audio_tuning', [ 'isChirp'=>$isChirp, 'rate'=>$rate, 'pitch'=>$pitch, 'volume_db'=>$volumeDb, 'effects'=>$effects ]); /* ---- CALL GOOGLE (microgenerator) ---- */ try { $opts = ['transport' => 'rest']; if (GCP_CREDENTIALS !== '' && is_file(GCP_CREDENTIALS)) { $opts['credentials'] = GCP_CREDENTIALS; } $client = new TextToSpeechClient($opts); $request = (new SynthesizeSpeechRequest()) ->setInput($input) ->setVoice($voiceSel) ->setAudioConfig($audioCfg); try { $resp = $client->synthesizeSpeech($request, ['timeoutMillis' => 10000]); logj('call_primary_ok'); } catch (\Throwable $e) { // Fallback: retry without specific voice name (language only) logj('call_primary_err', ['error'=>$e->getMessage()]); $request->setVoice((new VoiceSelectionParams())->setLanguageCode($lang)); $resp = $client->synthesizeSpeech($request, ['timeoutMillis' => 10000]); logj('call_fallback_ok'); } $bytes = $resp->getAudioContent(); $blen = is_string($bytes) ? strlen($bytes) : 0; logj('tts_result', ['bytes'=>$blen]); if (!$bytes) { http_response_code(502); $payload = ['ok'=>false,'error'=>'TTS returned empty audio']; echo json_encode($payload); logj('response', $payload); exit; } if (file_put_contents($absPath, $bytes) === false) { http_response_code(500); $payload = ['ok'=>false,'error'=>'Failed to write audio file','path'=>$absPath]; echo json_encode($payload); logj('response', $payload); exit; } // Publish for listener/stream publishAudio($id, $url, time()); $payload = ['ok'=>true,'id'=>$id,'url'=>$url]; echo json_encode($payload, JSON_UNESCAPED_SLASHES); logj('response', $payload); } catch (\Throwable $e) { http_response_code(500); $payload = ['ok'=>false,'error'=>$e->getMessage()]; echo json_encode($payload); logj('response', $payload); exit; } /* ---- SSE QUEUE ---- */ function publishAudio(string $id, string $url, int $ts): void { $line = json_encode(['id'=>$id,'url'=>$url,'ts'=>$ts], JSON_UNESCAPED_SLASHES) . "\n"; $f = fopen(QUEUE_FILE, 'ab'); if ($f) { flock($f, LOCK_EX); fwrite($f, $line); fflush($f); flock($f, LOCK_UN); fclose($f); logj('queue_publish', ['id'=>$id,'url'=>$url]); } else { logj('queue_error', ['id'=>$id,'url'=>$url]); } }