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

# بناء دفتر بحث صوتي

> حوّل مصادرك إلى إجابات موثّقة بالاستشهادات ونظرة عامة صوتية بمُقدِّمَين باستخدام 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" />

أدواتٌ مثل NotebookLM غيّرت ما يتوقعه الناس من كومة من الأبحاث. تُضيف مصادر، فتطرح أسئلة وتحصل على إجابات تُشير مرجعًا إلى المادة، ثم تُولِّد محادثة بين مُقدِّمَين يمكنك الاستماع إليها أثناء المشي.

هذا الدليل يبني ذلك في نحو مئتَي سطر من Python، فوق خمس نقاط نهاية من Venice. لا شيء يُخزَّن خارج جهازك سوى الطلبات نفسها، وVenice لا يحتفظ بها.

<Card title="Run this notebook in Google Colab" icon="notebook" href="https://colab.research.google.com/github/veniceai/api-docs/blob/main/notebooks/audio-research-notebook.ipynb">
  كل خطوة أدناه على هيئة دفتر قابل للتنفيذ، مع تشغيل النظرة العامة داخل الصفحة. لا شيء لتثبيته.
</Card>

## كيف يعمل

خمس نقاط نهاية، لكلٍّ منها مهمة واحدة:

| الخطوة                 | نقطة النهاية           | لماذا                                          |
| ---------------------- | ---------------------- | ---------------------------------------------- |
| قراءة صفحة ويب         | `/augment/scrape`      | يُعيد Markdown، لا HTML يحتاج إلى تنظيف        |
| قراءة ملف PDF أو مستند | `/augment/text-parser` | رفع multipart واحد، ويعود نصًّا                |
| فهرسة النص             | `/embeddings`          | يتيح لك الاسترجاع بالمعنى بدل الكلمة المفتاحية |
| الإجابة عن الأسئلة     | `/chat/completions`    | مُؤصَّلة في المقاطع المسترجعة، مع استشهادات    |
| نُطق النظرة العامة     | `/audio/speech`        | صوتان، واحد لكل مُقدِّم                        |

الاسترجاع هنا بسيط عن قصد: متجهات في قائمة Python، وتشابه جيب التمام في حلقة. هذا هو القدر الصحيح من الآليات لبضع عشرات من المصادر، ويُبقي الأجزاء المتحركة ظاهرة. حين تفوق ذلك، يغطي [بناء روبوت RAG خاص](/learn/private-rag-bot) الخطَّ نفسه بقاعدة بيانات متجهات حقيقية ومَرحلة إعادة ترتيب.

## الإعداد

اعتماديّة واحدة، ومفتاح من [صفحة إعدادات API](/guides/getting-started/generating-api-key).

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

أنشئ ملف `notebook.py` وابدأ بالاستيرادات والتهيئة. القائمتان في الأسفل هما كامل حالة الدفتر: `sources` يسجّل ما أضفتَه، و`chunks` يحمل القطع القابلة للبحث.

```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` اسم كل مُقدِّم بصوت. كلا الصوتين ينتميان إلى `tts-xai-v1`، وهذا مهم: الأصوات تخص النماذج، وإرسال صوت من عائلة إلى نموذج من عائلة أخرى هو أشيع خطأ أول مع نقطة نهاية الصوت.

## اختيار نموذج لن يتقادم

ترميز نموذج دردشة داخل مشروع يضمن تقادُم المشروع. تنشر Venice أيَّ نموذج يحمل حاليًا كل دور عبر `/models/traits`، فيمكنك طلب الافتراضي الحالي بدل تسمية نموذج بعينه.

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

تتوفر سمات أخرى إن لم يكن هذا الدفتر بالشكل الذي تريده. `most_intelligent` يمنحك نموذجًا أقوى للتلخيص الكثيف بالاستدلال، و`default_reasoning` يمنحك واحدًا يفكّر بشكل مكشوف. راجع [النماذج](/models/overview) للقائمة الكاملة.

## إضافة المصادر

المصدر إمّا رابط URL أو ملفٌ على القرص، ولدى Venice نقطة نهاية لكل منهما. كلاهما يُعيد نصًّا خامًا، وهذا هو المقصود: بقيّة الدفتر لا تهتم من أين جاء المصدر.

```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` نصَّ Markdown بدل HTML الخام، فلا حاجة لكتابة كود لتجريد القوالب. يقبل `/augment/text-parser` ملفات PDF وWord وExcel والنص العادي حتى 25 ميغابايت، ويُبلّغ عن عدد الرموز إلى جانب النص. يغطّي [معالجة المستندات](/guides/tools/document-processing) خياراته بالكامل.

