After lesson 05, Redact can read every line on the page and outline exactly where it sits. But it still has no idea which of those lines matter. “Maya Chen” and “Thanks” get the same grey outline. In this lesson we ask a second local model to tell them apart. Checkpoint: redact-06-laya, after redact-05-coordinates

A model that answers instead of writing

The first idea most people have is a chat model: paste in the OCR lines and ask “which of these are private?”. That works, but a language model writes its answer. You then have to parse its text, check it didn’t invent a line, and hope it answers the same way next time. Our question is narrower than that. For each line we want to know: is this a name, an email, a phone number, an address, an account number? That’s a fixed list of yes/no questions, known in advance. A decision model is built for exactly this. Laya takes a piece of text and questions you define, and returns a probability for each answer. It never writes anything, so there’s nothing to parse and nothing it can make up. For more on when to reach for which kind of model, see Choosing the right model for your data.

How you ask matters

There are two ways to put our question to Laya. The second way caught all six private fields. The highest score any ordinary line got was 0.37, for “My”. Decision models do best with plain statements a person could check, one fact at a time. In the app the numbers on each card will differ a little, for example 100% for the account and 82% for the reference, because a typed direction adds one more question and each card shows the strongest answer for its line. So Redact asks every line a short set of statements and keeps the one Laya is most sure about. If even that one is below 0.5, the line isn’t suggested.

Two safety nets

A probability is still a guess, so Redact checks it against the page. First, Laya only ever answers about lines OCR already found. It can point at a line; it can’t create one, so a suggestion can’t cover text that isn’t there. Second, a set of plain rules re-checks each answer against the line’s own text. An @ makes it an email, “account” plus digits makes it an account number, and so on. If the rules disagree with Laya, the rules win; if they find nothing, the suggestion is dropped. About grounded suggestions explains why.

1. Get this lesson’s code

Stop the app and the service if they’re running. From the repository root, redact-course, bring in this lesson’s commit:
No packages change, so there’s nothing to install. To see every change, open the comparison on GitHub. The files that matter:

2. Read the questions

Open qvac-service/reasoning.ts. The questions are plain sentences:
The Confidential level adds four more: amount, date, reference and business. Each key becomes the label of the suggestion if Laya says yes. Your typed direction becomes one more question:
noul is QVAC’s question type for a yes/no statement: Laya answers with the probability that the statement holds. A “yes” to this one becomes a suggestion labelled other.

3. Read how Laya is called

Still in reasoning.ts, the model is loaded once, the first time a page needs it:
modelType: "llamacpp-decision" tells QVAC this is a decision model, not a chat model. device: "gpu" runs it on the graphics chip when there is one. Then every OCR line goes in at once, with the same questions:
states is the list of lines; decide returns one result per line, in the same order. If the counts don’t match, something is wrong, so the whole analysis fails rather than guess which answer belongs to which line. Finally each line keeps its strongest answer, if it clears the threshold:
Then the rules in candidate-validation.ts get the last word. In index.ts you’ll see where it’s wired up: modelSource: qvac.LAYA_421M_Q8_0, the 421-million-parameter Laya.

4. Start the service and the app

In one terminal:
You should see Redact QVAC service listening on http://127.0.0.1:4317. In a second terminal:

5. Run the analysis

Open localhost:3101 and upload public/test-files/redact/chat-private.png. Choose Private and type this in Additional direction:
Select Prepare for review. If you did lesson 00, nothing downloads here: Laya is already in .qvac/models. Otherwise the first run downloads Laya, laya-Q8_0.gguf (450 MB), into .qvac/models, so give it a few minutes. After that, Laya’s part takes under a second per page; OCR is still most of the wait.

6. Read the suggestions

The review screen now has a Suggested redactions panel with seven suggestions: Each card shows a percentage followed by sensitivity: Laya’s probability for the field it picked. Below it is a short sentence such as “The block contains a person name.”: that comes from the rules, not from the model. On Original, each suggested line gets its own outline. Notice what’s missing: “My”, “Got it.” and “Thanks” are on the page but not suggested. Notice too that “reference” appears even though it isn’t a Private field: your direction asked for reference codes. And “keep the amount confidential.” is labelled other, because only the direction question said yes to it. At this checkpoint the suggestions are only shown. They don’t cover anything yet; that’s the next lesson.

If something goes wrong

  • The review screen says “Local analysis is unavailable. You can still redact by hand.” The service isn’t running or isn’t ready. Check the first terminal.
  • The first run seems stuck. It’s downloading 450 MB. Watch .qvac/models grow, then try again.
  • No suggestions at all. Check the privacy level and direction; with Private and no direction, fewer fields are asked about.

7. Check your work

Stop both terminals, then run this lesson’s tests:
Then the full checks:
npm test reports Tests 63 passed (63). The reasoning tests use a fake Laya, so they check the logic without the model.

So what just happened?

A second local model now reads every line OCR found and answers a fixed set of questions about it. Its answers are probabilities, filtered by a threshold and re-checked by plain rules against the text itself. What reaches the screen is a list of suggestions, each tied to a real line on the page.

Try it yourself

Prepare the same chat again, this time with Confidential and an empty direction. Compare the list: which fields appear because of the level, and which disappear without the direction? Then open reasoning.ts, change DECISION_THRESHOLD to 0.9, restart the service, and see which suggestions survive. Set it back to 0.5 when you’re done.

Where to next

Next: Accept or reject each suggestion (lesson 07)