-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstory_generator.py
1979 lines (1709 loc) · 123 KB
/
story_generator.py
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
# -*- coding: utf-8 -*-
import os
import sys
import argparse
# import json # Not currently used
import datetime
import time
import math
import re
import logging
import tempfile
import random # for jitter in retry
from typing import List, Tuple, Optional, Dict, Any
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv() # Loads .env for API Key if not passed directly
# === Configuration and Constants (Adopted from Web Script) ===
MODELL_NAME = "microsoft/phi-4" # Or your preferred story model
API_BASE_URL = "https://api.studio.nebius.com/v1/"
# Retry Constants
DEFAULT_RETRY_DELAY_S: int = 15 # Longer delay for potentially longer tasks
MAX_RETRIES: int = 3
RETRY_BACKOFF_FACTOR: float = 1.5
# Word Count & Token Limits
MIN_STORY_WORDS: int = 500
MAX_STORY_WORDS_NO_CHAPTERS: int = 25000 # Theoretical limit for single call
DEFAULT_TARGET_WORDS: int = 5000
TOKEN_WORD_RATIO: float = 1.6 # Heuristic ratio
MAX_TOKENS_PER_CALL: int = 15000 # API Limit (Input + Output) - Check limits
MIN_CHAPTERS_LONG_STORY: int = 3
TARGET_WORDS_PER_CHAPTER_DIVISOR: int = 2500
WORD_COUNT_BUFFER_FACTOR: float = 2.8 # Buffer for LLM word count inaccuracy
# Text Cleaning Constants
MIN_WORDS_FOR_VALID_ENDING: int = 4
MIN_CHARS_FOR_RESCUE: int = 100
# --- Module-Level Constants ---
SUPPORTED_LANGUAGES: List[str] = ["Deutsch", "Englisch"]
MAX_WORDS_PER_CHAPTER: int = 3000 # Max words per chapter *generation call*
# Logging configuration (will be reconfigured in main based on args)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__) # Use standard logger
class StoryGenerator: # Renamed class
"""
Generates short stories with quality guidelines in the prompt.
Includes enhanced chapter context handling using LLM summaries (detailed & running),
outline segmentation, and explicit hierarchical instructions for coherence.
CLI Version: Includes file saving.
"""
# --- Language configuration (Adopted from Web Script) ---
LANGUAGE_CONFIG: Dict[str, Dict[str, Any]] = {
"Deutsch": {
# --- Story/Outline Prompts ---
"PROMPT_STORY_GEN": """Du bist ein talentierter und erfahrener Autor von Kurzgeschichten in **deutscher Sprache**. Deine Aufgabe ist es, eine fesselnde, gut geschriebene Geschichte basierend auf den folgenden Vorgaben zu erstellen.
Ziel-Wortanzahl: ca. {wortanzahl} Wörter.
Titel: {titel}
Prompt (Grundidee): {prompt}
Setting (Ort, Zeit, Atmosphäre): {setting}
**Qualitätsrichtlinien für das Schreiben:**
- **Stil & Fluss:** Schreibe in klarem, prägnantem und ansprechendem Deutsch. Variiere Satzlänge und -struktur, um Monotonie zu vermeiden. Sorge für einen natürlichen Lesefluss.
- **Wiederholungen VERMEIDEN:** Achte SEHR GENAU darauf, Wort- und Phrasenwiederholungen sowie ähnliche Satzmuster zu minimieren. Nutze Synonyme oder formuliere Sätze kreativ um, ohne den Sinn zu verändern.
- **Starke Sprache:** Verwende präzise Verben und Adjektive. Bevorzuge aktive Formulierungen gegenüber passiven.
- **"Show, Don't Tell":** Zeige Emotionen, Charaktereigenschaften und Atmosphäre durch Handlungen, Dialoge und sensorische Details, anstatt sie nur zu behaupten.
- **Dialog:** Schreibe glaubwürdige, lebendige Dialoge, die zur jeweiligen Figur passen und die Handlung vorantreiben oder Charaktertiefe verleihen.
- **Setting & Atmosphäre:** Beschreibe den Ort, die Zeit und die Stimmung lebendig und immersiv. Nutze Details, die die Sinne ansprechen.
- **Pacing & Spannung:** Baue die Handlung logisch auf, erzeuge Spannung und variiere das Tempo angemessen.
- **Kohärenz & Konsistenz:** Erzähle eine in sich schlüssige Geschichte mit klarem Anfang, Mittelteil und Ende. Achte auf Konsistenz bei Zeitformen, Perspektive und Charakterdetails.
- **Abgeschlossenes Ende:** Die Geschichte MUSS ein klares, befriedigendes und abgeschlossenes Ende haben. Sie darf NICHT abrupt oder mitten im Satz/Absatz enden.
- **Formatierung:** Beginne NUR mit dem Titel im Format '# {titel}'. Füge KEINEN weiteren Titel hinzu. Strukturiere den Text sinnvoll mit Absätzen.
{zusatz_anweisungen}""",
"PROMPT_OUTLINE_GEN": """Du bist ein erfahrener Autor und Story-Entwickler. Erstelle eine detaillierte Plot-Outline für eine Geschichte mit {kapitel_anzahl} Kapiteln basierend auf:
Titel: {titel}
Prompt: {prompt}
Setting: {setting}
Gesamtwortzahl: ca. {wortanzahl} Wörter ({sprache})
Erstelle eine Outline mit **klar voneinander abgegrenzten Abschnitten für jedes Kapitel**:
1. Hauptcharaktere: Details (Name, Alter, Aussehen, Persönlichkeit, Motivation).
2. Wichtige Nebencharaktere: Namen, Beziehung.
3. Haupthandlung: Detaillierte Zusammenfassung des Verlaufs über alle Kapitel hinweg.
4. Weltregeln (falls relevant).
5. Kapitelstruktur ({kapitel_anzahl} Kapitel): **Für jedes Kapitel (beginne jeden Abschnitt deutlich mit z.B. 'Kapitel X:' oder '## Kapitel X')**: Aussagekräftiger Titel, Zeit/Ort, Anwesende Charaktere, Wichtigste Handlungspunkte/Szenen (5-7, spezifisch für dieses Kapitel), Charakterentwicklung/Wendungen in diesem Kapitel, Cliffhanger (optional).
**Wichtig:** Die Outline soll eine Grundlage für eine **qualitativ hochwertige Geschichte** bilden. Achte auf Konsistenz, Kausalität, logischen Aufbau, Charakterentwicklung und Potenzial für spannendes Erzählen gemäß hoher Schreibstandards (abwechslungsreiche Sprache, Vermeidung von Wiederholungen im Konzept etc.). Die Kapitelabschnitte müssen klar erkennbar sein.
{zusatz_anweisungen}""",
# --- **FINAL REVISED** Chapter Prompt with Hierarchy ---
"PROMPT_CHAPTER_GEN": """Du bist ein talentierter und erfahrener Autor von Kurzgeschichten in **deutscher Sprache**. Deine Aufgabe ist es, **Kapitel {kapitel_nummer} von {kapitel_anzahl}** einer Geschichte herausragend zu schreiben, indem du die bereitgestellten Kontexte **präzise und hierarchisch** nutzt.
**Kontext und Anweisungen für Kapitel {kapitel_nummer}:**
1. **WAS SOLL PASSIEREN? (Primärer Fokus!)** - Halte dich **strikt** an die Handlungspunkte für **dieses spezifische Kapitel**, wie sie in folgendem Auszug aus der Plot-Outline beschrieben sind:
--- START OUTLINE-AUSZUG KAPITEL {kapitel_nummer} ---
{plot_outline_segment}
--- ENDE OUTLINE-AUSZUG ---
Setze **nur** diese Punkte um. Weiche nicht ab, füge keine Elemente aus anderen Kapiteln hinzu.
2. **WAS IST BEREITS PASSIERT? (Globale Konsistenz & Anti-Looping!)** - Beachte die folgende Zusammenfassung der **gesamten bisherigen Handlung**. Stelle sicher, dass deine Erzählung dazu konsistent ist. **Wiederhole KEINE bereits abgeschlossenen Haupthandlungsstränge oder aufgelösten Kernkonflikte**, die in dieser Zusammenfassung erwähnt werden! Achte besonders auf den Status wichtiger Plotpunkte.
--- START LAUFENDE GESAMTZUSAMMENFASSUNG ---
{running_plot_summary}
--- ENDE LAUFENDE GESAMTZUSAMMENFASSUNG ---
3. **WIE ENDETE DAS LETZTE KAPITEL? (Direkter Anschluss!)** - Beginne **nahtlos** im Anschluss an das Ende des vorherigen Kapitels ({prev_kapitel_nummer}), wie im folgenden Detailkontext (Zusammenfassung + Raw End) beschrieben. Formuliere den Übergang neu und kopiere keine Sätze.
--- START KONTEXT KAPITEL {prev_kapitel_nummer} ---
{zusammenfassung_vorher}
--- ENDE KONTEXT KAPITEL {prev_kapitel_nummer} ---
**Weitere Qualitätsrichtlinien:**
- Umfang: ca. {kapitel_wortanzahl} Wörter (mind. {min_kapitel_worte}, max. {max_kapitel_worte}).
- Sprache & Stil: Klar, prägnant, ansprechend. **Starke Variation** in Satzlänge, Struktur und Wortwahl. Exzellenter Lesefluss.
- **Wiederholungen STRENG VERMEIDEN:** Vermeide nicht nur Phrasen, sondern auch **ähnliche Satzmuster, Beschreibungen und Handlungsmuster**. {zusatz_anweisungen}
- Starke Sprache, "Show, Don't Tell", glaubwürdige Dialoge, Atmosphäre, Pacing (gemäß Outline-Segment).
- Formatierung: Beginne mit '## Kapitel {kapitel_nummer}: [Passender Titel]' (AUSSER Kapitel 1: Beginne mit '# {titel}\\n\\n## Kapitel 1: [Kapiteltitel]'). Kein Titel am Ende.
- Abschluss: Vollständiger, sinnvoller Satz/Absatz.
**Schreibe jetzt Kapitel {kapitel_nummer} gemäß ALLEN Anweisungen:**
""",
# --- **FINAL REVISED** Epilogue Prompt ---
"PROMPT_EPILOG_GEN": """Du bist ein talentierter Autor. Schreibe einen **hochwertigen, gut geschriebenen** und befriedigenden Abschluss (Epilog) für eine Geschichte.
**Kontext:**
- **Titel:** {titel}
- **Plot-Outline (Gesamtübersicht):**
{plot_outline}
- **Laufende Zusammenfassung der GESAMTEN Handlung bis zum Ende des letzten Kapitels:**
{running_plot_summary}
- **Detaillierter Kontext & Raw End vom LETZTEN Kapitel ({kapitel_anzahl}):**
{zusammenfassung_vorher}
- **Originaltext-Fragment vom Ende des letzten Kapitels:**
...{letztes_kapitel_ende}
**Deine Aufgabe für den Epilog:**
1. **Auflösung:** Löse die wichtigsten offenen Handlungsstränge und Charakterbögen elegant auf, basierend auf der **Outline** und der **laufenden Zusammenfassung**.
2. **Anknüpfung:** Knüpfe stilistisch und thematisch an den **detaillierten Kontext** des letzten Kapitels an.
3. **Charakterende:** Führe die emotionale Reise der Hauptcharaktere zu einem sinnvollen Ende.
4. **Themen:** Greife die Hauptthemen der Geschichte nochmals auf.
5. **Qualität:** Schreibe stilistisch passend (abwechslungsreich, keine Wiederholungen). {zusatz_anweisungen}
6. **Umfang:** ca. 500-1000 Wörter.
7. **Formatierung:** Beginne mit '## Epilog'. Kein Titel am Ende.
8. **Ende:** Sorge für ein klares, befriedigendes Ende.
**Schreibe jetzt den Epilog:**
""",
# --- **FINAL REVISED** Detailed Summary Prompt ---
"PROMPT_SUMMARY_GEN": """Du bist ein Assistent, der darauf spezialisiert ist, Kapitel einer Geschichte prägnant zusammenzufassen, um die Kontinuität für den Autor (eine KI) zu gewährleisten.
**Aufgabe:** Lies den folgenden Kapiteltext sorgfältig durch und erstelle eine kurze, prägnante Zusammenfassung (ca. 150-250 Wörter) in **deutscher Sprache**.
**Fokus der Zusammenfassung:**
- Was sind die **wichtigsten Ereignisse**, die in diesem Kapitel stattgefunden haben?
- Welche **signifikanten Entwicklungen** gab es bei den Hauptcharakteren (Entscheidungen, Erkenntnisse, emotionale Zustände)?
- Wie ist der **genaue Zustand am Ende des Kapitels**? (Wo befinden sich die Charaktere? Was ist die unmittelbare Situation? Welche Spannung oder offene Frage besteht?)
- **WICHTIG:** Was ist der **aktuelle Status wichtiger offener Fragen oder Kernkonflikte** (z.B. Wurde ein entscheidendes Ritual durchgeführt? Wurde ein Hauptgeheimnis gelüftet? Wurde ein Hauptziel erreicht?), basierend auf den Ereignissen *dieses* Kapitels? Benenne den Status klar (z.B. "Ritual abgeschlossen", "Geheimnis X ungelöst", "Ziel Y näher gekommen").
- Vermeide Nebensächlichkeiten und konzentriere dich auf Informationen, die für das Verständnis des nächsten Kapitels **unbedingt notwendig** sind.
**Kapiteltext zum Zusammenfassen:**
--- START TEXT ---
{kapitel_text}
--- END TEXT ---
**Deine Zusammenfassung (nur der zusammenfassende Text):**
""",
# --- **FINAL REVISED** Running Summary Update Prompt ---
"PROMPT_RUNNING_SUMMARY_UPDATE": """Du bist ein Redakteur, der eine KI beim Schreiben einer Geschichte unterstützt. Deine Aufgabe ist es, eine **laufende Zusammenfassung der gesamten bisherigen Handlung** zu pflegen und zu aktualisieren.
**Bisherige Handlungszusammenfassung:**
{bisherige_zusammenfassung}
**Text des NEUEN Kapitels ({kapitel_nummer}), das gerade hinzugefügt wurde:**
--- START NEUES KAPITEL ---
{neues_kapitel_text}
--- END NEUES KAPITEL ---
**Deine Aufgabe:**
Aktualisiere die "Bisherige Handlungszusammenfassung" prägnant, indem du die **wichtigsten neuen Ereignisse, Charakterentwicklungen und Plotpunkte** aus dem NEUEN Kapitel hinzufügst. Behalte den roten Faden bei und **stelle klar den Fortschritt oder Abschluss wichtiger Handlungsbögen dar**. Die neue Zusammenfassung sollte die gesamte Geschichte bis einschließlich des neuen Kapitels abdecken und den **aktuellen Status der Hauptkonflikte/Ziele** widerspiegeln. Halte sie so kurz wie möglich, aber stelle sicher, dass alle *wesentlichen* Plotpunkte enthalten sind.
**Aktualisierte Gesamtzusammenfassung (nur der Text der neuen Zusammenfassung):**
""",
# --- Other texts (ensure all needed keys are present) ---
"USER_PROMPT_STORY": "Bitte schreibe eine hochwertige Kurzgeschichte basierend auf Titel '{titel}', Prompt und Setting. Ca. {wortanzahl} Wörter.",
"USER_PROMPT_OUTLINE": "Bitte erstelle eine detaillierte Plot-Outline für eine {kapitel_anzahl}-teilige, qualitativ hochwertige Geschichte mit Titel '{titel}'.",
"USER_PROMPT_CHAPTER": "Bitte schreibe Kapitel {kapitel_nummer} der Geschichte '{titel}' herausragend und gemäß allen Qualitätsrichtlinien, insbesondere unter Beachtung der hierarchischen Kontext-Anweisungen.",
"USER_PROMPT_EPILOG": "Bitte schreibe einen hochwertigen Epilog für die Geschichte '{titel}' unter Berücksichtigung des Kontextes des letzten Kapitels und der Gesamthandlung.",
"USER_PROMPT_SUMMARY": "Bitte fasse das vorherige Kapitel gemäß den Anweisungen zusammen, um die Kontinuität zu sichern.",
"USER_PROMPT_RUNNING_SUMMARY": "Bitte aktualisiere die laufende Zusammenfassung der Geschichte mit den Ereignissen aus dem neuesten Kapitel.",
"DEFAULT_CHAPTER_TITLE": "Kapitel {kapitel_nummer}", # Base name, formatted later
"FIRST_CHAPTER_TITLE": "Kapitel {kapitel_nummer}", # Base name, formatted to 1 later
"EPILOG_TITLE": "Epilog", # Base name
"ERROR_CHAPTER_TITLE": "Kapitel {kapitel_nummer}: [Fehler bei der Generierung]", # Internal use
"ERROR_CHAPTER_CONTENT": "Es ist ein Fehler bei der Generierung dieses Kapitels aufgetreten.", # Internal use
"RESCUED_CHAPTER_NOTICE": "[Das Kapitel wurde aufgrund technischer Probleme gekürzt oder konnte nicht vollständig wiederhergestellt werden. Die Geschichte wird im nächsten Kapitel fortgesetzt.]",
"RESCUED_EPILOG_NOTICE": "[Die Geschichte konnte nicht vollständig abgeschlossen werden. Der Epilog fehlt oder ist unvollständig.]",
"SUMMARY_FIRST_CHAPTER": "Dies ist das erste Kapitel. Es gibt keinen vorherigen Kontext.", # More explicit
"WARN_LOW_WORDCOUNT": "Warnung: Ziel-Wortanzahl {wortanzahl} sehr niedrig. Setze auf Minimum {min_val}.",
"WARN_HIGH_WORDCOUNT": "Warnung: Ziel-Wortanzahl {wortanzahl} zu hoch ohne Kapitel-Modus. Setze auf Maximum {max_val}.",
"INFO_CHAPTER_MODE_ACTIVATED": "Aktiviere Kapitel-Modus für Geschichte mit ca. {wortanzahl} Wörtern.",
"INFO_GENERATING_STORY": "Generiere Geschichte '{titel}' mit ca. {wortanzahl} Wörtern in {sprache} (Qualitätsfokus)...",
"INFO_SENDING_API_REQUEST": "Sende Anfrage an API (Model: {model}, Max Tokens: {max_tokens}, Temp: {temp:.2f})...",
"INFO_WAITING_API_RESPONSE": "Warte auf API-Antwort...",
"INFO_GENERATION_COMPLETE": "Generierung abgeschlossen nach {dauer:.1f} Sekunden.",
"INFO_SAVED_TEMP_FILE": "Zwischendaten in temporäre Datei gespeichert: {dateiname}", # Debugging
"WARN_CANNOT_WRITE_TEMP": "Warnung: Konnte temporäre Datei nicht schreiben: {error}", # Debugging
"WARN_CANNOT_DELETE_TEMP": "Warnung: Konnte temporäre Datei nicht löschen: {error}", # Debugging
"INFO_REMOVED_INCOMPLETE_SENTENCE": "Unvollständiger Satz am Ende entfernt.",
"INFO_CORRECTING_ENDING": "Textende wird korrigiert...",
"INFO_REMOVED_INCOMPLETE_DIALOG": "Unvollständigen Dialog am Ende entfernt.",
"INFO_ADDED_MISSING_QUOTE": "Fehlendes Anführungszeichen ergänzt.",
"INFO_REMOVED_INCOMPLETE_PARAGRAPH": "Unvollständigen letzten Absatz entfernt.",
"INFO_GENERATING_CHAPTERS": "Generiere Geschichte in {kapitel_anzahl} Kapiteln (Qualitätsfokus)...",
"INFO_GENERATING_OUTLINE": "Generiere Plot-Outline...",
"INFO_OUTLINE_CREATED": "Plot-Outline erstellt.",
"INFO_SAVED_OUTLINE_TEMP": "Plot-Outline temporär gespeichert: {dateiname}", # Debugging Temp Save
"INFO_SAVED_OUTLINE_FINAL": "Plot-Outline gespeichert als: {dateiname}", # Final Save
"WARN_CANNOT_SAVE_OUTLINE_TEMP": "Warnung: Konnte temporäre Plot-Outline nicht speichern: {error}", # Debugging Temp Save
"ERROR_SAVING_OUTLINE_FINAL": "Fehler beim Speichern der Plot-Outline: {error}. Generierung wird fortgesetzt.", # Final Save
"INFO_GENERATING_CHAPTER_NUM": "Generiere Kapitel {kapitel_nummer} von {kapitel_anzahl}...",
"INFO_CHAPTER_COMPLETE": "Kapitel {kapitel_nummer} abgeschlossen ({dauer:.1f}s).",
"ERROR_API_REQUEST_CHAPTER": "API-Fehler bei Kapitel {kapitel_nummer}: {error}",
"INFO_RESCUED_PARTIAL_CONTENT": "Teilweise generierter Inhalt ({chars} Zeichen) gerettet.",
"ERROR_READING_TEMP_FILE": "Fehler beim Lesen der temporären Datei: {error}", # Debugging
"INFO_LAST_CHAPTER_INCOMPLETE": "Letztes Kapitel wirkt unvollständig oder wurde gerettet. Generiere Epilog...",
"INFO_GENERATING_EPILOG": "Generiere Epilog...",
"INFO_EPILOG_GENERATED": "Epilog generiert ({dauer:.1f}s).",
"ERROR_GENERATING_EPILOG": "Fehler bei Generierung des Epilogs: {error}",
"INFO_FINAL_WORD_COUNT": "Finale Geschichte mit {wortanzahl} Wörtern generiert.",
"INFO_SAVED_TEXT_FILE": "Geschichte als Textdatei gespeichert: {dateiname}", # Used by CLI save_as_text_file
"ERROR_API_OVERLOAD": "API überlastet/Timeout. Warte {delay:.1f}s, Versuch {retries}/{max_retries}...",
"ERROR_API_CALL_FAILED": "Fehler während API-Aufruf: {error}",
"ERROR_ALL_RETRIES_FAILED": "Alle API-Wiederholungen fehlgeschlagen.",
"ERROR_UNSUPPORTED_LANGUAGE": "Sprache '{sprache}' nicht unterstützt. Unterstützt: {supported}",
"ERROR_MISSING_API_KEY": "API-Schlüssel erforderlich. NEBIUS_API_KEY setzen oder übergeben.",
"ERROR_GENERATION_FAILED": "Generierung der Geschichte fehlgeschlagen.",
"COMMON_NOUN_PREFIXES": ["Der", "Die", "Das", "Ein", "Eine"], # Needed for _extract_char_names (still used internally by summary heuristic)
"ACTION_VERBS": ["ging", "kam", "sprach", "sah", "fand", "entdeckte", "öffnete", "schloss", "rannte", "floh", "kämpfte", "starb", "tötete", "küsste", "sagte", "antwortete", "erwiderte", "blickte", "dachte"], # Needed for _extract_important_sentences (still used internally by summary heuristic)
"EMOTIONAL_WORDS": ["angst", "furcht", "freude", "glück", "trauer", "wut", "zorn", "liebe", "hass", "entsetzen", "überraschung", "schock", "verzweiflung", "erleichterung"], # Needed for _extract_important_sentences (still used internally by summary heuristic)
"CONJUNCTIONS_AT_END": ['und', 'aber', 'oder', 'denn', 'weil', 'dass', 'ob'],
# --- Context Markers (Adopted from Web Script) ---
"SUMMARY_LLM_PREFIX": "**Kontext - Zusammenfassung des vorherigen Kapitels (KI-generiert):**",
"SUMMARY_RAW_END_MARKER_START": "\n\n**--- EXAKTES ENDE DES VORHERIGEN KAPITELS (KONTEXT) ---**",
"SUMMARY_RAW_END_MARKER_END": "**--- ENDE KONTEXT ---**",
"SUMMARY_FALLBACK": "[Kontext des vorherigen Kapitels konnte nicht automatisch generiert werden. Bitte die Handlung aus dem vorherigen Kapitel beachten.]",
"INFO_GENERATING_SUMMARY": "Generiere LLM-Zusammenfassung + End-Extrakt für Kapitel {kapitel_nummer}...",
"INFO_SUMMARY_GENERATED": "LLM-Zusammenfassung + End-Extrakt für Kapitel {kapitel_nummer} erstellt.",
"ERROR_GENERATING_SUMMARY": "Fehler bei Generierung der LLM-Zusammenfassung für Kapitel {kapitel_nummer}: {error}",
# --- Running Summary (Adopted from Web Script) ---
"RUNNING_SUMMARY_PLACEHOLDER": "**Laufende Zusammenfassung der gesamten bisherigen Handlung:**\n",
"RUNNING_SUMMARY_INITIAL": "Die Geschichte beginnt.", # Initial text for empty summary
"INFO_UPDATING_RUNNING_SUMMARY": "Aktualisiere laufende Handlungszusammenfassung nach Kapitel {kapitel_nummer}...",
"INFO_RUNNING_SUMMARY_UPDATED": "Laufende Handlungszusammenfassung aktualisiert.",
"ERROR_UPDATING_RUNNING_SUMMARY": "Fehler beim Aktualisieren der laufenden Zusammenfassung nach Kapitel {kapitel_nummer}: {error}",
"ERROR_GENERATING_OUTLINE_FALLBACK": "Generierung der Plot-Outline fehlgeschlagen", # Fallback text for outline
},
"Englisch": {
# --- Story/Outline Prompts ---
"PROMPT_STORY_GEN": """You are a talented and experienced author of short stories in **English**. Your task is to create a compelling, well-written story based on the following specifications.
Target word count: approx. {wortanzahl} words.
Title: {titel}
Prompt (Basic Idea): {prompt}
Setting (Location, Time, Atmosphere): {setting}
**Quality Guidelines for Writing:**
- **Style & Flow:** Write in clear, concise, and engaging English. Vary sentence length and structure to avoid monotony. Ensure a natural reading flow.
- **AVOID Repetition:** Pay VERY CLOSE attention to minimizing word and phrase repetitions as well as similar sentence patterns. Use synonyms or creatively rephrase sentences without altering the meaning.
- **Strong Language:** Use precise verbs and evocative adjectives. Prefer active voice over passive.
- **"Show, Don't Tell":** Demonstrate emotions, character traits, and atmosphere through actions, dialogue, and sensory details, rather than just stating them.
- **Dialogue:** Write believable, vivid dialogue that fits the character and either advances the plot or reveals personality.
- **Setting & Atmosphere:** Describe the location, time, and mood vividly and immersively. Use details that appeal to the senses.
- **Pacing & Suspense:** Structure the plot logically, build suspense, and vary the pace appropriately.
- **Coherence & Consistency:** Tell a cohesive story with a clear beginning, middle, and end. Ensure consistency in tense, perspective, and character details.
- **Complete Ending:** The story MUST have a clear, satisfying, and conclusive ending. It must NOT end abruptly or mid-sentence/paragraph.
- **Formatting:** Start ONLY with the title in the format '# {titel}'. Do NOT add another title. Structure the text meaningfully with paragraphs.
{zusatz_anweisungen}""",
"PROMPT_OUTLINE_GEN": """You are a seasoned author and story developer. Create a detailed plot outline for a story with {kapitel_anzahl} chapters based on:
Title: {titel}
Prompt: {prompt}
Setting: {setting}
Total word count: approx. {wortanzahl} words ({sprache})
Create an outline with **clearly delineated sections for each chapter**:
1. Main Characters: Details (name, age, appearance, personality, motivation).
2. Important Side Characters: Names, relationships.
3. Main Plot: Detailed summary of the storyline across all chapters.
4. World Rules (if applicable).
5. Chapter Structure ({kapitel_anzahl} chapters): **For each chapter (clearly start each section with e.g., 'Chapter X:' or '## Chapter X')**: Meaningful Title, Time/Location, Characters Present, Key Plot Points/Scenes (5-7, specific to this chapter), Character Development/Twists in this chapter, Cliffhanger (optional).
**Important:** The outline should serve as a foundation for a **high-quality story**. Ensure consistency, causality, logical progression, character development, and potential for engaging narrative adhering to high writing standards (varied language, avoiding repetition in the concept, etc.). Chapter sections must be clearly identifiable.
{zusatz_anweisungen}""",
# --- **FINAL REVISED** Chapter Prompt with Hierarchy ---
"PROMPT_CHAPTER_GEN": """You are a talented and experienced author of short stories in **English**. Your task is to write **Chapter {kapitel_nummer} of {kapitel_anzahl}** of a story exceptionally well, using the provided contexts **precisely and hierarchically**.
**Context and Instructions for Chapter {kapitel_nummer}:**
1. **WHAT SHOULD HAPPEN? (Primary Focus!)** - Adhere **strictly** to the plot points for **this specific chapter** as described in the following excerpt from the plot outline:
--- START OUTLINE EXCERPT CHAPTER {kapitel_nummer} ---
{plot_outline_segment}
--- END OUTLINE EXCERPT ---
Implement **only** these points. Do not deviate, add elements from other chapters, or foreshadow excessively unless the outline implies it.
2. **WHAT HAS ALREADY HAPPENED? (Global Consistency & Anti-Looping!)** - Refer to the following summary of the **entire plot so far**. Ensure your narrative is consistent with it. **Do NOT repeat major plot arcs or resolved core conflicts** mentioned in this summary! Pay close attention to the status of key plot points.
--- START RUNNING OVERALL SUMMARY ---
{running_plot_summary}
--- END RUNNING OVERALL SUMMARY ---
3. **HOW DID THE LAST CHAPTER END? (Direct Connection!)** - Begin **seamlessly** following the end of the previous chapter ({prev_kapitel_nummer}) as described in the detailed context below (summary + raw end). Rephrase the transition and do not copy sentences.
--- START CONTEXT CHAPTER {prev_kapitel_nummer} ---
{zusammenfassung_vorher}
--- END CONTEXT CHAPTER {prev_kapitel_nummer} ---
**Further Quality Guidelines:**
- Length: Approx. {kapitel_wortanzahl} words (min {min_kapitel_worte}, max {max_kapitel_worte}).
- Language & Style: Clear, concise, engaging. **Strong variation** in sentence length, structure, and vocabulary. Excellent reading flow.
- **STRICTLY AVOID Repetition:** Avoid repeating not just phrases, but also **similar sentence patterns, descriptions, and plot patterns**. {zusatz_anweisungen}
- Strong language, "Show, Don't Tell", believable dialogue, atmosphere, pacing (according to outline segment).
- Formatierung: Beginne mit '## Chapter {kapitel_nummer}: [Appropriate Title]' (Exception: '# {titel}\\n\\n## Chapter 1: [Chapter Title]'). No title at the end.
- Conclusion: Complete, meaningful sentence/paragraph.
**Write Chapter {kapitel_nummer} now, following ALL instructions:**
""",
# --- **FINAL REVISED** Epilogue Prompt ---
"PROMPT_EPILOG_GEN": """You are a talented author. Write a **high-quality, well-written,** and satisfying conclusion (Epilogue) for a story.
**Context:**
- **Title:** {titel}
- **Plot Outline (Overall overview):**
{plot_outline}
- **Running Summary of the ENTIRE Plot up to the end of the last chapter:**
{running_plot_summary}
- **Detailed Context & Raw End from the LAST Chapter ({kapitel_anzahl}):**
{zusammenfassung_vorher}
- **Original Text Fragment from the end of the last chapter:**
...{letztes_kapitel_ende}
**Your Task for the Epilogue:**
1. **Resolution:** Elegantly resolve the most important open plot threads and character arcs, based on the **Outline** and the **running summary**.
2. **Connection:** Connect stylistically and thematically to the **detailed context** of the last chapter.
3. **Character Ending:** Bring the main characters' emotional journey to a meaningful end.
4. **Themes:** Revisit the main themes of the story.
5. **Quality:** Write in a matching style (varied language, no repetition). {zusatz_anweisungen}
6. **Length:** Approx. 500-1000 words.
7. **Formatting:** Start with '## Epilogue'. No title at the end.
8. **Ending:** Ensure a clear, satisfying ending.
**Write the Epilogue now:**
""",
# --- **FINAL REVISED** Detailed Summary Prompt ---
"PROMPT_SUMMARY_GEN": """You are an assistant specialized in concisely summarizing story chapters to ensure continuity for the author (an AI).
**Task:** Carefully read the following chapter text and create a brief, concise summary (approx. 150-250 words) in **English**.
**Focus of the Summary:**
- What are the **most important events** that occurred in this chapter?
- What **significant developments** happened with the main characters (decisions, realizations, emotional states)?
- What is the **exact state at the end of the chapter**? (Where are the characters? What is the immediate situation? What tension or open question remains?)
- **IMPORTANT:** What is the **current status of key open questions or core conflicts** (e.g., Was a crucial ritual performed? Was a main secret revealed? Was a primary goal achieved?) based on the events *in this* chapter? State the status clearly (e.g., "Ritual completed", "Mystery X unsolved", "Goal Y approached").
- Avoid minor details and focus on information that is **absolutely essential** for understanding the next chapter.
**Chapter Text to Summarize:**
--- START TEXT ---
{kapitel_text}
--- END TEXT ---
**Your Summary (only the summary text):**
""",
# --- **FINAL REVISED** Running Summary Update Prompt ---
"PROMPT_RUNNING_SUMMARY_UPDATE": """You are an editor assisting an AI in writing a story. Your task is to maintain and update a **running summary of the entire plot so far**.
**Previous Plot Summary:**
{bisherige_zusammenfassung}
**Text of the NEW Chapter ({kapitel_nummer}) that was just added:**
--- START NEW CHAPTER ---
{neues_kapitel_text}
--- END NEW CHAPTER ---
**Your Task:**
Concisely update the "Previous Plot Summary" by adding the **most important new events, character developments, and plot points** from the NEW chapter. Maintain the narrative thread and **clearly state the progress or completion of major plot arcs**. The new summary should cover the entire story up to and including the new chapter and reflect the **current status of the main conflicts/goals**. Keep it as brief as possible, but ensure all *essential* plot points are included.
**Updated Overall Summary (only the text of the new summary):**
""",
# --- Other texts (ensure all needed keys are present) ---
"USER_PROMPT_STORY": "Please write a high-quality short story based on title '{titel}', prompt, and setting. Approx. {wortanzahl} words.",
"USER_PROMPT_OUTLINE": "Please create a detailed plot outline for a {kapitel_anzahl}-chapter, high-quality story titled '{titel}'.",
"USER_PROMPT_CHAPTER": "Please write chapter {kapitel_nummer} of the story '{titel}' exceptionally well, following all quality guidelines, especially adhering to the hierarchical context instructions.",
"USER_PROMPT_EPILOG": "Please write a high-quality epilogue for the story '{titel}', considering the context from the last chapter and the overall plot.",
"USER_PROMPT_SUMMARY": "Please summarize the previous chapter according to the instructions to ensure continuity.",
"USER_PROMPT_RUNNING_SUMMARY": "Please update the running story summary with the events from the latest chapter.",
"DEFAULT_CHAPTER_TITLE": "Chapter {kapitel_nummer}", # Base name
"FIRST_CHAPTER_TITLE": "Chapter {kapitel_nummer}", # Base name, formatted to 1 later
"EPILOG_TITLE": "Epilogue", # Base name
"ERROR_CHAPTER_TITLE": "Chapter {kapitel_nummer}: [Generation Error]", # Internal use
"ERROR_CHAPTER_CONTENT": "An error occurred during the generation of this chapter.", # Internal use
"RESCUED_CHAPTER_NOTICE": "[This chapter was shortened due to technical issues or could not be fully recovered. The story continues in the next chapter.]",
"RESCUED_EPILOG_NOTICE": "[The story could not be fully completed. The epilogue is missing or incomplete.]",
"SUMMARY_FIRST_CHAPTER": "This is the first chapter. No previous context exists.", # More explicit
"WARN_LOW_WORDCOUNT": "Warning: Target word count {wortanzahl} very low. Setting to minimum {min_val}.",
"WARN_HIGH_WORDCOUNT": "Warning: Target word count {wortanzahl} too high without chapter mode. Setting to maximum {max_val}.",
"INFO_CHAPTER_MODE_ACTIVATED": "Activating chapter mode for story with approx. {wortanzahl} words.",
"INFO_GENERATING_STORY": "Generating story '{titel}' with approx. {wortanzahl} words in {sprache} (Quality Focus)...",
"INFO_SENDING_API_REQUEST": "Sending request to API (Model: {model}, Max Tokens: {max_tokens}, Temp: {temp:.2f})...",
"INFO_WAITING_API_RESPONSE": "Waiting for API response...",
"INFO_GENERATION_COMPLETE": "Generation completed in {dauer:.1f} seconds.",
"INFO_SAVED_TEMP_FILE": "Intermediate data saved to temporary file: {dateiname}", # Debugging
"WARN_CANNOT_WRITE_TEMP": "Warning: Could not write temporary file: {error}", # Debugging
"WARN_CANNOT_DELETE_TEMP": "Warning: Could not delete temporary file: {error}", # Debugging
"INFO_REMOVED_INCOMPLETE_SENTENCE": "Removed incomplete sentence at the end.",
"INFO_CORRECTING_ENDING": "Correcting text ending...",
"INFO_REMOVED_INCOMPLETE_DIALOG": "Removed incomplete dialogue at the end.",
"INFO_ADDED_MISSING_QUOTE": "Added missing quotation mark.",
"INFO_REMOVED_INCOMPLETE_PARAGRAPH": "Removed incomplete last paragraph.",
"INFO_GENERATING_CHAPTERS": "Generating story in {kapitel_anzahl} chapters (Quality Focus)...",
"INFO_GENERATING_OUTLINE": "Generating plot outline...",
"INFO_OUTLINE_CREATED": "Plot outline created.",
"INFO_SAVED_OUTLINE_TEMP": "Plot outline temporarily saved: {dateiname}", # Debugging Temp Save
"INFO_SAVED_OUTLINE_FINAL": "Plot outline saved as: {dateiname}", # Final Save
"WARN_CANNOT_SAVE_OUTLINE_TEMP": "Warning: Could not save temporary plot outline: {error}", # Debugging Temp Save
"ERROR_SAVING_OUTLINE_FINAL": "Error saving plot outline: {error}. Continuing generation.", # Final Save
"INFO_GENERATING_CHAPTER_NUM": "Generating chapter {kapitel_nummer} of {kapitel_anzahl}...",
"INFO_CHAPTER_COMPLETE": "Chapter {kapitel_nummer} completed ({dauer:.1f}s).",
"ERROR_API_REQUEST_CHAPTER": "API error during chapter {kapitel_nummer}: {error}",
"INFO_RESCUED_PARTIAL_CONTENT": "Rescued partially generated content ({chars} characters).",
"ERROR_READING_TEMP_FILE": "Error reading temporary file: {error}", # Debugging
"INFO_LAST_CHAPTER_INCOMPLETE": "Last chapter seems incomplete or was rescued. Generating epilogue...",
"INFO_GENERATING_EPILOG": "Generating epilogue...",
"INFO_EPILOG_GENERATED": "Epilogue generated ({dauer:.1f}s).",
"ERROR_GENERATING_EPILOG": "Error generating epilogue: {error}",
"INFO_FINAL_WORD_COUNT": "Final story generated with {wortanzahl} words.",
"INFO_SAVED_TEXT_FILE": "Story saved as text file: {dateiname}", # Used by CLI save_as_text_file
"ERROR_API_OVERLOAD": "API overloaded/timeout. Wait {delay:.1f}s, retry {retries}/{max_retries}...",
"ERROR_API_CALL_FAILED": "Error during API call: {error}",
"ERROR_ALL_RETRIES_FAILED": "All API retries failed.",
"ERROR_UNSUPPORTED_LANGUAGE": "Language '{sprache}' not supported. Supported: {supported}",
"ERROR_MISSING_API_KEY": "API key required. Set NEBIUS_API_KEY or pass directly.",
"ERROR_GENERATION_FAILED": "Story generation failed.",
"COMMON_NOUN_PREFIXES": ["The", "A", "An"], # Needed for _extract_char_names (if used internally)
"ACTION_VERBS": ["went", "came", "spoke", "said", "answered", "replied", "saw", "found", "discovered", "opened", "closed", "ran", "fled", "fought", "died", "killed", "kissed", "looked", "thought", "realized"], # Needed for _extract_important_sentences (if used internally)
"EMOTIONAL_WORDS": ["fear", "joy", "happiness", "sadness", "anger", "wrath", "love", "hate", "horror", "surprise", "shock", "despair", "relief"], # Needed for _extract_important_sentences (if used internally)
"CONJUNCTIONS_AT_END": ['and', 'but', 'or', 'so', 'yet', 'because', 'if', 'that', 'when', 'while', 'although'],
# --- Context Markers (Adopted from Web Script) ---
"SUMMARY_LLM_PREFIX": "**Context - Summary of the previous chapter (AI-generated):**",
"SUMMARY_RAW_END_MARKER_START": "\n\n**--- EXACT ENDING OF PREVIOUS CHAPTER (CONTEXT) ---**",
"SUMMARY_RAW_END_MARKER_END": "**--- END CONTEXT ---**",
"SUMMARY_FALLBACK": "[Context from the previous chapter could not be automatically generated. Please recall the events from the previous chapter.]",
"INFO_GENERATING_SUMMARY": "Generating LLM summary + end extract for chapter {kapitel_nummer}...",
"INFO_SUMMARY_GENERATED": "LLM summary + end extract for chapter {kapitel_nummer} created.",
"ERROR_GENERATING_SUMMARY": "Error generating LLM summary for chapter {kapitel_nummer}: {error}",
# --- Running Summary (Adopted from Web Script) ---
"RUNNING_SUMMARY_PLACEHOLDER": "**Running Summary of the Entire Plot So Far:**\n",
"RUNNING_SUMMARY_INITIAL": "The story begins.", # Initial text for empty summary
"INFO_UPDATING_RUNNING_SUMMARY": "Updating running plot summary after chapter {kapitel_nummer}...",
"INFO_RUNNING_SUMMARY_UPDATED": "Running plot summary updated.",
"ERROR_UPDATING_RUNNING_SUMMARY": "Error updating running summary after chapter {kapitel_nummer}: {error}",
"ERROR_GENERATING_OUTLINE_FALLBACK": "Plot outline generation failed", # Fallback text for outline
}
}
def __init__(self, api_key: Optional[str] = None, model: str = MODELL_NAME, output_dir: str = "."):
"""Initializes the StoryGenerator."""
self.resolved_api_key = api_key or os.environ.get("NEBIUS_API_KEY")
lang_conf_de = self.LANGUAGE_CONFIG["Deutsch"] # Use German for init errors
if not self.resolved_api_key:
raise ValueError(lang_conf_de["ERROR_MISSING_API_KEY"])
try:
self.client = OpenAI(
base_url=API_BASE_URL,
api_key=self.resolved_api_key,
timeout=300.0 # Longer timeout for potentially long story generation
)
except Exception as e:
log.error(f"Error initializing OpenAI client: {e}")
raise
self.model_name = model
self.output_dir = output_dir # Store output directory for saving outline
self.total_steps = 0 # For progress bar
self.current_step = 0 # For progress bar
def _get_lang_config(self, sprache: str) -> Dict[str, Any]:
"""Gets the configuration for the specified language ('Deutsch' or 'Englisch')."""
lang_lower = sprache.lower()
if lang_lower in ['deutsch', 'german', 'de']:
target_lang = "Deutsch"
elif lang_lower in ['englisch', 'english', 'en']:
target_lang = "Englisch"
else:
target_lang = sprache if sprache in self.LANGUAGE_CONFIG else None
config = self.LANGUAGE_CONFIG.get(target_lang) if target_lang else None
if not config:
supported = ", ".join(SUPPORTED_LANGUAGES) # Use module-level constant
lang_conf_de = self.LANGUAGE_CONFIG["Deutsch"] # Use German for the error itself
raise ValueError(lang_conf_de["ERROR_UNSUPPORTED_LANGUAGE"].format(sprache=sprache, supported=supported))
return config
def _safe_filename(self, title: str) -> str:
"""Creates a safe filename from a title."""
safe_title = re.sub(r'[^\w\s-]', '', title).strip()
safe_title = re.sub(r'\s+', '_', safe_title)
return safe_title[:50]
def show_progress_bar(self):
"""Displays or updates the console progress bar."""
if self.total_steps <= 0: return
percent = float(self.current_step) / self.total_steps
bar_length = 40
arrow_length = max(0, int(round(percent * bar_length) - 1))
arrow = '=' * arrow_length + ('>' if self.current_step < self.total_steps else '=')
spaces = ' ' * (bar_length - len(arrow))
percent_display = min(100, int(round(percent * 100)))
sys.stdout.write(f"\rProgress: [{arrow}{spaces}] {percent_display}% ({self.current_step}/{self.total_steps})")
sys.stdout.flush()
if self.current_step >= self.total_steps:
sys.stdout.write('\n')
def _update_progress(self, step_increment=1):
"""Increments progress step and updates the bar."""
self.current_step = min(self.current_step + step_increment, self.total_steps)
self.show_progress_bar()
def retry_api_call(self, call_function, *args, **kwargs):
"""Executes an API call with automatic retries on overload errors."""
retries = 0
lang_conf = self._get_lang_config("Deutsch") # Use German for generic messages
while retries <= MAX_RETRIES:
try:
return call_function(*args, **kwargs)
except Exception as e:
# Convert specific OpenAI errors for better matching
error_str = str(e).lower()
error_type = type(e).__name__
is_retryable = (
"overloaded" in error_str or
"rate limit" in error_str or
"timeout" in error_str or
"503" in error_str or
"504" in error_str or
"connection error" in error_str or
"service unavailable" in error_str or
isinstance(e, (OpenAI.APITimeoutError, OpenAI.APIConnectionError, OpenAI.RateLimitError, OpenAI.InternalServerError)) # Corrected attribute access
)
if is_retryable and retries < MAX_RETRIES:
retries += 1
current_retry_delay = (DEFAULT_RETRY_DELAY_S *
(RETRY_BACKOFF_FACTOR ** (retries - 1)) +
random.uniform(0.1, 1.0)) # Jitter
log.warning(lang_conf["ERROR_API_OVERLOAD"].format(
delay=current_retry_delay, retries=retries, max_retries=MAX_RETRIES
) + f" (Type: {error_type})", exc_info=False) # Log error type
time.sleep(current_retry_delay)
else:
# Log the final error with traceback if not retryable or retries exceeded
log.error(lang_conf["ERROR_API_CALL_FAILED"].format(error=str(e)) + f" (Type: {error_type})", exc_info=True)
raise # Re-raise the original exception
# This part should ideally not be reached if the loop always raises on failure
raise Exception(lang_conf["ERROR_ALL_RETRIES_FAILED"])
def _create_system_prompt(self, sprache: str, template_key: str, context: Dict[str, Any]) -> str:
"""Creates a system prompt based on language and template."""
lang_conf = self._get_lang_config(sprache)
template = lang_conf.get(template_key)
if not template:
raise ValueError(f"Prompt template '{template_key}' not found for language '{sprache}'.")
context['sprache'] = sprache # Ensure language is in context
context.setdefault('zusatz_anweisungen', '')
# Format additional instructions if provided
if context.get('zusatz_anweisungen'): # Check if value is truthy
if not context['zusatz_anweisungen'].strip().startswith("**Zusätzliche Anweisungen:**") and \
not context['zusatz_anweisungen'].strip().startswith("**Additional Instructions:**"):
prefix = "**Zusätzliche Anweisungen:**" if sprache == "Deutsch" else "**Additional Instructions:**"
context['zusatz_anweisungen'] = f"\n{prefix}\n{context['zusatz_anweisungen'].strip()}\n"
else:
context['zusatz_anweisungen'] = f"\n{context['zusatz_anweisungen'].strip()}\n"
else:
context['zusatz_anweisungen'] = ""
# Ensure all required keys for the specific template are present or provide defaults
required_keys = {
"PROMPT_STORY_GEN": ['wortanzahl', 'titel', 'prompt', 'setting'],
"PROMPT_OUTLINE_GEN": ['kapitel_anzahl', 'titel', 'prompt', 'setting', 'wortanzahl'],
"PROMPT_CHAPTER_GEN": ['kapitel_nummer', 'kapitel_anzahl', 'titel', # Removed 'prompt', 'setting' as they are less critical here now
'plot_outline_segment', # Uses segment
'zusammenfassung_vorher', 'running_plot_summary',
'kapitel_wortanzahl', 'min_kapitel_worte', 'max_kapitel_worte', 'prev_kapitel_nummer'],
"PROMPT_EPILOG_GEN": ['titel', 'plot_outline', 'zusammenfassung_vorher', # Epilog uses full outline
'running_plot_summary', 'letztes_kapitel_ende', 'kapitel_anzahl'],
"PROMPT_SUMMARY_GEN": ['kapitel_text'],
"PROMPT_RUNNING_SUMMARY_UPDATE": ['bisherige_zusammenfassung', 'neues_kapitel_text', 'kapitel_nummer']
}
defaults = {
'wortanzahl': DEFAULT_TARGET_WORDS,
'titel': 'Unbenannt' if sprache == "Deutsch" else 'Untitled',
'prompt': '', 'setting': '', 'kapitel_anzahl': 0, 'kapitel_nummer': 0,
'plot_outline': '[Keine Outline]' if sprache == "Deutsch" else '[No Outline]', # Default for Epilog
'plot_outline_segment': '[Outline Segment nicht verfügbar]' if sprache == "Deutsch" else '[Outline Segment Unavailable]',
'zusammenfassung_vorher': '', 'kapitel_wortanzahl': 1000,
'min_kapitel_worte': 800, 'max_kapitel_worte': 1500,
'letztes_kapitel_ende': '', 'kapitel_text': '',
'running_plot_summary': lang_conf.get("RUNNING_SUMMARY_PLACEHOLDER", "") + lang_conf.get("RUNNING_SUMMARY_INITIAL", ""), # Default for new key
'bisherige_zusammenfassung': lang_conf.get("RUNNING_SUMMARY_INITIAL", ""), 'neues_kapitel_text': '', # Defaults for new prompt
'prev_kapitel_nummer': 0
}
# Set defaults for keys required by the current template
if template_key in required_keys:
for key in required_keys[template_key]:
context.setdefault(key, defaults.get(key))
# Use .format_map for safer formatting
try:
# Add model name to context for potential use in prompts (optional)
context['model_name'] = self.model_name
return template.format_map(context)
except KeyError as e:
log.error(f"Missing key in prompt context for template '{template_key}': {e}. Context: {context}", exc_info=True)
raise ValueError(f"Missing context for prompt template '{template_key}': {e}")
def _generate_single_story(self, prompt: str, setting: str, titel: str,
word_count: int, sprache: str,
additional_instructions: Optional[str]
) -> Optional[str]:
"""Generates a shorter story in a single API call with quality focus."""
lang_conf = self._get_lang_config(sprache)
temp_fd, temp_dateiname = tempfile.mkstemp(prefix="temp_story_", suffix=".txt")
os.close(temp_fd)
self._update_progress(0) # Initialize progress for single story
try:
system_prompt = self._create_system_prompt(
sprache, "PROMPT_STORY_GEN", {
"wortanzahl": word_count, "titel": titel, "prompt": prompt,
"setting": setting, "zusatz_anweisungen": additional_instructions
})
max_tokens = min(int(word_count * TOKEN_WORD_RATIO * 1.1), MAX_TOKENS_PER_CALL)
temperature = 0.75
log.info(lang_conf["INFO_SENDING_API_REQUEST"].format(model=self.model_name, max_tokens=max_tokens, temp=temperature))
start_time = time.time()
log.info(lang_conf["INFO_WAITING_API_RESPONSE"] + " (Single Story)")
user_prompt_text = lang_conf["USER_PROMPT_STORY"].format(titel=titel, wortanzahl=word_count)
response = self.retry_api_call(
self.client.chat.completions.create,
model=self.model_name, max_tokens=max_tokens, temperature=temperature,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt_text},
]
)
story = response.choices[0].message.content
duration = time.time() - start_time
log.info(lang_conf["INFO_GENERATION_COMPLETE"].format(dauer=duration))
self._update_progress(1) # Complete progress for single story
try:
with open(temp_dateiname, 'w', encoding='utf-8') as f: f.write(story)
log.debug(lang_conf["INFO_SAVED_TEMP_FILE"].format(dateiname=temp_dateiname))
except Exception as e:
log.warning(lang_conf["WARN_CANNOT_WRITE_TEMP"].format(error=str(e)))
story = self._format_story(story, titel, sprache)
story = self._clean_text_ending(story, sprache)
return story
except Exception as e:
# Ensure progress bar finishes on error
if self.current_step < self.total_steps: self._update_progress(self.total_steps - self.current_step)
if not isinstance(e, (OpenAI.APIError, Exception)): # Avoid double logging if retry failed
log.error(f"Unexpected error during single story generation for '{titel}': {str(e)}", exc_info=True)
# Attempt rescue
if os.path.exists(temp_dateiname):
try:
with open(temp_dateiname, 'r', encoding='utf-8') as f:
partial_story = f.read()
if len(partial_story) > MIN_CHARS_FOR_RESCUE:
log.info(lang_conf["INFO_RESCUED_PARTIAL_CONTENT"].format(chars=len(partial_story)))
partial_story = self._format_story(partial_story, titel, sprache)
partial_story = self._clean_text_ending(partial_story, sprache)
notice_key = "RESCUED_EPILOG_NOTICE" # Use this key for general incompleteness
return partial_story + f"\n\n{lang_conf.get(notice_key, '[Story generation may be incomplete due to an error.]')}"
except Exception as e2:
log.error(lang_conf["ERROR_READING_TEMP_FILE"].format(error=str(e2)))
return None
finally:
if os.path.exists(temp_dateiname):
try: os.remove(temp_dateiname)
except Exception as e: log.warning(lang_conf["WARN_CANNOT_DELETE_TEMP"].format(error=str(e)))
def _format_story(self, story: str, titel: str, sprache: str) -> str:
"""Ensures the story starts correctly with the title (H1)."""
story = story.strip()
titel_prefix = f"# {titel}" # Markdown H1 format
if not story.startswith(titel_prefix):
lines = story.split('\n')
# Remove all lines at the beginning starting with #, except the expected title
while lines and lines[0].strip().startswith("#") and lines[0].strip() != titel_prefix:
log.debug(f"Removing incorrect leading line during format_story: {lines[0]}")
lines.pop(0)
story = "\n".join(lines).strip()
# Add the correct title if it's still missing
if not story.startswith(titel_prefix):
log.debug(f"Prepending main title '{titel_prefix}' during format_story")
story = f"{titel_prefix}\n\n{story}"
return story
def _clean_text_ending(self, text: str, sprache: str) -> str:
"""Checks and corrects abrupt endings in the text. (Adopted from Web version, generally more robust)"""
if not text or len(text.strip()) < MIN_CHARS_FOR_RESCUE // 3:
return text
lang_conf = self._get_lang_config(sprache)
original_text = text
text = text.rstrip()
# 1. Incomplete sentence ending (more conservative check)
# Looks for patterns like "... word Word." at the very end
match = re.search(r'[a-zäöüß][,;]?\s+[A-ZÄÖÜ][a-zäöüß]+\.?\s*$', text)
if match:
# Find last definite sentence end BEFORE the match
last_sentence_end = -1
potential_ends = ['. ', '! ', '? ', '." ', '!" ', '?" ', '\n\n']
for p in potential_ends:
found_pos = text.rfind(p, 0, match.start())
if found_pos > last_sentence_end:
# Calculate actual end index after punctuation + space/quote
end_idx = found_pos + len(p)
if p.endswith('" ') and len(text) > end_idx and text[end_idx] == '"': end_idx +=1 # Handle ." " etc.
last_sentence_end = end_idx
if last_sentence_end > 0:
text = text[:last_sentence_end].strip() # Cut after the end marker
log.info(lang_conf["INFO_REMOVED_INCOMPLETE_SENTENCE"])
else:
log.debug("Could not reliably fix potentially incomplete sentence at the end.")
# 2. No sentence-ending punctuation and ends with letter/number
ends_with_punctuation = any(text.endswith(p) for p in ['.', '!', '?', '"', "'", '”', '’'])
last_char_is_alphanum = text[-1].isalnum() if text else False
if not ends_with_punctuation and last_char_is_alphanum:
# Find the last punctuation mark anywhere before the end
last_sentence_end = max(text.rfind('.'), text.rfind('!'), text.rfind('?'), text.rfind('\n\n'))
if last_sentence_end > 0:
# Truncate after the last found punctuation/paragraph break
cutoff_point = last_sentence_end + 1
# Include trailing quotes/spaces after punctuation
while cutoff_point < len(text) and text[cutoff_point] in [' ', '"', "'", '”', '’']:
cutoff_point += 1
text = text[:cutoff_point].strip()
log.info(lang_conf["INFO_CORRECTING_ENDING"] + " (Missing punctuation/abrupt end)")
else:
# Maybe it ends mid-sentence without prior punctuation? Try cutting last line.
last_newline = text.rfind('\n')
if last_newline > 0 and len(text) - last_newline < 50: # If last line is short
text = text[:last_newline].strip()
log.info(lang_conf["INFO_CORRECTING_ENDING"] + " (Removed potentially incomplete last line)")
else:
log.debug("Text ends without punctuation, and no prior punctuation found.")
# 3. Incomplete dialogue quotes (Handles various quote types)
paragraphs = text.split('\n\n')
last_paragraph = paragraphs[-1].strip() if paragraphs else ""
if last_paragraph:
quote_chars = ['"', "'", '“', '”', '‘', '’']
matching_quotes = {'"': '"', "'": "'", '“': '”', '‘': '’'}
open_quote_stack = []
for char in last_paragraph:
if char in matching_quotes.keys(): # Opening quote
open_quote_stack.append(char)
elif char in matching_quotes.values(): # Closing quote
# Check if it matches the last opened quote
if open_quote_stack and char == matching_quotes.get(open_quote_stack[-1]):
open_quote_stack.pop()
# else: Mismatched closing quote - ignore for ending check
# If stack is not empty, a quote is unclosed
if open_quote_stack:
last_open_quote_char = open_quote_stack[-1]
# Find the position of the last unclosed opening quote
last_open_quote_index = last_paragraph.rfind(last_open_quote_char)
# Find the last sentence end *within the paragraph* before the unclosed quote
last_sentence_end_in_para = max(
last_paragraph.rfind('.', 0, last_open_quote_index),
last_paragraph.rfind('!', 0, last_open_quote_index),
last_paragraph.rfind('?', 0, last_open_quote_index)
)
# Check if the unclosed quote starts after the last sentence ends
if last_open_quote_index > last_sentence_end_in_para:
# Cut off after the last complete sentence before the dangling dialogue
text_before_last_para = '\n\n'.join(paragraphs[:-1])
if text_before_last_para: text_before_last_para += '\n\n'
cutoff_point_in_para = last_sentence_end_in_para
if cutoff_point_in_para >= 0 : # Found punctuation before the dangling quote
# Include trailing space/quote after punctuation
cutoff_point_in_para += 1
while cutoff_point_in_para < last_open_quote_index and last_paragraph[cutoff_point_in_para] in [' ', '"', "'", '”', '’']:
cutoff_point_in_para += 1
text = text_before_last_para + last_paragraph[:cutoff_point_in_para].strip()
log.info(lang_conf["INFO_REMOVED_INCOMPLETE_DIALOG"])
else:
# No sentence end found before the quote in this paragraph, remove the whole para
text = text_before_last_para.strip()
log.info(lang_conf["INFO_REMOVED_INCOMPLETE_PARAGRAPH"] + " (Due to dangling quote start)")
# 4. Short last paragraph or ending with conjunction
paragraphs = text.split('\n\n') # Recalculate in case text changed
if len(paragraphs) > 1:
last_paragraph_words = paragraphs[-1].strip().split()
# Get language-specific conjunctions
conjunctions = lang_conf.get("CONJUNCTIONS_AT_END", [])
ends_with_conj = False
if last_paragraph_words:
last_word_cleaned = last_paragraph_words[-1].lower().strip('".!?,’”)')
ends_with_conj = last_word_cleaned in conjunctions
# Check if the *second last* paragraph looks complete
second_last_para = paragraphs[-2].strip()
second_last_ends_ok = any(second_last_para.endswith(p) for p in ['.','!','?','"','\'','”','’'])
# Remove last paragraph if it's short/ends with conjunction AND previous looks complete
if (len(last_paragraph_words) < MIN_WORDS_FOR_VALID_ENDING or ends_with_conj) and second_last_ends_ok:
text = '\n\n'.join(paragraphs[:-1]).strip()
log.info(lang_conf["INFO_REMOVED_INCOMPLETE_PARAGRAPH"])
elif len(last_paragraph_words) < 2 and not second_last_ends_ok:
# Also remove very short (0/1 word) last paragraphs if prev is also incomplete
text = '\n\n'.join(paragraphs[:-1]).strip()
log.info(lang_conf["INFO_REMOVED_INCOMPLETE_PARAGRAPH"] + " (Very short)")
# Log if changes were made
if text != original_text:
log.info(f"Text ending cleaned. Length reduced from {len(original_text)} to {len(text)}.")
log.debug(f"Cleaned End: ...{text[-80:]}")
return text
def _optimize_chapter_structure(self, word_count: int, max_words_per_chapter: int) -> Tuple[int, List[int]]:
"""Calculates a simple chapter structure. (Adopted from Web version)"""
if word_count <= max_words_per_chapter * 1.5:
# If only slightly above max, create 1 or 2 chapters
num_chapters = max(1, math.ceil(word_count / max_words_per_chapter))
else:
# Otherwise, aim for a number based on total length or minimum required
num_chapters = max(MIN_CHAPTERS_LONG_STORY,
round(word_count / TARGET_WORDS_PER_CHAPTER_DIVISOR))
num_chapters = max(1, num_chapters) # Ensure at least 1
base_words = word_count // num_chapters
remainder = word_count % num_chapters
words_per_chapter = [base_words] * num_chapters
# Distribute remainder words to the first few chapters
for i in range(remainder):
words_per_chapter[i] += 1
# Recalculate if any chapter significantly exceeds the max
# Allow slightly more flexibility (e.g., 1.1x) before forcing recalculation
max_allowed_flex = max_words_per_chapter * 1.1
if any(w > max_allowed_flex for w in words_per_chapter):
log.debug(f"Recalculating chapter structure as limit {max_words_per_chapter} (~{max_allowed_flex:.0f}) was exceeded.")
num_chapters += 1 # Just add one more chapter
base_words = word_count // num_chapters
remainder = word_count % num_chapters
words_per_chapter = [base_words] * num_chapters
for i in range(remainder): words_per_chapter[i] += 1
# Check again, log warning if still problematic (e.g., extremely high total count)
if any(w > max_words_per_chapter * 1.2 for w in words_per_chapter): # Use 1.2x for warning
log.warning(f"Chapter structure could not strictly meet the max limit ({max_words_per_chapter}). Final distribution: {words_per_chapter}")
# Ensure no chapter has 0 words (minimum practical size)
words_per_chapter = [max(100, w) for w in words_per_chapter] # Set a minimum floor
return num_chapters, words_per_chapter
# Removed: _extract_character_names, _extract_important_sentences, _create_chapter_summary
# Added: _extract_outline_segment, _generate_chapter_summary_llm, _update_running_summary_llm (Copied from Web version)
def _extract_outline_segment(self, plot_outline: str, chapter_number: int, total_chapters: int, sprache: str) -> Optional[str]:
"""
Attempts to extract the specific section for the given chapter_number
from the full plot_outline text using regex. Returns the segment or None.
Relies on headings like "Kapitel X:" or "## Kapitel X".
"""
if not plot_outline: return None
lang_conf = self._get_lang_config(sprache)
if lang_conf.get("ERROR_GENERATING_OUTLINE_FALLBACK", "<ERR>") in plot_outline:
return None
chapter_word = "Kapitel" if sprache == "Deutsch" else "Chapter"
# Regex: Optional leading whitespace/markdown, chapter word, number, optional colon/space/newline
# Makes the chapter number mandatory \b ensures whole word match
start_pattern = re.compile(
rf"^[#\s]*{chapter_word}\s+{chapter_number}\b[:\s]*\n?",
re.IGNORECASE | re.MULTILINE
)
next_chapter_num = chapter_number + 1
# End pattern: Start of the next chapter OR common concluding words (Epilog, Fazit, etc.)
# Added more potential end markers
end_pattern = re.compile(
rf"^[#\s]*(?:(?:{chapter_word}\s+{next_chapter_num}\b)|(?:Epilog|Fazit|Conclusion|Summary|Gesamtfazit|Final Thoughts))[:\s]*\n?",
re.IGNORECASE | re.MULTILINE
)
start_match = start_pattern.search(plot_outline)
if not start_match:
# Fallback: Try finding just the number followed by a period or colon, e.g., "3." or "3:"
start_pattern_num_only = re.compile(rf"^[#\s]*{chapter_number}[.:]\s*\n?", re.MULTILINE)
start_match = start_pattern_num_only.search(plot_outline)
if not start_match:
log.warning(f"Could not find start pattern for Chapter {chapter_number} in outline.")
return None # Return None if not found
start_index = start_match.end() # Start *after* the found heading line
# Find the start of the next relevant section or end of text
end_match = end_pattern.search(plot_outline, pos=start_index)
end_index = end_match.start() if end_match else len(plot_outline) # Go to end of string if no next section found
segment = plot_outline[start_index:end_index].strip()
if not segment:
log.warning(f"Extracted empty outline segment for Chapter {chapter_number}. This might indicate an outline formatting issue.")
# Try a simple paragraph grab as fallback? Risky. Return None is safer.
return None
log.debug(f"Extracted outline segment for Chapter {chapter_number} ({len(segment)} chars)")
return segment
def _generate_chapter_summary_llm(self, chapter_text: str, chapter_number: int, sprache: str) -> Tuple[str, str]:
"""
Generates an LLM summary AND extracts the raw end of the chapter.
Returns a tuple: (formatted_summary_with_prefix_and_markers, raw_end_text_only)
"""
lang_conf = self._get_lang_config(sprache)
log.info(lang_conf["INFO_GENERATING_SUMMARY"].format(kapitel_nummer=chapter_number))
# --- 1. Extract Raw End ---
raw_end_text_only = ""
if chapter_text:
chapter_text_stripped = chapter_text.strip()
chars_for_raw_end = 800 # Target length for raw end context
start_index = max(0, len(chapter_text_stripped) - chars_for_raw_end)
raw_end_candidate = chapter_text_stripped[start_index:]
# Try to start raw end from the beginning of the last paragraph if feasible
last_para_break = raw_end_candidate.rfind('\n\n')
# Use paragraph break if it's not too far back (e.g., > 30% into the candidate)
if last_para_break > len(raw_end_candidate) * 0.3:
raw_end_text_only = raw_end_candidate[last_para_break:].strip()
else:
raw_end_text_only = raw_end_candidate.strip()
# Trim if excessively long (safety net)
if len(raw_end_text_only) > chars_for_raw_end * 1.5:
raw_end_text_only = raw_end_text_only[-(int(chars_for_raw_end * 1.5)):]
raw_end_text_only = raw_end_text_only.strip()
log.debug(f"Extracted raw end for chapter {chapter_number} ({len(raw_end_text_only)} chars)")
# --- 2. Generate LLM Summary ---
# Default fallback in case generation fails