> ## 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.

# Costruire un notebook di ricerca audio

> Trasforma le tue fonti in risposte con citazioni e in una panoramica audio a due voci con Venice.

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" />

Strumenti come NotebookLM hanno cambiato ciò che le persone si aspettano da una pila di materiale di ricerca. Aggiungi delle fonti, poni domande e ottieni risposte che rimandano al materiale, e poi generi una conversazione fra due voci che puoi ascoltare mentre passeggi.

Questa guida costruisce quello, in circa duecento righe di Python, su cinque endpoint Venice. Nulla viene archiviato al di fuori della tua macchina tranne le richieste stesse, e Venice non le conserva.

<Card title="Esegui questo notebook su Google Colab" icon="notebook" href="https://colab.research.google.com/github/veniceai/api-docs/blob/main/notebooks/audio-research-notebook.ipynb">
  Ogni passaggio qui sotto come notebook eseguibile, con la panoramica riprodotta in linea. Nulla da installare.
</Card>

## Come funziona

Cinque endpoint, ciascuno con un compito preciso:

| Passaggio                     | Endpoint               | Perché                                                                 |
| ----------------------------- | ---------------------- | ---------------------------------------------------------------------- |
| Leggere una pagina web        | `/augment/scrape`      | Restituisce Markdown, non HTML da ripulire                             |
| Leggere un PDF o un documento | `/augment/text-parser` | Un upload multipart, testo di ritorno                                  |
| Indicizzare il testo          | `/embeddings`          | Ti permette di recuperare per significato invece che per parola chiave |
| Rispondere alle domande       | `/chat/completions`    | Ancorato ai passaggi recuperati, con citazioni                         |
| Pronunciare la panoramica     | `/audio/speech`        | Due voci, una per conduttore                                           |

Il recupero qui è volutamente essenziale: vettori in una lista Python, similarità coseno in un ciclo. È la giusta quantità di infrastruttura per qualche decina di fonti e mantiene visibili le parti in movimento. Quando questa impostazione ti sta stretta, [Creare un bot RAG privato](/learn/private-rag-bot) copre la stessa pipeline con un vero database vettoriale e un passaggio di ri-ordinamento.

## Configurazione

Una dipendenza, e una chiave dalla [pagina delle impostazioni API](/guides/getting-started/generating-api-key).

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

Crea `notebook.py` e comincia con gli import e la configurazione. Le due liste in fondo rappresentano tutto lo stato del notebook: `sources` registra ciò che hai aggiunto, e `chunks` contiene i pezzi ricercabili.

```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` mappa il nome di un conduttore a una voce. Entrambe le voci provengono da `tts-xai-v1`, e questo conta: le voci appartengono ai modelli, e inviare una voce di una famiglia a un modello di un'altra è l'errore iniziale più comune con l'endpoint di sintesi vocale.

## Scegliere un modello che non diventerà obsoleto

Codificare a mano un modello di chat in un progetto garantisce che il progetto invecchi. Venice pubblica quale modello ricopre attualmente ciascun ruolo tramite `/models/traits`, così puoi richiedere il default corrente invece di nominarne uno.

```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"]
```

Sono disponibili altri trait se questo notebook non ha la forma che desideri. `most_intelligent` ti offre un modello più potente per la riassunzione ricca di ragionamento, e `default_reasoning` te ne dà uno che pensa apertamente. Vedi [Modelli](/models/overview) per l'elenco completo.

## Aggiungere fonti

Una fonte è un URL o un file su disco, e Venice ha un endpoint per ciascuno. Entrambi restituiscono testo semplice, che è proprio il punto: il resto del notebook non si preoccupa di dove provenga una fonte.

```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` restituisce Markdown invece di HTML grezzo, quindi non c'è nessun boilerplate da rimuovere. `/augment/text-parser` accetta PDF, Word, Excel e testo semplice fino a 25 MB, e riporta un conteggio dei token insieme al testo. [Elaborazione dei documenti](/guides/tools/document-processing) copre le sue opzioni per intero.

