# Fitur Shadowing — Rencana Implementasi

> **Tujuan:** latihan **listening + shadowing** berbasis audio native. Berbeda dari
> Ondoku (teks → ucapan, dinilai Whisper), Shadowing membalik arah: **audio native dulu,
> teks belakangan**. Dirancang untuk dipakai hands-free di kereta/kantor — **berjalan
> offline**, dengan repeat & auto-continue otomatis.
>
> **Dibuat:** 2026-06-09 · **Status:** rencana (belum implementasi) ·
> **Catatan:** fitur ini menjadi **pondasi Tahap 5 (Mendengar/聴解)** — skema & pemutar
> audio di sini akan dipakai ulang untuk audio JLPT asli nanti.

---

## 1. Ringkasan keputusan (hasil tanya-jawab)

| Aspek | Keputusan |
|---|---|
| **Sumber teks** | Transkrip via **Whisper** dari 88 mp3, dijalankan sekali lewat **Artisan command** (deterministik setelah jalan). |
| **Lokasi audio** | **Tetap** di `frontend/public/audio/` (aset statis Next.js, mudah di-cache PWA). |
| **Strategi offline** | **Cache otomatis saat diputar** (CacheFirst via workbox) **+ tombol "Unduh untuk offline"** (pre-download satu sesi). |
| **Mode latihan** | **Toggle Repeat** (ulang 1 klip N×) **+ Auto-continue** (lanjut klip berikutnya) **+ jeda shadowing** (hening selama durasi klip agar user menirukan). Hands-free. |
| **Alur reveal** | **Dengar dulu (teks tersembunyi)** → user tap **"Tampilkan teks"** untuk verifikasi. Preferensi diingat. |
| **Penilaian** | **Tidak ada** — latihan mandiri murni (tanpa rekam/mikrofon/Whisper saat dipakai). Penuh offline. |
| **Navigasi/IA** | **Menu sendiri "Shadowing"** (di grup *Jelajahi*) → halaman daftar klip → halaman pemutar. |

---

## 2. Kondisi saat ini (temuan)

- **Audio:** 88 file `frontend/public/audio/001.mp3 … 088.mp3`. **Tanpa transkrip/metadata** apa pun (tidak ada JSON/CSV pendamping; tidak direferensikan di kode).
- **PWA aktif:** `@ducanh2912/next-pwa` + Workbox (`next.config.ts`), `aggressiveFrontEndNavCaching: true`, custom worker di `worker/index.ts` (push notifications). **Belum ada runtime-caching rule khusus untuk `/audio`** → perlu ditambah agar offline andal.
- **Whisper sudah dipakai:** `TranscribeController` (`OpenAI::audio()->transcribe`, `whisper-1`, `language: ja`) — pola yang sama dipakai untuk command transkripsi.
- **Ondoku** (`app/ondoku/[passageId]/page.tsx`) menyediakan referensi: segmentasi teks bunsetsu, karaoke highlight, kontrol play/pause. Logikanya **teks→ucapan**; Shadowing **kebalikannya**.
- **Pola Artisan command** mapan (`BackfillKanjiMetadata`: `--dry-run/--limit/--offset/--sleep`, progress bar).
- **Navigasi:** `LEARN_SUB_ITEMS` di `data/navigation.ts` adalah grid *Jelajahi* di halaman roadmap (tempat alami menambah entri "Shadowing").

---

## 3. Arsitektur data (backend)

Konten shadowing adalah **kurikulum bersama** (sama untuk semua user), jadi tabel sistem — bukan terikat user (mirip keputusan `reading_passages` di Tahap 4).

### Tabel `shadowing_clips`
```php
Schema::create('shadowing_clips', function (Blueprint $table) {
    $table->id();
    $table->string('audio_key')->unique();      // '001' … '088' (memetakan ke /audio/001.mp3)
    $table->string('title')->nullable();         // judul singkat (boleh diisi belakangan)
    $table->string('level')->nullable();         // 'N5'..'N1' bila diketahui (untuk Tahap 5)
    $table->text('transcript_ja')->nullable();   // hasil Whisper (teks Jepang)
    $table->text('furigana')->nullable();        // opsional, di-generate AI
    $table->text('translation_id')->nullable();  // terjemahan Indonesia, opsional (AI)
    $table->unsignedInteger('duration_ms')->nullable(); // durasi klip (untuk jeda shadowing)
    $table->unsignedInteger('sort_order')->default(0);
    $table->timestamp('transcribed_at')->nullable();
    $table->timestamps();
});
```

> **Kenapa `audio_key`, bukan path penuh?** Frontend menyusun URL `/audio/{audio_key}.mp3`.
> Memisahkan key dari lokasi memudahkan pindah ke backend/CDN nanti (Tahap 5) tanpa migrasi.

