> ## Documentation Index
> Fetch the complete documentation index at: https://docs.venice.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Ein Audio-Recherche-Notebook bauen

> Verwandle deine Quellen mit Venice in zitierte Antworten und einen Audio-Überblick mit zwei Hosts.

export const AuthorByline = ({name, date}) => {
  return <p style={{
    marginTop: "-1rem",
    marginBottom: "1.5rem"
  }}>
      <small>
        Originally written by {name} - {date}
      </small>
    </p>;
};

<AuthorByline name="Sabrina Aquino" date="20 August 2026" />

Tools wie NotebookLM haben verändert, was Menschen von einem Stapel Recherchematerial erwarten. Du fügst Quellen hinzu, stellst Fragen und bekommst Antworten, die auf das Material zurückverweisen, und generierst dann ein Gespräch zwischen zwei Hosts, das du dir beim Spazierengehen anhören kannst.

Diese Anleitung baut genau das — in etwa zweihundert Zeilen Python und mit fünf Venice-Endpoints. Außer den Anfragen selbst wird nichts außerhalb deines Rechners gespeichert, und Venice bewahrt auch diese nicht auf.

<Card title="Dieses Notebook in Google Colab ausführen" icon="notebook" href="https://colab.research.google.com/github/veniceai/api-docs/blob/main/notebooks/audio-research-notebook.ipynb">
  Jeder Schritt unten als ausführbares Notebook, mit dem Überblick inline abspielbar. Nichts zu installieren.
</Card>

## So funktioniert es

Fünf Endpoints, jeder mit einer Aufgabe:

| Schritt                     | Endpoint               | Warum                                                    |
| --------------------------- | ---------------------- | -------------------------------------------------------- |
| Eine Webseite lesen         | `/augment/scrape`      | Liefert Markdown, kein HTML, das du säubern musst        |
| Ein PDF oder Dokument lesen | `/augment/text-parser` | Ein Multipart-Upload, Text zurück                        |
| Den Text indexieren         | `/embeddings`          | Ermöglicht Retrieval nach Bedeutung statt nach Stichwort |
| Fragen beantworten          | `/chat/completions`    | Verankert in abgerufenen Passagen, mit Zitaten           |
| Den Überblick sprechen      | `/audio/speech`        | Zwei Stimmen, eine pro Host                              |

Das Retrieval ist hier bewusst schlicht: Vektoren in einer Python-Liste, Cosinus-Ähnlichkeit in einer Schleife. Das ist genau die richtige Menge Maschinerie für ein paar Dutzend Quellen und hält die beweglichen Teile sichtbar. Wenn du daraus herauswächst, behandelt [Einen privaten RAG-Bot bauen](/learn/private-rag-bot) dieselbe Pipeline mit einer echten Vektordatenbank und einem Re-Ranking-Schritt.

## Einrichten

Eine Abhängigkeit und ein Schlüssel von [der API-Einstellungsseite](/guides/getting-started/generating-api-key).

```bash theme={"system"}
pip install requests
export VENICE_API_KEY="your-key-here"
```

Erstelle `notebook.py` und beginne mit den Imports und der Konfiguration. Die beiden Listen unten sind der gesamte Zustand des Notebooks: `sources` erfasst, was du hinzugefügt hast, und `chunks` enthält die durchsuchbaren Stücke.

```python theme={"system"}
import io
import json
import os
import re
import wave
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

import requests

BASE_URL = "https://api.venice.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VENICE_API_KEY']}"}

EMBED_MODEL = "text-embedding-bge-m3"
TTS_MODEL = "tts-xai-v1"
HOSTS = {"Ana": "luna", "Marco": "orion"}

sources = []
chunks = []
```

`HOSTS` ordnet einem Hostnamen eine Stimme zu. Beide Stimmen stammen aus `tts-xai-v1`, und das ist wichtig: Stimmen gehören zu Modellen, und eine Stimme aus einer Familie an ein Modell aus einer anderen zu schicken, ist der häufigste erste Fehler mit dem Speech-Endpoint.

## Ein Modell wählen, das nicht veraltet

Ein Chat-Modell in ein Projekt fest einzucodieren, garantiert, dass das Projekt altert. Venice veröffentlicht über `/models/traits`, welches Modell aktuell welche Rolle innehat, sodass du das aktuelle Default abfragen kannst, statt eines beim Namen zu nennen.

