diff --git a/ConsoleWhisper/ConsoleWhisper.csproj b/ConsoleWhisper/ConsoleWhisper.csproj index 59db273..059a834 100644 --- a/ConsoleWhisper/ConsoleWhisper.csproj +++ b/ConsoleWhisper/ConsoleWhisper.csproj @@ -11,7 +11,7 @@ ConsoleWhisper - 1.0.1 + 1.1.0 azhuge233 diff --git a/ConsoleWhisper/Model/Argument.cs b/ConsoleWhisper/Model/Argument.cs index d6b48c2..976ab56 100644 --- a/ConsoleWhisper/Model/Argument.cs +++ b/ConsoleWhisper/Model/Argument.cs @@ -4,8 +4,8 @@ namespace ConsoleWhisper.Model { public class Argument { - [Option('i', "input", Required = true, Hidden = false, Separator = ' ', HelpText = "Input media files.")] - [Value(4)] + [Option('i', "input", Required = true, Hidden = false, Separator = ',', HelpText = "Input media files.")] + [Value(5)] public IEnumerable Files { get; set; } [Option('m', "model", Required = false, Hidden = false, Default = "small", HelpText = "Whisper model: base, tiny, small, medium, large.")] @@ -24,6 +24,10 @@ public class Argument { [Value(3, Required = false)] public bool OnlyExtract { get; set; } + [Option("multithread", Required = false, Hidden = false, Default = false, HelpText = "Use multithreading when extracting soundtrack.")] + [Value(4, Required = false)] + public bool Multithread { get; set; } + //[Option('g', "gpu", Required = false, Hidden = true, HelpText = "Currently not implemented.")] //[Value(2, Required = false, Default = false)] //public bool GPU { get; set; } @@ -39,7 +43,7 @@ public class Argument { throw new ArgumentException(message: $"Language \"{Language}\" is not supported.\nCheck {LanguageLink} for available languages."); } - internal const int SupportedArgumentsCount = 7; + internal const int SupportedArgumentsCount = 8; private static readonly HashSet SupportedModels = new() { "base", "tiny", "small", "medium", "large" }; private static readonly HashSet SupportedLanguages = new() { "en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", diff --git a/ConsoleWhisper/Module/AudioHelper.cs b/ConsoleWhisper/Module/AudioHelper.cs index c4e6bec..27f0b52 100644 --- a/ConsoleWhisper/Module/AudioHelper.cs +++ b/ConsoleWhisper/Module/AudioHelper.cs @@ -1,4 +1,5 @@ -using NAudio.Wave; +using ConsoleWhisper.Model; +using NAudio.Wave; using System; using System.Collections.Generic; using System.IO; @@ -8,20 +9,20 @@ namespace ConsoleWhisper.Module { public static class AudioHelper { - public static async Task Extract(string outputDir, string mediaFilename, bool isOnlyExtract = false) { + public static async Task Extract(string mediaFilename, Argument arg) { try { - var audioFilename = isOnlyExtract ? FileHelper.GetAudioPath(outputDir, mediaFilename) : FileHelper.GetTempMp3File(); + var audioFilename = arg.OnlyExtract ? FileHelper.GetAudioPath(arg.OutputDir, mediaFilename) : FileHelper.GetTempMp3File(); var mediaInfo = await FFmpeg.GetMediaInfo(mediaFilename); - int audioStreamIndex = GetAudioStreamIndex(mediaInfo.AudioStreams.ToList()); + int audioStreamIndex = arg.Multithread ? 0 : GetAudioStreamIndex(mediaInfo.AudioStreams.ToList()); var audioStream = mediaInfo.AudioStreams .Skip(audioStreamIndex) .FirstOrDefault(); await DoConversion(audioStream, audioFilename); - if (!isOnlyExtract) { + if (!arg.OnlyExtract) { var resampledWaveFilename = Resample(audioFilename); FileHelper.DelFile(audioFilename); diff --git a/ConsoleWhisper/Module/FileHelper.cs b/ConsoleWhisper/Module/FileHelper.cs index 7fb17e7..764994a 100644 --- a/ConsoleWhisper/Module/FileHelper.cs +++ b/ConsoleWhisper/Module/FileHelper.cs @@ -1,15 +1,20 @@ -using System; +using ConsoleWhisper.Model; +using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using Xabe.FFmpeg; +using Xabe.FFmpeg.Downloader; namespace ConsoleWhisper.Module { internal static class FileHelper { + #region Directories internal static readonly string AppDirectory = AppDomain.CurrentDomain.BaseDirectory; internal static readonly string FFmpegLocation = Path.Combine(AppDirectory, "ffmpeg.exe"); internal static readonly string FFprobeLocation = Path.Combine(AppDirectory, "ffprobe.exe"); internal static readonly string ModelDirectory = Path.Combine(AppDirectory, "Model"); + #endregion internal static IEnumerable ExpandFilePaths(IEnumerable paths) { try { @@ -52,15 +57,20 @@ internal static class FileHelper { return Path.Combine(outputDir, audioName); } - #region Check if necessary file exists - internal static bool ModelExists(string modelFilename) { - return File.Exists(Path.Combine(ModelDirectory, modelFilename)); - } - - internal static bool FFmpegExists() { - return File.Exists(FFmpegLocation) && File.Exists(FFprobeLocation); + internal static async Task DownloadFFmpegandModel(Argument arg) { + try { + FFmpeg.SetExecutablesPath(AppDirectory); + + var tasks = new List() { + Task.Run(DownloadFFmpeg), + Task.Run(() => DownloadModel(arg.ModelType)) + }; + + await Task.WhenAll(tasks); + } catch (Exception) { + throw; + } } - #endregion #region Get temp file name internal static string GetTempFile() { @@ -125,5 +135,31 @@ internal static class FileHelper { private const string Mp3Extension = "mp3"; private const string SrtExtension = "srt"; #endregion + + #region downloader + private static async Task DownloadFFmpeg() { + if (!FFmpegExists()) { + Output.Warn($"FFmpeg not found, start downloading."); + await FFmpegDownloader.GetLatestVersion(FFmpegVersion.Official); + } + } + + private static async Task DownloadModel(string ModelType) { + if (!ModelExists(ModelType)) { + Output.Warn($"{ModelType} is not found in Models directory, start downloading."); + await WhisperHelper.DownloadModel(ModelType); + } + } + #endregion + + #region Check if necessary file exists + private static bool ModelExists(string modelFilename) { + return File.Exists(Path.Combine(ModelDirectory, modelFilename)); + } + + private static bool FFmpegExists() { + return File.Exists(FFmpegLocation) && File.Exists(FFprobeLocation); + } + #endregion } } diff --git a/ConsoleWhisper/Module/Output.cs b/ConsoleWhisper/Module/Output.cs index 8fdea84..e76dbf8 100644 --- a/ConsoleWhisper/Module/Output.cs +++ b/ConsoleWhisper/Module/Output.cs @@ -17,7 +17,12 @@ internal static class Output { Console.WriteLine(msg); ResetColor(); } - internal static void Success(string msg) { + internal static void WarnR(string msg) { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write(msg); + ResetColor(); + } + internal static void Success(string msg) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(msg); ResetColor(); diff --git a/ConsoleWhisper/Runner.cs b/ConsoleWhisper/Runner.cs index 7fc500b..0d5b195 100644 --- a/ConsoleWhisper/Runner.cs +++ b/ConsoleWhisper/Runner.cs @@ -3,11 +3,9 @@ using ConsoleWhisper.Model; using ConsoleWhisper.Module; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using System; -using Xabe.FFmpeg.Downloader; -using Xabe.FFmpeg; +using System.Linq; namespace ConsoleWhisper { internal static class Runner { @@ -16,33 +14,15 @@ internal static class Runner { arg.Validate(); arg.ModelType = FileHelper.GetModelName(arg.ModelType); - arg.Files = FileHelper.ExpandFilePaths(arg.Files); + var mediaFileList = FileHelper.ExpandFilePaths(arg.Files).ToList(); - FFmpeg.SetExecutablesPath(FileHelper.AppDirectory); + await FileHelper.DownloadFFmpegandModel(arg); - if (!FileHelper.FFmpegExists()) { - Output.Warn($"FFmpeg not found, start downloading."); - await FFmpegDownloader.GetLatestVersion(FFmpegVersion.Official); + if (arg.Multithread) { + await DoMultithread(mediaFileList, arg); + } else { + await DoSinglethread(mediaFileList, arg); } - - if (!FileHelper.ModelExists(arg.ModelType)) { - Output.Warn($"{arg.ModelType} is not found in Models directory, start downloading."); - await WhisperHelper.DownloadModel(arg.ModelType); - } - - int cnt = 1; - foreach (var file in arg.Files) { - var mediaFilename = Path.GetFileName(file); - var wavFilename = await AudioHelper.Extract(arg.OutputDir, file, arg.OnlyExtract); - - if (!arg.OnlyExtract) { - Output.Info($"Start transcribing file #{cnt++}: {mediaFilename}"); - await WhisperHelper.Transcribe(arg.ModelType, wavFilename, mediaFilename, arg.OutputDir, arg.Language); - FileHelper.DelFile(wavFilename); - } - } - - } catch (Exception) { throw; } @@ -59,5 +39,86 @@ internal static class Runner { Output.Help(helpText, errs.IsHelp(), errs.IsVersion()); } + + private static async Task DoMultithread(List mediaFileList, Argument arg) { + try { + Output.WarnR($"Use multithreading will disable user input.\n" + + $"If media file has multiple soundtracks, program will extract the first one, you will NOT be able to choose.\n" + + $"If you are transcribing, make sure to have enough disk space to store the temp .wav file.\n" + + $"Proceed? yes(y)/No(N)): "); + + var confirm = Console.ReadLine().TrimEnd(Environment.NewLine.ToCharArray()); + if (confirm.ToLower() != "y" && confirm.ToLower() != "yes") { + Output.Error($"Aborted."); + return; + } + + var tasks = Enumerable.Range(0, mediaFileList.Count) + .Select(i => Task.Run(() => DoExtractMultithread(mediaFileList[i], arg))); + + var mediaWavMap = await Task.WhenAll(tasks); + + if (!arg.OnlyExtract) { + await DoTranscribeMultithread(mediaWavMap, arg); + } + } catch (Exception) { + throw; + } + } + + private static async Task DoSinglethread(List mediaFileList, Argument arg) { + try { + int cnt = 1; + foreach (var mediaFilename in mediaFileList) { + var wavFilename = await DoExtract(mediaFilename, arg); + if (!arg.OnlyExtract) { + await DoTranscribe(wavFilename, mediaFilename, cnt++, arg); + } + } + } catch (Exception) { + throw; + } + } + + private static async Task DoExtract(string mediaFilename, Argument arg) { + try { + var wavFilename = await AudioHelper.Extract(mediaFilename, arg); + return wavFilename; + } catch (Exception) { + throw; + } + } + + private static async Task> DoExtractMultithread(string mediaFilename, Argument arg) { + try { + var wavFilename = await AudioHelper.Extract(mediaFilename, arg); + return new KeyValuePair(mediaFilename, wavFilename); + } catch (Exception ) { + throw; + } + } + + private static async Task DoTranscribe(string wavFilename, string mediaFilename, int index, Argument arg) { + try { + await WhisperHelper.Transcribe(arg.ModelType, wavFilename, mediaFilename, arg.OutputDir, arg.Language); + Output.Info($"Start transcribing media #{index}: {mediaFilename}"); + FileHelper.DelFile(wavFilename); + } catch (Exception) { + throw; + } + } + + private static async Task DoTranscribeMultithread(KeyValuePair[] mediaWavMap, Argument arg) { + try { + int cnt = 1; + foreach (var pair in mediaWavMap) { + var wavFilename = pair.Value; + var mediaFilename = pair.Key; + await DoTranscribe(wavFilename, mediaFilename, cnt++, arg); + } + } catch (Exception) { + throw; + } + } } }