ON-PREMISE AI PLATFORM

Private AI that never leaves your network.

Peki deploys local language models with a hybrid RAG engine — vector tables, SQL and a knowledge graph — over your PDFs, Word files and scans. Answers with citations, on your hardware, with zero internet dependency.

CAPABILITIES

Everything runs behind your firewall.

Document search

Ask questions across millions of pages — PDF, Word, spreadsheets and scanned images via OCR. Every answer cites file and page.

Hybrid RAG engine

Vector similarity for meaning, SQL for exact fields and dates, a knowledge graph for relationships — merged into one grounded answer.

Custom chat models

Local models tuned on your terminology, formats and policies. From 7B on a single workstation to 70B on a GPU node.

Specialized agents

Multi-step agents for complex tasks: contract review, compliance checks, report assembly — each step logged and auditable.

ARCHITECTURE

One pipeline, three indexes.

Documents are parsed, OCR’d and chunked once — then indexed three ways so every question finds the right kind of evidence.

RUNS ENTIRELY INSIDE YOUR NETWORK

Your documents

PDF · DOCX · XLSX
TIFF · PNG scans

Ingestion

parse · OCR
chunk · embed

Vector tablesVectorANN

SQLSQLEXACT

Knowledge graphGraphLINKS

Hybrid retriever

rank · merge
deduplicate

Local LLM

answers + citations
agents · tools

Vector — finds passages by meaning, even when wording differs across documents.

SQL — exact filters over extracted fields: dates, amounts, parties, statuses.

Graph — entities and relations, so amendments, parties and clauses stay connected.

PROJECTS

Systems running on client hardware.

Each one was built for a specific operation and deployed inside its own network. Open a project to read how it was built and see the interface.

IN PRODUCTION

US truck dispatch operation

Natural-language database access · repair-invoice extraction

Two on-premise AI systems for a dispatch operation running a 165-table production database: a natural-language assistant over the dispatch data, and an extraction pipeline that turns scanned repair invoices into database records.

Both run entirely on the client’s own hardware — a single 20 GB GPU. No data leaves the network.

  • text-to-SQL
  • vision extraction
  • on-premise
  • logistics
See more Close

The problem

A dispatch operation runs on a 165-table database. The data is there — driver performance, loads, broker rates, repair costs — but getting it out meant knowing the schema or asking someone who did. Repair invoices arrived as scanned PDFs and phone photos, and every line item was retyped by hand.

Neither problem could go to a cloud AI service: the database holds broker rates, margins and driver records.

What we built

Natural-language access to the dispatch database. A dispatcher asks in plain English — or Serbian — and gets an answer. The system writes the SQL, runs it, and shows the query it used alongside the answer, so nothing is a black box.

It is read-only by construction: the query guard accepts SELECT only, rejects every destructive keyword, and blocks chained statements. The model cannot write to the database no matter what it generates.

A dispatcher asks which truck has the highest total repair costs. The assistant replies with a table naming truck 0574 and its total, and below it shows the SQL SELECT statement it generated to get that answer.
The answer, and the query that produced it
The assistant’s opening screen, listing the operational areas it can query — logistics, invoicing, fleet assets, HR — above a grid of example questions graded from easy to very hard.
What the assistant can be asked, across the operation

Repair invoices into the database. A scanned or photographed invoice goes in; the invoice number, truck number and individual line items come out as structured records. A vision model reads the page directly — no separate OCR engine, no per-page licence, no cloud vision API.

A scanned repair invoice displayed on the left; on the right, the extracted invoice number, truck number and five line items with quantities and prices, ready for the operator to verify and export.
Scanned invoice in, verified line items out

The setup

Everything runs locally on one workstation-class machine with a single 20 GB GPU. Local models via Ollama, behind a single gateway so a model can be swapped without touching the applications.

  • 165TABLES
  • ~1,900LOADS / 6 MONTHS
  • 60DRIVERS
  • 5DISPATCHERS

IN PRODUCTION

Clinical consultation assistant

A doctor’s workspace where the AI answers only from the patient’s own chart

A consultation workspace for doctors: patient records on one side, a live AI conversation about that patient in the middle, consultation history on the other. The assistant answers strictly from the patient’s own chart — nothing else.

Built on a self-hosted language model. Patient data never leaves the clinic’s infrastructure, and every prompt sent to the model is logged for review.

  • clinical AI
  • FHIR-shaped records
  • ICD-10
  • self-hosted LLM
  • healthtech
See more Close