## التقطيع والتضمين

تضمين مستند كامل يُنتج متجهًا واحدًا هو معدَّل كل ما يقوله، وهذا أعمى من أن يسترجع ادعاءً بعينه. تقطيعه يُنتج متجهات يعني كلٌّ منها شيئًا.

قسِّم على حدود الفقرات لا على عدد أحرف ثابت. القطعة التي تتوقف في منتصف جملة تُسترجع رديئًا، لأن التضمين لجزء مبتور.

```python theme={"system"}
def split(text, limit=1200):
    """يحزم الفقرات في قطع دون تقطيع أيٍّ منها إلى نصفين."""
    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` الطلبات لأن نقطة النهاية تأخذ قائمة، وطلبٌ واحد لأربع وستّين قطعة أقلّ تكلفةً في الزمن الفعلي بكثير من أربعة وستّين طلبًا. يعيد `text-embedding-bge-m3` متجهًا بـ1024 بُعدًا ويتعامل جيدًا مع المصادر متعددة اللغات.

إضافة مصدر صارت الآن: اقرأ، وقسِّم، وضمِّن، وسجِّل. تُخزَّن سعة كل متجه إلى جانبه، لأنها لا تتغير أبدًا وإعادة حسابها داخل حلقة التشابه هدرٌ للجهد.

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

`number` هو ما يجعل الاستشهاد ممكنًا لاحقًا. كل قطعة تتذكر أيَّ مصدر جاءت منه، فيمكن للإجابة أن تُشير مرجعًا إليه.

## استرجاع المقاطع الصحيحة

تشابه جيب التمام بين متجه السؤال وكل متجه قطعة، ثم فرزٌ، ثم أفضل k. لبضعة آلاف من القطع، يعمل هذا أسرع من الاستدعاء الشبكي الذي أنتج متجه السؤال.

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

## الإجابة مع الاستشهادات

الفرق بين إجابة مؤصَّلة وتخمين واثق يكمن كليًّا في التعليمة. تعليمتان تؤدّيان العمل: أجِب فقط من الملاحظات، وقُل ذلك حين تعجز الملاحظات. من دون الثانية سيملأ النموذج الفراغ بهدوء من الذاكرة، وهذا هو نمط الفشل الذي تحاول تفاديه بالتصميم.

ترقيم الملاحظات في التعليمة يمنح النموذج مفردات استشهاد. يكتب `[2]`، فتستطيع أنت أن تُرجع ذلك إلى مصدر.

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

تحليل الأقواس مرة أخرى يستحق السطر الواحد. فهو يخبرك أيّ المصادر حملت الإجابة فعلًا، وبذلك تلاحظ أن مصدرًا ظننتَه محوريًّا لا يُستشهد به أبدًا.

## كتابة نصّ النظرة العامة

هنا يتوقف الدفتر عن أن يكون صندوق بحث. الملخّص شيء تقرأه؛ والنظرة العامة شيء تستمع إليه، وكلٌّ منهما يريد نثرًا مختلفًا. الحوار يعمل بشكل أفضل في الصوت لأن تبادل الأدوار يضبط الإيقاع نيابةً عنك، وسؤالٌ من أحد المُقدِّمَين طريقة طبيعية لتقديم الفكرة التالية.

ثلاثة قيود تهم، وكلها تأتي من الصوت لا من النص:

* **لا Markdown ولا روابط URL.** نموذج الكلام يقرأ `https://docs.venice.ai` حرفًا حرفًا.
* **اهجِ الاختصارات.** *T E E* في المرة الأولى، لا *tee*.
* **نوِّع طول المقاطع.** المقاطع المتساوية تبدو وكأن شخصَين يقرآن قائمة أحدهما للآخر.

طلب JSON بمُخطَّط هو ما يجعل النتيجة قابلة للتصيير. النص الحرّ يحتاج إلى تحليل، وتسميات المتحدثين هي بالضبط ما يُبدع فيه النموذج. القيد `enum` على `speaker` يعني أن كل مقطع يُقابل صوتًا لديك.

```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):
    """يطلب من نموذج دردشة حوارًا بمُقدِّمَين مُؤصَّلًا في المصادر."""
    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"]
```

تُغطّي النظرة العامة المصادر بشكل عريض بدل الإجابة عن سؤال واحد، لذا يأخذ `spread` عيّنة من القطع عبر المجموعة كلها بدل الاسترجاع بالتشابه. أخذ كل قطعة رقم n خامٌ لكنه يعمل جيدًا: فهو يبلغ نهاية المستندات الطويلة، وأخذ الاثنتَي عشرة الأولى ما كان ليصل إليها.

