Spaces:
Sleeping
Sleeping
Deploy tool gỡ băng video/audio
Browse files- README.md +21 -7
- app.py +188 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,13 +1,27 @@
|
|
| 1 |
---
|
| 2 |
-
title: Go Bang Video
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Go Bang Video Audio AI
|
| 3 |
+
emoji: 🎙️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# 🎙️ Gỡ băng Video / Audio bằng AI
|
| 13 |
+
|
| 14 |
+
Tool nội bộ cho phòng truyền thông: upload video/audio → AI (Gemini) chuyển thành
|
| 15 |
+
văn bản tiếng Việt. Mỗi người nhập API key riêng.
|
| 16 |
+
|
| 17 |
+
## Cách dùng
|
| 18 |
+
1. Lấy API key tại https://aistudio.google.com/apikey
|
| 19 |
+
2. Dán key vào ô **API key**
|
| 20 |
+
3. Kéo thả video/audio (nhiều file được)
|
| 21 |
+
4. Bấm **Bắt đầu gỡ băng** → đợi → copy kết quả hoặc tải file `.txt`
|
| 22 |
+
|
| 23 |
+
## Lưu ý
|
| 24 |
+
- Mỗi file < 2GB. File lớn nên tách lấy audio trước cho nhanh.
|
| 25 |
+
- File câm/không tiếng → trả `(không có nội dung thoại)`.
|
| 26 |
+
- Báo *hết quota* → đợi hôm sau hoặc bật billing cho key.
|
| 27 |
+
- ⚠️ Key = tiền công ty, không chia sẻ ra ngoài.
|
app.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool transcribe video/audio -> text bằng Gemini, giao diện Gradio.
|
| 3 |
+
Dành cho người không rành kỹ thuật: nhập API key, kéo thả file, bấm nút.
|
| 4 |
+
|
| 5 |
+
KHÔNG cần ffmpeg: upload file thẳng lên Gemini Files API (Gemini tự xử lý
|
| 6 |
+
audio/video). Có chống AI "bịa" (lặp dạ vâng) trên file câm/nhiễu.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
import time
|
| 12 |
+
import tempfile
|
| 13 |
+
from collections import Counter
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
import gradio as gr
|
| 17 |
+
from google import genai
|
| 18 |
+
from google.genai import types
|
| 19 |
+
|
| 20 |
+
# ---- Cấu hình ----
|
| 21 |
+
MODELS = ["gemini-2.5-flash", "gemini-3.1-flash-lite", "gemini-2.5-pro"]
|
| 22 |
+
DEFAULT_MODEL = "gemini-2.5-flash"
|
| 23 |
+
NO_SPEECH = "(không có nội dung thoại)"
|
| 24 |
+
MAX_MB = 1900 # Gemini Files API giới hạn ~2GB/file
|
| 25 |
+
|
| 26 |
+
TRANSCRIBE_PROMPT = """Bạn là công cụ gỡ băng (transcribe) audio. Hãy chuyển toàn bộ
|
| 27 |
+
lời nói trong file thành văn bản tiếng Việt VERBATIM (đúng từng từ).
|
| 28 |
+
|
| 29 |
+
YÊU CẦU BẮT BUỘC:
|
| 30 |
+
1. Ngôn ngữ chính là tiếng Việt; nếu người nói xen tiếng Anh thì GIỮ NGUYÊN, KHÔNG dịch.
|
| 31 |
+
2. KHÔNG tóm tắt, KHÔNG bình luận, KHÔNG thêm timestamp.
|
| 32 |
+
3. TUYỆT ĐỐI KHÔNG thêm mô tả âm thanh dạng [im lặng], [tiếng nhạc], [tiếng cười]...
|
| 33 |
+
4. Mỗi câu hoàn chỉnh hoặc mỗi lượt nói nằm trên một dòng riêng.
|
| 34 |
+
5. Nếu HOÀN TOÀN không có lời nói, chỉ trả về đúng một dòng:
|
| 35 |
+
(không có nội dung thoại)
|
| 36 |
+
6. Chỉ trả về phần text transcript thuần, không tiêu đề/ghi chú/markdown.
|
| 37 |
+
""".strip()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def looks_like_loop(text: str) -> bool:
|
| 41 |
+
"""Phát hiện AI bịa (lặp 'dạ vâng' vô tận) trên file câm/nhiễu."""
|
| 42 |
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
| 43 |
+
if len(lines) < 40:
|
| 44 |
+
return False
|
| 45 |
+
fillers = {"dạ", "vâng", "ạ", "ừm", "à", "ờ", "alo", "đúng rồi", "dạ vâng",
|
| 46 |
+
"vâng ạ", "dạ.", "vâng."}
|
| 47 |
+
def norm(s: str) -> str:
|
| 48 |
+
s = re.sub(r"[.,!?;:…\"'\-_/()]+", " ", s.lower())
|
| 49 |
+
return re.sub(r"\s+", " ", s).strip()
|
| 50 |
+
normed = [norm(x) for x in lines]
|
| 51 |
+
filler_ratio = sum(1 for n in normed if n in fillers
|
| 52 |
+
or all(w in fillers for w in n.split())) / len(normed)
|
| 53 |
+
counter = Counter(normed)
|
| 54 |
+
top3 = sum(c for _, c in counter.most_common(3)) / len(normed)
|
| 55 |
+
distinct = len(counter) / len(normed)
|
| 56 |
+
return filler_ratio >= 0.55 or top3 >= 0.6 or distinct <= 0.2
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def wait_active(client, file_obj, timeout_s: int = 300):
|
| 60 |
+
"""Đợi Gemini xử lý xong file vừa upload."""
|
| 61 |
+
deadline = time.time() + timeout_s
|
| 62 |
+
while file_obj.state.name == "PROCESSING":
|
| 63 |
+
if time.time() > deadline:
|
| 64 |
+
raise TimeoutError("File xử lý quá lâu trên server.")
|
| 65 |
+
time.sleep(2)
|
| 66 |
+
file_obj = client.files.get(name=file_obj.name)
|
| 67 |
+
if file_obj.state.name != "ACTIVE":
|
| 68 |
+
raise RuntimeError(f"File lỗi trạng thái: {file_obj.state.name}")
|
| 69 |
+
return file_obj
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def transcribe_one(client, path: str, model: str) -> str:
|
| 73 |
+
"""Upload 1 file -> transcribe -> trả text. Tự xóa file trên server."""
|
| 74 |
+
uploaded = client.files.upload(file=path)
|
| 75 |
+
try:
|
| 76 |
+
uploaded = wait_active(client, uploaded)
|
| 77 |
+
cfg = types.GenerateContentConfig(
|
| 78 |
+
system_instruction=TRANSCRIBE_PROMPT,
|
| 79 |
+
temperature=0.0,
|
| 80 |
+
thinking_config=types.ThinkingConfig(thinking_budget=0), # rẻ + nhanh
|
| 81 |
+
)
|
| 82 |
+
# Retry 503 (server quá tải tạm thời)
|
| 83 |
+
for wait in (0, 5, 10, 20):
|
| 84 |
+
if wait:
|
| 85 |
+
time.sleep(wait)
|
| 86 |
+
try:
|
| 87 |
+
resp = client.models.generate_content(
|
| 88 |
+
model=model,
|
| 89 |
+
contents=[uploaded, "Gỡ băng file trên theo đúng quy tắc."],
|
| 90 |
+
config=cfg,
|
| 91 |
+
)
|
| 92 |
+
text = (resp.text or "").strip()
|
| 93 |
+
return NO_SPEECH if looks_like_loop(text) else text
|
| 94 |
+
except Exception as e:
|
| 95 |
+
if "503" in str(e) or "UNAVAILABLE" in str(e):
|
| 96 |
+
continue
|
| 97 |
+
raise
|
| 98 |
+
return NO_SPEECH
|
| 99 |
+
finally:
|
| 100 |
+
try:
|
| 101 |
+
client.files.delete(name=uploaded.name)
|
| 102 |
+
except Exception:
|
| 103 |
+
pass
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def run(api_key, files, model, progress=gr.Progress()):
|
| 107 |
+
if not api_key or not api_key.strip():
|
| 108 |
+
raise gr.Error("Chưa nhập API key. Lấy key tại aistudio.google.com/apikey")
|
| 109 |
+
if not files:
|
| 110 |
+
raise gr.Error("Chưa chọn file nào.")
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
client = genai.Client(api_key=api_key.strip())
|
| 114 |
+
except Exception as e:
|
| 115 |
+
raise gr.Error(f"API key không hợp lệ: {e}")
|
| 116 |
+
|
| 117 |
+
sections, errors = [], []
|
| 118 |
+
total = len(files)
|
| 119 |
+
for i, f in enumerate(files):
|
| 120 |
+
path = f.name if hasattr(f, "name") else f
|
| 121 |
+
name = Path(path).name
|
| 122 |
+
size_mb = Path(path).stat().st_size / 1_000_000
|
| 123 |
+
progress((i) / total, desc=f"[{i+1}/{total}] {name}")
|
| 124 |
+
if size_mb > MAX_MB:
|
| 125 |
+
errors.append(f"{name}: quá lớn ({size_mb:.0f}MB > {MAX_MB}MB), bỏ qua.")
|
| 126 |
+
continue
|
| 127 |
+
try:
|
| 128 |
+
text = transcribe_one(client, path, model)
|
| 129 |
+
sections.append(f"## {name}\n\n{text}\n")
|
| 130 |
+
except Exception as e:
|
| 131 |
+
msg = str(e)
|
| 132 |
+
if "429" in msg or "RESOURCE_EXHAUSTED" in msg or "quota" in msg.lower():
|
| 133 |
+
errors.append(f"{name}: hết quota/giới hạn API key. Dừng.")
|
| 134 |
+
break
|
| 135 |
+
errors.append(f"{name}: lỗi - {msg[:120]}")
|
| 136 |
+
|
| 137 |
+
progress(1.0, desc="Xong")
|
| 138 |
+
combined = "\n---\n\n".join(sections) if sections else "(không có kết quả)"
|
| 139 |
+
if errors:
|
| 140 |
+
combined += "\n\n---\n\n### ⚠️ Ghi chú lỗi\n" + "\n".join(f"- {e}" for e in errors)
|
| 141 |
+
|
| 142 |
+
# Ghi file để tải xuống
|
| 143 |
+
out = Path(tempfile.gettempdir()) / "transcripts.txt"
|
| 144 |
+
out.write_text(combined, encoding="utf-8")
|
| 145 |
+
status = f"✅ Xong {len(sections)}/{total} file." + (
|
| 146 |
+
f" ({len(errors)} lỗi)" if errors else "")
|
| 147 |
+
return combined, str(out), status
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ---- Giao diện ----
|
| 151 |
+
with gr.Blocks(title="Gỡ băng Video/Audio bằng AI", theme=gr.themes.Soft()) as demo:
|
| 152 |
+
gr.Markdown(
|
| 153 |
+
"# 🎙️ Gỡ băng Video / Audio → Text (AI)\n"
|
| 154 |
+
"Upload video hoặc audio → AI chuyển thành văn bản tiếng Việt. "
|
| 155 |
+
"Hỗ trợ .mp4 .mov .m4a .mp3 .wav... (mỗi file < 2GB)."
|
| 156 |
+
)
|
| 157 |
+
with gr.Row():
|
| 158 |
+
with gr.Column(scale=1):
|
| 159 |
+
api_key = gr.Textbox(
|
| 160 |
+
label="🔑 API key (Gemini)", type="password",
|
| 161 |
+
placeholder="Dán key AIza... — lấy tại aistudio.google.com/apikey",
|
| 162 |
+
info="Key = tiền công ty. KHÔNG chia sẻ key cho người ngoài.",
|
| 163 |
+
)
|
| 164 |
+
model = gr.Dropdown(MODELS, value=DEFAULT_MODEL, label="Model")
|
| 165 |
+
files = gr.File(
|
| 166 |
+
label="📁 Chọn / kéo thả video, audio (nhiều file được)",
|
| 167 |
+
file_count="multiple",
|
| 168 |
+
file_types=["video", "audio"],
|
| 169 |
+
)
|
| 170 |
+
btn = gr.Button("▶️ Bắt đầu gỡ băng", variant="primary")
|
| 171 |
+
status = gr.Textbox(label="Trạng thái", interactive=False)
|
| 172 |
+
with gr.Column(scale=2):
|
| 173 |
+
out_text = gr.Textbox(label="📝 Kết quả transcript", lines=25,
|
| 174 |
+
show_copy_button=True)
|
| 175 |
+
out_file = gr.File(label="⬇️ Tải file .txt")
|
| 176 |
+
|
| 177 |
+
btn.click(run, inputs=[api_key, files, model],
|
| 178 |
+
outputs=[out_text, out_file, status])
|
| 179 |
+
|
| 180 |
+
gr.Markdown(
|
| 181 |
+
"---\n"
|
| 182 |
+
"**Mẹo:** file câm/không tiếng sẽ trả `(không có nội dung thoại)`. "
|
| 183 |
+
"File quá lớn nên tách lấy audio trước cho nhanh. "
|
| 184 |
+
"Nếu báo *hết quota* → đợi hôm sau hoặc bật billing cho key."
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
demo.queue(max_size=20).launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44
|
| 2 |
+
google-genai>=1.0.0
|