Spaces:
Sleeping
Sleeping
travahacker
commited on
Commit
·
8985e44
1
Parent(s):
31cb808
feat: usa legendas do YouTube como método principal (funciona para qualquer pessoa)
Browse files- youtube-transcript-api para vídeos com legendas (sem 403)
- fallback: yt-dlp + Whisper para vídeos sem legendas
- README.md +8 -6
- app.py +75 -18
- requirements.txt +2 -1
README.md
CHANGED
|
@@ -6,21 +6,23 @@ colorTo: purple
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 4.44.0
|
| 8 |
app_file: app.py
|
| 9 |
-
hardware: zerogpu
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
---
|
| 13 |
|
| 14 |
-
# Transcrição YouTube
|
| 15 |
|
| 16 |
-
Cola o link do YouTube
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
## Como usar
|
| 19 |
|
| 20 |
-
1. Cole o link do vídeo
|
| 21 |
-
2. Escolha o
|
| 22 |
3. Clique em Transcrever
|
| 23 |
-
4.
|
| 24 |
|
| 25 |
## Requisitos
|
| 26 |
|
|
|
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 4.44.0
|
| 8 |
app_file: app.py
|
|
|
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Transcrição YouTube
|
| 14 |
|
| 15 |
+
Cola o link do YouTube e transcreve. **Funciona para qualquer pessoa**, de qualquer computador.
|
| 16 |
+
|
| 17 |
+
- **Legendas do YouTube** (prioridade): usa legendas quando disponíveis — rápido, sem download
|
| 18 |
+
- **Whisper** (fallback): quando o vídeo não tem legendas, usa yt-dlp + Whisper na GPU
|
| 19 |
|
| 20 |
## Como usar
|
| 21 |
|
| 22 |
+
1. Cole o link do vídeo (ex: https://youtu.be/HidI2dvdTMw)
|
| 23 |
+
2. Escolha o idioma preferido (ou Auto)
|
| 24 |
3. Clique em Transcrever
|
| 25 |
+
4. Se houver legendas: resultado em segundos. Sem legendas: aguarde a fila da GPU (1–2 min)
|
| 26 |
|
| 27 |
## Requisitos
|
| 28 |
|
app.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
-
Transcrição YouTube
|
| 3 |
|
| 4 |
-
Cola o link,
|
|
|
|
| 5 |
"""
|
|
|
|
| 6 |
import subprocess
|
| 7 |
import tempfile
|
| 8 |
from pathlib import Path
|
|
@@ -21,16 +23,55 @@ except ImportError:
|
|
| 21 |
spaces = _Spaces()
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def baixar_audio(url: str, pasta: Path) -> Path:
|
| 25 |
-
"""Baixa áudio do YouTube com yt-dlp."""
|
| 26 |
pasta.mkdir(parents=True, exist_ok=True)
|
| 27 |
out = pasta / "audio.%(ext)s"
|
| 28 |
cmd = [
|
| 29 |
-
"yt-dlp", "-x", "--audio-format", "
|
| 30 |
-
"-o", str(out), "--no-playlist",
|
|
|
|
|
|
|
| 31 |
]
|
| 32 |
-
subprocess.run(cmd,
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
p = pasta / f"audio{ext}"
|
| 35 |
if p.exists():
|
| 36 |
return p
|
|
@@ -41,22 +82,35 @@ def baixar_audio(url: str, pasta: Path) -> Path:
|
|
| 41 |
|
| 42 |
|
| 43 |
@spaces.GPU(duration=180)
|
| 44 |
-
def
|
| 45 |
"""
|
| 46 |
-
Transcreve vídeo do YouTube.
|
| 47 |
-
|
|
|
|
| 48 |
"""
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
if not url or ("youtube.com" not in url and "youtu.be" not in url):
|
| 52 |
return "❌ Cole um link válido do YouTube."
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 55 |
pasta = Path(tmpdir)
|
| 56 |
try:
|
| 57 |
audio_path = baixar_audio(url, pasta)
|
| 58 |
except Exception as e:
|
| 59 |
-
return f"❌ Erro ao baixar: {e}"
|
| 60 |
|
| 61 |
model = WhisperModel(modelo, device="cuda", compute_type="float16")
|
| 62 |
lang = None if idioma == "Auto" else idioma.lower()
|
|
@@ -80,7 +134,7 @@ def transcrever_gpu(url: str, modelo: str, idioma: str) -> str:
|
|
| 80 |
if not texto:
|
| 81 |
return "⚠️ Nenhum texto transcrito (vídeo sem fala?)."
|
| 82 |
|
| 83 |
-
return f"Idioma detectado: {info.language}\n\n{texto}"
|
| 84 |
|
| 85 |
|
| 86 |
MODELOS = ["tiny", "base", "small", "medium", "large-v3"]
|
|
@@ -91,12 +145,15 @@ with gr.Blocks(
|
|
| 91 |
theme=gr.themes.Soft(),
|
| 92 |
) as demo:
|
| 93 |
gr.Markdown("# 🎙️ Transcrição YouTube")
|
| 94 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
with gr.Row():
|
| 97 |
url = gr.Textbox(
|
| 98 |
label="Link do YouTube",
|
| 99 |
-
placeholder="https://
|
| 100 |
scale=3,
|
| 101 |
)
|
| 102 |
with gr.Row():
|
|
@@ -121,7 +178,7 @@ with gr.Blocks(
|
|
| 121 |
)
|
| 122 |
|
| 123 |
btn.click(
|
| 124 |
-
fn=
|
| 125 |
inputs=[url, modelo, idioma],
|
| 126 |
outputs=saida,
|
| 127 |
)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Transcrição YouTube — ZeroGPU Space
|
| 3 |
|
| 4 |
+
Cola o link, transcreve. Usa legendas do YouTube quando disponíveis (rápido, sem GPU).
|
| 5 |
+
Fallback: Whisper quando o vídeo não tem legendas.
|
| 6 |
"""
|
| 7 |
+
import re
|
| 8 |
import subprocess
|
| 9 |
import tempfile
|
| 10 |
from pathlib import Path
|
|
|
|
| 23 |
spaces = _Spaces()
|
| 24 |
|
| 25 |
|
| 26 |
+
def extrair_video_id(url: str) -> str | None:
|
| 27 |
+
"""Extrai o ID do vídeo de uma URL do YouTube."""
|
| 28 |
+
if not url or ("youtube.com" not in url and "youtu.be" not in url):
|
| 29 |
+
return None
|
| 30 |
+
m = re.search(r"(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})", url)
|
| 31 |
+
return m.group(1) if m else None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def transcrever_via_legendas(video_id: str, idioma: str) -> tuple[str | None, str | None]:
|
| 35 |
+
"""
|
| 36 |
+
Busca legendas diretamente do YouTube (sem baixar vídeo).
|
| 37 |
+
Retorna (texto, None) em sucesso ou (None, mensagem_erro) em falha.
|
| 38 |
+
"""
|
| 39 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 40 |
+
from youtube_transcript_api._errors import NoTranscriptFound, RequestBlocked, IpBlocked
|
| 41 |
+
|
| 42 |
+
lang_codes = []
|
| 43 |
+
if idioma != "Auto":
|
| 44 |
+
lang_codes = [idioma.lower()]
|
| 45 |
+
lang_codes.extend(["pt", "en", "es", "fr"]) # fallbacks
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
api = YouTubeTranscriptApi()
|
| 49 |
+
transcript = api.fetch(video_id, languages=lang_codes)
|
| 50 |
+
linhas = [s.text for s in transcript if s.text.strip()]
|
| 51 |
+
return "\n".join(linhas), None
|
| 52 |
+
except NoTranscriptFound:
|
| 53 |
+
return None, "Este vídeo não tem legendas disponíveis."
|
| 54 |
+
except (RequestBlocked, IpBlocked):
|
| 55 |
+
return None, "YouTube bloqueou o acesso (IP de datacenter). Tente mais tarde."
|
| 56 |
+
except Exception as e:
|
| 57 |
+
return None, str(e)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
def baixar_audio(url: str, pasta: Path) -> Path:
|
| 61 |
+
"""Baixa áudio do YouTube com yt-dlp (fallback quando não há legendas)."""
|
| 62 |
pasta.mkdir(parents=True, exist_ok=True)
|
| 63 |
out = pasta / "audio.%(ext)s"
|
| 64 |
cmd = [
|
| 65 |
+
"yt-dlp", "-x", "--audio-format", "m4a",
|
| 66 |
+
"-o", str(out), "--no-playlist", "--no-warnings",
|
| 67 |
+
"--no-check-certificate",
|
| 68 |
+
url,
|
| 69 |
]
|
| 70 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
| 71 |
+
if result.returncode != 0:
|
| 72 |
+
err = (result.stderr or result.stdout or "").strip()
|
| 73 |
+
raise RuntimeError(f"yt-dlp falhou: {err or 'erro desconhecido'}")
|
| 74 |
+
for ext in [".m4a", ".opus", ".webm", ".wav", ".mp3"]:
|
| 75 |
p = pasta / f"audio{ext}"
|
| 76 |
if p.exists():
|
| 77 |
return p
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
@spaces.GPU(duration=180)
|
| 85 |
+
def transcrever(url: str, modelo: str, idioma: str) -> str:
|
| 86 |
"""
|
| 87 |
+
Transcreve vídeo do YouTube.
|
| 88 |
+
1. Tenta legendas do YouTube (rápido, sem GPU, funciona para qualquer pessoa)
|
| 89 |
+
2. Se não houver legendas, usa yt-dlp + Whisper (GPU)
|
| 90 |
"""
|
| 91 |
+
video_id = extrair_video_id(url)
|
| 92 |
+
if not video_id:
|
|
|
|
| 93 |
return "❌ Cole um link válido do YouTube."
|
| 94 |
|
| 95 |
+
# 1. Tentar legendas primeiro (funciona para ~85% dos vídeos, qualquer usuário)
|
| 96 |
+
texto, erro = transcrever_via_legendas(video_id, idioma)
|
| 97 |
+
if texto:
|
| 98 |
+
return f"📋 Legendas do YouTube\n\n{texto}"
|
| 99 |
+
|
| 100 |
+
# 2. Fallback: yt-dlp + Whisper (pode falhar com 403 em alguns ambientes)
|
| 101 |
+
return transcrever_com_whisper(url, modelo, idioma, msg_sem_legendas=erro)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def transcrever_com_whisper(url: str, modelo: str, idioma: str, msg_sem_legendas: str = "") -> str:
|
| 105 |
+
"""Transcreve com Whisper (baixa áudio via yt-dlp). Usa GPU."""
|
| 106 |
+
from faster_whisper import WhisperModel
|
| 107 |
+
|
| 108 |
with tempfile.TemporaryDirectory() as tmpdir:
|
| 109 |
pasta = Path(tmpdir)
|
| 110 |
try:
|
| 111 |
audio_path = baixar_audio(url, pasta)
|
| 112 |
except Exception as e:
|
| 113 |
+
return f"❌ Erro ao baixar: {e}\n\n💡 {msg_sem_legendas or 'Vídeo sem legendas — o download pode falhar (403) em servidores.'}"
|
| 114 |
|
| 115 |
model = WhisperModel(modelo, device="cuda", compute_type="float16")
|
| 116 |
lang = None if idioma == "Auto" else idioma.lower()
|
|
|
|
| 134 |
if not texto:
|
| 135 |
return "⚠️ Nenhum texto transcrito (vídeo sem fala?)."
|
| 136 |
|
| 137 |
+
return f"🎙️ Whisper — Idioma detectado: {info.language}\n\n{texto}"
|
| 138 |
|
| 139 |
|
| 140 |
MODELOS = ["tiny", "base", "small", "medium", "large-v3"]
|
|
|
|
| 145 |
theme=gr.themes.Soft(),
|
| 146 |
) as demo:
|
| 147 |
gr.Markdown("# 🎙️ Transcrição YouTube")
|
| 148 |
+
gr.Markdown(
|
| 149 |
+
"Cola o link e transcreve. **Legendas do YouTube** quando disponíveis (rápido, qualquer vídeo). "
|
| 150 |
+
"Sem legendas: usa Whisper (ZeroGPU)."
|
| 151 |
+
)
|
| 152 |
|
| 153 |
with gr.Row():
|
| 154 |
url = gr.Textbox(
|
| 155 |
label="Link do YouTube",
|
| 156 |
+
placeholder="https://youtu.be/HidI2dvdTMw",
|
| 157 |
scale=3,
|
| 158 |
)
|
| 159 |
with gr.Row():
|
|
|
|
| 178 |
)
|
| 179 |
|
| 180 |
btn.click(
|
| 181 |
+
fn=transcrever,
|
| 182 |
inputs=[url, modelo, idioma],
|
| 183 |
outputs=saida,
|
| 184 |
)
|
requirements.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
gradio>=4.0.0
|
| 2 |
huggingface_hub>=0.20.0,<0.23.0
|
| 3 |
faster-whisper>=1.0.0
|
| 4 |
-
yt-dlp>=2024.
|
|
|
|
|
|
| 1 |
gradio>=4.0.0
|
| 2 |
huggingface_hub>=0.20.0,<0.23.0
|
| 3 |
faster-whisper>=1.0.0
|
| 4 |
+
yt-dlp>=2024.12.0
|
| 5 |
+
youtube-transcript-api>=1.2.0
|