Sideproject Friday: LutherLLM

Entwicklung eines historischen KI-Avatars: Martin Luther in Godot

Veröffentlicht am 06.01.2024

Die Vision

Als jemand, der sich sowohl für Geschichte als auch für Bildung begeistert, hat mich schon immer gestört, wie statisch und eindimensional historische Bildung sein kann. Martin Luthers theologische Ideen zu lesen ist eine Sache, aber was wäre, wenn man tatsächlich mit ihm ins Gespräch kommen könnte?

Die Idee hinter diesem Projekt war es, die Kluft zwischen historischen Texten und modernem interaktivem Lernen zu überbrücken. Durch die Kombination von LLM-Technologie mit Charakteranimation haben wir einen KI-Avatar erschaffen, der nicht nur Fakten wiedergibt, sondern einen bedeutungsvollen Dialog über Theologie, Reform und Glauben führt – und dabei historisch akkurat bleibt.

Auch wollten wir zeigen, dass es möglich ist, ein solches Projekt in nur einem Tag zu entwickeln. Wir lieben Herausforderungen.

Technischer Aufbau

  • Game Engine: Godot 4.3
  • KI-Integration: OpenAI API mit gpt-4o-mini Modell
  • Animation: Maßgeschneidertes 2D-Sprite-System mit dynamischen Mundformen und Ausdrucksübergängen
  • Primärquellen: Kuratierte Sammlung von Luthers Schriften und zeitgenössischen Dokumenten
  • Konfiguration: Sichere Schlüsselverwaltung mit Umgebungs- und Datei-Fallbacks
  • Textanzeige: Optimiertes Zeichen-für-Zeichen-Animationssystem mit natürlichem Timing

Entwicklungsreise

Phase 1: Grundlagen und KI-Integration

Eine unserer ersten Herausforderungen war es, die richtige „Stimme“ für Luther zu finden. Erste Versuche produzierten entweder zu moderne oder zu steife und akademische Antworten. Der Durchbruch kam, als wir den Prompt umstrukturierten, um Luthers Rolle als Lehrer und Reformator zu betonen.

Wir haben schnell ein einfaches System mit der OpenAI API aufgebaut und diese Abstraktionsschicht hinzugefügt, um einfach zwischen verschiedenen LLM-Anbietern und Modellen wechseln zu können. So kann das Projekt intelligenter und kostengünstiger werden, wenn sich die Modelle verbessern.

Etwas Prompt-Engineering bringt uns auf ein vernünftiges Niveau von Luther-Authentizität. Nach einigen Experimenten stellten wir fest, dass ein Prompt für einen Lehrer, der Martin Luther spielt, am besten funktioniert. So wird das System nicht leicht durch moderne Themen verwirrt und lenkt das Gespräch zurück zu Luthers theologischen Ansichten.

Hier ist ein Beispiel für den System-Prompt, den wir verwenden:
Sie spielen Martin Luther, den protestantischen Reformator des 16. Jahrhunderts. Bleiben Sie in der Rolle, während Sie sich bewusst sind, dass dies eine historische Darstellung ist. Ihre Antworten sollten Luthers theologische Ansichten, Persönlichkeit und historischen Kontext widerspiegeln.

Kerncharakterzüge:
– Starke Überzeugungen zu Glaube, Schrift und Erlösung durch Gnade
– Direkter und leidenschaftlicher Kommunikationsstil
– Gelehrt, aber fähig, zum einfachen Volk zu sprechen
– Bekannt sowohl für ernsthafte theologische Diskurse als auch für geistreiche Bemerkungen

Die Animationsbefehle sind in den Prompt integriert, sodass das LLM Charakterausdrücke und -bewegungen natürlich steuern kann:

Animationsbefehle:
[POSE=X] für Grundhaltungsänderungen
[EMOTION=X] für Gesichtsausdrücke
[GESTURE=X] für spezifische Bewegungen
[MOUTH=X] für Mundformen während des Sprechens

Verfügbare Befehle:
POSE: neutral, nachdenklich, lehrend, leidenschaftlich, entspannt
EMOTION: neutral, freudig, besorgt, ernst, leidenschaftlich
GESTURE: nicken, kopf_schütteln, nach_oben_zeigen, hand_winken, buch_referenz
MOUTH: A, F, I, L, M, O, S, U, idle

Eine wichtige technische Entscheidung war die Implementierung einer geschichteten Architektur für die LLM-Integration:

# Basis-LLM-Client für gemeinsame Funktionalität
class_name LLMClient extends Node
# Spezifische Implementierung für OpenAI
class_name OpenAIClient extends LLMClient

Die OpenAI-Client-Implementierung enthält eine robuste Konfigurationsverwaltung:

class_name OpenAIClient extends LLMClient

