From 0b54fe430a6ea14976962d670891e8b3e6df95a5 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Fri, 22 May 2026 10:15:10 +0800 Subject: [PATCH 1/4] add YAML watcher --- OpenUtau.Core/Classic/YamlWatcher.cs | 48 +++++++++++++++++++ .../DiffSinger/DiffSingerBasePhonemizer.cs | 34 ++++++++++++- .../ArpasingPlusPhonemizer.cs | 4 +- .../EnglishCpVPhonemizer.cs | 4 +- OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs | 4 +- .../SyllableBasedPhonemizer.cs | 47 +++++++++++++++++- 6 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 OpenUtau.Core/Classic/YamlWatcher.cs diff --git a/OpenUtau.Core/Classic/YamlWatcher.cs b/OpenUtau.Core/Classic/YamlWatcher.cs new file mode 100644 index 000000000..870047867 --- /dev/null +++ b/OpenUtau.Core/Classic/YamlWatcher.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Serilog; + +namespace OpenUtau.Core { + public class YamlWatcher : IDisposable { + public bool Paused { get; set; } + + private FileSystemWatcher watcher; + private Action reloadCallback; + + public YamlWatcher(string path, Action reloadCallback) { + this.reloadCallback = reloadCallback; + + watcher = new FileSystemWatcher(path); + watcher.Changed += OnFileChanged; + watcher.Created += OnFileChanged; + watcher.Deleted += OnFileChanged; + watcher.Renamed += OnFileChanged; + watcher.Error += OnError; + + // Filters specifically for .yaml. + watcher.Filter = "*.yaml"; + watcher.IncludeSubdirectories = true; + watcher.EnableRaisingEvents = true; + } + + private void OnFileChanged(object sender, FileSystemEventArgs e) { + if (Paused) { + return; + } + Log.Information($"YAML File \"{e.FullPath}\" {e.ChangeType}"); + + // Execute the refresh logic passed in during initialization + reloadCallback?.Invoke(); + } + + private void OnError(object sender, ErrorEventArgs e) { + Log.Error($"YAML Watcher error {e}"); + } + + public void Dispose() { + if (watcher != null) { + watcher.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs index 9d30ea192..a9c8dc0b4 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs @@ -27,6 +27,10 @@ public abstract class DiffSingerBasePhonemizer : MachineLearningPhonemizer IG2p g2p; Dictionary phonemeTokens; DiffSingerSpeakerEmbedManager speakerEmbedManager; + private static int globalDsGeneration = 0; + private int localDsGeneration = 0; + private static YamlWatcher dsWatcher; + private static string currentlyWatchedDsDir; string defaultPause = "SP"; protected virtual string GetDictionaryName()=>"dsdict.yaml"; @@ -35,9 +39,11 @@ public abstract class DiffSingerBasePhonemizer : MachineLearningPhonemizer private bool _singerLoaded; public override void SetSinger(USinger singer) { - if (_singerLoaded && singer == this.singer) return; + if (_singerLoaded && singer == this.singer && localDsGeneration == globalDsGeneration) return; try { + localDsGeneration = globalDsGeneration; _singerLoaded = _executeSetSinger(singer); + SetupYamlWatcher(rootPath); } catch { _singerLoaded = false; throw; @@ -103,6 +109,32 @@ private bool _executeSetSinger(USinger singer) { return true; } + private void SetupYamlWatcher(string directory) { + if (string.IsNullOrEmpty(directory) || currentlyWatchedDsDir == directory) { + return; + } + + if (dsWatcher != null) { + dsWatcher.Dispose(); + dsWatcher = null; + } + + currentlyWatchedDsDir = directory; + + if (Directory.Exists(directory)) { + dsWatcher = new YamlWatcher(directory, () => { + Log.Information($"[DiffSingerBasePhonemizer] Detected YAML change in {directory}. Reloading globally..."); + System.Threading.Thread.Sleep(200); + globalDsGeneration++; + + // Signal OpenUtau to re-run the timeline runner + if (this.singer != null) { + OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); + } + }); + } + } + protected virtual IG2p LoadG2p(string rootPath, bool useLangId = false) { //Each phonemizer has a delicated dictionary name, such as dsdict-en.yaml, dsdict-ru.yaml. //If this dictionary exists, load it. diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 2ea0f4271..b8db43de1 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -202,7 +202,9 @@ protected override IG2p LoadBaseDictionary() { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer != null && this.singer.Loaded) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { + this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; consExceptions.Clear(); if (stop != null) consExceptions.AddRange(stop); diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index b308e6bba..630e2ea7e 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -212,7 +212,9 @@ protected override IG2p LoadBaseDictionary() { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer != null && this.singer.Loaded) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { + this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; string file = Path.Combine(this.singer.Location, YamlFileName); if (!File.Exists(file)) { diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index 96171865e..1c842b5e4 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -182,7 +182,9 @@ protected override IG2p LoadBaseDictionary() { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer != null && this.singer.Loaded) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { + this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; consExceptions.Clear(); if (stop != null) consExceptions.AddRange(stop); diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index c576430f0..27934ff12 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using static OpenUtau.Api.Phonemizer; using System.Collections; +using OpenUtau.Core; namespace OpenUtau.Plugin.Builtin { /// @@ -256,8 +257,9 @@ private Result HandleError() { } public override void SetSinger(USinger singer) { - if (this.singer != singer) { + if (this.singer != singer || this.localYamlGeneration != globalYamlGeneration) { this.singer = singer; + this.localYamlGeneration = globalYamlGeneration; if (this.singer == null || !this.singer.Loaded) { return; @@ -291,6 +293,8 @@ public override void SetSinger(USinger singer) { file = Path.Combine(PluginDir, YamlFileName); } + SetupYamlWatcher(file); + if (!string.IsNullOrEmpty(file)) { bool shouldWriteTemplate = false; bool shouldBackupOldFile = false; @@ -412,6 +416,10 @@ public override void SetSinger(USinger singer) { protected IG2p dictionary => dictionaries[GetType()]; protected bool isDictionaryLoading => dictionaries[GetType()] == null; protected double TransitionBasicLengthMs => 100; + public static YamlWatcher yamlWatcher; + public static string currentlyWatchedYaml; + public static int globalYamlGeneration = 0; + public int localYamlGeneration = 0; private Dictionary dictionaries = new Dictionary(); private const string FORCED_ALIAS_SYMBOL = "?"; @@ -745,6 +753,43 @@ protected double GetTransitionBasicLengthMsByConstant() { return TransitionBasicLengthMs * GetTempoNoteLengthFactor(); } + private void SetupYamlWatcher(string yamlFilePath) { + if (string.IsNullOrEmpty(yamlFilePath) || currentlyWatchedYaml == yamlFilePath) { + return; + } + + if (yamlWatcher != null) { + yamlWatcher.Dispose(); + yamlWatcher = null; + } + + currentlyWatchedYaml = yamlFilePath; + string directory = Path.GetDirectoryName(yamlFilePath); + + if (Directory.Exists(directory)) { + yamlWatcher = new YamlWatcher(directory, () => { + Log.Information($"[SyllableBasedPhonemizer] Detected change in {YamlFileName}, incrementing generation..."); + + System.Threading.Thread.Sleep(200); + + lock (dictionaries) { + if (dictionaries.ContainsKey(this.GetType())) { + dictionaries.Remove(this.GetType()); + } + } + + backupVowels = null; + backupConsonants = null; + backupDictionaryReplacements = null; + globalYamlGeneration++; + + if (this.singer != null) { + OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); + } + }); + } + } + /// /// a note length modifier, from 1 to 0.3. Used to make transition notes shorter on high tempo /// From 43975efba179ea51ee7fedef7c1b1569fb0db6c1 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Fri, 22 May 2026 18:05:49 +0800 Subject: [PATCH 2/4] Add Presamp Watcher --- OpenUtau.Core/Classic/PresampWatcher.cs | 45 +++++++++++++++++++ .../JapanesePresampPhonemizer.cs | 43 ++++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 OpenUtau.Core/Classic/PresampWatcher.cs diff --git a/OpenUtau.Core/Classic/PresampWatcher.cs b/OpenUtau.Core/Classic/PresampWatcher.cs new file mode 100644 index 000000000..9ca54ac68 --- /dev/null +++ b/OpenUtau.Core/Classic/PresampWatcher.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using Serilog; + +namespace OpenUtau.Core { + public class PresampWatcher : IDisposable { + public bool Paused { get; set; } + + private FileSystemWatcher watcher; + private Action reloadCallback; + + public PresampWatcher(string path, Action reloadCallback) { + this.reloadCallback = reloadCallback; + + watcher = new FileSystemWatcher(path); + watcher.Changed += OnFileChanged; + watcher.Created += OnFileChanged; + watcher.Deleted += OnFileChanged; + watcher.Renamed += OnFileChanged; + watcher.Error += OnError; + + watcher.Filter = "presamp.ini"; + // Set to false since presamp.ini is always at the root of the voicebank + watcher.IncludeSubdirectories = false; + watcher.EnableRaisingEvents = true; + } + + private void OnFileChanged(object sender, FileSystemEventArgs e) { + if (Paused) { + return; + } + Log.Information($"Presamp File \"{e.FullPath}\" {e.ChangeType}"); + reloadCallback?.Invoke(); + } + + private void OnError(object sender, ErrorEventArgs e) { + Log.Error($"Presamp Watcher error {e}"); + } + public void Dispose() { + if (watcher != null) { + watcher.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs index fceed12e9..6103ac137 100644 --- a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Classic; using OpenUtau.Api; +using OpenUtau.Core; using OpenUtau.Core.Ustx; //using Serilog; @@ -19,7 +21,12 @@ public class JapanesePresampPhonemizer : Phonemizer { // Partial supporting: [NUM][APPEND][PITCH] -> Using to exclude useless characters in lyrics private USinger singer; - private Presamp presamp; + private static Presamp sharedPresamp; + private Presamp presamp => sharedPresamp; + private static int globalPresampGeneration = 0; + private int localPresampGeneration = 0; + private static PresampWatcher presampWatcher; + private static string currentlyWatchedPresampDir; private UProject project; private UTrack track; @@ -45,16 +52,44 @@ public override void SetUp(Note[][] groups, UProject project, UTrack track) { } public override void SetSinger(USinger singer) { - if (this.singer == singer) { + if (this.singer == singer && this.localPresampGeneration == globalPresampGeneration) { return; } + this.singer = singer; if (this.singer == null) { return; } - presamp = new Presamp(); - presamp.ReadPresampIni(singer.Location, singer.TextFileEncoding); + if (sharedPresamp == null) { + sharedPresamp = new Presamp(); + sharedPresamp.ReadPresampIni(singer.Location, singer.TextFileEncoding); + } + + this.localPresampGeneration = globalPresampGeneration; + SetupPresampWatcher(singer.Location); + } + + private void SetupPresampWatcher(string directory) { + if (string.IsNullOrEmpty(directory) || currentlyWatchedPresampDir == directory) { + return; + } + if (presampWatcher != null) { + presampWatcher.Dispose(); + presampWatcher = null; + } + currentlyWatchedPresampDir = directory; + if (Directory.Exists(directory)) { + // Using the custom PresampWatcher class you just made! + presampWatcher = new PresampWatcher(directory, () => { + System.Threading.Thread.Sleep(200); + sharedPresamp = null; + globalPresampGeneration++; + if (this.singer != null) { + OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); + } + }); + } } public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { From 9db5baed0ab625eaf466b41ccde72db387ba91f6 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Fri, 22 May 2026 18:23:01 +0800 Subject: [PATCH 3/4] fix --- .../JapanesePresampPhonemizer.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs index 6103ac137..e6d78768b 100644 --- a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs @@ -21,14 +21,14 @@ public class JapanesePresampPhonemizer : Phonemizer { // Partial supporting: [NUM][APPEND][PITCH] -> Using to exclude useless characters in lyrics private USinger singer; - private static Presamp sharedPresamp; - private Presamp presamp => sharedPresamp; + private Presamp presamp; + private UProject project; + private UTrack track; + private static int globalPresampGeneration = 0; private int localPresampGeneration = 0; private static PresampWatcher presampWatcher; private static string currentlyWatchedPresampDir; - private UProject project; - private UTrack track; // in case voicebank is missing certain symbols static readonly string[] substitution = new string[] { @@ -52,21 +52,21 @@ public override void SetUp(Note[][] groups, UProject project, UTrack track) { } public override void SetSinger(USinger singer) { - if (this.singer == singer && this.localPresampGeneration == globalPresampGeneration) { + bool generationChanged = this.localPresampGeneration != globalPresampGeneration; + + if (this.singer == singer && !generationChanged) { return; } - this.singer = singer; if (this.singer == null) { return; } + this.localPresampGeneration = globalPresampGeneration; - if (sharedPresamp == null) { - sharedPresamp = new Presamp(); - sharedPresamp.ReadPresampIni(singer.Location, singer.TextFileEncoding); + if (this.presamp == null || generationChanged) { + this.presamp = new Presamp(); + this.presamp.ReadPresampIni(singer.Location, singer.TextFileEncoding); } - - this.localPresampGeneration = globalPresampGeneration; SetupPresampWatcher(singer.Location); } @@ -80,10 +80,8 @@ private void SetupPresampWatcher(string directory) { } currentlyWatchedPresampDir = directory; if (Directory.Exists(directory)) { - // Using the custom PresampWatcher class you just made! presampWatcher = new PresampWatcher(directory, () => { System.Threading.Thread.Sleep(200); - sharedPresamp = null; globalPresampGeneration++; if (this.singer != null) { OpenUtau.Core.SingerManager.Inst.ScheduleReload(this.singer); From 89de24e107bebc1eb27558af5c05de29ef30cd05 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 30 May 2026 20:24:24 +0800 Subject: [PATCH 4/4] Fix yamlData --- OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 27934ff12..a9d3c890a 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -341,7 +341,7 @@ public override void SetSinger(USinger singer) { if (File.Exists(file)) { try { - var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); + var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)) ?? new YAMLData(); if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); @@ -349,7 +349,7 @@ public override void SetSinger(USinger singer) { var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); vowels = backupVowels.Concat(yamlVowels).Distinct().ToArray(); - tails = (tails ?? Array.Empty()).Concat(data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol) ?? Array.Empty()).Distinct().ToArray(); + tails = new string[] { "-", "R" }.Concat(data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol) ?? Array.Empty()).Distinct().ToArray(); fricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); aspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); @@ -388,7 +388,7 @@ public override void SetSinger(USinger singer) { } } } - + yamlFallbacks.Clear(); if (data?.fallbacks != null) { yamlFallbacks.Clear(); foreach (var df in data.fallbacks) {