Python (ipin_vr/export.py): - generate_bank(): levels 1–2 enumerated exhaustively (120/720 combos), level 3 random with dedup; all deduplicated per-level - run_export(): CLI --per-level / --out / --seed flags per ONDEVICE §4.1 - cli.py routes "export" subcommand before session args (backward-compat) - 14 new tests: format, dedup for all 3 levels, space caps (L1=120, L2=720), seed determinism, file output Unity scaffold (ondevice/Scripts/): - CommandBank.cs: loads commands.json from StreamingAssets (WebRequest on Android) - TtsManager.cs: sherpa-onnx integration with [SHERPA] stubs, StreamingAssets→ persistentDataPath copy, defensive Speak() with onDone callback - SessionController.cs: passthrough flow §6, level switching, busy guard on Next - ondevice/README.md: full manual Editor steps §5 (Unity setup, Meta XR SDK, sherpa-onnx install, scene wiring, font, build, metrics) pytest: 37/37 passed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
6.4 KiB
C#
169 lines
6.4 KiB
C#
// TtsManager.cs — synteza mowy przez sherpa-onnx (głos vits-piper-pl_PL-gosia-medium).
|
|
//
|
|
// Instalacja wtyczki (wybierz jedną):
|
|
// A) Oficjalna: skopiuj .dll / .aar z wydania sherpa-onnx do Assets/Plugins/
|
|
// https://github.com/k2-fsa/sherpa-onnx/releases (szukaj unity-*)
|
|
// B) UPM: https://github.com/EitanWong/com.eitan.sherpa-onnx-unity
|
|
//
|
|
// Po zainstalowaniu: odkomentuj bloki oznaczone [SHERPA] poniżej.
|
|
//
|
|
// Pliki modelu (StreamingAssets/vits-piper-pl_PL-gosia-medium/):
|
|
// pl_PL-gosia-medium.onnx
|
|
// tokens.txt
|
|
// espeak-ng-data/ (katalog)
|
|
// Pobierz ze: https://github.com/k2-fsa/sherpa-onnx/releases/tag/tts-models
|
|
// → vits-piper-pl_PL-gosia-medium.tar.bz2
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using UnityEngine;
|
|
using UnityEngine.Networking;
|
|
|
|
// [SHERPA] using SherpaOnnx; // dokładna przestrzeń nazw zależy od wtyczki
|
|
|
|
namespace IpinVr
|
|
{
|
|
public class TtsManager : MonoBehaviour
|
|
{
|
|
[Header("Ścieżki modelu (względem StreamingAssets)")]
|
|
[SerializeField] private string modelOnnx =
|
|
"vits-piper-pl_PL-gosia-medium/pl_PL-gosia-medium.onnx";
|
|
[SerializeField] private string tokensFile =
|
|
"vits-piper-pl_PL-gosia-medium/tokens.txt";
|
|
[SerializeField] private string espeakDataDir =
|
|
"vits-piper-pl_PL-gosia-medium/espeak-ng-data";
|
|
|
|
[Header("Odtwarzanie")]
|
|
[SerializeField] private AudioSource audioSource;
|
|
|
|
[Header("Parametry syntezy")]
|
|
[SerializeField] [Range(1, 4)] private int numThreads = 2;
|
|
[SerializeField] [Range(0.5f, 2.0f)] private float speed = 1.0f;
|
|
|
|
public bool IsReady { get; private set; }
|
|
|
|
// [SHERPA] private OfflineTts _tts;
|
|
|
|
private void Awake()
|
|
{
|
|
if (audioSource == null)
|
|
audioSource = gameObject.AddComponent<AudioSource>();
|
|
}
|
|
|
|
/// <summary>Kopiuje pliki z StreamingAssets i inicjuje silnik TTS.</summary>
|
|
public void Initialize(Action onReady = null)
|
|
{
|
|
StartCoroutine(InitCoroutine(onReady));
|
|
}
|
|
|
|
private IEnumerator InitCoroutine(Action onReady)
|
|
{
|
|
string[] files = { modelOnnx, tokensFile };
|
|
foreach (var rel in files)
|
|
yield return StartCoroutine(EnsureFile(rel));
|
|
|
|
// espeak-ng-data: sherpa-onnx Unity plugins zwykle obsługują to wewnętrznie.
|
|
// Jeśli Twoja wtyczka tego nie robi, spakuj katalog jako .zip i rozpakowuj tu.
|
|
yield return StartCoroutine(EnsureEspeakData());
|
|
|
|
// [SHERPA] Inicjalizacja silnika:
|
|
//
|
|
// string base = Application.persistentDataPath;
|
|
// var config = new OfflineTtsConfig
|
|
// {
|
|
// model = new OfflineTtsModelConfig
|
|
// {
|
|
// vits = new OfflineTtsVitsModelConfig
|
|
// {
|
|
// model = Path.Combine(base, modelOnnx),
|
|
// tokens = Path.Combine(base, tokensFile),
|
|
// dataDir = Path.Combine(base, espeakDataDir),
|
|
// },
|
|
// numThreads = numThreads,
|
|
// provider = "cpu",
|
|
// debug = false,
|
|
// },
|
|
// };
|
|
// _tts = new OfflineTts(config);
|
|
|
|
IsReady = true;
|
|
Debug.Log("[TtsManager] Gotowy (sherpa-onnx stub — odkomentuj bloki [SHERPA])");
|
|
onReady?.Invoke();
|
|
}
|
|
|
|
/// <summary>Syntezuje text i odtwarza. Wywołuje onDone po zakończeniu odtwarzania.</summary>
|
|
public void Speak(string text, Action onDone = null)
|
|
{
|
|
if (!IsReady)
|
|
{
|
|
Debug.LogWarning("[TtsManager] Nie zainicjowany — pomijam TTS");
|
|
onDone?.Invoke();
|
|
return;
|
|
}
|
|
if (audioSource.isPlaying)
|
|
audioSource.Stop();
|
|
StartCoroutine(SpeakCoroutine(text, onDone));
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
StopAllCoroutines();
|
|
audioSource.Stop();
|
|
}
|
|
|
|
private IEnumerator SpeakCoroutine(string text, Action onDone)
|
|
{
|
|
// [SHERPA] Zastąp ten blok wywołaniem sherpa-onnx:
|
|
//
|
|
// var audio = _tts.Generate(text, speakerId: 0, speed: speed);
|
|
// var clip = AudioClip.Create("tts", audio.samples.Length, 1,
|
|
// audio.sampleRate, false);
|
|
// clip.SetData(audio.samples, 0);
|
|
// audioSource.clip = clip;
|
|
// audioSource.Play();
|
|
// yield return new WaitUntil(() => !audioSource.isPlaying);
|
|
|
|
Debug.Log($"[TtsManager] (stub) Speak: \"{text}\"");
|
|
yield return new WaitForSeconds(0.1f); // zastępcze opóźnienie
|
|
onDone?.Invoke();
|
|
}
|
|
|
|
// --- kopiowanie plików ze StreamingAssets ---
|
|
|
|
private IEnumerator EnsureFile(string relative)
|
|
{
|
|
string dest = Path.Combine(Application.persistentDataPath, relative);
|
|
if (File.Exists(dest)) yield break;
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
|
|
string src = Path.Combine(Application.streamingAssetsPath, relative);
|
|
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
using var req = UnityWebRequest.Get(src);
|
|
yield return req.SendWebRequest();
|
|
if (req.result == UnityWebRequest.Result.Success)
|
|
File.WriteAllBytes(dest, req.downloadHandler.data);
|
|
else
|
|
Debug.LogError($"[TtsManager] Błąd kopiowania {relative}: {req.error}");
|
|
#else
|
|
if (File.Exists(src))
|
|
File.Copy(src, dest, overwrite: false);
|
|
yield return null;
|
|
#endif
|
|
}
|
|
|
|
private IEnumerator EnsureEspeakData()
|
|
{
|
|
// Opcja A: Twoja wtyczka sherpa-onnx ustawia dataDir wewnętrznie
|
|
// → nic nie rób tutaj.
|
|
// Opcja B: Spakuj espeak-ng-data/ jako espeak-ng-data.zip w StreamingAssets
|
|
// i tu go rozpakuj do Application.persistentDataPath.
|
|
// Opcja C: Użyj ścieżki Application.streamingAssetsPath bezpośrednio,
|
|
// jeśli Twoja wtyczka obsługuje adb-accessible paths.
|
|
Debug.Log("[TtsManager] espeak-ng-data: zweryfikuj obsługę w wybranej wtyczce — patrz README");
|
|
yield return null;
|
|
}
|
|
}
|
|
}
|