Spaces:
Running
Running
Commit History
23 days ago
-
chore(hf): loại file chỉ-để-deploy khỏi bản đẩy Space Running
GitHub Actions30fc7a6 -
Merge PR #5: VieNeu nạp được với onnxruntime mới (tải model thành file thật)
Unverified signaturefix(dub): VieNeu nạp được với onnxruntime mới — tải model thành file thật7e213c1 -
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
-
Merge PR #4: sửa Gen voice ra audio 0 giây (Edge bị chặn 403, VieNeu tự nạp lại)
Unverified signatureImprove TTS engine resilience with retry logic and fallback chainsaa2a39b -
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(infra): chặn 502 từ gốc — uvicorn đóng kết nối rảnh quá sớm
Administrator +1: Claude Opus 5Đo thẳng lên Space: 5/20 lời gọi API trả 502. Thân của 502 là trang HTML của huggingface.co (3044 byte) và KHÔNG có header `server: uvicorn` -> request chưa hề tới app. Không phải app lỗi: router của HF không nối được tới uvicorn. Lý do: router giữ sẵn một nắm kết nối tới app rồi dùng lại, trong khi uvicorn mặc định đóng kết nối rảnh sau 5 GIÂY. Router gửi request vào kết nối vừa bị đóng -> 502. Đây chính là nguồn đẻ ra "Unexpected token '<'" mà người dùng gặp: các bản vá trước chỉ dịch nó thành câu tiếng Việt đọc được, gốc rễ nằm ở đây. - Dockerfile: uvicorn --timeout-keep-alive 120, cao hơn hẳn thời gian rảnh của router, để bên đóng kết nối là router và đóng đàng hoàng từ phía nó. - api.js: lời gọi CHỈ ĐỌC (GET/HEAD) gặp trục trặc tạm thời thì tự thử lại 2 nhịp (0.6s, 1.2s). Lệnh GHI KHÔNG tự thử lại — bấm một lần mà chạy hai job thì tai hại hơn là báo lỗi để người dùng bấm lại. Kiểm: chạy thử api.js — GET vượt qua 2 nhịp 502 rồi trả dữ liệu, POST vẫn báo lỗi sau đúng 1 lần …a04fadb -
fix(api): 7 màn còn lại dùng chung lớp đọc JSON an toàn
Administrator +1: Claude Opus 5Nốt các trang gọi fetch trần rồi r.json(): Listing ảnh, Listing free, Flux2, Tạo video, Lịch sử cá nhân, Đăng nhập, Quản trị người dùng (+ tab Thời lượng). Space trả trang HTML lúc bận là các trang này vẫn văng "Unexpected token '<'". - api.js mở ra `request/jget/jpost` cho các trang dùng chung; AbortError đi xuyên qua nguyên vẹn để Listing ảnh còn nhận ra "quá thời gian chờ 2 phút". - Flux2: vòng hỏi trạng thái chịu đựng 90s mất liên lạc thay vì bỏ mẻ ảnh đang vẽ dở ngay lần hỏng đầu — cùng cách đã làm cho pollJob. - Đăng nhập: tách "chưa được cấp quyền" (máy chủ trả lời rành mạch) khỏi "máy chủ trục trặc". Trước đây Space bận là báo oan người dùng chưa có quyền rồi gửi luôn yêu cầu cấp quyền cho người vốn đã có. - Quản trị: chỉ 403 mới là sai mật khẩu; lỗi khác không còn đá người quản trị ra khỏi phiên đang mở. - Lịch sử cá nhân: bỏ bản jpost chép tay, và xoá/liên kết Drive lỗi thì báo thay vì nuốt thành unhandled rejection. - Hai nút tải .zip: 200-mà-HTML không còn lưu …d94104e -
fix(api): trang HTML của cổng vào Space không còn giết job đang chạy dở
Administrator +1: Claude Opus 5Lỗi người dùng gặp: "Unexpected token '<', "<!DOCTYPE "... is not valid JSON" văng ra giữa lúc dịch đang chạy 60%. Gốc: api.job() (và các GET khác) gọi thẳng r.json() không xem r.ok. Khi Space bận / vừa khởi động lại / cổng vào HF hết giờ chờ, nó trả trang HTML 502-504; JSON.parse ném ra câu tiếng Anh khó hiểu, pollJob reject NGAY từ lần hỏng đầu tiên — trong khi job vẫn đang chạy ngon lành trên máy chủ. Sửa: - Mọi lời gọi đi qua một chỗ đọc body: nhận ra HTML/không-JSON, trả câu tiếng Việt đọc được, gắn cờ `transient` cho lỗi tạm thời (5xx, 429, 408, đứt mạng). - pollJob thử lại tối đa 90s khi mất liên lạc thay vì bỏ job; 404 (máy chủ khởi động lại, mất tiến trình trong RAM) thì báo rõ và dừng luôn. - Upload qua XHR cũng không còn ném nguyên khối HTML vào alert, và 200-mà-HTML không còn âm thầm resolve {}. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>5b27e49
-
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): gộp bộ lọc ngày thành 1 ô, thêm 2 ô tool đóng góp nhiều/ít nhất
Bộ lọc ngày: bỏ cặp "Từ ngày / Đến ngày" luôn hiện, thay bằng MỘT ô chọn có sẵn Hôm nay / Hôm qua / 7-30-90 ngày qua / Tuần này / Tuần trước / Tháng này / Tháng trước. Quản trị hầu hết chỉ muốn "7 ngày qua" chứ không muốn gõ hai cái ngày; chọn "Tùy chọn khoảng ngày…" mới mở lịch ra cho ai cần mốc chính xác. Tuần tính từ THỨ HAI theo lịch VN. Không có mốc tương lai (ngày mai, 7 ngày tới…) vì đây là báo cáo đọc log đã ghi, khoảng tương lai luôn rỗng. Nhóm "Năng suất" thêm hai ô "Tool đóng góp nhiều nhất / ít nhất" — cùng bảng xếp hạng với card 🏆 bên dưới, kéo lên đầu để nhìn phát thấy ngay. Giá trị của hai ô này là TÊN TOOL nên dùng cỡ chữ nhỏ hơn ô số (.val.text), kẻo tên dài như "Chèn chữ vào video" tràn khỏi thẻ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>cc970b0 -
fix(báo cáo): ô Hài lòng hiện "3.3/5" — thang điểm cỡ nhỏ cạnh con số
Bỏ "/5" khỏi dòng phụ, đưa lên ngay cạnh điểm dưới dạng chữ nhỏ màu mờ: con số đứng một mình thì không biết là thang mấy, mà để dưới dòng phụ thì đọc rời rạc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>146a9ed -
fix(báo cáo): ô Hài lòng bỏ chữ "/5" ở dòng phụ
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>09a5805 -
fix(báo cáo): commit trước lỡ đẩy bản JSX nháp, lệch hẳn với CSS
Commit 4ffa05a đẩy nhầm bản dashboard NHÁP: lệnh ghi file bản cuối bị hỏng (chuỗi lệnh quá dài, shell không chạy tới heredoc) nên JSX vẫn dùng lớp .rep-hero/.rep-seg/.kpi trong khi theme.css đã thay bằng .grp/.metric/ .rep-filters. Class không khớp -> trang báo cáo mất sạch định dạng, chữ tràn ra như văn bản thô. Nay đưa đúng bản cuối vào, và bỏ component Kpi đã chết trong UsageCharts (dashboard tự dựng ô Metric riêng). Bảng "Chi tiết theo tool": thêm cột "Việc xong" và chú thích cho ô rỗng. Tool mở ra rồi bỏ dở thì các cột thời gian/hài lòng KHÔNG có số thật để in — trước chỉ hiện trơ dấu "—", nhìn cứ như báo cáo mất dữ liệu. Nay rê chuột vào dấu — là biết vì sao trống, và cột "Việc xong" nói thẳng chưa việc nào chạy tới đích. Bỏ luôn tiêu đề "Quản trị thời lượng" trong component: trang AdminUsers đã in sẵn tiêu đề đổi theo tab, để cả hai thì lặp hai lần trên màn hình. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>16d58d0 -
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 -
style(analytics): bỏ chú thích "ít càng tốt" ở tiêu đề cột lần bấm/việc
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>31777af -
fix(analytics): thanh trong bảng đóng góp vẽ theo số thật, không theo điểm
Cột "lần bấm/việc" nhìn ngược đời: 0.5 lần/việc thanh DÀI hơn 0.8 lần/việc, vì thanh vẽ theo điểm quy đổi (ít bấm = điểm cao). Bảng này chủ yếu để nhìn cho nhanh nên đổi luật vẽ: thanh tỷ lệ với ĐÚNG CON SỐ hiện bên cạnh (so với tool cao nhất cùng cột) — số to là thanh dài, không ngoại lệ. Cột nào "ít càng tốt" thì ghi thẳng ở tiêu đề. Tool mở ra chưa bấm chạy lần nào vẽ thanh gạch mờ (không có số để vẽ) để khỏi đọc nhầm thành "0 lần bấm = giỏi nhất". Điểm quy đổi chuyển vào tooltip khi rê chuột. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>121ae3e -
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): nút xoá video + bản dịch ở bước xem/sửa bản dịch
Dịch xong cả lô mới thấy có video không muốn tạo giọng thì trước đây phải "Làm video mới" (xoá sạch) rồi tải lại từ đầu. Nay mỗi thẻ bản dịch ở bước 3 có nút ✕: xoá đúng video đó — bỏ cả bản dịch, video đã tải lên và kết quả cũ (nếu có) — các video còn lại giữ nguyên để tạo giọng tiếp. Xoá phía client như nút ✕ ở bước tải lên; file tạm trên server tự dọn theo phiên. Thêm class dùng chung .btn-x cho nút ✕ nằm trong hàng chữ nhỏ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>683c672 -
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
-
feat(dub): áp bản dịch cho các câu cùng câu gốc (dịch hàng loạt)
Mỗi câu trong "Xem & sửa bản dịch" thêm 2 nút: - "Áp trong video": áp bản dịch câu này cho mọi câu cùng câu gốc trong video đó. - "Áp mọi video": áp cho mọi câu cùng câu gốc ở tất cả video đang dịch. Khớp theo câu gốc đã chuẩn hoá (bỏ khoảng trắng thừa + không phân biệt hoa/thường) nên bắt được câu lặp dù lệch nhẹ. Tiện khi dịch hàng loạt (vd 200 video quảng cáo cùng chiến dịch) chỉ cần sửa 1 câu intro/CTA rồi áp cho tất cả. Thuần client-side nên tức thì, không đụng mốc thời gian; có banner báo số câu đã áp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>0172658
-
fix(dub): giữ bản dịch sau khi tạo giọng + nút làm video mới khỏi F5
- Panel "Xem & sửa bản dịch" không còn bị ẩn khi đã có video kết quả: user lỡ bấm tạo giọng vẫn sửa lời / dịch lại / đọc lại được. Nút "Dịch lại" giờ chạy lại pha dịch; nút chính đổi nhãn "Đọc & ghép lại" khi đã có output. - Thêm nút "Làm video mới" ở panel Kết quả: xoá video + bản dịch + kết quả hiện tại (giữ cài đặt ngôn ngữ/giọng) để làm video khác mà không cần F5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>9e3250e -
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