首页 / 应用软件 / 详细教程,为网站打造内嵌的在线简易视频字幕生成与编辑工具

详细教程,为网站打造内嵌的在线简易视频字幕生成与编辑工具

详细教程:为WordPress网站打造内嵌在线简易视频字幕生成与编辑工具

引言:为什么网站需要内置视频字幕工具?

在当今多媒体内容主导的互联网环境中,视频已成为网站吸引和保留用户的重要手段。然而,许多网站运营者面临一个共同挑战:如何高效地为视频内容添加字幕。字幕不仅有助于听力障碍用户访问内容,还能提高视频在静音环境下的观看率,对SEO优化也有显著帮助。

传统上,为视频添加字幕需要依赖第三方工具或专业软件,过程繁琐且耗时。本文将详细介绍如何通过WordPress代码二次开发,为您的网站打造一个内嵌的在线简易视频字幕生成与编辑工具,让字幕创建变得简单高效。

第一部分:项目规划与技术选型

1.1 功能需求分析

在开始开发之前,我们需要明确工具的核心功能:

  1. 视频上传与预览:支持常见视频格式上传和实时预览
  2. 自动语音识别(ASR):将视频中的语音转换为文字
  3. 字幕编辑界面:直观的时间轴编辑功能
  4. 字幕格式支持:至少支持SRT、VTT等常用字幕格式
  5. 实时预览:编辑字幕时可实时查看效果
  6. 导出与集成:将生成的字幕与视频关联或单独导出

1.2 技术架构设计

我们将采用以下技术栈:

  • 前端:HTML5、CSS3、JavaScript(使用Vue.js框架)
  • 视频处理:HTML5 Video API + FFmpeg.wasm(浏览器端处理)
  • 语音识别:Web Speech API(免费基础方案)或集成第三方API(如Google Cloud Speech-to-Text)
  • 后端:WordPress PHP环境,使用REST API处理文件操作
  • 存储:WordPress媒体库 + 自定义数据库表存储字幕数据

1.3 开发环境准备

确保您的WordPress开发环境满足以下条件:

  1. WordPress 5.0+(支持REST API)
  2. PHP 7.4+(支持最新语法特性)
  3. 至少256MB内存限制(用于处理视频文件)
  4. 启用文件上传功能(支持视频格式)
  5. 安装并启用必要的开发插件(如Query Monitor、Debug Bar)

第二部分:创建WordPress插件框架

2.1 初始化插件结构

首先,在WordPress的wp-content/plugins/目录下创建新文件夹video-subtitle-tool,并创建以下基础文件结构:

video-subtitle-tool/
├── video-subtitle-tool.php      # 主插件文件
├── includes/
│   ├── class-database.php       # 数据库处理类
│   ├── class-video-processor.php # 视频处理类
│   ├── class-subtitle-generator.php # 字幕生成类
│   └── class-api-handler.php    # REST API处理类
├── admin/
│   ├── css/
│   │   └── admin-style.css      # 后台样式
│   ├── js/
│   │   └── admin-script.js      # 后台脚本
│   └── admin-page.php           # 管理页面
├── public/
│   ├── css/
│   │   └── public-style.css     # 前端样式
│   ├── js/
│   │   ├── app.js               # 主Vue应用
│   │   ├── video-processor.js   # 视频处理逻辑
│   │   └── subtitle-editor.js   # 字幕编辑器
│   └── shortcode.php            # 短代码处理
├── assets/
│   ├── ffmpeg/
│   │   └── ffmpeg-core.js       # FFmpeg.wasm核心文件
│   └── icons/                   # 图标资源
└── templates/
    └── subtitle-editor.php      # 编辑器模板

2.2 主插件文件配置

编辑video-subtitle-tool.php,添加插件基本信息:

<?php
/**
 * Plugin Name: 视频字幕生成与编辑工具
 * Plugin URI:  https://yourwebsite.com/video-subtitle-tool
 * Description: 为WordPress网站添加内嵌的在线视频字幕生成与编辑功能
 * Version:     1.0.0
 * Author:      您的名称
 * License:     GPL v2 or later
 * Text Domain: video-subtitle-tool
 */

// 防止直接访问
if (!defined('ABSPATH')) {
    exit;
}

// 定义插件常量
define('VST_VERSION', '1.0.0');
define('VST_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VST_PLUGIN_URL', plugin_dir_url(__FILE__));
define('VST_MAX_FILE_SIZE', 500 * 1024 * 1024); // 500MB限制

