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) { //Console.WriteLine($"hoping for {srcKey} (would move it to {filePickups[srcKey]})"); if(File.Exists(srcKey)) { Console.WriteLine($"found {srcKey}"); string dest; if (filePickups.TryRemove(srcKey, out dest)) { File.Move(srcKey, dest, true); } else { Console.Error.WriteLine($"Failed to remove {srcKey} from filepickups. Unrecoverable, I think."); Environment.Exit(-1); } } } if(scriptFileCancellationToken.IsCancellationRequested) { //Console.WriteLine($"Cancellation requested. but I'm using it in more of a \"there won't be more work, but finish what you're doing\" kind of way."); if(filePickups.IsEmpty) { Console.WriteLine("and we got nothing left, so we quit."); return; } } System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5)); } } private static async void FFStuff() { var cardCount = 0; var magnitude = 1; Console.WriteLine($"{titleCardLines.Count} title card lines"); magnitude = (int)Math.Log10(titleCardLines.Count); Directory.CreateDirectory(conf.titlecards_dest); foreach(var tcL in titleCardLines) { Console.WriteLine("titlecard commanding"); await Process.Start(conf.titlecard_command, tcL).WaitForExitAsync(); cardCount++; File.Move("tc.png", Path.Combine(conf.titlecards_dest, $"tc{cardCount.ToString($"D{magnitude}")}.png"), true); } Console.WriteLine("titlecards done"); if(noteLines != null) { cardCount = 0; Console.WriteLine($"{noteLines?.Count} note lines"); magnitude = (int)Math.Log10(noteLines?.Count ?? 1); Directory.CreateDirectory(conf.notes_dest); foreach(var nL in noteLines) { Console.WriteLine("note commanding"); await Process.Start(conf.note_command, nL).WaitForExitAsync(); cardCount++; File.Move("note.png", Path.Combine(conf.notes_dest, $"note{cardCount.ToString($"D{magnitude}")}.png"), true); } } } 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 directiveLines = new List(); var altVOLines = 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.Enqueue(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.Enqueue(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)); Console.WriteLine("sleeping for 1 sec before requesting cancellation"); System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1)); 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()).Trim('[').Trim(']').Trim('"'); 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}"); } } } } }