## Suddivisione in chunk ed embedding

Fare l'embedding di un intero documento produce un unico vettore che è una media di tutto ciò che il documento dice, troppo grossolano per recuperare un'affermazione specifica. Suddividerlo produce vettori che significano ciascuno qualcosa.

Dividi ai confini di paragrafo invece che a un conteggio fisso di caratteri. Un chunk che si interrompe a metà frase viene recuperato male, perché l'embedding è di un frammento.

```python theme={"system"}
def split(text, limit=1200):
    """Impacchetta i paragrafi in chunk senza tagliarne uno a metà."""
    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` fa il batching perché l'endpoint accetta una lista, e una singola richiesta per sessantaquattro chunk è molto più economica in termini di tempo di attesa rispetto a sessantaquattro richieste. `text-embedding-bge-m3` restituisce 1024 dimensioni e gestisce bene le fonti multilingue.

Aggiungere una fonte ora significa leggere, dividere, fare embedding e registrare. La magnitudine di ogni vettore viene memorizzata accanto ad esso, perché non cambia mai e ricalcolarla dentro il ciclo di similarità è uno spreco.

```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")
```

Il `number` è ciò che rende possibile la citazione più avanti. Ogni chunk ricorda da quale fonte proviene, così una risposta può rimandare ad essa.

## Recuperare i passaggi giusti

Similarità coseno fra il vettore della domanda e ogni vettore chunk, ordinati, primi k. Per qualche migliaio di chunk questo procedimento è più veloce della chiamata di rete che ha prodotto il vettore della domanda.

```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]
```

## Rispondere con citazioni

La differenza fra una risposta ancorata e un'ipotesi sicura di sé è tutta nel prompt. Due istruzioni fanno il lavoro: rispondi solo dalle note, e dillo quando le note non bastano. Senza la seconda un modello riempirà silenziosamente la lacuna dalla memoria, che è la modalità di errore che stai cercando di progettare via.

Numerare le note nel prompt fornisce al modello un vocabolario di citazione. Scrive `[2]`, e tu puoi risolverlo di nuovo verso una fonte.

```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]
```

Estrarre di nuovo le parentesi vale la riga in più. Ti dice quali fonti hanno effettivamente sostenuto la risposta, che è come noti quando una fonte che credevi centrale non viene mai citata.

## Scrivere il copione della panoramica

Qui il notebook smette di essere una casella di ricerca. Un riassunto è qualcosa che leggi; una panoramica è qualcosa che ascolti, e le due cose richiedono una prosa diversa. Il dialogo funziona meglio in audio perché l'alternanza dei turni scandisce il ritmo al posto tuo, e una domanda di un conduttore è un modo naturale per introdurre l'idea successiva.

Tre vincoli contano, e tutti e tre derivano dall'audio piuttosto che dal testo:

* **Niente markdown, niente URL.** Un modello di sintesi vocale legge `https://docs.venice.ai` un carattere alla volta.
* **Sciogli le abbreviazioni.** *T E E* la prima volta, non *tii*.
* **Varia la lunghezza dei turni.** Turni di dimensione uniforme suonano come due persone che si leggono un elenco a vicenda.

Chiedere JSON con uno schema è ciò che rende il risultato renderizzabile. Il testo libero richiederebbe parsing, e le etichette dei parlanti sono esattamente la cosa su cui un modello si mette a fare il creativo. L'`enum` su `speaker` fa sì che ogni turno mappi a una voce che possiedi.

```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):
    """Chiedi a un modello di chat un dialogo a due conduttori ancorato alle fonti."""
    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"]
```

La panoramica copre le fonti in modo ampio invece di rispondere a una singola domanda, quindi `spread` campiona chunk lungo tutta la collezione invece di recuperarli per similarità. Prendere ogni n-esimo chunk è grezzo e funziona bene: raggiunge la fine di documenti lunghi, cosa che prendere i primi dodici non farebbe mai.