```python theme={"system"}
def default_text_model():
    response = requests.get(
        f"{BASE_URL}/models/traits", headers=HEADERS, params={"type": "text"}, timeout=60
    )
    response.raise_for_status()
    return response.json()["data"]["default"]


CHAT_MODEL = default_text_model()


def chat(messages, **options):
    response = requests.post(
        f"{BASE_URL}/chat/completions",
        headers=HEADERS,
        json={"model": CHAT_MODEL, "messages": messages, **options},
        timeout=300,
    )
    response.raise_for_status()
    return response.json()["choices"][0]["message"]["content"]
```

Weitere Traits sind verfügbar, falls dieses Notebook nicht die Form hat, die du möchtest. `most_intelligent` bringt dir ein stärkeres Modell für die argumentationslastige Zusammenfassung, und `default_reasoning` liefert eines, das offen denkt. Siehe [Modelle](/models/overview) für die vollständige Liste.

## Quellen hinzufügen

Eine Quelle ist entweder eine URL oder eine Datei auf der Festplatte, und Venice hat für jedes einen Endpoint. Beide liefern reinen Text zurück, und das ist der Punkt: Der Rest des Notebooks kümmert sich nicht darum, woher eine Quelle stammt.

```python theme={"system"}
def read_url(url):
    response = requests.post(
        f"{BASE_URL}/augment/scrape", headers=HEADERS, json={"url": url}, timeout=180
    )
    response.raise_for_status()
    return response.json()["content"]


def read_file(path):
    with open(path, "rb") as handle:
        response = requests.post(
            f"{BASE_URL}/augment/text-parser",
            headers=HEADERS,
            files={"file": (Path(path).name, handle)},
            timeout=180,
        )
    response.raise_for_status()
    return response.json()["text"]
```

`/augment/scrape` liefert Markdown statt rohem HTML zurück, sodass kein Boilerplate zu entfernen ist. `/augment/text-parser` akzeptiert PDF, Word, Excel und reinen Text bis zu 25 MB und meldet neben dem Text auch die Tokenanzahl. [Dokumentenverarbeitung](/guides/tools/document-processing) behandelt seine Optionen im Detail.

## Chunking und Embeddings

Ein ganzes Dokument einzubetten, erzeugt einen einzigen Vektor, der ein Mittelwert von allem ist, was es sagt — zu grob, um eine bestimmte Aussage abzurufen. Es aufzuteilen erzeugt Vektoren, die jeweils etwas bedeuten.

Teile an Absatzgrenzen statt bei einer festen Zeichenanzahl. Ein Chunk, der mitten im Satz endet, wird schlecht abgerufen, weil das Embedding von einem Fragment stammt.

```python theme={"system"}
def split(text, limit=1200):
    """Absätze zu Chunks packen, ohne einen zu zerteilen."""
    packed, current = [], ""
    for para in re.split(r"\n\s*\n", text):
        para = para.strip()
        if not para:
            continue
        if current and len(current) + len(para) + 2 > limit:
            packed.append(current)
            current = para
        else:
            current = f"{current}\n\n{para}" if current else para
    if current:
        packed.append(current)
    return packed


def embed(texts):
    vectors = []
    for start in range(0, len(texts), 64):
        response = requests.post(
            f"{BASE_URL}/embeddings",
            headers=HEADERS,
            json={"model": EMBED_MODEL, "input": texts[start : start + 64]},
            timeout=180,
        )
        response.raise_for_status()
        vectors.extend(row["embedding"] for row in response.json()["data"])
    return vectors
```

`embed` bündelt in Batches, weil der Endpoint eine Liste entgegennimmt und eine Anfrage für vierundsechzig Chunks in der Gesamtzeit weit günstiger ist als vierundsechzig Anfragen. `text-embedding-bge-m3` liefert 1024 Dimensionen zurück und kommt gut mit mehrsprachigen Quellen zurecht.

Eine Quelle hinzuzufügen ist nun: lesen, aufteilen, einbetten und erfassen. Der Betrag jedes Vektors wird daneben gespeichert, weil er sich nie ändert und ihn innerhalb der Ähnlichkeitsschleife neu zu berechnen verschwendete Arbeit wäre.