The problem

A doctor opening a patient file reads the same thing every time: a list of diagnoses, lab values, medications, allergies, past visits. The information is all there, but answering a simple question — what changed since the last visit, does this drug conflict with a known allergy, which findings support this diagnosis — means reading the whole file again, every time.

General-purpose AI chat does not solve this. It answers from the internet, not from the patient in front of you, and sending a medical record to a cloud service is not an option to begin with.

What we built

A three-pane consultation workspace. Consultations on the left, the conversation in the middle, the patient’s chart on the right. Pick a patient, start a session, and ask questions in plain language while the chart stays visible next to the answer.

The assistant is bound to the chart. Before every question, the patient’s record is assembled into the model’s context — name, age, gender, MRN, then every recorded condition, observation, allergy, medication, encounter, care plan and diagnostic report. The instruction is explicit: answer only from this data, and say “Information not available” rather than guess. Answers are grounded in the file, not in the model’s general medical training.

The three-pane workspace: the consultation list on the left, a question-and-answer exchange about the patient in the middle, and the patient’s chart on the right showing observations with values, units and interpretation.
Consultations, the conversation, and the chart it is answering from

A structured chart, not free text. The record follows FHIR resource shapes — Condition, Observation, Encounter, CarePlan, AllergyIntolerance, Medication, DiagnosticReport — each with its own fields, defined in a configuration file rather than in code. Adding a section or a field to the chart is a config change.

ICD-10 search built into the chart. A searchable index of 68,215 diagnosis codes sits in the right panel. Find the code, add it straight to the patient’s conditions — no separate lookup, no retyping.

Two kinds of sessions. A doctor’s own session is private and starts clean each time — just the patient’s current data, no earlier conversation. A shared session carries its full history and stays visible to the whole team, so a consultation can be picked up by whoever sees the patient next.

Everything is auditable. Every prompt sent to the model is stored with its response. Doctors rate finished consultations and flag individual answers as good or bad, with a reason. An admin panel shows the accounts, the patients and the activity log.

The setup

Python and FastAPI on the server, plain JavaScript in the browser — no build step, no framework. JWT sessions with bcrypt-hashed passwords, and two roles: doctor and admin.

The language model runs on our own inference server behind a single internal endpoint. The model layer is deliberately thin: a local MedAlpaca-7B and Ollama-hosted Llama models were both driven through the same interface, so the model can be replaced without touching the application. No third-party AI service is involved at any point.

Where it went

It became the foundation for a full clinical consultation platform: PDF intake that extracts a patient and their medical history from a referral document automatically, a persistent database, PDF export of a finished consultation, and per-specialty sessions. That system is deployed and running.

IN EVALUATION

Legal research assistant for Serbian law

Statute search, case analysis and Q&A over the client’s own file

Three tools over one retrieval core: ask a legal question and get the exact article with a citation; describe a case in plain language and get it broken down into legal issues, analysed one by one; ask what is actually written in the client’s own file — contracts, notices, scanned records.

Runs entirely on the firm’s own hardware. Nothing is sent to a cloud service, and every article number in an answer is checked against the database before it is printed. When the law is not in the corpus, the assistant says so instead of inventing a statute.

  • legal RAG
  • cited answers
  • self-hosted LLM
  • 12 statutes / 4,849 articles
  • measured, not assumed
See more Close

The problem

A lawyer’s questions come from two places: what the statute says, and what is in the client’s file. Both are searchable in principle and neither is searchable in practice — the statutes because finding the right article means knowing which article you are looking for, the file because it is a stack of PDFs, scans and phone photographs.

General-purpose AI chat fails here in a specific way. It produces article numbers that look exactly right and are wrong. A citation to “Article 51 of the Consumer Protection Act” sends a lawyer to court with a provision that, in the current redaction of that act, governs something else entirely. And a client’s file cannot be sent to a cloud service at all — it is privileged material.

So the requirement was never “an AI that talks about law”. It was: answers that carry a verifiable citation, an explicit “this is not in the database” when there is no basis, and no data leaving the building.

What we built

A corpus where one article is one chunk. The off-the-shelf parser merged four to five articles into a single chunk — 313 articles of the Labour Act became 74 chunks, and retrieval was wrong as a result. The ingest was rewritten so every article becomes exactly one chunk, carrying the statute name, the article number and the descriptive heading above it. Twelve statutes are loaded — obligations, company law, enforcement, civil procedure, criminal code, family, labour, inheritance, copyright, consumer protection, data protection, the bar — 4,849 chunks, each document verified 1:1 against its own article count.

