以下の内容はhttps://uepon.hatenadiary.com/entry/2025/04/08/005957より取得しました。


Whisperの文字起こし結果の検証に!タイムコード表示付き字幕の再生ツール開発

音声認識を行っていると、音声と認識された文字起こしデータを比較することが多いと思います。実際はあっている・あっていないは、開発側ではなく別の方に調べてもらうほうが良いのですが、最低限の確認をする必要はあると思います。他にも音声認識の結果が誤っている場合、音声側を聞き修正を行うことも多々あると思います。そこで今回はそのようなときにどのように行うかと調べてみました。

今回はVLCを使った場合確認方法とその限界を説明し、そのあと自作の確認ツールについて説明を行っています。この自作ツールはWSL上でも問題なく動作します。

前提として、Whisperなどによる文字起こし結果はテキストだけでなく、タイムコードも含まれているSRT形式(.srt)の字幕データにしておきましょう。

例えば、字幕に関しては以下の記事が参考になるかもしれません。

uepon.hatenadiary.com

SRT形式(.srt)の字幕データは以下の様になります。

今回のような用途には、一般的に使用できるVLCメディアプレーヤー(以下VLCが使えることがわかりました。VLCは無料で高機能な人気ソフトウェアですが、音声ファイルと字幕を同時に表示する機能があることは、あまり知られていないかもしれません。それもそのはず、音声ファイルを再生しても字幕が表示されないのです。

まずは、VLCで音声ファイルを再生しながら字幕を表示する方法を解説します。

VLCによる音声再生と字幕表示の同時表示再生

【重要】オーディオの可視化が必須

VLCは音声をそのまま再生しても字幕が表示されませんが、それはオーディオの可視化設定がデフォルトで無効になっているためです。そして、この設定を行うには音声ファイルを一旦読み込ませた後で無いと設定ができないのです🥲

実は以下のような理由でオーディオの可視化が必要になるようです。

  • 字幕を表示するには、表示するための領域(画面表示領域)が必要
  • 音声ファイルを再生する場合、VLCは画面表示領域を作成しません
  • オーディオの可視化機能を有効にすることで、この画面表示領域が有効化作成されて、字幕を表示できるようになる

多くのユーザーがVLCで音声に字幕が表示されない」と感じる主な原因になっているのではないかと思います。

音声ファイルを開く基本手順

オーディオの可視化をすることを覚えつつ以下のように操作を行います。

  1. VLCを起動
  2. 以下のいずれかの方法で音声ファイルを開きます
    • メニューから【メディア】→【ファイルを開く】を選択
    • キーボードショートカット【Ctrl+O】を使用
    • VLCに音声ファイルをドラッグ&ドロップ
  3. 音声ファイルを選択して「開く」をクリック

この段階では音声は再生されますが、まだ字幕を表示できません。オーディオの可視化の設定を行います。

オーディオの可視化設定

一時的な可視化設定方法

音声ファイルを再生中に一時的に可視化効果を適用する方法は以下の様に行います。

  1. 音声ファイルをVLCで開いてファイルを再生できる状態にする

  1. メニューから【オーディオ】→【視覚化】を選び、オーディオエフェクトのプルダウンメニューが開くので【無効】以外の視覚化の種類(スペクトラム、スコープなど)を選択(デフォルトは【無効】)

これで再生中のファイルの視覚効果が表示されるようになります。以下はスペクトルの設定です。 そして、字幕を表示するための画面領域が確保されます。

注意:この設定はVLCを終了すると失われるため、次回起動時には再度設定する必要があります。

永続的な可視化設定方法(おすすめ)

毎回設定するのが面倒な場合には、永続的に視覚化を有効にする設定がおすすめです。

  1. VLCを起動し、メニューから【ツール】→【設定】を選択(または 【Ctrl+P】)

  1. 【シンプルな設定】ダイアログが開く

  1. カテゴリから【オーディオ】を選択する

  1. 【視覚化】のプルダウンから【視覚化フィルター】などを選択する

  1. ダイアログの下部にある【保存】ボタンをクリックして設定を適用する

この設定を行うことで、以降はVLCで音声ファイルを再生するたびに自動的に視覚化が適用されます。一度設定すれば済むので楽になります😊

(蛇足)視覚化タイプの選択と効果

VLCには複数の視覚化タイプが用意されています。

  • スペクトラム: 周波数ごとの音量をグラフ表示(比較的控えめな表示)
  • スコープ: 波形を表示するシンプルな視覚化
  • スペクトロメーター: 周波数スペクトラムを3Dのように表示(派手な効果)
  • VU メーター: シンプルな音量レベル表示(最も控えめな表示)

字幕ファイルの追加と設定

字幕ファイルを手動で追加する方法

オーディオの可視化設定が完了したら、字幕ファイルを追加することで表示を行えます。字幕ファイルを手動で追加する手順は以下の通りです。

  1. VLCで音声ファイルを開き、オーディオの可視化設定が有効になっていることを確認する
  2. メニューから【字幕】→【字幕ファイルの追加】を選択する
  3. 表示されるファイル選択ダイアログで、字幕ファイル(.srtなど)を選択します
  4. 「開く」をクリックして字幕ファイルを読み込みます

字幕ファイルが正しく読み込まれると、音声の再生に合わせて字幕が表示されます。字幕が表示されない場合は、メニューの【字幕】→【字幕トラック】が有効になっているか確認してください。

便利なショートカットキーの活用

VLCの字幕関連のショートカットキーは以下の様になっています。

  • V: 字幕の表示/非表示を切り替え
  • G / H: 字幕を 50ms 早める / 遅らせる(字幕側の同期調整)
  • J / K: オーディオを 50ms 早める / 遅らせる(音声側の同期調整)

特に字幕の同期調整は頻繁に必要になるので、G/H/J/Kのショートカットは覚えておくと便利😎

字幕ファイルの自動読み込み設定

VLCでは、音声ファイルと字幕ファイルを同じフォルダに保存し、ファイル名を一定の形式にすることで、音声ファイルを読み込むことで、自動的に字幕ファイルを読み込む機能があります。

ファイル名の命名規則とポイント

VLCでは、正しい命名規則に従ってファイルを配置することで、字幕ファイルを自動的に読み込むことができます。これにより、毎回手動で字幕を追加する手間が省けます。

自動読み込みの基本ルールは次のようになっています。

  1. 同じファイル名(拡張子は異なっても良い)を使用する:

    • 音声ファイル: 講演.mp3
    • 字幕ファイル: 講演.srt
  2. 言語コードを追加する場合:

    • 音声ファイル: 講演.mp3
    • 日本語字幕: 講演.ja.srt
    • 英語字幕: 講演.en.srt

VLCは起動時にこれらのファイル名パターンを認識し、自動的に字幕を関連付けて読み込みを行ってくれます。

【重要】VLCでは上手くいかないこと

VLCは優れたメディアプレーヤーですが、今回使用している音声ファイルと字幕の同時表示には機能には問題があります。

シークバーで再生位置を移動すると字幕が表示されなくなる問題

VLCで音声ファイル再生中にシークバーを使って再生位置に移動すると、字幕が表示されなくなることがあります。これはVLCの既知の問題のようで、以下の対処法があります。ただ、正常になることもあれば総出なこともあるようです。

  1. 一時停止して再開する

    • シーク後、再生を一時停止(スペースキー)し、再度再生(スペースキー)することで字幕表示が復活することがあります
  2. 字幕トラックの再読み込み

    • メニューから【字幕】→【字幕トラック】を選択し、一度別の選択肢(【無効】など)に切り替えてから、再度正しい字幕トラックを選択

この問題は、文字起こしなどの確認で頻繁にシークする必要がある用途では大きな障害になります。

上手くいかないなら自作しよう!

仕方ないので自作をしましょう🤩🤩🤩今回はPythonTkinterを使用して実装例しています。

srt_playerの主な特徴

  1. 音声と字幕の同期再生: MP3などの音声ファイルとSRT形式の字幕ファイルを同期再生
  2. 字幕履歴表示: 表示した字幕を履歴として残し、スクロールして過去の字幕を確認可能
  3. 時間表示: 現在の再生位置を時間表示
  4. 一時停止・再開機能: 再生を一時停止し、同じ位置から再開可能
  5. 自動スクロール: 新しい字幕が表示されたときに自動的にスクロール(オプション)
  6. コマンドラインサポート: コマンドライン引数で音声ファイルと字幕ファイルを指定可能

起動時の様子

動作時の様子

このプレーヤーでは、VLCで遭遇した問題(特にシーク後の字幕消失など)を回避しています。また、字幕履歴機能を搭載しているため、文字起こしの確認作業が容易になるでしょう。

ソースコード

import tkinter as tk
from tkinter import filedialog, scrolledtext
import pygame
import threading
import time
import re
import os
import sys
import argparse

# このコードでは音声ファイルの長さ取得に mutagen ライブラリを使用します
# pip install mutagen でインストールしてください
try:
    import mutagen
    MUTAGEN_AVAILABLE = True
except ImportError:
    MUTAGEN_AVAILABLE = False

class SRTPlayer:
    def __init__(self, root):
        self.root = root
        self.root.title("SRT Player")
        self.root.geometry("800x600")
        
        # Initialize pygame mixer for audio playback
        pygame.init()
        pygame.mixer.init()
        
        # Variables
        self.audio_file = ""
        self.srt_file = ""
        self.subtitles = []
        self.playing = False
        self.paused = False
        self.total_length = 100.0  # デフォルト値
        
        # 時間管理のための変数
        self.current_position = 0.0  # 現在の再生位置(秒)
        self.paused_position = 0.0   # 一時停止した位置(秒)
        self.initial_position = 0.0  # 再生開始位置
        
        # 字幕履歴の管理
        self.subtitle_history = []   # 表示した字幕の履歴
        self.last_subtitle_id = ""   # 最後に表示した字幕のID
        
        # スレッド制御
        self.stop_thread = False
        self.subtitle_thread = None
        
        # Set application theme and style
        self.root.configure(bg='#f0f0f0')
        
        # Create the GUI elements
        self.create_widgets()
    
    def create_widgets(self):
        # Frame for buttons
        button_frame = tk.Frame(self.root)
        button_frame.pack(fill=tk.X, padx=10, pady=10)
        
        # 共通のボタンスタイル
        button_style = {'font': ('TkDefaultFont', 12), 'width': 12, 'height': 2}
        
        # ボタンを横方向に並べるのではなく、gridで配置
        self.audio_btn = tk.Button(button_frame, text="Select Audio", command=self.load_audio, **button_style)
        self.audio_btn.grid(row=0, column=0, padx=5, pady=5)
        
        self.srt_btn = tk.Button(button_frame, text="Select SRT", command=self.load_srt, **button_style)
        self.srt_btn.grid(row=0, column=1, padx=5, pady=5)
        
        self.play_btn = tk.Button(button_frame, text="Play", command=self.play, state=tk.DISABLED, **button_style)
        self.play_btn.grid(row=0, column=2, padx=5, pady=5)
        
        self.pause_btn = tk.Button(button_frame, text="Pause", command=self.pause, state=tk.DISABLED, **button_style)
        self.pause_btn.grid(row=0, column=3, padx=5, pady=5)
        
        self.stop_btn = tk.Button(button_frame, text="Stop", command=self.stop, state=tk.DISABLED, **button_style)
        self.stop_btn.grid(row=0, column=4, padx=5, pady=5)
        
        # ボタンの配置に合わせてグリッドを調整
        for i in range(5):
            button_frame.columnconfigure(i, weight=1)
            
        # シークバーの追加
        seek_frame = tk.Frame(self.root)
        seek_frame.pack(fill=tk.X, padx=10, pady=5)
        
        self.seek_var = tk.DoubleVar()
        self.seek_bar = tk.Scale(
            seek_frame, 
            variable=self.seek_var,
            from_=0, 
            to=100,  # 仮の値、音声読み込み時に更新
            orient=tk.HORIZONTAL, 
            length=700,
            showvalue=0,
            command=self.on_seek
        )
        self.seek_bar.pack(fill=tk.X, expand=True)
        
        # 時間表示ラベル(現在時間/総時間)
        self.duration_label = tk.Label(seek_frame, text="00:00/00:00", font=('TkDefaultFont', 10))
        self.duration_label.pack(side=tk.RIGHT, padx=5)
            
        # Info labels
        info_frame = tk.Frame(self.root)
        info_frame.pack(fill=tk.X, padx=10)
        
        tk.Label(info_frame, text="Audio File:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
        self.audio_label = tk.Label(info_frame, text="No file selected")
        self.audio_label.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
        
        tk.Label(info_frame, text="SRT File:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
        self.srt_label = tk.Label(info_frame, text="No file selected")
        self.srt_label.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
        
        tk.Label(info_frame, text="Current Time:", font=('TkDefaultFont', 12)).grid(row=2, column=0, sticky=tk.W, padx=5, pady=2)
        self.time_label = tk.Label(info_frame, text="00:00:00,000", font=('TkDefaultFont', 16, 'bold'))
        self.time_label.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
        
        # Subtitle display area
        subtitle_frame = tk.Frame(self.root)
        subtitle_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        self.subtitle_display = scrolledtext.ScrolledText(
            subtitle_frame, 
            wrap=tk.WORD, 
            font=("TkDefaultFont", 16),
            bg='#fafafa',
            padx=10,
            pady=10
        )
        self.subtitle_display.pack(fill=tk.BOTH, expand=True)
        
        # 自動スクロールを有効化するチェックボックス
        self.autoscroll_var = tk.BooleanVar(value=True)
        self.autoscroll_check = tk.Checkbutton(
            subtitle_frame, 
            text="自動スクロール", 
            variable=self.autoscroll_var,
            font=('TkDefaultFont', 10)
        )
        self.autoscroll_check.pack(anchor=tk.W, padx=10, pady=2)
    
    def on_seek(self, value):
        """シークバーの値が変更されたときに呼ばれる"""
        if not self.playing and not self.paused:
            return
            
        # 値をfloatに変換
        value = float(value)
        
        # 音声の総時間に対する割合を計算
        if hasattr(self, 'total_length') and self.total_length > 0:
            position = (value / 100.0) * self.total_length
            
            # 内部変数を更新
            self.current_position = position
            
            # 再生中なら再生位置を更新、一時停止中なら再生せずに位置だけ更新
            if self.playing:
                pygame.mixer.music.stop()
                pygame.mixer.music.play(start=position)
                self.initial_position = position
            else:  # paused状態
                self.paused_position = position
                self.initial_position = position
                # 一時停止中は再生を開始しない
                
            # 時間表示を更新
            time_str = self.format_time(position)
            self.time_label.config(text=time_str)
            
            # 字幕履歴をクリア(新しい位置から履歴を構築し直す)
            self.subtitle_history = []
            self.last_subtitle_id = ""
            
            # テキストエリアをクリア
            self.subtitle_display.delete(1.0, tk.END)
    
    def load_audio(self):
        self.audio_file = filedialog.askopenfilename(
            title="Select Audio File",
            filetypes=[("Audio Files", "*.mp3 *.wav *.ogg")]
        )
        if self.audio_file:
            self.audio_label.config(text=os.path.basename(self.audio_file))
            
            # 音声ファイルの長さを取得
            if MUTAGEN_AVAILABLE:
                try:
                    audio = mutagen.File(self.audio_file)
                    if audio and hasattr(audio.info, 'length'):
                        self.total_length = audio.info.length  # 総時間(秒)
                    else:
                        # mutagenで取得できない場合は仮の値を設定
                        self.total_length = 100.0
                except Exception:
                    self.total_length = 100.0
            else:
                # mutagenがインストールされていない場合は仮の値を設定
                self.total_length = 100.0
            
            # シークバーの最大値を設定(パーセント表示のため常に100)
            self.seek_bar.config(to=100)
            
            # 時間表示を更新
            duration_str = f"00:00/{self.format_time(self.total_length)}"
            self.duration_label.config(text=duration_str)
            
            self.check_files_loaded()
    
    def load_srt(self):
        self.srt_file = filedialog.askopenfilename(
            title="Select SRT File",
            filetypes=[("SRT Files", "*.srt")]
        )
        if self.srt_file:
            self.srt_label.config(text=os.path.basename(self.srt_file))
            self.parse_srt()
            self.check_files_loaded()
    
    def parse_srt(self):
        """Parse the SRT file and extract subtitles with their timings."""
        self.subtitles = []
        
        if not self.srt_file:
            return
        
        try:
            with open(self.srt_file, 'r', encoding='utf-8') as file:
                content = file.read()
        except UnicodeDecodeError:
            # UTF-8でエラーが出る場合、他のエンコーディングを試す
            try:
                with open(self.srt_file, 'r', encoding='shift-jis') as file:
                    content = file.read()
            except UnicodeDecodeError:
                # それでもダメな場合はLatin-1を試す
                with open(self.srt_file, 'r', encoding='latin-1') as file:
                    content = file.read()
        
        # Split by double newline to get each subtitle block
        subtitle_blocks = re.split(r'\n\n+', content.strip())
        
        for block in subtitle_blocks:
            lines = block.strip().split('\n')
            if len(lines) >= 3:  # At least 3 lines (number, timing, text)
                subtitle_num = lines[0]
                timing = lines[1]
                text = '\n'.join(lines[2:])  # The rest is subtitle text
                
                # Parse timing
                timing_pattern = r'(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})'
                match = re.match(timing_pattern, timing)
                
                if match:
                    h1, m1, s1, ms1, h2, m2, s2, ms2 = map(int, match.groups())
                    start_time = h1*3600 + m1*60 + s1 + ms1/1000
                    end_time = h2*3600 + m2*60 + s2 + ms2/1000
                    
                    self.subtitles.append({
                        'id': subtitle_num,
                        'start': start_time,
                        'end': end_time,
                        'text': text
                    })
        
        # 時間順にソート
        self.subtitles.sort(key=lambda x: x['start'])
    
    def check_files_loaded(self):
        """Enable play button if both files are loaded."""
        if self.audio_file and self.srt_file:
            self.play_btn.config(state=tk.NORMAL)
        else:
            self.play_btn.config(state=tk.DISABLED)
    
    def play(self):
        """Start playback of audio and subtitle display."""
        if not self.playing:
            if not self.paused:
                # 新規再生
                pygame.mixer.music.load(self.audio_file)
                pygame.mixer.music.play()
                self.current_position = 0.0
                self.paused_position = 0.0
                self.initial_position = 0.0
            else:
                # 一時停止した位置から再開
                # 一時停止位置から開始するようにpygameに指示
                pygame.mixer.music.play(start=self.paused_position)
                
                # GUIの時間表示を一時停止時点の値に即時更新
                time_str = self.format_time(self.paused_position)
                self.time_label.config(text=time_str)
                
                # initial_positionを正しく設定(このスレッドでセット)
                self.initial_position = self.paused_position
                
                # paused状態を解除
                self.paused = False
            
            self.playing = True
            self.play_btn.config(state=tk.DISABLED)
            self.pause_btn.config(state=tk.NORMAL)  # 再生中はPauseボタンを有効化
            self.stop_btn.config(state=tk.NORMAL)
            
            # 前回のスレッドが実行中なら停止フラグを立てる
            if self.subtitle_thread and self.subtitle_thread.is_alive():
                self.stop_thread = True
                self.subtitle_thread.join(timeout=1.0)  # 最大1秒待機
            
            # 停止フラグをリセット
            self.stop_thread = False
            
            # Start the subtitle display thread
            self.subtitle_thread = threading.Thread(target=self.update_subtitles)
            self.subtitle_thread.daemon = True
            self.subtitle_thread.start()
    
    def pause(self):
        """Pause playback."""
        if self.playing and not self.paused:
            pygame.mixer.music.pause()
            # 現在のpygameの再生位置を保存
            pos_ms = pygame.mixer.music.get_pos()
            # 負の値になることがあるためチェック
            if pos_ms >= 0:
                # 一時停止した位置を保存(秒単位)
                self.paused_position = pos_ms / 1000.0
                if hasattr(self, 'initial_position'):
                    self.paused_position += self.initial_position
            else:
                # get_posが失敗した場合、現在のcurrent_positionを使用
                self.paused_position = self.current_position
                
            self.paused = True
            self.playing = False
            self.play_btn.config(state=tk.NORMAL)
            self.pause_btn.config(state=tk.DISABLED)  # 一時停止中はPauseボタンを無効化
    
    def stop(self):
        """Stop playback."""
        self.stop_thread = True  # スレッドに停止を通知
        pygame.mixer.music.stop()
        self.playing = False
        self.paused = False
        self.current_position = 0.0
        self.paused_position = 0.0
        self.initial_position = 0.0
        
        # 字幕履歴をクリア
        self.subtitle_history = []
        self.last_subtitle_id = ""
        
        self.play_btn.config(state=tk.NORMAL)
        self.pause_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.DISABLED)
        self.subtitle_display.delete(1.0, tk.END)
        self.time_label.config(text="00:00:00,000")
        self.seek_var.set(0)  # シークバーをリセット
        
        # 時間表示をリセット
        duration_str = f"00:00/{self.format_time(self.total_length)}"
        self.duration_label.config(text=duration_str)
    
    def format_time(self, seconds):
        """秒数を00:00:00,000形式にフォーマット"""
        hours = int(seconds // 3600)
        minutes = int((seconds % 3600) // 60)
        seconds_part = int(seconds % 60)
        milliseconds = int((seconds % 1) * 1000)
        return f"{hours:02d}:{minutes:02d}:{seconds_part:02d},{milliseconds:03d}"
    
    def update_subtitles(self):
        """再生位置に基づいて字幕を更新するスレッド"""
        # 字幕同期のための補正値(秒)- 必要に応じて調整
        sync_offset = 0.1
        
        # 前回表示した字幕のテキスト
        last_subtitle_text = ""
        
        # 再生開始時間
        start_time = time.time()
        
        # この時点でself.initial_positionはplayメソッドで設定済み
        # そのまま使用する(ここで上書きしない)
        
        while not self.stop_thread and self.playing:
            if not pygame.mixer.music.get_busy():
                # 再生が終了
                self.root.after(100, self.stop)  # GUIスレッドでstopを呼び出す
                break
            
            # pygameの再生位置を取得(ミリ秒単位)
            pos_ms = pygame.mixer.music.get_pos()
            
            if pos_ms >= 0:
                # ミリ秒を秒に変換し、開始位置を加算
                # 一時停止からの再開時には新しい再生開始時点からの時間になる
                self.current_position = pos_ms / 1000.0 + self.initial_position
            else:
                # get_posが失敗した場合は経過時間を使用
                elapsed = time.time() - start_time
                self.current_position = self.initial_position + elapsed
            
            # 時間表示を更新(GUIスレッドで実行)
            time_str = self.format_time(self.current_position)
            self.root.after(0, lambda s=time_str: self.time_label.config(text=s))
            
            # シークバーの位置を更新
            if hasattr(self, 'total_length') and self.total_length > 0:
                seek_percent = (self.current_position / self.total_length) * 100
                self.root.after(0, lambda p=seek_percent: self.seek_var.set(p))
                
                # 時間表示も更新
                current_str = self.format_time(self.current_position)
                total_str = self.format_time(self.total_length)
                dur_str = f"{current_str}/{total_str}"
                self.root.after(0, lambda s=dur_str: self.duration_label.config(text=s))
            
            # 現在表示すべき字幕を見つける
            current_text = ""
            current_subtitle_id = None
            adjusted_time = self.current_position - sync_offset  # 表示タイミング調整
            
            for subtitle in self.subtitles:
                if subtitle['start'] <= adjusted_time <= subtitle['end']:
                    current_text = subtitle['text']
                    current_subtitle_id = subtitle['id']
                    break
            
            # テキストが変わった場合のみ更新(ちらつき防止)- GUIスレッドで実行
            if current_text != last_subtitle_text:
                self.root.after(0, lambda t=current_text, id=current_subtitle_id: 
                                self.update_subtitle_text(t, id))
                last_subtitle_text = current_text
            
            # CPU使用率を抑えるための短いスリープ
            time.sleep(0.05)
    
    def update_subtitle_text(self, text, subtitle_id=None):
        """GUIスレッドで字幕テキストを更新(スレッドセーフ)
        新しい字幕のみをテキストエリアに追加する
        タイムコード付きで表示する
        """
        # 空のテキストは処理しない
        if not text.strip():
            return
            
        # 新しい字幕の場合
        if subtitle_id and subtitle_id != self.last_subtitle_id:
            # 現在の字幕のタイムコードを取得
            current_timecode = ""
            current_start = 0
            current_end = 0
            
            for subtitle in self.subtitles:
                if subtitle['id'] == subtitle_id:
                    current_start = subtitle['start']
                    current_end = subtitle['end']
                    start_time = self.format_time(current_start)
                    end_time = self.format_time(current_end)
                    current_timecode = f"[{start_time} --> {end_time}]"
                    break
            
            # テキストをタイムコード付きで整形
            display_text = f"{current_timecode}\n{text}"
            
            # 内部履歴に追加(タイムコード付き)
            self.subtitle_history.append(display_text)
            self.last_subtitle_id = subtitle_id
            
            # テキストエリアに新しい字幕のみを追加
            if self.subtitle_display.get(1.0, tk.END).strip():
                # すでにテキストがある場合は、改行を追加してから新しい字幕を追加
                self.subtitle_display.insert(tk.END, "\n\n" + display_text)
            else:
                # テキストエリアが空の場合は、そのまま追加
                self.subtitle_display.insert(tk.END, display_text)
            
            # 自動スクロールが有効ならスクロール
            if self.autoscroll_var.get():
                self.subtitle_display.see(tk.END)
        elif not subtitle_id:
            # 履歴モードでない場合(従来の動作)
            self.subtitle_display.delete(1.0, tk.END)
            self.subtitle_display.insert(tk.END, text)


def main():
    # コマンドライン引数の処理
    parser = argparse.ArgumentParser(description='SRT Player - 音声と字幕を同期再生')
    parser.add_argument('-a', '--audio', help='音声ファイルのパス')
    parser.add_argument('-s', '--srt', help='字幕ファイル(SRT)のパス')
    args = parser.parse_args()
    
    root = tk.Tk()
    # ウィンドウサイズを大きく設定
    root.geometry("800x600")
    app = SRTPlayer(root)
    
    # コマンドライン引数でファイルが指定されていれば読み込む
    if args.audio and os.path.isfile(args.audio):
        app.audio_file = args.audio
        app.audio_label.config(text=os.path.basename(args.audio))
        app.check_files_loaded()
    
    if args.srt and os.path.isfile(args.srt):
        app.srt_file = args.srt
        app.srt_label.config(text=os.path.basename(args.srt))
        app.parse_srt()
        app.check_files_loaded()
    
    root.mainloop()

if __name__ == "__main__":
    main()

利用方法

# 基本的な起動方法
$ python srt_player.py

# 音声ファイルと字幕ファイルを指定して起動
$ python srt_player.py -a your_audio.mp3 -s your_subtitle.srt

この自作プレーヤーは、VLCが持つ制限を回避しつつ、文字起こしの確認作業に特化した機能を提供します。 必要に応じてカスタマイズなどしてみてくだださい。 Githubにも公開しています。

github.com

おわりに

VLCでの音声ファイルと字幕表示の方法とその制限、及び、回避するための自作ツールを作成しました。 まあ、VLCでもいいけど、シークバーの再生位置の問題があると早送りなどの連携ができないので、自作ツールで行っています😎

個人的には複数の文字起こしのツールを使って字幕化して、音声と複数の字幕を表示しどれくらい認識に差があるかなど確認できてもよかったかなと思います。




以上の内容はhttps://uepon.hatenadiary.com/entry/2025/04/08/005957より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14