// 自动加载类文件
spl_autoload_register(function ($class) {
    $prefix = 'VST_';
    $base_dir = VST_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 vst_init() {
    // 检查依赖
    if (!function_exists('wp_get_current_user')) {
        require_once(ABSPATH . 'wp-includes/pluggable.php');
    }
    
    // 初始化数据库
    VST_Database::init();
    
    // 注册短代码
    add_shortcode('video_subtitle_editor', array('VST_Shortcode', 'render_editor'));
    
    // 初始化REST API
    add_action('rest_api_init', array('VST_API_Handler', 'register_routes'));
    
    // 加载文本域
    load_plugin_textdomain('video-subtitle-tool', false, dirname(plugin_basename(__FILE__)) . '/languages/');
}
add_action('plugins_loaded', 'vst_init');

// 激活插件时执行
function vst_activate() {
    require_once VST_PLUGIN_DIR . 'includes/class-database.php';
    VST_Database::create_tables();
    
    // 设置默认选项
    add_option('vst_default_language', 'zh-CN');
    add_option('vst_max_video_duration', 3600); // 默认最大1小时
    add_option('vst_enable_auto_generation', true);
}
register_activation_hook(__FILE__, 'vst_activate');

// 停用插件时清理
function vst_deactivate() {
    // 清理临时文件
    VST_Video_Processor::cleanup_temp_files();
}
register_deactivation_hook(__FILE__, 'vst_deactivate');

第三部分:数据库设计与实现

3.1 创建数据库表

编辑includes/class-database.php

<?php
class VST_Database {
    
    public static function init() {
        // 数据库版本
        $db_version = '1.0';
        $current_version = get_option('vst_db_version', '0');
        
        if ($current_version !== $db_version) {
            self::create_tables();
            update_option('vst_db_version', $db_version);
        }
    }
    
    public static function create_tables() {
        global $wpdb;
        
        $charset_collate = $wpdb->get_charset_collate();
        $table_prefix = $wpdb->prefix . 'vst_';
        
        // 视频项目表
        $projects_table = $table_prefix . 'projects';
        $projects_sql = "CREATE TABLE IF NOT EXISTS $projects_table (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            user_id bigint(20) NOT NULL,
            title varchar(255) NOT NULL,
            video_url varchar(1000) NOT NULL,
            video_duration int(11) DEFAULT 0,
            language varchar(10) DEFAULT 'zh-CN',
            status varchar(20) DEFAULT 'draft',
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY user_id (user_id),
            KEY status (status)
        ) $charset_collate;";
        
        // 字幕表
        $subtitles_table = $table_prefix . 'subtitles';
        $subtitles_sql = "CREATE TABLE IF NOT EXISTS $subtitles_table (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            project_id bigint(20) NOT NULL,
            start_time int(11) NOT NULL,
            end_time int(11) NOT NULL,
            text text NOT NULL,
            confidence float DEFAULT 1.0,
            is_edited tinyint(1) DEFAULT 0,
            sequence int(11) NOT NULL,
            PRIMARY KEY (id),
            KEY project_id (project_id),
            KEY sequence (sequence)
        ) $charset_collate;";
        
        // 项目设置表
        $settings_table = $table_prefix . 'project_settings';
        $settings_sql = "CREATE TABLE IF NOT EXISTS $settings_table (
            project_id bigint(20) NOT NULL,
            font_family varchar(100) DEFAULT 'Arial',
            font_size int(11) DEFAULT 24,
            font_color varchar(7) DEFAULT '#FFFFFF',
            background_color varchar(7) DEFAULT '#00000080',
            position varchar(20) DEFAULT 'bottom',
            max_lines int(11) DEFAULT 2,
            UNIQUE KEY project_id (project_id)
        ) $charset_collate;";
        
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($projects_sql);
        dbDelta($subtitles_sql);
        dbDelta($settings_sql);
    }
    
    public static function cleanup_temp_files() {
        // 清理超过24小时的临时文件
        $temp_dir = wp_upload_dir()['basedir'] . '/vst_temp/';
        if (is_dir($temp_dir)) {
            $files = glob($temp_dir . '*');
            $now = time();
            
            foreach ($files as $file) {
                if (is_file($file)) {
                    if ($now - filemtime($file) >= 86400) { // 24小时
                        unlink($file);
                    }
                }
            }
        }
    }
}