```python theme={"system"}
def add_source(title, ref):
    text = read_url(ref) if ref.startswith("http") else read_file(ref)
    number = len(sources) + 1
    sources.append({"number": number, "title": title, "ref": ref})

    pieces = split(text)
    for piece, vector in zip(pieces, embed(pieces)):
        magnitude = sum(x * x for x in vector) ** 0.5
        chunks.append(
            {"source": number, "title": title, "text": piece,
             "vector": vector, "magnitude": magnitude}
        )
    print(f"[{number}] {title}: {len(text)} characters, {len(pieces)} chunks")
```

Die `number` ist das, was später Zitate ermöglicht. Jeder Chunk merkt sich, aus welcher Quelle er stammt, sodass eine Antwort darauf zurückverweisen kann.

## Die richtigen Passagen abrufen

Cosinus-Ähnlichkeit zwischen dem Fragevektor und jedem Chunk-Vektor, sortiert, top k. Für ein paar Tausend Chunks läuft das schneller als der Netzwerkaufruf, der den Fragevektor erzeugt hat.

```python theme={"system"}
def retrieve(question, k=6):
    query = embed([question])[0]
    query_magnitude = sum(x * x for x in query) ** 0.5

    def similarity(chunk):
        dot = sum(a * b for a, b in zip(query, chunk["vector"]))
        return dot / (query_magnitude * chunk["magnitude"])

    return sorted(chunks, key=similarity, reverse=True)[:k]
```

## Antworten mit Zitaten

Der Unterschied zwischen einer verankerten Antwort und einer selbstbewussten Vermutung liegt vollständig im Prompt. Zwei Anweisungen erledigen die Arbeit: Antworte nur aus den Notizen und sag es, wenn die Notizen nicht ausreichen. Ohne die zweite füllt ein Modell die Lücke leise aus dem Gedächtnis — genau der Fehlermodus, den du zu vermeiden versuchst.

Die Notizen im Prompt zu nummerieren, gibt dem Modell ein Zitations-Vokabular. Es schreibt `[2]`, und du kannst das zurück auf eine Quelle auflösen.

```python theme={"system"}
def ask(question, k=6):
    hits = retrieve(question, k)
    notes = "\n\n".join(f"[{h['source']}] {h['title']}\n{h['text']}" for h in hits)
    answer = chat(
        [
            {"role": "system", "content": (
                "Answer only from the numbered notes. Cite every claim with the bracket number "
                "of the note it came from. If the notes do not answer the question, say so "
                "instead of filling the gap.")},
            {"role": "user", "content": f"Notes:\n\n{notes}\n\nQuestion: {question}"},
        ],
        temperature=0.2,
    )
    cited = sorted({int(n) for n in re.findall(r"\[(\d+)\]", answer)})
    return answer, [s for s in sources if s["number"] in cited]
```

Die Klammern hinterher wieder herauszuparsen, ist die eine Zeile wert. Sie sagt dir, welche Quellen die Antwort tatsächlich getragen haben — so bemerkst du, dass eine Quelle, die du für zentral gehalten hast, nie zitiert wird.

## Das Skript für den Überblick schreiben

Hier hört das Notebook auf, eine Suchbox zu sein. Eine Zusammenfassung ist etwas, das du liest; ein Überblick ist etwas, das du anhörst, und die beiden wollen unterschiedliche Prosa. Dialog funktioniert im Audio besser, weil der Sprecherwechsel das Tempo für dich vorgibt, und eine Frage von einem Host ist eine natürliche Möglichkeit, die nächste Idee einzuführen.

Drei Einschränkungen sind wichtig, und alle drei kommen vom Audio, nicht vom Text:

* **Kein Markdown, keine URLs.** Ein Sprachmodell liest `https://docs.venice.ai` Zeichen für Zeichen vor.
* **Abkürzungen ausschreiben.** *T E E* beim ersten Mal, nicht *tee*.
* **Turn-Länge variieren.** Gleich lange Turns klingen wie zwei Personen, die einander eine Liste vorlesen.

JSON mit einem Schema anzufordern, ist das, was das Ergebnis renderbar macht. Freier Text müsste geparst werden, und Sprecherlabels sind genau das, wobei ein Modell kreativ wird. Das `enum` auf `speaker` sorgt dafür, dass jeder Turn auf eine Stimme abgebildet wird, die du hast.