Tratta il conteggio dei turni come un suggerimento invece che un'istruzione. Chiederne sedici ha prodotto qui qualsiasi cifra fra sedici e ventotto, a seconda di quanto le fonti hanno da dire. Se ti serve un tetto rigido, tronca `turns` prima del rendering invece di discutere con il prompt.

## Rendere due voci in una sola traccia

Ogni turno diventa una richiesta di sintesi vocale, con la voce scelta in base a chi sta parlando.

```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())
```

Leggere i frame di ogni clip, invece di salvare venti file e concatenarli dopo, è ciò che mantiene pulito il giunto. Concatenare audio codificato come MP3 non funziona in modo affidabile, perché ogni file porta con sé le proprie intestazioni. I frame decodificati sono solo campioni, quindi unirli equivale ad accodare byte.

Due dettagli fanno sì che il risultato suoni intenzionale. L'intestazione dell'output proviene dalla prima clip invece che da costanti, così la frequenza di campionamento è sempre quella giusta per il modello che hai scelto. E un quarto di secondo di silenzio fra i turni dà all'orecchio un istante per registrare che chi parla è cambiato. Senza di esso i conduttori si sovrappongono alla fine dei rispettivi interventi.

```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` conserva l'ordine di input, quindi i turni tornano nell'ordine in cui sono stati scritti indipendentemente da quale finisce prima. Quattro worker sono un tetto deliberato piuttosto che un massimo: più concorrenza inizierà a restituire 429 sui tier più bassi, e il job è già dominato dal singolo turno più lungo.

## Eseguire il tutto

```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
```

L'ingestione e la risposta richiedono pochi secondi. L'audio è la parte lenta e varia con il carico: circa sei minuti di parlato impiegano da mezzo minuto a tre minuti per essere resi.

## Personalizzarlo

**Le fonti sono tutto il gioco.** Tutto ciò che segue è limitato da ciò che metti dentro. Le pagine sottoposte a scraping portano con sé la loro navigazione e i piè di pagina, cosa innocua per rispondere ma che compare in una panoramica come un conduttore che discute seriamente un indice della documentazione. Se accade, scarta i chunk sotto una certa lunghezza o filtra gli elementi ovvi di contorno prima dell'embedding.

**Cambia le voci.** `HOSTS` sono due voci in un dizionario. `tts-xai-v1` include ventisei voci, e altre famiglie hanno le loro; `GET /models?type=tts` elenca `voices` per ciascun modello. Due voci che contrastano nettamente sono più facili da seguire di due che sono soltanto diverse.

**Clona la tua.** [Clonazione della voce](/guides/media/voice-cloning) trasforma un breve campione in un handle vocale che puoi inserire direttamente in `HOSTS`.

**Aggiungi un terzo partecipante.** Nulla nella pipeline presuppone due parlanti tranne l'`enum` dello schema. Aggiungere un intervistatore che pone solo domande cambia notevolmente la sensazione.

**Conserva il copione.** Scrivere `turns` in un file JSON accanto all'audio costa due righe e ti risparmia un re-rendering ogni volta che vuoi ritoccare una frase.

## Dove andare adesso

<CardGroup cols={2}>
  <Card title="Bot RAG privato" icon="database" href="/learn/private-rag-bot">
    La stessa pipeline di recupero con un vero database vettoriale e ri-ordinamento.
  </Card>

  <Card title="Risposte citate con ricerca web" icon="search" href="/guides/tools/cited-web-answers">
    Trova le fonti automaticamente invece di indicarle tu stesso.
  </Card>

  <Card title="Sintesi vocale" icon="microphone" href="/guides/media/text-to-speech">
    Riferimento per l'endpoint di sintesi vocale, le sue voci e lo streaming.
  </Card>

  <Card title="Elaborazione dei documenti" icon="file-text" href="/guides/tools/document-processing">
    Tutto ciò che accetta il text parser e ciò che restituisce.
  </Card>
</CardGroup>