The ingest view: the raw text of the Consumer Protection Act on the left, and on the right the chunks it produced — one per article, each labelled with the statute name, the article number and its heading.
One article, one chunk — with the statute and article number attached

A deterministic assistant for clear questions. Question in, article out, with the citation. A reranker sits between the search and the answer; adding it moved Hit@1 from 65% to 84% and Hit@3 from 80% to 100%. Four generative models were compared head to head on the same 22 questions before one was chosen — the winner cited correctly 94% of the time and refused out-of-scope questions 75% of the time, against 25% for the model it replaced.

A legal question typed in Serbian — when may an employer regulate rights and obligations by workplace rules — answered as a list of conditions, each carrying a citation marker, with the source statute file named beneath the answer.
Every clause in the answer carries its citation back to the statute

An analyst for whole cases. A client does not arrive with a legal question; they arrive with a story. Feeding that story to the search engine does not work, and we measured exactly how badly: the best-scoring article for a five-sentence narrative about unpaid design work scored 0.026 and was a provision about cancelling a package holiday. The same corpus, queried in statutory terminology, returns the right articles at 0.53–0.82.

So the story is decomposed first: facts are extracted, then legal issues, and each issue generates its own short queries in the language the statute actually uses. Every issue is retrieved separately, analysed separately in IRAC form, and only then synthesised into a position with risks, deadlines and open questions. The first measured run found all eight article groups a lawyer had identified by hand, in 62 seconds — against zero usable articles for a single pass.

A translation layer between how clients speak and how statutes are written. This turned out to be the actual bottleneck, not the search. A case about a tenancy dispute produced queries about “rent collection” and “lease of real estate” while the statute says zakupnina and zakup; a consumer case never once produced the word saobraznost, which is the term the entire chapter is built on — so five governing articles were unreachable. A dictionary maps lay terms to statutory ones, guarantees the good phrasings are actually used rather than left to the model’s judgement, and triggers mandatory questions about limitation and preclusion periods. It moved the main metric from 28% to 76%.

Whatever must not fail is not left to the model. Three separate findings pushed the same lesson. Asked for the deadline in an employment dispute, the model picked the wrong article in six runs out of six — even though the right one was ranked first with a score of 0.819 — so the deadlines section is now extracted from the retrieved provisions by rule, not by generation. Asked which statute was missing from the database, the model invented laws that do not exist, so naming a missing statute was taken away from it entirely and given to a registry. And every [Statute, Article N] marker in the output is checked against the provisions actually retrieved for that issue before the document is printed. Invented citations across every measured run: zero.

Language is part of accuracy, not style. The analysis had been quietly writing Croatian forms — and the same wrong terms were going into the search queries and missing the right articles. A counter for 27 such forms became a metric with only one acceptable value. It reads zero.

RAG over the client’s own file. A separate module ingests the case documents themselves: Word files, PDFs with a text layer, scans without one, photographs of receipts and records. Which tool reads which format was measured rather than assumed — the built-in document parser lost in every category, including on files that already had a clean text layer, where it introduced errors that were not in the file. On Latin-script scans, the chosen path makes 20–40× fewer character errors. Retrieval over the resulting chunks answers 95% of questions correctly at rank one; the assistant got 8 out of 8 facts right — amounts, dates, serial numbers — cited the right document every time, and refused both questions whose answers were not in the file.

Bad input is reported, never silently ingested. The built-in parser read a Cyrillic photograph as visually similar Latin garbage and loaded it into the database without a word of warning. Script and scan quality are therefore checked at ingest: anything below the threshold lands on a visible “needs a better scan” list instead of in the database, because a chunk that is confidently wrong is more dangerous than a chunk that is missing. Each case gets its own isolated knowledge base — deleting a case is one operation, and one client’s file cannot surface in another client’s search.

The setup

RAGFlow runs the retrieval core in Docker; the pipelines around it are our own Python against its API — ingest, decomposition, analysis, document processing and the evaluation harnesses. The language model, the embedding model and the reranker all run locally on a single 20 GB GPU. No third-party AI service is involved at any point, and the backend ports are closed from outside by a targeted firewall so the deployment can share a host without exposing the database.

The evaluation harness is not a phase at the end; it is how each decision was made. Every claim above has a script behind it and a number it produced: retrieval sets with ground-truth articles, per-case checklists that include traps — articles that must not appear — a character-error harness with a known-correct transcript for every sample document, and cached runs so metrics can be recomputed without re-running an eight-minute case. The discipline paid for itself early: a “tightened” prompt once dropped accuracy from 89% to 78%, and we only knew because it was being measured.

