File size: 13,010 Bytes
b5c8312 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | # 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** và **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.)
|