const API_CONFIG_FILE = "res://config/openai_config.cfg"
const API_KEY_ENV_VAR = "OPENAI_API_KEY"

var _cached_key: String = ""
var _config: ConfigFile

func _init() -> void:
    _config = ConfigFile.new()
    _load_config()

func _load_config() -> void:
    # Zuerst Umgebungsvariable prüfen
    var env_key = OS.get_environment(API_KEY_ENV_VAR)
    if env_key and env_key.length() > 0:
        _cached_key = env_key
        print("API-Schlüssel aus Umgebungsvariable wird verwendet")
        return

    # Auf Konfigurationsdatei zurückgreifen
    var err = _config.load(API_CONFIG_FILE)
    if err == OK:
        var config_key = _config.get_value("api", "key", "")
        if config_key and config_key.length() > 0:
            _cached_key = config_key
            print("API-Schlüssel aus Konfigurationsdatei wird verwendet")
            return

    print("Kein API-Schlüssel in Umgebung oder Konfiguration gefunden")

Wir haben auch eine Vorlagen-Konfigurationsdatei erstellt:

# openai_config.template.cfg
[api]
key="ihr-api-schlüssel-hier"
model="gpt-4-mini"
temperature=0.7
max_tokens=150

[system]
debug_logging=false
cache_responses=true

Diese Abstraktion erwies sich als wertvoll, da sie uns ermöglichte:

  • Einfacher Wechsel zwischen verschiedenen LLM-Anbietern
  • Tests mit verschiedenen Modellen (beginnend mit GPT-4-mini)
  • Konsistente Schnittstelle für die restliche Anwendung
  • Sichere Verwaltung von API-Schlüsseln mit mehreren Fallback-Optionen

Phase 2: Animationssystem

Das Animationssystem hat sich über mehrere Iterationen weiterentwickelt:

  1. Erste Version: Einfache Zuordnung von Zeichen zu Mundformen
  2. Erweiterte Version: Phonem-basierte Zuordnung mit Timing-Kontrollen
  3. Aktuelle Version: Kontextbewusstes System mit dynamischen Übergängen

Der Animationsmanager verwaltet nun alle Aspekte der Charakteranimation:

class_name AnimationManager extends Node

# Sprite-Referenzen
@onready var mouth_sprite: Sprite2D = $MouthSprite
@onready var expression_sprite: Sprite2D = $ExpressionSprite
@onready var pose_sprite: Sprite2D = $PoseSprite

# Animations-Status
var current_mouth_shape := "idle"
var current_expression := "neutral"
var current_pose := "neutral"
var is_transitioning := false

# Mundform-Texturen
var mouth_shapes = {
    "A": load("res://assets/A.jpg"),
    "O": load("res://assets/O.jpg"),
    "I": load("res://assets/I.jpg"),
    "S": load("res://assets/S.jpg"),
    "U": load("res://assets/U.jpg"),
    "M": load("res://assets/M.jpg"),
    "F": load("res://assets/F.jpg"),
    "L": load("res://assets/L.jpg"),
    "idle": load("res://assets/Relaxed.jpg")
}

# Phonem-Zuordnung für verbesserte Genauigkeit
const PHONEME_MAP = {
    "ah": "A",
    "aa": "A",
    "ae": "A",
    "oh": "O",
    "ow": "O",
    "oo": "U",
    "ee": "I",
    "ih": "I",
    "eh": "I",
    "ss": "S",
    "sh": "S",
    "ch": "S",
    "mm": "M",
    "bb": "M",
    "pp": "M",
    "ff": "F",
    "vh": "F",
    "th": "F",
    "ll": "L",
    "tt": "L",
    "dd": "L"
}

# Übergangs-Timing
const TRANSITION_TIME = 0.1
const MIN_SHAPE_TIME = 0.05

func _ready() -> void:
    # Mit Standardzuständen initialisieren
    set_mouth_shape("idle")
    set_expression("neutral")
    set_pose("neutral")

func set_mouth_shape(shape: String) -> void:
    if not mouth_shapes.has(shape):
        print("Unbekannte Mundform: ", shape)
        shape = "idle"

    if shape == current_mouth_shape:
        return

    if is_transitioning:
        await get_tree().create_timer(MIN_SHAPE_TIME).timeout

    is_transitioning = true
    current_mouth_shape = shape

    # Tween für sanften Übergang erstellen
    var tween = create_tween()
    tween.tween_property(mouth_sprite, "texture", 
        mouth_shapes[shape], TRANSITION_TIME)
    await tween.finished

    is_transitioning = false

Das Textanzeige-System wurde für flüssige Animation und natürliches Timing optimiert:

class_name TextRevealManager extends Node