Where it is

The deterministic assistant is done and measured. The analyst runs end to end — ten legal issues, zero invented citations per case — and is now in evaluation against a checklist of real cases, which is deliberately the gate before any further tuning: with three cases and one run each, most differences are inside the noise, and tuning on them would be guessing. The documents module is complete and measured on a controlled sample, with real case files as the next step.

The user interface is the one thing deliberately postponed. There is no point wrapping a pipeline in a chat window before its recall is known — for a tool whose worst failure mode is sounding convincing while missing a legal issue, the number comes first.

IN EVALUATION

Football betting analysis assistant

Plain-language questions answered from a production football database

Ask a question in plain Serbian — “what’s worth playing in Spain this weekend?” — and get an answer built on real numbers: per-match probabilities computed from a production football database, ranked against the league average, with the combinations that cannot logically coexist already thrown out.

The language model never calculates. It reads the question, and it explains the result. Everything between those two steps is deterministic code, verified against the database.

  • self-hosted LLM
  • probability modelling
  • read-only production DB
  • FastAPI + HTMX
See more Close

The problem

A football statistics database holds everything needed to judge a bet — how often each team’s matches go over 2.5 goals, how many corners, how the league behaves as a whole. What it does not hold is an answer. Turning “should I play this” into a number means joining half a dozen tables, knowing which column means what, and doing it again for every match on the coupon.

Ask a general-purpose AI instead and you get two failures at once. It cannot see the database, so it invents plausible percentages. And it multiplies: told that over 2.5 goals is 62% and both teams score is 58%, it reports 36% for the pair. Those two outcomes go together — the real number is far higher, and the confident-looking one is simply wrong.

What we built

Ask in your own words, get numbers you can check. The user types a sentence. The model’s only job at that point is to turn it into parameters — intent, country, date range, ranking mode — from a closed set of allowed values. The search, the arithmetic and the selection happen in plain code against the database. The model comes back at the very end to explain what the code found:

question  →  [MODEL] parse intent   →  parameters (closed value set)
                                    ↓
             [NO MODEL] search      →  real numbers from the database
                                    ↓
             [MODEL] explanation    →  text  →  guard  →  user
A question typed in Serbian asking what is worth playing tomorrow. The assistant first restates how it understood the query, then lists three bets — each with the match probability, the league average, an estimate-confidence rating, the sample size and a plain-language statement of what the bet means.
Every suggestion carries its league baseline, confidence and sample size

Estimates are assembled, not looked up. Upcoming matches have no precomputed row in the database — only the 206 already played do. So each estimate is built from club statistics: a sample-weighted blend of home and away season form, merged with a rolling ten-match window (0.6 season / 0.4 form), then compared against the league baseline. A bet at 95% in a league that runs 94% is not a safe pick, it is a degenerate one — the comparison is what separates the two, and bets whose league baseline exceeds 90% are dropped entirely.

Combinations are measured, not multiplied. Every bet is compiled into a predicate and replayed over historical matches in the same league. That catches three distinct problems a naive product cannot: impossible pairs (0-2 goals + 3+ goals), implications (one bet that silently contains another, counted twice), and correlation (both teams score + 3+ goals land together 1.94× more often than chance). On one real match, 28 of 56 candidate combinations were rejected outright. Combinations are also deduplicated by fingerprint — the set of historical matches on which they hold — because two differently-named tickets can be logically identical. The naive number is always shown alongside the corrected one, struck through, so the user can see how far off it was.

The predicates are proven against the database. The whole calculation rests on a predicate meaning exactly what the database means. A dedicated validator replays every predicate over each club’s home and away matches and compares the result to the stored statistics: currently 100% agreement across 44,557 checks. It found five bet semantics that had been assumed wrong — including one where “total cards” turned out to count red cards twice, and one where a bet read as “no goals in either half” actually means “not both halves”. Coverage is reported honestly: 1,334 of 1,850 bets (72.1%), with the uncovered remainder broken down by cause and by whether it is solvable at all.

The model is fenced in, and the fence is checked. Rules sit at the end of the prompt, not in the system message — measured behaviour: when the context overflows, Ollama silently eats the beginning, so instructions placed first disappear while the data survives. Truncation is never reported by the API, so the client watches whether prompt_tokens exactly hits the context limit and discards the answer rather than show one built on half a file. And because a 24B model does not reliably follow negative instructions — it kept claiming bets “don’t overlap” under an explicit rule never to claim that — a guard inspects the generated text and surfaces corrections next to the answer.