第四部分:视频处理与语音识别模块

4.1 视频处理类实现

创建includes/class-video-processor.php

<?php
class VST_Video_Processor {
    
    /**
     * 处理上传的视频文件
     */
    public static function process_upload($file) {
        // 安全检查
        if (!self::validate_file($file)) {
            return new WP_Error('invalid_file', '无效的视频文件');
        }
        
        // 创建临时目录
        $upload_dir = wp_upload_dir();
        $temp_dir = $upload_dir['basedir'] . '/vst_temp/';
        if (!is_dir($temp_dir)) {
            wp_mkdir_p($temp_dir);
        }
        
        // 生成唯一文件名
        $file_name = sanitize_file_name($file['name']);
        $unique_name = wp_unique_filename($temp_dir, $file_name);
        $temp_path = $temp_dir . $unique_name;
        
        // 移动文件
        if (!move_uploaded_file($file['tmp_name'], $temp_path)) {
            return new WP_Error('upload_failed', '文件上传失败');
        }
        
        // 获取视频信息
        $video_info = self::get_video_info($temp_path);
        
        return array(
            'path' => $temp_path,
            'url' => $upload_dir['baseurl'] . '/vst_temp/' . $unique_name,
            'name' => $file_name,
            'info' => $video_info
        );
    }
    
    /**
     * 验证文件
     */
    private static function validate_file($file) {
        $allowed_types = array('mp4', 'avi', 'mov', 'wmv', 'flv', 'webm');
        $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        
        // 检查文件类型
        if (!in_array($file_ext, $allowed_types)) {
            return false;
        }
        
        // 检查文件大小
        $max_size = get_option('vst_max_file_size', VST_MAX_FILE_SIZE);
        if ($file['size'] > $max_size) {
            return false;
        }
        
        // 检查MIME类型
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime_type = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        $allowed_mimes = array(
            'video/mp4',
            'video/x-msvideo',
            'video/quicktime',
            'video/x-ms-wmv',
            'video/x-flv',
            'video/webm'
        );
        
        return in_array($mime_type, $allowed_mimes);
    }
    
    /**
     * 获取视频信息
     */
    private static function get_video_info($file_path) {
        // 使用FFprobe获取视频信息(需要服务器安装FFmpeg)
        if (function_exists('shell_exec')) {
            $cmd = "ffprobe -v quiet -print_format json -show_format -show_streams " . escapeshellarg($file_path);
            $output = shell_exec($cmd);
            
            if ($output) {
                $info = json_decode($output, true);
                if ($info) {
                    return array(
                        'duration' => floatval($info['format']['duration']),
                        'width' => intval($info['streams'][0]['width']),
                        'height' => intval($info['streams'][0]['height']),
                        'bitrate' => intval($info['format']['bit_rate']),
                        'format' => $info['format']['format_name']
                    );
                }
            }
        }
        
        // 备用方案:使用PHP的getid3库
        if (!class_exists('getID3')) {
            require_once(ABSPATH . 'wp-admin/includes/media.php');
        }
        
        $id3 = new getID3();
        $file_info = $id3->analyze($file_path);
        
        return array(
            'duration' => isset($file_info['playtime_seconds']) ? $file_info['playtime_seconds'] : 0,
            'width' => isset($file_info['video']['resolution_x']) ? $file_info['video']['resolution_x'] : 0,
            'height' => isset($file_info['video']['resolution_y']) ? $file_info['video']['resolution_y'] : 0,
            'bitrate' => isset($file_info['bitrate']) ? $file_info['bitrate'] : 0,
            'format' => isset($file_info['fileformat']) ? $file_info['fileformat'] : 'unknown'
        );
    }
    
    /**
     * 提取音频用于语音识别
     */
    public static function extract_audio($video_path, $output_path = null) {
        if (!$output_path) {
            $upload_dir = wp_upload_dir();
            $output_path = $upload_dir['basedir'] . '/vst_temp/audio_' . uniqid() . '.wav';
        }
        
        // 使用FFmpeg提取音频
        $cmd = "ffmpeg -i " . escapeshellarg($video_path) . 
               " -ac 1 -ar 16000 -vn " . escapeshellarg($output_path) . 
               " 2>&1";
        
        exec($cmd, $output, $return_code);
        
        if ($return_code === 0 && file_exists($output_path)) {
            return $output_path;
        }
        
        return false;
    }
}

