chore: drop Unity no-LLM PoC-1 (superseded by native OpenXR coexistence spec)
This commit is contained in:
parent
cc3d066854
commit
ce2b99948d
117
docs/ONDEVICE.md
117
docs/ONDEVICE.md
|
|
@ -1,117 +0,0 @@
|
||||||
# ONDEVICE — PoC-1 na Meta Quest 3
|
|
||||||
|
|
||||||
Cel buildu dla pierwszego uruchomienia na urządzeniu. Spójny z `ROADMAP.md`.
|
|
||||||
|
|
||||||
## 1. Cel PoC-1
|
|
||||||
Uruchomić na Quest 3 minimalną aplikację, która w **passthrough** wyświetla i
|
|
||||||
**wypowiada** (Piper przez sherpa-onnx) polskie polecenia, oraz **zmierzyć
|
|
||||||
wydajność w realu**. **Bez żywego LLM na urządzeniu** (decyzja). **Bez STT.**
|
|
||||||
|
|
||||||
## 2. Zakres
|
|
||||||
W zakresie:
|
|
||||||
- bank poleceń wygenerowany offline (z istniejącego `ipin_vr`),
|
|
||||||
- TTS na urządzeniu (sherpa-onnx + głos polski),
|
|
||||||
- minimalne UI w passthrough (panel z tekstem + trigger „następne"),
|
|
||||||
- instrumentacja i pomiar wydajności.
|
|
||||||
|
|
||||||
Poza zakresem PoC-1: LLM na urządzeniu, STT, ocena wykonania, dopracowane UI/MR,
|
|
||||||
obiekty 3D. (To etap 2/3 wg `ROADMAP.md`.)
|
|
||||||
|
|
||||||
## 3. Runtime — decyzja
|
|
||||||
**Unity** dla PoC-1.
|
|
||||||
- Uzasadnienie: najszybsza droga do działającego passthrough + TTS; narzut RAM
|
|
||||||
Unity (~1 GiB) jest nieistotny, bo bez LLM cały budżet spokojnie mieści się w
|
|
||||||
limicie 5,75 GiB. Meta XR SDK daje passthrough „z pudełka", a sherpa-onnx ma
|
|
||||||
gotowe wtyczki do Unity.
|
|
||||||
- Cel docelowego produktu pozostaje **natywny OpenXR** (lżejszy, gdy dojdzie LLM).
|
|
||||||
- To jedyna łatwo odwracalna decyzja architektoniczna tego dokumentu; reszta nie
|
|
||||||
zależy od wyboru runtime.
|
|
||||||
|
|
||||||
## 4. Komponenty
|
|
||||||
|
|
||||||
### 4.1 Bank poleceń (offline)
|
|
||||||
Rozszerzyć `ipin_vr` o podkomendę `export`, która zrzuca bank gotowych,
|
|
||||||
poprawnych poleceń do JSON (reużywamy zweryfikowanego generatora — gramatyka
|
|
||||||
gwarantowana u źródła).
|
|
||||||
|
|
||||||
Wywołanie:
|
|
||||||
```
|
|
||||||
python -m ipin_vr export --per-level 300 --out commands.json [--seed N]
|
|
||||||
```
|
|
||||||
Format pliku:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"meta": {"version": "1", "per_level": 300},
|
|
||||||
"commands": [
|
|
||||||
{"level": 1, "text": "Połóż jabłko na stole."},
|
|
||||||
{"level": 2, "text": "Umieść niebieskie jabłko na stole."},
|
|
||||||
{"level": 3, "text": "Połóż zieloną książkę na półce i umieść klucz na biurku."}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Wymóg: deduplikacja w obrębie poziomu; `--per-level` to liczba unikatów na poziom
|
|
||||||
(jeśli przestrzeń jest mniejsza, tyle ile się da). Plik trafia do assetów aplikacji.
|
|
||||||
|
|
||||||
### 4.2 TTS na urządzeniu
|
|
||||||
- Biblioteka: **sherpa-onnx** przez wtyczkę Unity (np. `Ponyu-dev/Unity-Sherpa-ONNX`
|
|
||||||
lub `EitanWong/com.eitan.sherpa-onnx-unity`).
|
|
||||||
- Głos: **`vits-piper-pl_PL-gosia-medium`** (z releasu `tts-models` sherpa-onnx;
|
|
||||||
zawiera `.onnx`, `tokens.txt`, `espeak-ng-data`).
|
|
||||||
- ABI: **arm64-v8a**.
|
|
||||||
- Haczyk do obsłużenia: modele w `StreamingAssets` są spakowane w APK — przed
|
|
||||||
użyciem skopiować do ścieżki zapisywalnej i podać tę nową ścieżkę do sherpa-onnx
|
|
||||||
(wtyczki zwykle robią to same; zweryfikować).
|
|
||||||
|
|
||||||
### 4.3 Passthrough UI
|
|
||||||
- Passthrough przez **Meta XR SDK** (building block / komponent passthrough).
|
|
||||||
- Jeden panel (world-space lub HUD) z tekstem polecenia, czcionka z polskimi znakami.
|
|
||||||
- Trigger „następne polecenie": przycisk kontrolera albo prosty przycisk na panelu.
|
|
||||||
|
|
||||||
## 5. Podział pracy: kod vs Unity Editor
|
|
||||||
Część generowalna kodem (Claude Code):
|
|
||||||
- podkomenda `export` w `ipin_vr` (+ test),
|
|
||||||
- skrypty C# (ładowanie `commands.json`, wybór poziomu, wyświetlanie tekstu,
|
|
||||||
wywołanie TTS, obsługa triggera),
|
|
||||||
- `ondevice/README.md` z instrukcją złożenia projektu.
|
|
||||||
|
|
||||||
Część ręczna w Unity Editorze (nie da się w pełni wygenerować plikami):
|
|
||||||
- utworzenie projektu Unity (aktualne LTS) i konfiguracja Androida/OpenXR,
|
|
||||||
- instalacja Meta XR SDK i wtyczki sherpa-onnx,
|
|
||||||
- włączenie passthrough, podpięcie skryptów do sceny, import głosu i `commands.json`,
|
|
||||||
- build i sideload APK na Quest.
|
|
||||||
|
|
||||||
Skrypty C# mają być samodzielne i podpinalne, nie zakładać konkretnej hierarchii sceny.
|
|
||||||
|
|
||||||
## 6. Przepływ aplikacji
|
|
||||||
1. Start: passthrough on, wczytaj `commands.json` z assetów.
|
|
||||||
2. Ustaw poziom (domyślnie 1; prosty przełącznik 1/2/3).
|
|
||||||
3. Pokaż losowe polecenie danego poziomu na panelu.
|
|
||||||
4. Wypowiedz je (sherpa-onnx).
|
|
||||||
5. Trigger „następne" → wróć do kroku 3.
|
|
||||||
|
|
||||||
## 7. Pomiar wydajności (właściwy cel PoC-1)
|
|
||||||
Zmierzyć i zanotować:
|
|
||||||
- PSS aplikacji względem limitu 5,75 GiB,
|
|
||||||
- stabilność klatek przy włączonym passthrough (spadki/utracone klatki),
|
|
||||||
- latencja TTS: od wywołania do pierwszego dźwięku oraz czas pełnej syntezy,
|
|
||||||
- RAM zajmowany przez model głosu,
|
|
||||||
- moment wejścia w throttling termiczny przy dłuższym użyciu,
|
|
||||||
- zużycie baterii.
|
|
||||||
|
|
||||||
Narzędzia: OVR Metrics Tool, Perfetto, `logcat` (m.in. `lowmemorykiller`).
|
|
||||||
|
|
||||||
## 8. Środowisko developerskie
|
|
||||||
Najpierw **sanity check bez kodu**: wgrać prebudowane sherpa-onnx TTS APK z
|
|
||||||
`pl_PL-gosia` na telefon z Androidem (lub sideload na Quest) i potwierdzić jakość
|
|
||||||
polskiego głosu. Dopiero potem budować aplikację Unity. Iteracja skryptów/logiki
|
|
||||||
najszybsza na telefonie Snapdragon; pomiary docelowe — na Queście.
|
|
||||||
|
|
||||||
## 9. Definition of done
|
|
||||||
- Apka na Queście w passthrough pokazuje i wypowiada polskie polecenia z banku.
|
|
||||||
- Trigger „następne" działa; poziomy 1–3 dostępne.
|
|
||||||
- Zebrany komplet metryk z sekcji 7 (choćby zgrubnie) — to jest wynik PoC-1.
|
|
||||||
|
|
||||||
## 10. Dalej (poza PoC-1)
|
|
||||||
- PoC-2: dołożyć llama.cpp na urządzeniu i zmierzyć ponownie (żywy LLM).
|
|
||||||
- Etap 3: STT + ocena wykonania, sesje, statystyki.
|
|
||||||
- Ewentualne przejście runtime na natywny OpenXR pod docelowy budżet RAM z LLM.
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
# ondevice — składanie projektu Unity (PoC-1, Meta Quest 3)
|
|
||||||
|
|
||||||
Skrypty C# w `Scripts/` są gotowe do podpięcia. Poniżej lista ręcznych kroków
|
|
||||||
w Unity Editorze (nie da się ich wygenerować plikami). Odpowiada §5 z `docs/ONDEVICE.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Wymagania wstępne
|
|
||||||
|
|
||||||
| Narzędzie | Wersja |
|
|
||||||
|---|---|
|
|
||||||
| Unity | 2022 LTS (6000.x) lub 2023 LTS |
|
|
||||||
| Android Build Support + OpenJDK | przez Unity Hub |
|
|
||||||
| Meta XR SDK | aktualny (via Package Manager → Add by git URL) |
|
|
||||||
| sherpa-onnx Unity plugin | patrz krok 4 |
|
|
||||||
| TextMeshPro | com.unity.textmeshpro (wbudowany w Unity) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Nowy projekt Unity
|
|
||||||
|
|
||||||
1. Utwórz projekt **3D (URP)** lub **3D (Built-in)**.
|
|
||||||
2. Otwórz **Edit → Project Settings → Player → Android**:
|
|
||||||
- Target Architecture: **ARM64**
|
|
||||||
- Minimum API: **29** (Quest 3)
|
|
||||||
- Target API: **32** (lub latest)
|
|
||||||
3. Otwórz **Edit → Project Settings → XR Plug-in Management → Android**:
|
|
||||||
- Zaznacz **OpenXR**
|
|
||||||
- W zakładce OpenXR dodaj **Meta Quest feature set**
|
|
||||||
4. Otwórz **Edit → Project Settings → XR Plug-in Management → OpenXR**:
|
|
||||||
- Zaznacz **Meta Quest Support** i **Hand Tracking Subsystem** (opcjonalnie).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Meta XR SDK — passthrough
|
|
||||||
|
|
||||||
1. W **Package Manager** dodaj Meta XR SDK:
|
|
||||||
```
|
|
||||||
https://npm.developer.oculus.com (scope: com.meta.xr)
|
|
||||||
```
|
|
||||||
Zainstaluj: **Meta XR All-in-One SDK** lub minimum **Meta XR Core SDK**.
|
|
||||||
2. Utwórz scenę z **OVRCameraRig** (lub użyj prefabu z SDK).
|
|
||||||
3. Na `OVRManager` włącz **Passthrough Support = Required**.
|
|
||||||
4. Dodaj komponent **OVRPassthroughLayer** do kamery (Type: Underlay).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Bank poleceń — generowanie i import
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# W repo ipin-vr (Python):
|
|
||||||
python -m ipin_vr export --per-level 300 --out commands.json --seed 42
|
|
||||||
```
|
|
||||||
|
|
||||||
Następnie:
|
|
||||||
1. Utwórz katalog **Assets/StreamingAssets/** (jeśli nie istnieje).
|
|
||||||
2. Skopiuj `commands.json` do **Assets/StreamingAssets/commands.json**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. sherpa-onnx — instalacja wtyczki i modelu głosu
|
|
||||||
|
|
||||||
### 4a. Wtyczka
|
|
||||||
|
|
||||||
Wybierz jedną z opcji:
|
|
||||||
|
|
||||||
**Opcja A — ręczne .aar/.dll z wydania sherpa-onnx:**
|
|
||||||
1. Pobierz z https://github.com/k2-fsa/sherpa-onnx/releases pliki dla Unity
|
|
||||||
(szukaj `unity-*` w Assets danego wydania).
|
|
||||||
2. Umieść `.dll` (Windows/Editor) i `.aar` (Android) w `Assets/Plugins/`.
|
|
||||||
|
|
||||||
**Opcja B — pakiet UPM (community):**
|
|
||||||
```
|
|
||||||
https://github.com/EitanWong/com.eitan.sherpa-onnx-unity.git
|
|
||||||
```
|
|
||||||
Dodaj przez **Package Manager → Add package from git URL**.
|
|
||||||
|
|
||||||
### 4b. Model głosu polskiego
|
|
||||||
|
|
||||||
1. Pobierz `vits-piper-pl_PL-gosia-medium.tar.bz2` z:
|
|
||||||
https://github.com/k2-fsa/sherpa-onnx/releases/tag/tts-models
|
|
||||||
2. Rozpakuj — otrzymasz katalog `vits-piper-pl_PL-gosia-medium/` zawierający:
|
|
||||||
- `pl_PL-gosia-medium.onnx`
|
|
||||||
- `tokens.txt`
|
|
||||||
- `espeak-ng-data/` (katalog)
|
|
||||||
3. Skopiuj cały katalog do **Assets/StreamingAssets/**:
|
|
||||||
```
|
|
||||||
Assets/StreamingAssets/
|
|
||||||
commands.json
|
|
||||||
vits-piper-pl_PL-gosia-medium/
|
|
||||||
pl_PL-gosia-medium.onnx
|
|
||||||
tokens.txt
|
|
||||||
espeak-ng-data/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4c. Aktywacja silnika TTS w kodzie
|
|
||||||
|
|
||||||
Po zainstalowaniu wtyczki odkomentuj bloki oznaczone `[SHERPA]` w `TtsManager.cs`:
|
|
||||||
- `using SherpaOnnx;` (lub odpowiednia przestrzeń nazw wtyczki)
|
|
||||||
- blok konfiguracji `OfflineTtsConfig` w `InitCoroutine`
|
|
||||||
- blok `_tts.Generate(...)` w `SpeakCoroutine`
|
|
||||||
|
|
||||||
**Uwaga na espeak-ng-data:** Weryfikuj, czy Twoja wtyczka kopiuje `espeak-ng-data`
|
|
||||||
automatycznie ze StreamingAssets. Jeśli nie — spakuj katalog jako `.zip` i rozpakowuj
|
|
||||||
w `TtsManager.EnsureEspeakData()`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Scena — podpinanie skryptów
|
|
||||||
|
|
||||||
### 5a. Obiekty w hierarchii
|
|
||||||
|
|
||||||
```
|
|
||||||
Scene
|
|
||||||
├── OVRCameraRig ← z Meta XR SDK
|
|
||||||
│ └── CenterEyeAnchor
|
|
||||||
├── SessionManager ← pusty GameObject
|
|
||||||
│ ├── CommandBank.cs
|
|
||||||
│ ├── TtsManager.cs
|
|
||||||
│ └── SessionController.cs
|
|
||||||
└── Canvas (World Space) ← panel UI
|
|
||||||
├── LoadingPanel
|
|
||||||
│ └── TMP_Text "Ładowanie..."
|
|
||||||
└── SessionPanel
|
|
||||||
├── TMP_Text "CommandText" ← główny tekst polecenia
|
|
||||||
├── Button "Next" ← trigger "następne"
|
|
||||||
└── LevelButtons
|
|
||||||
├── Button "L1"
|
|
||||||
├── Button "L2"
|
|
||||||
└── Button "L3"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5b. Inspector — SessionController
|
|
||||||
|
|
||||||
| Pole | Przypisz |
|
|
||||||
|---|---|
|
|
||||||
| Command Bank | GameObject SessionManager → CommandBank |
|
|
||||||
| Tts Manager | GameObject SessionManager → TtsManager |
|
|
||||||
| Command Text | TMP_Text CommandText |
|
|
||||||
| Loading Panel | GameObject LoadingPanel |
|
|
||||||
| Session Panel | GameObject SessionPanel |
|
|
||||||
| Next Button | Button Next |
|
|
||||||
| Start Level | 1 |
|
|
||||||
|
|
||||||
### 5c. Przyciski poziomu — OnClick
|
|
||||||
|
|
||||||
Każdy przycisk L1/L2/L3:
|
|
||||||
- Cel: SessionManager → SessionController
|
|
||||||
- Funkcja: `SetLevel(1)` / `SetLevel(2)` / `SetLevel(3)`
|
|
||||||
|
|
||||||
### 5d. TMP_Text — czcionka z polskimi znakami
|
|
||||||
|
|
||||||
Domyślna czcionka TMP może nie zawierać wszystkich polskich znaków. Aby to naprawić:
|
|
||||||
1. Pobierz czcionkę z polskim zestawem (np. Noto Sans Regular).
|
|
||||||
2. Utwórz **TMP Font Asset** (**Window → TextMeshPro → Font Asset Creator**),
|
|
||||||
zaznaczając znaki Latin Extended-A i Latin Supplement.
|
|
||||||
3. Przypisz nowy Font Asset do komponentu TMP_Text.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Build i sideload na Quest 3
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# W Unity Editor:
|
|
||||||
# File → Build Settings → Android → Switch Platform
|
|
||||||
# Player Settings → Package Name: com.twojanazwa.ipinvr
|
|
||||||
# File → Build And Run (upewnij się, że Quest 3 podłączony przez USB + tryb dev)
|
|
||||||
```
|
|
||||||
|
|
||||||
Lub zbuduj APK i sideloaduj ręcznie:
|
|
||||||
```bash
|
|
||||||
adb install -r ipin-vr.apk
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Pomiar wydajności (cel PoC-1)
|
|
||||||
|
|
||||||
Po uruchomieniu na Queście zbierz metryki wg §7 z `docs/ONDEVICE.md`:
|
|
||||||
- **OVR Metrics Tool** (sideload z Meta) — FPS, CPU/GPU load, RAM
|
|
||||||
- **adb logcat** — `lowmemorykiller`, błędy sherpa-onnx
|
|
||||||
- **Perfetto** — latencja TTS od wywołania `Speak()` do pierwszego dźwięku
|
|
||||||
|
|
||||||
Metryki docelowe: PSS < 5,75 GiB, FPS stable 72/90, latencja TTS < 1 s.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Sanity check bez Unity (zalecane najpierw)
|
|
||||||
|
|
||||||
Zanim zaczniesz budować projekt, przetestuj głos na telefonie z Androidem:
|
|
||||||
1. Pobierz oficjalne APK TTS ze strony sherpa-onnx.
|
|
||||||
2. Sideloaduj i sprawdź jakość polskiego głosu `gosia-medium`.
|
|
||||||
3. Dopiero potem wchodź w pełny projekt Unity.
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
// CommandBank.cs — ładuje commands.json z StreamingAssets, udostępnia losowe
|
|
||||||
// polecenia per-level. Samoistny MonoBehaviour; podepnij do dowolnego GameObject.
|
|
||||||
//
|
|
||||||
// Plik commands.json generuj offline:
|
|
||||||
// python -m ipin_vr export --per-level 300 --out commands.json
|
|
||||||
// Następnie umieść go w Assets/StreamingAssets/commands.json.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Networking;
|
|
||||||
|
|
||||||
namespace IpinVr
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public class CommandEntry
|
|
||||||
{
|
|
||||||
public int level;
|
|
||||||
public string text;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
internal class BankMeta
|
|
||||||
{
|
|
||||||
public string version;
|
|
||||||
public int per_level;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
internal class BankData
|
|
||||||
{
|
|
||||||
public BankMeta meta;
|
|
||||||
public List<CommandEntry> commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CommandBank : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private string jsonFileName = "commands.json";
|
|
||||||
|
|
||||||
private readonly Dictionary<int, List<string>> _byLevel = new();
|
|
||||||
private bool _loaded;
|
|
||||||
|
|
||||||
public bool IsLoaded => _loaded;
|
|
||||||
|
|
||||||
/// <summary>Wczytaj bank asynchronicznie. onDone wywoływane po załadowaniu.</summary>
|
|
||||||
public void Load(Action onDone = null)
|
|
||||||
{
|
|
||||||
StartCoroutine(LoadCoroutine(onDone));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator LoadCoroutine(Action onDone)
|
|
||||||
{
|
|
||||||
string srcPath = Path.Combine(Application.streamingAssetsPath, jsonFileName);
|
|
||||||
string json;
|
|
||||||
|
|
||||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
||||||
// Na Androidzie StreamingAssets są wewnątrz APK — czytamy przez WebRequest.
|
|
||||||
using var req = UnityWebRequest.Get(srcPath);
|
|
||||||
yield return req.SendWebRequest();
|
|
||||||
if (req.result != UnityWebRequest.Result.Success)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[CommandBank] Błąd wczytywania {jsonFileName}: {req.error}");
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
json = req.downloadHandler.text;
|
|
||||||
#else
|
|
||||||
if (!File.Exists(srcPath))
|
|
||||||
{
|
|
||||||
Debug.LogError($"[CommandBank] Plik nie istnieje: {srcPath}");
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
json = File.ReadAllText(srcPath);
|
|
||||||
yield return null;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var data = JsonUtility.FromJson<BankData>(json);
|
|
||||||
if (data?.commands == null)
|
|
||||||
{
|
|
||||||
Debug.LogError("[CommandBank] Nie można sparsować commands.json");
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_byLevel.Clear();
|
|
||||||
foreach (var entry in data.commands)
|
|
||||||
{
|
|
||||||
if (!_byLevel.TryGetValue(entry.level, out var list))
|
|
||||||
_byLevel[entry.level] = list = new List<string>();
|
|
||||||
list.Add(entry.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
int total = data.commands.Count;
|
|
||||||
Debug.Log($"[CommandBank] Wczytano {total} poleceń (v{data.meta?.version})");
|
|
||||||
_loaded = true;
|
|
||||||
onDone?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Zwraca losowe polecenie dla danego poziomu, lub null jeśli brak.</summary>
|
|
||||||
public string GetRandom(int level)
|
|
||||||
{
|
|
||||||
if (!_loaded)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[CommandBank] Bank nie jest jeszcze wczytany");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!_byLevel.TryGetValue(level, out var list) || list.Count == 0)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[CommandBank] Brak poleceń dla poziomu {level}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return list[UnityEngine.Random.Range(0, list.Count)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CountForLevel(int level) =>
|
|
||||||
_byLevel.TryGetValue(level, out var l) ? l.Count : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
// SessionController.cs — główna logika aplikacji (ONDEVICE §6):
|
|
||||||
// Start → wczytaj bank → ustaw poziom → pokaż losowe polecenie → TTS
|
|
||||||
// → trigger "następne" → pokaż kolejne.
|
|
||||||
//
|
|
||||||
// Samoistny MonoBehaviour; podepnij do dowolnego GameObject w scenie.
|
|
||||||
// Wymagane referencje do uzupełnienia w Inspectorze:
|
|
||||||
// • commandBank — GameObject z CommandBank.cs
|
|
||||||
// • ttsManager — GameObject z TtsManager.cs
|
|
||||||
// • commandText — komponent TextMeshProUGUI z tekstem polecenia
|
|
||||||
// • loadingPanel — panel widoczny podczas ładowania (opcjonalnie)
|
|
||||||
// • sessionPanel — panel widoczny podczas sesji
|
|
||||||
// Przyciski poziomu: w Inspectorze podepnij OnClick → SetLevel(1/2/3).
|
|
||||||
// Trigger "następne": podepnij OnClick → OnNextTrigger().
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using TMPro;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
|
|
||||||
namespace IpinVr
|
|
||||||
{
|
|
||||||
public class SessionController : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("Komponenty (wymagane)")]
|
|
||||||
[SerializeField] private CommandBank commandBank;
|
|
||||||
[SerializeField] private TtsManager ttsManager;
|
|
||||||
[SerializeField] private TMP_Text commandText; // TextMeshPro — wymaga pakietu com.unity.textmeshpro
|
|
||||||
|
|
||||||
[Header("Panele UI (opcjonalne)")]
|
|
||||||
[SerializeField] private GameObject loadingPanel;
|
|
||||||
[SerializeField] private GameObject sessionPanel;
|
|
||||||
|
|
||||||
[Header("Przycisk 'Następne' (opcjonalny — można też wołać OnNextTrigger() z kodu)")]
|
|
||||||
[SerializeField] private Button nextButton;
|
|
||||||
|
|
||||||
[Header("Ustawienia")]
|
|
||||||
[SerializeField] [Range(1, 3)] private int startLevel = 1;
|
|
||||||
|
|
||||||
private int _currentLevel;
|
|
||||||
private bool _busy; // podczas TTS nie przyjmujemy triggera
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
_currentLevel = startLevel;
|
|
||||||
SetPanelsVisible(loading: true);
|
|
||||||
|
|
||||||
if (nextButton != null)
|
|
||||||
nextButton.onClick.AddListener(OnNextTrigger);
|
|
||||||
|
|
||||||
if (commandBank == null || ttsManager == null)
|
|
||||||
{
|
|
||||||
Debug.LogError("[Session] Brakuje referencji CommandBank lub TtsManager w Inspectorze");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commandBank.Load(() =>
|
|
||||||
ttsManager.Initialize(() =>
|
|
||||||
{
|
|
||||||
SetPanelsVisible(loading: false);
|
|
||||||
ShowNext();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
if (nextButton != null)
|
|
||||||
nextButton.onClick.RemoveListener(OnNextTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- publiczne API ---
|
|
||||||
|
|
||||||
/// <summary>Trigger "następne polecenie" — z przycisku kontrolera lub UI.</summary>
|
|
||||||
public void OnNextTrigger()
|
|
||||||
{
|
|
||||||
if (!_busy)
|
|
||||||
ShowNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Zmień poziom (1–3). Wywoływane przez przyciski poziomu w UI.</summary>
|
|
||||||
public void SetLevel(int level)
|
|
||||||
{
|
|
||||||
if (level < 1 || level > 3)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[Session] Nieprawidłowy poziom: {level}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_currentLevel = level;
|
|
||||||
Debug.Log($"[Session] Poziom → {level}");
|
|
||||||
if (!_busy)
|
|
||||||
ShowNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- logika wewnętrzna ---
|
|
||||||
|
|
||||||
private void ShowNext()
|
|
||||||
{
|
|
||||||
if (!commandBank.IsLoaded || !ttsManager.IsReady)
|
|
||||||
return;
|
|
||||||
|
|
||||||
string text = commandBank.GetRandom(_currentLevel);
|
|
||||||
if (text == null)
|
|
||||||
{
|
|
||||||
commandText.text = $"(brak poleceń dla poziomu {_currentLevel})";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commandText.text = text;
|
|
||||||
_busy = true;
|
|
||||||
if (nextButton != null) nextButton.interactable = false;
|
|
||||||
|
|
||||||
ttsManager.Speak(text, onDone: () =>
|
|
||||||
{
|
|
||||||
_busy = false;
|
|
||||||
if (nextButton != null) nextButton.interactable = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPanelsVisible(bool loading)
|
|
||||||
{
|
|
||||||
if (loadingPanel != null) loadingPanel.SetActive(loading);
|
|
||||||
if (sessionPanel != null) sessionPanel.SetActive(!loading);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue