OmniSub / PLAN.md
STBack23's picture
Upload OmniSub source
b5c8312 verified
|
Raw
History Blame Contribute Delete
13 kB
# PLAN — OmniSub (dịch SRT ZH→VI bằng Qwen3-Omni)
Tài liệu thiết kế chi tiết. Quyết định đã chốt: **cloud-first (Colab L4 24GB)**, **Qwen3-Omni 4-bit
làm tất cả** (nghe giọng + nhìn cảnh + dịch), **bỏ regex xưng hô** của bản cũ.
---
## 0. Quyết định đã chốt
- **Model**: `Qwen/Qwen3-Omni-30B-A3B-Instruct` (bản **Instruct**, 4-bit).
- **OCR sửa phụ đề cháy hình** (Bước 1, tùy chọn): **giữ lại nhưng MẶC ĐỊNH TẮT** (`correct_ocr: false`),
bật khi cần qua config/CLI. Nguồn SRT hiện đã sạch.
- **Tên file ra**: giữ nếp cũ `*.vi.srt` (+ `*.vi.srt.report.json`).
## 1. Mục tiêu
1. Dịch SRT **Trung → Việt** tự nhiên, đúng văn nói, súc tích.
2. **Xưng hô đúng vai vế** nhờ:
- **Nghe giọng**: ai đang nói (diarization) + giới/tuổi/cảm xúc/vai vế (Omni nghe trực tiếp).
- **Nhìn ngữ cảnh**: frame video của cảnh đang dịch.
3. Nhất quán **tên riêng****cách xưng hô** xuyên suốt phim.
4. Phụ đề **vừa thời lượng** hiển thị (chars/giây, số dòng).
## 2. Bài học từ `Gemma-4` (giữ / bỏ)
**Giữ (port sạch):**
- `parse_srt`, `write_srt`, `subtitle_char_budget`, `_seconds_to_srt_time` — I/O SRT vững.
- `group_scenes`, `merge_adjacent_pairs` — gom cảnh, ghép cue vụn (sentence-aware, same-speaker).
- Pattern `chat_json` + `_extract_json` — parse JSON từ output model bền bỉ.
- Cấu trúc Colab + Google Drive (`Phim/`, `Cache/`).
**Bỏ / thay thế:**
-`RelationshipRegistry`, `_CJK_ROMANCE_RE`, `_CJK_FAMILY_RE`, `_VN_ANH_EM_RE`, `enforce_cue`
(hàng trăm dòng regex khóa anh/em vs mẹ/con) → thay bằng **hồ sơ giọng + ngữ cảnh đa phương thức**.
-`estimate_speaker_demographics` (wav2vec2 age/gender) → Omni nghe hiểu trực tiếp.
-`_patch_speechbrain_lazy` (vá Windows) → Colab Linux không cần.
- 🔄 Gemma 4 12B (llama.cpp) → **Qwen3-Omni-30B-A3B** (transformers).
## 3. Kiến trúc & luồng 5 bước
```
INPUT: video.mp4 + video.srt (ZH)
┌───────────────────────────────────────────────────────────────────────────────┐
│ Bước 1 Chuẩn bị SRT srt.py + scenes.py │
│ parse → gom cảnh → ghép cue vụn → char budget │
│ [tùy chọn] OCR sửa phụ đề cháy hình (correct.py) — MẶC ĐỊNH TẮT │
└───────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────┐
│ Bước 2 Phân tích người nói diarize.py │
│ pyannote community-1 → speaker turns → gán cue.speaker │
└───────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────┐
│ Bước 3 Hồ sơ giọng profiles.py │
│ Qwen3-Omni nghe 3–5 clip/speaker → VoiceProfile │
│ {gender, age_range, emotion, role_guess, register_hint} │
└───────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────┐
│ Bước 4 Dịch đa phương thức translate.py + scene_context.py │
│ Qwen3-Omni: audio cảnh + frames + cues + profiles + glossary (partial)│
│ → bản dịch VN + ghi chú xưng hô │
└───────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────┐
│ Bước 5 Hậu kiểm & xuất glossary.py + srt.py │
│ nhất quán tên/xưng hô → fit thời lượng → ghi file │
└───────────────────────────────────────────────────────────────────────────────┘
OUTPUT: video.vi.srt + video.vi.srt.report.json + glossary.json
```
| Bước | Module | Model | Mặc định |
|---|---|---|---|
| 1 — Chuẩn bị SRT | `srt.py`, `scenes.py`, `correct.py` | — / Omni (nếu bật OCR) | OCR **tắt** |
| 2 — Phân tích người nói | `diarize.py` | pyannote | bật |
| 3 — Hồ sơ giọng | `profiles.py` | Qwen3-Omni | bật |
| 4 — Dịch | `translate.py`, `scene_context.py` | Qwen3-Omni | bật |
| 5 — Hậu kiểm | `glossary.py` | — / Omni (rút gọn cue) | bật |
### Vì sao vẫn cần pyannote (Bước 2)
Qwen3-Omni hiện **chưa gán nhãn speaker ổn định** trên cả phim (xác nhận qua tài liệu kỹ thuật &
nghiên cứu HumanOmni-Speaker 2026 — cần adapter riêng). pyannote cho **nhãn nhất quán** để gắn
hồ sơ giọng (Bước 3) cho đúng người ở mọi cảnh.
### Vì sao một mô hình Omni cho Bước 3 & 4
Đưa **audio đoạn cảnh + frame + cue** cùng lúc → model "nghe" được tông giọng (ai đang nói với ai,
tình cảm hay gắt gỏng) và "thấy" bối cảnh → tự chọn xưng hô. Không còn luật cứng.
## 4. Module chi tiết (`src/omnisub/`)
| File | Trách nhiệm | Nguồn |
|---|---|---|
| `srt.py` | Dataclass `Cue`, parse/write SRT, char budget | Port từ bản cũ |
| `scenes.py` | `Scene`, `group_scenes`, `merge_adjacent_pairs` | Port, dọn lại |
| `diarize.py` | Wrapper pyannote → `SpeakerTurn`, gán `cue.speaker` | Port rút gọn |
| `correct.py` | Bước 1 — OCR sửa phụ đề cháy hình (**mặc định tắt**) | Port từ `correct_scene` cũ |
| `profiles.py` | ★ `VoiceProfile`; trích clip/speaker; gọi Omni nghe → hồ sơ | Mới |
| `scene_context.py` | Trích frame (ffmpeg), chọn audio đoạn cảnh | Port `extract_frame` + mới |
| `translate.py` | Dựng prompt đa phương thức/cảnh, gọi Omni, parse kết quả | Viết lại (bỏ regex) |
| `glossary.py` | ★ Nhất quán tên riêng + xưng hô theo dữ liệu (không regex) | Mới (thay NameRegistry) |
| `backends/base.py` | Interface `LLMBackend.chat_json(text, images, audio, …)` | Mới |
| `backends/transformers_qwen.py` | Qwen3-Omni qua HF transformers (Colab) | Mới |
| `pipeline.py` | Điều phối Bước 1→5, logging, progress, stop | Viết lại gọn |
| `cli.py` | Argparse CLI | Phỏng theo bản cũ |
### `VoiceProfile` (Bước 3) — cấu trúc đề xuất
```python
@dataclass
class VoiceProfile:
speaker: str # "SPEAKER_01" (khớp pyannote)
gender: str # nam | nữ | trẻ em | chưa rõ
age_range: str # trẻ em | thiếu niên | thanh niên | trung niên | lớn tuổi
emotion_baseline: str # vd: điềm đạm / nóng nảy / dịu dàng
role_guess: str # vd: "phụ nữ trẻ, có vẻ là người yêu của SPEAKER_02"
register_hint: str # gợi ý xưng hô VN: "nên dùng em với SPEAKER_02"
evidence: str # vì sao (model tự giải thích ngắn)
```
Hồ sơ này được nhét vào prompt dịch để Omni giữ xưng hô nhất quán theo từng nhân vật.
### Glossary (Bước 5) — thay regex bằng dữ liệu
- Thu thập **tên riêng** (token CJK lặp lại) → khóa **một** cách phiên âm VN, tái dùng (giữ ý tưởng
`NameRegistry` cũ nhưng tách bạch, có thể chỉnh tay qua `glossary.json`).
- **Cặp xưng hô** giữa các speaker do **model đề xuất** (không hard-code), lưu vào glossary và đưa lại
vào prompt các cảnh sau để khóa nhất quán — thay cho `RelationshipRegistry`.
## 5. Backend & mô hình (Colab L4 24GB)
- **Model**: `Qwen/Qwen3-Omni-30B-A3B-Instruct` (Bước 3 & 4).
- **Lượng hóa**: **4-bit** (bitsandbytes hoặc checkpoint AWQ/GPTQ Int4) → ~17–18GB, vừa L4 24GB
(chừa chỗ cho KV cache + encoder audio/vision). *Cần kiểm tra checkpoint 4-bit sẵn có trên HF.*
- **Audio**: resample 16 kHz mono (đã có sẵn `extract_audio` bản cũ). Đưa audio **sau** text trong prompt
(theo best practice Gemma/Qwen multimodal: ảnh trước text, audio sau text).
- **Vision**: frame trích bằng ffmpeg, đưa **trước** text.
- **Tham số sampling**: theo khuyến nghị Qwen (temperature ~0.7–1.0, top_p 0.95, top_k 64) — chốt khi test.
- **Drive**: cache model ở `Gemma/Cache/`, phim ở `Gemma/Phim/` (giữ nếp cũ).
## 6. Cấu hình (`config.yaml`)
```yaml
models:
omni: "Qwen/Qwen3-Omni-30B-A3B-Instruct"
quant: "4bit"
diarize: "pyannote/speaker-diarization-community-1"
translate:
source_lang: "Chinese"
target_lang: "Vietnamese"
chars_per_sec: 22
max_line_chars: 52
max_lines: 2
correct_ocr: false # Bước 1 — OCR sửa phụ đề cháy hình (mặc định tắt)
output_suffix: ".vi.srt" # giữ nếp cũ
scene:
max_gap: 1.5
max_cues: 4
max_dur: 20.0
profiling:
clips_per_speaker: 5
max_seconds_per_clip: 8
paths:
drive_root: "/content/drive/MyDrive/Gemma"
```
## 7. Lộ trình (milestones)
- **M1 — Bước 1 Chuẩn bị SRT**: scaffold thư mục, `srt.py`, `scenes.py`, `config.yaml`, `backends/base.py`.
*Done khi*: parse SRT → gom cảnh → write SRT chạy được; `python -m omnisub.cli --help`.
- **M2 — Backend Omni (Colab)**: notebook tải Qwen3-Omni 4-bit; `chat_json` text-only; dịch thử 1 cảnh.- **M3 — Bước 2 Diarization**: port pyannote gọn; gán `cue.speaker`; xuất nhãn.
- **M4 — Bước 3 Voice Profiling ★**: `profiles.py` — Omni nghe clip → `VoiceProfile`.
- **M5 — Bước 4 Dịch đa phương thức**: `translate.py` ghép audio+frame+cue+profile; **không regex**.
- **M6 — Bước 5 Hậu kiểm**: nhất quán tên/xưng hô; fit timing; xuất `.vi.srt` + report JSON.
- **M7 — Tài liệu & hoàn thiện**: tinh chỉnh prompt, hoàn thiện README + notebook Colab.
## 8. Rủi ro & phương án
| Rủi ro | Phương án |
|---|---|
| Omni 4-bit không có checkpoint sẵn / chất lượng tụt | Thử AWQ Int4; nếu kẹt → tách: Qwen3-Omni nghe giọng + Qwen3-VL dịch |
| L4 24GB tràn VRAM khi kèm audio+vision dài | Giảm số frame/cảnh, cắt audio ≤ 8s/clip, giảm visual token budget |
| pyannote chậm với phim dài | Chỉ diarize tới `max(cue.end)`; cân nhắc NeMo Sortformer |
| Diarization gán sai người nói | Cho phép sửa tay nhãn speaker trước Bước 3 |
| Mất kết nối/ngắt phiên Colab giữa chừng | Cache theo cảnh, cho chạy lại từ cảnh dở; lưu kết quả ra Drive |
## 9. Quyết định (đã chốt — xem mục 0)
- ✅ Qwen3-Omni **`-Instruct`** 4-bit.
- ✅ OCR (Bước 1) **mặc định tắt** (`correct_ocr: false`), bật khi cần.
- ✅ Tên file ra **`.vi.srt`**.
### Việc cần kiểm tra trước M2
- Xác nhận có checkpoint **Qwen3-Omni-30B-A3B-Instruct 4-bit / AWQ Int4** trên Hugging Face.
Nếu chưa có → phương án dự phòng: Qwen3-Omni (nghe giọng) + Qwen3-VL (dịch). (Xem mục 8.)