4.2 语音识别集成

创建includes/class-subtitle-generator.php

<?php
class VST_Subtitle_Generator {
    
    /**
     * 使用Web Speech API进行语音识别(客户端方案)
     */
    public static function get_browser_recognition_code() {
        // 返回客户端JavaScript代码
        return '
        <script>
        class SpeechRecognitionTool {
            constructor() {
                this.recognition = null;
                this.isRecording = false;
                this.transcript = "";
                this.interimResults = [];
                this.init();
            }
            
            init() {
                const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
                if (!SpeechRecognition) {
                    throw new Error("浏览器不支持语音识别API");
                }
                
                this.recognition = new SpeechRecognition();
                this.recognition.continuous = true;
                this.recognition.interimResults = true;
                this.recognition.lang = "zh-CN";
                
                this.recognition.onresult = (event) => {
                    let interimTranscript = "";
                    let finalTranscript = "";
                    
                    for (let i = event.resultIndex; i < event.results.length; i++) {
                        const transcript = event.results[i][0].transcript;
                        if (event.results[i].isFinal) {
                            finalTranscript += transcript;
                        } else {
                            interimTranscript += transcript;
                        }
                    }
                    
                    this.interimResults = interimTranscript;
                    this.transcript += finalTranscript;
                    
                    // 触发自定义事件
                    const transcriptEvent = new CustomEvent("transcriptUpdate", {
                        detail: {
                            final: finalTranscript,
                            interim: interimTranscript,
                            full: this.transcript
                        }
                    });
                    document.dispatchEvent(transcriptEvent);
                };
                
                this.recognition.onerror = (event) => {
                    console.error("语音识别错误:", event.error);
                };
            }
            
            start() {
                if (this.recognition && !this.isRecording) {
                    this.recognition.start();
                    this.isRecording = true;
                }
            }
            
            stop() {
                if (this.recognition && this.isRecording) {
                    this.recognition.stop();
                    this.isRecording = false;
                }
            }
            

第四部分:视频处理与语音识别模块(续)

4.2 语音识别集成(续)

            setLanguage(lang) {
                if (this.recognition) {
                    this.recognition.lang = lang;
                }
            }
            
            getTranscript() {
                return this.transcript;
            }
            
            reset() {
                this.transcript = "";
                this.interimResults = "";
            }
        }
        
        // 导出到全局
        window.VSTSpeechRecognition = SpeechRecognitionTool;
        </script>
        ';
    }
    
    /**
     * 使用第三方API进行语音识别(服务器端方案)
     */
    public static function recognize_with_api($audio_path, $language = 'zh-CN') {
        $api_type = get_option('vst_speech_api', 'google');
        
        switch ($api_type) {
            case 'google':
                return self::google_speech_recognition($audio_path, $language);
            case 'azure':
                return self::azure_speech_recognition($audio_path, $language);
            case 'baidu':
                return self::baidu_speech_recognition($audio_path, $language);
            default:
                return new WP_Error('unsupported_api', '不支持的语音识别API');
        }
    }
    