```python theme={"system"}
DIALOGUE_SCHEMA = {
    "type": "json_schema",
    "json_schema": {
        "name": "dialogue",
        "strict": True,
        "schema": {
            "type": "object",
            "additionalProperties": False,
            "required": ["turns"],
            "properties": {
                "turns": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "additionalProperties": False,
                        "required": ["speaker", "text"],
                        "properties": {
                            "speaker": {"type": "string", "enum": list(HOSTS)},
                            "text": {"type": "string"},
                        },
                    },
                }
            },
        },
    },
}


def write_script(turns=16):
    """Ein Chat-Modell nach einem Zwei-Host-Dialog fragen, verankert in den Quellen."""
    spread = chunks[:: max(1, len(chunks) // 12)][:12]
    notes = "\n\n".join(f"{c['title']}\n{c['text']}" for c in spread)
    hosts = " and ".join(HOSTS)
    raw = chat(
        [
            {"role": "system", "content": (
                f"You write podcast dialogue for two hosts, {hosts}. Ground every statement in "
                "the supplied notes. Write for the ear: no markdown, no URLs, no bracket "
                "citations, no stage directions. Spell out abbreviations the first time they "
                "appear. Vary the length of turns. Open with a hook and close with a takeaway.")},
            {"role": "user", "content": f"Notes:\n\n{notes}\n\nWrite about {turns} turns."},
        ],
        temperature=0.7,
        response_format=DIALOGUE_SCHEMA,
    )
    return json.loads(raw)["turns"]
```

Der Überblick deckt die Quellen breit ab, statt eine einzelne Frage zu beantworten, weshalb `spread` Chunks über die gesamte Sammlung sampelt, anstatt per Ähnlichkeit abzurufen. Jeden n-ten Chunk zu nehmen ist grob und funktioniert gut: Es erreicht das Ende langer Dokumente, was das Nehmen der ersten zwölf nie tun würde.

Behandle die Turn-Anzahl als Hinweis, nicht als Anweisung. Sechzehn anzufordern hat hier je nach Materialfülle zwischen sechzehn und achtundzwanzig Turns geliefert. Wenn du eine harte Obergrenze brauchst, kürze `turns` vor dem Rendern, statt mit dem Prompt zu diskutieren.

## Zwei Stimmen in eine Spur rendern

Jeder Turn wird zu einer Speech-Anfrage, mit der Stimme, die vom Sprechenden bestimmt wird.

```python theme={"system"}
def speak(turn):
    response = requests.post(
        f"{BASE_URL}/audio/speech",
        headers=HEADERS,
        json={"model": TTS_MODEL, "voice": HOSTS[turn["speaker"]],
              "input": turn["text"], "response_format": "wav"},
        timeout=300,
    )
    response.raise_for_status()
    with wave.open(io.BytesIO(response.content)) as clip:
        return clip.getparams(), clip.readframes(clip.getnframes())
```

Die Frames aus jedem Clip herauszulesen, statt zwanzig Dateien zu speichern und sie hinterher zusammenzuflicken, ist das, was den Übergang sauber hält. Codiertes Audio wie MP3 zu verketten funktioniert nicht zuverlässig, weil jede Datei ihre eigenen Header trägt. Dekodierte Frames sind einfach Samples — sie zu verbinden ist ein Aneinanderhängen von Bytes.

Zwei Details lassen das Ergebnis absichtlich klingen. Der Header für die Ausgabe stammt vom ersten Clip statt von Konstanten, sodass die Sample-Rate immer für das gewählte Modell stimmt. Und eine Viertelsekunde Stille zwischen den Turns gibt dem Ohr einen Takt, um zu registrieren, dass der Sprecher gewechselt hat. Ohne sie reden die Hosts einander in die Schlussworte.

```python theme={"system"}
def audio_overview(turns, path="overview.wav", pause_seconds=0.25):
    with ThreadPoolExecutor(max_workers=4) as pool:
        rendered = list(pool.map(speak, turns))

    params = rendered[0][0]
    silence = b"\x00" * int(params.framerate * params.sampwidth * params.nchannels * pause_seconds)
    with wave.open(path, "wb") as out:
        out.setnchannels(params.nchannels)
        out.setsampwidth(params.sampwidth)
        out.setframerate(params.framerate)
        for position, (_, frames) in enumerate(rendered):
            if position:
                out.writeframes(silence)
            out.writeframes(frames)
    return path
```

