Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
}
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Dockerfile para Whisper + Streamlit + FastAPI
FROM python:3.10-slim

WORKDIR /app

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*

# Copiar archivos del proyecto
COPY . /app

# Instalar dependencias de Python
RUN pip install --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir streamlit fastapi uvicorn python-multipart

# Puerto para Streamlit y FastAPI
EXPOSE 8501 8000

# Comando por defecto: arranca ambos servicios
CMD streamlit run app.py & uvicorn api:app --host 0.0.0.0 --port 8000
47 changes: 47 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from fastapi import FastAPI, File, UploadFile, Form
from fastapi.responses import JSONResponse
import os
import shutil
from whisper.transcribe import transcribe
from whisper import load_model
from whisper.tokenizer import LANGUAGES
import torch

app = FastAPI()

# Cargar modelo solo una vez al iniciar el contenedor
MODEL_NAME = os.environ.get("WHISPER_MODEL", "turbo")
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
model = load_model(MODEL_NAME, device=DEVICE)

@app.post("/transcribe")
def transcribe_audio(
file: UploadFile = File(...),
task: str = Form("transcribe"),
language: str = Form(None),
temperature: float = Form(0.0),
word_timestamps: bool = Form(False)
):
temp_path = f"temp_{file.filename}"
with open(temp_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
try:
result = transcribe(
model,
temp_path,
task=task,
language=language,
temperature=temperature,
word_timestamps=word_timestamps,
verbose=False
)
os.remove(temp_path)
return JSONResponse({
"text": result["text"],
"segments": result.get("segments", []),
"language": result.get("language", "")
})
except Exception as e:
if os.path.exists(temp_path):
os.remove(temp_path)
return JSONResponse({"error": str(e)}, status_code=500)
146 changes: 146 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import streamlit as st
import torch
import numpy as np
import os
from whisper.transcribe import transcribe
from whisper import load_model
from whisper.tokenizer import LANGUAGES, TO_LANGUAGE_CODE

st.set_page_config(page_title="Whisper Transcriber", layout="wide")
st.title("Whisper Transcriber - Interfaz Streamlit")

# Sidebar options
st.sidebar.header("Opciones de transcripción")

# Idiomas para el selector (nombre completo)
language_names = ["(autodetect)"] + [LANGUAGES[k].title() for k in sorted(LANGUAGES.keys())]
language_key_map = dict(zip([LANGUAGES[k].title() for k in sorted(LANGUAGES.keys())], sorted(LANGUAGES.keys())))

# Modelo
model_name = st.sidebar.selectbox(
"Modelo",
["tiny", "base", "small", "medium", "large", "turbo"],
index=5,
help="Selecciona el modelo Whisper a utilizar. Modelos más grandes suelen ser más precisos pero más lentos."
)

# Tarea
task = st.sidebar.selectbox(
"Tarea",
["transcribe", "translate"],
index=0,
help="'transcribe' transcribe el audio en el mismo idioma. 'translate' traduce el audio al inglés."
)

# Idioma
language_display = st.sidebar.selectbox(
"Idioma (deja vacío para autodetectar)",
language_names,
index=0,
help="Selecciona el idioma hablado en el audio. Si eliges '(autodetect)', el modelo intentará detectarlo automáticamente."
)
if language_display == "(autodetect)":
language = None
else:
language = language_key_map[language_display]

# Temperatura
st.sidebar.markdown("<span style='font-size: 12px; color: gray;'>La temperatura controla la aleatoriedad de la transcripción. Valores bajos (0) hacen la salida más determinista, valores altos pueden generar resultados más creativos pero menos precisos.</span>", unsafe_allow_html=True)
temperature = st.sidebar.slider(
"Temperatura",
0.0, 1.0, 0.0, 0.1,
help="Controla la aleatoriedad de la transcripción. 0 es determinista, valores más altos pueden dar resultados más variados."
)

# Word timestamps
word_timestamps = st.sidebar.checkbox(
"Timestamps por palabra",
value=False,
help="Si está activado, se mostrarán marcas de tiempo para cada palabra."
)

# Selector de destino de salida
output_mode = st.sidebar.radio(
"¿Dónde quieres guardar la salida?",
("En mi equipo (local)", "En la ruta del contenedor (/app/output)"),
help="Elige si quieres guardar los archivos en tu equipo local o en la carpeta compartida del contenedor."
)

if output_mode == "En mi equipo (local)":
st.sidebar.markdown("<small>Selecciona cualquier archivo de la carpeta donde quieras guardar la salida. Se usará la ruta de esa carpeta.</small>", unsafe_allow_html=True)
selected_file = st.sidebar.file_uploader(
"Selecciona un archivo de la carpeta destino (solo para elegir la carpeta)",
type=None,
accept_multiple_files=False,
key="folder_selector"
)
if selected_file is not None and hasattr(selected_file, 'name'):
import pathlib
# En local, file_uploader solo da el nombre, no la ruta absoluta, así que usamos cwd
selected_folder = os.getcwd()
else:
selected_folder = os.getcwd()
default_dir = selected_folder
else:
default_dir = "/app/output"

output_dir = st.sidebar.text_input(
"Carpeta de salida para los resultados",
value=default_dir,
help="Ruta donde se guardarán los archivos de salida generados por la transcripción. Puedes personalizarla si lo deseas."
)

# Archivo de audio
st.header("Selecciona archivos de audio para transcribir")
audio_files = st.file_uploader(
"Selecciona uno o varios archivos de audio",
type=["wav", "mp3", "m4a", "flac", "ogg"],
accept_multiple_files=True
)

if st.button("Transcribir") and audio_files:
device = "cuda" if torch.cuda.is_available() else "cpu"
st.info(f"Cargando modelo '{model_name}' en {device}...")
model = load_model(model_name, device=device)
os.makedirs(output_dir, exist_ok=True)
for idx, audio_file in enumerate(audio_files):
st.write(f"Procesando archivo: {audio_file.name}")
with st.spinner(f"Transcribiendo {audio_file.name}..."):
# Guardar archivo temporalmente
temp_path = f"temp_{audio_file.name}"
with open(temp_path, "wb") as f:
f.write(audio_file.read())
# Barra de progreso
progress = st.progress(0)
def progress_callback(p):
progress.progress(int(p * 100))
# Ejecutar transcripción
result = transcribe(
model,
temp_path,
task=task,
language=language,
temperature=temperature,
word_timestamps=word_timestamps,
verbose=True
)
# Guardar resultado en archivo de texto en la carpeta de salida
output_txt = os.path.join(output_dir, f"{os.path.splitext(audio_file.name)[0]}.txt")
with open(output_txt, "w", encoding="utf-8") as f:
f.write(result["text"])
# Mostrar resultado
st.subheader(f"Transcripción de {audio_file.name}")
st.text_area("Texto transcrito", result["text"], height=200)
st.write("Detalles:", result)
# Botón de descarga siempre disponible
st.download_button(
label="Descargar transcripción",
data=result["text"],
file_name=f"{os.path.splitext(audio_file.name)[0]}.txt",
mime="text/plain"
)
os.remove(temp_path)
progress.progress(100)
st.success("¡Transcripción completada!")
else:
st.info("Selecciona archivos y pulsa 'Transcribir'.")
13 changes: 13 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3.8'
services:
whisper:
build: .
container_name: whisper-app
ports:
- "8501:8501"
- "8000:8000"
environment:
- WHISPER_MODEL=turbo
volumes:
- /home/usuario/whisper_output:/app/output
restart: unless-stopped
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ tqdm
more-itertools
tiktoken
triton>=2.0.0;platform_machine=="x86_64" and sys_platform=="linux" or sys_platform=="linux2"
fastapi
uvicorn
streamlit