    /**
     * Google Cloud Speech-to-Text
     */
    private static function google_speech_recognition($audio_path, $language) {
        $api_key = get_option('vst_google_api_key', '');
        
        if (empty($api_key)) {
            return new WP_Error('missing_api_key', '未配置Google API密钥');
        }
        
        // 读取音频文件
        $audio_content = file_get_contents($audio_path);
        $base64_audio = base64_encode($audio_content);
        
        $url = "https://speech.googleapis.com/v1/speech:recognize?key=" . $api_key;
        
        $data = array(
            'config' => array(
                'encoding' => 'LINEAR16',
                'sampleRateHertz' => 16000,
                'languageCode' => $language,
                'enableWordTimeOffsets' => true,
                'enableAutomaticPunctuation' => true
            ),
            'audio' => array(
                'content' => $base64_audio
            )
        );
        
        $response = wp_remote_post($url, array(
            'headers' => array('Content-Type' => 'application/json'),
            'body' => json_encode($data),
            'timeout' => 60
        ));
        
        if (is_wp_error($response)) {
            return $response;
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if (isset($body['error'])) {
            return new WP_Error('api_error', $body['error']['message']);
        }
        
        return self::format_recognition_results($body['results'], 'google');
    }
    
    /**
     * 格式化识别结果
     */
    private static function format_recognition_results($results, $provider) {
        $subtitles = array();
        $current_time = 0;
        
        foreach ($results as $result) {
            if (!isset($result['alternatives'][0])) {
                continue;
            }
            
            $alternative = $result['alternatives'][0];
            $text = $alternative['transcript'];
            
            if ($provider === 'google' && isset($alternative['words'])) {
                // 基于词语时间戳分割字幕
                $words = $alternative['words'];
                $chunks = self::split_into_subtitles($words);
                
                foreach ($chunks as $chunk) {
                    $subtitles[] = array(
                        'start' => $chunk['start'],
                        'end' => $chunk['end'],
                        'text' => $chunk['text'],
                        'confidence' => $alternative['confidence'] ?? 0.8
                    );
                }
            } else {
                // 简单分割:每5秒一句
                $duration = 5; // 每句5秒
                $sentences = self::split_text_into_sentences($text);
                
                foreach ($sentences as $sentence) {
                    $subtitles[] = array(
                        'start' => $current_time,
                        'end' => $current_time + $duration,
                        'text' => $sentence,
                        'confidence' => 0.7
                    );
                    $current_time += $duration;
                }
            }
        }
        
        return $subtitles;
    }
    
    /**
     * 将词语分割成字幕块
     */
    private static function split_into_subtitles($words, $max_chars = 40) {
        $chunks = array();
        $current_chunk = array(
            'text' => '',
            'start' => null,
            'end' => null,
            'word_count' => 0
        );
        
        foreach ($words as $word) {
            $word_text = $word['word'];
            $word_start = floatval($word['startTime']['seconds']) + 
                         (floatval($word['startTime']['nanos'] ?? 0) / 1000000000);
            $word_end = floatval($word['endTime']['seconds']) + 
                       (floatval($word['endTime']['nanos'] ?? 0) / 1000000000);
            
            // 如果当前块为空,设置开始时间
            if ($current_chunk['start'] === null) {
                $current_chunk['start'] = $word_start;
            }
            
            // 检查是否应该开始新块
            if (strlen($current_chunk['text'] . ' ' . $word_text) > $max_chars || 
                $current_chunk['word_count'] >= 8) {
                
                // 保存当前块
                if (!empty($current_chunk['text'])) {
                    $current_chunk['end'] = $word_start; // 使用下一个词的开始时间作为结束
                    $chunks[] = $current_chunk;
                }
                
                // 开始新块
                $current_chunk = array(
                    'text' => $word_text,
                    'start' => $word_start,
                    'end' => $word_end,
                    'word_count' => 1
                );
            } else {
                // 添加到当前块
                $current_chunk['text'] .= (empty($current_chunk['text']) ? '' : ' ') . $word_text;
                $current_chunk['end'] = $word_end;
                $current_chunk['word_count']++;
            }
        }
        
        // 添加最后一个块
        if (!empty($current_chunk['text'])) {
            $chunks[] = $current_chunk;
        }
        
        return $chunks;
    }
    
    /**
     * 将文本分割成句子
     */
    private static function split_text_into_sentences($text) {
        // 简单的句子分割逻辑
        $sentences = preg_split('/(?<=[。!?.!?])/u', $text, -1, PREG_SPLIT_NO_EMPTY);
        return array_filter(array_map('trim', $sentences));
    }
}

第五部分:前端编辑器开发

5.1 编辑器HTML结构

创建templates/subtitle-editor.php

<div id="vst-editor-app" class="vst-editor-container">
    <!-- 顶部工具栏 -->
    <div class="vst-toolbar">
        <div class="vst-toolbar-left">
            <button class="vst-btn vst-btn-primary" @click="uploadVideo">
                <i class="vst-icon-upload"></i> 上传视频
            </button>
            <button class="vst-btn" :class="{'vst-btn-active': isRecording}" @click="toggleRecording">
                <i class="vst-icon-mic"></i> {{ isRecording ? '停止识别' : '开始识别' }}
            </button>
            <select v-model="selectedLanguage" class="vst-select">
                <option value="zh-CN">中文(简体)</option>
                <option value="zh-TW">中文(繁体)</option>
                <option value="en-US">English</option>
                <option value="ja-JP">日本語</option>
                <option value="ko-KR">한국어</option>
            </select>
        </div>
        <div class="vst-toolbar-right">
            <button class="vst-btn" @click="exportSubtitles('srt')">
                <i class="vst-icon-download"></i> 导出SRT
            </button>
            <button class="vst-btn" @click="exportSubtitles('vtt')">
                <i class="vst-icon-download"></i> 导出VTT
            </button>
            <button class="vst-btn vst-btn-success" @click="saveProject">
                <i class="vst-icon-save"></i> 保存项目
            </button>
        </div>
    </div>
    
