*/ public function extractPayloads(string &$buffer): array { $frames = explode("\n\n", str_replace("\r\n", "\n", $buffer)); $buffer = array_pop($frames) ?? ''; $payloads = []; foreach ($frames as $frame) { foreach ($this->dataLines($frame) as $payload) { if ($payload === '[DONE]' || $payload === '') { continue; } $payloads[] = $payload; } } return $payloads; } /** * Flush a trailing incomplete frame at end-of-stream. * * @return list */ public function flushBuffer(string &$buffer): array { $trimmed = trim($buffer); if ($trimmed === '') { $buffer = ''; return []; } $buffer .= "\n\n"; return $this->extractPayloads($buffer); } /** * Parse one SSE JSON payload into append/replace/end/done fields. * * @return array{append: ?string, replace: ?string, end: ?float, done: bool, legacy: bool}|null */ public function parseEvent(string $json): ?array { $data = json_decode($json, true); if (! is_array($data)) { return null; } $type = $data['type'] ?? null; if ($type === 'transcript.text.delta') { $delta = $data['delta'] ?? ''; if (! is_string($delta) || $delta === '') { return null; } return [ 'append' => $delta, 'replace' => null, 'end' => $this->nullableFloat($data['end'] ?? null), 'done' => false, 'legacy' => false, ]; } if ($type === 'transcript.text.done') { $text = $data['text'] ?? ''; return [ 'append' => null, 'replace' => is_string($text) ? $text : '', 'end' => $this->nullableFloat($data['end'] ?? null), 'done' => true, 'legacy' => false, ]; } if ($type === null && isset($data['text']) && is_string($data['text']) && $data['text'] !== '') { return [ 'append' => $data['text'], 'replace' => null, 'end' => $this->nullableFloat($data['end'] ?? null), 'done' => false, 'legacy' => true, ]; } return null; } /** * Apply a parsed event to the accumulated transcript. * * @param array{append: ?string, replace: ?string, end: ?float, done: bool, legacy: bool} $event */ public function applyEvent(string $accumulated, array $event): string { if (is_string($event['replace'])) { return $event['replace']; } $chunk = $event['append'] ?? ''; if ($chunk === '') { return $accumulated; } if ($event['legacy']) { $chunk = trim($chunk); if ($accumulated === '') { return $chunk; } return rtrim($accumulated).' '.$chunk; } return $accumulated.$chunk; } /** * @return list */ private function dataLines(string $frame): array { $payloads = []; foreach (explode("\n", $frame) as $line) { if (str_starts_with($line, 'data:')) { $payloads[] = ltrim(substr($line, 5)); } } return $payloads; } private function nullableFloat(mixed $value): ?float { if (! is_numeric($value)) { return null; } return (float) $value; } }