Знакът на КАГАМИ КАГАМИ
kagami.bg/academy · lesson · machine-readable viewUPDATED 2026-10-03
IDENTITY
module
GX10-04-168 · Knowledge atoms: automatic atomization of text
series
GX10 (local AI server class: NVIDIA GB10, e.g. ASUS Ascent GX10 / DGX Spark)
level
Advanced
duration
2–3 h
prerequisites
Python 3.10+, Ollama, optional ffmpeg for audio
trust_label
UPDATED 2026-10-03 · NOT TESTED (no GB10-class machine available; scripts only syntax-checked; docs compared) · not yet VERIFIED
versions_seen
chromadb 1.5.9 · openai-whisper 20250625 · embeddings via Ollama /api/embed · bge-m3 (1024 dim, 8192 tokens, MIT)
language
human view: bg · english edition: /en/academy/gx10/ (same file name)
previous / next
GX10 series index / GX10 series index
PURPOSE

Build a small local pipeline that turns transcripts and documents into knowledge atoms: optional Whisper transcription, segmentation by a local LLM with a JSON schema, duplicate detection and link suggestion with bge-m3 embeddings in a Chroma cosine index, a faithfulness-aware LLM score used as triage, and a human review folder. Thresholds are starting values to calibrate on own data.

KEY CONCEPTS
COMMANDS / PATHS
CHECKLIST
NEXT MODULE

GX10 series index: kagami.bg/academy/gx10/ · related: 04-113 hybrid search (bge-m3 + pgvector), 04-84 local RAG · offer: Quick experiment (kagami.bg/stalbata/)

SOURCES
TAGS
gx10nvidia-gb10knowledge-atomsbge-m3chromadbollamawhisperdeduplication
ОБНОВЕНО · 03.10.2026

Знаниеви атоми: автоматично разбиване на текст на самостоятелни единици

Среща, статия или имейл съдържат десетки отделни мисли, смесени в един поток. Знаниеви атом е една такава мисъл — решение, факт, метод — записана така, че да се разбира сама. Тук строим малък локален конвейер, който ги изважда автоматично, пази ги като обикновени файлове с метаданни и не допуска дубликати. Моделът върви на твоята машина, затова текстовете не напускат мрежата ти.

⏱ 2–3 чНапредналиGX10Whisper · Ollama · bge-m3 · ChromaPython
Whisper large-v3 (транскрипция)🔒 локалноOllama: езиков модел и bge-m3🔒 локалноChromaDB (индекс)🔒 локално
🔄
ОБНОВЕНО · 03.10.2026 — какво
Урокът е преработен като общ модел. Махнахме примерите и числата, които описваха конкретна вътрешна база знания (брой атоми, имена на области, идентификатори, пътища), и твърденията за скорост и спестено време („12 пъти по-бързо от реално време“, „под 50 ms“, „8 минути вместо 2–3 часа“) — не са измервани и не са източник. Заменихме остарелия адрес /api/embeddings с текущия /api/embed, а търсенето на скоби в отговора на модела — със структуриран изход (format със схема). Поправихме грешките в стария код: новите атоми никога не влизаха в индекса, името на файла получаваше двоен префикс, а флагът „прегледан“ беше обърнат. Добавихме: стъпка за сверяване с изходния текст (оценка за вярност), папка за атоми, чакащи човек, и честна бележка, че праговете 0,97 и 0,92 са отправна точка, не измерен факт.
⚠️
Какво не сме пускали сами
Нито един скрипт от този урок не е пускан от нас на машина от класа GB10. Не сме мерили скорост, точност на сегментирането, подходящи прагове за български текст или качеството на оценката от езиков модел. Кодът е проверен само за синтаксис и сверен с документацията на използваните инструменти. Затова няма етикет „ТЕСТВАНО“, а „ПРОВЕРЕНО“ не е поставено, защото не сме сверили всяка команда на машина.

01Какво ще научиш

02Преди да започнеш

💡
Защо локално
Транскриптът на среща често съдържа неща, които не искаш в чужд облак. Тук и вграждането, и езиковият модел, и индексът са на твоята машина (🔒). Паметта на машината е обща за процесора и видеокартата, затова не пускай няколко големи модела наведнъж.

