Zum Inhalt springen

Linguistische Textanalyse: Eine hybride Pipeline mit Stanza, DeepSeek und Transformers + Spacy Vergleich

    Einleitung

    Stanza ist eine Open Source NLP Bibliothek der Stanford University, die auf modernen neuronalen Netzen basiert. Sie ermöglicht die umfassende linguistische Analyse von Texten in über 70 Sprachen. Ziel von Stanza ist es, ein vollständiges Pipeline System bereitzustellen, das alle gängigen Verarbeitungsschritte umfasst: Tokenisierung, Wortartenbestimmung (POS), Lemmatisierung, syntaktische Analyse (Abhängigkeiten und Konstituenten) sowie Named Entity Recognition (NER).

    Stanza eignet sich sowohl für Forschungszwecke als auch für produktive Anwendungen, etwa bei der Textklassifikation, Informationsextraktion oder dem Vorverarbeiten von Texten für Retrieval Augmented Generation (RAG). Die Modelle sind vortrainiert, können aber auch feinjustiert werden. Intern basiert Stanza auf dem PyTorch Framework.

    Eine Pipeline in der Sprachverarbeitung (NLP) bezeichnet eine festgelegte Abfolge von Verarbeitungsschritten, mit denen ein Text analysiert und strukturiert wird. Jeder Schritt nimmt den Output des vorherigen als Input und reichert ihn um weitere linguistische Informationen an. Ziel ist es, aus rohem Text schrittweise eine tiefere sprachliche Repräsentation zu erzeugen, die für weitere Anwendungen genutzt werden kann, z. B. Textklassifikation, Informationsextraktion oder Fragebeantwortung.

    Schritte einer typischen Stanza Pipeline

    Die Standardpipeline von Stanza besteht aus den folgenden Modulen (auch „Prozessoren“ genannt), die in dieser Reihenfolge arbeiten:

    1. Tokenisierung (tokenize)
    2. Mehrworterkennung (mwt)
    3. Wortartenbestimmung (pos)
    4. Lemmatisierung (lemma)
    5. Syntaktische Abhängigkeitsanalyse (depparse)
    6. Konstituentenanalyse (constituency) – Nur für Englisch
    7. Benannte Entitäten erkennen (ner)
    8. Stimmungsanalyse (sentiment) – Nur für Englisch

    1. Tokenisierung

    Die Tokenisierung ist der erste Schritt in der Verarbeitung eines Textes durch eine NLP Pipeline. Dabei wird der Eingabetext in einzelne Einheiten zerlegt. Diese Einheiten nennt man Tokens. Ein Token kann ein Wort, eine Zahl, ein Satzzeichen oder ein Symbol sein. Die Tokenisierung legt fest, wo ein Token anfängt und wo es endet. Das ist wichtig, weil alle weiteren Verarbeitungsschritte diese Einheiten verwenden.

    Stanza verwendet für die Tokenisierung sprachspezifische Modelle. Diese Modelle sind auf reale Sprachdaten trainiert und berücksichtigen Besonderheiten der jeweiligen Sprache. Im Englischen erkennt Stanza zum Beispiel, dass „U.S.A.“ ein einzelnes Token ist und nicht drei. Auch Abkürzungen, Zahlenformate und Emojis werden korrekt behandelt.

    Was Stanza in diesem Schritt liefert:

    • Eine Aufteilung des Textes in Sätze
    • Eine Liste von Tokens pro Satz
    • Zu jedem Token werden Start- und Endposition im Originaltext gespeichert

    2. Mehrworterkennung (MWT)

    Die Mehrworterkennung oder Multi Word Token Expansion ist ein optionaler Schritt in der Stanza Pipeline. Er wird nur für bestimmte Sprachen aktiviert, bei denen einzelne Tokens aus mehreren Wörtern bestehen können. Dazu gehören vor allem morphologisch komplexe Sprachen wie Arabisch oder Französisch. In Sprachen wie Deutsch oder Englisch ist dieser Schritt standardmäßig deaktiviert, da Wörter dort bereits einzeln geschrieben werden.

    Bei aktivierter MWT Komponente wird ein Token, das mehrere Wörter enthält, in seine Bestandteile zerlegt. Die ursprüngliche Tokenstruktur bleibt erhalten, aber es werden zusätzliche Wörter erzeugt. Diese Wörter sind die tatsächlichen Einheiten, mit denen die weiteren Module wie POS oder Lemmatisierung arbeiten.

    3. Wortartenbestimmung (Part-of-Speech Tagging / POS)

    Die Wortartenbestimmung ist ein zentraler Schritt in der Verarbeitung natürlicher Sprache. Dabei wird jedem Wort eine grammatische Kategorie zugewiesen. Beispiele für solche Kategorien sind Nomen, Verb, Adjektiv, Adverb, Artikel oder Präposition. Diese Informationen sind für fast alle weiteren Schritte erforderlich, da sie grammatische Strukturen sichtbar machen.

    Stanza verwendet neuronale Modelle, um diese Kategorisierung automatisch vorzunehmen. Für jedes Wort wird sowohl eine universelle Wortart (UPOS) als auch eine sprachspezifische, detaillierte Wortart (XPOS) vergeben. Zusätzlich werden morphologische Merkmale erfasst, wie Genus, Numerus, Kasus, Tempus oder Verbform.

    UPOS bedeutet „Universal Part of Speech“. Das ist eine Einteilung von Wörtern in grundlegende Wortarten wie Nomen, Verb, Adjektiv oder Artikel. Dieses System ist für alle Sprachen gleich. Ein Verb in Deutsch und ein Verb in Englisch erhalten beide das Merkmal VERB.

    XPOS ist die Wortart, wie sie in einer bestimmten Sprache üblich ist. In Stanza wird sie für jede Sprache anders festgelegt. Für Englisch bedeutet das:

    • NN steht für ein Nomen im Singular
    • VBZ steht für ein Verb mit dritter Person Singular im Präsens
    • JJ steht für ein Adjektiv

    XPOS ist also eine genauere Beschreibung der Wortart, wie sie in der jeweiligen Sprache gebraucht wird.

    4. Lemmatisierung

    Die Lemmatisierung ist der Prozess, bei dem ein Wort auf seine Grundform zurückgeführt wird. Diese Grundform nennt man Lemma. Ziel ist es, verschiedene grammatische Formen eines Wortes auf eine einheitliche Form zu bringen. Das ist wichtig, um Wörter unabhängig von Zeitform, Person oder Numerus vergleichen oder verarbeiten zu können.

    Beispiele:

    • „went“ wird zu „go“
    • „dogs“ wird zu „dog“

    Stanza verwendet für die Lemmatisierung ein Modell, das den Kontext des Wortes berücksichtigt. Dadurch kann es auch Wörter mit mehreren Bedeutungen korrekt behandeln.

    5. Syntaktische Abhängigkeitsanalyse

    Die syntaktische Abhängigkeitsanalyse untersucht die grammatische Struktur eines Satzes. Dabei wird für jedes Wort bestimmt, zu welchem anderen Wort es gehört und welche Rolle es dabei spielt. Das Ergebnis ist ein gerichteter Baum, in dem jedes Wort genau einem anderen untergeordnet ist. Die Verbindungen nennt man Kanten, und sie tragen grammatische Bezeichnungen wie Subjekt, Objekt oder Modifikator.

    Stanza verwendet dafür ein neuronales Modell, das sogenannte Universal Dependencies erzeugt. Diese Struktur zeigt, wie die Wörter im Satz miteinander verbunden sind. Jedes Wort hat dabei einen sogenannten Head, also das übergeordnete Wort, und eine Beziehung zu diesem Head.

    Wichtige Abhängigkeitsbeziehungen

    • nsubj: nominales Subjekt
    • obj: direktes Objekt
    • obl: adverbiale Ergänzung
    • root: Wurzel des Satzes, meist das Hauptverb
    • det: Artikel
    • amod: Adjektiv als Modifikator eines Nomens
    • case: Präposition oder Kasusanzeiger
    • punct: Satzzeichen

    6. Konstituentenanalyse – Nur für Englisch

    Die Konstituentenanalyse untersucht, aus welchen Satzteilen ein Satz besteht und wie diese Satzteile miteinander verschachtelt sind. Dabei wird erkannt, welche Wörter zusammen eine Einheit bilden, zum Beispiel ein Subjekt oder ein Objekt. Solche Einheiten nennt man Phrasen, zum Beispiel Nominalphrase oder Verbalphrase.

    Die Analyse zeigt die Struktur des Satzes als Baum. Jeder Satz wird dabei in immer größere Gruppen aufgeteilt, zum Beispiel: zuerst einzelne Wörter, dann Phrasen, dann der ganze Satz.

    Stanza verwendet dafür ein englisches Regelwerk namens Penn Treebank. Das funktioniert gut für Texte auf Englisch. Für deutsche Texte ist die Konstituentenanalyse in Stanza zurzeit nicht verfügbar. Die Funktion eignet sich vor allem für englische Sätze. Für Deutsch muss man zusätzlich die Berkeley Neural Parser (Benepar) Bibliothek verwenden.

    7. Erkennung benannter Entitäten (Named Entity Recognition)

    Die Erkennung benannter Entitäten ist ein Schritt in der NLP Verarbeitung, bei dem bestimmte Wörter oder Wortgruppen als bedeutende Objekte erkannt werden. Diese Objekte nennt man Entitäten. Sie beziehen sich zum Beispiel auf Personen, Orte, Organisationen, Zeitangaben oder Geldbeträge.

    Stanza erkennt Entitäten automatisch auf Basis eines trainierten neuronalen Modells. Jede erkannte Entität wird einem festen Typ zugeordnet. Das Modell bezieht den Kontext mit ein und kann auch mehrteilige Entitäten wie „New York City“ oder „United Nations“ korrekt erfassen.

    Entitätstypen in Stanza

    • PERSON: Vorname oder Nachname einer Person
    • GPE: Geopolitische Einheit wie Land oder Stadt
    • ORG: Organisation wie Firma oder Behörde
    • DATE: Datum
    • TIME: Uhrzeit
    • MONEY: Geldbetrag
    • LOC: Geografische Angabe ohne politische Funktion
    • PRODUCT: Gegenstand oder Produkt

    8. Stimmungsanalyse (Sentiment Analysis) – Nur für Englisch

    Die Stimmungsanalyse bewertet, ob der Inhalt eines Satzes eher positiv, neutral oder negativ ist. Das Modell untersucht dabei nicht einzelne Wörter, sondern den gesamten Satz in seinem Zusammenhang. So kann es zum Beispiel erkennen, dass ironische oder abschwächende Formulierungen eine eigentlich positive Aussage neutral oder sogar negativ erscheinen lassen.

    Stanza bietet die Stimmungsanalyse zurzeit nur für englische Texte an. Die Grundlage ist ein neuronales Modell, das auf dem SSTplus Korpus trainiert wurde. Das Modell ist in der Lage, jede Satzstruktur zu analysieren und einer von drei Kategorien zuzuordnen.

    Klassifikationsstufen

    • 0: negativ
    • 1: neutral
    • 2: positiv

    Code Beispiel

    import stanza

    stanza.download('en') # download English model
    nlp = stanza.Pipeline('en') # initialize English neural pipeline
    doc = nlp("Barack Obama was born in Hawaii.") # run annotation over a sentence

    print(doc)

    Ausgabe

    angraph/venv/python.exe c:/sources/agents/langraph/stanza-test.py
    Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 27.5MB/s]
    2025-06-07 12:39:46 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json
    2025-06-07 12:39:46 INFO: Downloading default packages for language: en (English) ...
    Downloading https://huggingface.co/stanfordnlp/stanza-en/resolve/v1.10.0/models/default.zip: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 526M/526M [00:16<00:00, 31.5MB/s]
    2025-06-07 12:40:04 INFO: Downloaded file to C:\Users\info\stanza_resources\en\default.zip
    2025-06-07 12:40:06 INFO: Finished downloading models and saved to C:\Users\info\stanza_resources
    2025-06-07 12:40:06 INFO: Checking for updates to resources.json in case models have been updated. Note: this behavior can be turned off with download_method=None or download_method=DownloadMethod.REUSE_RESOURCES
    Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 27.3MB/s]
    2025-06-07 12:40:06 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json
    2025-06-07 12:40:07 INFO: Loading these models for language: en (English):
    ============================================
    | Processor | Package |
    --------------------------------------------
    | tokenize | combined |
    | mwt | combined |
    | pos | combined_charlm |
    | lemma | combined_nocharlm |
    | constituency | ptb3-revised_charlm |
    | depparse | combined_charlm |
    | sentiment | sstplus_charlm |
    | ner | ontonotes-ww-multi_charlm |
    ============================================

    2025-06-07 12:40:07 INFO: Using device: cpu
    2025-06-07 12:40:07 INFO: Loading: tokenize
    2025-06-07 12:40:08 INFO: Loading: mwt
    2025-06-07 12:40:08 INFO: Loading: pos
    2025-06-07 12:40:09 INFO: Loading: lemma
    2025-06-07 12:40:10 INFO: Loading: constituency
    2025-06-07 12:40:10 INFO: Loading: depparse
    2025-06-07 12:40:10 INFO: Loading: sentiment
    2025-06-07 12:40:10 INFO: Loading: ner
    2025-06-07 12:40:12 INFO: Done loading processors!
    [
    [
    {
    "id": 1,
    "text": "Barack",
    "lemma": "Barack",
    "upos": "PROPN",
    "xpos": "NNP",
    "feats": "Number=Sing",
    "head": 4,
    "deprel": "nsubj:pass",
    "start_char": 0,
    "end_char": 6,
    "ner": "B-PERSON",
    "multi_ner": [
    "B-PERSON"
    ]
    },
    {
    "id": 2,
    "text": "Obama",
    "lemma": "Obama",
    "upos": "PROPN",
    "xpos": "NNP",
    "feats": "Number=Sing",
    "head": 1,
    "deprel": "flat",
    "start_char": 7,
    "end_char": 12,
    "ner": "E-PERSON",
    "multi_ner": [
    "E-PERSON"
    ]
    },
    {
    "id": 3,
    "text": "was",
    "lemma": "be",
    "upos": "AUX",
    "xpos": "VBD",
    "feats": "Mood=Ind|Number=Sing|Person=3|Tense=Past|VerbForm=Fin",
    "head": 4,
    "deprel": "aux:pass",
    "start_char": 13,
    "end_char": 16,
    "ner": "O",
    "multi_ner": [
    "O"
    ]
    },
    {
    "id": 4,
    "text": "born",
    "lemma": "bear",
    "upos": "VERB",
    "xpos": "VBN",
    "feats": "Tense=Past|VerbForm=Part|Voice=Pass",
    "head": 0,
    "deprel": "root",
    "start_char": 17,
    "end_char": 21,
    "ner": "O",
    "multi_ner": [
    "O"
    ]
    },
    {
    "id": 5,
    "text": "in",
    "lemma": "in",
    "upos": "ADP",
    "xpos": "IN",
    "head": 6,
    "deprel": "case",
    "start_char": 22,
    "end_char": 24,
    "ner": "O",
    "multi_ner": [
    "O"
    ]
    },
    {
    "id": 6,
    "text": "Hawaii",
    "lemma": "Hawaii",
    "upos": "PROPN",
    "xpos": "NNP",
    "feats": "Number=Sing",
    "head": 4,
    "deprel": "obl",
    "start_char": 25,
    "end_char": 31,
    "ner": "S-GPE",
    "multi_ner": [
    "S-GPE"
    ],
    "misc": "SpaceAfter=No"
    },
    {
    "id": 7,
    "text": ".",
    "lemma": ".",
    "upos": "PUNCT",
    "xpos": ".",
    "head": 4,
    "deprel": "punct",
    "start_char": 31,
    "end_char": 32,
    "ner": "O",
    "multi_ner": [
    "O"
    ],
    "misc": "SpaceAfter=No"
    }
    ]
    ]

    PDF-Datei Analyse

    import stanza
    import fitz # PyMuPDF
    from pathlib import Path

    # Schritt 1: PDF laden und Text extrahieren
    file_path = Path("test_oc.pdf")
    with fitz.open(file_path) as doc:
    text = "\n".join(page.get_text() for page in doc)

    # Schritt 2: Stanza-Modell für Deutsch laden
    stanza.download('de')
    nlp = stanza.Pipeline('de')

    # Schritt 3: Text analysieren
    doc = nlp(text)

    # Schritt 4: Ergebnis anzeigen (zum Beispiel Satzweise Ausgabe)
    for i, sentence in enumerate(doc.sentences, start=1):
    print(f"Satz {i}:")
    for word in sentence.words:
    print(f" Wort: {word.text}, Lemma: {word.lemma}, POS: {word.upos}")
    (c:\sources\agents\langraph\venv) PS C:\sources\agents\langraph> & c:/sources/agents/langraph/venv/python.exe c:/sources/agents/langraph/stanza-test.py
    Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 20.3MB/s]
    2025-06-07 15:01:21 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json
    2025-06-07 15:01:21 INFO: Downloading default packages for language: de (German) ...
    2025-06-07 15:01:22 INFO: File exists: C:\Users\info\stanza_resources\de\default.zip
    2025-06-07 15:01:25 INFO: Finished downloading models and saved to C:\Users\info\stanza_resources
    2025-06-07 15:01:25 INFO: Checking for updates to resources.json in case models have been updated. Note: this behavior can be turned off with download_method=None or download_method=DownloadMethod.REUSE_RESOURCES
    Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 20.5MB/s]
    2025-06-07 15:01:26 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json
    2025-06-07 15:01:27 INFO: Loading these models for language: de (German):
    ====================================
    | Processor | Package |
    ------------------------------------
    | tokenize | combined |
    | mwt | combined |
    | pos | combined_charlm |
    | lemma | combined_nocharlm |
    | constituency | spmrl_charlm |
    | depparse | combined_charlm |
    | sentiment | sb10k_charlm |
    | ner | germeval2014 |
    ====================================

    2025-06-07 15:01:27 INFO: Using device: cpu
    2025-06-07 15:01:27 INFO: Loading: tokenize
    2025-06-07 15:01:28 INFO: Loading: mwt
    2025-06-07 15:01:28 INFO: Loading: pos
    2025-06-07 15:01:29 INFO: Loading: lemma
    2025-06-07 15:01:35 INFO: Loading: constituency
    2025-06-07 15:01:35 INFO: Loading: depparse
    2025-06-07 15:01:35 INFO: Loading: sentiment
    2025-06-07 15:01:36 INFO: Loading: ner
    2025-06-07 15:01:38 INFO: Done loading processors!
    Satz 1:
    Wort: OPITZ, Lemma: OPITZ, POS: PROPN
    Wort: CONSULTING, Lemma: CONSULTING, POS: PROPN
    Wort: Deutschland, Lemma: Deutschland, POS: PROPN
    Wort: GmbH, Lemma: GmbH, POS: PROPN
    Wort: 2016, Lemma: 2016, POS: NUM
    Wort: Seite, Lemma: Seite, POS: NOUN
    Wort: 1, Lemma: 1, POS: NUM
    Wort: von, Lemma: von, POS: ADP
    Wort: 2, Lemma: 2, POS: NUM
    Satz 2:
    Wort: Presseinfo, Lemma: Presseinfo, POS: NOUN
    Satz 3:
    ....
    Ausgabe (Ausschnitt)

    Das Problem bei der obigen Ausgabe ist, dass der Text aus dem PDF ohne ausreichende Vorverarbeitung direkt und als Ganzes an die Stanza-Pipeline übergeben wurde. Dadurch werden nicht nur die eigentlichen inhaltlichen Sätze, sondern auch sämtliche Layout- und Formatierungsreste des Dokuments, wie beispielsweise Kopfzeilen, Seitennummern, einzelne Überschriften, Fußnoten oder Listenpunkte, von Stanza als eigenständige Sätze erkannt und verarbeitet. Das führt dazu, dass in der Ausgabe zahlreiche inhaltlich zusammenhanglose oder sogar bedeutungslose Fragmente als einzelne Sätze erscheinen. Auch inhaltlich zusammenhängende Sätze werden durch harte Zeilenumbrüche, wie sie bei PDFs häufig auftreten, von der Pipeline unterbrochen und als getrennte Einheiten analysiert. Die Folge ist, dass die linguistische Auswertung nicht die eigentliche Satzstruktur widerspiegelt, sondern vom Layout und den technischen Besonderheiten des PDFs dominiert wird. Für eine sinnvolle und präzise Analyse müssen solche Artefakte bereits vor der Übergabe an die Stanza-Pipeline bereinigt, Wörter an Zeilenumbrüchen korrekt zusammengesetzt und der Text in vollständige, grammatisch vollständige Einzelsätze segmentiert werden. Nur so lässt sich mit Stanza eine semantisch aussagekräftige und für nachgelagerte Verarbeitungsschritte brauchbare linguistische Analyse erzielen.

    Extraktion und linguistische Vorverarbeitung via DeepSeek & Stanza

    stanza
    torch
    pdfplumber
    requests
    requirements.txt
    # Vorbereitung: Bibliotheken und Hilfsfunktionen (außerhalb der Stanza-Pipeline)
    import pdfplumber
    import requests
    from pathlib import Path
    import stanza

    # Vorbereitung: Datei- und Modellpfade
    pdf_path = Path("test_oc.pdf")

    # Vorbereitung: PDF-Text extrahieren und bereinigen
    def extract_clean_text(path: Path, top_margin: float = 0.15, bottom_margin: float = 0.15) -> str:
    """
    Diese Funktion ist Teil der Vorbereitung und **kein Schritt der Stanza-Pipeline**.
    - PDF-Datei öffnen und jede Seite durchgehen.
    - Obere und untere Seitenränder abschneiden.
    - Text extrahieren, Zeilen normalisieren.
    - Worttrennungen am Zeilenende auflösen.
    - Textblöcke zu einem Fließtext zusammenfassen.
    """
    paragraphs = []
    with pdfplumber.open(path) as pdf:
    for page in pdf.pages:
    height = page.height
    top = height * top_margin
    bottom = height * (1 - bottom_margin)
    cropped = page.within_bbox((0, top, page.width, bottom))
    raw = cropped.extract_text()
    if not raw:
    continue
    current_line = ""
    for line in raw.splitlines():
    line = line.strip()
    if not line:
    continue
    if line.isupper():
    continue
    if line.endswith("-") and len(line) > 1 and line[-2].isalpha():
    current_line = line[:-1]
    else:
    current_line = " " line
    if line.endswith((".", "!", "?")):
    paragraphs.append(current_line.strip())
    current_line = ""
    if current_line:
    paragraphs.append(current_line.strip())
    return " ".join(paragraphs)

    # Zwischenschritt (kein Bestandteil der Stanza-Pipeline): Satzsegmentierung via DeepSeek
    def deepseek_sentence_split(text: str) -> list:
    """
    Diese Funktion ist KEIN Teil der Stanza-Pipeline.
    Sie dient der Aufteilung des Fließtexts in vollständige Sätze (für die nachfolgende Verarbeitung).
    """
    url = "http://localhost:11434/api/generate"
    prompt = (
    "Teile den folgenden deutschen Text in grammatisch vollständige, möglichst kurze Einzelsätze. "
    "Jede Zeile enthält genau einen abgeschlossenen Satz. "
    "Keine Nummerierung, keine Einleitung, keine Leerzeilen.\n\n" text.strip()
    )
    data = {
    "model": "deepseek-Coder-V2:latest",
    "prompt": prompt,
    "stream": False
    }
    response = requests.post(url, json=data, timeout=120)
    response.raise_for_status()
    raw = response.json()["response"]
    sentences = [line.strip() for line in raw.splitlines() if line.strip()]
    return sentences

    # Vorbereitung: Stanza-Modelle laden und Pipeline konfigurieren
    # Aktivierte Prozessoren für Deutsch: tokenize (1), mwt (2), pos (3), lemma (4), depparse (5), ner (7)
    # Schritt 6 (constituency) und Schritt 8 (sentiment) werden für Deutsch ausgelassen.
    stanza.download('de', processors='tokenize,mwt,pos,lemma,depparse,ner')
    nlp = stanza.Pipeline(
    'de',
    processors='tokenize,mwt,pos,lemma,depparse,ner',
    tokenize_no_ssplit=True
    )

    # Vorbereitung: PDF bereinigen und in Fließtext umwandeln
    text = extract_clean_text(pdf_path)

    # Zwischenschritt: Satzsegmentierung
    sentences = deepseek_sentence_split(text)

    # STANZA-PIPELINE SCHRITTE:
    for i, sentence in enumerate(sentences, start=1):
    print(f"\nSatz {i}:")
    print(f" [Original] {sentence}")

    # Pipeline-Schritt 1: Tokenisierung (tokenize)
    # Pipeline-Schritt 2: Mehrworterkennung (mwt)
    # Pipeline-Schritt 3: Wortartenbestimmung (pos)
    # Pipeline-Schritt 4: Lemmatisierung (lemma)
    # Pipeline-Schritt 5: Syntaktische Abhängigkeitsanalyse (depparse)
    # Pipeline-Schritt 6: Konstituentenanalyse enfällt, da nicht für Deutsch verfügbar
    # Pipeline-Schritt 7: Benannte Entitäten erkennen (ner)
    # Pipeline-Schritt 8: Stimmungsanalyse enfällt, da nicht für Deutsch verfügbar
    doc = nlp(sentence)

    for s in doc.sentences:
    print(" Pipeline-Ausgabe (Token-Ebene):")
    for word in s.words:
    print(
    f" 1) Tokenisierung: {word.text:<20}"
    f"| 2) MWT: {word.misc if word.misc else '-':<18}"
    f"| 3) POS: {word.upos:<8}"
    f"| 4) Lemma: {word.lemma:<20}"
    f"| 5a) Head: {word.head if word.head else '-':<3}"
    f"| 5b) DepRel: {word.deprel if word.deprel else '-':<15}"
    )

    # Benannte Entitäten (NER) – Schritt 7
    if s.ents:
    print(" Pipeline-Ausgabe (Entitäten, Schritt 7):")
    for ent in s.ents:
    print(f" 7) Entity: {ent.text} ({ent.type})")
    else:
    print(" Pipeline-Ausgabe (Entitäten, Schritt 7): Keine erkannt.")

    # Schritt 6 (Konstituentenanalyse) und Schritt 8 (Stimmungsanalyse) sind in Stanza für Deutsch nicht verfügbar
    # und werden bewusst nicht ausgegeben.
    app.py
    (c:\sources\agents\langraph\venv) PS C:\sources\agents\langraph> & c:/sources/agents/langraph/venv/python.exe c:/sources/agents/langraph/stanza-test.py
    Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 21.4MB/s]
    2025-06-07 20:35:12 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json
    2025-06-07 20:35:12 INFO: Downloading these customized packages for language: de (German)...
    =======================================
    | Processor | Package |
    ---------------------------------------
    | tokenize | combined |
    | mwt | combined |
    | pos | combined_charlm |
    | lemma | combined_nocharlm |
    | depparse | combined_charlm |
    | ner | germeval2014 |
    | forward_charlm | newswiki |
    | pretrain | conll17 |
    | pretrain | fasttextwiki |
    | backward_charlm | newswiki |
    =======================================

    2025-06-07 20:35:12 INFO: File exists: C:\Users\info\stanza_resources\de\tokenize\combined.pt
    2025-06-07 20:35:12 INFO: File exists: C:\Users\info\stanza_resources\de\mwt\combined.pt
    2025-06-07 20:35:12 INFO: File exists: C:\Users\info\stanza_resources\de\pos\combined_charlm.pt
    2025-06-07 20:35:12 INFO: File exists: C:\Users\info\stanza_resources\de\lemma\combined_nocharlm.pt
    2025-06-07 20:35:12 INFO: File exists: C:\Users\info\stanza_resources\de\depparse\combined_charlm.pt
    2025-06-07 20:35:13 INFO: File exists: C:\Users\info\stanza_resources\de\ner\germeval2014.pt
    2025-06-07 20:35:13 INFO: File exists: C:\Users\info\stanza_resources\de\forward_charlm\newswiki.pt
    2025-06-07 20:35:13 INFO: File exists: C:\Users\info\stanza_resources\de\pretrain\conll17.pt
    2025-06-07 20:35:13 INFO: File exists: C:\Users\info\stanza_resources\de\pretrain\fasttextwiki.pt
    2025-06-07 20:35:13 INFO: File exists: C:\Users\info\stanza_resources\de\backward_charlm\newswiki.pt
    2025-06-07 20:35:13 INFO: Finished downloading models and saved to C:\Users\info\stanza_resources
    2025-06-07 20:35:13 INFO: Checking for updates to resources.json in case models have been updated. Note: this behavior can be turned off with download_method=None or download_method=DownloadMethod.REUSE_RESOURCES
    Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 24.1MB/s]
    2025-06-07 20:35:13 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json
    2025-06-07 20:35:14 INFO: Loading these models for language: de (German):
    =================================
    | Processor | Package |
    ---------------------------------
    | tokenize | combined |
    | mwt | combined |
    | pos | combined_charlm |
    | lemma | combined_nocharlm |
    | depparse | combined_charlm |
    | ner | germeval2014 |
    =================================

    2025-06-07 20:35:14 INFO: Using device: cpu
    2025-06-07 20:35:14 INFO: Loading: tokenize
    2025-06-07 20:35:15 INFO: Loading: mwt
    2025-06-07 20:35:15 INFO: Loading: pos
    2025-06-07 20:35:16 INFO: Loading: lemma
    2025-06-07 20:35:22 INFO: Loading: depparse
    2025-06-07 20:35:22 INFO: Loading: ner
    2025-06-07 20:35:25 INFO: Done loading processors!

    Satz 1:
    [Original] 1. MIT STACKIT holt sich OPITZ CONSULTING einen souveränen deutschen Cloud Provider mit ins Boot.
    Pipeline-Ausgabe (Token-Ebene):
    1) Tokenisierung: 1 | 2) MWT: - | 3) POS: NUM | 4) Lemma: 1 | 5a) Head: 3 | 5b) DepRel: nummod
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 1 | 5b) DepRel: punct
    1) Tokenisierung: MIT | 2) MWT: - | 3) POS: PROPN | 4) Lemma: MIT | 5a) Head: 5 | 5b) DepRel: nsubj
    1) Tokenisierung: STACKIT | 2) MWT: - | 3) POS: PROPN | 4) Lemma: STACKIT | 5a) Head: 3 | 5b) DepRel: flat
    1) Tokenisierung: holt | 2) MWT: - | 3) POS: VERB | 4) Lemma: holen | 5a) Head: - | 5b) DepRel: root
    1) Tokenisierung: sich | 2) MWT: - | 3) POS: PRON | 4) Lemma: sich | 5a) Head: 5 | 5b) DepRel: obj
    1) Tokenisierung: OPITZ | 2) MWT: - | 3) POS: PROPN | 4) Lemma: OPITZ | 5a) Head: 5 | 5b) DepRel: nsubj
    1) Tokenisierung: CONSULTING | 2) MWT: - | 3) POS: PROPN | 4) Lemma: CONSULTING | 5a) Head: 7 | 5b) DepRel: flat
    1) Tokenisierung: einen | 2) MWT: - | 3) POS: DET | 4) Lemma: ein | 5a) Head: 12 | 5b) DepRel: det
    1) Tokenisierung: souveränen | 2) MWT: - | 3) POS: ADJ | 4) Lemma: souverän | 5a) Head: 12 | 5b) DepRel: amod
    1) Tokenisierung: deutschen | 2) MWT: - | 3) POS: ADJ | 4) Lemma: deutsch | 5a) Head: 12 | 5b) DepRel: amod
    1) Tokenisierung: Cloud | 2) MWT: - | 3) POS: PROPN | 4) Lemma: Cloud | 5a) Head: 5 | 5b) DepRel: obj
    1) Tokenisierung: Provider | 2) MWT: - | 3) POS: PROPN | 4) Lemma: Provider | 5a) Head: 12 | 5b) DepRel: flat
    1) Tokenisierung: mit | 2) MWT: - | 3) POS: ADP | 4) Lemma: mit | 5a) Head: 5 | 5b) DepRel: compound:prt
    1) Tokenisierung: in | 2) MWT: - | 3) POS: ADP | 4) Lemma: in | 5a) Head: 17 | 5b) DepRel: case
    1) Tokenisierung: das | 2) MWT: - | 3) POS: DET | 4) Lemma: der | 5a) Head: 17 | 5b) DepRel: det
    1) Tokenisierung: Boot | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Boot | 5a) Head: 5 | 5b) DepRel: obl
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 5 | 5b) DepRel: punct
    Pipeline-Ausgabe (Entitäten, Schritt 7):
    7) Entity: OPITZ CONSULTING (ORG)
    7) Entity: deutschen (LOC)

    Satz 2:
    [Original] 2. Seit dem 1. März 2024 bündeln OPITZ CONSULTING und STACKIT ihre Kräfte, um eine Allianz im Bereich Cloud-Infrastruktur und Cloud-Dienstleistungen zu schmieden.
    Pipeline-Ausgabe (Token-Ebene):
    1) Tokenisierung: 2 | 2) MWT: - | 3) POS: NUM | 4) Lemma: 2 | 5a) Head: 9 | 5b) DepRel: dep
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 1 | 5b) DepRel: punct
    1) Tokenisierung: Seit | 2) MWT: - | 3) POS: ADP | 4) Lemma: seit | 5a) Head: 7 | 5b) DepRel: case
    1) Tokenisierung: dem | 2) MWT: - | 3) POS: DET | 4) Lemma: der | 5a) Head: 7 | 5b) DepRel: det
    1) Tokenisierung: 1 | 2) MWT: - | 3) POS: NUM | 4) Lemma: 1 | 5a) Head: 7 | 5b) DepRel: compound
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 5 | 5b) DepRel: punct
    1) Tokenisierung: März | 2) MWT: - | 3) POS: PROPN | 4) Lemma: März | 5a) Head: 9 | 5b) DepRel: obl
    1) Tokenisierung: 2024 | 2) MWT: - | 3) POS: NUM | 4) Lemma: 2024 | 5a) Head: 7 | 5b) DepRel: nmod
    1) Tokenisierung: bündeln | 2) MWT: - | 3) POS: VERB | 4) Lemma: bündeln | 5a) Head: - | 5b) DepRel: root
    1) Tokenisierung: OPITZ | 2) MWT: - | 3) POS: PROPN | 4) Lemma: OPITZ | 5a) Head: 9 | 5b) DepRel: nsubj
    1) Tokenisierung: CONSULTING | 2) MWT: - | 3) POS: PROPN | 4) Lemma: CONSULTING | 5a) Head: 10 | 5b) DepRel: flat
    1) Tokenisierung: und | 2) MWT: - | 3) POS: CCONJ | 4) Lemma: und | 5a) Head: 13 | 5b) DepRel: cc
    1) Tokenisierung: STACKIT | 2) MWT: - | 3) POS: PROPN | 4) Lemma: STACKIT | 5a) Head: 10 | 5b) DepRel: conj
    1) Tokenisierung: ihre | 2) MWT: - | 3) POS: DET | 4) Lemma: ihr | 5a) Head: 15 | 5b) DepRel: det:poss
    1) Tokenisierung: Kräfte | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Kraft | 5a) Head: 9 | 5b) DepRel: obj
    1) Tokenisierung: , | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: , | 5a) Head: 31 | 5b) DepRel: punct
    1) Tokenisierung: um | 2) MWT: - | 3) POS: ADP | 4) Lemma: um | 5a) Head: 31 | 5b) DepRel: mark
    1) Tokenisierung: eine | 2) MWT: - | 3) POS: DET | 4) Lemma: ein | 5a) Head: 19 | 5b) DepRel: det
    1) Tokenisierung: Allianz | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Allianz | 5a) Head: 31 | 5b) DepRel: obj
    1) Tokenisierung: in | 2) MWT: - | 3) POS: ADP | 4) Lemma: in | 5a) Head: 22 | 5b) DepRel: case
    1) Tokenisierung: dem | 2) MWT: - | 3) POS: DET | 4) Lemma: der | 5a) Head: 22 | 5b) DepRel: det
    1) Tokenisierung: Bereich | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Bereich | 5a) Head: 19 | 5b) DepRel: nmod
    1) Tokenisierung: Cloud | 2) MWT: - | 3) POS: PROPN | 4) Lemma: Cloud | 5a) Head: 22 | 5b) DepRel: appos
    1) Tokenisierung: - | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: - | 5a) Head: 25 | 5b) DepRel: punct
    1) Tokenisierung: Infrastruktur | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Infrastruktur | 5a) Head: 23 | 5b) DepRel: flat
    1) Tokenisierung: und | 2) MWT: - | 3) POS: CCONJ | 4) Lemma: und | 5a) Head: 27 | 5b) DepRel: cc
    1) Tokenisierung: Cloud | 2) MWT: - | 3) POS: PROPN | 4) Lemma: Cloud | 5a) Head: 23 | 5b) DepRel: conj
    1) Tokenisierung: - | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: - | 5a) Head: 29 | 5b) DepRel: punct
    1) Tokenisierung: Dienstleistungen | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Dienstleistung | 5a) Head: 27 | 5b) DepRel: flat
    1) Tokenisierung: zu | 2) MWT: - | 3) POS: PART | 4) Lemma: zu | 5a) Head: 31 | 5b) DepRel: mark
    1) Tokenisierung: schmieden | 2) MWT: - | 3) POS: VERB | 4) Lemma: schmieden | 5a) Head: 9 | 5b) DepRel: advcl
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 9 | 5b) DepRel: punct
    Pipeline-Ausgabe (Entitäten, Schritt 7):
    7) Entity: OPITZ CONSULTING (ORG)
    7) Entity: STACKIT (ORG)

    Satz 3:
    [Original] 3. Diese Zusammenarbeit eröffnet den Kunden von OPITZ CONSULTING den Zugang zur STACKIT Cloud – einer deutschen, datensouveränen Cloud-Lösung.
    Pipeline-Ausgabe (Token-Ebene):
    1) Tokenisierung: 3 | 2) MWT: - | 3) POS: NUM | 4) Lemma: 3 | 5a) Head: 4 | 5b) DepRel: nummod
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 1 | 5b) DepRel: punct
    1) Tokenisierung: Diese | 2) MWT: - | 3) POS: DET | 4) Lemma: dieser | 5a) Head: 4 | 5b) DepRel: det
    1) Tokenisierung: Zusammenarbeit | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Zusammenarbeit | 5a) Head: 5 | 5b) DepRel: nsubj
    1) Tokenisierung: eröffnet | 2) MWT: - | 3) POS: VERB | 4) Lemma: eröffnen | 5a) Head: - | 5b) DepRel: root
    1) Tokenisierung: den | 2) MWT: - | 3) POS: DET | 4) Lemma: der | 5a) Head: 7 | 5b) DepRel: det
    1) Tokenisierung: Kunden | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Kunde | 5a) Head: 5 | 5b) DepRel: obl:arg
    1) Tokenisierung: von | 2) MWT: - | 3) POS: ADP | 4) Lemma: von | 5a) Head: 9 | 5b) DepRel: case
    1) Tokenisierung: OPITZ | 2) MWT: - | 3) POS: PROPN | 4) Lemma: OPITZ | 5a) Head: 7 | 5b) DepRel: nmod
    1) Tokenisierung: CONSULTING | 2) MWT: - | 3) POS: PROPN | 4) Lemma: CONSULTING | 5a) Head: 9 | 5b) DepRel: flat
    1) Tokenisierung: den | 2) MWT: - | 3) POS: DET | 4) Lemma: der | 5a) Head: 12 | 5b) DepRel: det
    1) Tokenisierung: Zugang | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Zugang | 5a) Head: 5 | 5b) DepRel: obj
    1) Tokenisierung: zu | 2) MWT: - | 3) POS: ADP | 4) Lemma: zu | 5a) Head: 15 | 5b) DepRel: case
    1) Tokenisierung: der | 2) MWT: - | 3) POS: DET | 4) Lemma: der | 5a) Head: 15 | 5b) DepRel: det
    1) Tokenisierung: STACKIT | 2) MWT: - | 3) POS: PROPN | 4) Lemma: STACKIT | 5a) Head: 12 | 5b) DepRel: nmod
    1) Tokenisierung: Cloud | 2) MWT: - | 3) POS: PROPN | 4) Lemma: Cloud | 5a) Head: 15 | 5b) DepRel: flat
    1) Tokenisierung: – | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: – | 5a) Head: 24 | 5b) DepRel: punct
    1) Tokenisierung: einer | 2) MWT: - | 3) POS: DET | 4) Lemma: ein | 5a) Head: 24 | 5b) DepRel: det
    1) Tokenisierung: deutschen | 2) MWT: - | 3) POS: ADJ | 4) Lemma: deutsch | 5a) Head: 24 | 5b) DepRel: amod
    1) Tokenisierung: , | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: , | 5a) Head: 19 | 5b) DepRel: punct
    1) Tokenisierung: datensouveränen | 2) MWT: - | 3) POS: ADJ | 4) Lemma: datensouveränen | 5a) Head: 19 | 5b) DepRel: conj
    1) Tokenisierung: Cloud | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Cloud | 5a) Head: 24 | 5b) DepRel: compound
    1) Tokenisierung: - | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: - | 5a) Head: 22 | 5b) DepRel: punct
    1) Tokenisierung: Lösung | 2) MWT: - | 3) POS: NOUN | 4) Lemma: Lösung | 5a) Head: 15 | 5b) DepRel: appos
    1) Tokenisierung: . | 2) MWT: - | 3) POS: PUNCT | 4) Lemma: . | 5a) Head: 5 | 5b) DepRel: punct
    Pipeline-Ausgabe (Entitäten, Schritt 7):
    7) Entity: OPITZ CONSULTING (ORG)
    7) Entity: deutschen (LOC)

    Satz 4:
    ....

    Ein aufmerksamer Blick auf die Ausgabe von Schritt 7, der Erkennung benannter Entitäten (NER), könnte für eine kurze Verwirrung sorgen. Obwohl Entitäten wie OPITZ CONSULTING und STACKIT korrekt als Organisationen (ORG) erkannt werden, fehlen erwartete Zuweisungen wie zum Beispiel für das Datum „1. März 2024“ oder für Produktbezeichnungen wie „STACKIT Cloud“.

    Der Grund hierfür liegt nicht in einem Fehler des Skripts, sondern im zugrundeliegenden Sprachmodell, das Stanza standardmäßig für Deutsch verwendet. Wie die Protokollausgabe beim Start des Skripts verrät, kommt hier das Paket germeval2014 zum Einsatz. Dieses Modell wurde auf Basis des „GermEval 2014 Shared Task“ trainiert und kennt daher hauptsächlich die vier Entitätstypen, die in diesem Wettbewerb im Fokus standen: PER (Person), LOC (Ort), ORG (Organisation) und OTH (Sonstiges).

    =======================================
    | Processor | Package |
    ---------------------------------------
    | tokenize | combined |
    | mwt | combined |
    | pos | combined_charlm |
    | lemma | combined_nocharlm |
    | depparse | combined_charlm |
    | ner | germeval2014 | <-- HIER
    | ... | ... |
    =======================================

    Kategorien wie DATE (Datum), MONEY (Geldbetrag) oder PRODUCT (Produkt) sind in diesem spezifischen Modell schlichtweg nicht vorgesehen. Es kann also nur das erkennen, wofür es trainiert wurde. Dies ist eine wichtige Erkenntnis bei der Arbeit mit vorgefertigten KI-Modellen: Ihre Leistungsfähigkeit und ihr „Wissen“ sind immer durch die Daten und die Zielsetzung ihres ursprünglichen Trainings begrenzt.

    Hybrid-Pipeline: Verbesserung von Named Entity Recognition (NER)

    Das ursprüngliche Skript wurde zu einer robusten, modularen Verarbeitungspipeline weiterentwickelt. Die wichtigsten Änderungen sind:

    • Austausch der NER-Komponente: Der entscheidende Schritt war, die standardmäßige Erkennung benannter Entitäten (NER) von Stanza, die auf dem älteren germeval2014-Modell basiert, zu entfernen. Stattdessen wird nun ein modernes, auf der Hugging Face transformers-Bibliothek basierendes Modell verwendet. Dieses neue Modell (domischwimmbeck/bert-base-german-cased-fine-tuned-ner) erkennt nicht nur mehr Entitätstypen, sondern bietet in der Regel auch eine höhere Genauigkeit.
    • Einführung einer Hybrid-Pipeline: Anstatt sich auf eine einzige Bibliothek zu verlassen, kombiniert das Skript nun die Stärken von zwei spezialisierten Werkzeugen.
    # Für die PDF-Verarbeitung
    pdfplumber

    # Für API-Anfragen an das lokale DeepSeek-Modell
    requests

    # Für die linguistische Analyse (Tokenisierung, POS, Lemma, Depparse)
    stanza

    # Kern-Bibliothek für Stanza und Transformers
    # Wichtig: Muss mit torchvision kompatibel sein
    torch

    # Wird von torch im Hintergrund genutzt
    torchvision

    # Für die moderne NER-Analyse mit Hugging Face
    transformers
    sentencepiece

    # Wichtig: Version fixiert für Kompatibilität mit Stanza/Torch
    numpy<2.0
    requirements.txt
    # Vorbereitung: Bibliotheken importieren
    import pdfplumber
    import requests
    from pathlib import Path
    import stanza
    from transformers import pipeline # Hinzugefügt für Hugging Face

    # ####################################################################
    # HILFSFUNKTIONEN (aus dem ursprünglichen Skript)
    # ####################################################################

    def extract_clean_text(path: Path, top_margin: float = 0.15, bottom_margin: float = 0.15) -> str:
    """
    Diese Funktion ist Teil der Vorbereitung und **kein Schritt der Stanza-Pipeline**.
    - PDF-Datei öffnen und jede Seite durchgehen.
    - Obere und untere Seitenränder abschneiden.
    - Text extrahieren, Zeilen normalisieren.
    - Worttrennungen am Zeilenende auflösen.
    - Textblöcke zu einem Fließtext zusammenfassen.
    """
    paragraphs = []
    try:
    with pdfplumber.open(path) as pdf:
    for page in pdf.pages:
    height = page.height
    top = height * top_margin
    bottom = height * (1 - bottom_margin)
    cropped = page.within_bbox((0, top, page.width, bottom))
    raw = cropped.extract_text()
    if not raw:
    continue
    current_line = ""
    for line in raw.splitlines():
    line = line.strip()
    if not line:
    continue
    # Vereinfachte Annahme: Zeilen in Großbuchstaben sind oft Titel und werden ignoriert
    if line.isupper() and len(line.split()) < 5:
    continue
    if line.endswith("-") and len(line) > 1 and line[-2].isalpha():
    current_line = line[:-1]
    else:
    current_line = " " line
    if line.endswith((".", "!", "?")):
    paragraphs.append(current_line.strip())
    current_line = ""
    if current_line:
    paragraphs.append(current_line.strip())
    except Exception as e:
    print(f"Fehler beim Lesen der PDF-Datei {path}: {e}")
    return ""
    return " ".join(paragraphs)

    def deepseek_sentence_split(text: str) -> list:
    """
    Diese Funktion ist KEIN Teil der Stanza-Pipeline.
    Sie dient der Aufteilung des Fließtexts in vollständige Sätze (für die nachfolgende Verarbeitung).
    """
    if not text:
    return []
    url = "http://localhost:11434/api/generate"
    prompt = (
    "Teile den folgenden deutschen Text in grammatisch vollständige, möglichst kurze Einzelsätze. "
    "Jede Zeile enthält genau einen abgeschlossenen Satz. "
    "Keine Nummerierung, keine Einleitung, keine Leerzeilen.\n\n" text.strip()
    )
    data = {
    "model": "deepseek-Coder-V2:latest", # Stellen Sie sicher, dass dieses Modell lokal verfügbar ist (z.B. via Ollama)
    "prompt": prompt,
    "stream": False
    }
    try:
    response = requests.post(url, json=data, timeout=120)
    response.raise_for_status()
    raw = response.json()["response"]
    sentences = [line.strip() for line in raw.splitlines() if line.strip()]
    return sentences
    except requests.exceptions.RequestException as e:
    print(f"Fehler bei der Anfrage an das lokale Sprachmodell unter {url}: {e}")
    print("Stellen Sie sicher, dass der lokale Server (z.B. Ollama) läuft und das Modell 'deepseek-Coder-V2:latest' geladen ist.")
    # Fallback: Einfache Satzteilung als Notlösung
    return text.split('.')

    # ####################################################################
    # HAUPTSKRIPT
    # ####################################################################

    # --- SCHRITT 1: Stanza-Pipeline OHNE NER konfigurieren ---
    print("Lade Stanza-Modelle (ohne NER)...")
    try:
    stanza.download('de', processors='tokenize,mwt,pos,lemma,depparse')
    nlp_stanza = stanza.Pipeline(
    'de',
    processors='tokenize,mwt,pos,lemma,depparse', # 'ner' wurde hier entfernt
    tokenize_no_ssplit=True
    )
    print("Stanza-Modelle geladen.")
    except Exception as e:
    print(f"Fehler beim Laden der Stanza-Modelle: {e}")
    exit()


    # --- SCHRITT 2: Hugging Face NER-Pipeline mit automatischem Download laden ---
    print("Lade Hugging Face NER-Modell (automatischer Download)...")
    try:
    # Ihr vorgeschlagenes Modell - das ist der Standardweg!
    # Die Bibliothek lädt das Modell automatisch herunter und speichert es im Cache.
    ner_pipeline_hf = pipeline(
    "ner", # "ner" ist der Task, "token-classification" ist ein Alias und funktioniert auch
    model="domischwimmbeck/bert-base-german-cased-fine-tuned-ner",
    aggregation_strategy="simple"
    )
    print("Hugging Face NER-Modell erfolgreich geladen.")
    except Exception as e:
    print(f"Fehler beim Laden des Hugging Face Modells: {e}")
    print("Stellen Sie sicher, dass eine stabile Internetverbindung besteht.")
    exit()

    # Vorbereitung: PDF bereinigen und in Sätze umwandeln
    pdf_path = Path("test_oc.pdf") # Stellen Sie sicher, dass diese Datei existiert
    text = extract_clean_text(pdf_path)
    if not text:
    print("Konnte keinen Text aus der PDF-Datei extrahieren. Das Skript wird beendet.")
    exit()

    sentences = deepseek_sentence_split(text)
    if not sentences:
    print("Konnte den Text nicht in Sätze unterteilen. Das Skript wird beendet.")
    exit()

    # --- HYBRIDE VERARBEITUNGSSCHLEIFE ---
    for i, sentence in enumerate(sentences, start=1):
    print(f"\nSatz {i}:")
    print(f" [Original] {sentence}")

    # --- Stanza-Analyse (Schritte 1-5) ---
    doc_stanza = nlp_stanza(sentence)

    # Ausgabe der Stanza-Ergebnisse (Token-Ebene)
    for s in doc_stanza.sentences:
    print(" Pipeline-Ausgabe (Stanza, Schritte 1-5):")
    for word in s.words:
    print(
    f" 1-4) Token/POS/Lemma: {word.text:<20} | {word.upos:<8} | {word.lemma:<20}"
    f" | 5) Depparse: Head={word.head}, DepRel={word.deprel}"
    )

    # --- Hugging Face NER-Analyse (Schritt 7, ausgetauscht) ---
    entities_hf = ner_pipeline_hf(sentence)

    # Ausgabe der Hugging Face-Ergebnisse
    if entities_hf:
    print(" Pipeline-Ausgabe (Hugging Face, Schritt 7):")
    for entity in entities_hf:
    print(f" - Entity: {entity['word']} ({entity['entity_group']}, Score: {entity['score']:.4f})")
    else:
    print(" Pipeline-Ausgabe (Hugging Face, Schritt 7): Keine erkannt.")
    app.py

    Neue Ausgabe der benannten Entitäten

      Pipeline-Ausgabe (Hugging Face, Schritt 7):
    - Entity: MIT STACKIT (ORG, Score: 0.8755)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9855)
    - Entity: Deutschland (LOC, Score: 0.9982)
    - Entity: Österreich (LOC, Score: 0.9984)
    - Entity: Gummersbach (LOC, Score: 0.9802)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9742)
    - Entity: STACKIT (ORG, Score: 0.9815)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9674)
    - Entity: STACKIT (ORG, Score: 0.8334)
    - Entity: ##ud (ORG, Score: 0.3354)
    - Entity: STACKIT ' s (ORG, Score: 0.9340)
    - Entity: OPITZ (ORG, Score: 0.7717)
    - Entity: CONSULTING ' s (ORG, Score: 0.6602)
    - Entity: STACKIT (ORG, Score: 0.9784)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9742)
    - Entity: STACKIT (ORG, Score: 0.7768)
    - Entity: ##ud (OTH, Score: 0.4203)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9938)
    - Entity: Christoph Pfinder (PER, Score: 0.9959)
    - Entity: STACKIT (ORG, Score: 0.9948)
    - Entity: STACKIT STACKIT (ORG, Score: 0.9558)
    - Entity: Schwarz Gruppe (ORG, Score: 0.8254)
    - Entity: ##C (LOC, Score: 0.6749)
    - Entity: Schwarz Gruppe (ORG, Score: 0.8838)
    - Entity: Deutschland (LOC, Score: 0.9984)
    - Entity: Österreich (LOC, Score: 0.9980)
    - Entity: STACKIT (ORG, Score: 0.9914)

    Unter Hugging Face stehen weitere NER-Modelle zur Verfügung:
    https://huggingface.co/models?library=pytorch&language=de&sort=likes

    Spacy statt Stanza

    In der neuen Version des Skripts wurde die komplette linguistische Basisanalyse, die zuvor von der Bibliothek Stanza durchgeführt wurde, durch die Bibliothek spaCy ersetzt. spaCy ist eine sehr populäre und auf Geschwindigkeit optimierte Bibliothek, die oft in produktiven Anwendungen zum Einsatz kommt. Während Stanza für seine hohe akademische Genauigkeit bekannt ist, bietet spaCy eine hervorragende Balance aus Performance und Präzision und liefert Analyseergebnisse, die für die Weiterverarbeitung in Softwareprojekten oft als besonders intuitiv empfunden werden. Die Kernlogik der hybriden Pipeline – die Kombination einer Basis-Analyse mit einem spezialisierten NER-Modell – bleibt dabei identisch.
    Die folgenden Analyse-Schritte werden nun nicht mehr von Stanza, sondern von spaCy übernommen:

    • Tokenisierung: Das Aufteilen der Sätze in einzelne Wörter (Tokens).
    • Wortartbestimmung (POS-Tagging): Die Zuweisung einer grammatikalischen Kategorie zu jedem Wort (z.B. Substantiv, Verb, Adjektiv).
    • Lemmatisierung: Das Zurückführen jedes Wortes auf seine Grundform (z.B. „ging“ → „gehen“).
    • Dependenz-Analyse: Die Analyse der syntaktischen Satzstruktur, also welche Wörter im Satz grammatikalisch voneinander abhängen.
    # Vorbereitung: Bibliotheken importieren
    import pdfplumber
    import requests
    from pathlib import Path
    import spacy # Statt stanza
    from transformers import pipeline

    # ####################################################################
    # HILFSFUNKTIONEN (unverändert)
    # ####################################################################

    def extract_clean_text(path: Path, top_margin: float = 0.15, bottom_margin: float = 0.15) -> str:
    """
    Diese Funktion extrahiert und bereinigt den Text aus einer PDF-Datei.
    """
    paragraphs = []
    try:
    with pdfplumber.open(path) as pdf:
    for page in pdf.pages:
    height = page.height
    top = height * top_margin
    bottom = height * (1 - bottom_margin)
    cropped = page.within_bbox((0, top, page.width, bottom))
    raw = cropped.extract_text()
    if not raw:
    continue
    current_line = ""
    for line in raw.splitlines():
    line = line.strip()
    if not line:
    continue
    if line.isupper() and len(line.split()) < 5:
    continue
    if line.endswith("-") and len(line) > 1 and line[-2].isalpha():
    current_line = line[:-1]
    else:
    current_line = " " line
    if line.endswith((".", "!", "?")):
    paragraphs.append(current_line.strip())
    current_line = ""
    if current_line:
    paragraphs.append(current_line.strip())
    except Exception as e:
    print(f"Fehler beim Lesen der PDF-Datei {path}: {e}")
    return ""
    return " ".join(paragraphs)

    def deepseek_sentence_split(text: str) -> list:
    """
    Nutzt ein lokales LLM zur intelligenten Satzzerlegung.
    """
    if not text:
    return []
    url = "http://localhost:11434/api/generate"
    prompt = (
    "Teile den folgenden deutschen Text in grammatisch vollständige, möglichst kurze Einzelsätze. "
    "Jede Zeile enthält genau einen abgeschlossenen Satz. "
    "Keine Nummerierung, keine Einleitung, keine Leerzeilen.\n\n" text.strip()
    )
    data = {
    "model": "deepseek-Coder-V2:latest",
    "prompt": prompt,
    "stream": False
    }
    try:
    response = requests.post(url, json=data, timeout=120)
    response.raise_for_status()
    raw = response.json()["response"]
    sentences = [line.strip() for line in raw.splitlines() if line.strip()]
    return sentences
    except requests.exceptions.RequestException as e:
    print(f"Fehler bei der Anfrage an das lokale Sprachmodell unter {url}: {e}")
    print("Stellen Sie sicher, dass der lokale Server (z.B. Ollama) läuft.")
    return text.split('.')

    # ####################################################################
    # HAUPTSKRIPT mit OPTIMIERTER REIHENFOLGE
    # ####################################################################

    # --- Modelle laden ---
    print("Lade spaCy-Modell...")
    try:
    nlp_spacy = spacy.load('de_core_news_lg')
    print("spaCy-Modell geladen.")
    except OSError:
    print("spaCy-Modell 'de_core_news_lg' nicht gefunden.")
    print("Bitte installieren Sie es mit dem Befehl: python -m spacy download de_core_news_lg")
    exit()

    print("Lade Hugging Face NER-Modell...")
    try:
    ner_pipeline_hf = pipeline(
    "ner",
    model="domischwimmbeck/bert-base-german-cased-fine-tuned-ner",
    aggregation_strategy="simple"
    )
    print("Hugging Face NER-Modell erfolgreich geladen.")
    except Exception as e:
    print(f"Fehler beim Laden des Hugging Face Modells: {e}")
    exit()

    # --- Schritt 1: PDF-Text extrahieren ---
    pdf_path = Path("test_oc.pdf")
    full_text = extract_clean_text(pdf_path)
    if not full_text:
    print("Konnte keinen Text aus der PDF-Datei extrahieren. Das Skript wird beendet.")
    exit()

    # --- Schritt 2: NER auf dem GESAMTEN Text für maximale Qualität ---
    print("\n--- Globale Entitäten (im gesamten Text erkannt) ---")
    all_entities = ner_pipeline_hf(full_text)
    if all_entities:
    for entity in all_entities:
    print(f" - Entity: {entity['word']} ({entity['entity_group']}, Score: {entity['score']:.4f})")
    else:
    print(" Keine Entitäten im gesamten Text erkannt.")
    print("--------------------------------------------------\n")


    # --- Schritt 3: Text in Sätze aufteilen ---
    sentences = deepseek_sentence_split(full_text)
    if not sentences:
    print("Konnte den Text nicht in Sätze unterteilen. Das Skript wird beendet.")
    exit()

    # --- Schritt 4: Detaillierte linguistische Analyse pro Satz ---
    for i, sentence in enumerate(sentences, start=1):
    print(f"Satz {i}:")
    print(f" [Original] {sentence}")

    # --- spaCy-Analyse pro Satz ---
    doc_spacy = nlp_spacy(sentence)

    # Ausgabe der spaCy-Ergebnisse (Token-Ebene)
    print(" Linguistische Analyse (spaCy):")
    for token in doc_spacy:
    print(
    f" - Token: {token.text:<20} | POS: {token.pos_:<8} | Lemma: {token.lemma_:<20}"
    f" | Dep: {token.dep_} -> {token.head.text}"
    )
    print("-" * 20) # Trennlinie für bessere Lesbarkeit
    app.py
    (c:\sources\agents\langraph\venv) PS C:\sources\agents\langraph> & c:/sources/agents/langraph/venv/python.exe c:/sources/agents/langraph/spacy-test.py
    Lade spaCy-Modell...
    spaCy-Modell geladen.
    Lade Hugging Face NER-Modell...
    Hugging Face NER-Modell erfolgreich geladen.

    --- Globale Entitäten (im gesamten Text erkannt) ---
    - Entity: M (ORG, Score: 0.5533)
    - Entity: ##IT (LOC, Score: 0.4629)
    - Entity: STACKIT (ORG, Score: 0.7900)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9710)
    - Entity: Deutschland (LOC, Score: 0.9976)
    - Entity: Österreich (LOC, Score: 0.9967)
    - Entity: Gummersbach (LOC, Score: 0.9925)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9790)
    - Entity: STACKIT (ORG, Score: 0.9806)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9497)
    - Entity: STACKIT (ORG, Score: 0.5693)
    - Entity: STACKIT ' s (ORG, Score: 0.5869)
    - Entity: O (PER, Score: 0.3188)
    - Entity: ##NS (ORG, Score: 0.3573)
    - Entity: ##L (PER, Score: 0.3493)
    - Entity: ##TIN (ORG, Score: 0.4552)
    - Entity: ##G (PER, Score: 0.6276)
    - Entity: s (PER, Score: 0.3930)
    - Entity: STACKIT (ORG, Score: 0.9729)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9848)
    - Entity: S (ORG, Score: 0.4554)
    - Entity: ##IT (ORG, Score: 0.5369)
    - Entity: OPITZ CONSULTING (ORG, Score: 0.9919)
    - Entity: Pfinder (PER, Score: 0.9634)
    - Entity: STACKIT STACKIT (ORG, Score: 0.8969)
    - Entity: Schwarz Gruppe (ORG, Score: 0.8239)
    - Entity: ##C (LOC, Score: 0.5142)
    - Entity: Schwarz Gruppe (ORG, Score: 0.9199)
    --------------------------------------------------

    Satz 1:
    [Original] 1. MIT STACKIT holt sich OPITZ CONSULTING einen souveränen deutschen Cloud Provider mit ins Boot.
    Linguistische Analyse (spaCy):
    - Token: 1. | POS: ADV | Lemma: 1. | Dep: mo -> MIT
    - Token: MIT | POS: ADP | Lemma: MIT | Dep: ROOT -> MIT
    - Token: STACKIT | POS: PROPN | Lemma: STACKIT | Dep: sb -> holt
    - Token: holt | POS: VERB | Lemma: holen | Dep: ROOT -> holt
    - Token: sich | POS: PRON | Lemma: sich | Dep: oa -> holt
    - Token: OPITZ | POS: PROPN | Lemma: OPITZ | Dep: ROOT -> OPITZ
    - Token: CONSULTING | POS: PROPN | Lemma: CONSULTING | Dep: ROOT -> CONSULTING
    - Token: einen | POS: DET | Lemma: ein | Dep: nk -> Provider
    - Token: souveränen | POS: ADJ | Lemma: souverän | Dep: nk -> Provider
    - Token: deutschen | POS: ADJ | Lemma: deutsch | Dep: nk -> Provider
    - Token: Cloud | POS: PROPN | Lemma: Cloud | Dep: nk -> Provider
    - Token: Provider | POS: NOUN | Lemma: Provider | Dep: oa -> CONSULTING
    - Token: mit | POS: ADP | Lemma: mit | Dep: mnr -> Provider
    - Token: ins | POS: ADP | Lemma: in | Dep: mo -> CONSULTING
    - Token: Boot | POS: NOUN | Lemma: Boot | Dep: nk -> ins
    - Token: . | POS: PUNCT | Lemma: -- | Dep: punct -> CONSULTING
    --------------------
    Satz 2:
    [Original] 2. Seit dem 1. März 2024 bündeln OPITZ CONSULTING und STACKIT ihre Kräfte, um eine Allianz im Bereich Cloud-Infrastruktur und Cloud-Dienstleistungen zu schmieden.
    Linguistische Analyse (spaCy):
    - Token: 2. | POS: ADV | Lemma: 2. | Dep: mo -> bündeln
    - Token: Seit | POS: ADP | Lemma: seit | Dep: mo -> bündeln
    - Token: dem | POS: DET | Lemma: der | Dep: nk -> März
    - Token: 1. | POS: ADJ | Lemma: 1. | Dep: nk -> März
    - Token: März | POS: NOUN | Lemma: März | Dep: nk -> Seit
    - Token: 2024 | POS: NUM | Lemma: 2024 | Dep: nk -> März
    - Token: bündeln | POS: VERB | Lemma: bündeln | Dep: ROOT -> bündeln
    - Token: OPITZ | POS: NOUN | Lemma: OPITZ | Dep: ROOT -> OPITZ
    - Token: CONSULTING | POS: PROPN | Lemma: CONSULTING | Dep: mo -> Kräfte
    - Token: und | POS: CCONJ | Lemma: und | Dep: cd -> CONSULTING
    - Token: STACKIT | POS: PROPN | Lemma: STACKIT | Dep: cj -> und
    - Token: ihre | POS: DET | Lemma: ihr | Dep: nk -> Kräfte
    - Token: Kräfte | POS: NOUN | Lemma: Kraft | Dep: ROOT -> Kräfte
    - Token: , | POS: PUNCT | Lemma: -- | Dep: punct -> Kräfte
    - Token: um | POS: SCONJ | Lemma: um | Dep: cp -> schmieden
    - Token: eine | POS: DET | Lemma: ein | Dep: nk -> Allianz
    - Token: Allianz | POS: NOUN | Lemma: Allianz | Dep: oa -> schmieden
    - Token: im | POS: ADP | Lemma: in | Dep: mnr -> Allianz
    - Token: Bereich | POS: NOUN | Lemma: Bereich | Dep: nk -> im
    - Token: Cloud-Infrastruktur | POS: NOUN | Lemma: Cloud-Infrastruktur | Dep: nk -> Bereich
    - Token: und | POS: CCONJ | Lemma: und | Dep: cd -> Cloud-Infrastruktur
    - Token: Cloud-Dienstleistungen | POS: NOUN | Lemma: Cloud-Dienstleistung | Dep: cj -> und
    - Token: zu | POS: PART | Lemma: zu | Dep: pm -> schmieden
    - Token: schmieden | POS: VERB | Lemma: schmieden | Dep: mnr -> Kräfte
    - Token: . | POS: PUNCT | Lemma: -- | Dep: punct -> Kräfte
    --------------------
    Satz 3:
    [Original] 3. Diese Zusammenarbeit eröffnet den Kunden von OPITZ CONSULTING den Zugang zur STACKIT Cloud – einer deutschen, datensouveränen Cloud-Lösung.
    Linguistische Analyse (spaCy):
    - Token: 3. | POS: ADV | Lemma: 3. | Dep: mo -> Zusammenarbeit
    - Token: Diese | POS: DET | Lemma: dieser | Dep: nk -> Zusammenarbeit
    - Token: Zusammenarbeit | POS: NOUN | Lemma: Zusammenarbeit | Dep: sb -> eröffnet
    - Token: eröffnet | POS: VERB | Lemma: eröffnen | Dep: ROOT -> eröffnet
    - Token: den | POS: DET | Lemma: der | Dep: nk -> Kunden
    - Token: Kunden | POS: NOUN | Lemma: Kunde | Dep: da -> eröffnet
    - Token: von | POS: ADP | Lemma: von | Dep: pg -> Kunden
    - Token: OPITZ | POS: PROPN | Lemma: OPITZ | Dep: nk -> von
    - Token: CONSULTING | POS: PROPN | Lemma: CONSULTING | Dep: ROOT -> CONSULTING
    - Token: den | POS: DET | Lemma: der | Dep: nk -> Zugang
    - Token: Zugang | POS: NOUN | Lemma: Zugang | Dep: oa -> CONSULTING
    - Token: zur | POS: ADP | Lemma: zu | Dep: mnr -> Zugang
    - Token: STACKIT | POS: PROPN | Lemma: STACKIT | Dep: pnc -> Cloud
    - Token: Cloud | POS: PROPN | Lemma: Cloud | Dep: nk -> zur
    - Token: – | POS: PUNCT | Lemma: -- | Dep: punct -> Cloud
    - Token: einer | POS: DET | Lemma: ein | Dep: nk -> Cloud-Lösung
    - Token: deutschen | POS: ADJ | Lemma: deutsch | Dep: nk -> Cloud-Lösung
    - Token: , | POS: PUNCT | Lemma: -- | Dep: punct -> deutschen
    - Token: datensouveränen | POS: ADJ | Lemma: datensouverän | Dep: cj -> deutschen
    - Token: Cloud-Lösung | POS: NOUN | Lemma: Cloud-Lösung | Dep: app -> Cloud
    - Token: . | POS: PUNCT | Lemma: -- | Dep: punct -> Zugang
    --------------------