`pool.map` bewahrt die Eingabereihenfolge, sodass die Turns in der Reihenfolge zurückkommen, in der sie geschrieben wurden, egal welcher zuerst fertig ist. Vier Worker sind eine bewusste Obergrenze und kein Maximum: Mehr Parallelität liefert auf niedrigeren Tiers 429-Fehler zurück, und der Job ist ohnehin vom längsten einzelnen Turn dominiert.

## Ausführen

```python theme={"system"}
if __name__ == "__main__":
    add_source("Venice Privacy", "https://docs.venice.ai/overview/privacy")
    add_source("TEE and E2EE Models", "https://docs.venice.ai/guides/features/tee-e2ee-models")
    add_source("VVV and DIEM", "https://docs.venice.ai/overview/vvv-diem")

    answer, cited = ask("How does Venice keep my prompts private, and what do I give up?")
    print(answer)
    print("\nSources:", ", ".join(f"[{s['number']}] {s['title']}" for s in cited))

    turns = write_script()
    print(f"\nWriting {len(turns)} turns to overview.wav")
    audio_overview(turns)
```

```bash theme={"system"}
python notebook.py
```

```
[1] Venice Privacy: 7507 characters, 7 chunks
[2] TEE and E2EE Models: 43859 characters, 40 chunks
[3] VVV and DIEM: 9609 characters, 11 chunks

Venice's privacy architecture is built around a proxy foundation. All requests pass
through Venice over HTTPS and are relayed to the model provider without Venice storing
your prompt or response content [1]. On top of that proxy, each model offers one of four
progressively stronger privacy modes [1]...

Sources: [1] Venice Privacy

Writing 23 turns to overview.wav
```

Das Einlesen und Beantworten dauert wenige Sekunden. Das Audio ist der langsame Teil und variiert mit der Last: Etwa sechs Minuten Sprache brauchen zwischen einer halben und drei Minuten zum Rendern.

## Mach es zu deinem

**Die Quellen sind das ganze Spiel.** Alles Nachgelagerte ist begrenzt durch das, was du hineinsteckst. Gescrapete Seiten bringen ihre Navigation und Footer mit, was fürs Beantworten harmlos ist, aber in einem Überblick als Host auftaucht, der ernsthaft ein Doku-Verzeichnis diskutiert. Wenn das passiert, wirf Chunks unter einer Längenschwelle raus oder filtere offensichtliches Möblierungsmaterial vor dem Embedden.

**Tausche die Stimmen aus.** `HOSTS` sind zwei Einträge in einem Dictionary. `tts-xai-v1` bringt sechsundzwanzig Stimmen mit, und andere Familien haben ihre eigenen; `GET /models?type=tts` listet `voices` pro Modell. Zwei Stimmen, die klar kontrastieren, sind leichter zu verfolgen als zwei, die lediglich verschieden sind.

**Klone deine eigene.** [Voice Cloning](/guides/media/voice-cloning) verwandelt eine kurze Probe in einen Voice-Handle, den du direkt in `HOSTS` einsetzen kannst.

**Füge eine dritte Person hinzu.** Nichts in der Pipeline setzt zwei Sprecher voraus außer dem Schema-`enum`. Einen Interviewer hinzuzufügen, der nur Fragen stellt, verändert das Gefühl erheblich.

**Bewahre das Skript auf.** `turns` in eine JSON-Datei neben dem Audio zu schreiben, kostet zwei Zeilen und erspart dir einen erneuten Render, wann immer du einen einzelnen Satz feinjustieren willst.

## Wie es weitergeht

<CardGroup cols={2}>
  <Card title="Privater RAG-Bot" icon="database" href="/learn/private-rag-bot">
    Dieselbe Retrieval-Pipeline mit einer echten Vektordatenbank und Re-Ranking.
  </Card>

  <Card title="Zitierte Antworten mit Websuche" icon="search" href="/guides/tools/cited-web-answers">
    Die Quellen automatisch finden, statt sie selbst zu benennen.
  </Card>

  <Card title="Text-to-Speech" icon="microphone" href="/guides/media/text-to-speech">
    Referenz zum Speech-Endpoint, seinen Stimmen und Streaming.
  </Card>

  <Card title="Dokumentenverarbeitung" icon="file-text" href="/guides/tools/document-processing">
    Alles, was der Text-Parser akzeptiert, und was er zurückliefert.
  </Card>
</CardGroup>