**Progres per-user (opsional, ringan):** `shadowing_progress` (`user_id`, `shadowing_clip_id`, `play_count`, `last_played_at`) — untuk menandai "sudah dilatih". *Bisa ditunda*; MVP boleh tanpa progres server (cukup `localStorage`), sesuai sifat offline. **Rekomendasi MVP: tunda tabel progres**, simpan status di `localStorage` agar 100% offline.

### Model
- `ShadowingClip` (fillable + casts `duration_ms:int`, `sort_order:int`, `transcribed_at:datetime`).

---

## 4. Transkripsi (Artisan command)

`php artisan nihongo:transcribe-shadowing {--dry-run} {--limit=0} {--only=} {--sleep=500} {--retranscribe}`

Alur per klip:
1. Pastikan baris `shadowing_clips` ada untuk tiap `audio_key` (`001…088`) — buat bila belum (idempotent, mirip seeder).
2. Lewati yang sudah punya `transcript_ja` kecuali `--retranscribe`.
3. Baca file dari `frontend/public/audio/{key}.mp3` (path relatif lintas-repo — command tahu lokasi frontend; **lihat §8 Risiko** soal path).
4. Kirim ke Whisper (`whisper-1`, `language: ja`) → simpan `transcript_ja`, set `transcribed_at`.
5. *(Opsional, flag terpisah/menyusul)* generate `furigana` + `translation_id` via `AiGenerativeService`.
6. Hitung `duration_ms` (via getID3/ffprobe bila tersedia; kalau tidak, kosongkan — frontend bisa membaca durasi dari elemen `<audio>` saat play).

Progress bar + `--sleep` antar panggilan (hindari rate-limit), `--limit/--only` untuk uji sebagian. `--dry-run` menampilkan rencana tanpa menulis.

> **Catatan biaya/waktu:** 88 klip pendek → sekali jalan, hasil permanen di DB. Tidak ada panggilan Whisper saat user memakai fitur (offline-friendly).

---

## 5. API (backend)

Semua di `/api/v1/`, file rute `routes/v1/shadowing.php` (mount setelah grup belajar).

| Method | Path | Guna |
|---|---|---|
| `GET` | `/shadowing/clips` | Daftar klip: `audio_key, title, level, duration_ms, has_text` (transcript tidak dikirim di list — ringan). |
| `GET` | `/shadowing/clips/{audio_key}` | Detail satu klip termasuk `transcript_ja, furigana, translation_id`. |

> List + detail dipisah agar **list cepat & cache-able offline**, transcript dimuat saat klip dibuka (dan ikut ter-cache PWA). Tidak ada endpoint tulis di MVP (progres via `localStorage`).

`ShadowingController` (`index`, `show`) + `ShadowingService` tipis (query + bentuk respons).

---

## 6. Frontend

### 6.1 Lokasi & navigasi
- `ROUTES.SHADOWING = '/shadowing'`.
- Entri baru di `LEARN_SUB_ITEMS` (`data/navigation.ts`): `{ icon: Headphones, label: 'Shadowing', path: ROUTES.SHADOWING }`.

### 6.2 Halaman daftar — `app/shadowing/page.tsx`
- Ambil `GET /shadowing/clips` (TanStack hook `use-shadowing`).
- Grid/daftar klip (nomor, judul bila ada, badge level bila ada, ikon "ada teks").
- Tombol **"Unduh semua untuk offline"** → fetch tiap `/audio/{key}.mp3` agar masuk cache Workbox (progress kecil). Status tersimpan di `localStorage`.

### 6.3 Halaman pemutar — `app/shadowing/[audioKey]/page.tsx` (inti fitur)
Elemen `<audio>` tunggal + state machine ringan. **Teks tersembunyi by default.**

Kontrol & mode:
- **Play/Pause**, **Prev/Next**, scrubber.
- **Toggle Repeat (×N)** — ulang klip yang sama N kali otomatis (N pilihan: 1/3/5/∞).
- **Toggle Auto-continue** — saat klip + (opsional jeda) selesai, lanjut klip berikutnya otomatis.
- **Toggle Jeda Shadowing** — setelah audio selesai, sisipkan **hening selama durasi klip** (atau ×faktor) agar user menirukan; baru lanjut/ulang. (`duration_ms` dari DB atau dari `audio.duration`.)
- **Kecepatan** 0.75/1.0/1.25 (latih telinga).
- **Tampilkan/Sembunyikan teks** — reveal `transcript_ja` (+ furigana/terjemahan bila ada). Preferensi "auto-hide untuk klip berikutnya" disimpan di `localStorage`.
- Semua kombinasi Repeat + Auto-continue + Jeda berjalan **hands-free** (penting untuk di kereta).

