Leo, me gusta resaltar cosas (uso un Kindle). Siento que al leer no consigo retener más del 10% de la información que consumo, pero es al releer los puntos destacados o resumir el libro usándolos lo que realmente me hace entender lo que leo.
El problema es que, a veces, termino resaltando mucho.
Y por mucho quiero decir MUCHO. Ni siquiera podemos llamarlas “notas clave”.
Entonces, en esos casos, después de leer el libro, termino perdiendo mucho tiempo resumiendo o simplemente dejo de hacerlo (esto último es lo más común).
Hace poco leí un libro que disfruté bastante y me gustaría retener plenamente lo que más me llamó la atención. Pero, repito, fue uno de esos libros que destaqué demasiado.
Y no quería dedicar gran parte de mi escaso tiempo libre a ello. Entonces decidí automatizar el proceso y utilizar mis habilidades tecnológicas y de datos. Como estoy contento con el resultado, pensé en compartirlo para que cualquier persona interesada pueda aprovechar esta herramienta también.
Descargo de responsabilidad: mi Kindle es bastante antiguo, por lo que esto también debería funcionar en los nuevos. De hecho, existe un enfoque ligeramente mejor para las nuevas versiones de Kindle (que también se explica en esta publicación).
El proyecto
Definamos el objetivo: generar un resumen de nuestros aspectos más destacados de Kindle.
Mientras pensaba en ello, imaginé el siguiente canal simple para un solo libro:
Obtener los aspectos más destacados del libro Crear un RAG o algo similar Exportar el resumen
El resultado es diferente en la primera parte, pero todo se debe al preprocesamiento necesario teniendo en cuenta cómo están estructurados los datos.
Así que estructuraré esta publicación en dos secciones principales:
Recuperación y procesamiento de datos Modelo y salida de IA
1. Recuperación y procesamiento de datos
Mi intuición me dijo que había una manera de extraer los aspectos más destacados de mi Kindle. Al final, están almacenados allí, así que sólo necesito una forma de sacarlos.
Hay varias formas de hacerlo, pero quería un enfoque que funcionara tanto con los libros comprados en la tienda oficial de Kindle como con los archivos PDF o archivos que envié desde mi computadora portátil.
Y también decidí que no usaría ningún software existente para extraer los datos. Solo mi libro electrónico y mi computadora portátil (y un USB que los conecta a ambos).
Por suerte para nosotros, no es necesario hacer jailbreak y hay dos formas de hacerlo dependiendo de tu versión de Kindle:
Todos los Kindles (presumiblemente) tienen un archivo en la carpeta de documentos llamado My Clippings.txt. Literalmente contiene cualquier recorte que hayas realizado en cualquier momento y en cualquier libro. Los nuevos Kindles también tienen un archivo SQLite en el directorio del sistema llamado annotations.db. Esto tiene tus aspectos más destacados de una manera más estructurada.
En esta publicación usaré el método 1 (Mis recortes.txt) principalmente porque mi Kindle no tiene la base de datos annotations.db. Pero si tiene la suerte de tener la base de datos, úsela, ya que será más sencilla y de mayor calidad (la mayor parte del preprocesamiento que veremos a continuación probablemente no sea necesario).
Así que obtener los recortes es tan fácil como leer el TXT. A continuación se muestran algunos aspectos y problemas clave que encontré al utilizar este método:
Todos los libros están en el mismo archivo. No estoy seguro de la definición exacta de "recorte" por parte de Amazon, pero según lo he visto es: cualquier cosa que resaltes en cualquier punto. Incluso si lo eliminas o lo expandes, el original permanecerá en el TXT. Supongo que esto es así porque, de hecho, estamos trabajando con un archivo TXT y es muy difícil eliminar cosas que no están indexadas de ninguna manera. Hay un límite para el recorte: no conozco el umbral exacto, pero una vez que lo cruzamos, no podemos recuperar más recortes. Esto se hace porque, de lo contrario, alguien podría resaltar el libro completo, extraerlo y compartirlo ilegalmente.
Y esta es la anatomía de un recorte:
========== Nombre del libro (Nombre del autor) – Lo más destacado en la página 145 | Ubicación 2212-2212 | Agregado el domingo 30 de agosto de 2020 23:25:29 el problema de transparencia termina en el mismo lugar que ==========
Entonces, el primer paso es analizar los aspectos más destacados, y aquí es donde comenzamos a ver el código Python:
def parse_clippings(file_path): raw = Ruta(file_path).read_text(encoding="utf-8") entradas = raw.split("==========") aspectos destacados =[]para entrada en entradas: líneas = [l.strip() for l in Entry.strip().split("n") if l.strip()] if len(lines) < 3: continuar libro = líneas[0]si "Resaltar" no está en las líneas[1]: continuar location_match = re.search(r"Ubicación (d+)", líneas[1]) si no, ubicación_match: continuar ubicación = int(ubicación_match.group(1)) texto = " ".join(lines[2:]).strip() highlights.append( { "libro": libro, "ubicación": ubicación, "texto": texto } ) devolver aspectos destacados
Dada la ruta del archivo de recortes, todo lo que hace esta función es dividir el texto en las diferentes entradas y luego recorrerlas. Para cada entrada, extrae el nombre del título, la ubicación y el texto resaltado.
Esta estructura final (una lista de diccionarios) facilita el filtrado por libro:
[ h para h en resaltados si book_name.lower() en h["book"].lower() ]
Una vez filtrados, debemos ordenar lo más destacado. Dado que los recortes se adjuntan al archivo TXT, el orden se basa en cuándo resaltamos, no en la ubicación del texto.
Y personalmente quiero que mis resultados aparezcan como aparecen en el libro, por lo que es necesario realizar el pedido:
ordenado(destacados, clave=lambda x: x["ubicación"])
Ahora, si revisa su archivo de recortes, es posible que encuentre recortes duplicados (o subrecortes duplicados). Esto sucede porque cada vez que editas un resaltado (que no incluyeste todas las palabras a las que apuntaste, por ejemplo), se contabiliza como uno nuevo. Así que habrá dos recortes muy similares en el TXT. O incluso más si lo editas muchas veces.
Necesitamos manejar esto aplicando de alguna manera una deduplicación. Es más fácil de lo esperado:
def deduplicar (destacados): limpiar =[]para h en resaltados: texto = h["texto"] duplicado = Falso para c en limpio: si texto == c["texto"]: duplicado = Verdadero romper si texto en c["texto"]: duplicado = Verdadero romper si c["texto"] en texto: c["texto"] = texto duplicado = Verdadero romper si no está duplicado: clean.append(h) devolver limpio
Es muy sencillo y se podría perfeccionar, pero básicamente comprobamos si hay recortes consecutivos con el mismo texto (o parte del mismo) y conservamos el más largo.
En este momento tenemos los aspectos más destacados del libro correctamente ordenados y podríamos detener el preprocesamiento aquí. Pero no puedo hacer eso. Me gusta resaltar títulos cada vez porque, al resumir, puedo asignar correctamente una sección a cada resaltado.
Pero nuestro código no es capaz de diferenciar entre un punto destacado real y un título de sección… todavía. Vea abajo:
def is_probable_title(texto): texto = text.strip() si len(texto) > 120: devuelve False si text.endswith("."): devuelve Palabras falsas = text.split() si len(palabras) > 12: devuelve False # prefijo de estilo de capítulo si has_chapter_prefix(texto): devuelve True # índice de capitalización en mayúscula = suma (1 para w en palabras si w[0].isupper() ) cap_ratio = mayúsculas / len(palabras) # proporción de palabras de parada stopword_count = suma( 1 para w en palabras si w.lower() en STOPWORDS ) stop_ratio = stopword_count / len(palabras) puntuación = 0 si cap_ratio > 0,6: puntuación += 1 si stop_ratio < 0,3: puntuación += 1 si len(palabras) <= 6: puntuación += 1 puntuación de devolución >= 2
Puede parecer bastante arbitrario y no es la mejor solución para este problema, pero funciona bastante bien. Utiliza una heurística basada en mayúsculas, longitud, palabras vacías y prefijos.
Esta función se llama dentro de un bucle a través de todos los resaltados, como hemos visto en funciones anteriores, para verificar si un resaltado es un título o no. El resultado es una lista de "secciones" de diccionarios donde el diccionario tiene dos claves:
Título: el título de la sección. Lo más destacado: lo más destacado de la sección.
Ahora mismo sí, estamos listos para resumir.
Modelo y resultados de IA
Quería que este fuera un proyecto gratuito, por lo que necesitamos un modelo de IA que sea de código abierto.
Pensé que Ollama[1]fue una de las mejores opciones para ejecutar un proyecto como este (al menos localmente). Además, nuestros datos siempre siguen siendo nuestros con ellos y podemos ejecutar los modelos sin conexión.
Una vez instalado, el código fue fácil. No soy un ingeniero rápido, por lo que cualquiera con los conocimientos obtendría resultados aún mejores, pero esto es lo que funciona para mí:
def resume_with_ollama(text, model): Prompt = f""" Estás resumiendo un libro a partir de los aspectos más destacados del lector. Produce un resumen estructurado con: – Tesis principal – Breve resumen – Ideas clave – Conceptos importantes – Conclusiones prácticas Aspectos destacados: {text} """ result = subprocess.run( ["ollama", "run", model], input=prompt, text=True, capture_output=True ) return result.stdout
Sencillo, lo sé. Pero funciona en parte porque el preprocesamiento de datos ha sido intenso, pero también porque ya estamos aprovechando los modelos creados allí.
¿Pero qué hacemos con el resumen? Me gusta usar obsidiana[2]entonces exportar un archivo Markdown es lo que tiene más sentido. Aquí lo tienes:
def export_markdown(libro, secciones, resumen, salida): md = f"# {libro}nn" para la sección en secciones: md += f"## {sección['título']}nn" para h en la sección["destacados"]: md += f"- {h}n" md += "n" md += "n—nn" md += "## Resumen del libronn" md += resumen ruta_salida = Ruta(salida) ruta_salida.parent.mkdir(parents=True, exist_ok=True) ruta_salida.write_text(md, codificación="utf-8") print(f"nGuardado en {ruta_salida}")
Et voilá.
¡Y así es como paso de lo más destacado a un resumen completo de Markdown (directo a Obsidian si quiero) con menos de 300 líneas de código Python!
Código completo y prueba
Aquí está el código completo, por si quieres copiarlo y pegarlo. Contiene lo que hemos visto además de algunas funciones auxiliares y análisis de argumentos:
importar re importar argparse desde pathlib import Ruta importar subproceso # ———- PARSE CLIPPINGS ———- def parse_clippings(file_path): raw = Ruta(file_path).read_text(encoding="utf-8") entradas = raw.split("==========") aspectos destacados =[]para entrada en entradas: líneas = [l.strip() for l in Entry.strip().split("n") if l.strip()] if len(lines) < 3: continuar libro = líneas[0]si "Resaltar" no está en las líneas[1]: continuar location_match = re.search(r"Ubicación (d+)", líneas[1]) si no coincide con la ubicación: continuar ubicación = int(ubicación_match.group(1)) texto = " ".join(lines[2:]).strip() highlights.append( { "libro": libro, "ubicación": ubicación, "texto": texto } ) devolver resaltados # ———- FILTRAR LIBRO ———- def filter_book(destacados, nombre_libro): regresa [ h para h en resaltados si nombre_libro.lower() en h["book"].lower() ] # ———- ORDENAR ———- def ordenar_por_ubicación(destacados): devolver ordenado(destacados, clave=lambda x: x["ubicación"]) # ———- DEDUPLICATE ———- def deduplicar(destacados): limpiar =[]para h en resaltados: texto = h["texto"] duplicado = Falso para c en limpio: si texto == c["texto"]: duplicado = Verdadero romper si texto en c["texto"]: duplicado = Verdadero romper si c["texto"] en texto: c["texto"] = texto duplicado = Verdadero romper si no está duplicado: clean.append(h) return clean # ———- DETECCIÓN DE TÍTULO ———- PALABRAS DE PARADA = { "el", "y", "o", "pero", "de", "en", "en", "para", "para", "es", "son", "fueron", "ser", "ser", "ser", "que", "esto", "con", "como", "por", "de" } def has_chapter_prefix(texto): return bool( re.match( r"^(capítulo|parte|sección)s+d+|^d+[.)]|^[ivxlcdm]+.", text.lower() ) ) def is_probable_title(texto): texto = text.strip() si len(texto) > 120: devuelve Falso si text.endswith("."): devuelve Palabras falsas = text.split() si len(palabras) > 12: devuelve Falso # prefijo de estilo de capítulo si tiene_chapter_prefix(texto): devuelve Verdadero # índice de capitalización en mayúscula = suma (1 para w en palabras si w[0].isupper() ) cap_ratio = mayúsculas / len(palabras) # proporción de palabras de parada stopword_count = suma( 1 para w en palabras si w.lower() en STOPWORDS ) stop_ratio = stopword_count / len(palabras) puntuación = 0 si cap_ratio > 0,6: puntuación += 1 si stop_ratio < 0,3: puntuación += 1 si len(palabras) <= 6: puntuación += 1 puntuación de retorno >= 2 # ———- GRUPO DE SECCIONES ———- def group_by_sections(destacados): secciones =[]actual = { "título": "Introducción", "aspectos destacados":[]} para h en aspectos destacados: texto = h["texto"] if is_probable_title(texto): secciones.append(actual) actual = { "título": texto, "destacados":[]} else: current["highlights"].append(texto) secciones.append(current) devolver secciones # ———- RESUMEN ———- # ———- EXPORTAR MARKDOWN ———- def export_markdown(libro, secciones, resumen, salida): md = f"# {libro}nn" para sección en secciones: md += f"## {sección['título']}nn" para h en sección["highlights"]: md += f"- {h}n" md += "n" md += "n—nn" md += "## Resumen del libronn" md += resumen ruta_salida = Ruta(salida) ruta_salida.parent.mkdir(parents=True, exist_ok=True) ruta_salida.write_text(md, codificación="utf-8") print(f"nGuardado en {ruta_salida}") # ———- PRINCIPAL ———- def principal(): analizador = argparse.ArgumentParser() parser.add_argument("–libro", requerido=True) parser.add_argument("–salida", requerido=Falso, predeterminado=Ninguno) parser.add_argument( "–recortes", predeterminado="Datos/Mis recortes.txt" ) parser.add_argument( "–model", default="mistral" ) args = parser.parse_args() destacados = parse_clippings(args.clippings) destacados = filter_book(destacados, args.book) destacados = sort_by_location(destacados) destacados = deduplicar(destacados) secciones = group_by_sections(destacados) all_text = "n".join( h["texto"] para h en aspectos destacados) resumen = resume_with_ollama(all_text, args.model) if args.output: export_markdown( args.book, secciones, resumen, args.output) else: print("n—- DESTACADOS —-n") para h en aspectos destacados: print(f"{h['text']}n") print("n—- RESUMEN —-n") imprimir (resumen) si __nombre__ == "__principal__": principal()
¡Pero veamos cómo funciona! El código en sí es útil pero apuesto a que estás dispuesto a ver los resultados. Es largo así que decidí eliminar la primera parte ya que lo único que hace es copiar y pegar lo más destacado.
Elegí al azar un libro que leí hace como 6 años (2020) llamado Hablando con extraños de Malcolm Gladwell (un éxito de ventas, una lectura bastante agradable). Vea la salida impresa del modelo (no el Markdown):
$ python3 kindle_summary.py –book "Hablar con extraños" —- DESTACADOS —- … —- RESUMEN —- Título: Hablar con extraños: Lo que debemos saber sobre la interacción humana Tesis principal: El libro explora las complejidades y paradojas de la interacción humana, particularmente en las conversaciones con extraños, y enfatiza la importancia de la precaución, la humildad y la comprensión del contexto en el que ocurren estas interacciones. Breve resumen: el autor profundiza en los conceptos erróneos y las deficiencias en nuestro trato con extraños, enfocándose en cómo a menudo hacemos suposiciones incorrectas sobre los demás basadas en información limitada o nociones preconcebidas. El libro ofrece información sobre por qué sucede esto, sus consecuencias y estrategias para mejorar nuestra capacidad de comprender y comunicarnos eficazmente con personas que no conocemos. Ideas clave: 1. El problema de la transparencia y el problema de la verdad por defecto: la gente suele asumir que los demás son libros abiertos que comparten sus verdaderas emociones e intenciones, cuando en realidad no siempre es así. 2. Acoplamiento: Los comportamientos están fuertemente vinculados a circunstancias y condiciones específicas, por lo que es esencial comprender el contexto en el que opera un extraño. 3. Limitaciones para comprender a los extraños: no existe un mecanismo perfecto para escudriñar las mentes de aquellos que no conocemos, lo que enfatiza la necesidad de moderación y humildad al interactuar con extraños. Conceptos importantes: 1. Respuestas emocionales que quedan fuera de las expectativas 2. Incumplimiento de la verdad 3. La transparencia como una ilusión 4. Comprensión contextual al tratar con extraños 5. La paradoja de hablar con extraños (necesidad versus terribleidad) 6. El fenómeno del acoplamiento y su influencia en el comportamiento 7. Culpar al extraño cuando las cosas salen mal Conclusiones prácticas: 1. Reconocer que las personas no siempre aparecen como parecen, tanto emocional como conductualmente. 2. Comprender la importancia del contexto al interpretar los comportamientos e intenciones de los extraños. 3. Ser cautelosos y humildes al interactuar con extraños, reconociendo nuestras limitaciones para comprenderlos plenamente. 4. Evite sacar conclusiones precipitadas sobre extraños basándose en información limitada o nociones preconcebidas. 5. Acepte que siempre habrá cierto grado de ambigüedad y complejidad al tratar con extraños. 6. Evitar penalizar a otros por no recurrir a la verdad como mecanismo de defensa. 7. Cuando las interacciones con extraños salen mal, considere el papel que uno podría haber desempeñado al contribuir a la situación en lugar de culpar únicamente al extraño.
Y todo ello en unos segundos. Bastante genial en mi opinión.
Conclusión
Y así es básicamente como ahora estoy ahorrando mucho tiempo libre (que puedo usar para escribir publicaciones como esta) aprovechando mis habilidades con datos y mi inteligencia artificial.
¡Espero que hayas disfrutado la lectura y te hayas sentido motivado para intentarlo! No será mejor que el resumen que escribirías con tu propia percepción del libro… ¡Pero no estará lejos de eso!
Gracias por su atención, no dude en comentar si tiene alguna idea o sugerencia.
Recursos
[1]Ollama. (Dakota del Norte). Ollama. https://ollama.com
[2]Obsidiana. (Dakota del Norte). Obsidiana. https://obsidiana.md