Audio Classification
unity-sentis
unity
sentis
on-device
audio-tagging
sound-event-detection
audioset
Instructions to use Sky-Kim/com.sky.sentis.ced-tiny with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- unity-sentis
How to use Sky-Kim/com.sky.sentis.ced-tiny with unity-sentis:
string modelName = "[Your model name here].sentis"; Model model = ModelLoader.Load(Application.streamingAssetsPath + "/" + modelName); IWorker engine = WorkerFactory.CreateWorker(BackendType.GPUCompute, model); // Please see provided C# file for more details
- Notebooks
- Google Colab
- Kaggle
Download Runtime/CedTiny.cs from Sky-Kim/com.sky.sentis.ced-tiny: direct link, hf CLI and curl.
- Browser
- Download file 7.87 kB
-
https://huggingface.co/Sky-Kim/com.sky.sentis.ced-tiny/resolve/main/Runtime/CedTiny.cs
- Command line
-
hf download hf://Sky-Kim/com.sky.sentis.ced-tiny/Runtime/CedTiny.cs
-
curl -L -o CedTiny.cs https://huggingface.co/Sky-Kim/com.sky.sentis.ced-tiny/resolve/main/Runtime/CedTiny.cs
7.87 kB
| // CED-Tiny (Consistent Ensemble Distillation ViT) AudioSet audio tagger. Self-contained Unity Sentis inference. | |
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Threading.Tasks; | |
| using Unity.InferenceEngine; | |
| using UnityEngine; | |
| namespace SentisModels | |
| { | |
| // Mono waveform -> 527 AudioSet class probabilities (multi-label: independent sigmoids, they do not sum to 1) and a | |
| // 192-d clip embedding. The log-mel frontend (STFT as a fixed-basis Conv1d) is baked into the model, so the input is | |
| // the raw 16000 Hz waveform in [-1, 1]; one inference covers a 3 s window. Other sample rates are resampled here. | |
| public sealed class CedTiny : IDisposable | |
| { | |
| public const int SampleRate = 16000; | |
| public const int WindowSamples = 48000; | |
| public const int ClassCount = 527; | |
| public const int EmbeddingSize = 192; | |
| const string DefaultModelFile = "ced_tiny_fp32.sentis"; | |
| const string DefaultLabelFile = "audioset_labels.txt"; | |
| public struct Prediction | |
| { | |
| public int Index; // AudioSet class index (ontology order, class_labels_indices.csv) | |
| public string Label; | |
| public float Score; // sigmoid probability | |
| } | |
| readonly BackendType m_BackendType; | |
| Worker m_Worker; | |
| Tensor<float> m_InputTensor; | |
| string[] m_Labels; | |
| readonly float[] m_Window = new float[WindowSamples]; | |
| readonly float[] m_Stream = new float[WindowSamples]; // sliding buffer for Push / ClassifyLatest | |
| // Embedding of the most recent Classify call (penultimate features; use for your own classes / similarity). | |
| public float[] Embedding { get; } = new float[EmbeddingSize]; | |
| public IReadOnlyList<string> Labels => m_Labels; | |
| public CedTiny(BackendType backendType = BackendType.GPUCompute) | |
| { | |
| m_BackendType = backendType == BackendType.GPUCompute && !SystemInfo.supportsComputeShaders | |
| ? BackendType.CPU | |
| : backendType; | |
| } | |
| public bool IsLoaded => m_Worker != null; | |
| // modelRoot: absolute path to the folder holding ced_tiny_fp32.sentis and audioset_labels.txt. | |
| public void Load(string modelRoot, string modelFile = DefaultModelFile) | |
| { | |
| Load(ModelLoader.Load(Path.Combine(modelRoot, modelFile)), File.ReadAllLines(Path.Combine(modelRoot, DefaultLabelFile))); | |
| } | |
| // For platforms where the files must be read as bytes / text (e.g. Android StreamingAssets). | |
| public void Load(byte[] modelBytes, string labelsText) | |
| { | |
| using var stream = new MemoryStream(modelBytes); | |
| Load(ModelLoader.Load(stream), labelsText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)); | |
| } | |
| void Load(Model model, string[] labels) | |
| { | |
| if (labels.Length != ClassCount) | |
| throw new ArgumentException($"expected {ClassCount} labels, got {labels.Length}"); | |
| DisposeResources(); | |
| m_Labels = labels; | |
| m_Worker = new Worker(model, m_BackendType); | |
| m_InputTensor = new Tensor<float>(new TensorShape(1, WindowSamples)); | |
| Array.Clear(m_Stream, 0, m_Stream.Length); | |
| } | |
| // One silent pass so the first real call doesn't pay shader compilation / allocation. | |
| public void Warmup() | |
| { | |
| Classify(new float[WindowSamples]); | |
| } | |
| // samples: mono PCM at sampleRate. The LAST 3 s are classified (zero-padded in front when shorter). | |
| public float[] Classify(float[] samples, int sampleRate = SampleRate) | |
| { | |
| Schedule(samples, sampleRate); | |
| return Read(); | |
| } | |
| // Async readback variant; prefer this for per-frame use on GPU backends (no main-thread stall). | |
| public async Task<float[]> ClassifyAsync(float[] samples, int sampleRate = SampleRate) | |
| { | |
| Schedule(samples, sampleRate); | |
| using var scores = await (m_Worker.PeekOutput("scores") as Tensor<float>).ReadbackAndCloneAsync(); | |
| using var embedding = await (m_Worker.PeekOutput("embedding") as Tensor<float>).ReadbackAndCloneAsync(); | |
| embedding.DownloadToArray().CopyTo(Embedding, 0); | |
| return scores.DownloadToArray(); | |
| } | |
| // Streaming: append new microphone / clip samples (mono, any rate) to the sliding window. | |
| public void Push(float[] samples, int sampleRate = SampleRate) | |
| { | |
| var x = sampleRate == SampleRate ? samples : Resample(samples, sampleRate, SampleRate); | |
| if (x.Length >= WindowSamples) | |
| { | |
| Array.Copy(x, x.Length - WindowSamples, m_Stream, 0, WindowSamples); | |
| return; | |
| } | |
| Array.Copy(m_Stream, x.Length, m_Stream, 0, WindowSamples - x.Length); | |
| Array.Copy(x, 0, m_Stream, WindowSamples - x.Length, x.Length); | |
| } | |
| // Classifies the last 3 s pushed with Push (call e.g. every 0.25-0.5 s for a sliding window). | |
| public float[] ClassifyLatest() => Classify(m_Stream); | |
| public Task<float[]> ClassifyLatestAsync() => ClassifyAsync(m_Stream); | |
| // Highest-scoring classes, best first. | |
| public List<Prediction> Top(float[] scores, int k = 5, float minScore = 0f) | |
| { | |
| var order = new int[scores.Length]; | |
| for (var i = 0; i < order.Length; i++) | |
| order[i] = i; | |
| Array.Sort(order, (a, b) => scores[b].CompareTo(scores[a])); | |
| var result = new List<Prediction>(); | |
| for (var i = 0; i < order.Length && result.Count < k; i++) | |
| { | |
| if (scores[order[i]] < minScore) | |
| break; | |
| result.Add(new Prediction { Index = order[i], Label = m_Labels[order[i]], Score = scores[order[i]] }); | |
| } | |
| return result; | |
| } | |
| // Average the channels of interleaved PCM (AudioClip.GetData / Microphone data). | |
| public static float[] ToMono(float[] interleaved, int channels) | |
| { | |
| if (channels <= 1) | |
| return interleaved; | |
| var mono = new float[interleaved.Length / channels]; | |
| for (var i = 0; i < mono.Length; i++) | |
| { | |
| var sum = 0f; | |
| for (var c = 0; c < channels; c++) | |
| sum += interleaved[i * channels + c]; | |
| mono[i] = sum / channels; | |
| } | |
| return mono; | |
| } | |
| // Linear-interpolation resampler (adequate for tagging; use a band-limited resampler for best fidelity). | |
| public static float[] Resample(float[] src, int srcRate, int dstRate) | |
| { | |
| if (srcRate == dstRate || src.Length == 0) | |
| return src; | |
| var n = (int)((long)src.Length * dstRate / srcRate); | |
| var dst = new float[n]; | |
| var step = (double)srcRate / dstRate; | |
| for (var i = 0; i < n; i++) | |
| { | |
| var p = i * step; | |
| var j = (int)p; | |
| var t = (float)(p - j); | |
| dst[i] = j + 1 < src.Length ? src[j] + (src[j + 1] - src[j]) * t : src[src.Length - 1]; | |
| } | |
| return dst; | |
| } | |
| void Schedule(float[] samples, int sampleRate) | |
| { | |
| if (m_Worker == null) | |
| throw new InvalidOperationException("CedTiny.Load must be called before Classify."); | |
| var x = sampleRate == SampleRate ? samples : Resample(samples, sampleRate, SampleRate); | |
| var n = Math.Min(x.Length, WindowSamples); | |
| Array.Clear(m_Window, 0, WindowSamples - n); | |
| Array.Copy(x, x.Length - n, m_Window, WindowSamples - n, n); | |
| m_InputTensor.Upload(m_Window); | |
| m_Worker.Schedule(m_InputTensor); | |
| } | |
| float[] Read() | |
| { | |
| using var scores = (m_Worker.PeekOutput("scores") as Tensor<float>).ReadbackAndClone(); | |
| using var embedding = (m_Worker.PeekOutput("embedding") as Tensor<float>).ReadbackAndClone(); | |
| embedding.DownloadToArray().CopyTo(Embedding, 0); | |
| return scores.DownloadToArray(); | |
| } | |
| void DisposeResources() | |
| { | |
| m_Worker?.Dispose(); | |
| m_Worker = null; | |
| m_InputTensor?.Dispose(); | |
| m_InputTensor = null; | |
| } | |
| public void Dispose() | |
| { | |
| DisposeResources(); | |
| } | |
| } | |
| } | |