Colin Calnan  /  Builds  /  Applied AI · Civic tech

339 checked quotes from my town’s election, built in one Sunday

See the live site →
What it is
A guide for my neighbours: all 18 candidates, in their own words.
What it proves
AI does the legwork, simple rules catch its mistakes, and I sign off.
Measured
339 of 339 quotes match the candidate’s own words exactly.
Lake Country Votes, the mayor's page, topic Why I'm running: three white quote cards side by side, one per candidate in alphabetical order, each with a pine bar, an ochre quote mark, the candidate's own words, its source and a link to the full answer.
The live site: one question, every candidate in the race, same card, same order.Live mayor page, quotes cut to two lines.
Status
Live, last updated Oct 11, 6:42 pm
Built
One Sunday: 9:12 am start, 2:32 pm live
Made with
Claude, a speech-to-text tool, the town’s address map
How
Started on my phone, finished on my Mac

How it works

Scattered answers in, one checked guide out

How it works. Written answers: four public sources, a saved copy of each, AI picks quotes, a word-for-word check, pages made, a final check, live. Panel video: the video, speech to text on my Mac, AI tags who is speaking, I check and approve, AI writes 75 short summaries that go into the pages. WRITTEN ANSWERS 4 public sourcesnone from Facebook Saved a copyof every source AI picks quotesno rewriting Word for word2 thrown out Pages madeone per candidate Final checkone fail stops it Liveon the web PANEL VIDEO Panel video1h55m on YouTube Speech to texton my Mac AI tags who spoke153 times I checkapprove or fix AI sums up75 short summaries AI simple rules, no AI me How it works, as two columns. Written answers: sources, a saved copy, AI picks quotes, word-for-word check, pages made, final check, live. Panel video: the video, speech to text on my Mac, AI tags who is speaking, I check, AI writes 75 short summaries that go into the pages. WRITTEN ANSWERSPANEL VIDEO 4 public sourcesnone from Facebook Saved a copyof every source AI picks quotesno rewriting Word for word2 thrown out Pages madeone per candidate Final checkone fail stops it Liveon the web Panel video1h55m on YouTube Speech to texton my Mac AI tags who spoke153 times I checkapprove or fix AI sums up75 short summaries AI simple rules, no AI me
The AI does four jobs; simple rules and I check what it makes.
  1. The AI picks quotes but can’t rewrite them

    It picks the best passages on each topic. A simple check throws out any quote it can’t find word for word.

    The rules, written firstCONTRACT.md, trimmed
    ## Non-negotiables
    1. Nothing fabricated. Every sentence attributed to a candidate on the site is either (a) a verbatim quote that exists character-for-character (after whitespace normalisation) in a file under data/sources/ or data/panel/transcript.json, or (b) a panel synopsis line that carries a timestamp link to the exact moment, and is labelled as a synopsis. There is no third kind.
    2. Identical treatment. Every candidate in a race gets the same fields in the same order. Alphabetical by last name within a race. ...
    My first rule, word for word: every quote must exist exactly in a saved source, or be a labelled summary linked to its moment in the video.
  2. A two-hour video, split into every answer

    A speech-to-text tool on my Mac wrote out the candidates’ panel. The AI named who was speaking: sure 130 times out of 153, it flagged 23. It built me a checking app in minutes, and the video answers stayed hidden until I approved.

    The local panel review tool: a dark header with an audio player over the 1:54:23 recording, a filter for turns that need checking, the status 8 left to check, 153 turns, APPROVED, and Regenerate summaries and Approve and build buttons. Below it, one turn card at 45:15: speaker set to Unknown, a Confirm button, a MEDIUM badge, the note closing thank you after the time call, and the words Thank you.
    The 8 left are thank-yous, chatter and questions read out, not answers.
  3. A final check that stops anything wrong going live

    Before every update a checklist with no AI runs: quotes match, names are alphabetical, links work, and the writing stays at high-school level.

    The final check’s real output, trimmednode scripts/check.mjs --grades
    grade 3.8 /about/index.html (396 words)
    grade 4.2 /index.html (344 words)
    grade 4.0 /mayor/index.html (87 words)
    grade 8.2 /panel/index.html (3734 words)
    checked 29 pages, 339 quotes
    ok
    Grade is reading level: pages read at about grade 3 to 8. Ok means all 29 pages and 339 quotes passed.
  4. Which ward are you in?

    Type your address and it finds your ward on the town’s own property map. It runs on your device, so nothing you type is sent anywhere.

    The ward finder: a street address box with a Find my ward button beside an outline map of Lake Country's four wards, Carr's Landing, Oyama, Okanagan Centre and Winfield.
    The ward finder, with 5,081 local addresses built in.

