diff --git a/OpenUtau.Core/G2p/Data/Resources.Designer.cs b/OpenUtau.Core/G2p/Data/Resources.Designer.cs index 605603c89..d5ea95929 100644 --- a/OpenUtau.Core/G2p/Data/Resources.Designer.cs +++ b/OpenUtau.Core/G2p/Data/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -189,5 +188,15 @@ internal static byte[] g2p_ru { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] g2p_uk { + get { + object obj = ResourceManager.GetObject("g2p-uk", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/OpenUtau.Core/G2p/Data/Resources.resx b/OpenUtau.Core/G2p/Data/Resources.resx index 062f69fb8..88fdc71e5 100644 --- a/OpenUtau.Core/G2p/Data/Resources.resx +++ b/OpenUtau.Core/G2p/Data/Resources.resx @@ -157,4 +157,7 @@ g2p-arpabet-plus.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + g2p-uk.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/OpenUtau.Core/G2p/Data/g2p-uk.zip b/OpenUtau.Core/G2p/Data/g2p-uk.zip new file mode 100644 index 000000000..56dec4b0b Binary files /dev/null and b/OpenUtau.Core/G2p/Data/g2p-uk.zip differ diff --git a/OpenUtau.Core/G2p/UkrainianG2p.cs b/OpenUtau.Core/G2p/UkrainianG2p.cs new file mode 100644 index 000000000..f8b572931 --- /dev/null +++ b/OpenUtau.Core/G2p/UkrainianG2p.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.ML.OnnxRuntime; +using OpenUtau.Api; + +// The data for the G2P is sourced from https://github.com/CUNY-CL/wikipron/blob/master/data/scrape/tsv/ukr_cyrl_narrow.tsv and edited by phi_pea +// G2P was trained by FRANKENRECORDS + +namespace OpenUtau.Core.G2p { + public class UkrainianG2p : G2pPack { + private static readonly string[] graphemes = new string[] { + "", "", "", "", "\'", "-", "а", "б", "в", "г", "ґ", "д", "е", "є", "ж", "з", "и", "і", "ї", "й", "к", "л", "м", "н", "о", "п", "р", "с", "т", "у", "ф", "х", "ц", "ч", "ш", "щ", "ь", "ю", "я" + }; + + private static readonly string[] phonemes = new string[] { + "", "", "", "", "a", "b","bq","d","dq","dz","dzh","dzhq","dzq","e","f","fq","g","gq","h","hq","i","j","k","kq","l","lq","m","mq","n","nq","o","p","pq","r","rq","s","sh","shq","sq","t","tq","ts","tsh","tshq","tsq","u","v","vq","x","xq","y","z","zh","zhq", "zq" + }; + + private static object lockObj = new object(); + private static Dictionary graphemeIndexes; + private static IG2p dict; + private static InferenceSession session; + private static Dictionary predCache = new Dictionary(); + + public UkrainianG2p() { + lock (lockObj) { + if (graphemeIndexes == null) { + graphemeIndexes = graphemes + .Skip(4) + .Select((g, i) => Tuple.Create(g, i)) + .ToDictionary(t => t.Item1, t => t.Item2 + 4); + var tuple = LoadPack( + Data.Resources.g2p_uk, + s => s.ToLowerInvariant()); + dict = tuple.Item1; + session = tuple.Item2; + } + } + GraphemeIndexes = graphemeIndexes; + Phonemes = phonemes; + Dict = dict; + Session = session; + PredCache = predCache; + } + } +} diff --git a/OpenUtau.Plugin.Builtin/UkrainianCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/UkrainianCVCPhonemizer.cs new file mode 100644 index 000000000..d16ddec94 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/UkrainianCVCPhonemizer.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenUtau.Api; +using OpenUtau.Core.G2p; + +namespace OpenUtau.Plugin.Builtin { + [Phonemizer("Ukrainian CVC Phonemizer", "UK CVC", "phi_pea", language:"UK")] + // contributed by phi_pea. special thanks to FRANKENRECORDS (who also trained the G2P this phonemizer is based on) and Layt_Desu! + + public class UkrainianCVCPhonemizer : SyllableBasedPhonemizer { + private readonly string[] vowels = "a,i,u,e,o,y".Split(","); + private readonly string[] consonants = "b,bq,d,dq,dz,dzh,dzhq,dzq,f,fq,g,gq,h,hq,j,k,kq,l,lq,m,mq,n,nq,p,pq,r,rq,s,sh,shq,sq,t,tq,ts,tsh,tshq,tsq,v,vq,x,xq,z,zh,zhq,zq".Split(","); + + private readonly string[] shortConsonants = "b,b\',d,d\',dz,dz\',dZ,dZ\',g,g\',k,k\',p,p\',t,t\',ts,ts\',tS,tS\'".Split(","); + private readonly string[] longConsonants = "f,f\',h,h\',l,l\',m,m\',n,n\',r,r\',s,s\',S,S\',v,v\',x,x\',z,z\',Z,Z\',j".Split(","); + + private readonly Dictionary dictionaryReplacements = ("a=a;i=i;u=u;e=e;o=o;y=y" + + "b=b;bq=b\';d=d;dq=d\';dz=dz;dzh=dZ;dzhq=dZ\';dzq=dz\';f=f;fq=f\';g=g;gq=g\';h=h;hq=h\';j=j;k=k;kq=k\';l=l;lq=l\';m=m;mq=m\';n=n;nq=n\';p=p;pq=p\';r=r;rq=r\';s=s;sh=S;shq=S\';sq=s\';t=t;tq=t\';ts=ts;tsh=tS;tshq=tS\';tsq=ts\';v=v;vq=v\';x=x;xq=x\';z=z;zh=Z;zhq=Z\';zq=z\'").Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + protected override string[] GetVowels() => vowels; + protected override string[] GetConsonants() => consonants; + protected override string GetDictionaryName() => "dict_uk.txt"; + protected override IG2p LoadBaseDictionary() => new UkrainianG2p(); + protected override Dictionary GetDictionaryPhonemesReplacement() => dictionaryReplacements; + protected override List ProcessSyllable(Syllable syllable) { + string prevV = syllable.prevV; + string[] cc = syllable.cc; + string v = syllable.v; + + string? basePhoneme = null; + var phonemes = new List(); + // ----- Starting V ----- // + // if no [- V] try [-V], if still not it - [V] + if (syllable.IsStartingV) { + basePhoneme = $"- {v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = $"-{v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = v; + } + } + } + // ----- VV transitions ----- // + // if no [V V], try in order: [VV], [_V], [* V], [*V], [V], otherwise extend the previous alias + else if (syllable.IsVV) { + if (!CanMakeAliasExtension(syllable)) { + basePhoneme = $"{prevV} {v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = $"{prevV}{v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = $"_{v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = $"* {v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = $"*{v}"; + if (!HasOto(basePhoneme, syllable.vowelTone)) { + basePhoneme = v; + } + } + } + } + } + } else { + basePhoneme = null; + } + } + // ----- starting CVs ----- // + else if (syllable.IsStartingCV) { + // ----- one-letter CVs ----- // + // first try [- CV], then [-CV]. if neither work - [CV] & try adding [- C] or [-C] before it + if (syllable.IsStartingCVWithOneConsonant) { + basePhoneme = $"- {cc.Last()}{v}"; + if (!HasOto(basePhoneme, syllable.tone)) { + basePhoneme = $"-{cc.Last()}{v}"; + if (!HasOto(basePhoneme, syllable.tone)) { + basePhoneme = $"{cc.Last()}{v}"; + var startingC = cc[0]; + if (v == "i" && cc.Last() != "j") { + startingC = $"{startingC}'"; + } + TryAddPhoneme(phonemes, syllable.tone, $"- {startingC}", $"-{startingC}"); + } + } + // ----- oh boy CCVs ----- // + // (bro i straight up have no idea what i'm doing here) + // but tldr for now it adds each C separately, first adds [- C]/[-C]/[C] fot the first consonant of the cluster, then [C -]/[C-]/[C] fot the following ones. + } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { + basePhoneme = $"{cc.Last()}{v}"; + + TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", $"-{cc[0]}", $"{cc[0]}"); + + for (var i = longConsonants.Contains(cc[0]) ? 1 : 0; i < cc.Length - 1; i++) { + string? startingC = $"{cc[i]} -"; + if (!HasOto(startingC, syllable.tone)) { + startingC = $"{cc[i]}-"; + if (!HasOto(startingC, syllable.tone)) { + startingC = $"{cc[i]}"; + } + } + phonemes.Add(startingC); + } + } + } + // ----- VCV, as it's called here ----- // + // + else { + // ----- one consonant ----- // + // first add [CV], then [V C]. if doesn't work, try [VC], if still not, no transition + if (syllable.IsVCVWithOneConsonant) { + basePhoneme = $"{cc.Last()}{v}"; + if (cc.Last().Contains('\'') && v == "i" && !HasOto(basePhoneme, syllable.tone)) { + basePhoneme = basePhoneme.Replace("'", ""); + } + + var vc = $"{prevV} {cc.Last()}"; + if ((v == "i" && !cc.Last().Contains('\'')) && cc.Last() != "j") { + vc = $"{prevV} {cc.Last()}'"; + } + if (!HasOto(vc, syllable.tone)) { + vc = $"{prevV}{cc.Last()}"; + if (v == "i" && cc.Last() != "j") { + vc = $"{prevV}{cc.Last()}'"; + } + if (!HasOto(vc, syllable.tone)) { + vc = null; + } + } + phonemes.Add(vc); + // ----- CCs (oh boy) ----- // + // first checks whether a [V C] or a [VC] transition is available for the first consonant of the cluster + // if none are present, don't add one at all, and if it's a short consonant, add the [C -]/[C-]/[C] for it + // if the first consonant was long, skip adding it as the previous step already handled it, then just add + // the rest of the consonants as [C -]s/[C-]s/[C]s, regardless of type + } else { + basePhoneme = $"{cc.Last()}{v}"; + if (cc.Last().Contains('\'') && v == "i" && !HasOto(basePhoneme, syllable.tone)) { + basePhoneme = basePhoneme.Replace("'", ""); + } + + string? vc = $"{prevV} {cc[0]}"; + if (!HasOto(vc, syllable.tone)) { + vc = $"{prevV}{cc[0]}"; + if (!HasOto(vc, syllable.tone) && longConsonants.Contains(cc[0])) { + vc = null; + TryAddPhoneme(phonemes, syllable.tone, $"{cc[0]} -", $"{cc[0]}-", $"{cc[0]}"); + } else if (!HasOto(vc, syllable.tone) && shortConsonants.Contains(cc[0])) { + vc = null; + } + } + phonemes.Add(vc); + for (var i = longConsonants.Contains(cc[0]) ? 1 : 0; i < cc.Length - 1; i++) { + TryAddPhoneme(phonemes, syllable.tone, $"{cc[i]} -", $"{cc[i]}-", $"{cc[i]}"); + } + } + + } + phonemes.Add(basePhoneme); + return phonemes; + } + + protected override List ProcessEnding(Ending ending) { + string[] cc = ending.cc; + string v = ending.prevV; + + string? endPhoneme = null; + var phonemes = new List(); + + // ----- Ending Vs ----- // + // try [V -], then [V-]. if none work, don't add an ending V + if (ending.IsEndingV) { + endPhoneme = $"{v} -"; + if (!HasOto(endPhoneme, ending.tone)) { + endPhoneme = $"{v}-"; + if (!HasOto(endPhoneme, ending.tone)) { + endPhoneme = null; + } + } + // ----- ending VCs ----- // + } else { + // ----- one consonant ----- // + // first try [VC -], then [C V]/[VC] + [C -]/[C-]/[C] + if (ending.IsEndingVCWithOneConsonant) { + endPhoneme = $"{v}{cc[0]} -"; + if (!HasOto(endPhoneme, ending.tone)) { + endPhoneme = $"{cc[0]} -"; + if (!HasOto(endPhoneme, ending.tone)) { + endPhoneme = $"{cc[0]}-"; + if (!HasOto(endPhoneme, ending.tone)) { + endPhoneme = $"{cc[0]}"; + } + } + //phonemes.Add($"{v} {cc[0]}"); + TryAddPhoneme(phonemes, ending.tone, $"{v} {cc[0]}", $"{v}{cc[0]}"); + } + // ----- VCCs (oh boy...) ----- // + // similar logic to VCVs with multiple consonants + } else { + endPhoneme = $"{cc.Last()} -"; + string? vc = $"{v} {cc[0]}"; + if (!HasOto(vc, ending.tone) && longConsonants.Contains(cc[0])) { + vc = null; + TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} -", $"{cc[0]}-", $"{cc[0]}"); + } + phonemes.Add(vc); + for (var i = longConsonants.Contains(cc.Last()) ? 0 : 1; i < cc.Length - 1; i++) { + TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} -", $"{cc[i]}-", $"{cc[i]}"); + } + } + } + + phonemes.Add(endPhoneme); + return phonemes; + } + + protected override double GetTransitionBasicLengthMs(string alias = "") { + foreach (var c in shortConsonants) { + if (alias.EndsWith(c)) { + return base.GetTransitionBasicLengthMs(); + } + } + foreach (var c in longConsonants) { + if (alias.EndsWith(c)) { + return base.GetTransitionBasicLengthMs() * 2; + } + } + return base.GetTransitionBasicLengthMs(); + } + } + + +}