| @@ -46,8 +46,65 @@ | ||
| 46 | 46 | return parent::build_payload( $model, $messages, $max_tokens, $temperature ); |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | /** |
| 50 | + * Transcribe audio or video via OpenAI's speech-to-text endpoint. | |
| 51 | + * | |
| 52 | + * The endpoint accepts video containers (mp4, webm, mpeg) as well as audio | |
| 53 | + * and reads the audio track out of them, which is why this feature needs no | |
| 54 | + * ffmpeg on the host — something no WordPress host can be assumed to have. | |
| 55 | + * | |
| 56 | + * `response_format=text` returns the transcript as a bare string rather than | |
| 57 | + * JSON; we ask for `json` instead so a provider error still decodes into the | |
| 58 | + * usual `{ error: { message } }` shape that post_multipart() can report. | |
| 59 | + * | |
| 60 | + * @param array $file `[ 'path', 'filename', 'mime' ]` | |
| 61 | + * @param array $options `[ 'model', 'timeout' ]` | |
| 62 | + * @return string|\WP_Error | |
| 63 | + */ | |
| 64 | + public function transcribe( $file, $options = array() ) { | |
| 65 | + if ( empty( $this->api_key ) ) { | |
| 66 | + return new \WP_Error( 'no_api_key', sprintf( | |
| 67 | + /* translators: %s: provider label */ | |
| 68 | + __( '%s API key is not configured.', 'betterdocs' ), | |
| 69 | + $this->label() | |
| 70 | + ) ); | |
| 71 | + } | |
| 72 | + | |
| 73 | + $model = ! empty( $options['model'] ) ? (string) $options['model'] : 'gpt-4o-mini-transcribe'; | |
| 74 | + $timeout = isset( $options['timeout'] ) ? (int) $options['timeout'] : 120; | |
| 75 | + $status = null; | |
| 76 | + | |
| 77 | + $data = $this->post_multipart( | |
| 78 | + $this->base_url() . '/audio/transcriptions', | |
| 79 | + array( 'Authorization' => 'Bearer ' . $this->api_key ), | |
| 80 | + array( | |
| 81 | + 'model' => $model, | |
| 82 | + 'response_format' => 'json', | |
| 83 | + ), | |
| 84 | + array( | |
| 85 | + 'name' => 'file', | |
| 86 | + 'filename' => $file['filename'], | |
| 87 | + 'type' => $file['mime'], | |
| 88 | + 'path' => $file['path'], | |
| 89 | + ), | |
| 90 | + $timeout, | |
| 91 | + $status | |
| 92 | + ); | |
| 93 | + | |
| 94 | + if ( is_wp_error( $data ) ) { | |
| 95 | + return $data; | |
| 96 | + } | |
| 97 | + | |
| 98 | + if ( isset( $data['error'] ) ) { | |
| 99 | + $message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown error.', 'betterdocs' ); | |
| 100 | + return new \WP_Error( 'provider_error', $this->classify_http_error( $status, $message, $model ) ); | |
| 101 | + } | |
| 102 | + | |
| 103 | + return isset( $data['text'] ) ? (string) $data['text'] : ''; | |
| 104 | + } | |
| 105 | + | |
| 106 | + /** | |
| 50 | 107 | * Default reasoning_effort for a gpt-5* model. |
| 51 | 108 | * |
| 52 | 109 | * The original GPT-5 generation (gpt-5, gpt-5-mini, gpt-5-nano) accepts |
| 53 | 110 | * 'minimal'. The gpt-5.x point releases (e.g. gpt-5.5) dropped 'minimal' |