用.srt字幕文件生成.wav语音

文摘   2024-11-06 07:30   日本  

最近有个需求,就是把我用中文录制的视频转成日语。基本思路是:

中文视频->采集中文字幕->优化->转成日语->转成日语语音->合成到视频中。

当前很多软件具有这个功能,一是收费,二是正好实现一下,把代码保留在自己手中,方便集成。

下面是直接看代码吧!

smartfill.srt

100:00:02,912 --> 00:00:05,905こんにちは、今日は皆さんに紹介します。
200:00:05,905 --> 00:00:08,412一つの生成型AIに基づいた
300:00:08,412 --> 00:00:11,162インテリジェントな補助ツール、
400:00:11,162 --> 00:00:13,913入力ツール、smart fillです。
500:00:13,913 --> 00:00:16,663smart fill は、私が開発したツールで、
600:00:17,391 --> 00:00:20,060生成型AIをベースにして、
700:00:20,303 --> 00:00:23,295大規模言語モデルを駆動基盤とした
800:00:23,295 --> 00:00:26,288Chrome拡張機能を使った、
900:00:26,612 --> 00:00:29,605インテリジェントな入力ツールです。
1000:00:29,605 --> 00:00:32,516smart fill は、ユーザーが提供する
1100:00:32,516 --> 00:00:35,509情報を抽出して、要約、計算、分類、
1200:00:35,711 --> 00:00:38,421変換することができます。
1300:00:38,583 --> 00:00:41,171これにより、迅速かつ正確にユーザーを
1400:00:41,495 --> 00:00:44,488サポートし、これらの情報をフォームに
1500:00:44,488 --> 00:00:47,400自動的に入力します。現在、
1600:00:47,400 --> 00:00:50,231smart fill はテキストの自動入力機能を
1700:00:50,231 --> 00:00:53,223提供しており、私たちが文章を書いて、
1800:00:53,223 --> 00:00:55,650拡張機能に入力すると、
1900:00:55,974 --> 00:00:58,805自動的にこのページに
2000:00:58,805 --> 00:01:01,797入力されます。このデモのように。
2100:01:01,797 --> 00:01:04,709さらに、様々なファイルのアップロードも
2200:01:04,709 --> 00:01:07,379サポートしており、画像やWordPDF
2300:01:07,621 --> 00:01:10,533ExcelTXTなど
2400:01:10,533 --> 00:01:13,122テキストファイルをアップロードして、
2500:01:13,445 --> 00:01:16,276要約と分類を行い、
2600:01:16,438 --> 00:01:18,137フォームに自動的に
2700:01:19,876 --> 00:01:22,788入力される機能を提供しています。
2800:01:22,788 --> 00:01:25,578また、リアルタイム音声機能も提供します。
2900:01:25,902 --> 00:01:28,571私たちが話している間に、
3000:01:28,571 --> 00:01:31,564smart fill が情報を収集し、
3100:01:31,564 --> 00:01:34,557それをテキストに変換し、最終的に
3200:01:34,557 --> 00:01:37,468送信されると、フォームに自動的に入力されます。
3300:01:37,468 --> 00:01:40,461同時に、スマホのリモート送信も
3400:01:40,461 --> 00:01:43,373サポートしています。スマホでQRコードを
3500:01:43,373 --> 00:01:46,123スキャンして、スマホのカメラや
3600:01:46,528 --> 00:01:49,318マイクを使って情報を
3700:01:49,318 --> 00:01:51,866収集し、それをテキストに
3800:01:51,866 --> 00:01:54,778変換して、フォームに自動的に
3900:01:54,778 --> 00:01:57,528入力することができます。
4000:01:59,874 --> 00:02:01,896さて、smart fill に関する
4100:02:02,624 --> 00:02:05,536紹介については、こちらのサイトで
4200:02:05,779 --> 00:02:08,691さらに詳しい情報を確認できます。

实现代码,这里用了Microsoft的Speech,当然也是收费的,至少手边有,不用重新购买。代码基本思路是把.srt转成Speech识别的SSML,然后交给Sppeck API就可以了。

using System.Text;using System.Text.RegularExpressions;using Microsoft.CognitiveServices.Speech;

