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****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.)