State machine pemutar (ringkas):
```
PLAYING → (audio end) → [Jeda Shadowing?] → SHADOW_PAUSE(durasi) → 
  → [Repeat tersisa?] → PLAYING(klip sama) 
  → else [Auto-continue?] → PLAYING(klip berikutnya) 
  → else → IDLE
```

### 6.4 Offline
- **Workbox runtime caching** untuk `/audio/*.mp3` (CacheFirst, `ExpirationPlugin` opsional). Karena memakai `@ducanh2912/next-pwa`, tambahkan via `workboxOptions.runtimeCaching` di `next.config.ts` (atau custom worker). Range-request audio ditangani `CacheableResponsePlugin` + `RangeRequestsPlugin`.
- **Respons API** (`/shadowing/clips` & detail) di-cache (NetworkFirst/StaleWhileRevalidate) agar daftar & transcript tersedia offline setelah sekali dibuka.
- **"Unduh untuk offline"** mengisi cache audio secara proaktif.
- Progres latihan (play count, terakhir dibuka) di **`localStorage`** → tanpa jaringan.

---

## 7. Tahapan kerja

1. **Skema & model** — migrasi `shadowing_clips` (+ `ShadowingClip`); seeder/`ensure` baris `001–088`.
2. **Transkripsi** — Artisan `nihongo:transcribe-shadowing` (Whisper) → isi `transcript_ja` + `duration_ms`. *(furigana/terjemahan menyusul/flag terpisah.)*
3. **API** — `ShadowingController` + rute + `ShadowingService`; resource ringkas untuk list.
4. **Frontend list** — route, nav entry, halaman daftar, hook TanStack.
5. **Frontend pemutar** — state machine (Repeat/Auto-continue/Jeda/Reveal/Speed), kontrol hands-free.
6. **Offline** — runtime caching audio + API di Workbox; tombol "Unduh untuk offline"; progres `localStorage`.
7. **Tes** — backend: command transkripsi (mock OpenAI), API list/detail, auth; frontend: typecheck/lint, smoke pemutar (mode repeat/continue).

---

## 8. Risiko & catatan

| Risiko | Mitigasi |
|---|---|
| **Path lintas-repo**: command backend membaca `frontend/public/audio`. | Buat path konfigurasi (`config('shadowing.audio_dir')`, default `../frontend/public/audio` relatif ke base_path). Bila repo terpisah saat deploy, override via `.env`. *(Audio tetap disajikan frontend; backend hanya butuh akses file saat transkripsi sekali.)* |
| **Akurasi Whisper** pada klip pendek/aksen. | Simpan mentah; sediakan `--retranscribe` + kemampuan edit manual (admin/seed override) belakangan. Teks hanya alat verifikasi, bukan penilaian. |
| **Caching audio Workbox** + range requests (seek). | Pakai `RangeRequestsPlugin` + `CacheableResponsePlugin`; uji seek offline. |
| **Ukuran cache** (88 mp3). | Klip pendek; "Unduh untuk offline" opsional, bukan paksa. `ExpirationPlugin` membatasi entri bila perlu. |
| **`duration_ms` untuk jeda shadowing** bila ffprobe tak ada. | Fallback: baca `audioEl.duration` di klien saat pertama play; simpan ke `localStorage`. |
| **Biaya Whisper**. | Sekali jalan 88 klip; tanpa panggilan saat runtime user. |

---

## 9. Hubungan dengan Tahap 5 (Mendengar/聴解)

Fitur ini **sengaja menjadi pondasi** Tahap 5:
- **Skema `shadowing_clips`** (audio_key, transcript, level, duration) dapat diperluas untuk **audio JLPT asli** (tinggal isi `level` + tambah tabel pertanyaan pemahaman → komponen `listening` di `AssessmentService`).
- **Pemutar audio + caching offline** dipakai ulang untuk soal listening.
- Saat Tahap 5: tambah `listening_questions` (mirip `reading_questions`) yang menunjuk ke klip, lalu `AssessmentService` menambah komponen `listening` (audio → MCQ pemahaman).

> Setelah fitur Shadowing selesai: **lanjut Tahap 5** dan **hapus kondisi TEMP** pada tombol Final Assessment (`app/learn/page.tsx`) — kembalikan versi bergerbang yang sudah disimpan dalam komentar.


# Coba 1 klip dulu, lihat hasilnya
php artisan nihongo:transcribe-shadowing --limit=1 --show

# Kalau bagus, naikkan ke 3, lalu 5
php artisan nihongo:transcribe-shadowing --limit=3 --show
php artisan nihongo:transcribe-shadowing --limit=5 --show