# Timing-Konstanten
const CHAR_REVEAL_TIME = 0.025  # Sekunden pro Zeichen
const WORD_PAUSE_TIME = 0.075   # Pause an Wortgrenzen
const PUNCTUATION_PAUSE_TIME = 0.15  # Pause bei Satzzeichen
const PARAGRAPH_PAUSE_TIME = 0.3  # Pause bei Absätzen

# Statusverfolgung
var _is_revealing := false
var _current_text := ""
var _revealed_text := ""
var _current_pos := 0

# Signale
signal text_revealed(text: String)
signal char_revealed(char: String, pos: int)
signal reveal_completed

func reveal_text(text: String) -> void:
    if _is_revealing:
        await cancel_reveal()

    _is_revealing = true
    _current_text = text
    _revealed_text = ""
    _current_pos = 0

    while _current_pos < _current_text.length():
        if not _is_revealing:
            break

        var char = _current_text[_current_pos]
        _revealed_text += char

        # Pausendauer basierend auf Zeichenkontext bestimmen
        var delay = _get_delay_for_char(char)

        # Mundform basierend auf aktuellem und umgebenden Zeichen aktualisieren
        var context = _get_char_context(_current_pos)
        var shape = animation_manager.get_mouth_shape(char, context)
        animation_manager.set_mouth_shape(shape)

        # Signale für UI-Updates senden
        char_revealed.emit(char, _current_pos)
        text_revealed.emit(_revealed_text)

        await get_tree().create_timer(delay).timeout
        _current_pos += 1

    # Zurück zur Ruhe-Mundform
    animation_manager.set_mouth_shape("idle")
    _is_revealing = false
    reveal_completed.emit()

func _get_delay_for_char(char: String) -> float:
    match char:
        ' ':
            return WORD_PAUSE_TIME
        '.', '!', '?', ':':
            return PUNCTUATION_PAUSE_TIME
        'n':
            return PARAGRAPH_PAUSE_TIME
        _:
            return CHAR_REVEAL_TIME

func _get_char_context(pos: int, window: int = 2) -> String:
    var start = max(0, pos - window)
    var end = min(_current_text.length(), pos + window + 1)
    return _current_text.substr(start, end - start)

Phase 3: Bildungsfunktionen

Wir haben ein dynamisches System zur Verfolgung von Lernzielen implementiert, das:

  1. Gesprächsthemen in Echtzeit überwacht
  2. Diskussionen mit vordefinierten Lernzielen verknüpft
  3. Kontextbezogene Prompts zur Gesprächsführung bereitstellt

Die Lernziele sind um theologische Kernkonzepte strukturiert:

const objectives = {
    "rechtfertigung": {
        "title": "Rechtfertigung durch den Glauben",
        "description": "Verständnis von Luthers Kernlehre der Erlösung allein durch den Glauben",
        "keywords": ["Glaube allein", "sola fide", "Rechtfertigung", "Erlösung", "Gnade"],
        "sources": ["unfreier_wille", "von_der_freiheit", "freiheit_eines_christenmenschen"],
        "prompts": [
            "Was meinten Sie mit 'Glaube allein'?",
            "Wie erlangt man die Erlösung?",
            "Warum waren Sie nicht einverstanden mit der katholischen Sicht der Werke?"
        ]
    },
    "schrift": {
        "title": "Autorität der Schrift",
        "description": "Erforschung von Luthers Betonung der biblischen Autorität",
        "keywords": ["Bibel", "Schrift", "sola scriptura", "Wort Gottes"],
        "sources": ["deutsche_bibel", "roemerbrief_vorlesung", "bibelkommentare"],
        "prompts": [
            "Warum haben Sie die Bibel ins Deutsche übersetzt?",
            "Welche Rolle sollte die Schrift in der Kirche spielen?",
            "Wie sollten Christen die Bibel auslegen?"
        ]
    }
}

Technische Herausforderungen

1. API-Schlüssel-Sicherheit

  • Implementierung mehrerer Fallback-Optionen für die API-Schlüssel-Speicherung
    • Umgebungsvariable (OPENAI_API_KEY)
    • Konfigurationsdatei (openai_config.cfg)
  • Hinzufügung von Schlüssel-Caching zur Reduzierung von Dateisystemzugriffen
  • Erstellung eines Template-Systems für sichere Versionskontrolle
  • Implementierung korrekter Fehlerbehandlung für fehlende Schlüssel
  • Hinzufügung von Debug-Logging für den Schlüssel-Ladeprozess

2. Antwort-Streaming

  • Entwicklung einer maßgeschneiderten Streaming-Lösung für Echtzeit-Textanzeige
  • Implementierung von Zeichen-für-Zeichen-Animation
  • Optimierung der Performance für flüssige Animation
  • Implementierung korrekter Pausen-Timing für Satzzeichen
  • Hinzufügung von abbrechbarer Textanzeige für bessere Benutzererfahrung
  • Erstellung eines Signal-Systems für UI-Synchronisation