Nothing is hidden when it fails. Every AI endpoint has a non-AI twin: if the model service is down, the interface still shows the numbers, without the prose. Matches dropped for being over the request limit are counted and reported rather than silently cut, because a silently truncated answer looks like it considered everything.

The production database cannot be damaged. The credentials carry full privileges — they are shared with the ETL that fills the database — so protection lives in the code, in four deliberately overlapping layers: textual query validation, a connector with multi-statement execution disabled, START TRANSACTION READ ONLY, and hard timeout plus row limits. Layers one and three overlap on purpose: a read-only transaction rejects UPDATE but lets DROP TABLE through, because DDL commits implicitly. The health endpoint does not take the code’s word for it — it fires a harmless UPDATE through both layers on every check and reports whether they refused it.

The setup

Python and FastAPI on the server, server-rendered HTML with HTMX in the browser — no build step, no Node, no CORS, because the same application serves both. HTTP Basic protection covers every route including the docs and the health check: the host has no firewall, and the data endpoints expose the full production statistics.

The language model runs on our own inference server behind an OpenAI-compatible endpoint — a 24B Mistral hosted with Ollama. No third-party AI service is involved, and no data leaves the infrastructure.

Underneath sits the ETL that fills the database: a Dockerised Python pipeline that imports league, club and head-to-head statistics from Excel into MySQL, with its own seeding, reset and per-league delete paths.

Where it went

The deterministic core is finished and verified; the remaining work is measurable rather than open-ended. 243 of the uncovered bets are implementable with data already present, and the validator is an exact judge of when they are correct. Known limits are documented rather than papered over: there are no odds in the database, so the system computes probability but not value; the dependence factor is measured at league level while the individual probabilities are match-specific, which makes the combined number a correction rather than an exact probability; and impossibility is empirical — “never occurred together in 1,500 matches” is strong evidence, not a proof, so the sample size travels with every result.

IN EVALUATION

Dating platform AI coach

A nightly job that decides who needs a nudge — and proves it works before launch

A nightly AI system that reads how each user is actually doing on the platform — profile, photos, conversations — decides who is worth a nudge and about what, and writes the message in that user’s own language.

Rules decide who gets coached and when; the model only writes the sentence. The platform assembles every prompt, so the AI service never sees the database. It all runs on the client’s own GPU — no user data leaves the network.

The platform has no live users yet, so the whole system was built and measured against a behavioural simulator: 200,000 simulated people, 24 million events, time simulated day by day rather than rows generated at random.

  • on-premise LLM
  • behavioural simulation
  • privacy by construction
  • dating
See more Close

The problem

A dating platform launching across three countries — English, German, Spanish, one codebase, several white-label sites on a 97-table MariaDB.

Its own data already knows which users are struggling: profiles nobody stops on, a main photo that performs worse than one further down the gallery, a conversation that died two days ago and could still be saved. None of it reached the user.

The obvious approach — ship the data to a cloud model and ask it what to say — was out. Profile text and conversation metadata are personal data of EU users, on a platform where the category itself is sensitive.

And there was a harder problem underneath: there were no users yet. Not few — none. Every threshold, every rule, every “is this even worth sending” would be a guess, with nothing to check it against.

What we built

Six signals, computed in SQL, with no threshold in the code. Daily counting and diagnosis are separate queries: the counting has no thresholds at all, it just counts, and the diagnoses read those counts. Every threshold is a parameter per site, so moving one doesn’t rewrite history. Rejection is a column, not a branch — “why did this user get nothing?” is answered by one SELECT.

A decision layer that remembers. Signals say what is going on; this layer decides whether we speak to the user at all, about what, and how often. Two lanes — an urgent one for dying conversations, where the window is 48 to 96 hours, and a slow one for profile and photo coaching, where you say one thing and wait to see if it helped. One topic at a time, which is not thrift but a condition of measurement: advise the profile and the photo at once, and a rise in views tells you nothing.

The model writes; the rules decide. A wrong model output can be a clumsy sentence — it cannot be the wrong person, the wrong moment, or the wrong topic. The number of model calls is determined by SQL, so cost is predictable before anything runs.

