-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtlon-ai.el
1816 lines (1607 loc) · 95.8 KB
/
tlon-ai.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; tlon-ai.el --- AI functionality for Tlön -*- lexical-binding: t -*-
;; Copyright (C) 2025
;; Author: Pablo Stafforini
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; AI functionality for Tlön.
;;; Code:
(require 'gptel)
(require 'gptel-curl)
(require 'gptel-extras)
(require 'shut-up)
(require 'tlon)
(require 'tlon-core)
(require 'tlon-tex) ; needed to set variables correctly
(require 'tlon-counterpart)
;;;; User options
(defgroup tlon-ai nil
"AI functionality for Tlön."
:group 'tlon)
(defcustom tlon-ai-batch-fun nil
"Function to run in batch mode."
:type 'symbol
:group 'tlon-ai)
(defcustom tlon-ai-overwrite-alt-text nil
"Whether to overwrite existing alt text in images.
This variable only affects the behavior of
`tlon-ai-set-image-alt-text-in-buffer'; it is ignored by
`tlon-ai-set-image-alt-text', which always overwrites."
:type 'boolean
:group 'tlon-ai)
(defcustom tlon-ai-edit-prompt nil
"Whether to edit the prompt before sending it to the AI model."
:type 'boolean
:group 'tlon-ai)
(defcustom tlon-ai-auto-proofread nil
"Whether to automatically proofread reference articles."
:type 'boolean
:group 'tlon-ai)
;;;;; Custom models
(defcustom tlon-ai-summarization-model
'("Gemini" . gemini-2.0-flash-thinking-exp-01-21)
"Model to use for summarization.
The value is a cons cell whose car is the backend and whose cdr is the model
itself. See `gptel-extras-ai-models' for the available options. If nil, do not
use a different model for summarization.
Note that the selected model should have a large context window, ideally larger
than 1m tokens, since otherwise some books will not be summarized."
:type '(cons (string :tag "Backend") (symbol :tag "Model"))
:group 'tlon-ai)
(defcustom tlon-ai-markdown-fix-model
'("Gemini" . gemini-2.0-flash-thinking-exp-01-21)
"Model to use for fixing the Markdown.
The value is a cons cell whose car is the backend and whose cdr is the model
itself. See `gptel-extras-ai-models' for the available options. If nil, do not
use a different model for fixing the Markdown."
:type '(cons (string :tag "Backend") (symbol :tag "Model"))
:group 'tlon-ai)
(defcustom tlon-ai-create-reference-article-model nil
"Model to use for creating reference articles.
The value is a cons cell whose car is the backend and whose cdr is the model
itself. See `gptel-extras-ai-models' for the available options. If nil, do not
use a different model for fixing the Markdown."
:type '(cons (string :tag "Backend") (symbol :tag "Model"))
:group 'tlon-ai)
(defcustom tlon-ai-proofread-reference-article-model
'("ChatGPT" . gpt-4.5-preview)
"Model to use for proofreading reference articles.
The value is a cons cell whose car is the backend and whose cdr is the model
itself. See `gptel-extras-ai-models' for the available options. If nil, do not
use a different model for fixing the Markdown."
:type '(cons (string :tag "Backend") (symbol :tag "Model"))
:group 'tlon-ai)
;;;; Variables
(defvar tlon-ai-retries 0
"Number of retries for AI requests.")
(defconst tlon-ai-string-wrapper
":\n\n```\n%s\n```\n\n"
"Wrapper for strings to be passed in prompts.")
(defconst tlon-gptel-error-message
"`gptel' failed with message: %s"
"Error message to display when `gptel-quick' fails.")
;;;;; Language detection
(defconst tlon-ai-detect-language-common-prompts
(format ":%s. Your answer should just be the language of the entry. For example, if you conclude that the language is English, your answer should be just 'english'. Moreover, your answer can be only one of the following languages: %s"
tlon-ai-string-wrapper
(mapconcat 'identity (mapcar (lambda (language)
(plist-get language :name))
tlon-languages-properties)
", "))
"Common prompts for language detection.")
(defconst tlon-ai-detect-language-prompt
(format "Please guess the language of the following text%s"
tlon-ai-detect-language-common-prompts)
"Prompt for language detection.")
(defconst tlon-ai-detect-language-bibtex-prompt
(format "Please guess the language of the work described in following BibTeX entry%s"
tlon-ai-detect-language-common-prompts)
"Prompt for language detection.")
;;;;; Translation
(defconst tlon-ai-translate-prompt
(format "Translate the following text into Spanish:%s" tlon-ai-string-wrapper)
"Prompt for translation.")
;; TODO: generalize to arbitrary langs
(defconst tlon-ai-translate-variants-prompt
(format "Please generate the best ten Spanish translations of the following English text:%s. Please return each translation on the same line, separated by '|'. Do not add a space either before or after the '|'. Do not precede your answer by 'Here are ten Spanish translations' or any comments of that sort: just return the translations. An example return string for the word 'very beautiful' would be: 'muy bello|muy bonito|muy hermoso|muy atractivo' (etc). Thanks!" tlon-ai-string-wrapper)
"Prompt for translation variants.")
;;;;; Writing
;; TODO: instruct the model to use `Cite' tags in Chicago-style citations
(defconst tlon-ai-write-reference-article-prompt
`((:prompt "You are an encyclopedia writer, and are currently writing a series of articles for an encyclopedia of effective altruism. Please write an entry on the topic of ‘%1$s’.\n\nYou should write the article *primarily* based on the text files attached, though you may also rely on your general knowledge of the topic. Each of these articles discusses the topic of the entry. So you should inspect each of these files closely and make an effort to understand what they claim thoroughly. Then, once you have inspected and understood the contents of all of these files, make a synthesis of the topic (%1$s) and write the article based on this synthesis.\n\nWrite the article in a sober, objective tone, avoiding cliches, excessive praise and unnecessary flourishes. In other words, draft it as if you were writing an article for a reputable encyclopedia, such as the Encyclopaedia Britannica (but remember that this is not a general encyclopedia, but specifically an encyclopdia of effective altruism, so it should be written from that perspective).\n\nWhen you make a claim traceable to a specific source, please credit this source in a footnote. Do not include a references section at the end."
:language "en")
(:prompt "Eres un escritor de enciclopedias y estás escribiendo una serie de artículos para una enciclopedia sobre el altruismo eficaz. Por favor, escribe una entrada sobre el tema ‘%1$s’.\n\nDebes escribir el artículo *principalmente* basándote en los archivos de texto adjuntos, aunque también puedes tener en cuenta tu conocimiento general del tema. Cada uno de estos artículos trata el tema de la entrada. Por lo tanto, debes examinar detenidamente cada uno de estos archivos y esforzarte por comprender a fondo lo que sostiene. Luego, una vez que hayas inspeccionado y comprendido el contenido de todos estos archivos, haz una síntesis del tema (%1$s) y escribe el artículo basándote en esta síntesis.\n\nAdjunto también un glosario sobre terminología relacionada con el altruismo eficaz. Procura utilizar estos términos para vertir al castellano expresiones peculiares de ese movimiento.\n\nEscribe el artículo en un tono sobrio y objetivo, evitando clichés, elogios excesivos y florituras innecesarias. En otras palabras, redáctalo como si estuvieras escribiendo un artículo para una enciclopedia de prestigio, como la Encyclopaedia Britannica (pero recuerda que no se trata de una enciclopedia general, sino específicamente de una enciclopedia aobre el altruismo eficaz, por lo que debe redactarse desde esa perspectiva).\n\nCuando hagas una afirmación que pueda atribuirse a una fuente específica, menciona dicha fuente en una nota al pie. No incluyas una sección de referencias al final."
:language "es"))
"Prompt for writing a reference article.")
(defconst tlon-ai-proofread-reference-article-prompt
`((:prompt "You are an expert proofreader. Please proofread the following article. The article is intended for an encyclopedia of effective altruism. Your task is to correct any errors you find, especially factual errors, calculation errors, and any other errors that you think are important.\n\n%s"
:language "en")
(:prompt "Eres un corrector experto. Por favor, corrije el siguiente artículo. El artículo está destinado a una enciclopedia sobre altruismo eficaz. Tu tarea consiste en corregir los errores que encuentres, especialmente errores fácticos, errores de cálculo y cualquier otro error que consideres importante.\n\n%s"
:language "es")))
;;;;; Rewriting
(defconst tlon-ai-rewrite-prompt
(format "Por favor, genera las mejores diez variantes del siguiente texto castellano:%s. Por favor, devuelve todas las variantes en una única linea, separadas por '|'. No insertes un espacio ni antes ni después de '|'. No agregues ningún comentario aclaratorio: solo necesito la lista de variantes. A modo de ejemplo, para la expresión 'búsqueda de poder' el texto a devolver sería: 'ansia de poder|ambición de poder|búsqueda de autoridad|sed de poder|afán de poder|aspiración de poder|anhelo de poder|deseo de control|búsqueda de dominio|búsqueda de control' (esta lista solo pretende ilustrar el formato en que debes presentar tu respuesta). Gracias!" tlon-ai-string-wrapper)
"Prompt for rewriting.")
;;;;; File comparison
;; this should be a general category that compares an original file and its
;; translation, and admits of specific prompts, with ‘fix formatting’ being one
;; of these species
;; this does not need to be translated
(defconst tlon-ai-fix-markdown-format-prompt
`((:prompt "Please take a look at the two paragraphs attached (paragraphs may contain only one word). The first, ‘%s’, is taken from the original document in %s, while the second, ‘%s’, is taken from a translation of that document into %s. In the translation, some of the original formatting (which includes not only Markdown elements but potentially HTML components and SSML tags) has been lost or altered. What I want you to do is to generate a new paragraph with all the original formatting restored, but without changing the text of the translation. Add missing links. Add missing asterisks. Add any other missing markdown signs. Don't add any missing text. Don't change the name of the files referred to as images. And please do not surround the text in backticks. Add missing links. Add missing asterisks. Add any other missing markdown signs. Just give the output but don't add comments or clarifications, even if there's nothing to restore. Thank you!"
:language "en")))
;;;;; Image description
(defconst tlon-ai-describe-image-prompt
`((:prompt "Please provide a concise description of the attached image. The description should consist of one or two sentences and must never exceed 50 words. If you need to use quotes, please use single quotes."
:language "en")
(:prompt "Por favor, describe brevemente la imagen adjunta. La descripción debe consistir de una o dos oraciones y en ningún caso debe exceder las 50 palabras. Si necesitas usar comillas, por favor utiliza comillas simples."
:language "es")
(:prompt "Veuillez fournir une description concise de l'image ci-jointe. La description doit consister en une ou deux phrases et ne doit pas dépasser 50 mots. Si vous devez utiliser des guillemets, veuillez utiliser des guillemets simples."
:language "fr")
(:prompt "Si prega di fornire una descrizione concisa dell'immagine allegata. La descrizione deve consistere in una o due frasi e non deve mai superare le 50 parole. Se è necessario utilizzare le virgolette, si prega di utilizzare le virgolette singole."
:language "it")
(:prompt "Bitte geben Sie eine kurze Beschreibung des beigefügten Bildes. Die Beschreibung sollte aus ein oder zwei Sätzen bestehen und darf 50 Wörter nicht überschreiten. Wenn Sie Anführungszeichen verwenden müssen, verwenden Sie bitte einfache Anführungszeichen."
:language "de")))
;;;;; Summarization
;;;;;; Abstracts
(defconst tlon-ai-how-to-write-abstract-prompt
`((:prompt ,(format "Write the abstract in a sober, objective tone, avoiding cliches, excessive praise and unnecessary flourishes. In other words, draft it as if you were writing the abstract of a scientific paper. The abstract should be only one paragraph long and have a rough length of 100 to 250 words (feel free to exceed it if you really need to, but never go over %s words). It should not mention bibliographic data of the work (such as title or author). Write the abstract directly stating what the article argues, rather than using phrases such as 'The article argues that...'. For example, instead of writing 'The article ‘The eradication of smallpox’ by William D. Tierney tells that mankind fought smallpox for centuries...', write 'Mankind fought smallpox for centuries...'. Also, please omit any disclaimers of the form 'As an AI language model, I'm unable to browse the internet in real-time.' Finally, end your abstract with the phrase ' – AI-generated abstract.'" tlon-tex-max-abstract-length)
:language "en")
(:prompt ,(format "Redacta el resumen en un tono sobrio y objetivo, evitando los lugares comunes, los elogios excesivos y las florituras innecesarias. En otras palabras, redáctalo como si estuvieras escribiendo el resumen de un artículo científico. El resumen debe constar de un solo párrafo y tener una extensión de unas 100 a 250 palabras (puedes exceder este umbral de ser necesario, pero el resumen no debe tener en ningún caso más de %s palabras). No debe mencionar datos bibliográficos de la obra (como el título o el autor). Escribe el resumen indicando directamente lo que argumenta el artículo, en lugar de utilizar frases como ‘El artículo argumenta que...’. Por ejemplo, en lugar de escribir ‘El artículo 'La erradicación de la viruela' de William D. Tierney sostiene que la humanidad luchó contra la viruela durante siglos...’, escribe ‘La humanidad luchó contra la viruela durante siglos...’. Además, omite cualquier descargo de responsabilidad del tipo ‘Como modelo de lenguaje de inteligencia artificial, no puedo navegar por Internet en tiempo real.’ Por último, termina tu resumen con la frase ‘ - Resumen generado por inteligencia artificial.’" tlon-tex-max-abstract-length)
:language "es")
(:prompt ,(format "Rédigez le résumé sur un ton sobre et objectif, en évitant les clichés, les éloges excessifs et les fioritures inutiles. En d'autres termes, rédigez-le comme si vous écriviez le résumé d'un article scientifique. Le résumé ne doit comporter qu'un seul paragraphe et avoir une longueur approximative de 100 à 250 mots (n'hésitez pas à le dépasser si vous en avez vraiment besoin, mais ne dépassez jamais %s mots). Il ne doit pas mentionner les données bibliographiques de l'ouvrage (telles que le titre ou l'auteur). Rédigez le résumé en indiquant directement ce que l'article soutient, plutôt qu'en utilisant des phrases telles que 'L'article soutient que...'. Par exemple, au lieu d'écrire 'L'article 'L'éradication de la variole' de William D. Tierney affirme que l'humanité a combattu la variole pendant des siècles...', écrivez 'L'humanité a combattu la variole pendant des siècles...'. Veuillez également omettre toute clause de non-responsabilité du type 'En tant que modèle linguistique de l'IA, je ne suis pas en mesure de naviguer sur l'internet en temps réel'. Enfin, terminez votre résumé par la phrase ' - Résumé généré par l'IA.'" tlon-tex-max-abstract-length)
:language "fr")
(:prompt ,(format "Scrivete l'abstract con un tono sobrio e oggettivo, evitando i cliché, le lodi eccessive e i fronzoli inutili. In altre parole, scrivetelo come se steste scrivendo l'abstract di un articolo scientifico. L'abstract dovrebbe essere lungo solo un paragrafo e avere una lunghezza approssimativa di 100-250 parole (sentitevi liberi di superarlo se ne avete davvero bisogno, ma non superate mai le %s di parole). Non deve riportare i dati bibliografici del lavoro (come il titolo o l'autore). Scrivete l'abstract indicando direttamente ciò che l'articolo sostiene, piuttosto che usare frasi come 'L'articolo sostiene che...'. Ad esempio, invece di scrivere 'L'articolo 'L'eradicazione del vaiolo' di William D. Tierney afferma che l'umanità ha combattuto il vaiolo per secoli...', scrivete 'L'umanità ha combattuto il vaiolo per secoli...'. Inoltre, omettete qualsiasi dichiarazione di non responsabilità del tipo 'Come modello linguistico dell'IA, non sono in grado di navigare in Internet in tempo reale'. Infine, terminate il vostro riassunto con la frase ' - riassunto generato dall'IA'." tlon-tex-max-abstract-length)
:language "it")
(:prompt ,(format "Schreiben Sie die Zusammenfassung in einem nüchternen, sachlichen Ton und vermeiden Sie Klischees, übermäßiges Lob und unnötige Schnörkel. Mit anderen Worten: Verfassen Sie sie so, als ob Sie die Zusammenfassung einer wissenschaftlichen Arbeit schreiben würden. Die Zusammenfassung sollte nur einen Absatz lang sein und eine ungefähre Länge von 100 bis 250 Wörtern haben (Sie können diese Zahl ruhig überschreiten, wenn es wirklich nötig ist, aber nie mehr als %s Wörter). Sie sollte keine bibliografischen Daten der Arbeit (wie Titel oder Autor) enthalten. Geben Sie in der Zusammenfassung direkt an, worum es in dem Artikel geht, und verwenden Sie keine Sätze wie 'In dem Artikel wird argumentiert, dass...'. Schreiben Sie zum Beispiel statt 'Der Artikel 'Die Ausrottung der Pocken' von William D. Tierney besagt, dass die Menschheit jahrhundertelang die Pocken bekämpfte...' lieber 'Die Menschheit bekämpfte die Pocken jahrhundertelang...'. Lassen Sie bitte auch Haftungsausschlüsse der Form 'Als KI-Sprachmodell bin ich nicht in der Lage, das Internet in Echtzeit zu durchsuchen' weg. Beenden Sie Ihre Zusammenfassung schließlich mit dem Satz ' - KI-generierte Zusammenfassung.'" tlon-tex-max-abstract-length)
:language "de")))
(defconst tlon-ai-get-abstract-prompts
`((:prompt ,(format "The following work may or may not contain an abstract%s. If it contains an abstract, please return it. Otherwise, create an abstract of it yourself. %s However, please omit this phrase if you are simply copying verbatim an abstract you found in the work."
tlon-ai-string-wrapper
(tlon-lookup tlon-ai-how-to-write-abstract-prompt
:prompt :language "en"))
:language "en")
(:prompt ,(format "La siguiente obra puede contener o no un resumen%s. Si contiene un resumen, devuélvelo. En caso contrario, crea tú mismo un resumen. %s Sin embargo, omite esta frase si simplemente está devolviendo un resumen que encontraste en la obra.En otras palabras, incluye la frase sólo cuando tú hayas creado el resumen."
tlon-ai-string-wrapper
(tlon-lookup tlon-ai-how-to-write-abstract-prompt
:prompt :language "es"))
:language "es")
(:prompt ,(format "L'œuvre suivante peut ou non contenir un résumé%s. S'il contient un résumé, veuillez le renvoyer. Sinon, créez un résumé vous-même. %s Toutefois, veuillez omettre cette phrase si vous ne faites que copier mot pour mot un résumé que vous avez trouvé dans l'œuvre."
tlon-ai-string-wrapper
(tlon-lookup tlon-ai-how-to-write-abstract-prompt
:prompt :language "fr"))
:language "fr")
(:prompt ,(format "Il seguente lavoro può contenere o meno un estratto%s. Se contiene un estratto, si prega di restituirlo. Altrimenti, creane uno tu stesso. %s Tuttavia, si prega di omettere questa frase se si sta semplicemente copiando alla lettera un estratto trovato nell'opera."
tlon-ai-string-wrapper
(tlon-lookup tlon-ai-how-to-write-abstract-prompt
:prompt :language "it"))
:language "it")
(:prompt ,(format "Das folgende Werk kann eine Zusammenfassung enthalten oder auch nicht%s. Wenn es eine Zusammenfassung enthält, geben Sie sie bitte zurück. Andernfalls erstellen Sie bitte selbst eine Zusammenfassung des Werks. %s Bitte lassen Sie diesen Satz jedoch weg, wenn Sie einfach eine wortwörtliche Zusammenfassung kopieren, die Sie in dem Werk gefunden haben."
tlon-ai-string-wrapper
(tlon-lookup tlon-ai-how-to-write-abstract-prompt
:prompt :language "de"))
:language "de"))
"Prompts for summarization.")
(defconst tlon-ai-shorten-abstract-prompts
`((:prompt ,(format "Please shorten the following abstract to %s words or less. The shortened version should consist of only one paragraph.%s"
tlon-tex-max-abstract-length
tlon-ai-string-wrapper)
:language "en")
(:prompt ,(format "Por favor, acorta el siguiente resumen a %s palabras o menos. La versión acortada debe constar de un solo párrafo.%s"
tlon-tex-max-abstract-length
tlon-ai-string-wrapper)
:language "es")
(:prompt ,(format "Veuillez raccourcir le résumé suivant à %s mots ou moins. La version raccourcie doit se composer d'un seul paragraphe.%s"
tlon-tex-max-abstract-length
tlon-ai-string-wrapper)
:language "fr")
(:prompt ,(format "Si prega di abbreviare il seguente abstract a %s parole o meno. La versione abbreviata deve essere composta da un solo paragrafo.%s"
tlon-tex-max-abstract-length
tlon-ai-string-wrapper)
:language "it")
(:prompt ,(format "Bitte kürzen Sie die folgende Zusammenfassung auf %s Wörter oder weniger. Die gekürzte Version sollte nur aus einem Absatz bestehen.%s"
tlon-tex-max-abstract-length
tlon-ai-string-wrapper)
:language "de"))
"Prompts for summarization.")
;;;;;; Synopsis
(defconst tlon-ai-get-synopsis-prompts
`((:prompt ,(format "Please write an detailed abstract of the following work%s Write it in a sober, objective tone, avoiding cliches, excessive praise and unnecessary flourishes. In other words, draft it as if you were writing the abstract of a scientific paper or academic publication. The summary should provide a detail account of the work’s main claims and arguments; it may be between one and two thousand words in length. Also, please omit any disclaimers of the form 'As an AI language model, I'm unable to browse the internet in real-time.'"
tlon-ai-string-wrapper)
:language "en")
(:prompt ,(format "Por favor, escribe un resumen detallado de la presente obra%s Redáctalo en un tono sobrio y objetivo, evitando cliches, elogios excesivos y florituras innecesarias. En otras palabras, redáctalo como si estuvieras escribiendo el resumen de un artículo científico o de una publicación académica. El resumen debe dar cuenta detallada de las principales afirmaciones y argumentos de la obra; su extensión puede oscilar entre mil y dos mil palabras. Por favor, omite también cualquier descargo de responsabilidad del tipo 'Como modelo de lenguaje de inteligencia artificial, no puedo navegar por Internet en tiempo real'." tlon-ai-string-wrapper)
:language "es")
(:prompt ,(format "Veuillez rédiger un résumé détaillé de ce travail%s Rédigez-le sur un ton sobre et objectif, en évitant les clichés, les éloges excessifs et les fioritures inutiles. En d'autres termes, rédigez-le comme si vous écriviez le résumé d'un article scientifique ou d'une publication universitaire. Le résumé doit fournir un compte rendu détaillé des principales revendications et des principaux arguments du travail ; il peut compter entre un et deux mille mots. Veuillez également omettre toute clause de non-responsabilité du type \"En tant que modèle de langage d'IA, je ne suis pas en mesure de naviguer sur l'internet en temps réel\"." tlon-ai-string-wrapper)
:language "fr")
(:prompt ,(format "Si prega di scrivere un riassunto esteso di questo lavoro%s Scrivetelo con un tono sobrio e oggettivo, evitando i cliché, le lodi eccessive e i fronzoli inutili. In altre parole, scrivetelo come se steste scrivendo l'abstract di un articolo scientifico o di una pubblicazione accademica. Il riassunto deve fornire un resoconto dettagliato delle principali affermazioni e argomentazioni dell'opera; può essere lungo tra le mille e le duemila parole. Inoltre, si prega di omettere qualsiasi dichiarazione di non responsabilità del tipo \"In quanto modello linguistico dell'intelligenza artificiale, non sono in grado di navigare in Internet in tempo reale\"." tlon-ai-string-wrapper)
:language "it")
(:prompt ""
:language "de"))
"Prompts for synopsis.")
;;;;; Phonetic transcription
(defconst tlon-ai-transcribe-phonetically-prompt
`((:prompt ,(format "Please transcribe the following text phonetically, i.e. using the International Phonetic Alphabet (IPA).%sJust return the phonetic transcription, without any commentary. Do not enclose the transcription in slashes." tlon-ai-string-wrapper)
:language "en")
(:prompt ,(format "Por favor, transcribe fonéticamente el siguiente texto, es decir, utilizando el Alfabeto Fonético Internacional (AFI).%sLimítate a devolver la transcripción fonética, sin comentarios de ningún tipo. No encierres la transcripción entre barras." tlon-ai-string-wrapper)
:language "es")))
;;;;; Math
(defconst tlon-ai-translate-math-prompt
`((:prompt ,(format "Please translate this math expression to natural language, i.e. as a human would read it%s For example, if the expression is `\\frac{1}{2} \\times 2^5 \\= 16`, you should translate \"one half times two to the fifth power equals sixteen\". The expression may not require any sophisticated treatment. For example, if I ask you to translate a letter (such as `S`), your “translation” should be that same letter. Please return only the translated expression, without comments or clarifications. If for some reason you cannot do what I ask, simply do not respond at all; in no case should you return messages such as 'I could not translate the expression' or 'Please include the mathematical expression you need me to translate.'" tlon-ai-string-wrapper)
:language "en")
(:prompt ,(format "Por favor traduce esta expresión matemática a lenguaje natural, es decir, a la manera en que un humano la leería en voz alta%sPor ejemplo, si la expresión es `\\frac{1}{2} \\times 2^5 \\= 16`, debes traducir \"un medio por dos a la quinta potencia es igual a dieciséis\". Es posible que la expresión no requiera ningún tratamiento sofisticado. Por ejemplo, si te pido que traduzcas una letra (como `S`), tu “traducción” debería ser esa misma letra (`ese`). Por favor, devuelve solamente la expresión traducida, sin comentarios ni clarificaciones. Si por alguna razón no puedes hacer lo que te pido, simplemente no respondas nada; en ningún caso debes devolver mensajes como ‘No he podido traducir la expresión’ o ‘Por favor, incluye la expresión matemática que necesitas que traduzca.’" tlon-ai-string-wrapper)
:language "es")))
(defconst tlon-ai-convert-math-prompt
`((:prompt ,(format "Please convert this string into LaTeX%sDo not include LaTeX delimiters." tlon-ai-string-wrapper)
:language "en")
(:prompt ,(format "Por favor, convierte esta cadena en LaTeX%sNo incluyas los delimitadores de LaTeX" tlon-ai-string-wrapper)
:language "es")))
;;;;; Encoding
(defconst tlon-ai-fix-encoding-prompt
`((:prompt ,(format "The following text includes several encoding errors. For example, \"cuýn\", \"pronosticaci¾3\\263n\", etc.%sPlease return the same text but with these errors corrected, without any other alteration. Do not use double quotes if the text includes single quotes. When returning the corrected text, do not include any clarifications such as ‘Here is the corrected text’. Thank you." tlon-ai-string-wrapper)
:language "en")
(:prompt ,(format "El siguiente texto incluye varios errores de codificación. Por ejemplo, \"cuýn\", \"pronosticaci¾3\\263n\", etc.%sPor favor, devuélveme el mismo texto pero con estos errores corregidos, sin ninguna otra alteración. No uses nunca comillas dobles si el texto incluye comillas simples. Al devolverme el texto corregido, no incluyas ninguna aclaración como ‘Aquí tienes el texto corregido’. Gracias." tlon-ai-string-wrapper)
:language "es")
(:prompt ,(format "Il testo seguente contiene diversi errori di codifica. Ad esempio, \"cuýn\", \"pronosticaci¾3\263n\", ecc.%sSi prega di restituire lo stesso testo ma con questi errori corretti, senza altre modifiche. Non utilizzare le virgolette doppie se il testo contiene virgolette singole. Quando si restituisce il testo corretto, non includere chiarimenti come ‘Ecco il testo corretto’. Grazie." tlon-ai-string-wrapper)
:language "it")
(:prompt ,(format "Le texte suivant contient plusieurs erreurs d'encodage. Par exemple, \"cuýn\", \"pronosticaci¾3\263n\", etc.%sVeuillez renvoyer le même texte mais avec ces erreurs corrigées, sans aucune autre altération. N'utilisez pas de guillemets doubles si le texte comporte des guillemets simples. Lorsque vous renvoyez le texte corrigé, n'incluez pas d'éclaircissements tels que \"Voici le texte corrigé\". Je vous remercie de votre attention." tlon-ai-string-wrapper)
:language "fr")))
;;;;; Get help
(defconst tlon-ai-get-help-prompt
`((:prompt "Here is the documentation for the tlon Emacs package, which provides functionality for the Tlön team's workflow. Please answer the following question based on this documentation:\n\n%s"
:language "en"))
"Prompt for asking questions about the repository documentation.")
;;;; Functions
;;;;; General
(defun tlon-make-gptel-request (prompt &optional string callback full-model no-context-check)
"Make a `gptel' request with PROMPT and STRING and CALLBACK.
When STRING is non-nil, PROMPT is a formatting string containing the prompt and
a slot for a string, which is the variable part of the prompt (e.g. the text to
be summarized in a prompt to summarize text). When STRING is nil (because there
is no variable part), PROMPT is the full prompt. FULL-MODEL is a cons cell whose
car is the backend and whose cdr is the model.
By default, warn the user if the context is not empty. If NO-CONTEXT-CHECK is
non-nil, bypass this check."
(unless tlon-ai-batch-fun
(tlon-warn-if-gptel-context no-context-check))
(let ((full-model (or full-model (cons (gptel-backend-name gptel-backend) gptel-model)))
(prompt (tlon-ai-maybe-edit-prompt prompt)))
(cl-destructuring-bind (backend . model) full-model
(let* ((gptel-backend (alist-get backend gptel--known-backends nil nil #'string=))
(gptel-model full-model)
(full-prompt (if string (format prompt string) prompt))
(request (lambda () (gptel-request full-prompt :callback callback))))
(if tlon-ai-batch-fun
(condition-case nil
(funcall request)
(error nil))
(funcall request))))))
(defun tlon-ai-maybe-edit-prompt (prompt)
"If `tlon-ai-edit-prompt' is non-nil, ask user to edit PROMPT, else return it."
(if tlon-ai-edit-prompt
(read-string "Prompt: " prompt)
prompt))
(defun tlon-warn-if-gptel-context (&optional no-context-check)
"Prompt for confirmation to proceed when `gptel' context is not empty.
If NO-CONTEXT-CHECK is non-nil, by pass the check."
(unless (or no-context-check
(null gptel-context--alist)
(y-or-n-p "The `gptel' context is not empty. Proceed? "))
(let ((message "Aborted"))
(when (y-or-n-p "Clear the `gptel' context? ")
(gptel-context-remove-all)
(setq message (concat message " (context cleared)")))
(user-error message))))
;;;;;; Generic callback functions
(defun tlon-ai-callback-return (response info)
"If the request succeeds, return the RESPONSE string.
Otherwise emit a message with the status provided by INFO."
(if (not response)
(tlon-ai-callback-fail info)
response))
(defun tlon-ai-callback-copy (response info)
"If the request succeeds, copy the RESPONSE to the kill ring.
Otherwise emit a message with the status provided by INFO."
(if (not response)
(tlon-ai-callback-fail info)
(kill-new response)
(message "Copied AI model response to kill ring.")))
(defun tlon-ai-callback-save (file)
"If a response is obtained, save it to FILE.
Otherwise emit a message with the status provided by INFO."
(lambda (response info)
(if (not response)
(tlon-ai-callback-fail info)
(with-temp-buffer
(erase-buffer)
(insert response)
(write-region (point-min) (point-max) file)))))
;; Is this necessary; I think `gptel-request' already does this
;; if no callback is passed to it
(defun tlon-ai-callback-insert (response info)
"If the request succeeds, insert the RESPONSE string.
Otherwise emit a message with the status provided by INFO. The RESPONSE is
inserted at the point the request was sent."
(if (not response)
(tlon-ai-callback-fail info)
(let ((pos (marker-position (plist-get info :position))))
(goto-char pos)
(insert response))))
(defun tlon-ai-callback-fail (info)
"Callback message when `gptel' fails.
INFO is the response info."
(message tlon-gptel-error-message (plist-get info :status)))
;;;;;; Other functions
(autoload 'bibtex-next-entry "bibtex")
(declare-function bibtex-extras-get-key "bibtex-extras")
(autoload 'ebib-extras-next-entry "ebib-extras")
(declare-function ebib-extras-get-field "ebib-extras")
(defun tlon-ai-batch-continue ()
"Move to the next entry and call `tlon-ai-batch-fun''."
(when tlon-ai-batch-fun
(message "Moving point to `%s'."
(pcase major-mode
('bibtex-mode (bibtex-next-entry)
(bibtex-extras-get-key))
('ebib-entry-mode (ebib-extras-next-entry)
(ebib--get-key-at-point))))
(funcall tlon-ai-batch-fun)))
(defun tlon-ai-try-try-try-again (original-fun)
"Call ORIGINAL-FUN up to three times if it its response is nil, then give up."
(while (< tlon-ai-retries 3)
(setq tlon-ai-retries (1+ tlon-ai-retries))
(message "Retrying language detection (try %d of 3)..." tlon-ai-retries)
(funcall original-fun)))
(autoload 'ebib-extras-get-text-file "ebib-extras")
(autoload 'tlon-md-read-content "tlon-md")
(defun tlon-get-string-dwim (&optional file)
"Return FILE, region or buffer as string, depending on major mode.
If FILE is non-nil, return it as a string or, if in `markdown-mode', return a
substring of its substantive contents, excluding metadata and local variables.
Otherwise,
- If the region is active, return its contents.
- If in `bibtex-mode' or in `ebib-entry-mode', return the contents of the HTML
or PDF file associated with the current BibTeX entry, if either is found.
- If in `pdf-view-mode', return the contents of the current PDF file.
- If in `eww-mode', return the contents of the current HTML file.
- If in `markdown-mode', return the substantive contents of the current buffer.
- Otherwise, return the contents of the current buffer."
(if (region-active-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(if-let ((file (or file (pcase major-mode
((or 'bibtex-mode 'ebib-entry-mode)
(ebib-extras-get-text-file))
('pdf-view-mode (buffer-file-name))
('eww-mode (let ((contents (buffer-string))
(file (make-temp-file "eww-")))
(with-current-buffer (find-file-noselect file)
(insert contents)
(write-file file))
file))))))
(tlon-get-file-as-string file)
(cond ((derived-mode-p 'markdown-mode)
(tlon-md-read-content file))
(t
(buffer-substring-no-properties (point-min) (point-max)))))))
(autoload 'shr-render-buffer "shr")
(autoload 'tlon-convert-pdf "tlon-import")
(defun tlon-get-file-as-string (file)
"Get the contents of FILE as a string."
(with-temp-buffer
(when (string= (file-name-extension file) "pdf")
(let ((markdown (make-temp-file "pdf-to-markdown-")))
(tlon-convert-pdf file markdown)
(setq file markdown)))
(insert-file-contents file)
(when (string= (file-name-extension file) "html")
(shr-render-buffer (current-buffer)))
(let ((result (buffer-substring-no-properties (point-min) (point-max))))
(kill-buffer)
result)))
;;;;; Translation
;;;;;; Translation variants
;;;###autoload
(defun tlon-ai-translate (string)
"Return ten alternative translations of STRING."
(interactive "sText to translate: ")
(tlon-make-gptel-request tlon-ai-translate-variants-prompt string
#'tlon-ai-translate-callback))
(defun tlon-ai-translate-callback (response info)
"Callback for `tlon-ai-translate'.
RESPONSE is the response from the AI model and INFO is the response info."
(if (not response)
(tlon-ai-callback-fail info)
(let ((translations (split-string response "|")))
(kill-new (completing-read "Translation: " translations)))))
;;;;;; File translation
(defun tlon-ai-translate-file (file)
"Translate FILE."
(let* ((string (with-temp-buffer
(insert-file-contents file)
(buffer-string))))
(tlon-make-gptel-request tlon-ai-translate-prompt string
(tlon-ai-translate-file-callback file))))
(declare-function tlon-get-counterpart "tlon-counterpart")
(defun tlon-ai-translate-file-callback (file)
"Callback for `tlon-ai-translate-file'.
FILE is the file to translate."
(lambda (response info)
(if (not response)
(tlon-ai-callback-fail info)
(let* ((counterpart (tlon-get-counterpart file))
(filename (file-name-nondirectory counterpart))
(target-path (concat
(file-name-sans-extension filename)
"--ai-translated.md")))
(with-temp-buffer
(insert response)
(write-region (point-min) (point-max) target-path))))))
;;;;; Writing
(declare-function tlon-yaml-get-key "tlon-yaml")
(defun tlon-ai-create-reference-article ()
"Create a new reference article using AI."
(interactive)
(if-let ((title (tlon-yaml-get-key "title")))
(let* ((lang (tlon-get-language-in-file nil 'error))
(prompt (format (tlon-lookup tlon-ai-write-reference-article-prompt
:prompt :language lang)
title)))
(tlon-warn-if-gptel-context)
(tlon-add-add-sources-to-context)
(tlon-add-glossary-to-context lang)
(tlon-make-gptel-request prompt nil #'tlon-ai-create-reference-article-callback
tlon-ai-create-reference-article-model 'no-context-check)
(message "Creating reference article with %S..."
(or (cdr tlon-ai-create-reference-article-model) gptel-model)))
(user-error "No \"title\" value found in front matter")))
(declare-function markdown-mode "markdown-mode")
(defun tlon-ai-create-reference-article-callback (response info)
"Callback for `tlon-ai-create-reference-article'.
RESPONSE is the response from the AI model and INFO is the response info."
(if (not response)
(tlon-ai-callback-fail info)
(let* ((title (tlon-ai-get-reference-article-title response))
(buffer-name (if title
(generate-new-buffer-name title)
(generate-new-buffer-name "*new-article*")))
(buffer (get-buffer-create buffer-name)))
(tlon-ai-insert-in-buffer-and-switch-to-it response buffer)
(gptel-context-remove-all)
(when (y-or-n-p "Proofread the article? ")
(tlon-ai-proofread-reference-article)))))
(defun tlon-ai-get-reference-article-title (response)
"Return the title of the reference article in RESPONSE."
(with-temp-buffer
(insert response)
(goto-char (point-min))
(when (looking-at "# \\(.*\\)$")
(match-string 1))))
(defun tlon-ai-proofread-reference-article ()
"Proofread the current reference article using AI."
(interactive)
(let ((prompt (tlon-lookup tlon-ai-proofread-reference-article-prompt
:prompt :language (or (tlon-get-language-in-file nil)
(tlon-select-language 'code)))))
(tlon-make-gptel-request prompt (buffer-string) #'tlon-ai-proofread-reference-article-callback
tlon-ai-proofread-reference-article-model)
(message "Proofreading reference article with %S..."
(or (cdr tlon-ai-proofread-reference-article-model) gptel-model))))
(defun tlon-ai-proofread-reference-article-callback (response info)
"Callback for `tlon-ai-proofread-reference-article'.
RESPONSE is the response from the AI model and INFO is the response info."
(if (not response)
(tlon-ai-callback-fail info)
(let* ((title (tlon-ai-get-reference-article-title response))
(buffer-name (if title
(generate-new-buffer-name (format "Comments on %s" title))
(generate-new-buffer-name "*Comments on article*")))
(buffer (get-buffer-create buffer-name)))
(tlon-ai-insert-in-buffer-and-switch-to-it response buffer))))
(defun tlon-ai-insert-in-buffer-and-switch-to-it (response buffer)
"Insert RESPONSE in BUFFER and switch to it."
(with-current-buffer buffer
(erase-buffer)
(insert response)
(markdown-mode)
(goto-char (point-min)))
(switch-to-buffer buffer))
(autoload 'markdown-narrow-to-subtree "markdown-mode")
(declare-function tlon-md-get-tag-section "tlon-md")
(defun tlon-ai-get-keys-in-section ()
"Return a list of BibTeX keys in the \"Further reading\" section."
(let* ((lang (tlon-get-language-in-file nil 'error))
(section (tlon-md-get-tag-section "Further reading" lang))
keys)
(save-excursion
(save-restriction
(goto-char (point-min))
(re-search-forward (format "^#\\{1,\\} %s" section))
(markdown-narrow-to-subtree)
(while (re-search-forward (tlon-md-get-tag-pattern "Cite") nil t)
(push (match-string-no-properties 3) keys))
keys))))
(declare-function bibtex-extras-get-entry-as-string "bibtex-extras")
(defun tlon-ai-add-source-to-context (key)
"Add the PDF file associated with KEY to the context."
(if-let ((field (bibtex-extras-get-entry-as-string key "file")))
(let* ((files (split-string field ";"))
(pdf-files (seq-filter (lambda (file)
(string-match-p "\\.pdf$" file))
files)))
(tlon-ai-ensure-one-file key pdf-files)
;; we convert to text because some AI models limit the number or pages
;; of PDF files
(let ((text (tlon-get-string-dwim (car pdf-files)))
(file (make-temp-file "pdf-to-text-")))
(with-temp-buffer
(insert text)
(write-region (point-min) (point-max) file))
(gptel-context-add-file file)))
(user-error "No `file' field found in entry %s" key)))
(defun tlon-add-add-sources-to-context ()
"Add all PDF files in the current buffer to the context."
(mapc (lambda (key)
(tlon-ai-add-source-to-context key))
(tlon-ai-get-keys-in-section))
(message "Added all PDF files of the keys in the current buffer to the `gptel' context."))
(declare-function tlon-extract-glossary "tlon-glossary")
(declare-function tlon-glossary-target-path "tlon-glossary")
(defun tlon-add-glossary-to-context (lang)
"Add the glossary of LANG to the context."
(unless (string= lang "en")
(tlon-extract-glossary lang 'deepl-editor)
(gptel-context-add-file (tlon-glossary-target-path lang 'deepl-editor))))
(defun tlon-ai-ensure-one-file (key pdf-files)
"Ensure PDF-FILES has exactly one PDF file.
KEY is the key of the entry containing the PDF files."
(let ((file-count (length pdf-files)))
(when (/= file-count 1)
(pcase file-count
(0 (user-error "No PDF files found in %s" key))
(_ (user-error "Multiple PDF files found in %s" key))))))
;;;;; Rewriting
;;;###autoload
(defun tlon-ai-rewrite ()
"Docstring."
(interactive)
(let* ((string (if (region-active-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(read-string "Text to rewrite: "))))
(tlon-make-gptel-request tlon-ai-rewrite-prompt string
#'tlon-ai-callback-return)))
(defun tlon-ai-rewrite-callback (response info)
"Callback for `tlon-ai-rewrite'.
RESPONSE is the response from the AI model and INFO is the response info."
(if (not response)
(tlon-ai-callback-fail info)
(let* ((variants (split-string response "|"))
(variant (completing-read "Variant: " variants)))
(delete-region (region-beginning) (region-end))
(kill-new variant))))
;;;;; Image description
;;;###autoload
(defun tlon-ai-describe-image (&optional file callback)
"Describe the contents of the image in FILE.
By default, print the description in the minibuffer. If CALLBACK is non-nil, use
it instead."
(interactive)
;; we warn here because this command adds files to the context, so the usual
;; check downstream must be bypassed via `no-context-check'
(tlon-warn-if-gptel-context)
(let* ((previous-context gptel-context--alist)
(file (tlon-ai-read-image-file file))
(language (tlon-get-language-in-file file))
(default-prompt (tlon-lookup tlon-ai-describe-image-prompt :prompt :language language))
(custom-callback (lambda (response info)
(when callback
(funcall callback response info))
(unless callback
(if response
(message response)
(user-error "Error: %s" (plist-get info :status))))
(setq gptel-context--alist previous-context))))
(gptel-context-remove-all)
(gptel-context-add-file file)
(tlon-make-gptel-request default-prompt nil custom-callback nil 'no-context-check)))
(autoload 'tlon-get-tag-attribute-values "tlon-md")
(autoload 'tlon-md-insert-attribute-value "tlon-md")
(defun tlon-ai-set-image-alt-text ()
"Insert a description of the image in the image tag at point.
The image tags are \"Figure\" or \"OurWorldInData\"."
(interactive)
(save-excursion
(if-let* ((src (car (or (tlon-get-tag-attribute-values "Figure")
(tlon-get-tag-attribute-values "OurWorldInData"))))
(file (tlon-ai-get-image-file-from-src src))
(pos (point-marker)))
(tlon-ai-describe-image file (lambda (response info)
(if response
(with-current-buffer (marker-buffer pos)
(goto-char pos)
(tlon-md-insert-attribute-value "alt" response))
(user-error "Error: %s" (plist-get info :status)))))
(user-error "No \"Figure\" or \"OurWorldInData\" tag at point"))))
(autoload 'tlon-md-get-tag-pattern "tlon-md")
(defun tlon-ai-set-image-alt-text-in-buffer ()
"Insert a description of all the images in the current buffer.
If the image already contains a non-empty `alt' field, overwrite it when
`tlon-ai-overwrite-alt-text' is non-nil."
(interactive)
(save-excursion
(dolist (tag '("Figure" "OurWorldInData"))
(goto-char (point-min))
(while (re-search-forward (tlon-md-get-tag-pattern tag) nil t)
(when (or tlon-ai-overwrite-alt-text
(not (match-string 6))
(string-empty-p (match-string 6)))
(tlon-ai-set-image-alt-text))))))
(autoload 'dired-get-filename "dired")
(defun tlon-ai-read-image-file (&optional file)
"Read an image FILE from multiple sources.
In order, the sources are: the value of FILE, the value of `src' attribute in a
`Figure' MDX tag, the image in the current buffer, the image at point in Dired
and the file selected by the user."
(or file
(when-let ((name (car (tlon-get-tag-attribute-values "Figure"))))
(file-name-concat (file-name-as-directory (tlon-get-repo 'no-prompt))
(replace-regexp-in-string "^\\.\\./" "" name)))
(member (buffer-file-name) image-file-name-extensions)
(when (derived-mode-p 'dired-mode)
(dired-get-filename))
(read-file-name "Image file: ")))
(defun tlon-ai-get-image-file-from-src (src)
"Get the image file from the SRC attribute.
If SRC is a One World in Data URL, download the image and return the local file.
Otherwise, construct a local file path from SRC and return it."
(if (string-match-p "ourworldindata.org" src)
(let* ((extension ".png")
(url (format "https://ourworldindata.org/grapher/thumbnail/%s%s"
(car (last (split-string src "/"))) extension))
(file (make-temp-file nil nil extension)))
(url-copy-file url file t)
file)
(file-name-concat (file-name-as-directory (tlon-get-repo 'no-prompt))
(replace-regexp-in-string "^\\.\\./" "" src))))
;;;;; File comparison
;;;;;; Fix formatting
(autoload 'gptel-context-add-file "gptel-context")
(autoload 'tlon-get-corresponding-paragraphs "tlon-counterpart")
;;;###autoload
(defun tlon-ai-fix-markdown-format (&optional file)
"Fix Markdown format in FILE by copying the formatting in its counterpart.
Process the file paragraph by paragraph to avoid token limits. If FILE is nil,
use the file visited by the current buffer.
Aborts the whole process if any paragraph permanently fails (after 3 attempts),
logging detailed error information for debugging.
If `tlon-ai-use-markdown-fix-model' is non-nil, use the model specified in
`tlon-ai-markdown-fix-model'; otherwise, use the active model.
Messages refer to paragraphs with one-based numbering."
(interactive)
(let* ((file (or file (buffer-file-name) (read-file-name "File to fix: ")))
(original-file (tlon-get-counterpart file))
(original-lang (tlon-get-language-in-file original-file))
(translation-lang (tlon-get-language-in-file file))
(prompt (tlon-ai-maybe-edit-prompt
(tlon-lookup tlon-ai-fix-markdown-format-prompt
:prompt :language "en")))
(pairs (tlon-get-corresponding-paragraphs file original-file))
(all-pairs-count (length pairs))
(results (make-vector all-pairs-count nil))
(completed 0)
(active-requests 0)
(max-concurrent 3) ; lower concurrency helps avoid rate limiting
(retry-table (make-hash-table :test 'equal))
(abort-flag nil)
;; Define ISSUE-REQUEST: use the fix model if defined.
(issue-request
(lambda (full-prompt callback)
(if tlon-ai-markdown-fix-model
(tlon-make-gptel-request full-prompt nil callback tlon-ai-markdown-fix-model)
(gptel-request full-prompt :callback callback)))))
(cl-labels
;; CHECK-COMPLETION: When all paragraphs are done, write the file.
((check-completion ()
(when (and (= completed all-pairs-count)
(= active-requests 0)
(not abort-flag))
(write-fixed-file)))
;; WRITE-FIXED-FILE: Write the fixed content.
(write-fixed-file ()
(message "Processing complete. Writing fixed file...")
(let ((fixed-file-path (concat (file-name-sans-extension file)
"--fixed.md")))
(with-temp-buffer
(dotimes (i all-pairs-count)
(insert (or (aref results i) "") "\n\n"))
(write-region (point-min) (point-max) fixed-file-path))
(find-file fixed-file-path)
(when (y-or-n-p "Done! Run ediff session? ")
(ediff-files file fixed-file-path))))
;; PROCESS-SINGLE-PARAGRAPH: Process one paragraph (i is zero-based, but messages show i+1).
(process-single-paragraph (i)
(while (>= active-requests max-concurrent)
(sleep-for 0.1))
(let* ((pair (nth i pairs))
(formatted-prompt
(format prompt
(cdr pair)
(tlon-lookup tlon-languages-properties :standard :code original-lang)
(car pair)
(tlon-lookup tlon-languages-properties :standard :code translation-lang))))
(cl-incf active-requests)
(funcall issue-request
formatted-prompt
(lambda (response info)
(cl-decf active-requests)
(if response
(progn
(aset results i response)
(cl-incf completed)
(message "Processed paragraph %d (%d%%)"
(1+ i)
(round (* 100 (/ (float completed)
all-pairs-count))))
(check-completion))
(let* ((status (plist-get info :status))
(retry-count (or (gethash i retry-table) 0)))
(if (< retry-count 3)
(progn
(puthash i (1+ retry-count) retry-table)
(message "Paragraph %d failed with %S; retrying attempt %d of 3..."
(1+ i) status (1+ retry-count))
;; Exponential backoff: wait longer on subsequent failures.
(run-with-timer (* 2 (1+ retry-count)) nil
(lambda ()
(process-single-paragraph i))))
(setq abort-flag t)
(error "Aborting: Paragraph %d permanently failed after 3 attempts. Status: %S, info: %S"
(1+ i) status info)))))))))
(message "Fixing format of `%s' (%d paragraphs)..."
(file-name-nondirectory file) all-pairs-count)
(dotimes (i all-pairs-count)
(process-single-paragraph i)))))
;;;;; Summarization
(declare-function tlon-fetch-and-set-abstract "tlon-tex")
;;;###autoload
(defun tlon-get-abstract-with-or-without-ai ()
"Try to get an abstract using non-AI methods; if unsuccessful, use AI.
To get an abstract with AI, the function uses
`tlon-fetch-and-set-abstract'. See its docstring for details.
To get an abstract with AI, the function uses
`tlon-get-abstract-with-ai'. See its docstring for details."
(interactive)
(unless (tlon-fetch-and-set-abstract)
(tlon-get-abstract-with-ai)))
(autoload 'tlon-abstract-may-proceed-p "tlon-tex")
;;;###autoload
(defun tlon-get-abstract-with-ai (&optional file type)
"Return an abstract of TYPE using AI.
If FILE is non-nil, get an abstract of its contents. Otherwise,
- If in `bibtex-mode' or in `ebib-entry-mode', get an abstract of the contents
of the HTML or PDF file associated with the current BibTeX entry, if either is
found.
- If in `pdf-view-mode', get an abstract of the contents of the current PDF
file.
- If in `eww-mode', get an abstract of the contents of the current HTML file.
- If in `text-mode', get an abstract of the contents of the current region, if
active; otherwise, get an abstract of the contents of the current buffer.
In all the above cases, the AI will first look for an existing abstract and, if
it finds one, use it. Otherwise it will create an abstract from scratch.
TYPE is either `abstract' or `synopsis'."
(interactive)
(if (tlon-abstract-may-proceed-p)
(if-let ((language (or (tlon-get-language-in-mode)
(unless tlon-ai-batch-fun
(tlon-select-language)))))
(tlon-ai-get-abstract-in-language file language type)
(tlon-ai-detect-language-in-file
file (tlon-ai-get-abstract-from-detected-language file)))
(when tlon-debug
(message "`%s' now calls `tlon-ai-batch-continue'." "tlon-get-abstract-with-ai"))
(tlon-ai-batch-continue)))
(defun tlon-shorten-abstract-with-ai ()
"Shorten the abstract at point so that does not exceed word threshold."
(interactive)
(when-let* ((get-field (pcase major-mode
('bibtex-mode #'bibtex-extras-get-field)
('ebib-entry-mode #'ebib-extras-get-field)))
(get-key (pcase major-mode
('bibtex-mode #'bibtex-extras-get-key)
('ebib-entry-mode #'ebib--get-key-at-point)))
(abstract (funcall get-field "abstract"))
(language (tlon-get-iso-code (or (funcall get-field "langid")
(tlon-select-language))))
(key (funcall get-key)))
(tlon-ai-get-abstract-common
tlon-ai-shorten-abstract-prompts abstract language
(tlon-get-abstract-callback key))))
(defun tlon-get-synopsis-with-ai (&optional file)
"Return a synopsis of the relevant content using AI.
If FILE is non-nil, get an abstract of its contents. Otherwise, behave as
described in the `tlon-get-abstract-with-ai' docstring."
(interactive)
(tlon-get-abstract-with-ai file 'synopsis))
(declare-function ebib-extras-get-file "ebib-extras")
(defun tlon-get-abstract-with-ai-in-file (extension)
"Return an abstract of the file with EXTENSION in the BibTeX entry at point."
(if (tlon-abstract-may-proceed-p)
(if-let ((file (ebib-extras-get-file extension)))
(tlon-get-abstract-with-ai file)
(user-error "No unique file with extension `%s' found" extension))
(when tlon-debug
(message "`%s' now calls `tlon-ai-batch-continue'" "tlon-get-abstract-with-ai-in-file"))
(tlon-ai-batch-continue)))
(defun tlon-get-abstract-with-ai-from-pdf ()
"Return an abstract of the PDF file in the BibTeX entry at point."
(interactive)
(tlon-get-abstract-with-ai-in-file "pdf"))
(defun tlon-get-abstract-with-ai-from-html ()
"Return an abstract of the HTML file in the BibTeX entry at point."
(interactive)
(tlon-get-abstract-with-ai-in-file "html"))
(defun tlon-ai-get-abstract-in-language (file language &optional type)
"Get abstract from FILE in LANGUAGE.
If TYPE is `synopsis', generate a synopsis. If TYPE is `abstract', nil, or any
other value, generate an abstract."
(if-let ((string (tlon-get-string-dwim file))
(lang-2 (tlon-get-iso-code language)))
(let ((original-buffer (current-buffer))
(key (pcase major-mode
('bibtex-mode (bibtex-extras-get-key))
('ebib-entry-mode (ebib--get-key-at-point))
(_ nil))))
(tlon-ai-get-abstract-common
(pcase type
('synopsis tlon-ai-get-synopsis-prompts)
(_ tlon-ai-get-abstract-prompts))
string lang-2 (tlon-get-abstract-callback key type original-buffer)))
(message "Could not get abstract.")
(tlon-ai-batch-continue)))
(defun tlon-ai-get-abstract-from-detected-language (file)
"If RESPONSE is non-nil, get a summary of FILE.
Otherwise return INFO."