Source: Providers/OpenAI/Whisper.php

  1. <?php
  2. /**
  3. * OpenAI Whisper (speech to text) integration
  4. */
  5. namespace Classifai\Providers\OpenAI;
  6. use Classifai\Features\AudioTranscriptsGeneration;
  7. use Classifai\Providers\Provider;
  8. use WP_Error;
  9. class Whisper extends Provider {
  10. use \Classifai\Providers\OpenAI\OpenAI;
  11. /**
  12. * ID of the current provider.
  13. *
  14. * @var string
  15. */
  16. const ID = 'openai_whisper';
  17. /**
  18. * OpenAI Whisper URL
  19. *
  20. * @var string
  21. */
  22. protected $whisper_url = 'https://api.openai.com/v1/audio/';
  23. /**
  24. * OpenAI Whisper model
  25. *
  26. * @var string
  27. */
  28. protected $whisper_model = 'whisper-1';
  29. /**
  30. * Supported file formats
  31. *
  32. * @var array
  33. */
  34. public $file_formats = [
  35. 'mp3',
  36. 'mp4',
  37. 'mpeg',
  38. 'mpga',
  39. 'm4a',
  40. 'wav',
  41. 'webm',
  42. ];
  43. /**
  44. * Maximum file size our model supports
  45. *
  46. * @var int
  47. */
  48. public $max_file_size = 25 * MB_IN_BYTES;
  49. /**
  50. * OpenAI Whisper constructor.
  51. *
  52. * @param \Classifai\Features\Feature $feature_instance The feature instance.
  53. */
  54. public function __construct( $feature_instance = null ) {
  55. $this->feature_instance = $feature_instance;
  56. }
  57. /**
  58. * Builds the API url.
  59. *
  60. * @param string $path Path to append to API URL.
  61. * @return string
  62. */
  63. public function get_api_url( string $path = '' ): string {
  64. return sprintf( '%s%s', trailingslashit( $this->whisper_url ), $path );
  65. }
  66. /**
  67. * Register settings for this provider.
  68. */
  69. public function render_provider_fields() {
  70. $settings = $this->feature_instance->get_settings( static::ID );
  71. add_settings_field(
  72. static::ID . '_api_key',
  73. esc_html__( 'API Key', 'classifai' ),
  74. [ $this->feature_instance, 'render_input' ],
  75. $this->feature_instance->get_option_name(),
  76. $this->feature_instance->get_option_name() . '_section',
  77. [
  78. 'option_index' => static::ID,
  79. 'label_for' => 'api_key',
  80. 'input_type' => 'password',
  81. 'default_value' => $settings['api_key'],
  82. 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this.
  83. 'description' => $this->feature_instance->is_configured_with_provider( static::ID ) ?
  84. '' :
  85. sprintf(
  86. wp_kses(
  87. /* translators: %1$s is replaced with the OpenAI sign up URL */
  88. __( 'Don\'t have an OpenAI account yet? <a title="Sign up for an OpenAI account" href="%1$s">Sign up for one</a> in order to get your API key.', 'classifai' ),
  89. [
  90. 'a' => [
  91. 'href' => [],
  92. 'title' => [],
  93. ],
  94. ]
  95. ),
  96. esc_url( 'https://platform.openai.com/signup' )
  97. ),
  98. ]
  99. );
  100. do_action( 'classifai_' . static::ID . '_render_provider_fields', $this );
  101. }
  102. /**
  103. * Get the default settings for this provider.
  104. *
  105. * @return array
  106. */
  107. public function get_default_provider_settings(): array {
  108. $common_settings = [
  109. 'api_key' => '',
  110. 'authenticated' => false,
  111. ];
  112. return $common_settings;
  113. }
  114. /**
  115. * Sanitize the settings for this provider.
  116. *
  117. * @param array $new_settings New settings.
  118. * @return array
  119. */
  120. public function sanitize_settings( array $new_settings ): array {
  121. $settings = $this->feature_instance->get_settings();
  122. $api_key_settings = $this->sanitize_api_key_settings( $new_settings, $settings );
  123. $new_settings[ static::ID ]['api_key'] = $api_key_settings[ static::ID ]['api_key'];
  124. $new_settings[ static::ID ]['authenticated'] = $api_key_settings[ static::ID ]['authenticated'];
  125. return $new_settings;
  126. }
  127. /**
  128. * Common entry point for all REST endpoints for this provider.
  129. *
  130. * @param int $post_id The Post ID we're processing.
  131. * @param string $route_to_call The route we are processing.
  132. * @param array $args Optional arguments to pass to the route.
  133. * @return string|WP_Error
  134. */
  135. public function rest_endpoint_callback( $post_id = 0, string $route_to_call = '', array $args = [] ) {
  136. if ( ! $post_id || ! get_post( $post_id ) ) {
  137. return new WP_Error( 'post_id_required', esc_html__( 'A valid attachment ID is required to generate a transcript.', 'classifai' ) );
  138. }
  139. $route_to_call = strtolower( $route_to_call );
  140. $return = '';
  141. // Handle all of our routes.
  142. switch ( $route_to_call ) {
  143. case 'transcript':
  144. $return = $this->transcribe_audio( $post_id, $args );
  145. break;
  146. }
  147. return $return;
  148. }
  149. /**
  150. * Start the audio transcription process.
  151. *
  152. * @param int $attachment_id Attachment ID to process.
  153. * @param array $args Optional arguments passed in.
  154. * @return WP_Error|bool
  155. */
  156. public function transcribe_audio( int $attachment_id = 0, array $args = [] ) {
  157. if ( $attachment_id && ! current_user_can( 'edit_post', $attachment_id ) && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) {
  158. return new \WP_Error( 'no_permission', esc_html__( 'User does not have permission to edit this attachment.', 'classifai' ) );
  159. }
  160. $feature = new AudioTranscriptsGeneration();
  161. if ( ! $feature->is_feature_enabled() ) {
  162. return new WP_Error( 'not_enabled', esc_html__( 'Transcript generation is disabled. Please check your settings.', 'classifai' ) );
  163. }
  164. if ( ! $feature->should_process( $attachment_id ) ) {
  165. return new WP_Error( 'process_error', esc_html__( 'Attachment does not meet processing requirements. Ensure the file type and size meet requirements.', 'classifai' ) );
  166. }
  167. $settings = $feature->get_settings( static::ID );
  168. $request = new APIRequest( $settings['api_key'] ?? '', $feature->get_option_name() );
  169. /**
  170. * Filter the request body before sending to Whisper.
  171. *
  172. * @since 2.2.0
  173. * @hook classifai_whisper_transcribe_request_body
  174. *
  175. * @param {array} $body Request body that will be sent to Whisper.
  176. * @param {int} $attachment_id ID of attachment we are transcribing.
  177. *
  178. * @return {array} Request body.
  179. */
  180. $body = apply_filters(
  181. 'classifai_whisper_transcribe_request_body',
  182. [
  183. 'file' => get_attached_file( $attachment_id ) ?? '',
  184. 'model' => $this->whisper_model,
  185. 'response_format' => 'json',
  186. 'temperature' => 0,
  187. ],
  188. $attachment_id
  189. );
  190. // Make our API request.
  191. $response = $request->post_form(
  192. $this->get_api_url( 'transcriptions' ),
  193. $body
  194. );
  195. set_transient( 'classifai_openai_whisper_latest_response', $response, DAY_IN_SECONDS * 30 );
  196. // Extract out the text response, if it exists.
  197. if ( ! is_wp_error( $response ) && isset( $response['text'] ) ) {
  198. $response = $response['text'];
  199. }
  200. return $response;
  201. }
  202. /**
  203. * Returns the debug information for the provider settings.
  204. *
  205. * @return array
  206. */
  207. public function get_debug_information(): array {
  208. $settings = $this->feature_instance->get_settings();
  209. $debug_info = [];
  210. if ( $this->feature_instance instanceof AudioTranscriptsGeneration ) {
  211. $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_whisper_latest_response' ) );
  212. }
  213. return apply_filters(
  214. 'classifai_' . self::ID . '_debug_information',
  215. $debug_info,
  216. $settings,
  217. $this->feature_instance
  218. );
  219. }
  220. }