PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 All 202 releases
← All changes | includes/AI/Providers/GeminiProvider.php +70 -0 4.9.1 → 4.9.4 View file →
@@ -118,8 +118,78 @@
118 118 );
119 119 }
120 120
121 121 /**
122 + * Transcribe audio or video.
123 + *
124 + * Gemini has no dedicated speech endpoint — the media rides in the ordinary
125 + * `generateContent` call as an `inline_data` part beside a text instruction,
126 + * and the model returns the transcript as its answer. Inline data is base64,
127 + * which inflates the payload by roughly a third; the caller caps media at
128 + * 15 MB for this platform so the request stays inside Gemini's inline
129 + * ceiling. (Files API upload would lift that, and is the obvious next step
130 + * if longer recordings are ever needed.)
131 + *
132 + * @param array $file `[ 'path', 'filename', 'mime' ]`
133 + * @param array $options `[ 'model', 'timeout', 'max_tokens' ]`
134 + * @return string|\WP_Error
135 + */
136 + public function transcribe( $file, $options = array() ) {
137 + if ( empty( $this->api_key ) ) {
138 + return new \WP_Error( 'no_api_key', sprintf( __( '%s API key is not configured.', 'betterdocs' ), $this->label() ) );
139 + }
140 +
141 + $bytes = @file_get_contents( $file['path'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local temp upload, not a remote fetch.
142 + if ( false === $bytes ) {
143 + return new \WP_Error( 'api_error', __( 'Could not read the uploaded file.', 'betterdocs' ) );
144 + }
145 +
146 + $model = ! empty( $options['model'] ) ? (string) $options['model'] : 'gemini-flash-latest';
147 + $timeout = isset( $options['timeout'] ) ? (int) $options['timeout'] : 120;
148 +
149 + $payload = array(
150 + 'contents' => array(
151 + array(
152 + 'role' => 'user',
153 + 'parts' => array(
154 + array( 'text' => __( 'Transcribe the speech in this recording verbatim as plain text. Do not summarise, translate or add commentary. If there is no speech, reply with nothing at all.', 'betterdocs' ) ),
155 + array( 'inline_data' => array(
156 + 'mime_type' => $file['mime'],
157 + 'data' => base64_encode( $bytes ),
158 + ) ),
159 + ),
160 + ),
161 + ),
162 + 'generationConfig' => array(
163 + 'maxOutputTokens' => isset( $options['max_tokens'] ) ? (int) $options['max_tokens'] : 8192,
164 + ),
165 + );
166 +
167 + unset( $bytes );
168 +
169 + $url = $this->base_url() . '/models/' . rawurlencode( $model ) . ':generateContent';
170 + $status = null;
171 + $data = $this->post_json(
172 + $url,
173 + array( 'Content-Type' => 'application/json', 'x-goog-api-key' => $this->api_key ),
174 + $payload,
175 + $timeout,
176 + $status
177 + );
178 +
179 + if ( is_wp_error( $data ) ) {
180 + return $data;
181 + }
182 +
183 + if ( ! empty( $data['error'] ) ) {
184 + $raw = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown API error.', 'betterdocs' );
185 + return new \WP_Error( 'provider_error', $this->classify_http_error( $status, $raw, $model ) );
186 + }
187 +
188 + return $this->extract_text( $data );
189 + }
190 +
191 + /**
122 192 * Concatenate all text parts of the first candidate.
123 193 *
124 194 * @param array $data
125 195 * @return string