最近有个需求,就是把我用中文录制的视频转成日语。基本思路是:
中文视频->采集中文字幕->优化->转成日语->转成日语语音->合成到视频中。
当前很多软件具有这个功能,一是收费,二是正好实现一下,把代码保留在自己手中,方便集成。
下面是直接看代码吧!
smartfill.srt
1
00:00:02,912 --> 00:00:05,905
こんにちは、今日は皆さんに紹介します。
2
00:00:05,905 --> 00:00:08,412
一つの生成型AIに基づいた
3
00:00:08,412 --> 00:00:11,162
インテリジェントな補助ツール、
4
00:00:11,162 --> 00:00:13,913
入力ツール、smart fillです。
5
00:00:13,913 --> 00:00:16,663
smart fill は、私が開発したツールで、
6
00:00:17,391 --> 00:00:20,060
生成型AIをベースにして、
7
00:00:20,303 --> 00:00:23,295
大規模言語モデルを駆動基盤とした
8
00:00:23,295 --> 00:00:26,288
Chrome拡張機能を使った、
9
00:00:26,612 --> 00:00:29,605
インテリジェントな入力ツールです。
10
00:00:29,605 --> 00:00:32,516
smart fill は、ユーザーが提供する
11
00:00:32,516 --> 00:00:35,509
情報を抽出して、要約、計算、分類、
12
00:00:35,711 --> 00:00:38,421
変換することができます。
13
00:00:38,583 --> 00:00:41,171
これにより、迅速かつ正確にユーザーを
14
00:00:41,495 --> 00:00:44,488
サポートし、これらの情報をフォームに
15
00:00:44,488 --> 00:00:47,400
自動的に入力します。現在、
16
00:00:47,400 --> 00:00:50,231
smart fill はテキストの自動入力機能を
17
00:00:50,231 --> 00:00:53,223
提供しており、私たちが文章を書いて、
18
00:00:53,223 --> 00:00:55,650
拡張機能に入力すると、
19
00:00:55,974 --> 00:00:58,805
自動的にこのページに
20
00:00:58,805 --> 00:01:01,797
入力されます。このデモのように。
21
00:01:01,797 --> 00:01:04,709
さらに、様々なファイルのアップロードも
22
00:01:04,709 --> 00:01:07,379
サポートしており、画像やWord、PDF、
23
00:01:07,621 --> 00:01:10,533
Excel、TXTなど
24
00:01:10,533 --> 00:01:13,122
テキストファイルをアップロードして、
25
00:01:13,445 --> 00:01:16,276
要約と分類を行い、
26
00:01:16,438 --> 00:01:18,137
フォームに自動的に
27
00:01:19,876 --> 00:01:22,788
入力される機能を提供しています。
28
00:01:22,788 --> 00:01:25,578
また、リアルタイム音声機能も提供します。
29
00:01:25,902 --> 00:01:28,571
私たちが話している間に、
30
00:01:28,571 --> 00:01:31,564
smart fill が情報を収集し、
31
00:01:31,564 --> 00:01:34,557
それをテキストに変換し、最終的に
32
00:01:34,557 --> 00:01:37,468
送信されると、フォームに自動的に入力されます。
33
00:01:37,468 --> 00:01:40,461
同時に、スマホのリモート送信も
34
00:01:40,461 --> 00:01:43,373
サポートしています。スマホでQRコードを
35
00:01:43,373 --> 00:01:46,123
スキャンして、スマホのカメラや
36
00:01:46,528 --> 00:01:49,318
マイクを使って情報を
37
00:01:49,318 --> 00:01:51,866
収集し、それをテキストに
38
00:01:51,866 --> 00:01:54,778
変換して、フォームに自動的に
39
00:01:54,778 --> 00:01:57,528
入力することができます。
40
00:01:59,874 --> 00:02:01,896
さて、smart fill に関する
41
00:02:02,624 --> 00:02:05,536
紹介については、こちらのサイトで
42
00: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; }
}
最后的结果如下: