# [メタ情報] # 識別子: 無料アカウント向け_動画からVTTを生成する_exe # 補足: Gem共有から無料アカウントでVTTを作るには動画は1本5分以内の制約があるため、動画を分割する # [/メタ情報] 要約: 1. **「VTT作成用に動画を分割する」ワークフロー**は、ユーザーが指定した基本秒数を基に、動画を無音区間で自動分割し、VTTの「秒ずらし」に役立つオフセット記録を生成します。`ffmpeg`とPythonスクリプトを連携させ、再エンコードなしで高速分割します。 2. **「字幕時刻の秒ずらしを行う」ワークフロー**は、選択したVTTファイルの全タイムスタンプを、ユーザーが入力した秒数だけシフト(前後にずらす)させ、新しいVTTファイルを出力します。`awk`を用いて精密な時間計算を行います。 3. **「VTTの連番を付け直す」ワークフロー**は、VTTファイル内の古い連番や不要なヘッダを削除し、字幕ブロックに正しい連番を振り直して整理されたVTTファイルを生成します。 これらのワークフローは、それぞれ動画の分割、字幕の時間調整、字幕ファイルの整理という異なる課題に対応し、VTT作成・管理作業を自動化・簡素化することを目的としています。 VTT作成用に動画を分割する.workflow Automator クイックアクション ムービーファイル すべてのアプリケーション AppleScriptを実行 ``` on run {input, parameters} -- 秒数を入力するダイアログを表示 display dialog "分割する基本秒数を入力してください:(例: 270)" default answer "270" set baseSeconds to text returned of result -- 入力された秒数とファイルパスを返す set filePath to POSIX path of (item 1 of input) return {filePath, baseSeconds} end run ``` シェルスクリプトを実行 /bin/bash 引数として ``` #!/bin/bash # AppleScriptから受け取った入力 input_file="$1" base_seconds="$2" # Homebrew等でインストールしたffmpegを利用できるようパスを通す export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH # 【修正箇所】正しいPythonスクリプトのパスを指定 python_script="$HOME/python_scripts/smart_split.py" # Pythonスクリプトを実行 python3 "$python_script" "$input_file" "$base_seconds" # 完了通知 osascript -e 'display notification "動画の分割とオフセット記録の生成が完了しました。" with title "XXXXXXシステム"' ``` /Users/XXXXXX/python_scripts/smart_split.py /Users/XXXXXX/python_scripts/smart_split.py ``` import sys import os import subprocess import re def get_silence_points(video_path): # 【変更点1】無音の長さを1.0秒(d=1.0)に変更。 # ※もしノイズで無音判定されにくい場合は、noise=-30dB を noise=-25dB などに変更してください。 cmd = ['ffmpeg', '-i', video_path, '-af', 'silencedetect=noise=-30dB:d=1.0', '-f', 'null', '-'] result = subprocess.run(cmd, stderr=subprocess.PIPE, text=True) silence_points = [] # 出力ログから無音のタイムスタンプを抽出 for line in result.stderr.split('\n'): # 【変更点2】「無音の終わり」ではなく「無音の始まり(silence_start)」で区切るように変更 match = re.search(r'silence_start: (\d+(\.\d+)?)', line) if match: silence_points.append(float(match.group(1))) return silence_points def get_video_duration(video_path): # 動画の総秒数を取得 cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path] result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True) try: return float(result.stdout.strip()) except: return 0.0 def main(): if len(sys.argv) < 3: print("エラー: 引数が足りません。") sys.exit(1) video_path = sys.argv[1] base_seconds = float(sys.argv[2]) # 基本秒数(例: 270) dir_name = os.path.dirname(video_path) base_name, ext = os.path.splitext(os.path.basename(video_path)) print("無音区間を解析中...") silence_points = get_silence_points(video_path) total_duration = get_video_duration(video_path) if total_duration == 0: print("動画の長さを取得できませんでした。") sys.exit(1) split_points = [0.0] current_target = base_seconds # 分割ポイントの計算 while current_target < total_duration: # ターゲット秒数付近の無音区間を探す(手前〜少し後) valid_points = [p for p in silence_points if p > split_points[-1] and p <= current_target + 15] if valid_points: # ターゲット秒数に最も近い無音区間を採用 best_point = min(valid_points, key=lambda x: abs(x - current_target)) split_points.append(best_point) current_target = best_point + base_seconds else: # 適切な無音区間がない場合は、強制的にターゲット秒数で区切る split_points.append(current_target) current_target += base_seconds split_points.append(total_duration) # オフセット記録用ファイルのパス offset_log_path = os.path.join(dir_name, f"{base_name}_offsets.txt") with open(offset_log_path, 'w', encoding='utf-8') as log_file: log_file.write(f"【オフセット記録】 {os.path.basename(video_path)}\n") log_file.write("※後でVTTを「秒ずらし」する際に、この秒数を入力してください。\n") log_file.write("-" * 40 + "\n") # FFmpegによる実際の分割処理 for i in range(len(split_points) - 1): start = split_points[i] end = split_points[i+1] out_filename = f"{base_name}_split_{i+1:02d}{ext}" out_path = os.path.join(dir_name, out_filename) # 再エンコードなしで高速分割(-c copy) split_cmd = [ 'ffmpeg', '-y', '-i', video_path, '-ss', str(start), '-to', str(end), '-c', 'copy', out_path ] subprocess.run(split_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) log_file.write(f"{out_filename} -> ズレ: {start:.3f} 秒\n") print("分割処理が完了しました。") if __name__ == "__main__": main() ``` 字幕時刻の秒ずらしを行う.workflow Automator クイックアクション ファイルまたはフォルダ すべてのアプリケーション AppleScriptを実行 ``` on run {input, parameters} -- 秒数を入力するダイアログを表示 display dialog "タイムシフトする秒数を入力してください:(マイナス入力も可)" default answer "0" set shiftSeconds to text returned of result -- 入力された秒数とファイルパスを返す set filePath to POSIX path of (item 1 of input) return {filePath, shiftSeconds} end run ``` シェルスクリプトを実行 /bin/bash 引数として ``` #!/bin/bash # AppleScriptから受け取った入力 input_file="$1" shift_seconds="$2" # 元のファイルと同じディレクトリに出力ファイルを作成 output_file="${input_file%.vtt}_timeshifted.vtt" # タイムスタンプをシフトする関数 shift_timestamp() { local timestamp="$1" local shift="$2" # 時間を抽出 local hours=${timestamp:0:2} local minutes=${timestamp:3:2} local seconds=${timestamp:6:2} local milliseconds=${timestamp:9:3} # awkを使って小数を含む計算と再フォーマットを行う awk -v h="$hours" -v m="$minutes" -v s="$seconds" -v ms="$milliseconds" -v shift="$shift" ' BEGIN { # ミリ秒も含めてすべて秒(小数)に変換し、シフト値を加算 total_seconds = (h * 3600) + (m * 60) + s + (ms / 1000) + shift; # 負の時間を扱う if (total_seconds < 0) { total_seconds = 0; } # 時間、分、秒の整数部分を取り出す int_seconds = int(total_seconds); new_h = int(int_seconds / 3600); new_m = int((int_seconds % 3600) / 60); new_s = int_seconds % 60; # ミリ秒を再計算して四捨五入 new_ms = int((total_seconds - int_seconds) * 1000 + 0.5); # ミリ秒が1000に達した場合の繰り上げ処理 if (new_ms >= 1000) { new_s++; new_ms -= 1000; if (new_s >= 60) { new_s -= 60; new_m++; if (new_m >= 60) { new_m -= 60; new_h++; } } } # HH:MM:SS.mmm の形式で出力 printf "%02d:%02d:%02d.%03d\n", new_h, new_m, new_s, new_ms; }' } # ファイル処理 { while IFS= read -r line || [ -n "$line" ]; do [cite: 4] if [[ "$line" =~ (-->) ]]; then [cite: 5] # タイムスタンプを抽出 start_time="${line:0:12}" end_time="${line:17:12}" # タイムスタンプをシフト new_start_time=$(shift_timestamp "$start_time" "$shift_seconds") new_end_time=$(shift_timestamp "$end_time" "$shift_seconds") # 新しい行を生成 echo "${new_start_time} --> ${new_end_time}" else # タイムスタンプ行以外はそのまま出力 echo "$line" fi done } < "$input_file" > "$output_file" echo "タイムシフトされたファイルが ${output_file} に保存されました。" ``` VTTの連番を付け直す.workflow Automator クイックアクション ファイルまたはフォルダ Finder.app シェルスクリプトを実行 /bin/bash 引数として ``` #!/bin/bash # 選択されたすべてのファイルに対して処理を実行 for input_file in "$@" do # 拡張子がvttでない場合はスキップ if [[ "${input_file##*.}" != "vtt" ]]; then continue fi # 出力ファイル名(_renumbered.vtt) output_file="${input_file%.vtt}_renumbered.vtt" # 先頭に必ずWEBVTTを入れる(上書きして初期化) echo "WEBVTT" > "$output_file" echo "" >> "$output_file" counter=1 # ファイルを1行ずつ読み込む while IFS= read -r line || [ -n "$line" ]; do # 改行コード(CR)が含まれている場合は削除(環境による文字化け対策) line="${line//$'\r'/}" # "WEBVTT" という文字列を含む行はスキップ(結合時に混ざった不要なヘッダーを削除) if [[ "$line" == *"WEBVTT"* ]]; then continue fi # 数字だけの行(古い連番)はスキップ if [[ "$line" =~ ^[0-9]+$ ]]; then continue fi # タイムスタンプ行(--> を含む)を見つけたら、新しい連番を打ってからタイムスタンプを出力 if [[ "$line" =~ "-->" ]]; then echo "$counter" >> "$output_file" echo "$line" >> "$output_file" counter=$((counter + 1)) else # 空行や、実際の字幕テキスト行はそのまま出力 # ただし、一番最初のWEBVTT直後以外の純粋なデータ行のみ追記していく if [[ -n "$line" || $counter -gt 1 ]]; then echo "$line" >> "$output_file" fi fi done < "$input_file" # 完了通知 osascript -e "display notification \"整理と連番振り直しが完了しました。\" with title \"VTT連番振り直し\"" done ```