Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions OpenUtau.Core/DiffSinger/DiffSingerBasePhonemizer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -49,10 +49,11 @@ private bool _executeSetSinger(USinger singer) {
if (singer == null) {
throw new Exception("Singer is null.");
}
if(singer.Location == null){
throw new Exception("Singer location is null.");
if (File.Exists(Path.Join(singer.Location, "dsdur", "dsconfig.yaml"))) {
rootPath = Path.Combine(singer.Location, "dsdur");
} else {
rootPath = singer.Location;
}
rootPath = Path.Combine(singer.Location, "dsdur");
//Load Config
var configPath = Path.Join(rootPath, "dsconfig.yaml");
try {
Expand Down Expand Up @@ -207,9 +208,10 @@ string GetSpeakerAtIndex(Note note, int index) {
speaker = singer.Subbanks.FirstOrDefault();
}
if (speaker is null) {
throw new Exception(
$"No subbanks defined for singer \"{singer.Name}\". " +
"Please check the singer's configuration.");
if (dsConfig.speakers != null && dsConfig.speakers.Count > 0) {
return dsConfig.speakers[0];
}
return "";
}
return speaker.Suffix;
}
Expand Down
116 changes: 87 additions & 29 deletions OpenUtau.Core/DiffSinger/DiffSingerSpeakerEmbedManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public DiffSingerSpeakerEmbedManager(DsConfig dsConfig, string rootPath) {
this.dsConfig = dsConfig;
this.rootPath = rootPath;
}

public NDArray loadSpeakerEmbed(string speaker) {
string path = Path.Join(rootPath, speaker + ".emb");
if(File.Exists(path)) {
Expand Down Expand Up @@ -50,14 +51,16 @@ public NDArray getSpeakerEmbeds() {

public bool IsVoiceColorCurve(string abbr, out int subBankId) {
subBankId = 0;
if (abbr.StartsWith(VoiceColorHeader) && int.TryParse(abbr.Substring(2), out subBankId)) {;
if (abbr.StartsWith(VoiceColorHeader) && int.TryParse(abbr.Substring(2), out subBankId)) {
subBankId -= 1;
return true;
} else {
return false;
}
}

static readonly HashSet<string> warnedMissingSpeakerSuffixes = new();

public int getSpeakerIndexBySuffix(string suffix) {
var speakerIndex = dsConfig.speakers.IndexOf(suffix);
if (speakerIndex >= 0) {
Expand All @@ -72,69 +75,124 @@ public int getSpeakerIndexBySuffix(string suffix) {
if (speakerIndex >= 0) {
return speakerIndex;
}
if (dsConfig.speakers.Count == 0) {
if (dsConfig.speakers == null || dsConfig.speakers.Count == 0) {
throw new InvalidOperationException(
"Subbanks are defined in character.yaml but \"speakers\" is empty in dsconfig.yaml.");
}
Log.Warning(
$"Speaker suffix \"{suffix}\" not found in dsConfig.speakers, falling back to first speaker. " +
$"Candidates: {string.Join(',', dsConfig.speakers)}.");
var fallback = dsConfig.speakers[0];
var warnKey = $"{rootPath}|{suffix}|{fallback}";
lock (warnedMissingSpeakerSuffixes) {
if (warnedMissingSpeakerSuffixes.Add(warnKey)) {
Log.Warning(
"Speaker suffix \"{Suffix}\" not found in dsConfig.speakers ({Candidates}). Falling back to \"{Fallback}\".",
suffix,
string.Join(", ", dsConfig.speakers),
fallback);
}
}
return 0;
}

//used by phonemizer (duration model)
public Tensor<float> PhraseSpeakerEmbedByPhone(string[] speakerByPhone){
public Tensor<float> PhraseSpeakerEmbedByPhone(string[] speakerByPhone) {
var hiddenSize = dsConfig.hiddenSize;
var speakerEmbeds = getSpeakerEmbeds();
if (speakerEmbeds == null) {
return null;
}
int speakerCount = dsConfig.speakers.Count;
var speakerEmbedArrays = new float[speakerCount][];
for (int spk = 0; spk < speakerCount; spk++) {
speakerEmbedArrays[spk] = speakerEmbeds[":", spk].ToArray<float>();
}

var totalPhones = speakerByPhone.Length;
NDArray spkCurves = np.zeros<float>(totalPhones, dsConfig.speakers.Count);
foreach(int phoneId in Enumerable.Range(0,totalPhones)) {
var result = new float[totalPhones * hiddenSize];

for (int phoneId = 0; phoneId < totalPhones; phoneId++) {
var spkId = getSpeakerIndexBySuffix(speakerByPhone[phoneId]);
spkCurves[phoneId, spkId] = 1;
var embed = speakerEmbedArrays[spkId];
var dest = result.AsSpan(phoneId * hiddenSize, hiddenSize);
embed.AsSpan().CopyTo(dest);
}
var spkEmbedResult = np.dot(spkCurves, speakerEmbeds.T);
var spkEmbedTensor = new DenseTensor<float>(spkEmbedResult.ToArray<float>(),
new int[] { totalPhones, hiddenSize })

return new DenseTensor<float>(result, new int[] { totalPhones, hiddenSize })
.Reshape(new int[] { 1, totalPhones, hiddenSize });
return spkEmbedTensor;
}

//used by variance, pitch and acoustic
public Tensor<float> PhraseSpeakerEmbedByFrame(RenderPhrase phrase, IList<int> durations, float frameMs, int totalFrames, int headFrames, int tailFrames){
public Tensor<float> PhraseSpeakerEmbedByFrame(RenderPhrase phrase, IList<int> durations, float frameMs, int totalFrames, int headFrames, int tailFrames) {
var singer = phrase.singer;
var hiddenSize = dsConfig.hiddenSize;
var speakerEmbeds = getSpeakerEmbeds();
//get default speaker for each phoneme
if (speakerEmbeds == null) {
return null;
}
// Per-frame CLR / phoneme suffix is always weight 1.0 ("100%").
// Voice-color curves add on top; then weights are normalized to a convex mix.
// Example: CLR=A and cl_B=100% → A:B = 1:1 (not pure B).
var headDefaultSpk = getSpeakerIndexBySuffix(phrase.phones[0].suffix);
var tailDefaultSpk = getSpeakerIndexBySuffix(phrase.phones[^1].suffix);
var defaultSpkByFrame = Enumerable.Repeat(headDefaultSpk, headFrames).ToList();
defaultSpkByFrame.AddRange(Enumerable.Range(0, phrase.phones.Length)
.SelectMany(phIndex => Enumerable.Repeat(getSpeakerIndexBySuffix(phrase.phones[phIndex].suffix), durations[phIndex+1])));
.SelectMany(phIndex => Enumerable.Repeat(
getSpeakerIndexBySuffix(phrase.phones[phIndex].suffix),
durations[phIndex + 1])));
defaultSpkByFrame.AddRange(Enumerable.Repeat(tailDefaultSpk, tailFrames));
//get speaker curves
NDArray spkCurves = np.zeros<float>(totalFrames, dsConfig.speakers.Count);
foreach(var curve in phrase.curves) {
if(IsVoiceColorCurve(curve.Item1,out int subBankId) && subBankId < singer.Subbanks.Count) {
foreach (var curve in phrase.curves) {
if (IsVoiceColorCurve(curve.Item1, out int subBankId) && subBankId < singer.Subbanks.Count) {
var spkId = getSpeakerIndexBySuffix(singer.Subbanks[subBankId].Suffix);
spkCurves[":", spkId] += DiffSingerUtils.SampleCurve(phrase, curve.Item2, 0,
frameMs, totalFrames, headFrames, tailFrames, x => x * 0.01f)
.Select(f => (float)f).ToArray();
}
}
foreach(int frameId in Enumerable.Range(0,totalFrames)) {
//standarization
var spkSum = spkCurves[frameId, ":"].ToArray<float>().Sum();
if (spkSum > 1) {
spkCurves[frameId, ":"] /= spkSum;
} else {
spkCurves[frameId, defaultSpkByFrame[frameId]] += 1 - spkSum;

int speakerCount = dsConfig.speakers.Count;
var speakerEmbedArrays = new float[speakerCount][];
for (int spk = 0; spk < speakerCount; spk++) {
speakerEmbedArrays[spk] = speakerEmbeds[":", spk].ToArray<float>();
}

var result = new float[totalFrames * hiddenSize];
var weights = new float[speakerCount];
for (int frameId = 0; frameId < totalFrames; frameId++) {
Array.Clear(weights, 0, speakerCount);
int clrSpkId = defaultSpkByFrame[frameId];
weights[clrSpkId] = 1f;
for (int spk = 0; spk < speakerCount; spk++) {
weights[spk] += (float)spkCurves[frameId, spk];
}

float weightSum = 0f;
for (int spk = 0; spk < speakerCount; spk++) {
if (weights[spk] < 0f) {
weights[spk] = 0f;
}
weightSum += weights[spk];
}
if (weightSum < 1e-8f) {
weights[clrSpkId] = 1f;
weightSum = 1f;
}

var dest = result.AsSpan(frameId * hiddenSize, hiddenSize);
dest.Clear();
for (int spk = 0; spk < speakerCount; spk++) {
float w = weights[spk] / weightSum;
if (w < 1e-8f) {
continue;
}
var embed = speakerEmbedArrays[spk];
for (int j = 0; j < dest.Length; j++) {
dest[j] += w * embed[j];
}
}
}
var spkEmbedResult = np.dot(spkCurves, speakerEmbeds.T);
var spkEmbedTensor = new DenseTensor<float>(spkEmbedResult.ToArray<float>(),
new int[] { totalFrames, hiddenSize })
return new DenseTensor<float>(result, new int[] { totalFrames, hiddenSize })
.Reshape(new int[] { 1, totalFrames, hiddenSize });
return spkEmbedTensor;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Serilog;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
Expand Down
8 changes: 4 additions & 4 deletions OpenUtauMobile/Helpers/LocalizationManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand All @@ -21,7 +21,7 @@ public static class LocalizationManager
public const string FollowSystemLanguageCode = "system";

private const string ExplicitLanguageFallbackCode = "en";
private const string SystemLanguageFallbackCode = "zh-Hans";
private const string SystemLanguageFallbackCode = "en";

/// <summary>Supported languages (language code => display name).</summary>
public static readonly IReadOnlyList<(string Code, string DisplayName)> AvailableLanguages =
Expand Down Expand Up @@ -77,7 +77,7 @@ public static void LoadLanguage(string langCode)

/// <summary>
/// Resolves language preference to a supported resource language code.
/// Empty or "system" preference uses system UI language and falls back to zh-Hans.
/// Empty or "system" preference uses system UI language and falls back to en.
/// Explicit codes fall back to en when unsupported.
/// </summary>
public static string ResolveLanguagePreference(string? preferenceCode)
Expand All @@ -93,7 +93,7 @@ public static string ResolveLanguagePreference(string? preferenceCode)

/// <summary>
/// Resolves current system language to a supported resource language code.
/// Falls back to zh-Hans when system language is not in the supported list.
/// Falls back to en when system language is not in the supported list.
/// </summary>
public static string ResolveSystemLanguage()
{
Expand Down
57 changes: 56 additions & 1 deletion OpenUtauMobile/ViewModels/ClassicSingerSetupViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -99,6 +99,22 @@ public ClassicSingerSetupViewModel(MainViewModel navigator) : base(navigator)
VoicebankConfig? config = LoadCharacterYaml(ArchiveFilePath);
MissingInfo = config == null || string.IsNullOrEmpty(config.SingerType);

if (!MissingInfo)
{
// character.yaml has a SingerType — use it as default selection.
var declaredType = config!.SingerType;
if (SingerTypes.Contains(declaredType))
{
SingerType = declaredType;
}
}
else
{
// No SingerType in yaml (or no yaml at all).
// Mirror VoicebankLoader.LoadInfo heuristic: scan archive for config files.
SingerType = DetectSingerTypeFromArchive(ArchiveFilePath);
}

if (!string.IsNullOrEmpty(config?.TextFileEncoding))
{
try
Expand Down Expand Up @@ -291,6 +307,45 @@ private bool IsEncrypted(string archiveFilePath)
}
}

/// <summary>
/// Scans archive entries for dsconfig.yaml, enuconfig.yaml, or info.toml to infer singer type.
/// Mirrors the legacy detection heuristic in VoicebankLoader.LoadInfo and NeutrinoConfig.Load.
/// </summary>
private string DetectSingerTypeFromArchive(string archiveFilePath)
{
try
{
using (IArchive archive = ArchiveFactory.OpenArchive(archiveFilePath))
{
bool hasDsconfig = archive.Entries.Any(e =>
Path.GetFileName(e.Key) == "dsconfig.yaml");
if (hasDsconfig)
{
return "diffsinger";
}

bool hasEnuconfig = archive.Entries.Any(e =>
Path.GetFileName(e.Key) == "enuconfig.yaml");
if (hasEnuconfig)
{
return "enunu";
}

bool hasNeutrino = archive.Entries.Any(e =>
Path.GetFileName(e.Key) == "info.toml");
if (hasNeutrino)
{
return "neutrino";
}
}
}
catch
{
// Fall through to default
}
return "utau";
}

private async Task InstallAsync()
{
string archiveFilePath = ArchiveFilePath;
Expand Down