-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDiffSingerModels.cs
More file actions
297 lines (268 loc) · 15.1 KB
/
Copy pathDiffSingerModels.cs
File metadata and controls
297 lines (268 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.ML.OnnxRuntime;
using TuneLab.Foundation;
using TuneLab.SDK;
using YamlDotNet.Serialization;
namespace DiffSingerForTuneLab;
// 引擎级模型缓存:按 voiceId 缓存声学模型束、按声码器名共享声码器会话。
// · 懒加载(首次合成时按需载入)、加锁串行化首载;载入后多会话共享同一会话,但 DirectML EP 的 Run
// 是设备级不可并发(跨会话亦然),故所有推理 Run 进程级全局串行(见 InProcessModelSession);
// · 执行设备是进程级同质承诺、不做进程内 DML→CPU 回退:onnxruntime-DirectML 单包内,同进程一旦碰过 DML EP,
// 再建 CPU 会话(或反之)会触发原生 AccessViolation 直接崩进程。故 directml 失败即抛异常上冒、绝不就地建 CPU 会话;
// 用户需改设置为 CPU 并重启(新进程从未碰 DML、纯 CPU 安全)。彻底隔离/自愈留给后续 MLRuntime.exe 子进程方案;
// · provider 变更 → 引擎弃旧缓存建新缓存(旧缓存 Dispose 释放原生会话)。
// 原生 onnxruntime/DirectML 库随包进本插件 ALC(见 csproj 注释);首次构造 InferenceSession 即触发原生加载,
// 失败抛异常由宿主在调用边界 catch 标插件失败(§5.10)。
public sealed class DiffSingerModelCache : IDisposable
{
readonly string mProvider;
readonly ILogger mLogger;
readonly object mLock = new();
readonly Dictionary<string, VoiceModels> mVoices = new(StringComparer.Ordinal);
readonly Dictionary<string, (IModelSession Session, ulong Hash)> mVocoders = new(StringComparer.Ordinal);
// MLRuntime 路由:推理模式由设置决定(默认 subprocess——隔离子进程跑 ONNX,原生崩溃不拖垮宿主):
// · "subprocess"(默认)→ PipeTransport:spawn mlruntime/MLRuntime.exe;启动探测失败(杀软/权限)则退回进程内。
// · "inprocess" → mRuntimeClient=null,走 InProcessModelSession(进程内直跑,逃生模式)。
// · env DIFFSINGER_RUNTIME_LOOPBACK=1(dev)→ LoopbackTransport:进程内 host、走完整 IPC 编解码链(测试用)。
// 非 null 的 mRuntimeClient 令 LoadSession 返回 RemoteModelSession。
readonly RuntimeClient? mRuntimeClient;
public DiffSingerModelCache(string provider, string runtimeMode, ILogger logger)
{
mProvider = provider;
mLogger = logger;
if (Environment.GetEnvironmentVariable("DIFFSINGER_RUNTIME_LOOPBACK") == "1")
{
mRuntimeClient = new RuntimeClient(provider, p => new LoopbackTransport(new RuntimeHost(p)), canRespawn: false);
mLogger.Info("DiffSinger:MLRuntime loopback 模式(dev)");
}
else if (runtimeMode != "inprocess") // 默认 subprocess
{
mRuntimeClient = TryCreateSubprocessClient(provider);
}
}
// 建子进程客户端:先立即 spawn+连一个传输作启动探测——成功则以它为初始传输建弹性 client;
// 失败(exe 缺失/杀软拦截/权限/管道建不起)则记警告、返回 null 退回进程内(这是「起不来」非「崩」,退回救场)。
RuntimeClient? TryCreateSubprocessClient(string provider)
{
var dir = Path.GetDirectoryName(typeof(DiffSingerModelCache).Assembly.Location)!;
var exe = Path.Combine(dir, "mlruntime", "MLRuntime.exe");
Action<string> exeLog = line => mLogger.Info($"[MLRuntime] {line}"); // 子进程 stdout/stderr 转发
Func<string, IRuntimeTransport> factory = p => new PipeTransport(exe, p, exeLog);
try
{
var probe = factory(provider); // 立即 spawn+连,探测子进程能否起来
mLogger.Info($"DiffSinger:MLRuntime 子进程模式({exe})");
return new RuntimeClient(provider, factory, canRespawn: true, line => mLogger.Info(line), probe);
}
catch (Exception ex)
{
mLogger.Warning($"DiffSinger:MLRuntime 子进程启动失败({ex.Message}),本次退回进程内。可在插件设置的「推理模式」调整,或检查杀软/权限。");
return null;
}
}
// 默认声码器根:插件用户数据根下的 Vocoders 目录(与声库目录彼此独立,仅默认值恰好同在用户数据根下);声学 dsconfig 的 vocoder 字段即子目录名。
// 作为可配置列表的默认播种项——用户可在设置里追加其他目录(如已有的声码器收藏位置),免去复制。
public static string VocodersDirectory => Path.Combine(DiffSingerDeclarations.UserDataRoot, "Vocoders");
// 声码器搜索根(默认 + 用户追加,由引擎每次取会话前刷入);按序取首个含 <vocoderName>/vocoder.yaml 的目录。
// 引用整体赋值原子、读取取一次快照即可;默认仅含固定目录,引擎未设置时行为与旧版一致。
public IReadOnlyList<string> VocoderRoots { get; set; } = new[] { VocodersDirectory };
// 按物理模型包目录(config.RootPath)缓存——同一 voice 选不同 model/version = 不同物理包 = 各自缓存、不重复加载。
public VoiceModels GetOrLoad(VoicebankConfig config)
{
lock (mLock)
{
if (mVoices.TryGetValue(config.RootPath, out var cached))
return cached;
var acousticPath = Path.Combine(config.RootPath, config.AcousticFileName);
var acoustic = LoadSession(acousticPath);
var acousticHash = DiffSingerTensorCache.HashFile(acousticPath);
var (vocoder, vocoderHash) = GetOrLoadVocoder(config.VocoderName);
// 预测器(dsdur/dspitch/dsvariance)懒加载共用本缓存的会话加载器(含 DML→CPU 回退)。
var models = new VoiceModels(config, acoustic, acousticHash, vocoder, vocoderHash, LoadSession, mLogger);
mVoices[config.RootPath] = models;
return models;
}
}
(IModelSession Session, ulong Hash) GetOrLoadVocoder(string vocoderName)
{
if (mVocoders.TryGetValue(vocoderName, out var cached))
return cached;
var dir = ResolveVocoderDir(vocoderName);
if (dir is null)
throw new InvalidOperationException(
$"未找到声码器 {vocoderName}:搜索的声码器目录下均无 {vocoderName}/vocoder.yaml。" +
$"(已搜索:{string.Join(" ; ", VocoderRoots)})请把声码器放入其中之一,或在插件设置的「声码器目录」里追加其所在目录。");
var yaml = new DeserializerBuilder().Build()
.Deserialize<Dictionary<string, object?>>(File.ReadAllText(Path.Combine(dir, "vocoder.yaml")));
var modelFile = yaml.TryGetValue("model", out var m) ? m?.ToString() : null;
if (string.IsNullOrEmpty(modelFile))
throw new InvalidOperationException($"声码器 {vocoderName} 的 vocoder.yaml 缺少 model 字段");
var modelPath = Path.Combine(dir, modelFile);
var entry = (LoadSession(modelPath), DiffSingerTensorCache.HashFile(modelPath));
mVocoders[vocoderName] = entry;
return entry;
}
// 在 VocoderRoots 里按序找到首个含 <vocoderName>/vocoder.yaml 的目录;无则 null。
string? ResolveVocoderDir(string vocoderName)
{
foreach (var root in VocoderRoots)
{
if (string.IsNullOrWhiteSpace(root))
continue;
var dir = Path.Combine(root, vocoderName);
if (File.Exists(Path.Combine(dir, "vocoder.yaml")))
return dir;
}
return null;
}
// 执行设备同质承诺,不做进程内回退(见类注释的 AccessViolation 约束):
// · cpu 设置:全程只建 CPU 会话,绝不碰 DML EP;
// · directml 设置:建 DML 会话,失败即抛可读异常上冒(宿主按 §5.10 标合成失败),绝不就地回退 CPU。
IModelSession LoadSession(string modelPath)
{
var fileName = Path.GetFileName(modelPath);
if (mRuntimeClient != null) // loopback 验证模式:会话在 host 侧、经 IPC 派发
{
var remote = RemoteModelSession.Load(mRuntimeClient, modelPath);
mLogger.Info($"DiffSinger:加载 {fileName} · MLRuntime({mProvider})");
return remote;
}
if (mProvider == "cpu")
{
var cpu = new InferenceSession(modelPath);
mLogger.Info($"DiffSinger:加载 {fileName} · CPU");
return new InProcessModelSession(cpu);
}
try
{
var options = new SessionOptions();
options.AppendExecutionProvider_DML(0);
var session = new InferenceSession(modelPath, options);
mLogger.Info($"DiffSinger:加载 {fileName} · DirectML");
return new InProcessModelSession(session);
}
catch (Exception ex)
{
// 全量异常入日志(含类型/HResult/inner/栈),供定位 DML 失败根因——是算子不支持还是设备层起不来。
mLogger.Error($"DiffSinger:DirectML 加载 {fileName} 失败:{ex}");
throw new InvalidOperationException(
$"DirectML 加载 {fileName} 失败,无法在同进程内安全回退 CPU。" +
$"请在插件设置里把「执行设备」改为 CPU 并重启 TuneLab。原始错误:{ex.Message}", ex);
}
}
public void Dispose()
{
lock (mLock)
{
foreach (var v in mVoices.Values)
v.Dispose(); // 释放声学 + 懒加载的预测器(声码器为缓存共享、单独释放)
// 声码器会话(会话自身在设备级锁内退役并释放,杜绝与在飞 Run 并发释放)。
foreach (var entry in mVocoders.Values)
entry.Session.Dispose();
mVoices.Clear();
mVocoders.Clear();
// loopback 模式:真实会话在 host 侧,释放 client 连带释放 host 及其所有会话(RemoteModelSession.Dispose 为空操作)。
mRuntimeClient?.Dispose();
}
}
}
// 一个声库的已加载模型束(声学私有、声码器为缓存共享引用)+ 推理所需的音素/语言表与说话人嵌入。
public sealed class VoiceModels : IDisposable
{
readonly VoicebankConfig mConfig;
readonly ILogger mLogger;
readonly Func<string, IModelSession> mLoad;
readonly Dictionary<string, int> mPhonemes;
readonly Dictionary<string, int> mLanguages;
readonly Dictionary<string, float[]> mEmbCache = new(StringComparer.Ordinal);
readonly object mEmbLock = new();
// 预测器懒加载缓存(键 = 子目录名);缺失或加载失败记 null(不重试)。
readonly Dictionary<string, DiffSingerPredictor?> mPredictors = new(StringComparer.Ordinal);
readonly object mPredictorLock = new();
public IModelSession Acoustic { get; }
public IModelSession Vocoder { get; }
// 张量缓存 identifier(模型 .onnx 文件内容哈希,加载时算一次)。
public ulong AcousticHash { get; }
public ulong VocoderHash { get; }
public int HiddenSize => mConfig.HiddenSize;
public int HopSize => mConfig.HopSize;
public int SampleRate => mConfig.SampleRate;
public int NumMelBins => mConfig.NumMelBins;
public double MaxDepth => mConfig.MaxDepth;
public IReadOnlyList<string> Speakers => mConfig.Speakers;
public VoiceModels(VoicebankConfig config, IModelSession acoustic, ulong acousticHash,
IModelSession vocoder, ulong vocoderHash, Func<string, IModelSession> load, ILogger logger)
{
mConfig = config;
mLogger = logger;
mLoad = load;
Acoustic = acoustic;
Vocoder = vocoder;
AcousticHash = acousticHash;
VocoderHash = vocoderHash;
var yaml = new DeserializerBuilder().Build(); // JSON ⊂ YAML:音素/语言表(值为 int id)
mPhonemes = yaml.Deserialize<Dictionary<string, int>>(
File.ReadAllText(Path.Combine(config.RootPath, config.PhonemesFileName)));
mLanguages = string.IsNullOrEmpty(config.LanguagesFileName)
? new Dictionary<string, int>()
: yaml.Deserialize<Dictionary<string, int>>(
File.ReadAllText(Path.Combine(config.RootPath, config.LanguagesFileName)));
}
public bool TryGetPhoneme(string symbol, out int id) => mPhonemes.TryGetValue(symbol, out id);
public bool TryGetLanguage(string lang, out int id) => mLanguages.TryGetValue(lang, out id);
// 预测器懒加载(子目录如 "dsvariance" / "dspitch"):首用时按声库会话加载器构造;
// 子目录无 dsconfig 或加载失败返回 null(记忆化,不重试),调用方据此降级。
public DiffSingerPredictor? GetPredictor(string subdir)
{
lock (mPredictorLock)
{
if (mPredictors.TryGetValue(subdir, out var cached))
return cached;
DiffSingerPredictor? predictor = null;
var dir = Path.Combine(mConfig.RootPath, subdir);
if (File.Exists(Path.Combine(dir, "dsconfig.yaml")))
{
try { predictor = new DiffSingerPredictor(dir, mLoad); }
catch (Exception ex) { mLogger.Warning($"DiffSinger:加载预测器 {subdir} 失败:{ex.Message}"); }
}
mPredictors[subdir] = predictor;
return predictor;
}
}
// 说话人嵌入向量(.emb = HiddenSize 个 float32 LE):按 suffix 关联同后缀声学条目、无则回退首个;按 suffix 缓存。
// 忠实对齐 OpenUtau getSpeakerIndexBySuffix 的「按 suffix 解析、回退首个」;供 DiffSingerSpeakerMix 逐帧混合解析声学域 emb。
public float[] GetSpeakerEmbeddingBySuffix(string suffix)
{
lock (mEmbLock)
{
if (mEmbCache.TryGetValue(suffix, out var cached))
return cached;
string? match = mConfig.Speakers.FirstOrDefault(s => DiffSingerDeclarations.Suffix(s) == suffix)
?? (mConfig.Speakers.Count > 0 ? mConfig.Speakers[0] : null);
var emb = new float[mConfig.HiddenSize];
if (match != null)
{
var bytes = File.ReadAllBytes(Path.Combine(mConfig.RootPath, match + ".emb"));
for (int i = 0; i < emb.Length; i++)
emb[i] = BitConverter.ToSingle(bytes, i * 4);
}
mEmbCache[suffix] = emb;
return emb;
}
}
// 释放声学 + 懒加载的预测器;声码器为缓存共享引用,由缓存单独释放(不在此 Dispose)。
// 会话经退役机制在推理锁内释放(杜绝与在飞 Run 并发释放);预测器自退役其会话。
public void Dispose()
{
Acoustic.Dispose(); // 会话自身在设备级锁内退役并释放
lock (mPredictorLock)
{
foreach (var p in mPredictors.Values)
p?.Dispose();
mPredictors.Clear();
}
}
}