using System.Collections.Concurrent; using System.Data; using System.Diagnostics; using System.Net; using System.Reflection.Metadata.Ecma335; using System.Runtime.CompilerServices; using System.Runtime.InteropServices.Marshalling; using System.Text; using System.Text.RegularExpressions; using System.Timers; using System.Xml.Schema; using Newtonsoft.Json; namespace placeholdervo { public partial class Program { [GeneratedRegex("^(\\[|#)")] private static partial Regex NonTextLine(); public static Config conf; private static string workingDir = "./"; private static HttpClient hc; private static ConcurrentQueue titleCardLines = new ConcurrentQueue(); private static ConcurrentQueue noteLines = new ConcurrentQueue(); private static ConcurrentDictionary filePickups = new ConcurrentDictionary(); private static CancellationToken scriptFileCancellationToken; ///mostly just to give myself the reminder about paths ///the *complete path* of the awaited file. you might want conf.sync_dropoff ///the *complete path* to move the file to. private static async void awaitFile(string src, string dest) { filePickups[src] = dest; } private static async void filePickup() { while(true) { foreach(var srcKey in filePickups.Keys) { // var files = Directory.GetFiles(conf.sync_dropoff); // Console.WriteLine($"files listed: {string.Join(", ", files)}"); // Console.WriteLine($"hoping for: {string.Join(", ", filePickups.Keys)}"); // Console.WriteLine($"sanity check: first listed file: {Path.GetFileName(files.FirstOrDefault())}"); // Console.WriteLine($"sanity check: first key: {scriptFragmentVoiceFiles.Keys.FirstOrDefault()}"); // Console.WriteLine($"sanity check: first value: {scriptFragmentVoiceFiles[scriptFragmentVoiceFiles.Keys.FirstOrDefault()]}"); if(File.Exists(srcKey)) { Console.WriteLine($"found {srcKey}"); string dest; if (filePickups.TryRemove(srcKey, out dest)) { File.Move(srcKey, dest); } else { Console.Error.WriteLine($"Failed to remove {srcKey} from filepickups. Unrecoverable, I think."); Environment.Exit(-1); } } } if(scriptFileCancellationToken.IsCancellationRequested) { return; } System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5)); } } private static async void FFStuff() { var cardCount = 0; foreach(var tcL in titleCardLines) { await Process.Start(conf.titlecard_command, tcL).WaitForExitAsync(); File.Move("tc.png", Path.Combine(conf.titlecards_dest, $"tc{cardCount++}.png")); } cardCount = 0; foreach(var nL in noteLines) { await Process.Start(conf.titlecard_command, nL).WaitForExitAsync(); File.Move("n0.png", Path.Combine(conf.notes_dest, $"note{cardCount++}.png")); } } public static async Task Main(string[] args) { conf = JsonConvert.DeserializeObject( File.ReadAllText( AppDomain.CurrentDomain.BaseDirectory + "appsettings.json")) ?? new Config(); hc = new HttpClient(){ BaseAddress = new Uri(conf.speech_service)}; var taskHeap = new List(); var scriptPath = args?.FirstOrDefault(); if(string.IsNullOrWhiteSpace(scriptPath)) { scriptPath = "./script.md"; if(!File.Exists(scriptPath)) { scriptPath = "./script.txt"; if(!File.Exists(scriptPath)) { Console.Error.WriteLine("no script found."); return; } } } var scriptBasename = Path.GetFileNameWithoutExtension(scriptPath); workingDir = Path.GetDirectoryName(scriptPath); var cancelSource = new CancellationTokenSource(); scriptFileCancellationToken = cancelSource.Token; var lines = File.ReadAllLines(scriptPath) .Append("\n").Append("\n"); var primaryVOLines = new List(); var titlecardUnprocessed = new List(); var titlecardLines = new List(); var directiveLines = new List(); var altVOLines = new List(); var noteLines = new List(); foreach(var l in lines) { if(l.StartsWith('#')) { titlecardUnprocessed.Add(l); } else if (l.StartsWith('[')) { directiveLines.Add(l); } else { //as opposed to Environment.NewLine, which will be running on linux, and thus \n. ms speech api cooperates more with \r\n. l.ReplaceLineEndings("\r\n"); primaryVOLines.Add(l); } } var scriptStripped = $"{scriptBasename}-primaryVO.txt"; File.WriteAllLines(scriptStripped, primaryVOLines); taskHeap.Add(ttsVO(scriptStripped, Path.Combine(conf.VODropoff, $"primaryVO"))); var titleLevels = new List(){0}; var titleIncrementDepth = 0; foreach(var l in titlecardUnprocessed) { titleIncrementDepth = 1; var thisString = l; while(thisString.StartsWith('#')) { thisString = thisString.Substring(1); if(!thisString.StartsWith('#')) { if(titleLevels.Count < titleIncrementDepth) { titleLevels = [.. titleLevels, 0]; } titleLevels[titleIncrementDepth-1]++; if(titleLevels.Count > titleIncrementDepth) { titleLevels.RemoveRange(titleIncrementDepth, titleLevels.Count - titleIncrementDepth); } var TitleCardText = thisString.Trim(); if(titleLevels.Count > 1) { TitleCardText = string.Join('.', titleLevels[1..]) + ": " + TitleCardText; } Console.WriteLine($"title card: {TitleCardText}"); titlecardLines.Add(TitleCardText); } else { titleIncrementDepth++; } } } foreach(var l in directiveLines) { var thisLine = l.Trim(); if(!thisLine.Contains(']')) { Console.Error.WriteLine("malformed directive line: "); Console.WriteLine(thisLine); continue; } if (thisLine.EndsWith(']')) { thisLine = thisLine.Trim('[', ']'); if (Uri.TryCreate(thisLine, UriKind.Absolute, out Uri asUri) && (asUri.Scheme == Uri.UriSchemeHttp || asUri.Scheme == Uri.UriSchemeHttps)) { Console.WriteLine($"uri: {asUri}. ship off to yt-dlp or project Ose?"); } else { //just a directive, we can't do anything here Console.WriteLine($"mere directive: {thisLine}"); } } else { thisLine = thisLine.TrimStart('['); var directiveName = thisLine[..thisLine.IndexOf(']')]; var parameterText = thisLine[(thisLine.IndexOf(']') + 1) ..].Trim(); if(directiveName == "note") { noteLines.Add(parameterText); Console.WriteLine($"on screen note: {parameterText}"); } else { altVOLines.Add(parameterText); Console.WriteLine($"alt VA: {parameterText}"); } } } if(altVOLines?.Count > 0) { var altVOscript = $"{scriptBasename}-altVO.txt"; File.WriteAllLines(altVOscript, altVOLines); taskHeap.Add(ttsVO(altVOscript, Path.Combine(conf.VODropoff, $"altVO"))); } // execute them all taskHeap.Add(Task.Run(FFStuff)); taskHeap.Add(Task.Run(filePickup)); cancelSource.Cancel(); Task.WaitAll([.. taskHeap]); Console.WriteLine("kbai"); } private static async Task ttsVO(string scriptPath, string destPath) { HttpContent fileStreamContent = new StreamContent(File.OpenRead(scriptPath)); using (var formData = new MultipartFormDataContent()) { formData.Add(fileStreamContent, "file1", "file1"); var response = await hc.PostAsync("/speak", formData); if (!response.IsSuccessStatusCode) { Console.Error.WriteLine($"{response.StatusCode} - {response.ReasonPhrase} - {await response.Content.ReadAsStringAsync()}"); } else { var fname = await response.Content.ReadAsStringAsync(); Console.WriteLine($"success; {fname}."); //"success" might be a strong word; it's successfully submitted. Its decided what filename it will, eventually, give it. var ext = Path.GetExtension(fname); Directory.CreateDirectory(conf.VODropoff); awaitFile(Path.Combine(conf.sync_dropoff, fname), destPath + $".{ext}"); } } } } }