string subscriptionKey = File.ReadAllText("C:/GPT/audiokey.txt");string region = "westus2";var config = SpeechConfig.FromSubscription(subscriptionKey, region);var voceName = "ja-JP-KeitaNeural";config.SpeechSynthesisVoiceName = voceName;using var synthesizer = new SpeechSynthesizer(config, null);synthesizer.SynthesisStarted += (s, e) =>{ Console.WriteLine($"开始生成音频……");};synthesizer.SynthesisCompleted += (s, e) =>{ Console.WriteLine($"生成语音完成!");};
var subtitles = ParseSrt(Directory.GetCurrentDirectory() + "/smartfill.srt");var voices = ParseVoices(subtitles);var ssml = BuildSSML(voices);
using (var result = await synthesizer.SpeakSsmlAsync(ssml.ToString())){ if (result.Reason == ResultReason.SynthesizingAudioCompleted) { using (var outputStream = new FileStream("smartfill.wav", FileMode.Create)) { using (var waveFileWriter = new NAudio.Wave.WaveFileWriter(outputStream, new NAudio.Wave.WaveFormat(16000, 16, 1))) { waveFileWriter.Write(result.AudioData, 0, result.AudioData.Length); } } } else { Console.WriteLine($"语音合成失败:{result.Reason}"); }}string BuildSSML(List<VoiceItem> voices){ var ssmlBuilder = new StringBuilder($@"<speak version=""1.0"" xmlns=""http://www.w3.org/2001/10/synthesis"" xmlns:mstts=""http://www.w3.org/2001/mstts"" xml:lang=""en-US"">"); foreach (var voice in voices) { if (string.IsNullOrWhiteSpace(voice.Text)) { ssmlBuilder.AppendLine(@$"<voice name=""{voceName}""><mstts:silence type=""comma-exact"" value=""{voice.TotalMillisecond}ms""/>,</voice>"); } else { ssmlBuilder.AppendLine(@$"<voice name=""{voceName}""><mstts:audioduration value=""{voice.TotalMillisecond}ms""/> <mstts:express-as style=""advertisement_upbeat"" styledegree=""2"">{voice.Text} </mstts:express-as></voice>"); } } ssmlBuilder.AppendLine("</speak>"); return ssmlBuilder.ToString();}List<VoiceItem> ParseVoices(List<SubtitleItem> subtitles){ var voices = new List<VoiceItem>(); if (subtitles[0].StartTime != TimeSpan.Zero) { var voice = new VoiceItem(); voice.Text = ""; voice.TotalMillisecond = (long)(subtitles[0].StartTime - TimeSpan.Zero).TotalMilliseconds; voices.Add(voice); } var newVoice = new VoiceItem(); var starTS = subtitles[0].StartTime; newVoice.Text += subtitles[0].Text.Replace(" ", ""); var endTS = TimeSpan.Zero; for (var i = 0; i < subtitles.Count - 1; i++) { var subtitle1 = subtitles[i]; var subtitle2 = subtitles[i + 1]; if (subtitle1.EndTime == subtitle2.StartTime) { endTS = subtitle2.EndTime; newVoice.Text += subtitle2.Text.Replace(" ", ""); } else { endTS = subtitle1.EndTime; newVoice.TotalMillisecond += (long)(endTS - starTS).TotalMilliseconds; voices.Add(newVoice); voices.Add(new VoiceItem { Text = "", TotalMillisecond = (long)(subtitle2.StartTime - subtitle1.EndTime).TotalMilliseconds }); newVoice = new VoiceItem(); newVoice.Text = subtitle2.Text.Replace(" ", ""); starTS = subtitle2.StartTime; } } endTS = subtitles[subtitles.Count - 1].EndTime; newVoice.TotalMillisecond += (long)(endTS - starTS).TotalMilliseconds; voices.Add(newVoice);
return voices;}List<SubtitleItem> ParseSrt(string filePath){ var subtitles = new List<SubtitleItem>(); var regex = new Regex( @"^\s*(\d+)\s*(?:\r\n|\n|\r) (?<start>\d{2}:\d{2}:\d{2},\d{3})\s*-->?\s* (?<end>\d{2}:\d{2}:\d{2},\d{3})(?:\r\n|\n|\r) (?<text>.*?)(?=(?:\r\n\r\n|\n\n|\r\r|\z))", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace);
var srtContent = File.ReadAllText(filePath); foreach (Match match in regex.Matches(srtContent)) { var subtitle = new SubtitleItem { StartTime = TimeSpan.Parse(match.Groups["start"].Value.Replace(",", ".")), EndTime = TimeSpan.Parse(match.Groups["end"].Value.Replace(",", ".")), Text = match.Groups["text"].Value.Replace("\r\n", " ").Replace("\n", " ").Replace(" ", "") }; subtitles.Add(subtitle); } return subtitles;}
public class VoiceItem{ public long TotalMillisecond { get; set; } public string Text { get; set; }}
public class SubtitleItem{ public TimeSpan StartTime { get; set; } public TimeSpan EndTime { get; set; } public string Text { get; set; }}

最后的结果如下:

当然,质量还需要提升优化,这都是细活了,这里就不再继续了。

桂迹
分享原创,记录痕迹!
 最新文章