文章目录[隐藏]
WordPress插件开发教程:实现网站文章自动转语音并生成播客订阅
引言:为什么需要文章转语音功能?
在当今快节奏的数字时代,用户获取信息的方式日益多样化。虽然阅读文字内容仍然是主要方式,但越来越多的人开始通过音频内容获取信息——在通勤途中、做家务时、运动时,音频内容提供了无需视觉参与的便利。根据Edison Research的数据,2023年有超过1亿美国人每月收听播客,这一数字比五年前增长了近一倍。
对于WordPress网站所有者而言,将文章内容转换为音频格式具有多重优势:
- 提高内容可访问性,服务视觉障碍用户
- 增加用户停留时间,降低跳出率
- 拓展内容分发渠道,触及更广泛的受众
- 提升SEO表现,增加网站可见性
- 创造新的变现机会,如播客广告
本教程将详细指导您开发一个完整的WordPress插件,实现文章自动转语音并生成播客订阅功能。我们将从零开始,逐步构建这个功能强大的工具。
第一部分:开发环境准备与插件基础结构
1.1 开发环境配置
在开始插件开发前,确保您已准备好以下环境:
- 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel
- WordPress安装:最新版本的WordPress(建议5.8+)
- 代码编辑器:VS Code、PHPStorm或Sublime Text
- PHP版本:7.4或更高版本
- 调试工具:安装Query Monitor和Debug Bar插件
1.2 创建插件基础文件结构
首先,在WordPress的wp-content/plugins/目录下创建一个新文件夹,命名为article-to-podcast。在该文件夹中创建以下基础文件:
article-to-podcast/
├── article-to-podcast.php # 主插件文件
├── uninstall.php # 卸载脚本
├── includes/ # 核心功能文件
│ ├── class-tts-engine.php # 文字转语音引擎
│ ├── class-podcast-feed.php # 播客Feed生成
│ ├── class-admin-ui.php # 管理界面
│ └── class-ajax-handler.php # AJAX处理
├── assets/ # 静态资源
│ ├── css/
│ ├── js/
│ └── images/
├── languages/ # 国际化文件
└── templates/ # 前端模板
1.3 编写插件主文件
打开article-to-podcast.php,添加以下代码作为插件头部信息:
<?php
/**
* Plugin Name: Article to Podcast Converter
* Plugin URI: https://yourwebsite.com/article-to-podcast
* Description: 自动将WordPress文章转换为语音并生成播客订阅
* Version: 1.0.0
* Author: Your Name
* Author URI: https://yourwebsite.com
* License: GPL v2 or later
* Text Domain: article-to-podcast
* Domain Path: /languages
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('ATPC_VERSION', '1.0.0');
define('ATPC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('ATPC_PLUGIN_URL', plugin_dir_url(__FILE__));
define('ATPC_PLUGIN_BASENAME', plugin_basename(__FILE__));
// 自动加载类文件
spl_autoload_register(function ($class) {
$prefix = 'ATPC_';
$base_dir = ATPC_PLUGIN_DIR . 'includes/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php';
if (file_exists($file)) {
require $file;
}
});
// 初始化插件
function atpc_init() {
// 检查必要扩展
if (!extension_loaded('simplexml')) {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>';
echo __('Article to Podcast插件需要SimpleXML扩展。请联系您的主机提供商启用此扩展。', 'article-to-podcast');
echo '</p></div>';
});
return;
}
// 初始化核心类
$tts_engine = new ATPC_TTS_Engine();
$podcast_feed = new ATPC_Podcast_Feed();
$admin_ui = new ATPC_Admin_UI();
$ajax_handler = new ATPC_Ajax_Handler();
// 注册激活/停用钩子
register_activation_hook(__FILE__, ['ATPC_Admin_UI', 'activate_plugin']);
register_deactivation_hook(__FILE__, ['ATPC_Admin_UI', 'deactivate_plugin']);
// 加载文本域
load_plugin_textdomain('article-to-podcast', false, dirname(ATPC_PLUGIN_BASENAME) . '/languages');
}
add_action('plugins_loaded', 'atpc_init');
第二部分:文字转语音引擎实现
2.1 选择TTS(文字转语音)服务
目前市场上有多种TTS服务可供选择,每种都有其优缺点:
- Google Cloud Text-to-Speech:质量高,支持多种语言,但需要付费
- Amazon Polly:自然语音,价格合理,有免费套餐
- Microsoft Azure Cognitive Services:语音自然度高,支持情感表达
- IBM Watson Text to Speech:企业级解决方案
- 本地解决方案:如eSpeak(免费但质量较低)
本教程将使用Amazon Polly作为示例,因为它提供每月500万字符的免费套餐,适合中小型网站。
2.2 实现TTS引擎类
创建includes/class-tts-engine.php文件:
<?php
class ATPC_TTS_Engine {
private $aws_access_key;
private $aws_secret_key;
private $aws_region;
private $polly_client;
public function __construct() {
$options = get_option('atpc_settings');
$this->aws_access_key = isset($options['aws_access_key']) ? $options['aws_access_key'] : '';
$this->aws_secret_key = isset($options['aws_secret_key']) ? $options['aws_secret_key'] : '';
$this->aws_region = isset($options['aws_region']) ? $options['aws_region'] : 'us-east-1';
// 初始化AWS Polly客户端
$this->init_polly_client();
// 添加文章保存钩子
add_action('save_post', [$this, 'generate_audio_on_save'], 10, 3);
}
private function init_polly_client() {
if (empty($this->aws_access_key) || empty($this->aws_secret_key)) {
return;
}
try {
require_once ATPC_PLUGIN_DIR . 'vendor/autoload.php';
$this->polly_client = new AwsPollyPollyClient([
'version' => 'latest',
'region' => $this->aws_region,
'credentials' => [
'key' => $this->aws_access_key,
'secret' => $this->aws_secret_key
]
]);
} catch (Exception $e) {
error_log('ATPC: Failed to initialize Polly client - ' . $e->getMessage());
}
}
public function generate_audio_on_save($post_id, $post, $update) {
// 检查是否自动生成音频
$auto_generate = get_option('atpc_auto_generate', 'yes');
if ($auto_generate !== 'yes') {
return;
}
// 检查文章状态和类型
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
return;
}
$allowed_post_types = get_option('atpc_post_types', ['post']);
if (!in_array($post->post_type, $allowed_post_types)) {
return;
}
// 检查文章是否已发布
if ($post->post_status !== 'publish') {
return;
}
// 生成音频
$this->generate_audio($post_id);
}
public function generate_audio($post_id) {
$post = get_post($post_id);
if (!$post) {
return false;
}
// 获取文章内容
$content = $this->prepare_content($post);
// 检查内容长度
if (strlen($content) < 50) {
error_log('ATPC: Content too short for post ID ' . $post_id);
return false;
}
// 生成音频文件
$audio_url = $this->synthesize_speech($content, $post_id);
if ($audio_url) {
// 保存音频信息到文章元数据
update_post_meta($post_id, '_atpc_audio_url', $audio_url);
update_post_meta($post_id, '_atpc_audio_generated', current_time('mysql'));
update_post_meta($post_id, '_atpc_audio_duration', $this->calculate_duration($content));
// 触发动作,可供其他插件使用
do_action('atpc_audio_generated', $post_id, $audio_url);
return $audio_url;
}
return false;
}
private function prepare_content($post) {
// 获取文章标题和内容
$title = $post->post_title;
$content = $post->post_content;
// 移除短代码
$content = strip_shortcodes($content);
// 移除HTML标签,但保留段落结构
$content = wp_strip_all_tags($content);
// 清理多余空格和换行
$content = preg_replace('/s+/', ' ', $content);
// 添加标题
$full_content = sprintf(__('文章标题:%s。正文内容:%s', 'article-to-podcast'), $title, $content);
// 限制长度(Polly限制为3000个字符)
if (strlen($full_content) > 3000) {
$full_content = substr($full_content, 0, 2997) . '...';
}
return $full_content;
}
private function synthesize_speech($text, $post_id) {
if (!$this->polly_client) {
error_log('ATPC: Polly client not initialized');
return false;
}
try {
// 获取语音设置
$options = get_option('atpc_settings');
$voice_id = isset($options['voice_id']) ? $options['voice_id'] : 'Zhiyu';
$engine = isset($options['engine']) ? $options['engine'] : 'standard';
$language_code = isset($options['language_code']) ? $options['language_code'] : 'cmn-CN';
// 调用Polly API
$result = $this->polly_client->synthesizeSpeech([
'Text' => $text,
'OutputFormat' => 'mp3',
'VoiceId' => $voice_id,
'Engine' => $engine,
'LanguageCode' => $language_code,
'TextType' => 'text'
]);
// 保存音频文件
$upload_dir = wp_upload_dir();
$audio_dir = $upload_dir['basedir'] . '/atpc-audio/';
if (!file_exists($audio_dir)) {
wp_mkdir_p($audio_dir);
}
$filename = 'post-' . $post_id . '-' . time() . '.mp3';
$filepath = $audio_dir . $filename;
// 保存音频数据
$audio_data = $result->get('AudioStream')->getContents();
file_put_contents($filepath, $audio_data);
// 返回音频URL
return $upload_dir['baseurl'] . '/atpc-audio/' . $filename;
} catch (Exception $e) {
error_log('ATPC: Failed to synthesize speech - ' . $e->getMessage());
return false;
}
}
private function calculate_duration($text) {
// 粗略估算:平均阅读速度约为150字/分钟
$word_count = str_word_count($text);
$minutes = ceil($word_count / 150);
// 格式化为HH:MM:SS
$hours = floor($minutes / 60);
$minutes = $minutes % 60;
$seconds = 0;
return sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
}
public function get_available_voices() {
if (!$this->polly_client) {
return [];
}
try {
$result = $this->polly_client->describeVoices();
$voices = $result->get('Voices');
$voice_list = [];
foreach ($voices as $voice) {
if (strpos($voice['LanguageCode'], 'zh') === 0 ||
strpos($voice['LanguageCode'], 'cmn') === 0) {
$voice_list[] = [
'id' => $voice['Id'],
'name' => $voice['Name'],
'language' => $voice['LanguageName'],
'gender' => $voice['Gender']
];
}
}
return $voice_list;
} catch (Exception $e) {
error_log('ATPC: Failed to fetch voices - ' . $e->getMessage());
return [];
}
}
}
第三部分:播客Feed生成与管理
3.1 理解播客RSS Feed规范
播客本质上是一个特殊的RSS Feed,包含一些额外的标签。关键的播客标签包括:
<itunes:title>:播客标题<itunes:author>:作者<itunes:image>:播客封面<itunes:category>:分类<itunes:explicit>:是否包含成人内容<itunes:duration>:音频时长<enclosure>:音频文件URL、类型和大小
3.2 实现播客Feed类
创建includes/class-podcast-feed.php文件:
<?php
class ATPC_Podcast_Feed {
private $feed_slug = 'podcast';
public function __construct() {
// 添加播客Feed端点
add_action('init', [$this, 'add_podcast_feed_endpoint']);
add_action('template_redirect', [$this, 'generate_podcast_feed']);
// 添加播客头部信息
add_action('wp_head', [$this, 'add_podcast_feed_link']);
}
public function add_podcast_feed_endpoint() {
add_rewrite_endpoint($this->feed_slug, EP_ROOT);
add_rewrite_rule('^podcast/?$', 'index.php?podcast=feed', 'top');
add_rewrite_rule('^podcast/feed/?$', 'index.php?podcast=feed', 'top');
}
public function generate_podcast_feed() {
if (get_query_var('podcast') !== 'feed') {
return;
}
// 设置内容类型为XML
header('Content-Type: application/rss+xml; charset=' . get_option('blog_charset'), true);
// 获取播客设置
$options = get_option('atpc_podcast_settings');
// 开始输出XML
echo '<?xml version="1.0" encoding="' . get_option('blog_charset') . '"?>';
echo '<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
echo '<channel>';
// 频道信息
echo '<title>' . esc_html($options['title'] ?? get_bloginfo('name') . '播客') . '</title>';
echo '<link>' . esc_url(home_url()) . '</link>';
echo '<language>' . get_bloginfo('language') . '</language>';
echo '<copyright>' . esc_html($options['copyright'] ?? '版权所有 ' . date('Y') . ' ' . get_bloginfo('name')) . '</copyright>';
echo '<itunes:author>' . esc_html($options['author'] ?? get_bloginfo('name')) . '</itunes:author>';
echo '<description>' . esc_html($options['description'] ?? get_bloginfo('description')) . '</description>';
// 播客封面
if (!empty($options['cover_image'])) {
echo '<itunes:image href="' . esc_url($options['cover_image']) . '" />';
}
// 分类
if (!empty($options['category'])) {
echo '<itunes:category text="' . esc_attr($options['category']) . '" />';
}
// 是否包含成人内容
echo '<itunes:explicit>' . ($options['explicit'] ?? 'no') . '</itunes:explicit>';
// 获取有音频的文章
$args = [
'post_type' => get_option('atpc_post_types', ['post']),
'posts_per_page' => 50,
'meta_query' => [
[
'key' => '_atpc_audio_url',
'compare' => 'EXISTS'
]
],
'orderby' => 'date',
'order' => 'DESC'
];
$podcast_posts = new WP_Query($args);
作为播客项目
if ($podcast_posts->have_posts()) {
while ($podcast_posts->have_posts()) {
$podcast_posts->the_post();
global $post;
$audio_url = get_post_meta($post->ID, '_atpc_audio_url', true);
$audio_duration = get_post_meta($post->ID, '_atpc_audio_duration', true);
if (!$audio_url) {
continue;
}
echo '<item>';
echo '<title>' . esc_html(get_the_title()) . '</title>';
echo '<link>' . esc_url(get_permalink()) . '</link>';
echo '<guid isPermaLink="false">' . esc_url($audio_url) . '</guid>';
echo '<pubDate>' . get_post_time('r', true) . '</pubDate>';
echo '<description><![CDATA[' . get_the_excerpt() . ']]></description>';
echo '<content:encoded><![CDATA[' . get_the_content() . ']]></content:encoded>';
// 作者信息
$author = get_the_author();
echo '<itunes:author>' . esc_html($author) . '</itunes:author>';
// 音频时长
if ($audio_duration) {
echo '<itunes:duration>' . esc_html($audio_duration) . '</itunes:duration>';
}
// 音频文件
$audio_size = $this->get_remote_file_size($audio_url);
echo '<enclosure url="' . esc_url($audio_url) . '" length="' . esc_attr($audio_size) . '" type="audio/mpeg" />';
// 分类
$categories = get_the_category();
if (!empty($categories)) {
echo '<category>' . esc_html($categories[0]->name) . '</category>';
}
echo '</item>';
}
wp_reset_postdata();
}
echo '</channel>';
echo '</rss>';
exit;
}
private function get_remote_file_size($url) {
// 尝试获取文件大小
$headers = get_headers($url, 1);
if (isset($headers['Content-Length'])) {
return $headers['Content-Length'];
}
// 如果无法获取,使用默认值
return '1048576'; // 1MB默认值
}
public function add_podcast_feed_link() {
$feed_url = home_url('/podcast/');
echo '<link rel="alternate" type="application/rss+xml" title="' . esc_attr(get_bloginfo('name') . '播客') . '" href="' . esc_url($feed_url) . '" />';
}
public function get_feed_url() {
return home_url('/podcast/');
}
public function submit_to_podcast_directories() {
$options = get_option('atpc_podcast_settings');
$feed_url = $this->get_feed_url();
$directories = [
'itunes' => 'https://podcasts.apple.com/podcasts/submit',
'google' => 'https://podcastsmanager.google.com/',
'spotify' => 'https://podcasters.spotify.com/submit',
'amazon' => 'https://podcasters.amazon.com/',
];
$submission_links = [];
foreach ($directories as $platform => $url) {
$submission_links[$platform] = [
'url' => $url,
'feed_param' => '?feed=' . urlencode($feed_url)
];
}
return $submission_links;
}
}
## 第四部分:管理界面设计与实现
### 4.1 创建插件设置页面
创建`includes/class-admin-ui.php`文件:
<?php
class ATPC_Admin_UI {
public function __construct() {
// 添加管理菜单
add_action('admin_menu', [$this, 'add_admin_menu']);
// 注册设置
add_action('admin_init', [$this, 'register_settings']);
// 添加文章列表音频列
add_filter('manage_posts_columns', [$this, 'add_audio_column']);
add_action('manage_posts_custom_column', [$this, 'display_audio_column'], 10, 2);
// 添加批量操作
add_filter('bulk_actions-edit-post', [$this, 'add_bulk_actions']);
add_filter('handle_bulk_actions-edit-post', [$this, 'handle_bulk_actions'], 10, 3);
// 添加文章编辑框元数据
add_action('add_meta_boxes', [$this, 'add_audio_meta_box']);
add_action('save_post', [$this, 'save_audio_meta_box'], 10, 2);
// 添加脚本和样式
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
}
public static function activate_plugin() {
// 创建必要的数据库表
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'atpc_audio_logs';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
post_id bigint(20) NOT NULL,
audio_url varchar(500) NOT NULL,
generated_at datetime DEFAULT CURRENT_TIMESTAMP,
status varchar(20) DEFAULT 'success',
error_message text,
PRIMARY KEY (id),
KEY post_id (post_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 设置默认选项
$default_settings = [
'aws_access_key' => '',
'aws_secret_key' => '',
'aws_region' => 'us-east-1',
'voice_id' => 'Zhiyu',
'engine' => 'standard',
'language_code' => 'cmn-CN'
];
add_option('atpc_settings', $default_settings);
$default_podcast_settings = [
'title' => get_bloginfo('name') . '播客',
'author' => get_bloginfo('name'),
'description' => get_bloginfo('description'),
'cover_image' => '',
'category' => 'Technology',
'explicit' => 'no',
'copyright' => '版权所有 ' . date('Y') . ' ' . get_bloginfo('name')
];
add_option('atpc_podcast_settings', $default_podcast_settings);
add_option('atpc_auto_generate', 'yes');
add_option('atpc_post_types', ['post']);
// 刷新重写规则
flush_rewrite_rules();
}
public static function deactivate_plugin() {
// 清理临时数据
// 注意:不删除设置和音频文件,以便重新激活时继续使用
flush_rewrite_rules();
}
public function add_admin_menu() {
// 主菜单
add_menu_page(
__('文章转播客', 'article-to-podcast'),
__('文章转播客', 'article-to-podcast'),
'manage_options',
'article-to-podcast',
[$this, 'display_main_page'],
'dashicons-controls-volumeon',
30
);
// 子菜单
add_submenu_page(
'article-to-podcast',
__('设置', 'article-to-podcast'),
__('设置', 'article-to-podcast'),
'manage_options',
'atpc-settings',
[$this, 'display_settings_page']
);
add_submenu_page(
'article-to-podcast',
__('播客设置', 'article-to-podcast'),
__('播客设置', 'article-to-podcast'),
'manage_options',
'atpc-podcast-settings',
[$this, 'display_podcast_settings_page']
);
add_submenu_page(
'article-to-podcast',
__('批量生成', 'article-to-podcast'),
__('批量生成', 'article-to-podcast'),
'manage_options',
'atpc-batch-generate',
[$this, 'display_batch_generate_page']
);
add_submenu_page(
'article-to-podcast',
__('统计', 'article-to-podcast'),
__('统计', 'article-to-podcast'),
'manage_options',
'atpc-stats',
[$this, 'display_stats_page']
);
}
public function display_main_page() {
?>
<div class="wrap atpc-dashboard">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<div class="atpc-stats-cards">
<div class="card">
<h3><?php _e('已生成音频', 'article-to-podcast'); ?></h3>
<p class="number"><?php echo $this->get_audio_count(); ?></p>
</div>
<div class="card">
<h3><?php _e('播客订阅', 'article-to-podcast'); ?></h3>
<p class="number"><?php echo $this->get_feed_url(); ?></p>
</div>
<div class="card">
<h3><?php _e('最近生成', 'article-to-podcast'); ?></h3>
<p class="number"><?php echo $this->get_recent_activity(); ?></p>
</div>
</div>
<div class="atpc-quick-actions">
<h2><?php _e('快速操作', 'article-to-podcast'); ?></h2>
<div class="action-buttons">
<a href="<?php echo admin_url('admin.php?page=atpc-batch-generate'); ?>" class="button button-primary">
<?php _e('批量生成音频', 'article-to-podcast'); ?>
</a>
<a href="<?php echo home_url('/podcast/'); ?>" target="_blank" class="button">
<?php _e('查看播客Feed', 'article-to-podcast'); ?>
</a>
<a href="<?php echo admin_url('admin.php?page=atpc-podcast-settings'); ?>" class="button">
<?php _e('播客目录提交', 'article-to-podcast'); ?>
</a>
</div>
</div>
<div class="atpc-recent-audio">
<h2><?php _e('最近生成的音频', 'article-to-podcast'); ?></h2>
<?php $this->display_recent_audio_table(); ?>
</div>
</div>
<?php
}
public function display_settings_page() {
?>
<div class="wrap">
<h1><?php _e('TTS服务设置', 'article-to-podcast'); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('atpc_settings_group');
do_settings_sections('atpc-settings');
submit_button();
?>
</form>
<div class="atpc-test-section">
<h2><?php _e('测试TTS服务', 'article-to-podcast'); ?></h2>
<textarea id="atpc-test-text" rows="4" style="width: 100%;" placeholder="<?php esc_attr_e('输入要测试的文字...', 'article-to-podcast'); ?>"></textarea>
<button id="atpc-test-tts" class="button button-secondary">
<?php _e('测试语音合成', 'article-to-podcast'); ?>
</button>
<div id="atpc-test-result"></div>
</div>
</div>
<?php
}
public function register_settings() {
// TTS设置
register_setting('atpc_settings_group', 'atpc_settings');
register_setting('atpc_settings_group', 'atpc_auto_generate');
register_setting('atpc_settings_group', 'atpc_post_types');
// 播客设置
register_setting('atpc_podcast_group', 'atpc_podcast_settings');
// TTS设置部分
add_settings_section(
'atpc_tts_section',
__('文字转语音服务设置', 'article-to-podcast'),
[$this, 'tts_section_callback'],
'atpc-settings'
);
// AWS凭证字段
add_settings_field(
'aws_access_key',
__('AWS访问密钥', 'article-to-podcast'),
[$this, 'text_field_callback'],
'atpc-settings',
'atpc_tts_section',
[
'label_for' => 'aws_access_key',
'option_group' => 'atpc_settings',
'description' => __('Amazon Polly服务的Access Key ID', 'article-to-podcast')
]
);
// 更多设置字段...
}
public function text_field_callback($args) {
$option_group = $args['option_group'];
$field_name = $args['label_for'];
$options = get_option($option_group);
$value = isset($options[$field_name]) ? $options[$field_name] : '';
echo '<input type="text" id="' . esc_attr($field_name) . '"
name="' . esc_attr($option_group) . '[' . esc_attr($field_name) . ']"
value="' . esc_attr($value) . '" class="regular-text">';
if (!empty($args['description'])) {
echo '<p class="description">' . esc_html($args['description']) . '</p>';
}
}
public function add_audio_column($columns) {
$columns['atpc_audio'] = __('音频', 'article-to-podcast');
return $columns;
}
public function display_audio_column($column, $post_id) {
if ($column === 'atpc_audio') {
$audio_url = get_post_meta($post_id, '_atpc_audio_url', true);
if ($audio_url) {
echo '<a href="' . esc_url($audio_url) . '" target="_blank" class="button button-small">';
echo __('播放', 'article-to-podcast');
echo '</a>';
echo '<button class="button button-small atpc-regenerate" data-post-id="' . esc_attr($post_id) . '">';
echo __('重新生成', 'article-to-podcast');
echo '</button>';
} else {
echo '<button class="button button-small button-primary atpc-generate" data-post-id="' . esc_attr($post_id) . '">';
echo __('生成音频', 'article-to-podcast');
echo '</button>';
}
}
}
public function add_bulk_actions($bulk_actions) {
$bulk_actions['generate_audio'] = __('生成音频', 'article-to-podcast');
$bulk_actions['regenerate_audio'] = __('重新生成音频', 'article-to-podcast');
return $bulk_actions;
}
public function handle_bulk_actions($redirect_to, $doaction, $post_ids) {
if ($doaction === 'generate_audio' || $doaction === 'regenerate_audio') {
$tts_engine = new ATPC_TTS_Engine();
$processed = 0;
foreach ($post_ids as $post_id) {
if ($tts_engine->generate_audio($post_id)) {
$processed++;
}
}
$redirect_to = add_query_arg('bulk_audio_processed', $processed, $redirect_to);
}
return $redirect_to;
}
public function add_audio_meta_box() {
$post_types = get_option('atpc_post_types', ['post']);
foreach ($post_types as $post_type) {
add_meta_box(
'atpc_audio_meta_box',
__('文章音频', 'article-to-podcast'),
[$this, 'render_audio_meta_box'],
$post_type,
'side',
'high'
);
}
}
public function render_audio_meta_box($post) {
wp_nonce_field('atpc_audio_meta_box', 'atpc_audio_meta_box_nonce');
$audio_url = get_post_meta($post->ID, '_atpc_audio_url', true);
$generated_time = get_post_meta($post->ID, '_atpc_audio_generated', true);
if ($audio_url) {
echo '<audio controls style="width: 100%; margin-bottom: 10px;">';
echo '<source src="' . esc_url($audio_url) . '" type="audio/mpeg">';
echo __('您的浏览器不支持音频播放。', 'article-to-podcast');
echo '</audio>';
echo '<p><strong>' . __('音频URL:', 'article-to-podcast') . '</strong><br>';
echo '<input type="text" readonly value="' . esc_url($audio_url) . '" style="width: 100%; font-size: 11px;"></p>';
if ($generated_time) {
echo '<p><strong>' . __('生成时间:', 'article-to-podcast') . '</strong><br>';
echo esc_html($generated_time) . '</p>';
}
echo '<button type="button" class="button button-secondary atpc-regenerate-single" data-post-id="' . esc_attr($post->ID) . '">';
echo __('重新生成音频', 'article-to-podcast');
echo '</button>';
echo '<button type="button" class="button atpc-copy-url" data-url="' . esc_url($audio_url) . '">';
echo __('复制URL', 'article-to-podcast');
echo '</button>';
} else {
echo '<p>' . __('此文章