    <!-- 主编辑区 -->
    <div class="vst-main-editor">
        <!-- 视频播放器 -->
        <div class="vst-video-container">
            <video 
                ref="videoPlayer" 
                :src="videoUrl" 
                controls
                @timeupdate="updateCurrentTime"
                @loadedmetadata="onVideoLoaded"
            ></video>
            <div class="vst-video-overlay" v-if="subtitles.length > 0">
                <div class="vst-subtitle-display" :style="subtitleStyle">
                    {{ currentSubtitleText }}
                </div>
            </div>
        </div>
        
        <!-- 字幕编辑区 -->
        <div class="vst-subtitle-editor">
            <div class="vst-subtitle-header">
                <h3>字幕编辑</h3>
                <div class="vst-subtitle-actions">
                    <button class="vst-btn vst-btn-small" @click="addSubtitle">
                        <i class="vst-icon-add"></i> 添加字幕
                    </button>
                    <button class="vst-btn vst-btn-small" @click="autoSync">
                        <i class="vst-icon-sync"></i> 自动同步
                    </button>
                </div>
            </div>
            
            <!-- 字幕列表 -->
            <div class="vst-subtitle-list">
                <div 
                    v-for="(subtitle, index) in subtitles" 
                    :key="index"
                    class="vst-subtitle-item"
                    :class="{'vst-subtitle-active': currentSubtitleIndex === index}"
                    @click="selectSubtitle(index)"
                >
                    <div class="vst-subtitle-index">{{ index + 1 }}</div>
                    <div class="vst-subtitle-time">
                        <input 
                            type="number" 
                            v-model="subtitle.start" 
                            step="0.1" 
                            @change="updateSubtitleTime(index, 'start', $event)"
                        >
                        <span> → </span>
                        <input 
                            type="number" 
                            v-model="subtitle.end" 
                            step="0.1"
                            @change="updateSubtitleTime(index, 'end', $event)"
                        >
                    </div>
                    <div class="vst-subtitle-text">
                        <textarea 
                            v-model="subtitle.text" 
                            @input="updateSubtitleText(index, $event)"
                            rows="2"
                        ></textarea>
                    </div>
                    <div class="vst-subtitle-actions">
                        <button class="vst-btn-icon" @click="playSegment(index)">
                            <i class="vst-icon-play"></i>
                        </button>
                        <button class="vst-btn-icon" @click="removeSubtitle(index)">
                            <i class="vst-icon-delete"></i>
                        </button>
                    </div>
                </div>
            </div>
            
