*/ public static function data( int $id ): array { $job = get_post_meta( $id, self::JOB, true ); $job = is_array( $job ) ? $job : array(); return array( 'id' => $id, 'transcribable' => self::is_transcribable( $id ), 'state' => (string) get_post_meta( $id, self::STATE, true ), 'progress' => $job['progress'] ?? null, 'error' => $job['error'] ?? null, 'errorCode' => $job['errorCode'] ?? null, 'recordingId' => $job['recordingId'] ?? null, 'transcriptionId' => (string) get_post_meta( $id, self::TRANSCRIPTION_ID, true ), 'transcript' => (string) get_post_meta( $id, self::TRANSCRIPT, true ), 'sentences' => self::sentences( $id ), 'language' => (string) get_post_meta( $id, self::LANGUAGE, true ), 'transcribedAt' => (string) get_post_meta( $id, self::TRANSCRIBED_AT, true ), ); } /** * One row of the media list: the attachment's VocalStack state without the * transcript itself. * * @param int $id Attachment ID. * @return array */ public static function row( int $id ): array { $job = get_post_meta( $id, self::JOB, true ); $job = is_array( $job ) ? $job : array(); $meta = wp_get_attachment_metadata( $id ); $transcript = (string) get_post_meta( $id, self::TRANSCRIPT, true ); $title = get_the_title( $id ); return array( 'id' => $id, 'title' => '' !== $title ? $title : wp_basename( (string) get_attached_file( $id ) ), 'kind' => str_starts_with( (string) get_post_mime_type( $id ), 'video/' ) ? 'video' : 'audio', 'length' => is_array( $meta ) && ! empty( $meta['length_formatted'] ) ? (string) $meta['length_formatted'] : '', 'state' => (string) get_post_meta( $id, self::STATE, true ), 'progress' => $job['progress'] ?? null, 'error' => $job['error'] ?? null, 'words' => '' === $transcript ? 0 : count( preg_split( '/\s+/u', trim( $transcript ) ) ), 'languages' => self::languages( $id ), 'autoLanguage' => self::auto_language( $id ), 'speakers' => '' === (string) get_post_meta( $id, self::SPEAKERS, true ) ? null : (int) get_post_meta( $id, self::SPEAKERS, true ), 'url' => (string) wp_get_attachment_url( $id ), 'transcribedAt' => (string) get_post_meta( $id, self::TRANSCRIBED_AT, true ), 'editUrl' => (string) get_edit_post_link( $id, 'raw' ), ); } /** * Every language detected in the transcript, most spoken first. * * @param int $id Attachment ID. * @return list */ public static function languages( int $id ): array { $languages = get_post_meta( $id, self::LANGUAGES, true ); if ( is_array( $languages ) && $languages ) { return array_values( $languages ); } $main = (string) get_post_meta( $id, self::LANGUAGE, true ); return '' === $main ? array() : array( $main ); } /** * Whether the transcript's language was auto-detected; null when the * transcript predates the plugin recording it. * * @param int $id Attachment ID. */ public static function auto_language( int $id ): ?bool { if ( ! metadata_exists( 'post', $id, self::REQUESTED ) ) { return null; } return '' === (string) get_post_meta( $id, self::REQUESTED, true ); } /** * The VocalStack recording behind the attachment's transcript, or ''. * * @param int $id Attachment ID. */ public static function recording_id( int $id ): string { $job = get_post_meta( $id, self::JOB, true ); return is_array( $job ) ? (string) ( $job['recordingId'] ?? '' ) : ''; } /** * The timed sentences. * * @param int $id Attachment ID. * @return list */ public static function sentences( int $id ): array { $sentences = get_post_meta( $id, self::SENTENCES, true ); return is_array( $sentences ) ? $sentences : array(); } /** * Store a finished transcript from VocalStack's `/transcript` answer. * * @param int $id Attachment ID. * @param array $transcript The answer. * @param string|null $requested The language asked for ('' for auto-detect), or null if unknown. */ public static function save_transcript( int $id, array $transcript, ?string $requested = null ): void { $sentences = array(); $languages = array(); $speakers = array(); foreach ( (array) ( $transcript['sentences'] ?? array() ) as $s ) { if ( ! empty( $s['suppressed'] ) || '' === trim( (string) ( $s['text'] ?? '' ) ) ) { continue; } if ( ! empty( $s['language'] ) ) { $languages[ $s['language'] ] = ( $languages[ $s['language'] ] ?? 0 ) + 1; } // -1: speakers were not told apart. if ( (int) ( $s['speaker'] ?? -1 ) >= 0 ) { $speakers[ (int) $s['speaker'] ] = true; } $sentences[] = array( 'start' => round( (float) $s['start'], 2 ), 'end' => round( (float) $s['end'], 2 ), 'speaker' => (int) ( $s['speaker'] ?? 0 ), 'text' => trim( (string) $s['text'] ), ); } // wp_slash: update_post_meta unslashes, which would eat backslashes in the text. update_post_meta( $id, self::SENTENCES, wp_slash( $sentences ) ); update_post_meta( $id, self::TRANSCRIPT, wp_slash( Text::plain( $sentences ) ) ); arsort( $languages ); $languages = array_keys( $languages ); if ( ! $languages && ! empty( $transcript['lang'] ) ) { $languages = array( (string) $transcript['lang'] ); } $labelled = 'off' !== ( $transcript['speakerLabels'] ?? 'off' ) || $speakers; $count = ! empty( $transcript['speakers'] ) && is_array( $transcript['speakers'] ) ? count( $transcript['speakers'] ) : count( $speakers ); update_post_meta( $id, self::LANGUAGE, (string) ( $transcript['lang'] ?? $languages[0] ?? '' ) ); update_post_meta( $id, self::LANGUAGES, $languages ); update_post_meta( $id, self::SPEAKERS, $labelled ? max( 1, $count ) : '' ); if ( null !== $requested ) { update_post_meta( $id, self::REQUESTED, $requested ); } update_post_meta( $id, self::TRANSCRIBED_AT, gmdate( 'c' ) ); } /** * Add VocalStack's state to attachments in the media modal. * * @param array $response The attachment for JS. * @param \WP_Post $post The attachment. * @return array */ public static function prepare_for_js( array $response, \WP_Post $post ): array { if ( self::is_transcribable( $post->ID ) ) { $response['vocalstack'] = array( 'state' => (string) get_post_meta( $post->ID, self::STATE, true ), 'hasTranscript' => '' !== (string) get_post_meta( $post->ID, self::TRANSCRIPT, true ), ); } return $response; } }