03Стъпки

  1. Идеята: какво е атом

    Атомът е едно твърдение, което може да се цитира отделно и пак да има смисъл. Не: „Обсъдихме много теми.“ Да: „На прегледа на спринта екипът реши седмичният отчет да се праща в понеделник, защото повечето отговори от клиенти идват през почивните дни.“ Втората версия казва кой, кога и какво. Ориентир: 50–250 думи, но смисълът е важен, не броят.

    ПолеЗа какво служи
    idстабилен идентификатор (ATM-0001…) за връзки
    title, typeкратко заглавие и вид (решение, факт, метод…)
    linksидентификатори на близки атоми, предложени от сходството
    qualityгруб триаж от езиковия модел; не е истина
    reviewedдали човек го е прегледал
  2. Конвейерът: пет стъпки

    Текст → (по избор) транскрипция → разбиване на атоми → сравнение с вече познатите → предложения за връзки → оценка и запис. Всяка стъпка е отделна функция, затова можеш да я тестваш и подменяш поотделно. Защо в такъв ред: дубликатите се отсяват преди оценката, за да не плащаш време на модела за атом, който и без това ще отпадне.

  3. Подготви средата

    Виртуална среда и два модела: bge-m3 дава вектори (1024 измерения, над 100 езика, до 8192 токена вход, лиценз MIT — по картата му в Hugging Face), а llama3.1:8b е примерен езиков модел. Можеш да подадеш друг през променливата ATOM_MODEL. ⚠️ Качеството на 8B модел върху български не сме мерили.

    bash · на машината
    mkdir -p ~/atoms-lab && cd ~/atoms-lab
    python3 -m venv .venv && . .venv/bin/activate
    pip install chromadb httpx pyyaml openai-whisper
    
    ollama pull bge-m3
    ollama pull llama3.1:8b
    ollama list
    ℹ️
    Незадължително: аудио
    Пакетът openai-whisper изисква ffmpeg. Името на модела large-v3 е валидно в библиотеката; turbo е по-бърза негова оптимизация, но не превежда. На GB10 е нужна версия на PyTorch с поддръжка на CUDA за Arm — ⚠️ не сме проверявали как се инсталира; без нея транскрипцията върви на процесора и е бавна.
  4. Кодът: един файл

    Целият конвейер е в един файл. Забележи четири решения. (1) format със схема принуждава Ollama да върне валиден JSON — не търсим скоби в текста. (2) options.num_ctx е зададено явно, защото дълъг вход иначе може да бъде отрязан; текстът се реже на части от по 900 думи. (3) В косинусово пространство Chroma връща разстояние = 1 − сходство, затова сходството е 1 − d. (4) Атом отива в индекса само ако е приет; чакащите човек стоят в _review/. Коментарите и подканите в кода са на английски — моделите ги следват надеждно.

    python · atoms.py
    # atoms.py - text -> self-contained knowledge atoms (one file, five stages)
    import hashlib, json, os, re
    from datetime import date
    from pathlib import Path
    
    import chromadb, httpx, yaml
    
    OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
    LLM    = os.environ.get("ATOM_MODEL", "llama3.1:8b")   # any local instruct model
    EMB    = "bge-m3"
    GRAPH  = Path("atoms")
    REVIEW = GRAPH / "_review"
    INDEX  = GRAPH / "_index"
    
    SKIP_AT, REVIEW_AT, PASS_SCORE = 0.97, 0.92, 6        # starting points - calibrate!
    ATOM_TYPES = ["concept", "decision", "fact", "method",
                  "tool", "event", "insight", "question"]
    
    # ---------- stage 1: audio -> text (skip if you already have text) ----------
    _whisper = None
    def transcribe(path, language="bg"):
        global _whisper
        if _whisper is None:
            import whisper                      # pip install openai-whisper; needs ffmpeg
            _whisper = whisper.load_model("large-v3")
        return _whisper.transcribe(path, language=language)["text"]
    
    # ---------- stage 2: text -> candidate atoms (structured output) ----------
    ATOM_SCHEMA = {"type": "object", "required": ["atoms"], "properties": {"atoms": {
        "type": "array", "items": {"type": "object", "required": ["title", "content", "type"],
        "properties": {"title": {"type": "string"}, "content": {"type": "string"},
                       "type": {"type": "string", "enum": ATOM_TYPES},
                       "tags": {"type": "array", "items": {"type": "string"}}}}}}}
    
    PROMPT = """You split a text into knowledge atoms.
    Rules: one atom = one statement, decision or fact that still makes sense when read alone;
    50-250 words; keep the language of the text; do not invent anything that is not in the text;
    write who/when/what explicitly instead of 'we' or 'it'.
    TEXT:
    {text}"""
    
    def parts(text, words=900):
        w = text.split()
        for i in range(0, len(w), words):
            yield " ".join(w[i:i + words])
    
    def extract(text):
        found = []
        for part in parts(text):
            r = httpx.post(f"{OLLAMA}/api/generate", timeout=300, json={
                "model": LLM, "prompt": PROMPT.format(text=part), "stream": False,
                "format": ATOM_SCHEMA, "options": {"temperature": 0.1, "num_ctx": 8192}})
            r.raise_for_status()
            found += json.loads(r.json()["response"]).get("atoms", [])
        return found
    
    # ---------- stage 3 and 4: duplicates and link candidates ----------
    client = chromadb.PersistentClient(path=str(INDEX))
    col = client.get_or_create_collection("atoms", configuration={"hnsw": {"space": "cosine"}})
    
    def embed(text):
        r = httpx.post(f"{OLLAMA}/api/embed", json={"model": EMB, "input": text}, timeout=120)
        r.raise_for_status()
        return r.json()["embeddings"][0]
    
    def nearest(vec, k=3):
        n = col.count()
        if n == 0:
            return []
        res = col.query(query_embeddings=[vec], n_results=min(k, n), include=["metadatas", "distances"])
        # in a cosine space Chroma returns distance = 1 - cosine similarity
        return [{"id": i, "title": m["title"], "similarity": round(1 - d, 4)}
                for i, m, d in zip(res["ids"][0], res["metadatas"][0], res["distances"][0])]
    
    def verdict(top_similarity):
        if top_similarity >= SKIP_AT:   return "skip"      # practically the same atom
        if top_similarity >= REVIEW_AT: return "review"    # similar: a person decides
        return "new"
    
    # ---------- stage 5: quality triage ----------
    SCORE_SCHEMA = {"type": "object", "properties": {k: {"type": "integer", "minimum": 0, "maximum": 2}
        for k in ["atomicity", "completeness", "specificity", "faithfulness", "linkability"]},
        "required": ["atomicity", "completeness", "specificity", "faithfulness", "linkability"]}
    
    def score(atom, source_text):
        prompt = ("Score the atom from 0 to 2 on: atomicity (one statement), completeness (understandable alone), "
                  "specificity (concrete), faithfulness (supported by the source text), linkability.\n"
                  f"SOURCE:\n{source_text[:3000]}\n\nATOM:\n{atom['title']}\n{atom['content']}")
        r = httpx.post(f"{OLLAMA}/api/generate", timeout=120, json={
            "model": LLM, "prompt": prompt, "stream": False, "format": SCORE_SCHEMA,
            "options": {"temperature": 0}})
        r.raise_for_status()
        s = json.loads(r.json()["response"])
        return sum(s.values()), s
    
    # ---------- writing atoms ----------
    def next_id():
        nums = [int(m.group(1)) for p in GRAPH.rglob("ATM-*.md") if (m := re.match(r"ATM-(\d+)", p.name))]
        return f"ATM-{max(nums, default=0) + 1:04d}"
    
    def save(atom, links, total, scores, source, needs_review):
        aid = next_id()
        folder = REVIEW if needs_review else GRAPH
        folder.mkdir(parents=True, exist_ok=True)
        meta = {"id": aid, "title": atom["title"], "type": atom["type"], "created": date.today().isoformat(),
                "source": source, "links": links, "tags": atom.get("tags", []), "quality": total,
                "quality_scores": scores, "auto_generated": True, "reviewed": False}
        text = "---\n" + yaml.safe_dump(meta, allow_unicode=True, sort_keys=False) + "---\n\n"
        text += f"# {atom['title']}\n\n{atom['content']}\n"
        (folder / f"{aid}.md").write_text(text, encoding="utf-8")
        if not needs_review:
            index(aid, atom)
        return aid
    
    def index(aid, atom):
        body = f"{atom['title']}\n\n{atom['content']}"
        col.upsert(ids=[aid], embeddings=[embed(body)], documents=[body],
                   metadatas=[{"title": atom["title"]}])
    
    # ---------- the whole run ----------
    def process(text, source):
        stats = {"saved": 0, "skipped": 0, "to_review": 0}
        for atom in extract(text):
            vec = embed(f"{atom['title']}\n\n{atom['content']}")
            near = nearest(vec)
            v = verdict(near[0]["similarity"] if near else 0.0)
            if v == "skip":
                stats["skipped"] += 1
                continue
            total, scores = score(atom, text)
            links = [n["id"] for n in near if 0.35 <= n["similarity"] < REVIEW_AT]
            needs_review = v == "review" or total < PASS_SCORE
            save(atom, links, total, scores, source, needs_review)
            stats["to_review" if needs_review else "saved"] += 1
        return stats
    
    if __name__ == "__main__":
        import sys
        path = sys.argv[1]
        text = transcribe(path) if path.lower().endswith((".mp3", ".wav", ".m4a", ".flac")) else Path(path).read_text(encoding="utf-8")
        print(process(text, source=Path(path).name))

    Пусни само проверка на синтаксиса, без модели: python -m py_compile atoms.py.

  5. Прагове: откъде идват и как да ги сверяваш

    Кодът пази два прага: 0,97 — „практически същото, пропусни“ и 0,92 — „много близко, реши човек“. Това са отправни стойности, а не измерен факт: сходството зависи от модела, езика и дължината на атомите. Как да ги настроиш: събери 30–50 двойки атоми от свои данни, раздели ги на „същото“, „близко, но различно“ и „различно“, отпечатай сходствата им и сложи праговете между групите. Ако често губиш различни атоми, вдигни прага; ако дубликати минават, свали го.

  6. Пробен запуск

    Запиши измислен текст във файл и го пусни. Очакваш няколко атома в atoms/ или atoms/_review/ и ред със статистика.

    text · note.md (измислен пример)
    # note.md - an invented example text (the lesson's test input)
    In the sprint review the team decided to move the weekly report from Friday to Monday,
    because most customer replies arrive over the weekend. Maria will own the report template.
    Open question: should the report include the support backlog or only closed tickets?
    The team also agreed to try a shared checklist for onboarding new colleagues; the first
    version is due in two weeks.
    bash
    python atoms.py note.md
    ls atoms atoms/_review
    sed -n 1,40p atoms/ATM-0001.md

    Пусни същия файл втори път: атомите трябва да отпаднат като „skipped“ — това е проверката, че дубликатите работят. Прегледай поне няколко ръчно: излязъл ли е „Мария отговаря за шаблона“ като отделен атом и с името в текста, или като „тя“?

  7. Преглед от човек

    Оценката 0–10 от модел е триаж, не присъда: малък модел не усеща добре собствените си грешки. Затова нищо не се смята за „истина“, докато човек не го отвори. Работен ред: прегледай папката _review/; за приет атом сложи reviewed: true, премести го в atoms/ и извикай index(id, atom) (или пусни пак индексирането); за отхвърлен — архивирай го, не го трий. Веднъж седмично извади случайни 10 приети атома и ги провери срещу изходния текст: така виждаш реалния процент грешки.

    ⚖️
    Лични данни и правила
    Ако текстът съдържа лични данни (имена, гласове, договори), прилагат се правилата за защита на личните данни (GDPR) и договорните задължения. Атомите са производни на текста и носят същия риск. Урокът е технически и не е правна консултация; за реален случай поискай юрист.

04Проверка

Тест

1. Кое прави текст „атом“?

2. В косинусово пространство Chroma връща разстояние 0,05. Колко е сходството?

3. Защо оценката от езиков модел е само триаж?

4. Праговете 0,97 и 0,92 в урока са:

05Какво следва

Следващата стъпка е да закачиш индекса към търсене: тогава връзките между атомите стават полезни. Подобна идея за търсене по смисъл и по думи е в урока за хибридно търсене.

06Източници

  1. BAAI/bge-m3: карта на модела — 1024 измерения, до 8192 токена, над 100 езика, MIT.
  2. bge-m3 в Ollama 🔒 локално — размер около 1,2 GB; API: /api/embed.
  3. Ollama API: /api/generate — format (JSON или схема), options, num_ctx.
  4. llama3.1 🔒 локално — размери на модела (8B около 4,9 GB).
  5. Chroma: настройка на колекции — configuration с hnsw.space, косинусово разстояние = 1 − сходство.
  6. openai/whisper — инсталация, ffmpeg, модели, лиценз MIT.