Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -4,175 +4,294 @@ import whisper
|
|
4 |
import tempfile
|
5 |
import gradio as gr
|
6 |
from pydub import AudioSegment
|
7 |
-
import fitz # PyMuPDF
|
8 |
-
import docx #
|
9 |
-
import pandas as pd #
|
10 |
-
#
|
11 |
import requests
|
12 |
from bs4 import BeautifulSoup
|
|
|
|
|
|
|
13 |
|
14 |
-
#
|
15 |
-
|
|
|
16 |
|
17 |
-
#
|
|
|
|
|
|
|
18 |
openai.api_key = os.getenv("OPENAI_API_KEY")
|
19 |
|
20 |
-
#
|
21 |
model = whisper.load_model("large")
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
def preprocess_audio(audio_file):
|
24 |
-
"""
|
25 |
try:
|
26 |
audio = AudioSegment.from_file(audio_file)
|
27 |
audio = audio.apply_gain(-audio.dBFS + (-20))
|
28 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
29 |
audio.export(temp_file.name, format="mp3")
|
|
|
30 |
return temp_file.name
|
31 |
except Exception as e:
|
32 |
-
|
|
|
33 |
|
34 |
-
def
|
35 |
-
"""Transcribe
|
36 |
try:
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
except Exception as e:
|
41 |
-
|
|
|
42 |
|
43 |
-
def
|
44 |
-
"""
|
45 |
try:
|
46 |
-
if
|
47 |
-
doc = fitz.open(
|
48 |
-
return "\n".join([
|
49 |
-
elif
|
50 |
-
doc = docx.Document(
|
51 |
-
return "\n".join([
|
52 |
-
elif
|
53 |
-
return pd.read_excel(
|
54 |
-
elif
|
55 |
-
return pd.read_csv(
|
56 |
else:
|
57 |
-
return "
|
58 |
except Exception as e:
|
59 |
-
return f"Error
|
60 |
|
61 |
-
def
|
62 |
-
"""
|
63 |
try:
|
64 |
response = requests.get(url)
|
65 |
response.raise_for_status()
|
66 |
soup = BeautifulSoup(response.content, 'html.parser')
|
67 |
return soup.get_text()
|
68 |
except Exception as e:
|
69 |
-
return f"Error
|
70 |
|
71 |
-
def
|
72 |
-
"""
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
audios = args[:num_audios]
|
76 |
-
|
|
|
|
|
77 |
|
78 |
-
for url in urls
|
79 |
if url:
|
80 |
-
|
81 |
|
82 |
-
for
|
83 |
-
if
|
84 |
-
|
85 |
|
86 |
for i in range(0, len(audios), 3):
|
87 |
-
audio_file,
|
88 |
if audio_file is not None:
|
89 |
-
|
90 |
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
|
93 |
-
|
|
|
|
|
94 |
if data["audio"] is not None:
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
"""
|
118 |
|
119 |
prompt = f"""
|
120 |
-
{
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
{
|
128 |
"""
|
129 |
|
130 |
try:
|
131 |
-
|
132 |
-
model="gpt-
|
133 |
messages=[{"role": "user", "content": prompt}],
|
134 |
temperature=0.1
|
135 |
)
|
136 |
-
|
137 |
-
return
|
138 |
except Exception as e:
|
139 |
-
|
|
|
140 |
|
141 |
with gr.Blocks() as demo:
|
142 |
-
gr.Markdown("##
|
143 |
with gr.Row():
|
144 |
with gr.Column(scale=2):
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
urls = gr.Textbox(label="URLs (separated by space)", lines=2)
|
150 |
with gr.Column(scale=3):
|
151 |
-
inputs_list = [
|
152 |
with gr.Tabs():
|
153 |
for i in range(1, 6):
|
154 |
-
with gr.TabItem(f"Audio {i}"):
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
inputs_list.extend([
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
for i in range(1, 6):
|
160 |
-
with gr.TabItem(f"
|
161 |
-
|
162 |
-
inputs_list.append(
|
163 |
|
164 |
-
gr.Markdown("---")
|
165 |
|
166 |
with gr.Row():
|
167 |
-
|
168 |
|
169 |
-
gr.Markdown("---")
|
170 |
|
171 |
with gr.Row():
|
172 |
-
|
173 |
with gr.Row():
|
174 |
-
|
175 |
|
176 |
-
|
177 |
|
178 |
demo.launch(share=True)
|
|
|
4 |
import tempfile
|
5 |
import gradio as gr
|
6 |
from pydub import AudioSegment
|
7 |
+
import fitz # PyMuPDF para manejar PDFs
|
8 |
+
import docx # Para manejar archivos .docx
|
9 |
+
import pandas as pd # Para manejar archivos .xlsx y .csv
|
10 |
+
#from google.colab import userdata # Importa userdata de google.colab
|
11 |
import requests
|
12 |
from bs4 import BeautifulSoup
|
13 |
+
from moviepy.editor import VideoFileClip
|
14 |
+
import yt_dlp
|
15 |
+
import logging
|
16 |
|
17 |
+
# Configurar logging
|
18 |
+
logging.basicConfig(level=logging.INFO)
|
19 |
+
logger = logging.getLogger(_name_)
|
20 |
|
21 |
+
# Configura tu clave API de OpenAI usando Google Colab userdata
|
22 |
+
#openai.api_key = userdata.get('OPENAI_API_KEY')
|
23 |
+
|
24 |
+
# Cargar las variables de entorno desde el entorno de Hugging Face
|
25 |
openai.api_key = os.getenv("OPENAI_API_KEY")
|
26 |
|
27 |
+
# Cargar el modelo Whisper de mayor calidad una vez
|
28 |
model = whisper.load_model("large")
|
29 |
|
30 |
+
def download_social_media_video(url):
|
31 |
+
"""Descarga un video de redes sociales."""
|
32 |
+
ydl_opts = {
|
33 |
+
'format': 'bestaudio/best',
|
34 |
+
'postprocessors': [{
|
35 |
+
'key': 'FFmpegExtractAudio',
|
36 |
+
'preferredcodec': 'mp3',
|
37 |
+
'preferredquality': '192',
|
38 |
+
}],
|
39 |
+
'outtmpl': '%(id)s.%(ext)s',
|
40 |
+
}
|
41 |
+
try:
|
42 |
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
43 |
+
info_dict = ydl.extract_info(url, download=True)
|
44 |
+
audio_file = f"{info_dict['id']}.mp3"
|
45 |
+
logger.info(f"Video descargado exitosamente: {audio_file}")
|
46 |
+
return audio_file
|
47 |
+
except Exception as e:
|
48 |
+
logger.error(f"Error al descargar el video: {str(e)}")
|
49 |
+
raise
|
50 |
+
|
51 |
+
def convert_video_to_audio(video_file):
|
52 |
+
"""Convierte un archivo de video a audio."""
|
53 |
+
try:
|
54 |
+
video = VideoFileClip(video_file)
|
55 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
56 |
+
video.audio.write_audiofile(temp_file.name)
|
57 |
+
logger.info(f"Video convertido a audio: {temp_file.name}")
|
58 |
+
return temp_file.name
|
59 |
+
except Exception as e:
|
60 |
+
logger.error(f"Error al convertir el video a audio: {str(e)}")
|
61 |
+
raise
|
62 |
+
|
63 |
def preprocess_audio(audio_file):
|
64 |
+
"""Preprocesa el archivo de audio para mejorar la calidad."""
|
65 |
try:
|
66 |
audio = AudioSegment.from_file(audio_file)
|
67 |
audio = audio.apply_gain(-audio.dBFS + (-20))
|
68 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
|
69 |
audio.export(temp_file.name, format="mp3")
|
70 |
+
logger.info(f"Audio preprocesado: {temp_file.name}")
|
71 |
return temp_file.name
|
72 |
except Exception as e:
|
73 |
+
logger.error(f"Error al preprocesar el archivo de audio: {str(e)}")
|
74 |
+
raise
|
75 |
|
76 |
+
def transcribir_audio(file):
|
77 |
+
"""Transcribe un archivo de audio o video."""
|
78 |
try:
|
79 |
+
if isinstance(file, str) and file.startswith('http'):
|
80 |
+
logger.info(f"Descargando video de red social: {file}")
|
81 |
+
archivo_path = download_social_media_video(file)
|
82 |
+
elif isinstance(file, str) and file.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
83 |
+
logger.info(f"Convirtiendo video local a audio: {file}")
|
84 |
+
archivo_path = convert_video_to_audio(file)
|
85 |
+
else:
|
86 |
+
logger.info(f"Preprocesando archivo de audio: {file}")
|
87 |
+
archivo_path = preprocess_audio(file)
|
88 |
+
|
89 |
+
logger.info(f"Transcribiendo audio: {archivo_path}")
|
90 |
+
resultado = model.transcribe(archivo_path)
|
91 |
+
transcripcion = resultado.get("text", "Error en la transcripción")
|
92 |
+
logger.info(f"Transcripción completada: {transcripcion[:50]}...")
|
93 |
+
return transcripcion
|
94 |
except Exception as e:
|
95 |
+
logger.error(f"Error al procesar el archivo: {str(e)}")
|
96 |
+
return f"Error al procesar el archivo: {str(e)}"
|
97 |
|
98 |
+
def leer_documento(documento_path):
|
99 |
+
"""Lee el contenido de un documento PDF, DOCX, XLSX o CSV."""
|
100 |
try:
|
101 |
+
if documento_path.endswith(".pdf"):
|
102 |
+
doc = fitz.open(documento_path)
|
103 |
+
return "\n".join([pagina.get_text() for pagina in doc])
|
104 |
+
elif documento_path.endswith(".docx"):
|
105 |
+
doc = docx.Document(documento_path)
|
106 |
+
return "\n".join([parrafo.text for parrafo in doc.paragraphs])
|
107 |
+
elif documento_path.endswith(".xlsx"):
|
108 |
+
return pd.read_excel(documento_path).to_string()
|
109 |
+
elif documento_path.endswith(".csv"):
|
110 |
+
return pd.read_csv(documento_path).to_string()
|
111 |
else:
|
112 |
+
return "Tipo de archivo no soportado. Por favor suba un documento PDF, DOCX, XLSX o CSV."
|
113 |
except Exception as e:
|
114 |
+
return f"Error al leer el documento: {str(e)}"
|
115 |
|
116 |
+
def leer_url(url):
|
117 |
+
"""Lee el contenido de una URL."""
|
118 |
try:
|
119 |
response = requests.get(url)
|
120 |
response.raise_for_status()
|
121 |
soup = BeautifulSoup(response.content, 'html.parser')
|
122 |
return soup.get_text()
|
123 |
except Exception as e:
|
124 |
+
return f"Error al leer la URL: {str(e)}"
|
125 |
|
126 |
+
def procesar_contenido_social(url):
|
127 |
+
"""Procesa el contenido de una URL de red social, manejando tanto texto como video."""
|
128 |
+
try:
|
129 |
+
# Primero, intentamos leer el contenido como texto
|
130 |
+
contenido_texto = leer_url(url)
|
131 |
+
|
132 |
+
# Luego, intentamos procesar como video
|
133 |
+
try:
|
134 |
+
contenido_video = transcribir_audio(url)
|
135 |
+
except Exception:
|
136 |
+
contenido_video = None
|
137 |
+
|
138 |
+
return {
|
139 |
+
"texto": contenido_texto,
|
140 |
+
"video": contenido_video
|
141 |
+
}
|
142 |
+
except Exception as e:
|
143 |
+
logger.error(f"Error al procesar contenido social: {str(e)}")
|
144 |
+
return None
|
145 |
+
|
146 |
+
def generar_noticia(instrucciones, hechos, tamaño, tono, *args):
|
147 |
+
"""Genera una noticia a partir de instrucciones, hechos, URLs, documentos, transcripciones y contenido de redes sociales."""
|
148 |
+
base_de_conocimiento = {
|
149 |
+
"instrucciones": instrucciones,
|
150 |
+
"hechos": hechos,
|
151 |
+
"contenido_documentos": [],
|
152 |
+
"audio_data": [],
|
153 |
+
"contenido_urls": [],
|
154 |
+
"contenido_social": []
|
155 |
+
}
|
156 |
+
num_audios = 5 * 3 # 5 audios/videos * 3 campos (archivo, nombre, cargo)
|
157 |
+
num_social_urls = 3 * 3 # 3 URLs de redes sociales * 3 campos (URL, nombre, contexto)
|
158 |
+
num_urls = 5 # 5 URLs generales
|
159 |
audios = args[:num_audios]
|
160 |
+
social_urls = args[num_audios:num_audios+num_social_urls]
|
161 |
+
urls = args[num_audios+num_social_urls:num_audios+num_social_urls+num_urls]
|
162 |
+
documentos = args[num_audios+num_social_urls+num_urls:]
|
163 |
|
164 |
+
for url in urls:
|
165 |
if url:
|
166 |
+
base_de_conocimiento["contenido_urls"].append(leer_url(url))
|
167 |
|
168 |
+
for documento in documentos:
|
169 |
+
if documento is not None:
|
170 |
+
base_de_conocimiento["contenido_documentos"].append(leer_documento(documento.name))
|
171 |
|
172 |
for i in range(0, len(audios), 3):
|
173 |
+
audio_file, nombre, cargo = audios[i:i+3]
|
174 |
if audio_file is not None:
|
175 |
+
base_de_conocimiento["audio_data"].append({"audio": audio_file, "nombre": nombre, "cargo": cargo})
|
176 |
|
177 |
+
for i in range(0, len(social_urls), 3):
|
178 |
+
social_url, social_nombre, social_contexto = social_urls[i:i+3]
|
179 |
+
if social_url:
|
180 |
+
contenido_social = procesar_contenido_social(social_url)
|
181 |
+
if contenido_social:
|
182 |
+
base_de_conocimiento["contenido_social"].append({
|
183 |
+
"url": social_url,
|
184 |
+
"nombre": social_nombre,
|
185 |
+
"contexto": social_contexto,
|
186 |
+
"texto": contenido_social["texto"],
|
187 |
+
"video": contenido_social["video"]
|
188 |
+
})
|
189 |
+
logger.info(f"Contenido de red social procesado: {social_url}")
|
190 |
|
191 |
+
transcripciones_texto, transcripciones_brutas = "", ""
|
192 |
+
|
193 |
+
for idx, data in enumerate(base_de_conocimiento["audio_data"]):
|
194 |
if data["audio"] is not None:
|
195 |
+
transcripcion = transcribir_audio(data["audio"])
|
196 |
+
transcripcion_texto = f'"{transcripcion}" - {data["nombre"]}, {data["cargo"]}'
|
197 |
+
transcripcion_bruta = f'[Audio/Video {idx + 1}]: "{transcripcion}" - {data["nombre"]}, {data["cargo"]}'
|
198 |
+
transcripciones_texto += transcripcion_texto + "\n"
|
199 |
+
transcripciones_brutas += transcripcion_bruta + "\n\n"
|
200 |
+
|
201 |
+
for data in base_de_conocimiento["contenido_social"]:
|
202 |
+
if data["texto"]:
|
203 |
+
transcripcion_texto = f'[Texto de red social]: "{data["texto"][:200]}..." - {data["nombre"]}, {data["contexto"]}'
|
204 |
+
transcripciones_texto += transcripcion_texto + "\n"
|
205 |
+
transcripciones_brutas += transcripcion_texto + "\n\n"
|
206 |
+
if data["video"]:
|
207 |
+
transcripcion_video = f'[Video de red social]: "{data["video"]}" - {data["nombre"]}, {data["contexto"]}'
|
208 |
+
transcripciones_texto += transcripcion_video + "\n"
|
209 |
+
transcripciones_brutas += transcripcion_video + "\n\n"
|
210 |
+
|
211 |
+
contenido_documentos = "\n\n".join(base_de_conocimiento["contenido_documentos"])
|
212 |
+
contenido_urls = "\n\n".join(base_de_conocimiento["contenido_urls"])
|
213 |
+
|
214 |
+
prompt_interno = """
|
215 |
+
Instrucciones para el modelo:
|
216 |
+
- Debes seguir los principios de una noticia: es decir, procura siempre responder las 5 W de una noticia en el primer párrafo (Who?, What?, When?, Where?, Why?).
|
217 |
+
- Asegúrate de que al menos el 80% de las citas sean directas y estén entrecomilladas.
|
218 |
+
- El 20% restante puede ser citas indirectas.
|
219 |
+
- No inventes información nueva.
|
220 |
+
- Sé riguroso con los hechos proporcionados.
|
221 |
+
- Al procesar los documentos cargados, extrae y resalta citas importantes y testimonios textuales de las fuentes.
|
222 |
+
- Al procesar los documentos cargados, extrae y resalta cifras clave.
|
223 |
+
- Evita usar la fecha al comienzo del cuerpo de la noticia. Empieza directamente con las 5W.
|
224 |
+
- Incluye el contenido de las redes sociales de manera relevante, citando la fuente y proporcionando el contexto adecuado.
|
225 |
+
- Asegúrate de relacionar el contexto proporcionado para el contenido de red social con su transcripción o texto correspondiente.
|
226 |
"""
|
227 |
|
228 |
prompt = f"""
|
229 |
+
{prompt_interno}
|
230 |
+
Escribe una noticia con la siguiente información, incluyendo un título, un gancho de 15 palabras (el gancho es lo que se conoce en inglés como hook, información adicional que complementa el título), y el cuerpo del contenido cuyo tamaño es {tamaño} palabras. El tono debe ser {tono}.
|
231 |
+
Instrucciones: {base_de_conocimiento["instrucciones"]}
|
232 |
+
Hechos: {base_de_conocimiento["hechos"]}
|
233 |
+
Contenido adicional de los documentos: {contenido_documentos}
|
234 |
+
Contenido adicional de las URLs: {contenido_urls}
|
235 |
+
Utiliza las siguientes transcripciones como citas directas e indirectas (sin cambiar ni inventar contenido):
|
236 |
+
{transcripciones_texto}
|
237 |
"""
|
238 |
|
239 |
try:
|
240 |
+
respuesta = openai.ChatCompletion.create(
|
241 |
+
model="gpt-4o-mini",
|
242 |
messages=[{"role": "user", "content": prompt}],
|
243 |
temperature=0.1
|
244 |
)
|
245 |
+
noticia = respuesta['choices'][0]['message']['content']
|
246 |
+
return noticia, transcripciones_brutas
|
247 |
except Exception as e:
|
248 |
+
logger.error(f"Error al generar la noticia: {str(e)}")
|
249 |
+
return f"Error al generar la noticia: {str(e)}", ""
|
250 |
|
251 |
with gr.Blocks() as demo:
|
252 |
+
gr.Markdown("## Generador de noticias todo en uno")
|
253 |
with gr.Row():
|
254 |
with gr.Column(scale=2):
|
255 |
+
instrucciones = gr.Textbox(label="Instrucciones para la noticia", lines=2)
|
256 |
+
hechos = gr.Textbox(label="Describe los hechos de la noticia", lines=4)
|
257 |
+
tamaño = gr.Number(label="Tamaño del cuerpo de la noticia (en palabras)", value=100)
|
258 |
+
tono = gr.Dropdown(label="Tono de la noticia", choices=["serio", "neutral", "divertido"], value="neutral")
|
|
|
259 |
with gr.Column(scale=3):
|
260 |
+
inputs_list = [instrucciones, hechos, tamaño, tono]
|
261 |
with gr.Tabs():
|
262 |
for i in range(1, 6):
|
263 |
+
with gr.TabItem(f"Audio/Video {i}"):
|
264 |
+
file = gr.File(label=f"Audio/Video {i}", type="filepath", file_types=["audio", "video"])
|
265 |
+
nombre = gr.Textbox(label="Nombre", scale=1)
|
266 |
+
cargo = gr.Textbox(label="Cargo", scale=1)
|
267 |
+
inputs_list.extend([file, nombre, cargo])
|
268 |
+
for i in range(1, 4):
|
269 |
+
with gr.TabItem(f"Red Social {i}"):
|
270 |
+
social_url = gr.Textbox(label=f"URL de red social {i}", lines=1)
|
271 |
+
social_nombre = gr.Textbox(label=f"Nombre de persona/cuenta {i}", scale=1)
|
272 |
+
social_contexto = gr.Textbox(label=f"Contexto del contenido {i}", lines=2)
|
273 |
+
inputs_list.extend([social_url, social_nombre, social_contexto])
|
274 |
+
for i in range(1, 6):
|
275 |
+
with gr.TabItem(f"URL {i}"):
|
276 |
+
url = gr.Textbox(label=f"URL {i}", lines=1)
|
277 |
+
inputs_list.append(url)
|
278 |
for i in range(1, 6):
|
279 |
+
with gr.TabItem(f"Documento {i}"):
|
280 |
+
documento = gr.File(label=f"Documento {i}", type="filepath", file_count="single")
|
281 |
+
inputs_list.append(documento)
|
282 |
|
283 |
+
gr.Markdown("---") # Separador visual
|
284 |
|
285 |
with gr.Row():
|
286 |
+
transcripciones_output = gr.Textbox(label="Transcripciones", lines=10)
|
287 |
|
288 |
+
gr.Markdown("---") # Separador visual
|
289 |
|
290 |
with gr.Row():
|
291 |
+
generar = gr.Button("Generar borrador")
|
292 |
with gr.Row():
|
293 |
+
noticia_output = gr.Textbox(label="Borrador generado", lines=20)
|
294 |
|
295 |
+
generar.click(fn=generar_noticia, inputs=inputs_list, outputs=[noticia_output, transcripciones_output])
|
296 |
|
297 |
demo.launch(share=True)
|