DEV Community

Cover image for I gave it four facts and it invented a fifth
Eugen Taranowski
Eugen Taranowski

Posted on Originally published at watchnext.leyu.studio

I gave it four facts and it invented a fifth

Show data on my TV tracker comes from TMDB, like it does for a great many TV apps. That includes the synopsis — which means the paragraph on my page for a given show is the same paragraph on TMDB itself, on JustWatch, on Trakt, and on every other app built from the same API.

Duplicate text isn't a penalty. It just can't win anything. Those are the pages meant to answer "when is the next episode of X", and the only original thing on them was my own countdown.

So: generate something. I have a machine on the LAN running a 35B model, which is more than enough to write a paragraph. The interesting part turned out to be everything I had to forbid.

The obvious thing to generate is the wrong thing

The instinct is to rewrite the synopsis. Same information, different words, no longer duplicate. I didn't, for two reasons.

The first is that it lands squarely in what Google calls scaled content abuse — generating many pages without adding value. A reworded plot summary is a different arrangement of the same information: high volume, nothing new. Whether or not it trips anything, it's hard to argue you've added something the reader didn't have.

The second is simpler. Nobody searches for a synopsis. People type "is Silo weekly or all at once", "what day does Silo come out", "how many episodes in season 3". A rewritten plot summary matches none of that.

What does match it is release cadence — and cadence isn't a field. Nobody has it, because it has to be derived:

// Modal gap between consecutive episode air dates, not the mean:
// one abnormal break (a strike, a pandemic) drags an average enough
// to describe an annual show as arriving every three years.
const gaps: number[] = [];
for (let i = 1; i < dates.length; i += 1) {
  gaps.push(Math.round(dates[i].diff(dates[i - 1], "days").days));
}
// 7 -> weekly, 1 -> daily, 0 -> all at once
Enter fullscreen mode Exit fullscreen mode

The same trick over season premieres gives "new seasons have arrived roughly every two years", which is genuinely useful and which no other TV site states.

The division of labour

This is the part worth copying, if anything here is: my code derives the facts, and the model is only ever asked to turn them into sentences.

It receives a small JSON object and a rule that everything in the paragraph must come from it:

{
  "name": "House of the Dragon",
  "networks": ["HBO"],
  "status": "Returning Series",
  "cadence": "weekly",
  "releaseWeekday": "Sunday",
  "numberOfSeasons": 3,
  "firstAirYear": 2022
}
Enter fullscreen mode Exit fullscreen mode

If the model supplied the facts too, the failure mode would be confident invention across several hundred pages, on a site whose entire premise is telling people a date accurately. Not a risk worth taking to save writing a function.

Then it invented things anyway

Four failures, in the order I found them. None threw an error. Each would have been published.

It explained an internal flag, backwards. My facts included a boolean recording that TMDB stores this show's dates a day before the network advertises them — a real convention I correct for. Handed that flag, the model wrote:

New episodes are released weekly, typically arriving one day before the scheduled Thursday air date.

Which isn't what the flag means, isn't true, and is meaningless to a reader. I stopped giving it that field. Facts a model can't phrase safely don't belong in its input.

It wrote a date into text meant to last months. One note ended "…with the next installment airing tomorrow." The entire design keeps dates out of the stored text and computes them live on every render, precisely so nothing goes stale — and the model reached for "tomorrow" anyway. My validation rejected months and years. It did not reject relative time.

It described a running show as finished. Given numberOfSeasons: 4 it wrote "has completed four seasons" about a series airing its fourth. That field is how many seasons exist, not how many have ended. An easy thing for a person to misread too — but a person misreads it once, not two hundred times.

And then it made something up. At temperature 0.5:

The third season follows three years after the second.

That sentence appears nowhere in its input. It came from the model's own knowledge of the show, in direct violation of an instruction telling it not to, and it reads exactly like the sentences around it that were true.

What I did about it

Prompt rules for what a rule can fix, and a validator for what it can't:

const RELATIVE_TIME =
  /\b(today|tomorrow|tonight|yesterday|this week|next week|right now)\b/i;

const rejectReason = (body: string, facts: ShowFacts): string | null => {
  // Check everything EXCEPT the show's own name — "The Tonight Show
  // Starring Jimmy Fallon" was rejected for containing "Tonight".
  const withoutTitle = body.replace(titlePattern(facts.name), " ");

  if (MONTHS.test(withoutTitle)) return "names a month";
  const rel = withoutTitle.match(RELATIVE_TIME);
  if (rel) return `relative time ("${rel[0]}")`;
  // ...stray years, spelled-out large numbers
  return null;
};
Enter fullscreen mode Exit fullscreen mode

Rejected output is regenerated, twice, then skipped.

That last check is worth a note on where to stop tuning a prompt. A prompt rule took spelled-out numbers from three notes in twenty down to one, and no further. Past that point another sentence of instruction was worth less than four lines of regex the retry loop enforces. A model can be asked; a check can insist.

Temperature went 0.5 → 0.2 → 0.35. At 0.2 the invention stopped and every note became structurally identical — the same sentence with the values swapped. Once the fact rules were strong enough to carry the discipline themselves, 0.35 bought back sentence variety without the invention returning. I checked that by running the offending show five times, rather than assuming.

The validator had its own bugs, of course

It rejected The Tonight Show Starring Jimmy Fallon for containing "tonight", and Reply 1988 for naming a year. Both were in the show's own title. The fix — strip the title before checking — was already in place for two of the four checks and had simply never been applied to the others.

It also rejected two shows for naming the year they ended, which can never go stale and should always have been allowed. "Aired from 2011 to 2020" is strictly better than trailing off.

Final run: 251 notes, one failure. That one still has no note, because it was rejected twice for a badly formatted number. No note is better than a bad one, and a pipeline that can decline to publish is worth more than one that always produces something.

What I'd take from it

Give a model facts to phrase, not questions to answer. Everything that went wrong was the model reaching past its input — for a fact it knew, for a word that felt natural, for an explanation of something it had been handed but didn't understand.

Whatever you forbid in the prompt, check for in code. Every one of these failures was already forbidden in writing. The rules weren't ignored so much as outweighed by whatever made the sentence read well.

And the thing that made the output useful wasn't the model at all. It was spending an afternoon working out which facts were worth having — cadence, release weekday, the gap between seasons — none of which existed as fields, all of which had to be computed first.

The model wrote the sentences. The value was in what it was given to say.

Top comments (0)