ipin-vr/ondevice/Scripts/TtsManager.cs

169 lines
6.4 KiB
C#
Raw Normal View History

// 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;
}
}
}