mirror of
https://github.com/forkless/Piperless.git
synced 2026-08-22 10:42:08 +02:00
Fix: FFmpeg Binaries Path now accepts directory-only paths (auto-appends /ffmpeg)
This commit is contained in:
@@ -79,11 +79,14 @@ class Cache_Manager {
|
||||
* @param string $quality Quality tier.
|
||||
* @return string SHA-256 hash.
|
||||
*/
|
||||
public function cache_key( string $text, string $model, string $language, string $quality, string $bitrate = '' ): string {
|
||||
public function cache_key( string $text, string $model, string $language, string $quality, string $bitrate = '', string $format = 'mp3' ): string {
|
||||
$seed = $text . '|' . $model . '|' . $language . '|' . $quality;
|
||||
if ( '' !== $bitrate ) {
|
||||
$seed .= '|br:' . $bitrate;
|
||||
}
|
||||
if ( '' !== $format ) {
|
||||
$seed .= '|fmt:' . $format;
|
||||
}
|
||||
return hash( 'sha256', $seed );
|
||||
}
|
||||
|
||||
@@ -94,7 +97,12 @@ class Cache_Manager {
|
||||
* @return bool
|
||||
*/
|
||||
public function exists( string $cache_key ): bool {
|
||||
return file_exists( $this->file_path( $cache_key ) );
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$format = $settings['piper_audio_format'] ?? 'mp3';
|
||||
return file_exists( $this->file_path( $cache_key, $format ) )
|
||||
|| file_exists( $this->file_path( $cache_key, 'mp3' ) )
|
||||
|| file_exists( $this->file_path( $cache_key, 'opus' ) )
|
||||
|| file_exists( $this->cache_dir . '/' . $cache_key . '.wav' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,8 +111,8 @@ class Cache_Manager {
|
||||
* @param string $cache_key Cache key.
|
||||
* @return string
|
||||
*/
|
||||
public function file_path( string $cache_key ): string {
|
||||
return $this->cache_dir . '/' . $cache_key . '.mp3';
|
||||
public function file_path( string $cache_key, string $format = 'mp3' ): string {
|
||||
return $this->cache_dir . '/' . $cache_key . '.' . $format;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,9 +135,13 @@ class Cache_Manager {
|
||||
public function put( string $cache_key, string $data ): bool {
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$ffmpeg = $this->find_ffmpeg();
|
||||
$format = $settings['piper_audio_format'] ?? 'mp3';
|
||||
$bitrate = ( 'opus' === $format )
|
||||
? ( $settings['piper_opus_bitrate'] ?? '24k' )
|
||||
: ( $settings['piper_mp3_bitrate'] ?? '32k' );
|
||||
|
||||
if ( null !== $ffmpeg ) {
|
||||
// Write WAV to temp, convert to MP3, store MP3.
|
||||
// Write WAV to temp, convert to selected format.
|
||||
$wav_tmp = tempnam( sys_get_temp_dir(), 'piperless_' ) . '.wav';
|
||||
|
||||
error_clear_last();
|
||||
@@ -139,30 +151,41 @@ class Cache_Manager {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mp3_path = $this->file_path( $cache_key );
|
||||
$bitrate = $settings['piper_mp3_bitrate'] ?? '32k';
|
||||
$cmd = sprintf(
|
||||
'%s -i %s -codec:a libmp3lame -b:a %s -ac 1 -y %s 2>&1',
|
||||
escapeshellarg( $ffmpeg ),
|
||||
escapeshellarg( $wav_tmp ),
|
||||
escapeshellarg( $bitrate ),
|
||||
escapeshellarg( $mp3_path )
|
||||
);
|
||||
$out_path = $this->file_path( $cache_key, $format );
|
||||
|
||||
if ( 'opus' === $format ) {
|
||||
$cmd = sprintf(
|
||||
'%s -i %s -c:a libopus -b:a %s -ac 1 -application voip -y %s 2>&1',
|
||||
escapeshellarg( $ffmpeg ),
|
||||
escapeshellarg( $wav_tmp ),
|
||||
escapeshellarg( $bitrate ),
|
||||
escapeshellarg( $out_path )
|
||||
);
|
||||
} else {
|
||||
$cmd = sprintf(
|
||||
'%s -i %s -codec:a libmp3lame -b:a %s -ac 1 -y %s 2>&1',
|
||||
escapeshellarg( $ffmpeg ),
|
||||
escapeshellarg( $wav_tmp ),
|
||||
escapeshellarg( $bitrate ),
|
||||
escapeshellarg( $out_path )
|
||||
);
|
||||
}
|
||||
|
||||
$output = [];
|
||||
$ret = 0;
|
||||
exec( $cmd, $output, $ret );
|
||||
@unlink( $wav_tmp );
|
||||
|
||||
if ( 0 === $ret && file_exists( $mp3_path ) && filesize( $mp3_path ) > 0 ) {
|
||||
$this->logger->info( 'Cached MP3: {key} ({size} bytes)', [
|
||||
if ( 0 === $ret && file_exists( $out_path ) && filesize( $out_path ) > 0 ) {
|
||||
$format_label = strtoupper( $format );
|
||||
$this->logger->info( "Cached {$format_label}: {key} ({size} bytes)", [
|
||||
'key' => $cache_key,
|
||||
'size' => filesize( $mp3_path ),
|
||||
'size' => filesize( $out_path ),
|
||||
] );
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->logger->error( 'ffmpeg conversion failed for {key}, code {code}', [
|
||||
$this->logger->error( "ffmpeg {$format} conversion failed for {key}, code {code}", [
|
||||
'key' => $cache_key,
|
||||
'code' => $ret,
|
||||
] );
|
||||
@@ -215,7 +238,7 @@ class Cache_Manager {
|
||||
$deleted = false;
|
||||
|
||||
// Delete MP3.
|
||||
$mp3_path = $this->file_path( $cache_key );
|
||||
$mp3_path = $this->file_path( $cache_key, 'mp3' );
|
||||
if ( file_exists( $mp3_path ) ) {
|
||||
if ( @unlink( $mp3_path ) ) {
|
||||
$deleted = true;
|
||||
@@ -224,6 +247,14 @@ class Cache_Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Opus.
|
||||
$opus_path = $this->file_path( $cache_key, 'opus' );
|
||||
if ( file_exists( $opus_path ) ) {
|
||||
if ( @unlink( $opus_path ) ) {
|
||||
$deleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete legacy WAV.
|
||||
$wav_path = $this->cache_dir . '/' . $cache_key . '.wav';
|
||||
if ( file_exists( $wav_path ) ) {
|
||||
@@ -263,6 +294,14 @@ class Cache_Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Opus files.
|
||||
$opus_files = glob( $this->cache_dir . '/*.opus' );
|
||||
if ( false !== $opus_files ) {
|
||||
foreach ( $opus_files as $file ) {
|
||||
if ( @unlink( $file ) ) { $count++; }
|
||||
}
|
||||
}
|
||||
|
||||
// Also clean up any legacy WAV files.
|
||||
$wav_files = glob( $this->cache_dir . '/*.wav' );
|
||||
if ( false !== $wav_files ) {
|
||||
@@ -314,16 +353,12 @@ class Cache_Manager {
|
||||
// "orphaned audio" in the user-facing sense.
|
||||
$preview_patterns = [ 'model_preview_*', 'piperless_test_preview*' ];
|
||||
foreach ( $preview_patterns as $pattern ) {
|
||||
$preview_files = glob( $this->cache_dir . '/' . $pattern . '.mp3' );
|
||||
if ( false !== $preview_files ) {
|
||||
foreach ( $preview_files as $file ) {
|
||||
@unlink( $file );
|
||||
}
|
||||
}
|
||||
$preview_wavs = glob( $this->cache_dir . '/' . $pattern . '.wav' );
|
||||
if ( false !== $preview_wavs ) {
|
||||
foreach ( $preview_wavs as $file ) {
|
||||
@unlink( $file );
|
||||
foreach ( [ '.mp3', '.opus', '.wav' ] as $ext ) {
|
||||
$matches = glob( $this->cache_dir . '/' . $pattern . $ext );
|
||||
if ( false !== $matches ) {
|
||||
foreach ( $matches as $file ) {
|
||||
@unlink( $file );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,12 +382,13 @@ class Cache_Manager {
|
||||
|
||||
$all_files = array_merge(
|
||||
(array) glob( $this->cache_dir . '/*.mp3' ),
|
||||
(array) glob( $this->cache_dir . '/*.opus' ),
|
||||
(array) glob( $this->cache_dir . '/*.wav' )
|
||||
);
|
||||
|
||||
foreach ( $all_files as $file ) {
|
||||
$key = basename( $file );
|
||||
$key = str_replace( [ '.mp3', '.wav' ], '', $key );
|
||||
$key = str_replace( [ '.mp3', '.opus', '.wav' ], '', $key );
|
||||
if ( str_starts_with( $key, 'model_preview_' ) || str_starts_with( $key, 'piperless_test_preview' ) ) {
|
||||
continue;
|
||||
}
|
||||
@@ -384,7 +420,7 @@ class Cache_Manager {
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_ffmpeg(): ?string {
|
||||
public function find_ffmpeg(): ?string {
|
||||
static $cached = null;
|
||||
static $resolved_path = null;
|
||||
|
||||
@@ -400,6 +436,10 @@ class Cache_Manager {
|
||||
$custom = $settings['piper_ffmpeg_binary'] ?? '';
|
||||
|
||||
if ( '' !== $custom ) {
|
||||
// Support both full binary path and directory-only path.
|
||||
if ( @is_dir( $custom ) ) {
|
||||
$custom = rtrim( $custom, '/' ) . '/ffmpeg';
|
||||
}
|
||||
if ( @file_exists( $custom ) && @is_executable( $custom ) ) {
|
||||
$cached = true;
|
||||
$resolved_path = $custom;
|
||||
@@ -440,7 +480,7 @@ class Cache_Manager {
|
||||
* @param int $per_page Entries per page.
|
||||
* @return array{entries:array,total:int,pages:int}
|
||||
*/
|
||||
public function get_entries( int $page = 1, int $per_page = 20 ): array {
|
||||
public function get_entries( int $page = 1, int $per_page = 20, string $sort_by = 'created', bool $sort_asc = false ): array {
|
||||
if ( ! is_dir( $this->cache_dir ) ) {
|
||||
return [ 'entries' => [], 'total' => 0, 'pages' => 0 ];
|
||||
}
|
||||
@@ -480,9 +520,10 @@ class Cache_Manager {
|
||||
)
|
||||
);
|
||||
|
||||
// Scan both MP3 and legacy WAV files.
|
||||
// Scan MP3, Opus, and legacy WAV files.
|
||||
$all_files = array_merge(
|
||||
(array) glob( $this->cache_dir . '/*.mp3' ),
|
||||
(array) glob( $this->cache_dir . '/*.opus' ),
|
||||
(array) glob( $this->cache_dir . '/*.wav' )
|
||||
);
|
||||
|
||||
@@ -513,9 +554,10 @@ class Cache_Manager {
|
||||
$size_bytes = filesize( $path );
|
||||
$is_mp3 = ( 'mp3' === $ext );
|
||||
|
||||
// Check for the companion format.
|
||||
$has_mp3 = $is_mp3 || file_exists( $this->cache_dir . '/' . $key . '.mp3' );
|
||||
$has_wav = ( ! $is_mp3 ) || file_exists( $this->cache_dir . '/' . $key . '.wav' );
|
||||
// Check for companion formats.
|
||||
$has_mp3 = $is_mp3 || file_exists( $this->cache_dir . '/' . $key . '.mp3' );
|
||||
$has_opus = ( 'opus' === $ext ) || file_exists( $this->cache_dir . '/' . $key . '.opus' );
|
||||
$has_wav = file_exists( $this->cache_dir . '/' . $key . '.wav' );
|
||||
|
||||
// Check if this entry is the active audio for its post.
|
||||
$enabled = false;
|
||||
@@ -533,7 +575,9 @@ class Cache_Manager {
|
||||
'filename' => basename( $path ),
|
||||
'size_bytes' => $size_bytes,
|
||||
'has_mp3' => $has_mp3,
|
||||
'bitrate' => $has_mp3 ? ( $settings['piper_mp3_bitrate'] ?? '32k' ) : '',
|
||||
'has_opus' => $has_opus,
|
||||
'bitrate' => $has_opus ? ( $settings['piper_opus_bitrate'] ?? '24k' )
|
||||
: ( $has_mp3 ? ( $settings['piper_mp3_bitrate'] ?? '32k' ) : '' ),
|
||||
'created' => $mtime ? gmdate( 'Y-m-d H:i', $mtime ) : '',
|
||||
'enabled' => $enabled,
|
||||
'model' => $model,
|
||||
@@ -545,6 +589,21 @@ class Cache_Manager {
|
||||
];
|
||||
}
|
||||
|
||||
// ── Sort before pagination ──────────────────────────────
|
||||
if ( 'size' === $sort_by ) {
|
||||
usort( $entries, function ( $a, $b ) use ( $sort_asc ) {
|
||||
return $sort_asc
|
||||
? ( ( $a['size_bytes'] ?? 0 ) <=> ( $b['size_bytes'] ?? 0 ) )
|
||||
: ( ( $b['size_bytes'] ?? 0 ) <=> ( $a['size_bytes'] ?? 0 ) );
|
||||
} );
|
||||
} else {
|
||||
usort( $entries, function ( $a, $b ) use ( $sort_asc ) {
|
||||
return $sort_asc
|
||||
? ( $a['created'] ?? '' ) <=> ( $b['created'] ?? '' )
|
||||
: ( $b['created'] ?? '' ) <=> ( $a['created'] ?? '' );
|
||||
} );
|
||||
}
|
||||
|
||||
$total = count( $entries );
|
||||
$pages = (int) ceil( $total / max( 1, $per_page ) );
|
||||
$page = max( 1, min( $page, max( 1, $pages ) ) );
|
||||
|
||||
@@ -365,6 +365,7 @@ class Gutenberg {
|
||||
return rest_ensure_response( [
|
||||
'url' => $result['url'],
|
||||
'duration' => get_post_meta( $post_id, '_piperless_duration', true ),
|
||||
'format' => get_post_meta( $post_id, '_piperless_audio_format', true ) ?: 'mp3',
|
||||
] );
|
||||
}
|
||||
|
||||
@@ -504,16 +505,25 @@ class Gutenberg {
|
||||
|
||||
set_transient( $rate_key, $rate_count + 1, 60 );
|
||||
|
||||
// Try MP3 first (canonical format).
|
||||
$mp3_path = $this->cache->file_path( $cache_key );
|
||||
// Try configured format first, then MP3, then Opus, fall back to WAV.
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$audio_format = $settings['piper_audio_format'] ?? 'mp3';
|
||||
|
||||
if ( file_exists( $mp3_path ) ) {
|
||||
return $this->stream_file( $mp3_path, 'audio/mpeg' );
|
||||
// Try the configured format.
|
||||
$format_path = $this->cache->file_path( $cache_key, $audio_format );
|
||||
if ( file_exists( $format_path ) ) {
|
||||
return $this->stream_file( $format_path, 'opus' === $audio_format ? 'audio/ogg' : 'audio/mpeg' );
|
||||
}
|
||||
|
||||
// Try the other format.
|
||||
$alt_format = ( 'opus' === $audio_format ) ? 'mp3' : 'opus';
|
||||
$alt_path = $this->cache->file_path( $cache_key, $alt_format );
|
||||
if ( file_exists( $alt_path ) ) {
|
||||
return $this->stream_file( $alt_path, 'opus' === $alt_format ? 'audio/ogg' : 'audio/mpeg' );
|
||||
}
|
||||
|
||||
// Fall back to legacy WAV.
|
||||
$wav_path = $this->cache->dir() . '/' . $cache_key . '.wav';
|
||||
|
||||
if ( ! file_exists( $wav_path ) ) {
|
||||
return new \WP_Error( 'not_found', __( 'Audio file not found.', 'piperless' ), [ 'status' => 404 ] );
|
||||
}
|
||||
|
||||
@@ -184,8 +184,8 @@ class Settings {
|
||||
],
|
||||
], $this->page_slug_piper );
|
||||
|
||||
$this->add_field( 'piper_ffmpeg_binary', __( 'FFmpeg Binary Path', 'piperless' ), 'text', 'piperless_piper_section', [
|
||||
'description' => __( 'Absolute path to ffmpeg for MP3 conversion. Auto-detected from common paths if left empty.', 'piperless' ),
|
||||
$this->add_field( 'piper_ffmpeg_binary', __( 'FFmpeg Binaries Path', 'piperless' ), 'text', 'piperless_piper_section', [
|
||||
'description' => __( 'Absolute path to ffmpeg tools for MP3/Opus conversion. Auto-detected from common paths if left empty.', 'piperless' ),
|
||||
], $this->page_slug_piper );
|
||||
|
||||
$this->add_field( 'piper_mp3_bitrate', __( 'MP3 Bitrate', 'piperless' ), 'select', 'piperless_piper_section', [
|
||||
@@ -196,6 +196,23 @@ class Settings {
|
||||
],
|
||||
], $this->page_slug_piper );
|
||||
|
||||
$this->add_field( 'piper_audio_format', __( 'Audio Format', 'piperless' ), 'select', 'piperless_piper_section', [
|
||||
'description' => __( 'Output audio format. MP3 is universally supported. Opus offers better quality at the same bitrate but has narrower browser support.', 'piperless' ),
|
||||
'options' => [
|
||||
'mp3' => 'MP3',
|
||||
'opus' => 'Opus',
|
||||
],
|
||||
], $this->page_slug_piper );
|
||||
|
||||
$this->add_field( 'piper_opus_bitrate', __( 'Opus Bitrate', 'piperless' ), 'select', 'piperless_piper_section', [
|
||||
'description' => __( 'Bitrate for Opus encoding. Opus achieves good quality at much lower bitrates than MP3. Mono output.', 'piperless' ),
|
||||
'options' => [
|
||||
'24k' => __( '24 kbps (standard)', 'piperless' ),
|
||||
'16k' => __( '16 kbps (compact)', 'piperless' ),
|
||||
'12k' => __( '12 kbps (minimal)', 'piperless' ),
|
||||
],
|
||||
], $this->page_slug_piper );
|
||||
|
||||
$this->add_field( 'piper_sentence_silence', __( 'Sentence Silence', 'piperless' ), 'text', 'piperless_piper_section', [
|
||||
'description' => __( 'Adds silence after each sentence, in seconds (e.g. 0.2, 0.5). Leave empty for Piper default. Applies to Standard mode only.', 'piperless' ),
|
||||
], $this->page_slug_piper );
|
||||
@@ -453,6 +470,10 @@ class Settings {
|
||||
$clean['default_quality'] = sanitize_text_field( $input['default_quality'] ?? 'medium' );
|
||||
$clean['piper_ffmpeg_binary'] = sanitize_text_field( $input['piper_ffmpeg_binary'] ?? '' );
|
||||
$clean['piper_mp3_bitrate'] = sanitize_text_field( $input['piper_mp3_bitrate'] ?? '32k' );
|
||||
$clean['piper_audio_format'] = in_array( $input['piper_audio_format'] ?? 'mp3', [ 'mp3', 'opus' ], true )
|
||||
? $input['piper_audio_format'] : 'mp3';
|
||||
$clean['piper_opus_bitrate'] = in_array( $input['piper_opus_bitrate'] ?? '24k', [ '24k', '16k', '12k' ], true )
|
||||
? $input['piper_opus_bitrate'] : '24k';
|
||||
$clean['piper_sentence_silence'] = sanitize_text_field( $input['piper_sentence_silence'] ?? '' );
|
||||
$clean['piper_length_scale'] = sanitize_text_field( $input['piper_length_scale'] ?? '' );
|
||||
$clean['player_style'] = sanitize_text_field( $input['player_style'] ?? 'classic' );
|
||||
@@ -863,8 +884,10 @@ class Settings {
|
||||
|
||||
$page = max( 1, (int) ( $_POST['page'] ?? 1 ) );
|
||||
$per_page = max( 5, min( 50, (int) ( $_POST['per_page'] ?? 20 ) ) );
|
||||
$sort_by = in_array( $_POST['sort_by'] ?? 'created', [ 'created', 'size' ], true ) ? $_POST['sort_by'] : 'created';
|
||||
$sort_asc = ( 'asc' === ( $_POST['sort_order'] ?? 'desc' ) );
|
||||
|
||||
wp_send_json_success( $this->cache->get_entries( $page, $per_page ) );
|
||||
wp_send_json_success( $this->cache->get_entries( $page, $per_page, $sort_by, $sort_asc ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -884,6 +907,36 @@ class Settings {
|
||||
}
|
||||
|
||||
$deleted = $this->cache->delete_entry( $key );
|
||||
|
||||
// Clear post meta if this entry was linked to a post.
|
||||
if ( $deleted ) {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
||||
'_piperless_cache_key'
|
||||
) );
|
||||
|
||||
foreach ( $rows as $row ) {
|
||||
$keys = maybe_unserialize( $row->meta_value );
|
||||
$owns = false;
|
||||
if ( is_array( $keys ) ) {
|
||||
$owns = array_key_exists( $key, $keys );
|
||||
} elseif ( is_string( $keys ) ) {
|
||||
$owns = ( $keys === $key );
|
||||
}
|
||||
|
||||
if ( $owns ) {
|
||||
delete_post_meta( (int) $row->post_id, '_piperless_audio_url' );
|
||||
delete_post_meta( (int) $row->post_id, '_piperless_cache_key' );
|
||||
delete_post_meta( (int) $row->post_id, '_piperless_duration' );
|
||||
delete_post_meta( (int) $row->post_id, '_piperless_model_name' );
|
||||
delete_post_meta( (int) $row->post_id, '_piperless_generated_at' );
|
||||
delete_post_meta( (int) $row->post_id, '_piperless_audio_format' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( [ 'deleted' => $deleted ] );
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,11 @@ class Transcriber {
|
||||
return $this->error( __( 'No text content available for this post.', 'piperless' ) );
|
||||
}
|
||||
|
||||
// Cache key (includes bitrate so changing it regenerates files).
|
||||
$bitrate = get_option( 'piperless_settings', [] )['piper_mp3_bitrate'] ?? '32k';
|
||||
$cache_key = $this->cache->cache_key( $text, $model, $language, $quality, $bitrate );
|
||||
// Cache key (includes bitrate and format so changing them regenerates files).
|
||||
$settings_cache = get_option( 'piperless_settings', [] );
|
||||
$bitrate = $settings_cache['piper_mp3_bitrate'] ?? '32k';
|
||||
$audio_format = $settings_cache['piper_audio_format'] ?? 'mp3';
|
||||
$cache_key = $this->cache->cache_key( $text, $model, $language, $quality, $bitrate, $audio_format );
|
||||
|
||||
// Store cache key in post meta before generation so it is tracked.
|
||||
$model_basename = basename( $model, '.onnx' );
|
||||
@@ -211,11 +213,14 @@ class Transcriber {
|
||||
$duration = get_post_meta( $post_id, '_piperless_duration', true );
|
||||
$cache_key = get_post_meta( $post_id, '_piperless_cache_key', true );
|
||||
|
||||
$format = get_post_meta( $post_id, '_piperless_audio_format', true );
|
||||
|
||||
return [
|
||||
'has_audio' => ! empty( $url ),
|
||||
'url' => $url ?: null,
|
||||
'duration' => $duration ? (float) $duration : null,
|
||||
'cache_key' => $cache_key ?: null,
|
||||
'format' => $format ?: 'mp3',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -376,6 +381,10 @@ class Transcriber {
|
||||
update_post_meta( $post_id, '_piperless_duration', $duration );
|
||||
update_post_meta( $post_id, '_piperless_generated_at', current_time( 'mysql', true ) );
|
||||
|
||||
// Store the audio format for the sidebar preview player.
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
update_post_meta( $post_id, '_piperless_audio_format', $settings['piper_audio_format'] ?? 'mp3' );
|
||||
|
||||
if ( '' !== $model_basename ) {
|
||||
update_post_meta( $post_id, '_piperless_model_name', $model_basename );
|
||||
}
|
||||
@@ -388,15 +397,37 @@ class Transcriber {
|
||||
* @return float Duration in seconds.
|
||||
*/
|
||||
public function wav_duration( string $cache_key ): float {
|
||||
$file_path = $this->cache->file_path( $cache_key );
|
||||
// Try compressed formats first via ffprobe.
|
||||
$settings = get_option( 'piperless_settings', [] );
|
||||
$format = $settings['piper_audio_format'] ?? 'mp3';
|
||||
$path = $this->cache->file_path( $cache_key, $format );
|
||||
if ( file_exists( $path ) ) {
|
||||
$dur = $this->ffprobe_duration( $path );
|
||||
if ( $dur > 0.0 ) {
|
||||
return $dur;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to WAV header reading.
|
||||
$file_path = $this->cache->file_path( $cache_key, 'mp3' );
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
$file_path = $this->cache->file_path( $cache_key, 'opus' );
|
||||
}
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
// Try legacy WAV path.
|
||||
$file_path = $this->cache->dir() . '/' . $cache_key . '.wav';
|
||||
}
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// If not WAV, try ffprobe.
|
||||
if ( ! str_ends_with( $file_path, '.wav' ) ) {
|
||||
$dur = $this->ffprobe_duration( $file_path );
|
||||
if ( $dur > 0.0 ) {
|
||||
return $dur;
|
||||
}
|
||||
}
|
||||
|
||||
$fp = @fopen( $file_path, 'rb' );
|
||||
if ( false === $fp ) {
|
||||
return 0.0;
|
||||
@@ -455,4 +486,74 @@ class Transcriber {
|
||||
'error' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio duration using ffprobe.
|
||||
*
|
||||
* @param string $file_path Absolute path to the audio file.
|
||||
* @return float Duration in seconds, or 0.0 on failure.
|
||||
*/
|
||||
private function ffprobe_duration( string $file_path ): float {
|
||||
$ffprobe = $this->find_ffprobe();
|
||||
if ( null === $ffprobe ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$cmd = sprintf(
|
||||
'%s -v error -show_entries format=duration -of csv=p=0 %s 2>&1',
|
||||
escapeshellarg( $ffprobe ),
|
||||
escapeshellarg( $file_path )
|
||||
);
|
||||
|
||||
$output = [];
|
||||
$ret = 0;
|
||||
exec( $cmd, $output, $ret );
|
||||
|
||||
if ( 0 !== $ret || empty( $output ) ) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) trim( $output[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ffprobe binary on the system.
|
||||
*
|
||||
* @return string|null Absolute path, or null.
|
||||
*/
|
||||
private function find_ffprobe(): ?string {
|
||||
static $cached = null;
|
||||
static $resolved = null;
|
||||
|
||||
if ( null !== $cached ) {
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
$cached = true;
|
||||
|
||||
// Use the same directory as ffmpeg — wherever it was resolved,
|
||||
// ffprobe is likely right next to it.
|
||||
$ffmpeg_path = $this->cache->find_ffmpeg();
|
||||
if ( null !== $ffmpeg_path ) {
|
||||
$candidate = dirname( $ffmpeg_path ) . '/ffprobe';
|
||||
if ( @is_executable( $candidate ) ) {
|
||||
$resolved = $candidate;
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: common paths extended via filter.
|
||||
$candidates = apply_filters(
|
||||
'piperless_ffprobe_paths',
|
||||
[ '/usr/bin/ffprobe', '/usr/local/bin/ffprobe', '/opt/bin/ffprobe' ]
|
||||
);
|
||||
foreach ( $candidates as $candidate ) {
|
||||
if ( @is_executable( $candidate ) ) {
|
||||
$resolved = $candidate;
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user