            <!-- 时间轴编辑器 -->
            <div class="vst-timeline-editor">
                <div class="vst-timeline-header">
                    <span>时间轴</span>
                    <span class="vst-current-time">{{ formatTime(currentTime) }}</span>
                </div>
                <div class="vst-timeline-container" ref="timeline">
                    <div 
                        v-for="(subtitle, index) in subtitles" 
                        :key="'timeline-' + index"
                        class="vst-timeline-segment"
                        :style="{
                            left: (subtitle.start / videoDuration * 100) + '%',
                            width: ((subtitle.end - subtitle.start) / videoDuration * 100) + '%'
                        }"
                        @mousedown="startDrag(index, $event)"
                    >
                        <div class="vst-timeline-label">{{ index + 1 }}</div>
                    </div>
                    <div 
                        class="vst-timeline-cursor" 
                        :style="{ left: (currentTime / videoDuration * 100) + '%' }"
                    ></div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- 设置面板 -->
    <div class="vst-settings-panel" v-if="showSettings">
        <div class="vst-settings-header">
            <h3>字幕设置</h3>
            <button class="vst-btn-icon" @click="showSettings = false">
                <i class="vst-icon-close"></i>
            </button>
        </div>
        <div class="vst-settings-content">
            <div class="vst-setting-item">
                <label>字体</label>
                <select v-model="settings.fontFamily">
                    <option value="Arial">Arial</option>
                    <option value="Microsoft YaHei">微软雅黑</option>
                    <option value="SimSun">宋体</option>
                    <option value="SimHei">黑体</option>
                </select>
            </div>
            <div class="vst-setting-item">
                <label>字体大小</label>
                <input type="range" v-model="settings.fontSize" min="12" max="48">
                <span>{{ settings.fontSize }}px</span>
            </div>
            <div class="vst-setting-item">
                <label>字体颜色</label>
                <input type="color" v-model="settings.fontColor">
            </div>
            <div class="vst-setting-item">
                <label>背景颜色</label>
                <input type="color" v-model="settings.backgroundColor">
                <input type="range" v-model="settings.backgroundOpacity" min="0" max="100">
                <span>{{ settings.backgroundOpacity }}%</span>
            </div>
            <div class="vst-setting-item">
                <label>位置</label>
                <select v-model="settings.position">
                    <option value="bottom">底部</option>
                    <option value="top">顶部</option>
                    <option value="middle">中间</option>
                </select>
            </div>
        </div>
    </div>
    
    <!-- 上传模态框 -->
    <div class="vst-modal" v-if="showUploadModal">
        <div class="vst-modal-content">
            <div class="vst-modal-header">
                <h3>上传视频</h3>
                <button class="vst-btn-icon" @click="showUploadModal = false">
                    <i class="vst-icon-close"></i>
                </button>
            </div>
            <div class="vst-modal-body">
                <div class="vst-upload-area" 
                     @dragover.prevent="onDragOver"
                     @dragleave.prevent="onDragLeave"
                     @drop.prevent="onDrop"
                     :class="{'vst-upload-dragover': isDragOver}"
                >
                    <i class="vst-icon-upload-large"></i>
                    <p>拖放视频文件到这里,或</p>
                    <input 
                        type="file" 
                        ref="fileInput" 
                        accept="video/*" 
                        @change="onFileSelected"
                        hidden
                    >
                    <button class="vst-btn" @click="$refs.fileInput.click()">
                        选择文件
                    </button>
                    <p class="vst-upload-hint">支持 MP4, AVI, MOV, WMV, FLV, WebM 格式,最大500MB</p>
                </div>
                <div v-if="uploadProgress > 0" class="vst-upload-progress">
                    <div class="vst-progress-bar">
                        <div class="vst-progress-fill" :style="{ width: uploadProgress + '%' }"></div>
                    </div>
                    <span>{{ uploadProgress }}%</span>
                </div>
            </div>
        </div>
    </div>
</div>

5.2 编辑器Vue.js应用

创建public/js/app.js

// Vue.js应用主文件
document.addEventListener('DOMContentLoaded', function() {
    // 检查Vue是否已加载
    if (typeof Vue === 'undefined') {
        console.error('Vue.js未加载,请确保已引入Vue库');
        return;
    }
    
    // 创建Vue应用
    const app = Vue.createApp({
        data() {
            return {
                // 视频相关
                videoUrl: '',
                videoDuration: 0,
                currentTime: 0,
                videoFile: null,
                
                // 字幕数据
                subtitles: [],
                currentSubtitleIndex: -1,
                currentSubtitleText: '',
                
                // 识别状态
                isRecording: false,
                selectedLanguage: 'zh-CN',
                recognitionTool: null,
                
                // 界面状态
                showSettings: false,
                showUploadModal: false,
                isDragOver: false,
                uploadProgress: 0,
                
                // 设置
                settings: {
                    fontFamily: 'Microsoft YaHei',
                    fontSize: 24,
                    fontColor: '#FFFFFF',
                    backgroundColor: '#000000',
                    backgroundOpacity: 50,
                    position: 'bottom'
                },
                
                // 项目信息
                projectId: null,
                projectTitle: '未命名项目',
                isSaved: false
            };
        },
        
        computed: {
            // 计算字幕显示样式
            subtitleStyle() {
                const bgColor = this.hexToRgba(
                    this.settings.backgroundColor, 
                    this.settings.backgroundOpacity / 100
                );
                
本文来自网络,不代表柔性供应链服务中心立场,转载请注明出处:https://mall.org.cn/5264.html

EXCHANGES®作者

上一篇
下一篇

为您推荐

发表回复

联系我们

联系我们

18559313275

在线咨询: QQ交谈

邮箱: vip@exchanges.center

工作时间:周一至周五,9:00-17:30,节假日休息
返回顶部