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
- Dịch SRT Trung → Việt tự nhiên, đúng văn nói, súc tích.
- 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.
- Nhất quán tên riêng và cách xưng hô xuyên suốt phim.
- 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
@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
NameRegistrycũ nhưng tách bạch, có thể chỉnh tay quaglossary.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_audiobả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)
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_jsontext-only; dịch thử 1 cảnh.- M3 — Bước 2 Diarization: port pyannote gọn; gáncue.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.pyghé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
-Instruct4-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.)