| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| using System; |
| using System.Collections.Generic; |
| using System.IO; |
| using System.Linq; |
| using Microsoft.ML.OnnxRuntime; |
| using Microsoft.ML.OnnxRuntime.Tensors; |
|
|
| namespace CohereTranscribeExample |
| { |
| class Program |
| { |
| |
| const int NumDecoderLayers = 8; |
| const int DecoderHiddenSize = 1024; |
| const int NumHeads = 8; |
| const int HeadDim = 128; |
| const int MaxSeqLen = 1024; |
| const int VocabSize = 16384; |
| const int SampleRate = 16000; |
|
|
| static void Main(string[] args) |
| { |
| if (args.Length < 1) |
| { |
| Console.WriteLine("Usage: CohereTranscribe <audio.wav> [language]"); |
| Console.WriteLine(" language: en, de, fr, es, it, pt, nl, pl, el, ar, ja, zh, vi, ko"); |
| return; |
| } |
|
|
| string audioPath = args[0]; |
| string language = args.Length > 1 ? args[1] : "en"; |
| string modelDir = "./onnx-baked"; |
|
|
| |
| var tokens = LoadTokens(Path.Combine(modelDir, "tokens.txt")); |
| var tokenToId = tokens.ToDictionary(kv => kv.Value, kv => kv.Key); |
|
|
| |
| var promptTokenIds = BuildPromptTokens(language, tokenToId); |
| Console.WriteLine($"Prompt: {string.Join(" ", promptTokenIds.Select(id => tokens[id]))}"); |
|
|
| |
| var wav = new WaveReader(audioPath); |
| float[] audio = wav.Samples; |
| if (wav.SampleRate != SampleRate) |
| { |
| Console.WriteLine($"Warning: audio is {wav.SampleRate}Hz, model expects {SampleRate}Hz. Resample first!"); |
| return; |
| } |
| Console.WriteLine($"Audio: {audio.Length} samples ({audio.Length / (float)SampleRate:F1}s)"); |
|
|
| |
| var sessionOptions = new SessionOptions(); |
| sessionOptions.InterOpNumThreads = 4; |
| sessionOptions.IntraOpNumThreads = 4; |
|
|
| string encoderPath = Path.Combine(modelDir, "cohere-encoder.int8.onnx"); |
| string decoderPath = Path.Combine(modelDir, "cohere-decoder.int8.onnx"); |
|
|
| Console.WriteLine("Loading encoder..."); |
| using var encoder = new InferenceSession(encoderPath, sessionOptions); |
| Console.WriteLine("Loading decoder..."); |
| using var decoder = new InferenceSession(decoderPath, sessionOptions); |
|
|
| |
| Console.WriteLine("Running encoder..."); |
| var audioTensor = new DenseTensor<float>(audio, new[] { 1, audio.Length }); |
| var encoderInputs = new List<NamedOnnxValue> |
| { |
| NamedOnnxValue.CreateFromTensor("audio", audioTensor) |
| }; |
|
|
| float[] crossK, crossV; |
| int[] crossKShape, crossVShape; |
| using (var encoderResults = encoder.Run(encoderInputs)) |
| { |
| var crossKTensor = encoderResults.First(r => r.Name == "n_layer_cross_k").AsTensor<float>(); |
| var crossVTensor = encoderResults.First(r => r.Name == "n_layer_cross_v").AsTensor<float>(); |
| crossK = crossKTensor.ToArray(); |
| crossV = crossVTensor.ToArray(); |
| crossKShape = crossKTensor.Dimensions.ToArray(); |
| crossVShape = crossVTensor.Dimensions.ToArray(); |
| } |
| int T_enc = crossKShape[2]; |
| Console.WriteLine($"Encoder output: T_enc={T_enc}"); |
|
|
| |
| Console.WriteLine("Decoding..."); |
| var generatedIds = new List<int>(promptTokenIds); |
| int eosId = tokenToId.GetValueOrDefault("<|endoftext|>", -1); |
| int maxNewTokens = 256; |
|
|
| |
| int cacheSize = NumDecoderLayers * 1 * NumHeads * MaxSeqLen * HeadDim; |
| float[] selfKCache = new float[cacheSize]; |
| float[] selfVCache = new float[cacheSize]; |
| int[] cacheShape = new[] { NumDecoderLayers, 1, NumHeads, MaxSeqLen, HeadDim }; |
|
|
| |
| int offset = 0; |
| var currentTokens = promptTokenIds.ToArray(); |
|
|
| for (int step = 0; step < maxNewTokens; step++) |
| { |
| int nTokens = currentTokens.Length; |
| var tokensTensor = new DenseTensor<long>( |
| currentTokens.Select(t => (long)t).ToArray(), |
| new[] { 1, nTokens }); |
|
|
| var decoderInputs = new List<NamedOnnxValue> |
| { |
| NamedOnnxValue.CreateFromTensor("tokens", tokensTensor), |
| NamedOnnxValue.CreateFromTensor("in_n_layer_self_k_cache", |
| new DenseTensor<float>(selfKCache, cacheShape)), |
| NamedOnnxValue.CreateFromTensor("in_n_layer_self_v_cache", |
| new DenseTensor<float>(selfVCache, cacheShape)), |
| NamedOnnxValue.CreateFromTensor("n_layer_cross_k", |
| new DenseTensor<float>(crossK, crossKShape)), |
| NamedOnnxValue.CreateFromTensor("n_layer_cross_v", |
| new DenseTensor<float>(crossV, crossVShape)), |
| NamedOnnxValue.CreateFromTensor("offset", |
| new DenseTensor<long>(new[] { (long)offset }, Array.Empty<int>())), |
| }; |
|
|
| using var decoderResults = decoder.Run(decoderInputs); |
|
|
| |
| var logitsTensor = decoderResults.First(r => r.Name == "logits").AsTensor<float>(); |
| int lastPos = nTokens - 1; |
|
|
| |
| int bestId = 0; |
| float bestScore = float.NegativeInfinity; |
| for (int v = 0; v < VocabSize; v++) |
| { |
| float score = logitsTensor[0, lastPos, v]; |
| if (score > bestScore) |
| { |
| bestScore = score; |
| bestId = v; |
| } |
| } |
|
|
| |
| if (bestId == eosId) break; |
|
|
| generatedIds.Add(bestId); |
|
|
| |
| var outKCache = decoderResults.First(r => r.Name == "out_n_layer_self_k_cache").AsTensor<float>(); |
| var outVCache = decoderResults.First(r => r.Name == "out_n_layer_self_v_cache").AsTensor<float>(); |
| selfKCache = outKCache.ToArray(); |
| selfVCache = outVCache.ToArray(); |
|
|
| |
| offset += nTokens; |
| currentTokens = new[] { bestId }; |
| } |
|
|
| |
| |
| var outputIds = generatedIds.Skip(promptTokenIds.Length).ToList(); |
| string text = string.Join("", outputIds |
| .Where(id => tokens.ContainsKey(id)) |
| .Select(id => tokens[id]) |
| .Select(t => t.StartsWith("<|") ? "" : t.Replace("\u2581", " "))); |
|
|
| Console.WriteLine($"\nLanguage: {language}"); |
| Console.WriteLine($"Text: {text.Trim()}"); |
| Console.WriteLine($"Generated {outputIds.Count} tokens"); |
| } |
|
|
| static int[] BuildPromptTokens(string language, Dictionary<string, int> tokenToId) |
| { |
| |
| |
| var promptParts = new[] |
| { |
| "<|startofcontext|>", |
| "<|startoftranscript|>", |
| "<|emo:undefined|>", |
| $"<|{language}|>", |
| $"<|{language}|>", |
| "<|pnc|>", |
| "<|noitn|>", |
| "<|notimestamp|>", |
| "<|nodiarize|>", |
| }; |
| return promptParts |
| .Where(t => tokenToId.ContainsKey(t)) |
| .Select(t => tokenToId[t]) |
| .ToArray(); |
| } |
|
|
| static Dictionary<int, string> LoadTokens(string path) |
| { |
| var tokens = new Dictionary<int, string>(); |
| foreach (var line in File.ReadAllLines(path)) |
| { |
| int lastSpace = line.LastIndexOf(' '); |
| if (lastSpace < 0) continue; |
| string token = line.Substring(0, lastSpace); |
| if (int.TryParse(line.Substring(lastSpace + 1), out int id)) |
| tokens[id] = token; |
| } |
| return tokens; |
| } |
| } |
|
|
| class WaveReader |
| { |
| public int SampleRate { get; } |
| public float[] Samples { get; } |
|
|
| public WaveReader(string path) |
| { |
| using var reader = new BinaryReader(File.OpenRead(path)); |
|
|
| reader.ReadBytes(4); |
| reader.ReadInt32(); |
| reader.ReadBytes(4); |
|
|
| while (true) |
| { |
| string chunkId = new string(reader.ReadChars(4)); |
| int chunkSize = reader.ReadInt32(); |
|
|
| if (chunkId == "fmt ") |
| { |
| int audioFormat = reader.ReadInt16(); |
| int numChannels = reader.ReadInt16(); |
| SampleRate = reader.ReadInt32(); |
| reader.ReadInt32(); |
| reader.ReadInt16(); |
| int bitsPerSample = reader.ReadInt16(); |
|
|
| if (chunkSize > 16) |
| reader.ReadBytes(chunkSize - 16); |
|
|
| while (true) |
| { |
| string dataId = new string(reader.ReadChars(4)); |
| int dataSize = reader.ReadInt32(); |
|
|
| if (dataId == "data") |
| { |
| int numSamples = dataSize / (bitsPerSample / 8) / numChannels; |
| Samples = new float[numSamples]; |
|
|
| for (int i = 0; i < numSamples; i++) |
| { |
| float sample = 0; |
| for (int ch = 0; ch < numChannels; ch++) |
| { |
| short s = reader.ReadInt16(); |
| sample += s / 32768.0f; |
| } |
| Samples[i] = sample / numChannels; |
| } |
| return; |
| } |
| else |
| { |
| reader.ReadBytes(dataSize); |
| } |
| } |
| } |
| else |
| { |
| reader.ReadBytes(chunkSize); |
| } |
| } |
| } |
| } |
| } |
|
|