目次
自動翻訳動画作成方法
はじめに
- STTとLLMを利用して動画から日本語字幕と日本語音声を生成する方法です。
- 全てのプログラムはLLMによって出力されたものです。
手順
- yt-dlp などを利用して翻訳対象の動画をダウンロードする
- Subtitle Edit などの SST 機能を利用して (1) の動画ファイルから字幕ファイルを生成する
- 以下のようなプロンプトを利用して字幕ファイルを翻訳する
// URL Context: ON
// Grounding with Google Search: ON
韓国語の字幕ファイル(SRT形式)を日本語訳しなさい。
字幕ファイルの内容はアラド戦記(던전앤파이터)というゲームのディレジエレイドというコンテンツの解説です。
詳細な情報は以下URLのwikiに記述されています。参考にしなさい。
- https://namu.wiki/w/%EA%B2%80%EC%9D%80%20%EC%A7%88%EB%B3%91%EC%9D%98%20%EB%94%94%EB%A0%88%EC%A7%80%EC%97%90%20%EB%A0%88%EC%9D%B4%EB%93%9C
- https://namu.wiki/w/%EA%B2%80%EC%9D%80%20%EC%A7%88%EB%B3%91%EC%9D%98%20%EB%94%94%EB%A0%88%EC%A7%80%EC%97%90%20%EB%A0%88%EC%9D%B4%EB%93%9C/1%ED%8E%98%EC%9D%B4%EC%A6%88
- https://namu.wiki/w/%EA%B2%80%EC%9D%80%20%EC%A7%88%EB%B3%91%EC%9D%98%20%EB%94%94%EB%A0%88%EC%A7%80%EC%97%90%20%EB%A0%88%EC%9D%B4%EB%93%9C/2%ED%8E%98%EC%9D%B4%EC%A6%88
- Qwen3-TTS を利用して日本語音声ファイルを生成する
# .venv\Scripts\python.exe -m pip install webvtt-py
# .venv\Scripts\python.exe vtt_to_audio.py
import torch
import soundfile as sf
from qwen_tts import Qwen3TTSModel
from pydub import AudioSegment
import webvtt
# モデル読み込み(初回はHugging Faceからダウンロード)
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-Base",
device_map="cuda:0",
dtype=torch.float16
)
print('model load finished')
ref_audio = "ref_audio.wav"
ref_text = "おはようございます よろしくお願いします"
i=1
for caption in webvtt.read("ja.vtt"):
# 改行を削除する
captionText = caption.text.replace('\r\n', '\n').replace('\n', '')
print(captionText)
# 音声ファイルを生成する
wavs, sr = model.generate_voice_clone(
text=captionText,
language="Japanese",
ref_audio=ref_audio,
ref_text=ref_text,
)
# 保存する
sf.write(f"c:\\work\\wav\\{i:03d}.wav", wavs[0], sr)
print(f"Saved! {i}")
i = i + 1
- wavファイルをmp3ファイルに変換する。再生速度は1.5倍速にする。
for (int i = 1; i <= 330; i++)
{
var num = i.ToString("D3"); // 001, 002, …, 330
var line = $"ffmpeg -i {num}.wav -filter:a \"atempo=1.5\" -vn -y ..\\mp3\\{num}.mp3";
Console.WriteLine(line);
}
- 動画ファイルに日本語音声をマージする
using SubtitlesParserV2;
using System.Diagnostics;
using System.Text;
class AudioVideoMerger
{
// FFmpeg 実行パス (環境変数にパスが通っている場合は "ffmpeg" で可)
private const string FfmpegPath = "ffmpeg";
// 音声パート情報 (ファイルパス, 開始秒, 終了秒)
private readonly List<(string filePath, double start, double end)> audioParts;
private readonly string videoPath;
private readonly string outputPath;
private readonly string tempDir;
public AudioVideoMerger(string videoPath, List<(string, double, double)> audioParts, string outputPath)
{
this.videoPath = videoPath;
this.audioParts = audioParts;
this.outputPath = outputPath;
this.tempDir = Path.Combine(Path.GetTempPath(), "AudioVideoMerge_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(this.tempDir);
}
public void Execute()
{
try
{
Mux(videoPath, outputPath);
// MergeAudioWithVideo(mergedAudioPath, videoPath, outputPath);
}
finally
{
Console.WriteLine("ffmpeg の終了待機中(5秒)");
Thread.Sleep(TimeSpan.FromSeconds(5));
// 一時ファイルの削除 (必要に応じてコメントアウト可)
Directory.Delete(tempDir, true);
}
}
private void Mux(string videoPath, string outputPath)
{
string filterFile = Path.Combine(Path.GetTempPath(), "filter.txt");
var filter = new StringBuilder();
// 元の動画の音量を5%で維持する
// filter.AppendLine("[0:a]volume=0.05[base];");
// 元の動画の音量を0%で維持する
filter.AppendLine("[0:a]volume=0.00[base];");
for (int i = 0; i < audioParts.Count; i++)
{
// ミリ秒単位に変換
int delay = (int)Math.Round(audioParts[i].start * 1000);
filter.AppendLine($"[{i+1}:a]adelay={delay}|{delay}[a{i+1}];");
}
for (int i = 0; i < audioParts.Count; i++)
{
filter.Append($"[a{i+1}]");
}
filter.AppendLine($"amix=inputs={audioParts.Count}:dropout_transition=0:normalize=0[jpmix];");
filter.AppendLine("[base][jpmix]amix=inputs=2:dropout_transition=0[aout];");
File.WriteAllText(filterFile, filter.ToString());
var args = new StringBuilder();
args.Append($"-i \"{videoPath}\" ");
foreach (var part in audioParts)
{
args.Append($"-i \"{part.filePath}\" ");
}
args.Append($"-filter_complex_script \"{filterFile}\" ");
args.Append("-map 0:v ");
args.Append("-map \"[aout]\" ");
args.Append("-c:v copy ");
args.Append("-c:a aac -b:a 128k -y ");
args.Append($"\"{outputPath}\"");
RunProcess(FfmpegPath, args.ToString());
}
// 音声と動画を合成 (映像はコピー、音声は AAC に変換)
private void MergeAudioWithVideo(string audioPath, string videoPath, string outputPath)
{
// string args = $"-i \"{videoPath}\" -i \"{audioPath}\" -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 -shortest -y \"{outputPath}\"";
var args = $"-i {videoPath} -i {audioPath} -filter_complex \"[0:a]volume=0.1[a0];[a0][1:a]amix=inputs=2:duration=first:dropout_transition=2[aout]\" -map 0:v -map \"[aout]\" -c:v copy -c:a aac -b:a 192k -strict -2 -y {outputPath}";
Console.WriteLine(args);
RunProcess(FfmpegPath, args);
}
// 汎用的なプロセス実行ユーティリティ
private void RunProcess(string fileName, string arguments)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true
};
using (var proc = Process.Start(startInfo))
{
proc.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
Console.WriteLine(e.Data);
}
};
proc.ErrorDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
Console.Error.WriteLine(e.Data);
}
};
proc.Start();
// 非同期で読み取り開始
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
proc.WaitForExit();
Console.WriteLine($"FFmpeg ExitCode={proc.ExitCode}");
}
}
}
class Program
{
/// <summary>
/// ミリ秒を秒に変換する。
/// </summary>
/// <param name="milliseconds">変換対象のミリ秒。整数でも実数でも可。</param>
/// <returns>変換後の秒数(小数部を含む)</returns>
public static double MillisecondsToSeconds(double milliseconds)
{
return milliseconds / 1000.0;
}
static void Main()
{
// 動画ファイル
string videoFile = @"C:\work\input.webm";
// 音声パート (ファイルパス, 開始秒, 終了秒)
var parts = new List<(string, double, double)>();
using (FileStream fileStream = System.IO.File.OpenRead("C:\\work\\\\ja.vtt"))
{
var result = SubtitleParser.ParseStream(fileStream, Encoding.UTF8);
foreach (var x in result?.Subtitles?.Index() ?? [])
{
var index = x.Index + 1;
parts.Add((@$"C:\work\mp3\{index:D3}.mp3", MillisecondsToSeconds(x.Item.StartTime), MillisecondsToSeconds(x.Item.EndTime)));
}
}
// 出力動画
string outputFile = @"C:\work\output.mp4";
var merger = new AudioVideoMerger(videoFile, parts, outputFile);
merger.Execute();
Console.WriteLine("合成が完了した。");
}
}
以上