Zuletzt aktiv 16 hours ago

atareao's Avatar atareao hat die Gist bearbeitet 16 hours ago. Zu Änderung gehen

1 file changed, 587 insertions

embeddings.py(Datei erstellt)

@@ -0,0 +1,587 @@
1 + #!/usr/bin/env python3
2 + """
3 + 🚀 Embeddings para todo — Demo completa
4 + ========================================
5 + Episodio 817: Un solo script que recorre todos los casos de uso.
6 + Búsqueda semántica, clasificación, deduplicación, recomendación y ChromaDB.
7 +
8 + Requisitos:
9 + - Ollama corriendo en localhost:11434
10 + - Modelo nomic-embed-text descargado: ollama pull nomic-embed-text
11 + - Python 3.10+
12 +
13 + Uso:
14 + python3 setup_completo.py # Ejecuta todo
15 + python3 setup_completo.py --fast # Solo pasos principales, sin ChromaDB
16 + python3 setup_completo.py --list # Lista los pasos disponibles
17 + python3 setup_completo.py --step 3 # Ejecuta solo el paso 3
18 + python3 setup_completo.py --step 2,4,6 # Ejecuta pasos específicos
19 + python3 setup_completo.py --interactive # Paso a paso con confirmación
20 + """
21 +
22 + import os, sys, json, math, glob, time, shutil, subprocess, textwrap, argparse
23 +
24 + # ─── Configuración ────────────────────────────────────────────────────────
25 + MODEL = "bge-m3"
26 + OLLAMA_URL = "http://localhost:11434"
27 + DIR_DOCS = "./docs_prueba" # Documentos de prueba para búsqueda semántica
28 + DIR_CHROMA = "./chroma_store" # Donde ChromaDB guarda los datos
29 +
30 + # Colores para la terminal
31 + VERDE = "\033[92m"
32 + AZUL = "\033[94m"
33 + AMAR = "\033[93m"
34 + ROJO = "\033[91m"
35 + FIN = "\033[0m"
36 + NEGR = "\033[1m"
37 +
38 +
39 + def ok(msg):
40 + print(f" {VERDE}✅{FIN} {msg}")
41 +
42 +
43 + def info(msg):
44 + print(f" {AZUL}ℹ️{FIN} {msg}")
45 +
46 +
47 + def warn(msg):
48 + print(f" {AMAR}⚠️{FIN} {msg}")
49 +
50 +
51 + def error(msg):
52 + print(f" {ROJO}❌{FIN} {msg}")
53 +
54 +
55 + def titulo(t):
56 + print(f"\n{NEGR}{'=' * 60}{FIN}\n{NEGR} {t}{FIN}\n{'=' * 60}")
57 +
58 +
59 + # ─── Helpers ──────────────────────────────────────────────────────────────
60 +
61 +
62 + def check_ollama():
63 + """Verifica que Ollama está corriendo y el modelo está disponible."""
64 + try:
65 + r = subprocess.run(
66 + ["curl", "-s", f"{OLLAMA_URL}/api/tags"],
67 + capture_output=True,
68 + text=True,
69 + timeout=5,
70 + )
71 + if r.returncode != 0:
72 + error("Ollama no responde. ¿Está corriendo?")
73 + info("Ejecuta: ollama serve")
74 + return False
75 + modelos = json.loads(r.stdout)
76 + disponible = any(MODEL in m["name"] for m in modelos.get("models", []))
77 + if not disponible:
78 + warn(f"Modelo {MODEL} no encontrado. Descargando...")
79 + subprocess.run(["ollama", "pull", MODEL], check=True)
80 + ok(f"Modelo {MODEL} descargado")
81 + return True
82 + except Exception as e:
83 + error(f"Error conectando con Ollama: {e}")
84 + return False
85 +
86 +
87 + def emb(texto):
88 + """Genera un embedding usando Ollama."""
89 + payload = json.dumps({"model": MODEL, "prompt": texto})
90 + r = subprocess.run(
91 + ["curl", "-s", f"{OLLAMA_URL}/api/embeddings", "-d", payload],
92 + capture_output=True,
93 + text=True,
94 + )
95 + return json.loads(r.stdout)["embedding"]
96 +
97 +
98 + def cos_sim(a, b):
99 + """Similitud coseno entre dos vectores."""
100 + dot = sum(x * y for x, y in zip(a, b))
101 + na = math.sqrt(sum(x * x for x in a))
102 + nb = math.sqrt(sum(y * y for y in b))
103 + return dot / (na * nb) if na and nb else 0
104 +
105 +
106 + # ─── 1. Crear documentos de prueba ────────────────────────────────────────
107 +
108 +
109 + def crear_documentos():
110 + titulo("1️⃣ CREAR DOCUMENTOS DE PRUEBA")
111 +
112 + if os.path.exists(DIR_DOCS):
113 + shutil.rmtree(DIR_DOCS)
114 + os.makedirs(DIR_DOCS)
115 +
116 + documentos = {
117 + "facturas.txt": """Factura hosting enero 2026: 12.99€
118 + Factura dominio atareao.es 2026: 18.50€
119 + Pago servidor VPS febrero: 24.99€
120 + Total gastos infraestructura Q1: 156.47€
121 + Próximo pago hosting: 1 de marzo""",
122 + "apuntes-linux.txt": """Cómo montar un servidor web con Nginx en Ubuntu 24.04
123 + Configurar firewall con nftables: tabla inet filter con policy drop
124 + Instalar Docker: sudo apt install docker.io docker-compose
125 + Los logs del sistema están en /var/log/syslog
126 + systemctl enable --now para activar servicios al arranque""",
127 + "recetas.txt": """Paella valenciana: arroz bomba, pollo, conejo, judía verde, garrofón
128 + Tortilla de patatas: 4 huevos, 2 patatas grandes, cebolla, aceite de oliva
129 + Pan casero: 500g harina de fuerza, 300ml agua, 10g sal, 5g levadura
130 + La clave de un buen arroz es el sofrito y el caldo caliente""",
131 + "compras.txt": """Lista de la compra: leche, huevos, pan, arroz, tomates, lechuga
132 + Comprar regalo cumpleaños: libro de Rust
133 + Recoger paquete en Correos: número de seguimiento 123456789
134 + Repostar coche: gasolina 95, 40€""",
135 + "ideas-proyectos.txt": """App web para gestionar gastos compartidos con amigos
136 + Script que monitoriza el precio de la luz y avisa cuando baja
137 + Bot de Telegram que resuma noticias de IA cada mañana
138 + Generador de menú semanal con recetas de un archivo de cocina""",
139 + }
140 +
141 + for nombre, contenido in documentos.items():
142 + with open(os.path.join(DIR_DOCS, nombre), "w") as f:
143 + f.write(contenido.strip())
144 + ok(f"Documento creado: {nombre}")
145 +
146 + info("5 documentos de prueba listos en ./docs_prueba/")
147 + return list(documentos.keys())
148 +
149 +
150 + # ─── 2. Búsqueda semántica ────────────────────────────────────────────────
151 +
152 +
153 + def busqueda_semantica():
154 + titulo("2️⃣ BÚSQUEDA SEMÁNTICA — buscar por significado")
155 +
156 + # Indexar documentos
157 + info("Generando embeddings de los documentos de prueba...")
158 + archivos = sorted(glob.glob(f"{DIR_DOCS}/*.txt"))
159 + index = []
160 +
161 + for path in archivos:
162 + with open(path) as f:
163 + texto = f.read()
164 + nombre = os.path.basename(path)
165 + vec = emb(texto)
166 + index.append((nombre, vec, texto))
167 + print(f" {nombre}: {len(vec)} dimensiones")
168 +
169 + ok(f"{len(index)} documentos indexados")
170 +
171 + # Consultas de prueba
172 + consultas = [
173 + "¿Cuánto dinero gastamos en servidores?",
174 + "¿Qué hay para cenar esta noche?",
175 + "Alguna idea para un proyecto con IA",
176 + ]
177 +
178 + for consulta in consultas:
179 + print(f'\n {NEGR}🔍 Consulta:{FIN} "{consulta}"')
180 + vec_q = emb(consulta)
181 +
182 + resultados = sorted(
183 + [
184 + (nombre, cos_sim(vec_q, vec), texto[:100])
185 + for nombre, vec, texto in index
186 + ],
187 + key=lambda x: -x[1],
188 + )
189 +
190 + for nombre, sim, preview in resultados[:3]:
191 + barra = "█" * int(sim * 20) + "░" * (20 - int(sim * 20))
192 + print(f" {barra} {sim:.4f} {nombre}")
193 +
194 + mejor = resultados[0]
195 + print(f" → Mejor resultado: {NEGR}{mejor[0]}{FIN} ({mejor[1]:.1%})")
196 + if mejor[1] > 0.7:
197 + ok("Coincidencia semántica alta")
198 + elif mejor[1] > 0.5:
199 + info("Coincidencia semántica media")
200 + else:
201 + warn("Coincidencia baja — prueba con otra consulta")
202 +
203 +
204 + # ─── 3. Clasificación ─────────────────────────────────────────────────────
205 +
206 +
207 + def clasificacion():
208 + titulo("3️⃣ CLASIFICACIÓN — sin entrenar modelos")
209 +
210 + categorias = {
211 + "urgente": [
212 + "se ha caído el servidor de producción",
213 + "necesito esto para mañana sin falta",
214 + "error crítico en la base de datos",
215 + ],
216 + "normal": [
217 + "te envío el documento actualizado",
218 + "confirma si has recibido el email",
219 + ],
220 + "spam": [
221 + "gana dinero rápido desde casa",
222 + "has ganado un premio exclusivo",
223 + ],
224 + "cocina": [
225 + "receta de paella valenciana",
226 + "cómo hacer pan casero",
227 + ],
228 + }
229 +
230 + # Pre-calcular embedding de cada categoría (promedio de ejemplos)
231 + info("Calculando firmas de categorías...")
232 + firmas = {}
233 + for cat, ejemplos in categorias.items():
234 + vecs = [emb(e) for e in ejemplos]
235 + firmas[cat] = [sum(vals) / len(vals) for vals in zip(*vecs)]
236 + ok(f"Categoría '{cat}' lista ({len(ejemplos)} ejemplos)")
237 +
238 + # Textos a clasificar
239 + textos_prueba = [
240 + "el servidor web no responde desde las 3 de la mañana",
241 + "te adjunto la factura de este mes para tu revisión",
242 + "FELICIDADES HAS GANADO UN VIAJE AL CARIBE",
243 + "cómo hacer una tortilla de patatas con cebolla caramelizada",
244 + ]
245 +
246 + for texto in textos_prueba:
247 + vec = emb(texto)
248 + resultados = sorted(
249 + [(cat, cos_sim(vec, firma)) for cat, firma in firmas.items()],
250 + key=lambda x: -x[1],
251 + )
252 + mejor = resultados[0]
253 + barra = "█" * int(mejor[1] * 20) + "░" * (20 - int(mejor[1] * 20))
254 + print(f'\n 📝 "{texto[:60]}..."')
255 + print(f" → {barra} {NEGR}{mejor[0]}{FIN} ({mejor[1]:.1%})")
256 +
257 +
258 + # ─── 4. Deduplicación ─────────────────────────────────────────────────────
259 +
260 +
261 + def deduplicacion():
262 + titulo("4️⃣ DEDUPLICACIÓN — encontrar contenido repetido")
263 +
264 + items = [
265 + {"id": "doc1", "texto": "Cómo instalar Ollama en Ubuntu 24.04 paso a paso"},
266 + {"id": "doc2", "texto": "Guía completa para instalar Ollama en Ubuntu 24.04"},
267 + {"id": "doc3", "texto": "Receta de paella valenciana tradicional con marisco"},
268 + {"id": "doc4", "texto": "Cómo configurar nftables como firewall en Linux"},
269 + {"id": "doc5", "texto": "Instalación de Ollama en Ubuntu 24.04 - tutorial"},
270 + ]
271 +
272 + info("Generando embeddings...")
273 + embebidos = [(item, emb(item["texto"])) for item in items]
274 +
275 + print(f"\n {'Item':<10} {'Similitud con':<20} {'Score':<8}")
276 + print(f" {'-' * 40}")
277 + for i, (item_a, emb_a) in enumerate(embebidos):
278 + for j, (item_b, emb_b) in enumerate(embebidos[i + 1 :], i + 1):
279 + sim = cos_sim(emb_a, emb_b)
280 + status = (
281 + "🔴 DUPLICADO"
282 + if sim > 0.90
283 + else "🟡 PARECIDO"
284 + if sim > 0.75
285 + else "🟢 DISTINTO"
286 + )
287 + print(f" {item_a['id']:<10} {item_b['id']:<20} {sim:.4f} {status}")
288 +
289 + # Detectar duplicados
290 + duplicados = [
291 + (item_a, item_b, sim)
292 + for (item_a, emb_a), (item_b, emb_b) in [
293 + ((items[i], embebidos[i][1]), (items[j], embebidos[j][1]))
294 + for i in range(len(items))
295 + for j in range(i + 1, len(items))
296 + ]
297 + if (sim := cos_sim(emb_a, emb_b)) > 0.85
298 + ]
299 +
300 + if duplicados:
301 + print(
302 + f"\n {NEGR}🔴 Se detectaron {len(duplicados)} grupos de duplicados:{FIN}"
303 + )
304 + for a, b, sim in duplicados:
305 + print(f' • "{a["texto"][:50]}..."')
306 + print(f' "{b["texto"][:50]}..."')
307 + print(f" → Similitud: {sim:.1%}")
308 + else:
309 + info("No se detectaron duplicados significativos")
310 +
311 +
312 + # ─── 5. Recomendación ─────────────────────────────────────────────────────
313 +
314 +
315 + def recomendacion():
316 + titulo("5️⃣ RECOMENDACIÓN — más como esto")
317 +
318 + articulos = [
319 + "Linux es el mejor sistema operativo para servidores",
320 + "Cómo instalar Docker en Ubuntu 24.04",
321 + "Introducción a Rust: seguridad de memoria sin recolector de basura",
322 + "PostgreSQL con pgvector para búsqueda semántica",
323 + "Ollama: modelos de lenguaje locales en tu servidor",
324 + "Configurar nftables como firewall en Linux",
325 + "Rust vs Go: comparativa para backend en 2026",
326 + "Embeddings: coordenadas en un mapa de significados",
327 + ]
328 +
329 + info("Generando embeddings de {len(articulos)} artículos...")
330 + index = [(tit, emb(tit)) for tit in articulos]
331 +
332 + # Elegir un artículo de referencia
333 + ref = "Ollama: modelos de lenguaje locales en tu servidor"
334 + print(f'\n 📖 Artículo de referencia: "{ref}"')
335 + print(f"\n {NEGR}📚 Recomendaciones:{FIN}")
336 +
337 + vec_ref = emb(ref)
338 + recomendados = sorted(
339 + [(tit, cos_sim(vec_ref, vec)) for tit, vec in index if tit != ref],
340 + key=lambda x: -x[1],
341 + )
342 +
343 + for i, (tit, sim) in enumerate(recomendados[:4], 1):
344 + barra = "█" * int(sim * 20) + "░" * (20 - int(sim * 20))
345 + print(f" {i}. {barra} {sim:.4f} {tit}")
346 +
347 +
348 + # ─── 6. ChromaDB ──────────────────────────────────────────────────────────
349 +
350 +
351 + def chroma_demo():
352 + titulo("6️⃣ CHROMADB — base de datos vectorial")
353 +
354 + try:
355 + import chromadb
356 + from chromadb.utils import embedding_functions
357 + except ImportError:
358 + warn("chromadb no instalado. Ejecuta: pip install chromadb")
359 + return
360 +
361 + # Limpiar y crear
362 + if os.path.exists(DIR_CHROMA):
363 + shutil.rmtree(DIR_CHROMA)
364 +
365 + info("Creando base de datos vectorial con ChromaDB...")
366 + client = chromadb.PersistentClient(path=DIR_CHROMA)
367 + collection = client.get_or_create_collection(
368 + name="demo_817",
369 + embedding_function=embedding_functions.OllamaEmbeddingFunction(
370 + url=f"{OLLAMA_URL}/api/embeddings", model_name=MODEL
371 + ),
372 + )
373 +
374 + # Documentos de prueba
375 + docs = [
376 + "Linux es el mejor sistema operativo para servidores por su estabilidad",
377 + "Para instalar Docker en Ubuntu ejecuta sudo apt install docker.io",
378 + "La paella valenciana lleva arroz, pollo, conejo, judía y garrofón",
379 + "Ollama permite ejecutar modelos de lenguaje localmente en tu hardware",
380 + "Rust es un lenguaje de programación de sistemas seguro y rápido",
381 + "PostgreSQL con pgvector permite búsqueda semántica sobre documentos",
382 + "Los embeddings convierten texto en vectores numéricos",
383 + "El jamón ibérico de bellota se diferencia por la dieta del cerdo",
384 + ]
385 + metadatos = [
386 + {"tema": "linux"},
387 + {"tema": "linux"},
388 + {"tema": "cocina"},
389 + {"tema": "ia"},
390 + {"tema": "programacion"},
391 + {"tema": "ia"},
392 + {"tema": "ia"},
393 + {"tema": "cocina"},
394 + ]
395 + ids = [f"doc_{i}" for i in range(len(docs))]
396 +
397 + collection.add(documents=docs, metadatas=metadatos, ids=ids)
398 + ok(f"{len(docs)} documentos indexados en ChromaDB")
399 +
400 + # Búsqueda
401 + consultas = [
402 + "¿Qué framework usar para backend?",
403 + "Dime algo de cocina",
404 + ]
405 +
406 + for consulta in consultas:
407 + print(f'\n {NEGR}🔍 Consulta:{FIN} "{consulta}"')
408 + results = collection.query(query_texts=[consulta], n_results=3)
409 +
410 + for doc, meta, dist in zip(
411 + results["documents"][0], results["metadatas"][0], results["distances"][0]
412 + ):
413 + conf = 1 - dist
414 + barra = "█" * int(conf * 20) + "░" * (20 - int(conf * 20))
415 + print(f" {barra} {conf:.1%} [{meta['tema']}] {doc}")
416 +
417 + # Filtro por metadatos
418 + print(f" 🔽 Con filtro [tema=linux]:")
419 + filtrados = collection.query(
420 + query_texts=[consulta], n_results=2, where={"tema": "linux"}
421 + )
422 + for doc in filtrados["documents"][0]:
423 + print(f" 🐧 {doc}")
424 +
425 + # Persistencia
426 + if os.path.exists(DIR_CHROMA):
427 + ok(f"Datos persistentes en {DIR_CHROMA}/")
428 + info("Cierra y abre Python — los datos siguen ahí")
429 +
430 + # Limpiar
431 + shutil.rmtree(DIR_CHROMA)
432 + info("ChromaDB limpiada (demo temporal)")
433 +
434 +
435 + # ─── MAIN ──────────────────────────────────────────────────────────────────
436 +
437 + PASOS = {
438 + 1: ("Crear documentos de prueba", crear_documentos, True),
439 + 2: ("Búsqueda semántica", busqueda_semantica, True),
440 + 3: ("Clasificación", clasificacion, True),
441 + 4: ("Deduplicación", deduplicacion, True),
442 + 5: ("Recomendación", recomendacion, True),
443 + 6: ("ChromaDB", chroma_demo, False),
444 + }
445 +
446 +
447 + def listar_pasos():
448 + print(f"\n{'=' * 60}")
449 + print(f" PASOS DISPONIBLES")
450 + print(f"{'=' * 60}")
451 + for n in sorted(PASOS):
452 + nombre, _, esencial = PASOS[n]
453 + req = " (requiere chromadb)" if not esencial else ""
454 + print(f" {n}. {nombre}{req}")
455 + print(f"\n --fast omite el paso 6 (ChromaDB)")
456 + print(f" --interactive pide confirmación entre pasos\n")
457 +
458 +
459 + def ejecutar_paso(n, limpiar_al_final=False):
460 + if n not in PASOS:
461 + warn(f"Paso {n} no válido. Usa --list para ver los pasos.")
462 + return
463 + nombre, fn, _ = PASOS[n]
464 + info(f"Ejecutando paso {n}: {nombre}...")
465 + fn()
466 + if limpiar_al_final and os.path.exists(DIR_DOCS):
467 + shutil.rmtree(DIR_DOCS)
468 +
469 +
470 + def main():
471 + parser = argparse.ArgumentParser(
472 + description="Episodio 817 — Embeddings para todo. Demo completa.",
473 + formatter_class=argparse.RawDescriptionHelpFormatter,
474 + epilog=textwrap.dedent("""\
475 + Ejemplos:
476 + python3 setup_completo.py # todo
477 + python3 setup_completo.py --fast # sin ChromaDB
478 + python3 setup_completo.py --list # listar pasos
479 + python3 setup_completo.py --step 3 # solo paso 3
480 + python3 setup_completo.py --step 1,3,5 # pasos 1,3,5
481 + python3 setup_completo.py --interactive # paso a paso
482 + """),
483 + )
484 + parser.add_argument("--fast", action="store_true", help="Omite ChromaDB (paso 6)")
485 + parser.add_argument(
486 + "--list", "-l", action="store_true", help="Lista los pasos disponibles"
487 + )
488 + parser.add_argument(
489 + "--step",
490 + "-s",
491 + type=str,
492 + help="Ejecuta paso(s) específicos: 3, 1,3,5, 1-4",
493 + )
494 + parser.add_argument(
495 + "--interactive",
496 + "-i",
497 + action="store_true",
498 + help="Ejecuta paso a paso con confirmación",
499 + )
500 + args = parser.parse_args()
501 +
502 + if args.list:
503 + listar_pasos()
504 + return
505 +
506 + print(f"""
507 + {NEGR}🚀 EPISODIO 817 — Embeddings para todo{FIN}
508 + {NEGR} Demo completa: búsqueda, clasificación, deduplicación, recomendación, ChromaDB{FIN}
509 +
510 + {NEGR}Requisitos:{FIN}
511 + • Ollama en {OLLAMA_URL} → {VERDE}ollama serve{FIN}
512 + • Modelo: {MODEL} → {VERDE}ollama pull {MODEL}{FIN}
513 + • Python 3.10+
514 + • pip install chromadb (opcional, para el bloque 6)
515 + """)
516 +
517 + if not check_ollama():
518 + sys.exit(1)
519 +
520 + # Resolver qué pasos ejecutar
521 + if args.step:
522 + pasos = set()
523 + for parte in args.step.split(","):
524 + parte = parte.strip()
525 + if "-" in parte:
526 + a, b = parte.split("-", 1)
527 + pasos.update(range(int(a), int(b) + 1))
528 + else:
529 + pasos.add(int(parte))
530 + pasos = sorted(pasos)
531 + info(f"Ejecutando pasos: {', '.join(str(p) for p in pasos)}")
532 + for p in pasos:
533 + ejecutar_paso(p)
534 + if os.path.exists(DIR_DOCS):
535 + shutil.rmtree(DIR_DOCS)
536 + elif args.interactive:
537 + info(
538 + "Modo interactivo: presiona Enter para avanzar, 's' para saltar, 'q' para salir"
539 + )
540 + for n in sorted(PASOS):
541 + nombre, fn, esencial = PASOS[n]
542 + if args.fast and n == 6:
543 + info(f"Paso {n} omitido (--fast)")
544 + continue
545 + r = (
546 + input(f"\n{NEGR}¿Ejecutar paso {n}: {nombre}?{FIN} [Enter/s/q] ")
547 + .strip()
548 + .lower()
549 + )
550 + if r == "q":
551 + info("Interrumpido por el usuario")
552 + break
553 + if r == "s":
554 + info(f"Paso {n} saltado")
555 + continue
556 + fn()
557 + if os.path.exists(DIR_DOCS):
558 + shutil.rmtree(DIR_DOCS)
559 + else:
560 + crear_documentos()
561 + busqueda_semantica()
562 + clasificacion()
563 + deduplicacion()
564 + recomendacion()
565 +
566 + if not args.fast:
567 + chroma_demo()
568 + else:
569 + info("Modo rápido: omitiendo ChromaDB")
570 + info("Ejecuta sin --fast para incluir ChromaDB")
571 +
572 + if os.path.exists(DIR_DOCS):
573 + shutil.rmtree(DIR_DOCS)
574 +
575 + print(f"\n{NEGR}{'=' * 60}{FIN}")
576 + print(f"\n{VERDE}🎉 Demo completada{FIN}")
577 + print(f" • 6 casos de uso ejecutados")
578 + print(f" • 1 modelo de embeddings ({MODEL})")
579 + print(f" • 0 modelos entrenados, 0 GPUs, 0€")
580 + print(
581 + f"\n {AZUL}📖 Escaleta completa en: episodio-817-escaleta-embeddings.md{FIN}"
582 + )
583 + print(f" {AZUL}🔗 https://github.com/aejimmi/fail2ban-rs{FIN}")
584 +
585 +
586 + if __name__ == "__main__":
587 + main()
Neuer Älter