Output checking that would rather send nothing. Fifteen failure codes across two severity levels: wrong language, invented numbers, forbidden claims, prompt injection caught on input before the model ever sees the text. What fails is not sent.

Privacy enforced by code, not by promise. Four access levels; a single module assembles context and raises on any overreach. Coaching may use what a user can already see and platform-wide aggregates — never how a named other person behaves. Enforced by an allowlist of keys, not a blocklist, because a blocklist is the thing people forget to extend.

Proving it works without a production database

This is the part the project is really about.

A simulator that simulates time, not rows. People with attractiveness and profile quality as separate traits, behaving day by day; conversations that stall on their own. Deterministic — the same seed produces byte-identical SQL. Three scales, from a few hundred users up to 199,733 users and 24 million events.

Every SQL signal has an independent second implementation. The production query in SQL, a reference implementation in Python written from the specification, and a differ that runs both and compares row by row: 2.1 million rows at staging scale, zero mismatches, against both SQLite and the real MariaDB.

Replay through 90 days. The decision layer is run across simulated time, because some faults only exist in sequence and are invisible in any single snapshot.

A dated audit of our own claims. Thirteen independent passes over the code, including adversarial ones whose job was to break the project’s own headline claims, under one rule: where the documentation and the code disagree, the code wins. It corrected a dozen numbers in our own status document and found checks that had been reporting “zero mismatches” over branches they never actually executed.

What the measuring found

  • 59% of the first end-to-end run failed the language check. The language never reached the generator — German users would have quietly received text in the wrong language. Found on day one by the check, not in production.
  • A description-scoring feature passed 90% of its output checks and still didn’t work. The checks verify shape — integer in range, enum present, quote actually from the text — and a wrong answer passes all of that. Separation between empty and rich profiles was +0.45, because the model was giving nearly everyone a 3. It took a separate accuracy test, using the one advantage a simulator gives you — it knows which profile is empty — to show the feature was broken. Rewritten prompt: +1.17, every empty profile below the threshold.
  • Bots change the diagnosis for 6.3% of users. 897 of 14,333 would be diagnosed differently if bot views counted as real ones. Which is why every counter exists twice, total and real-only, and every diagnosis uses only the second.
  • One query costs the same for one day as for forty-five. 632 seconds either way, because a running total with no lower time bound reads the whole history every night. It is the only cost in the project that grows with the calendar rather than with traffic — it quietly doubles without a single new user. Invisible at development scale, where the query runs in under a second.
  • Enumerating enum values literally in the prompt is the difference between 0% and 100%. Describing the scale did not work; listing every value did. The same fix worked three separate times, on three different fields.

The setup

Local models on the client’s own GPU server, behind a REST gateway — nothing goes to a cloud AI provider. 7.2 seconds per call, which is fine for a nightly job and would never be acceptable in a request path, so it was built as a nightly job from the start. A local MariaDB matching the production version exactly, which caught three bugs SQLite had silently tolerated — including a division precision difference that gave one user in 804 a different diagnosis depending on which side computed it.

Six modules, 10,524 lines of Python. Ten new tables with migration, rollback and a consistency checker. Regression suite runs in about a minute: 42/42 simulator checks, 130/130 schema checks, 17/17 conformance vectors from the AI service author, and fault injection proving the output checks actually catch what they claim to.

At production scale the SQL phase finishes in 592 seconds against an eight-hour nightly window — with the entire remaining question being how much of the AI phase the decision layer removes.

Where it stands

The chain runs end to end on real infrastructure: behaviour in, generated and checked text out, history written to the database and re-readable. Approval flow and delivery are the next phase, along with two performance fixes that measuring found and that deliberately have not been merged yet — they change SQL that goes to production, so they enter only through the verification suite.

Every number above comes from simulated data, and will until the platform launches. That limit is stated in the project’s own documents rather than hidden in them: the difference between “the query runs” and “the query measures the right thing” is the largest known risk, and no amount of test coverage retires it.

IN DEVELOPMENT

Live chat translation

Two people, no shared language, one conversation

A working simulation of in-chat live translation: two people who share no language, both phones on one screen, every message silently translated in both directions as it is sent.

Behind it sits a translation pipeline built for chat rather than for documents — it preserves formality, protects links and @mentions, carries conversational context, and runs on Claude, on a local model, or fully offline, deciding which at boot.

  • live translation
  • LLM pipeline
  • local-model fallback
  • product prototype
See more Close

The problem

Translating a chat message is not the same problem as translating a document, and the difference is not the length. A chat translator has to get right everything that sits around the words.

