Spaces:
Running
Running
Commit History
23 days ago
-
fix(dub): VieNeu nạp được với onnxruntime mới — tải model thành file thật
Claude +1: Claude Opus 5.5Unverified signatureLỗi trên Space: "External data path escapes model directory". Cache Hugging Face để mỗi file ở một blob riêng, thư mục snapshot chỉ là symlink -> .onnx và vieneu_backbone_shared.data phân giải ra hai thư mục khác nhau, onnxruntime mới (tái hiện được với 1.29) từ chối nạp. VieNeu hỏng -> tiếng Việt câm. Nay VieNeu tải model bằng local_dir (file thật, nằm cạnh nhau) trong HF_HOME. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RwiJZLUb773B8NwksxfTU92f83e46
25 days ago
-
fix(dub): Edge bị chặn 403 không còn kéo VieNeu và cả tool câm theo
Claude +1: Claude Opus 5.5Unverified signatureLỗi trên Space: Microsoft trả 403 khi bắt tay WebSocket với Edge (edge-tts đã là bản mới nhất, nên không phải do thư viện cũ). Tiếng Việt cũng câm vì VieNeu từng nạp hỏng một lần và load_engine gán luôn Edge vào chỗ VieNeu tới lần restart. - load_engine: nạp hỏng thì Edge chỉ đứng TẠM, sau 5 phút nạp lại; nạp model trong khoá để job và việc làm sẵn giọng thử lúc khởi động không nạp song song. - synth_line: Edge cũng hỏng -> en/fil đọc bằng OmniVoice (local), giọng cùng giới tính với giọng Edge đang chọn. - Thêm env EDGE_TTS_PROXY để Edge đi qua proxy khi IP máy chủ bị chặn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RwiJZLUb773B8NwksxfTU98b45574 -
fix(dub): gen voice từ text báo lỗi thật thay vì trả audio câm
Claude +1: Claude Opus 5.5Unverified signatureEngine TTS hỏng (VieNeu nạp lỗi, Edge không gọi được) thì run_tts trả clip lặng 0.4s và job vẫn "xong" -> người dùng chỉ thấy audio 0 giây, không biết vì sao. - /api/dub/tts: clip ra lặng -> báo lỗi kèm nguyên nhân của từng engine. - Giao diện: job không có kết quả thì hiện thông báo lỗi. - synth_line: lỗi của Edge (khi đứng thay engine nạp hỏng) ghi dưới tên "edge", không đè mất lỗi nạp thật của engine gốc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RwiJZLUb773B8NwksxfTU9bb75713
-
fix(down list): trục trặc relay thì dừng ngay, đừng thử 4 site vô ích
Đo thật trên Space sau khi deploy: một link Douyin mất 228 GIÂY mới chịu báo lỗi, vì Worker relay không phản hồi mà code vẫn kiên nhẫn thử 4 site x 3 lần, mỗi lần chờ hết timeout 30s. Trước khi có nhánh site tải hộ thì nó hỏng nhanh, nên đây là hồi quy do chính tôi gây ra. Nay tách bạch "site nói không có video" với "trục trặc ở chính relay": - Gọi relay ném exception -> RelayProblem, dừng cả chuỗi ngay, kèm loại lỗi thật (vd OSError: ...) để biết đường mà sửa thay vì đoán. - Worker bản cũ trả JSON thiếu-tham-số -> cũng RelayProblem như trước. - Timeout POST qua relay 30s -> 12s. 120/120 test PASS (thêm test bắt đúng ca 228s này). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>bf478ef -
fix(down list): /api/download/start tải song song — chỗ người dùng thật sự chạm
Commit trước song song hoá `download_many`, nhưng api/download.py KHÔNG gọi hàm đó: nó có vòng lặp tuần tự riêng. Nên tối ưu không chạm tới người dùng. Nay `work()` tải nhiều link cùng lúc (trần 4, đổi bằng DOWNLOAD_WORKERS). Giữ nguyên: thứ tự kết quả theo thứ tự link nhập, huỷ giữa chừng, log/tiến độ từng link; thêm: 1 link ném exception không còn giết cả mẻ. Test mới bắt đúng lỗi này (4 link x 0.4s phải xong dưới 1.2s). 119/119 PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>8ae4ca6 -
feat(down list): port đường tải qua site tải hộ từ tool local down_ads
Bản web mới chỉ có yt-dlp + tikwm/ssstik, còn Douyin thì đi qua trang chia sẻ iesdouyin — mà trang đó đã bị ByteDance rút sạch dữ liệu (đo 2026-09), nên Douyin trên web coi như không tải được. Bản local down_ads đã giải bằng cách hỏi các SITE TẢI HỘ; nay bê phần đó sang. Thêm backend/app/services/downloader/webdl.py (port từ down_ads/core/webdl.py): - 4 site: savetik, tiktokio, snapsave, lovetik; parser CHUNG (quét URL giống link media, không dò thẻ/class) nên site đổi giao diện vẫn chạy. - Lưới cuối: không khớp tên miền CDN nào thì quét mọi URL rồi HỎI SERVER cái nào là video qua Content-Type — không cần biết trước tên miền. - Đo dung lượng thật bằng Range rồi lấy bản to nhất; nhãn "MP4 HD" của savetik trỏ file 576p nhỏ hơn cả link thường, không tin được. - Bóc link CDN gốc trong token JWT của snapcdn; giữ token nguyên vẹn lúc tải vì token dùng được đúng MỘT LẦN. - Giữ nhịp hỏi theo từng site (savetik chịu ~1 request/2.5s). Nối vào ydl.py: - douyin: site tải hộ -> iesdouyin …7196abe -
fix(cut): cắt 2+ video không còn 500; tách khúc tự động nhanh gấp ~3 lần
Lỗi: thả nhiều video một lượt lúc chưa có video nào -> `cur` là biến chụp lúc render nên mọi lần upload đều gửi session rỗng -> backend tạo mỗi video một phiên riêng. Lúc cắt, FE chỉ gửi phiên của video đầu nên video thứ 2 trở đi không tìm thấy file, ffprobe chết, cả request 500 "Internal Server Error". Một video thì không bao giờ dính. Sửa lỗi: - FE giữ sid chạy theo vòng lặp upload -> mọi video vào cùng một phiên; và gửi kèm session của TỪNG video khi cắt để các phiên cũ đã lỡ tách vẫn chạy. - resolve_src() chặn thoát thư mục phiên; probe() báo lý do đọc được thay vì CalledProcessError trống trơn. - execute-batch đưa việc nặng vào job: 1 video hỏng chỉ hỏng phần của nó, hỏng hết thì job báo lỗi thật. - Khúc end <= start trước đây bị ffmpeg nuốt im lặng (-frames:v âm = không giới hạn) và xuất ra clip dài tới hết video -> nay chặn. - alert hiện `detail` thay vì nguyên cục JSON. Tăng tốc (đo thật, video QC 1080x1920 30s): - Tách khúc tự động 11.9s -> 3.9s/video. PySceneDetect …07d3e14
-
feat(health): /api/health báo luôn model dịch đang chạy thật trên Space
Biến môi trường trên HF Space (Settings -> Variables) ĐÈ mặc định trong code, nên nhìn code không biết Space đang gọi model nào — deploy xong vẫn có thể chạy model cũ mà không ai hay. Nay /api/health trả chuỗi model, mức thinking và SỐ LƯỢNG key (không trả key) để kiểm tra được từ ngoài sau mỗi lần deploy. Đổi tên translate._models() -> model_chain() vì giờ có chỗ ngoài module gọi tới. Test: 95/95 PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>ef0bf7f -
feat(dịch): cạn quota cả 4 key thì xoay sang model khác thay vì rơi xuống Groq
Quota miễn phí của Gemini tính RIÊNG THEO TỪNG MODEL, nên khi cả 4 key đều 429 với model chính thì quota của model khác vẫn còn nguyên — dịch bằng model đó vẫn hơn hẳn rơi xuống Groq/deep-translator. - Thêm GEMINI_MODEL_CHAIN (mặc định gemini-3.1-flash-lite, gemini-3.5-flash, gemini-flash-latest): chuỗi model thử tiếp sau model chính + model dự phòng. Đo thật: bí danh '-latest' DÙNG CHUNG túi quota với bản đánh số (gemini-flash-lite-latest = 3.5-flash-lite, gemini-flash-latest = 3.8-flash), nên chuỗi chỉ xếp model khác túi mới có tác dụng. - Cooldown và con trỏ key nay tính theo CẶP (key, model) chứ không theo key: key cạn quota model này vẫn được ưu tiên dùng cho model khác. - _gemini vét hai lượt: lượt đầu chỉ dùng cặp còn khoẻ (đủ mọi model), lượt sau mới đụng cặp đang nghỉ — thà gọi model còn quota hơn đốt lại túi vừa cạn. - Cặp (key, model) bị 404 ("no longer available to new users") nhớ trong _km_404, khỏi thử lại; mỗi ngày mới xoá đi dò lại một lần. - check_gemini…db170a7 -
fix(dịch): sang ngày mới xoay lại từ key 1; chuyển cả 4 key sang gemini-3.5-flash-lite
Xoay key: trước đây con trỏ key dính lại ở key cuối còn sống của hôm trước, sáng hôm sau vẫn đốt tiếp key đó dù quota mọi key đã nạp lại, lại còn bị cooldown cũ (tối đa 1h) chặn. Nay `_reset_if_new_day()` so ngày theo giờ Thái Bình Dương — đúng mốc Google nạp quota ngày — sang ngày mới thì về key 1 và xoá sạch cooldown. Model: quota miễn phí tính RIÊNG THEO MODEL. Đo thực tế trên chính 4 key đang dùng: họ 'flash' (2.5-flash, 3.5-flash, 3.8-flash, gemini-flash-latest) chỉ 5 request/phút/key, họ 'flash-lite' được 15 — gấp 3. gemini-3.5-flash-lite bật thinkingLevel 'medium' dịch bám hạn ký tự và tự nhiên ngang 3.5-flash (~5s cho cả batch), nên lấy làm model chính cho cả 4 key, dự phòng gemini-flash-lite-latest. Gemini 3.x chỉnh suy nghĩ bằng `thinkingLevel`, 2.5 dùng `thinkingBudget` — gửi nhầm loại là 400, nên `_gemini_once` thử lần lượt rồi nhớ config dùng được theo từng model. Thêm GEMINI_THINKING_LEVEL (mặc định medium) để chỉnh không cần sửa code. Test: 94/94 PASS (thêm 2 test: re…c969932
-
fix(dub): cho cache giọng thử cũ hết hiệu lực sau khi sửa mã giọng
Cache giọng thử nằm trong STORAGE_DIR nên SỐNG QUA build lại, mà vân tay của nó chỉ gồm engine + ngôn ngữ + id giọng — không đổi theo bản sửa vừa rồi. Kết quả: deploy xong máy chủ vẫn phục vụ đúng file my_review 87 Hz (giọng đàn ông) đã cache, vì bản đó có tiếng thật nên mọi phép kiểm "file dùng được" đều thấy hợp lệ. Thêm _VOICE_CACHE_VER vào chuỗi vân tay, tăng lên 2 -> tên file cache đổi -> _drop_stale_previews xoá bản cũ và đọc lại. Lần sau sửa gì ảnh hưởng tiếng giọng đọc mà không nằm trong chuỗi vân tay thì tăng tiếp số này. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>9d606df -
fix(dub): bỏ file mã giọng tính sẵn — sai codec là đọc ra giọng người khác
Bản trước commit sẵn <wav>.v3turbo.codes.npy vào repo để lần chạy đầu khỏi chờ mã hoá. Sai. Mã giọng chỉ đúng với ĐÚNG bộ codec sinh ra nó, mà máy chủ tự tải model (HF_HOME=/tmp/hf_cache) nên không chắc trùng bản với máy dev — và khi lệch thì nó KHÔNG báo lỗi, chỉ đọc ra giọng người khác. Đo trên Space, cùng một my_review.wav (giọng nữ, F0 259 Hz): - dùng .npy tính ở máy dev -> 87 Hz, nghe ra đàn ông - để Space tự mã hoá -> 243 Hz, đúng giọng nữ Nay: xoá 2 file .npy khỏi repo, cache chuyển sang STORAGE_DIR/voice_codes/ (thư mục mã nguồn có thể chỉ-đọc trên máy chủ; STORAGE_DIR luôn ghi được và sống qua build lại). Tên file cache = tên mẫu + băm NỘI DUNG wav + VÂN TAY CODEC đang chạy, nên đổi model là cache tự hết hiệu lực thay vì đọc nhầm giọng. Lấy vân tay từ moss_audio_tokenizer_encode.onnx, đọc đầu+đuôi cho nhanh; không lấy được thì bỏ cache đĩa, chỉ giữ cache RAM. Vẫn giữ nguyên cái lợi ban đầu: mã hoá 1 lần rồi dùng lại, không clone lại ở mỗi dòng thoại. Kiểm: 3 dòng li…8568254 -
feat(dub): thêm giọng "My Review"; giọng clone chạy bằng trọng số mã hoá sẵn
Thêm my_review vào danh sách giọng có sẵn tiếng Việt (file mẫu my_review.wav). Clone trong VieNeu = chạy codec-encoder trên wav mẫu để ra mảng ref_codes. Bản cũ truyền ref_audio= nên việc này LẶP LẠI Ở MỖI DÒNG THOẠI. Đo thực tế: my_review ~8,5s lần đầu (nạp session codec) rồi ~1,5s/dòng; adam ~27s lần đầu — trong khi mảng codes chỉ 10-28 KB và không bao giờ đổi. Nay mã hoá một lần rồi tái sử dụng, 3 tầng: - Nhớ trong RAM theo (đường dẫn, mtime, cỡ file), có khoá vì synth_line chạy song song nhiều luồng. - File <wav>.v3turbo.codes.npy cạnh wav, sống qua cả lần khởi động lại. Hai giọng preset đã có sẵn file này trong repo -> lần chạy đầu trên máy chủ cũng không phải chờ mã hoá. - Không có thì mới mã hoá, xong ghi lại cho lần sau (thư mục chỉ-đọc thì bỏ qua, vẫn còn cache RAM). Sidecar để git thường chứ không qua LFS: file chỉ 10-28 KB mà code đọc lúc khởi động, LFS lỗi smudge sẽ thành file con trỏ. Mọi nhánh đều có đường lùi về ref_audio nên hỏng sidecar/đổi engine cũng không…9e4ef47 -
fix(báo cáo): ô "Số tài khoản được cấp" đếm nhầm bảng; buộc tải video qua relay trên Space
Ô "Số tài khoản được cấp" hiện 4 trong khi thực tế đã cấp hơn 80. Nó đang đếm bảng mã U00N, mà mã chỉ sinh khi người đó ĐĂNG NHẬP lần đầu hoặc khi quản trị tự tay thêm — duyệt từ hàng chờ thì không cấp mã. Đổi sang đếm bảng tài khoản đã duyệt, đúng bằng số dòng tab "Quản lý người dùng". Kèm nhóm sửa relay/HF đang làm dở: - Trên Space, tikwm/iesdouyin/Piped BẮT BUỘC đi qua TIKTOK_RELAY_URL; thiếu relay thì nêu lỗi rõ ràng thay vì gọi thẳng từ IP datacenter (hay bị chặn). Dev local vẫn gọi trực tiếp như cũ. - Worker relay đặt Referer theo đúng nguồn: CDN Douyin chống hotlink, sai Referer là 403. - CI loại thư mục deploy/ khỏi bản đẩy lên Space (commit tạm trong runner, main trên GitHub giữ nguyên). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>9dbe21e
-
Merge PR #3: nhan log tu 3 app desktop vao tab Quan ly thoi luong
huytran049Unverified signaturefeat(báo cáo): nhận log từ 3 app desktop vào tab "Quản lý thời lượng"8ed38c8 -
feat(báo cáo): nhận log từ 3 app desktop vào tab "Quản lý thời lượng"
Ba tool đóng gói cho Windows/Mac (down_ads, remove_subtitle, listing_image) nay bắn log về chung `POST /api/events`. Chúng dùng đúng schema và từ điển event của bản web nên ETL không phải sửa gì; ở đây chỉ thêm phần còn thiếu: - FEATURE_LABELS: 3 khoá mới, ĐỂ RIÊNG chứ không dùng lại khoá của bản web. Gộp chung thì bảng "Chi tiết theo tool" cộng số hai bản làm một, không còn biết bản nào chạy tốt. Muốn xem gộp thì lọc theo properties.client. - PHASE_LABELS: các pha máy chạy của bản app (tải video, xoá phụ đề, SEO, sửa ảnh hàng loạt...). - VALUE_LABELS: win/mac/linux, để cột "Thiết bị" đọc được thay vì mã thô. GIỮ ĐÚNG NGÀY CHO LÔ GỬI BÙ (sửa /api/events): App desktop có hàng đợi lưu đĩa — mất mạng thì gom lại, mở lần sau gửi bù. Trước đây mốc chỉnh lệch đồng hồ lấy theo event MỚI NHẤT trong lô, nên cả lô cũ bị kéo về hiện tại: việc làm tối qua bị đếm sang hôm nay. Nay lô nào có `sent_at` (giờ máy khách lúc bấm gửi) thì lấy nó làm mốc, và kéo luôn `server_ts` về đúng lúc việc …ea44972 -
fix(core): mọi tool đều giữ chỗ phiên khi chạy, thêm lớp chặn cho việc chạy đồng bộ
Nối tiếp 20e69c0 (mới bọc riêng Dịch/đổi giọng). Cut video, Xào video và Down list dùng chung cơ chế phiên nên vẫn dính: bộ dọn phiên quá hạn có thể xoá trúng thư mục job đang ghi. - Chuyển hàm bọc thành submit_for_session() dùng chung trong core/jobs.py; cả 4 tool (11 chỗ tạo job) đi qua đó, không còn jobs.submit trần. - Listing ảnh chạy THẲNG TRONG REQUEST, không qua job nên không đăng ký được vào sổ phiên-đang-bận. Thêm lớp chặn thứ hai ngay trong cleanup_old: quét sâu tìm dấu vết ghi mới, thấy là bỏ qua. Dừng sớm khi gặp file mới và có trần số mục để phiên vài trăm video không làm chậm đợt dọn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>c8ce1a0 -
fix(dub): dọn phiên quá hạn xoá trúng phiên đang chạy -> job chết, trả video gốc
Lỗi người dùng gặp: Không tổng hợp giọng được nên trả video gốc: ffmpeg ... anullsrc ... line_0000.wav returned non-zero exit status 254 và last_error trên Space: FileNotFoundError: '/data/tnt_work/dub_.../work/tts/line_0000.wav.mp3' Cả engine chính, Edge đọc đỡ, lẫn bước tạo file lặng đều không ghi nổi file -> thư mục làm việc đã BIẾN MẤT giữa job. Thủ phạm là cleanup_old: nó xét tuổi bằng mtime của THƯ MỤC PHIÊN, mà mtime thư mục chỉ đổi khi thêm/bớt mục con TRỰC TIẾP. Job ghi sâu bên trong (out/<video>/work/tts/...) không hề chạm tới mtime đó, nên phiên đang xử lý vẫn bị tính là quá hạn và bị rmtree ngay giữa chừng (sweep chạy ké mỗi lần có người mở phiên mới). - Đếm job đang chạy theo phiên (session_in_use); cleanup_old bỏ qua phiên bận. - cleanup_old cũng thôi đụng thư mục dùng chung (voice_previews) — không phải phiên thì không có "hạn". - Mọi bước ghi file TTS tự dựng lại thư mục nếu mất, để một sự cố tương tự chỉ làm chậm chứ không giết cả job. Co-Authored-By: Clau…20e69c0 -
feat(báo cáo): dựng lại tab thời lượng theo mẫu, CSV đọc được bằng tiếng Việt
Bảng nhân sự: cột "Xong" chỉ in SỐ việc xong, bỏ phần trăm đi kèm — cột này đứng cạnh cột "Số task" nên tỷ lệ tự đọc ra được, in thêm % chỉ làm ô chật. Thêm cột Team/BU tách từ tên đăng nhập kiểu "họ tên - team - bu". Giao diện dựng theo hai file mẫu đã chốt form: tiêu đề + thanh lọc (người dùng / công cụ / từ ngày / đến ngày) + ba nhóm ô chỉ số có gạch phân cách, rồi tới các card nội dung. Bật lại bảng "Tốc độ máy mỗi bước (B2)". Chỉ số mới trong ETL: số user đã dùng thật (không tính người chỉ ngó trang), dùng hàng tuần/hàng ngày — đếm theo mốc MỚI NHẤT trong khoảng lọc chứ không so với hôm nay, để xem lại báo cáo cũ vẫn đúng. CSV tải về: mỗi cột là một câu tiếng Việt, cục properties_json được bung thành "Bước máy: Bóc lời & dịch · Lần bấm chạy thứ: 1". Cột mã và JSON gốc dồn về cuối để vẫn dò ngược được. `?raw=1` giữ nguyên 11 cột schema v2 cho bên nào cần nạp vào công cụ khác. Bảng dịch để một chỗ ở labels.py, màn "Nhật ký chi tiết" đọc chung nên không lệch với file tải về; khoá …4ffa05a
-
feat(dub): en/fil — giọng dựng sẵn chạy Edge, OmniVoice chỉ lo phần clone
Đo thật trên Space: OmniVoice trên CPU mất 47.8s để đọc 2.83s tiếng (~17x thời gian thực), giọng khác 61.7s cho 2.92s (~21x). Quy ra video 60s có 50s lời thoại = 14-17 phút chỉ riêng khâu đọc, trong khi Edge làm cả video ~10s (8 luồng song song). Không đáng đánh đổi cho giọng dựng sẵn. Nay một ngôn ngữ chạy HAI engine: giọng dựng sẵn = Edge (nhanh như cũ), clone = OmniVoice — thứ Edge hoàn toàn không làm được, đáng để chờ. pick_engine chỉ chọn omni khi CÓ MẪU GIỌNG thật (speaker_wav hoặc clone-preset); bấm "Clone từ video" mà chưa cắt được mẫu thì vẫn Edge. /api/dub/voices trả riêng hai vế (engine/model_label cho giọng sẵn, clone_engine/clone_label/clone_slow cho clone). Giao diện báo trước ngay dưới nút chọn nguồn giọng: mỗi 1 giây lời thoại tốn ~15-20 giây xử lý, kẻo user tưởng treo máy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>54ef81a -
fix(dub): cache giọng thử đặt tên theo ĐỊNH NGHĨA giọng, không chỉ theo id
Bản instruct đã sửa lên Space rồi (nhãn giọng mới, last_error rỗng) nhưng /voice-preview vẫn trả y nguyên 220110 byte suốt 19 phút: đó là bản do Edge đọc đỡ hồi OmniVoice còn hỏng, đã nằm trong cache. Nó NGHE RA TIẾNG nên phép kiểm "file câm" thêm hôm trước không bắt được, mà STORAGE_DIR thì sống qua cả lần build lại -> nghe giọng Edge mãi dù model đã chạy đúng. Tên file cache nay kèm VÂN TAY của định nghĩa giọng (engine + chuỗi instruct thật): đổi engine hoặc sửa preset là bản cũ tự hết hiệu lực. Ghi bản mới thì xoá luôn bản cũ cùng giọng cho khỏi đọng rác. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>92f69cd -
fix(dub): instruct của OmniVoice phải là THẺ thuộc tính, không phải văn xuôi
Lỗi thật đọc được từ Space: ValueError: Unsupported instruct items found in "An adult female speaker with a soft, expressive voice and a British accent..." `instruct` không nhận mô tả tự do — nó là danh sách thẻ có sẵn ngăn bằng dấu phẩy (omnivoice/utils/voice_design.py), mỗi nhóm chọn đúng một thẻ: male|female · child|teenager|young adult|middle-aged|elderly · very low..very high pitch · whisper · <quốc gia> accent Mấy câu mô tả tôi tự nghĩ ra sai hết -> MỌI câu en/fil đều hỏng, phải nhờ Edge đọc đỡ nên nghe ra giọng Edge dù nhãn ghi OmniVoice. - Viết lại 10 giọng preset bằng thẻ hợp lệ. - BỎ ngữ điệu khỏi instruct: không có thẻ nào tả ngữ điệu (quảng cáo/đọc tin/kể chuyện), nhét vào là hỏng câu. Ngữ điệu vẫn dùng cho VieNeu. - fil không có thẻ chất giọng riêng -> bỏ trống, chốt tiếng bằng tham số `language` ("en"/"fil", khớp LANG_NAME_TO_ID của model). - Test khoá từ vựng lại: mọi preset chỉ được dùng thẻ hợp lệ, mỗi nhóm một thẻ, và instruct không được chứa chữ tả ng…7a822fa -
fix(dub): bỏ bản giọng thử CÂM đã nằm trong cache
Đo trên Space: /voice-preview trả đúng 19278 byte lặng ở MỌI lần gọi, thời gian 1.3s, mà `last_error` vẫn rỗng -> không hề có lần tổng hợp nào chạy. Bản lặng do lần TTS hỏng trước ghi ra đã nằm trong STORAGE_DIR và sống qua cả lần build lại, nên phép thử cũ ("có file và > 44 byte") luôn đúng và đường đi không bao giờ chạm tới engine — vừa câm vừa không ghi nổi lỗi. Nay cache chỉ tính là dùng được khi file KHÔNG câm; câm thì xoá, làm lại. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>268fabb -
fix(dub): TTS hỏng thì đọc bằng Edge, không trả cả video tiếng lặng
Test thật trên Space: /voice-preview trả về ĐÚNG 0.4s im lặng tuyệt đối — là nhánh _silence(0.4) của synth_line, tức OmniVoice ném lỗi ở MỌI câu. Hai lỗi lộ ra: 1. synth_line nuốt lỗi thành khoảng lặng và KHÔNG log gì. Hỏng 1 câu thì không sao, nhưng engine hỏng hệ thống (model chưa tải xong, thiếu RAM) thì cả video câm mà chẳng báo gì. Nay hỏng -> đọc lại bằng Edge TTS (online, gần như luôn có); chỉ khi Edge cũng hỏng mới trả khoảng lặng. 2. _ensure_preview cache luôn bản câm đó -> giọng thử câm vĩnh viễn kể cả sau khi model đã chạy được. Nay phát hiện file câm thì xoá + báo lỗi. Thêm đường chẩn đoán TỪ XA: ghi lỗi gần nhất của từng engine và trả qua /api/dub/voices (`last_error`) — trên Space không xem log tiện, mà triệu chứng thì chỉ là "nghe không thấy gì". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>35076e5 -
build(hf): cài OmniVoice trên Space, tránh gradio đè web stack
- Dockerfile cài `omnivoice` bằng --no-deps + đúng các gói đường suy luận (transformers>=5.3, accelerate, soundfile, pydub, librosa). Metadata của omnivoice khai cả `gradio`/`webdataset`/`tensorboardx` — chỉ dùng cho demo UI và huấn luyện. Dựng thử cây phụ thuộc: cài trọn bộ kéo gradio 6 -> starlette 1.x + fastapi 0.141 + pydantic 2.13 ĐÈ LÊN web stack đang chạy. - Thêm chốt an toàn ngay trong build: import fastapi/pydantic/cv2/numpy/ gradio_client/transformers/omnivoice — gãy thì HỎNG BUILD, không để chết lúc chạy trên Space. - Trọng số 3.3GB không nhúng vào ảnh: warm_voice_previews sẵn có chạy nền lúc khởi động sẽ nạp model (mạng nội bộ HF nhanh) -> job đầu tiên khỏi chờ. - Clone từ video: đưa luôn LỜI GỐC của đoạn mẫu (lấy `src` trong state.json, các câu chồng lấn cửa sổ 15s) sang OmniVoice qua ref_text. Không đưa thì nó tự tải Whisper large-v3-turbo ~1.6GB về bóc lại, trong khi transcript đã có sẵn từ bước ASR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.c…4a400b2 -
feat(dub): thêm OmniVoice (k2-fsa) làm engine TTS cho tiếng Anh & Filipino
en/fil trước chỉ có Edge TTS: giọng dựng sẵn, KHÔNG clone được. Nay chạy OmniVoice (k2-fsa/OmniVoice, 0.6B, 646 ngôn ngữ, ra 24 kHz) với 2 chế độ: - CLONE : có mẫu giọng (từ video hoặc file tải lên) -> ref_audio. - DESIGN : không mẫu -> "instruct" tả giọng bằng chữ. Gói sẵn 6 giọng en + 4 giọng fil (nam/nữ, Mỹ/Anh), ghép thêm ngữ điệu đang chọn (tự nhiên / quảng cáo / đọc tin / kể chuyện). CHƯA CÀI GÓI thì en/fil tự chạy Edge TTS y như cũ — quyết định lấy engine nào làm TRƯỚC bằng find_spec (không import torch trong request /voices), nên /voices trả đúng danh sách giọng của engine sẽ thật sự chạy. /api/dub/voices trả thêm engine + model_label + can_clone; giao diện bỏ hàm đoán isEdge(lang) — clone được hay không là do ENGINE quyết, không phải do tiếng gì. Edge chặn id giọng lạ (vd "omni:*" còn kẹt trong state trình duyệt) để không lỗi cả câu. Chỉnh bằng env: OMNI_DEVICE (cpu|cuda:0|mps), OMNI_STEPS (16), OMNI_ENABLED=0. Co-Authored-By: Claude Opus 5 <…6f5141d -
fix(analytics): bảng đóng góp hiện số thật; tool chưa chạy hết được 100đ "ít chạy lại"
Ảnh chụp thực tế cho thấy cột "Ít chạy lại" TOOL NÀO CŨNG 100 -> thành phần 20% mất tác dụng. Nguyên nhân: điểm kẹp sàn attempts_avg = 1.0, nên tool mở ra rồi bỏ (attempts_avg = 0) cũng hoá "ít chạy lại nhất". Bỏ kẹp sàn: chỉ so nghịch đảo giữa các tool THẬT SỰ có bấm chạy; chưa chạy lần nào = 0 điểm. Giao diện: 3 cột nhu cầu / ít chạy lại / sản phẩm giờ hiện SỐ THẬT (33 việc, 0.8 lần/việc, 26 sản phẩm) thay vì điểm quy đổi — thanh tỷ lệ vẫn vẽ theo điểm, rê chuột vào thanh mới hiện điểm. Tool chưa chạy ghi rõ "chưa chạy". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>4b0cace -
feat(analytics): xếp hạng tool đóng góp nhiều/ít nhất; tạm ẩn bảng B2
Điểm đóng góp mỗi tool (etl.score_contribution), trọng số nghiệp vụ chốt: 40% nhu cầu người dùng — số việc người giao cho tool 20% ít phải bấm chạy lại — A4 nghịch đảo, làm 1 phát ăn ngay = điểm cao 40% sản phẩm đầu ra — B1 Mỗi thành phần quy về thang 100 theo tool cao nhất TRONG KỲ ĐANG LỌC, nên điểm chỉ so được giữa các tool cùng khoảng thời gian. Tool chưa chạy việc nào = 0 điểm. Giao diện: thẻ "🏆 Tool đóng góp nhiều nhất / ít nhất" — 2 ô nổi bật + bảng đủ 3 điểm thành phần (có thanh tỷ lệ) để đối chiếu vì sao ra thứ hạng đó. Tạm ẩn (comment) bảng "Tốc độ máy mỗi bước (B2)" theo yêu cầu; backend vẫn tính và trả data.jobs như cũ, muốn hiện lại chỉ cần bỏ comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>392e31c
-
feat(dub): xoay nhiều key Gemini khi hết quota để dịch không gãy
Trước đây chỉ có 1 GOOGLE_API_KEY: key hết quota (429) là rơi thẳng xuống Groq rồi deep-translator -> chất lượng dịch tụt hẳn. Nay: - Thêm GOOGLE_API_KEYS (nhiều key, ngăn cách bằng dấu phẩy/xuống dòng). Key đang dùng dính 429/401/403/5xx -> XOAY SANG KEY KẾ TIẾP NGAY trong chính request đó; chỉ khi mọi key đều hỏng mới rơi xuống Groq như cũ. - Con trỏ key "dính" ở key tốt (không round-robin vô ích); key hết quota được cho nghỉ GEMINI_KEY_COOLDOWN giây, nhân đôi mỗi lần hỏng liên tiếp, tối đa 1h -> không đốt request vào key đã cạn quota ngày. - Google khoá model cũ với key mới tạo (404 "no longer available to new users"), nên mỗi key tự dò và NHỚ model dùng được: key cũ giữ nguyên GEMINI_MODEL, key mới rơi về GEMINI_MODEL_FALLBACK (gemini-flash-latest). - backend/check_gemini_keys.py: kiểm tra thật từng key trước khi deploy (thoát mã != 0 nếu có key hỏng). Đã chạy: 4/4 key gọi được model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>a835d98
-
feat(analytics): mã người dùng U00N + task_id/session_id đọc-được + source/parent_task
Theo góp ý chốt với team về 2 cột task_id/session_id trong bản log: - user_id đổi từ tên tự do sang mã U00N cấp tuần tự khi tạo tài khoản. Mã lưu bảng/file RIÊNG (user_codes), KHÔNG đụng bảng phân quyền: cấp mã lỗi thì đăng nhập vẫn chạy (lùi về username). login trả code; admin thêm user cấp mã ngay. - task_id = <tool>_<user>_<YYYYMMDD>_<HHMMSS>_<rand4>, session_id = s_<user>_<YYYYMMDD>_<HHMMSS>_<rand4> — đọc được, tự sắp theo thời gian, dùng giờ+random thay số thứ tự trong ngày nên mở 2 tab/2 máy không đụng ID. - task mới sinh từ task cũ ghi source + parent_task: rerun (nút Làm mới) và forward (chuyển sang tool khác, mang "cha" qua sessionStorage). - dashboard hiện TÊN cạnh mã (map U00N -> username) để quản trị vẫn biết ai. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>5edb87f -
feat(analytics): tab "Quản lý thời lượng" theo dõi người dùng cho quản trị
Ghi log thô raw_events rồi suy ra 9 chỉ số (A1-A5, B1-B4) bằng ETL, không lưu sẵn chỉ số. Lưu Postgres nếu có DATABASE_URL, không thì SQLite trong STORAGE_DIR (giữ 90 ngày, tự dọn). - backend: services/analytics (storage + etl + track), api/analytics (POST /api/events + các endpoint quản trị), middleware reqctx đọc header X-TNT-* để dòng log máy chạy gắn đúng task; JobManager tự phát job_queued/started/done kèm latency_ms (đã trừ thời gian chờ hàng đợi). - frontend: lib/track.js (đồng hồ người), nút 👍/👎 (ThumbsFeedback) cho B3, dashboard SVG thuần trong tab Quản lý thời lượng; gắn điểm đo vào 8 tool. Kèm sửa lỗi phát hiện khi test: - khôi phục core.files.rel_path() + gom mọi đường dẫn phiên ở cut/dub/shuffle/ download về "/" (trước nối "\" của Windows làm hỏng URL). - get_file trả 404 khi trỏ vào thư mục (thay vì 500). - run_tests.py cô lập STORAGE_DIR sang temp (trước dọn nhầm phiên thật). - cập nhật test_cut/test_dub theo API hiện hành + thêm test_analytics (14 test). …00d2c12
-
fix(dub): caption chứa "#" làm hỏng URL tải video/audio/srt (500)
Tên thư mục output ở luồng dịch/đổi giọng lấy thẳng caption video; caption TikTok chứa "#" -> "#" trong URL bị coi là fragment -> file endpoint nhận nhầm thư mục -> 500, video đen + không tải được. - Làm sạch tên thư mục output (bỏ #, [], dấu cách...) + hash tránh đè, giống luồng "Chèn phụ đề" vốn đã đúng; giữ caption để hiển thị. - Sửa kèm auto-save: bỏ đúng tiền tố /api/files/{session}/ nên trước đây dựng sai path -> không backup được file lên History/Drive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>1d527a5
-
feat(text): thêm tab "Chèn chữ vào video" (nhiều video) + nhận dạng lời (ASR)
- Tab mới /chen-chu: vào 1..N video, chèn chữ hard-sub, ra N video tương ứng. - Dùng lại SubtitleEditor (đủ phông/màu/11 kiểu nền/3 kiểu hiển thị/kéo thả) qua props tùy chọn initialSegs/initialMode/onChange/headerExtra/onFetchLines (mặc định null -> tab Dịch/đổi giọng không đổi). - Lấy text: nút "Tự lấy lời từ video (nhận dạng)" -> ASR Whisper (Groq) không dịch, đổ lời + mốc vào editor để sửa; hoặc gõ tay. - Áp form giữa các video: "Áp cả lời + kiểu" (cùng nội dung) và "Áp kiểu (giữ lời riêng)" (mỗi video tự nhận dạng lời khác nhau). - Backend: endpoint /api/dub/subtitle/transcribe (extract_tracks + asr, nhẹ); burn + preview-frame ra thư mục RIÊNG theo từng video (hash) -> burn nhiều video 1 phiên không đè nhau. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>788bc93
-
fix(dub): đuôi speak câu trước dính vào đầu câu sau khi ghép
fit_to_slot chỉ tăng tốc atempo và kẹp trần 2x, nên câu TTS quá dài (>2x khe) vẫn dài hơn khe -> đặt tại mốc start thì tràn qua mốc câu kế, amix cộng chồng nghe như dính tiếng. Thêm hard_limit = gap tới câu kế (trừ 30ms đệm): atempo trước, nếu vẫn dư thì cắt cứng + fade-out ngắn để đuôi câu không bao giờ vượt mốc bắt đầu câu tiếp theo. Câu cuối để tự do. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>cd8a98d
-
fix(download): Douyin link rút gọn v.douyin.com không tải được
- _resolve_tiktok_url nay giải cả v.douyin.com/<code> (trước chỉ vt/vm.tiktok.com) -> ra iesdouyin.com/share/video/<id> để bước lấy id chạy được. - _douyin_direct tự resolve khi chưa thấy /video/<id> (phòng mọi đường gọi). - Text chia sẻ Douyin dán nguyên đoạn vẫn tách URL qua URL_RE trong parse_links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>214bb2a -
deploy(hf): CI trỏ sang space mới tntgroup/website_content + định tuyến relay đầy đủ
- Workflow đẩy sang spaces/tntgroup/website_content (space cũ bị HF flag 'abusive'). - Mọi call domain khả nghi (iesdouyin/tikwm/piped/invidious/resolve) đi qua Worker relay (IP sạch) khi có TIKTOK_RELAY_URL -> không gọi thẳng từ IP Space -> tránh bot abuse-handler khoá Space. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>8e3e6cb
-
feat(download): relay 'server sạch' (Cloudflare Worker) vượt chặn IP datacenter cho TikTok
Cách FREE, không cần proxy trả phí. tikwm/anti-bot chặn IP datacenter (HF/AWS) -> dựng relay nhỏ trên IP sạch của Cloudflare Workers (hoặc Deno Deploy), backend gọi relay thay vì gọi thẳng tikwm. - ydl.py: _tiktok_relay() đọc secret TIKTOK_RELAY_URL. _fetch_json() ưu tiên relay (?url=) trước, rồi tikwm trực tiếp/proxy. _dl_bytes() thử relay (?proxy=) để tải hộ media qua IP sạch. Không set -> hành vi như cũ. - deploy/tiktok_relay_worker.js: Worker sẵn sàng dán lên Cloudflare (100k req/ngày free) hoặc Deno Deploy; gọi hộ tikwm + tải hộ file, trả JSON y hệt. Đã test end-to-end bằng relay giả lập local: tải video TikTok qua relay OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>d4ddb29 -
build(download): thêm PySocks để proxy socks5 dùng được ở fallback tikwm
Fallback tikwm gọi qua `requests` với proxies; nếu TIKTOK_PROXY/DOWNLOAD_PROXY là socks5:// mà thiếu PySocks -> "Missing dependencies for SOCKS support". Thêm PySocks để hỗ trợ cả http(s):// lẫn socks5://. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>d736cf4 -
feat(download): hỗ trợ cookies TikTok cho yt-dlp (qua bot-check từ IP server)
Chẩn đoán sâu lỗi deploy: kể cả yt-dlp mới nhất, extractor TikTok chỉ trả 1 format AUDIO (0 video) cho link ads; tự bóc trang cũng chỉ nhận trang "verify/challenge" ~1.4KB (TikTok chặn khách vãng lai/IP datacenter). Đường ra video duy nhất phải qua IP không phải datacenter: proxy (đã thêm) HOẶC phiên đăng nhập. Thêm _tiktok_cookiefile(): đọc TIKTOK_COOKIES_FILE / TIKTOK_COOKIES_CONTENT (giống cơ chế cookies YouTube) và nạp vào yt-dlp cho nguồn tiktok/tiktok_photo -> cookies phiên đã đăng nhập giúp lấy trang đầy đủ = có luồng video. Tuỳ chọn, thiếu thì bỏ qua êm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>ee900d3 -
fix(download): TikTok trên deploy — proxy cho tikwm + impersonate an toàn
Lỗi deploy thực tế: "Impersonate target chrome is not available | tikwm: 403 Forbidden". Hai nguyên nhân, hai fix: 1) Impersonate: server có curl_cffi cũ (>=0.7) THIẾU target 'chrome' -> yt-dlp nổ lỗi cứng. Ghim curl_cffi>=0.11 (bust cache Docker) + hàm _impersonate_target hỏi yt-dlp target THỰC SỰ dùng được, không có thì bỏ impersonate (êm) thay vì nổ. 2) tikwm 403: Cloudflare chặn IP datacenter (HF/AWS). Fallback tikwm giờ: - Dùng proxy TIKTOK_PROXY / DOWNLOAD_PROXY (trước đây KHÔNG dùng proxy nào). - Thử proxy trước rồi trực tiếp; nhiều endpoint GET/POST + host tikwm.com. - Tải media (video/ảnh/nhạc) trực tiếp trước, lỗi mới qua proxy (tiết kiệm băng thông). - Gửi đủ header (Referer/Accept/X-Requested-With); báo lỗi gợi ý đặt proxy. - Đường yt-dlp tiktok cũng nhận TIKTOK_PROXY. Lưu ý vận hành: muốn tải TikTok ổn định trên HF cần đặt secret TIKTOK_PROXY (hoặc DOWNLOAD_PROXY) = proxy dân dụng; không có proxy thì tikwm vẫn có thể 403. Co-Authored-By: Claude Opu…d9f1807 -
fix(download): sửa tải TikTok quảng cáo lỗi khi deploy
Commit 81399e3 revert nhầm phần xử lý ads, gây 2 hồi quy: - Khôi phục _resolve_tiktok_url + gọi trong normalize_url: giải link rút gọn vt/vm.tiktok.com (Spark Ads hầu hết là link rút gọn). - tiktok_photo (ảnh/slideshow ad) lại vào impersonation, _REFERER và nhánh fallback tikwm (trước đó bị bỏ -> fail hẳn). - Regex tách aweme id mạnh hơn (item_id=, số dài). - tikwm: thêm retry + backoff cho rate-limit "Free Api Limit" (code -1) hay gặp khi deploy nhiều user; báo rõ khi là Spark Ads nhắm mục tiêu. - Sửa test_download.py đang gãy (bỏ import _format_issue không còn tồn tại, cập nhật format mặc định) + thêm test chốt hồi quy tiktok_photo. Lưu ý: yt-dlp extractor TikTok hiện đã hỏng ("rehydration"), mọi link TikTok thực tế chỉ tải được nhờ fallback tikwm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>0889623 -
update tiktok download
81399e3 -
update tiktok download
cc13dca
-
feat(shuffle): bảng tổ hợp mirror video_cook — ảo hoá 300k + lọc bằng nút (JS)
Làm phần hiển thị list combo giống tool video_cook: - Thứ tự combo: tuần tự từ điển nếu ≤ trần, LẤY MẪU ĐA DẠNG nếu vượt (bỏ cách cũ chỉ lấy N cái đầu theo product). /plan trả sequences + clip_names (payload gọn). - Trần 300.000 combo. Bảng FE ẢO HOÁ (chỉ vẽ dòng đang thấy) -> không treo dù hàng trăm nghìn dòng. Hiển thị "tên + tên + tên" + cột 🎵 Source (xoay vòng theo thứ hạng trong tập đang chọn) + STT + ✓, giống combo_model.py. - Bộ lọc bằng NÚT nối chuỗi trên tập ĐANG CHỌN (giống tab2_output.py): Đặt lại / Chọn hết / Bỏ hết / Lọc ngẫu nhiên % / Lọc tương đồng % + "Ưu tiên khúc khác hoàn toàn". Lọc tương đồng port Y HỆT SimilarityWorker (greedy keep-first + nén, 0%=khác hoàn toàn), chạy Web Worker -> UI không đơ. Đã cross-check 40 case khớp 100% với bản Python. Component mới: ComboTable.jsx, worker: simFilter.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>824497d -
feat(dub): mục "Nâng cao (ASR)" cho user chỉnh ngưỡng lọc Whisper (ẩn, mặc định như cũ)
Thêm chỗ tinh chỉnh 2 ngưỡng quyết định giữ nhiều/ít lời khi Whisper bỏ sót (video nói nhỏ / lẫn nhạc): no_speech_max (0.6) và logprob_min (-1.0). Ẩn trong mục thu gọn "⚙ Nâng cao — nhận dạng lời (ASR)" vì đa số không cần; MẶC ĐỊNH giữ đúng giá trị hiện tại nên hành vi không đổi trừ khi user tự chỉnh. Có nút "Bắt nhiều lời hơn" (0.85/-1.6) và "Về mặc định". Nối: FE DubVideo -> dubPrepare -> DubReq -> DubOptions -> pipeline -> asr.transcribe -> _filter_speech. Đã verify: default bỏ đoạn ranh giới, nới thì giữ. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>aab8e67
-
feat(sub): thêm 7 phông chữ phụ đề (Oswald, Nunito, Lato, Merriweather, Playfair, Kanit, Dancing Script)
Phông mã nguồn mở (Google Fonts, OFL), đã kiểm cmap phủ ĐỦ dấu tiếng Việt. Variable font -> instance về 1 weight tĩnh bằng fontTools + đặt tên family sạch (như bộ cũ là 1 face khai báo normal). Nhúng Git LFS + @font-face; Dockerfile tự cp *.ttf. Đo hệ số co k (libass/CSS, nhất quán <1% qua 3 chuỗi) cho từng phông để preview KHỚP burn: Oswald .587, Nunito .724, Lato .714, Merriweather .577, Playfair Display .705, Kanit .630, Dancing Script .710. Đã verify overlay Oswald + Dancing Script khớp cỡ/ giãn dòng/vị trí, dấu tiếng Việt hiện đủ. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>9206420 -
feat(sub+shuffle): tay kéo giãn ô phụ đề + giữ/tắt tiếng gốc khi xào
Phụ đề: thêm 4 tay kéo cạnh cho ô đang chọn — trái/phải đổi BỀ RỘNG vùng (w), trên/dưới GIÃN cỡ chữ (fontSize theo khoảng cách con trỏ tới tâm ô, con trỏ bám cạnh). Bỏ tay kéo góc cũ. Xào video: thêm tùy chọn "Giữ âm thanh gốc" (mặc định BẬT) — ghép nối tiếng gốc các khúc theo đúng thứ tự xào; nếu có lồng track thì TRỘN track lên tiếng gốc. Tắt -> video câm (như cũ) / chỉ còn track. Mô hình theo tool video_cook: - normalize_clip(with_audio): giữ tiếng gốc (aac 44100 stereo), clip câm chèn im lặng để concat copy đồng nhất; cache key riêng theo chế độ audio. - attach_audio(keep_original): amix track lên tiếng gốc (normalize=0) thay vì thay hẳn. Đã verify render thật: giữ/tắt/trộn đều đúng, tiếng gốc đúng vị trí từng khúc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>ad3a570 -
fix(sub): CSS xem trước KHỚP burn - hệ số co cỡ chữ + giãn dòng đo thực nghiệm
Nguyên nhân: libass vẽ Fontsize=F ra cỡ pixel nhỏ hơn CSS font-size=F. Đo render_frame(libass) vs canvas trình duyệt -> hệ số co K cố định mỗi font (BVP .655, Roboto .829, OpenSans .692, Montserrat .641, Arimo .694, Tinos .905). Khoảng cách dòng libass = 1.0*fsize mọi font. - FE: cỡ chữ preview = fontSize*k*scale; lineHeight = 1/k; measureBreaks đo ở fontSize*k; padding/bo góc hộp quy theo fpx; lề ngang 1.5% khớp measureBreaks. - BE: box_h = n*fsize + 2*pad (bỏ *1.25*K sai) -> hộp ôm sát chữ, khớp hộp CSS. Kiểm chứng overlay CSS đè khung burn thật: cỡ/bề rộng/ngắt dòng/giãn dòng/hộp khớp cho cả font mặc định lẫn font lệch nhất. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>0ce96ff -
fix(sub): lúc phát ẩn phụ đề gần-đúng + render khung chuẩn nhanh 3x
Gốc hiểu lầm: đang PHÁT vẫn hiện phụ đề CSS gần-đúng nên trông khác bản burn. - FE: chỉ hiện CSS khi TẮT exact (xem nhanh) hoặc đang gõ/kéo. Bật exact + đang phát -> KHÔNG hiện phụ đề (tránh nhầm); DỪNG mới hiện phụ đề CHUẨN (=burn). - Hiện lỗi rõ nếu server render khung thất bại ("Lỗi dựng khung chuẩn"). - render_frame: seek ĐẦU VÀO + -copyts (nhanh gấp ~3: 0.22s vs 0.62s, ảnh trùng khít) -> server yếu vẫn mượt. Debounce 200ms; render ngay khi có metadata. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>6547b89 -
fix(sub): preview khớp 100% burn bằng render server (bỏ libass-wasm)
libass-wasm và ffmpeg dùng 2 BẢN libass khác phiên bản -> render KHÁC nhau (chữ bị rớt do bug fallback libass #460, khác font-hinting...). Không thể đảm bảo giống hệt bằng bộ vẽ thứ 2 trong trình duyệt. Cách chắc chắn = dùng CHÍNH ffmpeg/libass của bản burn để xem trước: - Bỏ hẳn libass-wasm (gỡ package + public/libass + endpoint /subtitle/ass). - Khi BẬT "Xem trước khớp 100%" + video TẠM DỪNG + không gõ/kéo -> tự gọi /subtitle/preview-frame render đúng khung tại thời điểm hiện tại (server ffmpeg) rồi đè ảnh lên video. Ảnh này = ĐÚNG video sẽ xuất. - Đang phát/gõ/kéo -> hiện CSS nhanh (mượt); dừng lại tự cập nhật khung chuẩn (debounce 350ms, chống race bằng frameSeq). - Giữ fix cỡ video = nội dung thật (bỏ letterbox) để ảnh đè khớp vùng video. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>4f9859d