Home → Blog → Speech to text in PHP: requests, uploads and error handling
Speech to text in PHP: requests, uploads and error handling
PHP still runs a large share of the web, and the task of accepting a recording and returning text comes up constantly. No special library is needed: cURL from the standard build is enough. Here is the working code and the usual traps.
Plain cURL
<?php
$ch = curl_init("https://voicesscribe.com/v1/audio/transcriptions");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("VS_KEY")],
CURLOPT_POSTFIELDS => [
"model" => "whisper-1",
"file" => new CURLFile("call.ogg", "audio/ogg", "call.ogg"),
],
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
throw new RuntimeException("error $code: $response");
}
echo json_decode($response, true)["text"];
CURLFile is mandatory: hand cURL a plain path string and modern PHP sends it as an ordinary text field, which the service rejects with 400.
The Guzzle version
$client = new GuzzleHttp\Client(["base_uri" => "https://voicesscribe.com/v1/"]);
$response = $client->post("audio/transcriptions", [
"headers" => ["Authorization" => "Bearer " . getenv("VS_KEY")],
"multipart" => [
["name" => "model", "contents" => "whisper-1"],
["name" => "file", "contents" => fopen("call.ogg", "r"), "filename" => "call.ogg"],
["name" => "response_format", "contents" => "verbose_json"],
],
]);
$data = json_decode((string) $response->getBody(), true);
The filename key is as mandatory here as it is everywhere else: without it the server cannot determine the format.
Accepting an upload
An upload form leaves the file in a temporary directory, and that is what you send:
$upload = $_FILES["audio"] ?? null;
if (!$upload || $upload["error"] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit("no file uploaded");
}
if ($upload["size"] > 25 * 1024 * 1024) {
http_response_code(413);
exit("file larger than 25 MB");
}
$file = new CURLFile($upload["tmp_name"], $upload["type"], $upload["name"]);
Remember upload_max_filesize and post_max_size in your PHP configuration: the defaults are often smaller than you need and the file never reaches your code.
Batch processing
A loop works for an archive but is slow. A queue is better: put jobs in a table and let several workers drain it, each with its own retry policy for temporary failures.
The default cURL timeout is too short for long recordings — set CURLOPT_TIMEOUT to at least 120 seconds, or an hour of audio will be cut off mid-processing.
Try it on your own recordings. Sign-up takes a minute, and the free minutes are enough to judge the quality.
Get a free API keyFrequently asked questions
Do I need a special PHP library?
No. cURL from the standard build is sufficient; Guzzle is simply more convenient for concurrent uploads.
Why do I get 400 when sending a file?
Most often the file was passed as a string instead of a CURLFile, or the multipart part has no filename. The service needs a name to detect the format.
How do I raise the upload limit?
Through upload_max_filesize and post_max_size in php.ini. On the service side the ceiling is 25 MB per file.
What about long recordings?
Compress to opus and, if needed, split into parts, processing them in parallel and joining the text in order.
Related reading
- Speech to text in Python: from one file to a working script — A working Python speech-to-text setup in ten minutes: install, the first request, response formats, error handling, and the mistakes that cost the most time.
- Speech to text in Node.js: working code and the usual pitfalls — How to transcribe audio from Node.js: the official SDK, streams and FormData, handling uploads in Express, timeouts and retries — with code you can paste.
- Transcription API errors and what they actually mean — A walkthrough of transcription API status codes: why 401, 400, 413, 429 and 502 happen, how to fix each one and which failures are worth retrying.