文章目录
-
- 在当今快节奏的数字时代,用户获取信息的方式日益多样化。虽然阅读文字内容仍然是主要方式,但越来越多的人开始通过音频内容获取信息——在通勤途中、做家务时、运动时,音频内容提供了无需视觉参与的便利。根据Edison Research的数据,2023年有超过1亿美国人每月收听播客,这一数字比五年前增长了近一倍。 对于WordPress网站所有者而言,将文章内容转换为音频格式具有多重优势: 提高内容可访问性,服务视觉障碍用户 增加用户停留时间,降低跳出率 拓展内容分发渠道,触及更广泛的受众 提升SEO表现,增加网站可见性 创造新的变现机会,如播客广告 本教程将详细指导您开发一个完整的WordPress插件,实现文章自动转语音并生成播客订阅功能。我们将从零开始,逐步构建这个功能强大的工具。
-
- 在开始插件开发前,确保您已准备好以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress(建议5.8+) 代码编辑器:VS Code、PHPStorm或Sublime Text PHP版本:7.4或更高版本 调试工具:安装Query Monitor和Debug Bar插件
- 首先,在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/ # 前端模板
- 打开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');
-
- 目前市场上有多种TTS服务可供选择,每种都有其优缺点: Google Cloud Text-to-Speech:质量高,支持多种语言,但需要付费 Amazon Polly:自然语音,价格合理,有免费套餐 Microsoft Azure Cognitive Services:语音自然度高,支持情感表达 IBM Watson Text to Speech:企业级解决方案 本地解决方案:如eSpeak(免费但质量较低) 本教程将使用Amazon Polly作为示例,因为它提供每月500万字符的免费套餐,适合中小型网站。
- 创建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 []; } } }
-
- 播客本质上是一个特殊的RSS Feed,包含一些额外的标签。关键的播客标签包括: <itunes:title>:播客标题 <itunes:author>:作者 <itunes:image>:播客封面 <itunes:category>:分类 <itunes:explicit>:是否包含成人内容 <itunes:duration>:音频时长 <enclosure>:音频文件URL、类型和大小
- 创建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`文件: <?phpclass 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>' . __('此文章
在当今快节奏的数字时代,用户获取信息的方式日益多样化。虽然阅读文字内容仍然是主要方式,但越来越多的人开始通过音频内容获取信息——在通勤途中、做家务时、运动时,音频内容提供了无需视觉参与的便利。根据Edison Research的数据,2023年有超过1亿美国人每月收听播客,这一数字比五年前增长了近一倍。
对于WordPress网站所有者而言,将文章内容转换为音频格式具有多重优势:
- 提高内容可访问性,服务视觉障碍用户
- 增加用户停留时间,降低跳出率
- 拓展内容分发渠道,触及更广泛的受众
- 提升SEO表现,增加网站可见性
- 创造新的变现机会,如播客广告
本教程将详细指导您开发一个完整的WordPress插件,实现文章自动转语音并生成播客订阅功能。我们将从零开始,逐步构建这个功能强大的工具。
在开始插件开发前,确保您已准备好以下环境:
- 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel
- WordPress安装:最新版本的WordPress(建议5.8+)
- 代码编辑器:VS Code、PHPStorm或Sublime Text
- PHP版本:7.4或更高版本
- 调试工具:安装Query Monitor和Debug Bar插件
首先,在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/ # 前端模板
打开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');
目前市场上有多种TTS服务可供选择,每种都有其优缺点:
- Google Cloud Text-to-Speech:质量高,支持多种语言,但需要付费
- Amazon Polly:自然语音,价格合理,有免费套餐
- Microsoft Azure Cognitive Services:语音自然度高,支持情感表达
- IBM Watson Text to Speech:企业级解决方案
- 本地解决方案:如eSpeak(免费但质量较低)
本教程将使用Amazon Polly作为示例,因为它提供每月500万字符的免费套餐,适合中小型网站。
创建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 [];
}
}
}
播客本质上是一个特殊的RSS Feed,包含一些额外的标签。关键的播客标签包括:
<itunes:title>:播客标题<itunes:author>:作者<itunes:image>:播客封面<itunes:category>:分类<itunes:explicit>:是否包含成人内容<itunes:duration>:音频时长<enclosure>:音频文件URL、类型和大小
创建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>' . __('此文章


