<?xml version="1.0" encoding="UTF-8"?>
<!-- Собирается скриптом scripts/make-assets.mjs -->
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ru">
  <title>Заметки — Бехруз Авезматов</title>
  <subtitle>Короткие разборы задач, на которых я застревал. Пишу, чтобы не забыть, и выкладываю, чтобы кто-то не терял на них вечер.</subtitle>
  <link href="https://kietos.me/feed.xml" rel="self"/>
  <link href="https://kietos.me/notes.html"/>
  <id>https://kietos.me/feed.xml</id>
  <updated>2026-05-26T00:00:00Z</updated>
  <author><name>Бехруз Авезматов</name><email>behruzavezmatov@gmail.com</email></author>
  <entry>
    <title>Маскировать данные до модели, а не после</title>
    <link href="https://kietos.me/note.html?id=mask-before-model"/>
    <id>https://kietos.me/note.html?id=mask-before-model</id>
    <updated>2026-05-26T00:00:00Z</updated>
    <summary>Очевидная схема — отправить текст в модель и вычистить ответ — не работает вообще. Разбираю, почему порядок шагов здесь важнее самих шагов.</summary>
    <category term="backend"/>
    <category term="llm"/>
    <category term="приватность"/>
    <content type="html">&lt;p&gt;Задача звучала просто: разбирать рабочие сообщения языковой моделью, но так, чтобы имена, телефоны и номера договоров не утекали наружу.&lt;/p&gt;
&lt;p&gt;Первая мысль была неправильная, и неправильная поучительно.&lt;/p&gt;
&lt;h2&gt;Как хотелось сделать&lt;/h2&gt;
&lt;p&gt;Отправить текст модели, получить ответ, вычистить из ответа персональные данные. Схема выглядит рабочей ровно до вопроса: а что уже произошло с исходным текстом?&lt;/p&gt;
&lt;p&gt;Он ушёл. Дальше можно чистить что угодно — данные уже вне контура. Если модель внешняя, они в чужих логах; если внутренняя, но с сетью — потенциально там же.&lt;/p&gt;
&lt;h2&gt;Как надо&lt;/h2&gt;
&lt;p&gt;Маскирование идёт первым:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot;&gt;&lt;code&gt;сообщение → Presidio → [ИМЯ_1], [ТЕЛЕФОН_1] → модель → ответ → обратная подстановка&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Модель физически не видит настоящих значений. Она работает с метками, и это не мешает ей понимать смысл: «свяжитесь с [ИМЯ_1] по [ТЕЛЕФОН_1]» разбирается не хуже оригинала.&lt;/p&gt;
&lt;h2&gt;Что из этого следует для архитектуры&lt;/h2&gt;
&lt;p&gt;Раз маскирование обязано случиться до обращения к модели, оно не может быть «фильтром на выходе» — его место в конвейере, до самого дорогого шага. У меня получилось так:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;шлюз&lt;/strong&gt; принимает сообщение и кладёт в очередь;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;воркер&lt;/strong&gt; достаёт, маскирует, зовёт локальную модель;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;обратная подстановка&lt;/strong&gt; возвращает значения уже в готовый ответ.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Шлюз и воркер пришлось разделить: разбор идёт секунды, и держать на нём HTTP-соединение — значит упереться в таймауты на первом же десятке запросов.&lt;/p&gt;
&lt;h2&gt;Проверка, которая ловит регресс&lt;/h2&gt;
&lt;p&gt;Логи воркера пишут текст на каждом шаге. Если однажды кто-то поменяет порядок и маскирование съедет за вызов модели, это видно сразу — в логе будет настоящий телефон там, где должна быть метка.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Почему детектор кругов не находит пузырьки</title>
    <link href="https://kietos.me/note.html?id=why-hough-misses-bubbles"/>
    <id>https://kietos.me/note.html?id=why-hough-misses-bubbles</id>
    <updated>2026-05-04T00:00:00Z</updated>
    <summary>HoughCircles на видео с пузырьками промахивается на восьми записях из десяти. Причина в том, что подаётся ему на вход.</summary>
    <category term="компьютерное зрение"/>
    <category term="opencv"/>
    <content type="html">&lt;p&gt;Пузырёк — это круг. Значит, находим круги. Так я думал первые полдня.&lt;/p&gt;