Formality carries the relationship between two people: render tu as Sie and you have changed who they are to each other, not just the grammar. A username, a URL, a time comes back mangled if the model is allowed to translate it. Pronouns and gender only resolve if the model can see the previous turns. And the moment someone changes their language mid-conversation, the history they have already read must stay as they read it — re-rendering the past is not a feature, it is a rewrite of what was said.

There is a second problem underneath, which is that every message reaching a model costs money. A demo with a live API key and an open port is an open wallet.

What we built

Two phones on one screen. The left one belongs to Claire, who reads and writes French; the right one to Thomas, who reads German. You type on either side and watch the message arrive translated on the other. The idea the whole codebase rests on is that a message has two texts — the one its author wrote and the one its recipient receives — so the same message renders differently on each phone. Changing a language applies from the next message on; an older one can be brought forward individually, one explicit click at a time.

Two phone screens side by side showing the same conversation. Claire writes in French and Thomas reads German; each translated message carries a note naming the language it came from and a link to reveal the original.
The same conversation, rendered in each participant’s language

A pipeline ordered to keep work away from the model. 🙂, ok and a bare link are returned untouched. Links, mentions, e-mails, code and times are masked into placeholders before the model sees them and spliced back after. Language detection is script ranges and stopword scoring first — microseconds for the easy 90% — and only genuinely ambiguous input is delegated. A message already in the recipient’s language ships unchanged. What survives all of that hits an LRU cache keyed on text, direction, register and a fingerprint of recent turns; a duplicate arriving while the first is still in flight waits for it instead of starting a second call.

Only then does a model see anything, with the last six turns supplied as context — the originals each person actually wrote, never the translations, because feeding a model its own output compounds errors turn over turn.

Details settled by measurement, not taste. The placeholder format is [[0]] because against qwen2.5:14b-instruct it survived translation 7 times in 8, the fancier ⟦0⟧ glyphs 6 in 8, and <x0/> only 3 — models rewrite self-closing tags. Every lost placeholder costs a second model call, so the choice is latency: a message with a link and a time went from 6.3s to 3.7s.

The setup

Three engines, tried in order and probed rather than assumed — Claude, a local model via Ollama, and an offline phrasebook. A key that is present but rejected falls through at boot instead of failing every message at runtime, and whatever sits below the chosen engine stays live as a runtime fallback, so a rate limit or a stopped daemon degrades a message instead of losing it. Both real engines are driven from the same prompt and the same output schema, so switching changes latency and quality — not behaviour.

Spending is bounded where the money is spent. An access token in front of every route, a per-session rate limit with a per-address ceiling above it, and daily call and token caps asserted inside the paid provider rather than at the HTTP layer — which is what makes them total. The one combination the server refuses outright is a public bind with a live key and no token: it prints a freshly generated token and exits rather than opening the port.

Node and Express on the back, vanilla JavaScript on the front with no build step, ~5,000 lines in total, everything in memory. 14 languages, and two test suites that run offline with no API key.

IN DEVELOPMENT

Aesthetic surgery clinic

Rhinoplasty outcome simulation from a single photograph

A consultation tool for aesthetic clinics: a patient photo goes in, and the surgeon shows what a rhinoplasty would plausibly look like — with the patient’s face still recognisably their own.

Built around one hard constraint: the AI may refine what is already there, never invent a new face. Runs entirely on the clinic’s own GPU, so patient photos never leave the network.

  • image editing
  • diffusion / inpainting
  • on-premise GPU
  • GDPR
  • healthtech
See more Close

The problem

Aesthetic consultations run on words. The surgeon describes the outcome, the patient imagines something else, and the gap between the two only becomes visible after the operation. The tools that close that gap — Crisalix, VECTRA — are 3D rigs with hardware and pricing to match.

The goal here was a consultation tool, not a surgical planner: realistic and convincing enough to align expectations, from a single ordinary photograph.

Two constraints shaped everything. Patient photographs are Article 9 special-category data under GDPR, so no cloud AI service was an option. And the result has to stay the same person — a simulation that returns a beautiful stranger is worse than useless in a consultation room.

What we built

Parametric morphing first. MediaPipe FaceMesh finds 478 facial landmarks, and a Similarity MLS warp (Moving Least Squares) narrows the nose — alar base, tip and bridge as three independent controls. Because the warp only moves existing pixels, identity is preserved mathematically rather than hoped for. The deformation is strictly local: every landmark outside the nose is an anchor with zero displacement, so eyes and cheeks stay pixel-still, and the narrowing axis is derived per photo by PCA, so it still works when the head is tilted.

