init
This commit is contained in:
41
csharp/.gitignore
vendored
Normal file
41
csharp/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# Build results
|
||||
bin/
|
||||
obj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
x86/
|
||||
[Aa]rm/
|
||||
[Aa]rm64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio files
|
||||
.vs/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.userprefs
|
||||
|
||||
# Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Output directory
|
||||
results/*.wav
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
118
csharp/ExampleONNX.cs
Normal file
118
csharp/ExampleONNX.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Supertonic
|
||||
{
|
||||
class Program
|
||||
{
|
||||
class Args
|
||||
{
|
||||
public bool UseGpu { get; set; } = false;
|
||||
public string OnnxDir { get; set; } = "assets/onnx";
|
||||
public int TotalStep { get; set; } = 5;
|
||||
public int NTest { get; set; } = 4;
|
||||
public List<string> VoiceStyle { get; set; } = new List<string> { "assets/voice_styles/M1.json" };
|
||||
public List<string> Text { get; set; } = new List<string>
|
||||
{
|
||||
"This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
};
|
||||
public string SaveDir { get; set; } = "results";
|
||||
}
|
||||
|
||||
static Args ParseArgs(string[] args)
|
||||
{
|
||||
var result = new Args();
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--use-gpu":
|
||||
result.UseGpu = true;
|
||||
break;
|
||||
case "--onnx-dir" when i + 1 < args.Length:
|
||||
result.OnnxDir = args[++i];
|
||||
break;
|
||||
case "--total-step" when i + 1 < args.Length:
|
||||
result.TotalStep = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--n-test" when i + 1 < args.Length:
|
||||
result.NTest = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--voice-style" when i + 1 < args.Length:
|
||||
result.VoiceStyle = args[++i].Split(',').ToList();
|
||||
break;
|
||||
case "--text" when i + 1 < args.Length:
|
||||
result.Text = args[++i].Split('|').ToList();
|
||||
break;
|
||||
case "--save-dir" when i + 1 < args.Length:
|
||||
result.SaveDir = args[++i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== TTS Inference with ONNX Runtime (C#) ===\n");
|
||||
|
||||
// --- 1. Parse arguments --- //
|
||||
var parsedArgs = ParseArgs(args);
|
||||
int totalStep = parsedArgs.TotalStep;
|
||||
int nTest = parsedArgs.NTest;
|
||||
string saveDir = parsedArgs.SaveDir;
|
||||
var voiceStylePaths = parsedArgs.VoiceStyle;
|
||||
var textList = parsedArgs.Text;
|
||||
|
||||
if (voiceStylePaths.Count != textList.Count)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Number of voice styles ({voiceStylePaths.Count}) must match number of texts ({textList.Count})");
|
||||
}
|
||||
|
||||
int bsz = voiceStylePaths.Count;
|
||||
|
||||
// --- 2. Load Text to Speech --- //
|
||||
var textToSpeech = Helper.LoadTextToSpeech(parsedArgs.OnnxDir, parsedArgs.UseGpu);
|
||||
Console.WriteLine();
|
||||
|
||||
// --- 3. Load Voice Style --- //
|
||||
var style = Helper.LoadVoiceStyle(voiceStylePaths, verbose: true);
|
||||
|
||||
// --- 4. Synthesize speech --- //
|
||||
for (int n = 0; n < nTest; n++)
|
||||
{
|
||||
Console.WriteLine($"\n[{n + 1}/{nTest}] Starting synthesis...");
|
||||
|
||||
var (wav, duration) = Helper.Timer("Generating speech from text", () =>
|
||||
textToSpeech.Call(textList, style, totalStep)
|
||||
);
|
||||
|
||||
if (!Directory.Exists(saveDir))
|
||||
{
|
||||
Directory.CreateDirectory(saveDir);
|
||||
}
|
||||
|
||||
for (int b = 0; b < bsz; b++)
|
||||
{
|
||||
string fname = $"{Helper.SanitizeFilename(textList[b], 20)}_{n + 1}.wav";
|
||||
|
||||
int wavLen = (int)(textToSpeech.SampleRate * duration[b]);
|
||||
var wavOut = new float[wavLen];
|
||||
Array.Copy(wav, b * wav.Length / bsz, wavOut, 0, Math.Min(wavLen, wav.Length / bsz));
|
||||
|
||||
string outputPath = Path.Combine(saveDir, fname);
|
||||
Helper.WriteWavFile(outputPath, wavOut, textToSpeech.SampleRate);
|
||||
Console.WriteLine($"Saved: {outputPath}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("\n=== Synthesis completed successfully! ===");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
612
csharp/Helper.cs
Normal file
612
csharp/Helper.cs
Normal file
@@ -0,0 +1,612 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
|
||||
namespace Supertonic
|
||||
{
|
||||
// ============================================================================
|
||||
// Configuration classes
|
||||
// ============================================================================
|
||||
|
||||
public class Config
|
||||
{
|
||||
public AEConfig AE { get; set; } = null!;
|
||||
public TTLConfig TTL { get; set; } = null!;
|
||||
|
||||
public class AEConfig
|
||||
{
|
||||
public int SampleRate { get; set; }
|
||||
public int BaseChunkSize { get; set; }
|
||||
}
|
||||
|
||||
public class TTLConfig
|
||||
{
|
||||
public int ChunkCompressFactor { get; set; }
|
||||
public int LatentDim { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Style class
|
||||
// ============================================================================
|
||||
|
||||
public class Style
|
||||
{
|
||||
public float[] Ttl { get; set; }
|
||||
public long[] TtlShape { get; set; }
|
||||
public float[] Dp { get; set; }
|
||||
public long[] DpShape { get; set; }
|
||||
|
||||
public Style(float[] ttl, long[] ttlShape, float[] dp, long[] dpShape)
|
||||
{
|
||||
Ttl = ttl;
|
||||
TtlShape = ttlShape;
|
||||
Dp = dp;
|
||||
DpShape = dpShape;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unicode text processor
|
||||
// ============================================================================
|
||||
|
||||
public class UnicodeProcessor
|
||||
{
|
||||
private readonly Dictionary<int, long> _indexer;
|
||||
|
||||
public UnicodeProcessor(string unicodeIndexerPath)
|
||||
{
|
||||
var json = File.ReadAllText(unicodeIndexerPath);
|
||||
var indexerArray = JsonSerializer.Deserialize<long[]>(json) ?? throw new Exception("Failed to load indexer");
|
||||
_indexer = new Dictionary<int, long>();
|
||||
for (int i = 0; i < indexerArray.Length; i++)
|
||||
{
|
||||
_indexer[i] = indexerArray[i];
|
||||
}
|
||||
}
|
||||
|
||||
private string PreprocessText(string text)
|
||||
{
|
||||
// Simple normalization (C# has Normalize built-in)
|
||||
return text.Normalize(NormalizationForm.FormKD);
|
||||
}
|
||||
|
||||
private int[] TextToUnicodeValues(string text)
|
||||
{
|
||||
return text.Select(c => (int)c).ToArray();
|
||||
}
|
||||
|
||||
private float[][][] GetTextMask(long[] textIdsLengths)
|
||||
{
|
||||
return Helper.LengthToMask(textIdsLengths);
|
||||
}
|
||||
|
||||
public (long[][] textIds, float[][][] textMask) Call(List<string> textList)
|
||||
{
|
||||
var processedTexts = textList.Select(t => PreprocessText(t)).ToList();
|
||||
var textIdsLengths = processedTexts.Select(t => (long)t.Length).ToArray();
|
||||
long maxLen = textIdsLengths.Max();
|
||||
|
||||
var textIds = new long[textList.Count][];
|
||||
for (int i = 0; i < processedTexts.Count; i++)
|
||||
{
|
||||
textIds[i] = new long[maxLen];
|
||||
var unicodeVals = TextToUnicodeValues(processedTexts[i]);
|
||||
for (int j = 0; j < unicodeVals.Length; j++)
|
||||
{
|
||||
if (_indexer.TryGetValue(unicodeVals[j], out long val))
|
||||
{
|
||||
textIds[i][j] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var textMask = GetTextMask(textIdsLengths);
|
||||
return (textIds, textMask);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TextToSpeech class
|
||||
// ============================================================================
|
||||
|
||||
public class TextToSpeech
|
||||
{
|
||||
private readonly Config _cfgs;
|
||||
private readonly UnicodeProcessor _textProcessor;
|
||||
private readonly InferenceSession _dpOrt;
|
||||
private readonly InferenceSession _textEncOrt;
|
||||
private readonly InferenceSession _vectorEstOrt;
|
||||
private readonly InferenceSession _vocoderOrt;
|
||||
public readonly int SampleRate;
|
||||
private readonly int _baseChunkSize;
|
||||
private readonly int _chunkCompressFactor;
|
||||
private readonly int _ldim;
|
||||
|
||||
public TextToSpeech(
|
||||
Config cfgs,
|
||||
UnicodeProcessor textProcessor,
|
||||
InferenceSession dpOrt,
|
||||
InferenceSession textEncOrt,
|
||||
InferenceSession vectorEstOrt,
|
||||
InferenceSession vocoderOrt)
|
||||
{
|
||||
_cfgs = cfgs;
|
||||
_textProcessor = textProcessor;
|
||||
_dpOrt = dpOrt;
|
||||
_textEncOrt = textEncOrt;
|
||||
_vectorEstOrt = vectorEstOrt;
|
||||
_vocoderOrt = vocoderOrt;
|
||||
SampleRate = cfgs.AE.SampleRate;
|
||||
_baseChunkSize = cfgs.AE.BaseChunkSize;
|
||||
_chunkCompressFactor = cfgs.TTL.ChunkCompressFactor;
|
||||
_ldim = cfgs.TTL.LatentDim;
|
||||
}
|
||||
|
||||
private (float[][][] noisyLatent, float[][][] latentMask) SampleNoisyLatent(float[] duration)
|
||||
{
|
||||
int bsz = duration.Length;
|
||||
float wavLenMax = duration.Max() * SampleRate;
|
||||
var wavLengths = duration.Select(d => (long)(d * SampleRate)).ToArray();
|
||||
int chunkSize = _baseChunkSize * _chunkCompressFactor;
|
||||
int latentLen = (int)((wavLenMax + chunkSize - 1) / chunkSize);
|
||||
int latentDim = _ldim * _chunkCompressFactor;
|
||||
|
||||
// Generate random noise
|
||||
var random = new Random();
|
||||
var noisyLatent = new float[bsz][][];
|
||||
for (int b = 0; b < bsz; b++)
|
||||
{
|
||||
noisyLatent[b] = new float[latentDim][];
|
||||
for (int d = 0; d < latentDim; d++)
|
||||
{
|
||||
noisyLatent[b][d] = new float[latentLen];
|
||||
for (int t = 0; t < latentLen; t++)
|
||||
{
|
||||
// Box-Muller transform for normal distribution
|
||||
double u1 = 1.0 - random.NextDouble();
|
||||
double u2 = 1.0 - random.NextDouble();
|
||||
noisyLatent[b][d][t] = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var latentMask = Helper.GetLatentMask(wavLengths, _baseChunkSize, _chunkCompressFactor);
|
||||
|
||||
// Apply mask
|
||||
for (int b = 0; b < bsz; b++)
|
||||
{
|
||||
for (int d = 0; d < latentDim; d++)
|
||||
{
|
||||
for (int t = 0; t < latentLen; t++)
|
||||
{
|
||||
noisyLatent[b][d][t] *= latentMask[b][0][t];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (noisyLatent, latentMask);
|
||||
}
|
||||
|
||||
public (float[] wav, float[] duration) Call(List<string> textList, Style style, int totalStep)
|
||||
{
|
||||
int bsz = textList.Count;
|
||||
if (bsz != style.TtlShape[0])
|
||||
{
|
||||
throw new ArgumentException("Number of texts must match number of style vectors");
|
||||
}
|
||||
|
||||
// Process text
|
||||
var (textIds, textMask) = _textProcessor.Call(textList);
|
||||
var textIdsShape = new long[] { bsz, textIds[0].Length };
|
||||
var textMaskShape = new long[] { bsz, 1, textMask[0][0].Length };
|
||||
|
||||
var textIdsTensor = Helper.IntArrayToTensor(textIds, textIdsShape);
|
||||
var textMaskTensor = Helper.ArrayToTensor(textMask, textMaskShape);
|
||||
|
||||
var styleTtlTensor = new DenseTensor<float>(style.Ttl, style.TtlShape.Select(x => (int)x).ToArray());
|
||||
var styleDpTensor = new DenseTensor<float>(style.Dp, style.DpShape.Select(x => (int)x).ToArray());
|
||||
|
||||
// Run duration predictor
|
||||
var dpInputs = new List<NamedOnnxValue>
|
||||
{
|
||||
NamedOnnxValue.CreateFromTensor("text_ids", textIdsTensor),
|
||||
NamedOnnxValue.CreateFromTensor("style_dp", styleDpTensor),
|
||||
NamedOnnxValue.CreateFromTensor("text_mask", textMaskTensor)
|
||||
};
|
||||
using var dpOutputs = _dpOrt.Run(dpInputs);
|
||||
var durOnnx = dpOutputs.First(o => o.Name == "duration").AsTensor<float>().ToArray();
|
||||
|
||||
// Run text encoder
|
||||
var textEncInputs = new List<NamedOnnxValue>
|
||||
{
|
||||
NamedOnnxValue.CreateFromTensor("text_ids", textIdsTensor),
|
||||
NamedOnnxValue.CreateFromTensor("style_ttl", styleTtlTensor),
|
||||
NamedOnnxValue.CreateFromTensor("text_mask", textMaskTensor)
|
||||
};
|
||||
using var textEncOutputs = _textEncOrt.Run(textEncInputs);
|
||||
var textEmbTensor = textEncOutputs.First(o => o.Name == "text_emb").AsTensor<float>();
|
||||
|
||||
// Sample noisy latent
|
||||
var (xt, latentMask) = SampleNoisyLatent(durOnnx);
|
||||
var latentShape = new long[] { bsz, xt[0].Length, xt[0][0].Length };
|
||||
var latentMaskShape = new long[] { bsz, 1, latentMask[0][0].Length };
|
||||
|
||||
var totalStepArray = Enumerable.Repeat((float)totalStep, bsz).ToArray();
|
||||
|
||||
// Iterative denoising
|
||||
for (int step = 0; step < totalStep; step++)
|
||||
{
|
||||
var currentStepArray = Enumerable.Repeat((float)step, bsz).ToArray();
|
||||
|
||||
var vectorEstInputs = new List<NamedOnnxValue>
|
||||
{
|
||||
NamedOnnxValue.CreateFromTensor("noisy_latent", Helper.ArrayToTensor(xt, latentShape)),
|
||||
NamedOnnxValue.CreateFromTensor("text_emb", textEmbTensor),
|
||||
NamedOnnxValue.CreateFromTensor("style_ttl", styleTtlTensor),
|
||||
NamedOnnxValue.CreateFromTensor("text_mask", textMaskTensor),
|
||||
NamedOnnxValue.CreateFromTensor("latent_mask", Helper.ArrayToTensor(latentMask, latentMaskShape)),
|
||||
NamedOnnxValue.CreateFromTensor("total_step", new DenseTensor<float>(totalStepArray, new int[] { bsz })),
|
||||
NamedOnnxValue.CreateFromTensor("current_step", new DenseTensor<float>(currentStepArray, new int[] { bsz }))
|
||||
};
|
||||
|
||||
using var vectorEstOutputs = _vectorEstOrt.Run(vectorEstInputs);
|
||||
var denoisedLatent = vectorEstOutputs.First(o => o.Name == "denoised_latent").AsTensor<float>();
|
||||
|
||||
// Update xt
|
||||
int idx = 0;
|
||||
for (int b = 0; b < bsz; b++)
|
||||
{
|
||||
for (int d = 0; d < xt[b].Length; d++)
|
||||
{
|
||||
for (int t = 0; t < xt[b][d].Length; t++)
|
||||
{
|
||||
xt[b][d][t] = denoisedLatent.GetValue(idx++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run vocoder
|
||||
var vocoderInputs = new List<NamedOnnxValue>
|
||||
{
|
||||
NamedOnnxValue.CreateFromTensor("latent", Helper.ArrayToTensor(xt, latentShape))
|
||||
};
|
||||
using var vocoderOutputs = _vocoderOrt.Run(vocoderInputs);
|
||||
var wavTensor = vocoderOutputs.First(o => o.Name == "wav_tts").AsTensor<float>();
|
||||
|
||||
return (wavTensor.ToArray(), durOnnx);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper class with utility functions
|
||||
// ============================================================================
|
||||
|
||||
public static class Helper
|
||||
{
|
||||
// ============================================================================
|
||||
// Utility functions
|
||||
// ============================================================================
|
||||
|
||||
public static float[][][] LengthToMask(long[] lengths, long maxLen = -1)
|
||||
{
|
||||
if (maxLen == -1)
|
||||
{
|
||||
maxLen = lengths.Max();
|
||||
}
|
||||
|
||||
var mask = new float[lengths.Length][][];
|
||||
for (int i = 0; i < lengths.Length; i++)
|
||||
{
|
||||
mask[i] = new float[1][];
|
||||
mask[i][0] = new float[maxLen];
|
||||
for (int j = 0; j < maxLen; j++)
|
||||
{
|
||||
mask[i][0][j] = j < lengths[i] ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
public static float[][][] GetLatentMask(long[] wavLengths, int baseChunkSize, int chunkCompressFactor)
|
||||
{
|
||||
int latentSize = baseChunkSize * chunkCompressFactor;
|
||||
var latentLengths = wavLengths.Select(len => (len + latentSize - 1) / latentSize).ToArray();
|
||||
return LengthToMask(latentLengths);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ONNX model loading
|
||||
// ============================================================================
|
||||
|
||||
public static InferenceSession LoadOnnx(string onnxPath, SessionOptions opts)
|
||||
{
|
||||
return new InferenceSession(onnxPath, opts);
|
||||
}
|
||||
|
||||
public static (InferenceSession dp, InferenceSession textEnc, InferenceSession vectorEst, InferenceSession vocoder)
|
||||
LoadOnnxAll(string onnxDir, SessionOptions opts)
|
||||
{
|
||||
var dpPath = Path.Combine(onnxDir, "duration_predictor.onnx");
|
||||
var textEncPath = Path.Combine(onnxDir, "text_encoder.onnx");
|
||||
var vectorEstPath = Path.Combine(onnxDir, "vector_estimator.onnx");
|
||||
var vocoderPath = Path.Combine(onnxDir, "vocoder.onnx");
|
||||
|
||||
return (
|
||||
LoadOnnx(dpPath, opts),
|
||||
LoadOnnx(textEncPath, opts),
|
||||
LoadOnnx(vectorEstPath, opts),
|
||||
LoadOnnx(vocoderPath, opts)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration loading
|
||||
// ============================================================================
|
||||
|
||||
public static Config LoadCfgs(string onnxDir)
|
||||
{
|
||||
var cfgPath = Path.Combine(onnxDir, "tts.json");
|
||||
var json = File.ReadAllText(cfgPath);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
return new Config
|
||||
{
|
||||
AE = new Config.AEConfig
|
||||
{
|
||||
SampleRate = root.GetProperty("ae").GetProperty("sample_rate").GetInt32(),
|
||||
BaseChunkSize = root.GetProperty("ae").GetProperty("base_chunk_size").GetInt32()
|
||||
},
|
||||
TTL = new Config.TTLConfig
|
||||
{
|
||||
ChunkCompressFactor = root.GetProperty("ttl").GetProperty("chunk_compress_factor").GetInt32(),
|
||||
LatentDim = root.GetProperty("ttl").GetProperty("latent_dim").GetInt32()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static UnicodeProcessor LoadTextProcessor(string onnxDir)
|
||||
{
|
||||
var unicodeIndexerPath = Path.Combine(onnxDir, "unicode_indexer.json");
|
||||
return new UnicodeProcessor(unicodeIndexerPath);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Voice style loading
|
||||
// ============================================================================
|
||||
|
||||
public static Style LoadVoiceStyle(List<string> voiceStylePaths, bool verbose = false)
|
||||
{
|
||||
int bsz = voiceStylePaths.Count;
|
||||
|
||||
// Read first file to get dimensions
|
||||
var firstJson = File.ReadAllText(voiceStylePaths[0]);
|
||||
using var firstDoc = JsonDocument.Parse(firstJson);
|
||||
var firstRoot = firstDoc.RootElement;
|
||||
|
||||
var ttlDims = ParseInt64Array(firstRoot.GetProperty("style_ttl").GetProperty("dims"));
|
||||
var dpDims = ParseInt64Array(firstRoot.GetProperty("style_dp").GetProperty("dims"));
|
||||
|
||||
long ttlDim1 = ttlDims[1];
|
||||
long ttlDim2 = ttlDims[2];
|
||||
long dpDim1 = dpDims[1];
|
||||
long dpDim2 = dpDims[2];
|
||||
|
||||
// Pre-allocate arrays with full batch size
|
||||
int ttlSize = (int)(bsz * ttlDim1 * ttlDim2);
|
||||
int dpSize = (int)(bsz * dpDim1 * dpDim2);
|
||||
var ttlFlat = new float[ttlSize];
|
||||
var dpFlat = new float[dpSize];
|
||||
|
||||
// Fill in the data
|
||||
for (int i = 0; i < bsz; i++)
|
||||
{
|
||||
var json = File.ReadAllText(voiceStylePaths[i]);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Flatten data
|
||||
var ttlData3D = ParseFloat3DArray(root.GetProperty("style_ttl").GetProperty("data"));
|
||||
var ttlDataFlat = new List<float>();
|
||||
foreach (var batch in ttlData3D)
|
||||
{
|
||||
foreach (var row in batch)
|
||||
{
|
||||
ttlDataFlat.AddRange(row);
|
||||
}
|
||||
}
|
||||
|
||||
var dpData3D = ParseFloat3DArray(root.GetProperty("style_dp").GetProperty("data"));
|
||||
var dpDataFlat = new List<float>();
|
||||
foreach (var batch in dpData3D)
|
||||
{
|
||||
foreach (var row in batch)
|
||||
{
|
||||
dpDataFlat.AddRange(row);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy to pre-allocated array
|
||||
int ttlOffset = (int)(i * ttlDim1 * ttlDim2);
|
||||
ttlDataFlat.CopyTo(ttlFlat, ttlOffset);
|
||||
|
||||
int dpOffset = (int)(i * dpDim1 * dpDim2);
|
||||
dpDataFlat.CopyTo(dpFlat, dpOffset);
|
||||
}
|
||||
|
||||
var ttlShape = new long[] { bsz, ttlDim1, ttlDim2 };
|
||||
var dpShape = new long[] { bsz, dpDim1, dpDim2 };
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Console.WriteLine($"Loaded {bsz} voice styles");
|
||||
}
|
||||
|
||||
return new Style(ttlFlat, ttlShape, dpFlat, dpShape);
|
||||
}
|
||||
|
||||
private static float[][][] ParseFloat3DArray(JsonElement element)
|
||||
{
|
||||
var result = new List<float[][]>();
|
||||
foreach (var batch in element.EnumerateArray())
|
||||
{
|
||||
var batch2D = new List<float[]>();
|
||||
foreach (var row in batch.EnumerateArray())
|
||||
{
|
||||
var rowData = new List<float>();
|
||||
foreach (var val in row.EnumerateArray())
|
||||
{
|
||||
rowData.Add(val.GetSingle());
|
||||
}
|
||||
batch2D.Add(rowData.ToArray());
|
||||
}
|
||||
result.Add(batch2D.ToArray());
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static long[] ParseInt64Array(JsonElement element)
|
||||
{
|
||||
var result = new List<long>();
|
||||
foreach (var val in element.EnumerateArray())
|
||||
{
|
||||
result.Add(val.GetInt64());
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TextToSpeech loading
|
||||
// ============================================================================
|
||||
|
||||
public static TextToSpeech LoadTextToSpeech(string onnxDir, bool useGpu = false)
|
||||
{
|
||||
var opts = new SessionOptions();
|
||||
if (useGpu)
|
||||
{
|
||||
throw new NotImplementedException("GPU mode is not supported yet");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Using CPU for inference");
|
||||
}
|
||||
|
||||
var cfgs = LoadCfgs(onnxDir);
|
||||
var (dpOrt, textEncOrt, vectorEstOrt, vocoderOrt) = LoadOnnxAll(onnxDir, opts);
|
||||
var textProcessor = LoadTextProcessor(onnxDir);
|
||||
|
||||
return new TextToSpeech(cfgs, textProcessor, dpOrt, textEncOrt, vectorEstOrt, vocoderOrt);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WAV file writing
|
||||
// ============================================================================
|
||||
|
||||
public static void WriteWavFile(string filename, float[] audioData, int sampleRate)
|
||||
{
|
||||
using var writer = new BinaryWriter(File.Open(filename, FileMode.Create));
|
||||
|
||||
int numChannels = 1;
|
||||
int bitsPerSample = 16;
|
||||
int byteRate = sampleRate * numChannels * bitsPerSample / 8;
|
||||
short blockAlign = (short)(numChannels * bitsPerSample / 8);
|
||||
int dataSize = audioData.Length * bitsPerSample / 8;
|
||||
|
||||
// RIFF header
|
||||
writer.Write(Encoding.ASCII.GetBytes("RIFF"));
|
||||
writer.Write(36 + dataSize);
|
||||
writer.Write(Encoding.ASCII.GetBytes("WAVE"));
|
||||
|
||||
// fmt chunk
|
||||
writer.Write(Encoding.ASCII.GetBytes("fmt "));
|
||||
writer.Write(16); // fmt chunk size
|
||||
writer.Write((short)1); // audio format (PCM)
|
||||
writer.Write((short)numChannels);
|
||||
writer.Write(sampleRate);
|
||||
writer.Write(byteRate);
|
||||
writer.Write(blockAlign);
|
||||
writer.Write((short)bitsPerSample);
|
||||
|
||||
// data chunk
|
||||
writer.Write(Encoding.ASCII.GetBytes("data"));
|
||||
writer.Write(dataSize);
|
||||
|
||||
// Write audio data
|
||||
foreach (var sample in audioData)
|
||||
{
|
||||
float clamped = Math.Max(-1.0f, Math.Min(1.0f, sample));
|
||||
short intSample = (short)(clamped * 32767);
|
||||
writer.Write(intSample);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tensor conversion utilities
|
||||
// ============================================================================
|
||||
|
||||
public static DenseTensor<float> ArrayToTensor(float[][][] array, long[] dims)
|
||||
{
|
||||
var flat = new List<float>();
|
||||
foreach (var batch in array)
|
||||
{
|
||||
foreach (var row in batch)
|
||||
{
|
||||
flat.AddRange(row);
|
||||
}
|
||||
}
|
||||
return new DenseTensor<float>(flat.ToArray(), dims.Select(x => (int)x).ToArray());
|
||||
}
|
||||
|
||||
public static DenseTensor<long> IntArrayToTensor(long[][] array, long[] dims)
|
||||
{
|
||||
var flat = new List<long>();
|
||||
foreach (var row in array)
|
||||
{
|
||||
flat.AddRange(row);
|
||||
}
|
||||
return new DenseTensor<long>(flat.ToArray(), dims.Select(x => (int)x).ToArray());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timer utility
|
||||
// ============================================================================
|
||||
|
||||
public static T Timer<T>(string name, Func<T> func)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
Console.WriteLine($"{name}...");
|
||||
var result = func();
|
||||
var elapsed = (DateTime.Now - start).TotalSeconds;
|
||||
Console.WriteLine($" -> {name} completed in {elapsed:F2} sec");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string SanitizeFilename(string text, int maxLen)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int count = 0;
|
||||
foreach (char c in text)
|
||||
{
|
||||
if (count >= maxLen) break;
|
||||
if (char.IsLetterOrDigit(c))
|
||||
{
|
||||
result.Append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append('_');
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
99
csharp/README.md
Normal file
99
csharp/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# TTS ONNX Inference Examples
|
||||
|
||||
This guide provides examples for running TTS inference using `ExampleONNX.cs`.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- .NET 9.0 SDK or later
|
||||
- [Download .NET SDK](https://dotnet.microsoft.com/download)
|
||||
|
||||
### Install dependencies
|
||||
```bash
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Example 1: Default Inference
|
||||
Run inference with default settings:
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
dotnet run -- \
|
||||
--voice-style assets/voice_styles/M1.json,assets/voice_styles/F1.json \
|
||||
--text "The sun sets behind the mountains, painting the sky in shades of pink and orange.|The weather is beautiful and sunny outside. A gentle breeze makes the air feel fresh and pleasant."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate speech for 2 different voice-text pairs
|
||||
- Use male voice style (M1.json) for the first text
|
||||
- Use female voice style (F1.json) for the second text
|
||||
- Process both samples in a single batch
|
||||
|
||||
### Example 3: High Quality Inference
|
||||
Increase denoising steps for better quality:
|
||||
```bash
|
||||
dotnet run -- \
|
||||
--total-step 10 \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
## Available Arguments
|
||||
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (not supported yet) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s) (comma-separated) |
|
||||
| `--text` | str+ | (long default text) | Text(s) to synthesize (pipe-separated: `|`) |
|
||||
| `--save-dir` | str | `results` | Output directory |
|
||||
|
||||
## Notes
|
||||
|
||||
- **Batch Processing**: The number of `--voice-style` files must match the number of `--text` entries
|
||||
- **Quality vs Speed**: Higher `--total-step` values produce better quality but take longer
|
||||
- **GPU Support**: GPU mode is not supported yet
|
||||
|
||||
## Building the Project
|
||||
|
||||
### Build for Release
|
||||
```bash
|
||||
dotnet build -c Release
|
||||
```
|
||||
|
||||
### Run the compiled executable
|
||||
```bash
|
||||
./bin/Release/net9.0/Supertonic
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
csharp/
|
||||
├── ExampleONNX.cs # Main inference script
|
||||
├── Helper.cs # Helper functions and classes
|
||||
├── Supertonic.csproj # Project configuration
|
||||
├── README.md # This file
|
||||
└── results/ # Output directory (created automatically)
|
||||
```
|
||||
|
||||
|
||||
17
csharp/Supertonic.csproj
Normal file
17
csharp/Supertonic.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
1
csharp/assets
Symbolic link
1
csharp/assets
Symbolic link
@@ -0,0 +1 @@
|
||||
../assets
|
||||
Reference in New Issue
Block a user