&lt;p&gt;На картинке из учебника HoughCircles находит круги. На настоящей записи он либо не видит ничего, либо видит сотни кругов там, где их нет, и настройка порогов из этого не выводит.&lt;/p&gt;
&lt;h2&gt;В чём подвох&lt;/h2&gt;
&lt;p&gt;Детектор ищет края. А края на реальном кадре сломаны сразу по трём причинам:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;освещение неравномерное&lt;/strong&gt; — один угол кадра ярче другого, и порог, подходящий слева, съедает всё справа;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;пузырьки полупрозрачные&lt;/strong&gt; — граница размытая, перепад яркости слабый;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;пузырьки мелкие&lt;/strong&gt; — на них приходится десяток пикселей, и шум сопоставим с сигналом.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Крутить параметры детектора здесь бесполезно: он делает свою работу правильно, ему просто дают негодную картинку.&lt;/p&gt;
&lt;h2&gt;Что помогло&lt;/h2&gt;
&lt;p&gt;Кадр важнее детектора. Две операции перед ним делают больше, чем любые пороги.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CLAHE&lt;/strong&gt; — выравнивание контраста по локальным участкам, а не по всему кадру целиком. Тёмный угол становится сопоставим со светлым, и один порог начинает работать везде.&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;python&quot;&gt;&lt;code&gt;clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Top-Hat&lt;/strong&gt; — морфологическая операция: из картинки вычитается её же «открытие». Остаётся то, что мельче структурного элемента и ярче фона. То есть ровно наши пузырьки, без крупного фона и градиентов.&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;python&quot;&gt;&lt;code&gt;kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
gray = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Вывод, который пригодился дальше&lt;/h2&gt;
&lt;p&gt;Алгоритм «не работает» — смотри на его вход. У меня из этого выросла привычка выводить промежуточную картинку после каждой ступени: видно, на какой из них всё разваливается.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Перевод денег и гонка на двух запросах</title>
    <link href="https://kietos.me/note.html?id=transfer-race"/>
    <id>https://kietos.me/note.html?id=transfer-race</id>
    <updated>2026-02-10T00:00:00Z</updated>
    <summary>Проверил баланс, списал, зачислил. Между первым и вторым шагом помещается второй запрос, и баланс уходит в минус.</summary>
    <category term="backend"/>
    <category term="postgres"/>
    <content type="html">&lt;p&gt;Учебная платёжная система, перевод между счетами. Код читается как условие задачи:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;python&quot;&gt;&lt;code&gt;if account.balance &amp;gt;= amount:
    account.balance -= amount
    target.balance += amount&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Работает, пока запрос один.&lt;/p&gt;
&lt;h2&gt;Где ломается&lt;/h2&gt;
&lt;p&gt;Два перевода приходят одновременно. Оба читают баланс — у обоих проверка проходит. Оба списывают. Баланс уходит в минус на сумму второго перевода.&lt;/p&gt;
&lt;p&gt;Воспроизвести легко: два запроса в цикле, и через несколько итераций видно отрицательное число там, где его быть не может.&lt;/p&gt;
&lt;h2&gt;Что не помогает&lt;/h2&gt;
&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Проверка после списания.&lt;/strong&gt; Уменьшает окно, но не закрывает: гонка случается реже и потому всплывает уже в бою.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Блокировка в приложении.&lt;/strong&gt; Работает ровно до второго процесса. А второй процесс появляется в тот день, когда сервис ставят за балансировщик.&lt;/li&gt;&lt;/ul&gt;
&lt;h2&gt;Что помогает&lt;/h2&gt;
&lt;p&gt;Пусть решает база — она умеет. Строка блокируется на время транзакции:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;sql&quot;&gt;&lt;code&gt;SELECT balance FROM accounts WHERE id = :id FOR UPDATE;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Второй запрос ждёт на этой строке, пока первый не завершится, и читает уже новое значение. Плюс ограничение в схеме — как последний рубеж:&lt;/p&gt;
&lt;pre class=&quot;md-code&quot; data-lang=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE accounts ADD CONSTRAINT balance_non_negative CHECK (balance &amp;gt;= 0);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Проверка в коде остаётся: она даёт человеку понятную ошибку вместо исключения базы. Но полагаться на неё одну нельзя — это подсказка, а не гарантия.&lt;/p&gt;
&lt;h2&gt;Что я из этого забрал&lt;/h2&gt;
&lt;p&gt;Правило, которое с тех пор проверяю первым: если между чтением и записью помещается чужой запрос — значит, однажды он туда поместится.&lt;/p&gt;</content>
  </entry>
</feed>