Proof

What the checks found

10 of 23Unsure AI guesses about who was speaking that I changed after checking.
1,946 of 1,946Sample addresses where the town’s two records of their ward agreed, so the ward finder gives the right answer.

Found and fixed. That evening I doubted the links that jump to each answer in the video. The AI timed every word again and checked the audio at each link. Most landed within a second. It caught an answer split in two, a statement filed under the wrong question, and two "please repeat the question" moments posted as answers.

Every panel link, placed along the 1 hour 55 minute recording, each one tested. 0:00 1:54:23 Every link into the video, checked against the audio Every panel link, placed along the 1 hour 55 minute recording, each one tested. 0:001:54:23 Every link into the video, checked

AI and me

Who did what

  • The AI (Claude). Found the sources, wrote all the code (three copies of itself at once), picked quotes, named speakers, wrote the summaries.

  • Simple rules, no AI. The word-for-word check, the alphabetical order and the final check before anything goes live.

  • Me. The idea, what it must never do, how it looks, and signing off who spoke when.

Still breaks

Never finished

  • No candidate invite. The email asking every candidate to check their page never went.

  • No updates. The four-hourly refresh was never turned on, so later answers were missed.

  • A wobble on small phones. The page slides sideways a little, found the night I shared it.

The full story

Why

Before our municipal election I kept hearing the same thing from friends: you couldn't tell what any candidate stood for from the scatter of Facebook posts and comments. Every candidate had said something somewhere. Nobody was going to read all of it.

My first message asked Claude to scrape the candidates' Facebook pages. It said no. Logged out, Meta hides most of it, and scraping breaks Meta's terms, "a bad look on a site whose whole value is trust." Its research found public text already covered every candidate, so Facebook wasn't used at all.

Where the words came from

The District's election page (17 official statements), the Chamber of Commerce Q&A (10 fixed questions, all 18 answered, about 12,000 words), the local paper's questionnaire series (10 published by that Sunday), 8 candidate websites, and the recorded candidates panel. The local news site Castanet blocked the download, so it wasn't used.

The day

I started it on my phone while making breakfast, kept going while folding laundry and stretching, and moved to the Mac in the afternoon to preview things properly. Claude Code ran on my server: three AI helpers researching, then three building at once, one each on the sources, the site and the panel video.

TimeWhat happened
9:12 amFirst message, on my phone.
9:42 amPlan agreed: my Bold Talks design in new colours, a clickable index for the panel video, nothing from my other work.
10:56 amSite built.
2:03 pmMoved to the Mac.
2:32 pmLive.
3:11 pmSpeaker names approved. The video index goes up.
6:32 pmThe triple check.
7:01 pmShared with friends.

There's no analytics on it, so I don't know how many people used it.

Design rounds

It's built on Bold Talks, my design system, in a palette with nothing of mine in it: ink, bone, ochre, pine and clay, with Bebas Neue and Inter. Every round started with something I typed.

  • "Maybe use some nice iconography"

    Claude drew nine line icons, one per topic, no icon library.

  • "It's close but it's a bit off"

    Quotes moved onto white cards with a pine bar and an ochre mark, the question asked sits above each answer, and an empty topic gets a dashed card.

  • "The line spacing on the title is just a little tight"

    Fixed.

  • "That's a lot to read"

    The panel page went from about 106,000 pixels tall on a phone to about 7,900, with folding questions and each answer moved onto the candidate's own page.

Two audits

Before it went live, a separate AI reviewer checked the whole site ("ship after fixes") and caught an incumbent flag that was wrong. That evening I asked for a triple check: "triple check that all content has been vetted, reviewed and we've not made anything up." Besides the video links, it merged three re-read questions and found the panel page hiding the two trustees, who had answered questions. All 339 quotes were re-checked against the live sources.

One rule for everyone

I asked whether the quotes read well. One headline quote had a speaking slip, a phrase said twice. Editing it would change their words, so the rule changed for everyone instead: avoid passages with obvious slips, never cut into the middle of one. Quotes went from 298 to 339.

Tools

  • Claude Code
  • Whisper large-v3-turbo
  • yt-dlp
  • District open data
  • Node
  • Vercel
The races grid on the live home page: Mayor, Councillor at large and Councillor Carr's Landing headings in pine, each with its number of seats and its candidates listed by last name.
See the site as it stood on voting day →Next build: Gig Splitter →