عامِل عدد المقاطع كتلميح لا كتعليمة. طلبُ ستة عشر أنتج هنا ما بين ستة عشر وثمانية وعشرين، بحسب ما لدى المصادر لتقوله. إن احتجتَ سقفًا صارمًا، فاقتطع `turns` قبل التصيير بدل مجادلة التعليمة.

## تصيير صوتَين إلى مسار واحد

يصبح كل مقطع طلب كلام واحدًا، والصوت يُختار بحسب من يتكلم.

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

قراءة الإطارات من كل مقطع، بدل حفظ عشرين ملفًّا وخياطتها لاحقًا، هي ما يُبقي التوصيل نظيفًا. تسلسل الصوت المُشفَّر مثل MP3 لا يعمل بشكل موثوق، لأن كل ملف يحمل ترويسته الخاصة. الإطارات المفكوكة مجرد عيّنات، فربطها هو إلحاق بايتات.

تفصيلان يجعلان النتيجة تبدو مقصودة. ترويسة المُخرج تأتي من أول مقطع لا من ثوابت، فيكون معدَّل العيّنات صحيحًا دائمًا مع أيِّ نموذج اخترتَه. وربع ثانية من الصمت بين المقاطع يعطي الأذن مهلةً لتُدرك أن المتحدث تغيَّر. من دونه يتحدث المُقدِّمان فوق نهايات بعضهما.

```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` ترتيب المُدخلات، فتعود المقاطع بالترتيب الذي كُتبت به مهما بلغ أيُّها الأول. أربعة عاملين سقفٌ متعمَّد لا حدٌّ أقصى: تزامُنٌ أكبر سيبدأ بإعادة رموز 429 على الطبقات الأدنى، والمهمة أصلًا يهيمن عليها أطول مقطع منفرد.

## تشغيله

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

الاستيعاب والإجابة يستغرقان ثوانيَ قليلة. الصوت هو الجزء البطيء، ويتفاوت مع الحمل: نحو ست دقائق من الكلام تستغرق ما بين نصف دقيقة وثلاث دقائق للتصيير.

## اجعله لك

**المصادر هي اللعبة كلها.** كل ما يأتي بعد ذلك محدود بما أدخلتَه. الصفحات المستخرَجة تجلب معها شريط التنقل والتذييل، وهذا غير ضارٍّ للإجابة لكنه يظهر في النظرة العامة على شكل مُقدِّم يناقش بجدٍّ فهرس توثيق. إن حدث ذلك، فأسقط القطع تحت عتبة طول أو رشِّح الأثاث الواضح قبل التضمين.

**استبدل الأصوات.** `HOSTS` هو مُدخلان في قاموس. يشحن `tts-xai-v1` ستة وعشرين صوتًا، وللعائلات الأخرى أصواتها؛ `GET /models?type=tts` يُدرج `voices` لكل نموذج. صوتان يتباينان بوضوح أسهل في المتابعة من صوتَين مختلفَين فقط.

**استنسِخ صوتك.** يحوّل [استنساخ الأصوات](/guides/media/voice-cloning) عيّنة قصيرة إلى مقبض صوت يمكنك إسقاطه مباشرة في `HOSTS`.

**أضف مشاركًا ثالثًا.** لا شيء في المسار يفترض متحدثَين إلا `enum` المُخطَّط. إضافة مُحاوِر يطرح الأسئلة فقط تُغيّر الإحساس تغييرًا كبيرًا.

**احفظ النص.** كتابة `turns` إلى ملف JSON إلى جوار الصوت تكلّف سطرَين وتوفّر إعادة تصيير كلَّ مرة تريد فيها تعديل جملة واحدة.

## إلى أين تذهب بعد ذلك

<CardGroup cols={2}>
  <Card title="روبوت RAG خاص" icon="database" href="/learn/private-rag-bot">
    خط الاسترجاع نفسه بقاعدة بيانات متجهات حقيقية وإعادة ترتيب.
  </Card>

  <Card title="إجابات موثّقة مع بحث الويب" icon="search" href="/guides/tools/cited-web-answers">
    اعثر على المصادر تلقائيًا بدل تسميتها بنفسك.
  </Card>

  <Card title="النص إلى كلام" icon="microphone" href="/guides/media/text-to-speech">
    مرجع لنقطة نهاية الكلام وأصواتها والبثّ.
  </Card>

  <Card title="معالجة المستندات" icon="file-text" href="/guides/tools/document-processing">
    كل ما يقبله محلِّل النصوص وما يعيده.
  </Card>
</CardGroup>