3. Ressourcenverwaltung

  • Erstellung eines Relevanz-Bewertungsalgorithmus für Primärquellen
  • Implementierung von URL-Validierung und Sicherheitsprüfungen
  • Entwicklung eines Caching-Systems für häufig aufgerufene Ressourcen
  • Hinzufügung von Typsicherheit für String-Arrays und Dictionaries
  • Verbesserung der Schlüsselwort-Übereinstimmung mit Begriffsvariationen

4. Animationssystem

  • Behebung von Mundform-Ladeproblemen durch Wechsel von preload zu load
  • Behebung von Typ-Unstimmigkeiten im Animations-Manager
  • Optimierung von Sprite-Übergängen für flüssigere Animation
  • Hinzufügung von Debug-Logging für Animations-Status-Verfolgung
  • Verbesserung des Timing-Systems für natürlicheren Sprechrhythmus
  • Implementierung kontextbewusster Mundform-Auswahl

Zukünftige Entwicklung

1. Sprachsynthese-Integration

  • Untersuchung der Integration mit ElevenLabs für zeitgemäße Stimme
  • Planung von Lip-Sync-Verbesserungen für Sprachausgabe

2. Erweitertes Animationssystem

  • Implementierung von Gesten-Blending für flüssigere Übergänge
  • Hinzufügung einer Emotions-State-Machine für natürlichere Ausdrücke
  • Verbesserung der Mundform-Übergänge mit Interpolation
  • Hinzufügung vielfältigerer und dynamischerer Ausdrücke
  • Implementierung fortgeschrittener Timing-Algorithmen für Sprechmuster
  • Hinzufügung von Unterstützung für betonte Silben und Betonungsmuster

3. Bildungsfunktionen

  • Entwicklung eines Lehrplan-Builders für Lehrer
  • Hinzufügung von Unterstützung für benutzerdefinierte Primärquellen
  • Erstellung eines Fortschritts-Tracking-Dashboards
  • Implementierung eines ausgefeilteren Relevanz-Bewertungssystems
  • Hinzufügung von Unterstützung für mehrere Sprachen und Übersetzungen

4. Sicherheit und Konfiguration

  • Implementierung verschlüsselter Speicherung für sensible Daten
  • Hinzufügung benutzerspezifischer Konfigurationsprofile
  • Verbesserung der Fehlerbehandlung und -wiederherstellung
  • Erweiterung der Logging- und Debugging-Werkzeuge

Erkenntnisse

1. KI-Integration

  • Sorgfältige API-Schlüssel-Verwaltung ist entscheidend
  • Prompt-Engineering erfordert umfangreiche Tests
  • Konfigurationsverwaltung muss Sicherheit und Benutzerfreundlichkeit ausbalancieren

2. Animationssysteme

  • Echtzeit-Animation ist auf grundlegendem Niveau täuschend einfach zu implementieren
  • Performance war kein Engpass dank des leichtgewichtigen Systems
  • Typsicherheit in Godot 4 erfordert besondere Aufmerksamkeit
  • Timing ist entscheidend für natürlich wirkende Animation
  • Kontextbewusste Animationsentscheidungen führen zu realistischeren Ergebnissen

3. Bildungsdesign

  • Während wir eine Grundlage für Lernfunktionen geschaffen haben, hatten wir keine Zeit, sie gründlich mit echten Benutzern zu testen
  • Das dynamische Ressourcensystem erwies sich als wertvoller als ursprünglich erwartet und half Benutzern, natürlich tiefer in Themen einzutauchen
  • Die Balance zwischen historischer Genauigkeit und Zugänglichkeit erfordert sorgfältige Überlegung
  • Primärquellen benötigen klaren Kontext und Erklärung, um nützlich zu sein

Fazit

Dieses Projekt zeigt, wie Spielentwicklungstechnologie mit KI kombiniert werden kann, um fesselnde Bildungserlebnisse zu schaffen. Der Martin Luther Avatar zeigt, dass historische Bildung nicht statisch sein muss – sie kann interaktiv und personalisiert sein und dabei historisch akkurat bleiben.

Die Kombination von LLM-Technologie mit Echtzeit-Animation und Bildungsfunktionen schafft eine einzigartige Lernumgebung, die Benutzer einbindet und dabei historische Authentizität bewahrt. Während es noch Raum für Verbesserungen gibt, insbesondere bei der Animations-Verfeinerung und dem Testen der Bildungsfunktionen, ist das Fundament für die zukünftige Entwicklung solide.


Jonas Heinke

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Nach oben scrollen