The simulation screen: a before-and-after slider dragged across a face photograph, labelled original on the left and simulation on the right, with three sliders beside it controlling how much the alar base, tip and bridge of the nose are narrowed.
Alar base, tip and bridge as three independent controls, against the untouched original

Then the honest finding: 2D warping is a dead end where it matters most. On profile shots — exactly where the patient’s actual complaint usually is, a dorsal hump — landmark detection is unreliable and “narrowing the width” is the wrong operation entirely. Real patient photos confirmed it.

A generative layer on the client’s own GPU. The second approach edits the photo with a diffusion model instead of deforming it. The finding that made it work: a full-image edit preserves identity perfectly but changes the nose far too little, and no amount of guidance tuning or chaining moves it — the model is deliberately conservative about geometry. Masked inpainting, with the same model, produces a real and controllable change, and everything outside the mask stays pixel-identical to the original.

That gave the interface its shape. The surgeon brushes the nose region — robust on profile shots, where landmark detection isn’t — and one strength slider maps to the diffusion denoise level: subtle, moderate, strong. The mask is the safety boundary: the model is allowed to work inside it and nowhere else.

Consent and audit built in, not bolted on. Upload is blocked without an active patient consent (a hard 403, not a warning). Each consent stores a snapshot of the exact text and version the patient accepted. Every meaningful action is written to an immutable audit log, and deleting a photo really deletes it from storage — right to erasure as code, not as policy.

The setup

FastAPI and PostgreSQL for the clinic backend, React for the surgeon’s portal. The generative work runs on a separate machine — a single 20 GB GPU — behind a small HTTP service, so the clinic application never talks to the model directly and the model can be swapped without touching the application.

Landmark detection and morphing run on CPU. Only the generative render needs the GPU, and no part of the pipeline calls an external API.

Status

In development. Auth, consent, upload, landmark detection and the warp simulation are built and tested end-to-end; the generative pipeline is validated on the GPU server and is being wired into the clinic application. Scope is deliberately narrow — the nose, until it is genuinely good — before lips, chin and cheekbones follow on the same engine.

SECURITY

Air-gapped by design, not by policy.

There is no cloud fallback and no telemetry endpoint to disable — the software has no route out of your network to begin with.

  • Zero external calls. Inference, indexing and OCR run on your GPUs — or on a pre-configured appliance we ship.
  • Role-based access + audit log. Every query, source and answer is recorded and attributable.
  • Offline updates. Models and knowledge bases arrive as signed bundles you install on your schedule.
  • Your data stays yours. Nothing is used for training elsewhere — there is no “elsewhere”.

YOUR NETWORK

Documents

file server · DMS

Indexes

vector · SQL · graph

Local LLM

GPU node / appliance

Your team

browser · LAN only

AIR GAP

Cloud APIs

no route out

DEPLOYMENT

From audit to answers in four weeks.

  1. 01 · DAYS 0–3

    Scope

    We audit your document estate, tasks and hardware. You get a sizing plan — GPU node or shipped appliance.

  2. 02 · WEEK 1

    Deploy

    Install on your network — no inbound or outbound internet required. Integration with AD/LDAP and file shares.

  3. 03 · WEEKS 2–3

    Index

    Ingestion pipeline parses, OCRs and indexes your documents into vector tables, SQL and the knowledge graph.

  4. 04 · WEEK 4

    Specialize

    We tune chat models on your terminology and configure agents for your workflows. Your team goes live.

KNOWLEDGE BASES

Domain expertise, pre-loaded.

Curated, versioned and citable corpora that ship with your deployment — updated through the same signed offline bundles.

Law

Statutes, case law, contract doctrine and regulatory texts.

2.1M SOURCES

Medicine

Clinical guidelines, drug interactions, coding and protocols.

870K SOURCES

Finance & tax

Accounting standards, tax codes and reporting frameworks.

640K SOURCES

Engineering

Industry norms, standards and technical documentation.

1.3M SOURCES

Your field

We build and maintain a corpus for your domain, to order.

BUILT TO ORDER →

CONTACT

Talk to an engineer.

No sales script. Describe your documents and tasks — we’ll tell you what hardware it needs and what a pilot looks like.

  • → reply within one business day
  • → NDA on request
  • → on-site or air-gapped pilot available

We’ll only use this to reply. Nothing goes into a CRM you didn’t agree to.