• v4.2.0 f7aa15e457

    v4.2.0
    All checks were successful
    CI / Backend lint (ruff) (push) Successful in 27s
    CI / Bot/backend version sync (push) Successful in 22s
    Release / Create Forgejo release (push) Successful in 12s
    CI / Bot tests and audit (push) Successful in 1m26s
    Release / Build and push versioned images (push) Successful in 46s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 2m1s
    CI / Frontend tests, audit, and build (push) Successful in 2m42s
    CI / Docker image build (push) Successful in 4m24s
    CI / Backend migration, tests, and audit (push) Successful in 9m55s
    CI / Synthetic session harness (no GPU, no LLM) (push) Successful in 14m20s
    Stable

    claude-bot released this 2026-09-05 06:22:55 +00:00 | 210 commits to main since this release

    AI Provider Abstraction & BYO-AI. No commitment to any single transcription or
    language-model provider: a normalised contract with swappable adapters, a
    campaign can bring its own providers, self-hosting is a first-class, measured
    target on both GPU and CPU, and the pipeline tells you what your configuration
    cannot do instead of quietly working around it.

    ⚠️ Five migrationsa5b6c7d8e9fa (provenance on summary runs),
    b6c7d8e9fa0b (per-session usage and cost), c7d8e9fa0b1c (per-word timings
    on transcript segments), d8e9fa0b1c2d (per-campaign AI settings) and
    e9fa0b1c2d3e (resolved provider identity on usage rows). All are additive —
    new tables or nullable columns, nothing rewritten, nothing backfilled — and
    each was verified to apply, roll back and re-apply cleanly. For a minor
    upgrade this size take a snapshot first: make migrate-safe does the
    preflight, backup and migrate in one step.

    The bot contract is unchanged (still v1) and nothing under /api/bot/*
    changed shape, so the backend and bot images can be upgraded independently in
    either order.

    ⚠️ If you run the bundled transcription server, update it. The word
    timings and the campaign-vocabulary hint in this release need its API v2;
    against an older server Quest Board keeps working exactly as before and the
    new "What your AI configuration can and cannot do" panel tells you to update.
    The server lives outside this repository; see OPERATIONS → "Upgrading the
    bundled transcription server".

    Six new optional .env settings, all with safe defaults:
    QB_LLM_CONCURRENCY_LIMIT, QB_ASR_CONCURRENCY_LIMIT, QB_AI_PROFILE,
    QB_LLM_TIMEOUT_SECONDS, QB_LLM_PROSE_TIMEOUT_SECONDS,
    QB_ASR_TIMEOUT_SECONDS. Leave them unset unless OPERATIONS tells you
    otherwise; a CPU-only deployment should pin QB_AI_PROFILE=cpu.

    The repository now carries ~9 MB of invented audio — a committed
    synthetic 30-minute session that exercises transcription end to end in CI.
    No real voices, no real content; see CLAUDE.md for the carve-out.

    Added

    • [webapp] Each group can now bring its own AI provider, and pay its own
      bill
      (#128). One additive migration (d8e9fa0b1c2d) adds a new
      campaign_ai_settings table; nothing existing is touched, nothing is
      backfilled, and every campaign keeps using the instance's providers until
      someone changes that. Transcription and summarisation have always been one
      endpoint each for the whole instance, which is right when you host one group
      and awkward the moment you host several — "whose OpenAI bill is this" is a
      question those groups need to be able to answer separately. Admins get one new
      switch in Admin → Bot Settings, off by default: "Let campaigns bring their
      own AI providers"
      . With it on, each campaign's settings page grows a panel
      where its GM can point transcription and summarisation at their own endpoints
      and keys. The two sides are independent, so "our own key for summaries, your
      Whisper box for transcription" works; leaving either blank keeps using yours.
      Keys are encrypted at rest with the same key as your own instance secrets and
      are never shown again, to anyone. Every URL goes through the same checks an
      admin's does, so a GM cannot aim the instance at an address on your private
      network. Turning the switch back off makes every override on the instance
      inert immediately without deleting any of them — switch it on again and
      each group has exactly what it had. Two things worth knowing before you enable
      it: a group pointing at a hosted service is uploading their session audio to
      that service under their contract rather than yours, and a campaign whose own
      endpoint is misconfigured fails visibly rather than quietly borrowing yours.
      See "Per-campaign AI providers (bring your own)" in docs/OPERATIONS.md.

    • [webapp] Quote timestamps now point at the word, not at the sentence — if
      your transcription server is current
      (#352). One additive migration
      (c7d8e9fa0b1c) adds a nullable words column to transcript_segments;
      nothing existing is touched and nothing is backfilled. The bundled
      transcription server used to return one time per sentence, so a quote
      captioned "[01:12:40]" was really "somewhere in the twenty seconds that
      started at 01:12:40" — which is fine for a one-line reply and useless for a
      line thirty seconds into somebody's monologue. The current server aligns
      every word, and Quest Board now reads those timings and anchors each quote to
      the word it actually begins on. Alignment is not perfect and is not pretended
      to be: a word the server could not place is positioned between its neighbours
      and marked as an estimate, and a quote anchored to an estimated word keeps
      the old sentence-level timestamp rather than a prettier invented one.
      Because two versions of that server exist and you update it on your own
      schedule, Quest Board now asks it what it can do — one cached GET /health
      — instead of assuming. On an older server everything behaves exactly as it
      did before, with one deliberate change: it is now reported as taking no
      vocabulary hints, because it accepts the campaign-names field and silently
      ignores it, and Admin → Bot Settings tells you that updating the server is
      the fix. Word timings and vocabulary bias apply to new sessions only —
      nothing is reprocessed, and older transcripts keep their sentence-level
      times. See OPERATIONS.md → "Upgrading the bundled transcription server".

    • [webapp] A busy evening now makes you wait instead of making you retry
      (#356). Groups play on the same two evenings, so a week of recordings lands
      within a few hours of each other — against one transcriber that does one
      session at a time and one AI endpoint that does two requests at a time.
      Quest Board now knows those limits and queues behind them instead of pushing
      past them, so a Saturday backlog costs you some patience rather than a
      failed session with a rate-limit error in it. If a service turns a request
      away anyway — a shared GPU box, or a hosted provider's own limit — that
      session goes back in the queue and waits as long as the service asked,
      instead of failing with something you could not have fixed. While a session is waiting its
      page says so — "Queued behind 3 other sessions — about 20 minutes" — rather
      than showing the same silent "Processing…" a genuinely stuck session shows,
      which is what has been sending GMs to the Retry button and putting a second
      copy of the same recording into the same queue. /ask and the Test LLM
      button never wait: if the queue is full they say so immediately, and say
      that your endpoint is not the problem. Admins get the whole picture at
      GET /api/admin/ai/queue. The defaults suit the deployments this ships
      with; if you run llama.cpp with --parallel 4, or several transcription
      replicas behind one URL, two new optional settings say so —
      QB_LLM_CONCURRENCY_LIMIT and QB_ASR_CONCURRENCY_LIMIT in .env. No
      migration, and nothing to change if you leave them alone.

    • [webapp] Transcription is now tested end to end against a session that
      never happened
      (#433). Recording, transcribing and attributing a session is
      the part of Quest Board with the most ways to go quietly wrong, and until now
      it was only ever tested against mocks — nothing checked that a real speech
      server, given six speakers talking over each other for half an hour, gave
      every line back to the right person. There is now a complete invented session
      in the repository: six speaker tracks of computer-generated speech, thirty
      minutes, every word written for the test, and every check that guards your
      recordings watched actually firing against it. No real voices and no real
      session content are involved, and none ever will be. This costs the
      repository about 9 MB of audio; the reasoning for allowing that at all, and
      the rules it lives under, are in CLAUDE.md.

    • [webapp] Transcription is now told your campaign's names before it
      listens
      (#355). Your party's character names, your wiki's people, places,
      factions and artifacts, your NPC cards and any alternate names you have
      recorded are handed to the speech model as a hint at the start of every
      recording it transcribes. Invented names are the one thing a general-purpose
      speech model has never heard, and they are also the words that matter most in
      a session — and once a name is wrong in the transcript, nothing further down
      can fix it, because the summary, the highlights and the wiki suggestions all
      read the transcript rather than the audio. You should see fewer sessions
      where a character's name is spelled three different ways, or split into two
      ordinary words. Nothing to configure, and nothing changes for a campaign with
      an empty wiki. The list is rebuilt from your wiki each time a recording is
      processed, so a name you add tonight is in effect for tonight's session, and
      a name you retire stops being suggested. Entries in the wiki trash and
      suggestions the bot has made but you have not accepted are deliberately left
      out.

    • [webapp] Admin → Bot Settings now tells you what your AI providers cannot
      do, instead of quietly working around it
      (#358). One additive migration
      (a5b6c7d8e9fa) adds a nullable column to summarisation_runs; nothing is
      backfilled and nothing existing changes. A new panel, "What your AI
      configuration can and cannot do", names the resolved provider on each side and
      lists every gap in plain language with the consequence you would otherwise
      just experience: an endpoint that cannot enforce a JSON schema (the big one —
      events can be dropped or mis-attributed from a summary with nothing failing),
      a context window too small to hold a session in one prompt and how many passes
      it will take instead, a provider that does not report prompt tokens so an
      over-long transcript cannot be detected, an endpoint that silently discards
      the start of an over-long prompt, a provider guessed from its URL rather
      than configured, and transcription without word-level timestamps so quote
      timings are estimated. The "Test connection" buttons report the same thing
      alongside reachability, because an endpoint can answer in 300 ms and still be
      unable to do any of this. Quest Board has known all of it since v4.1.1 and
      told nobody, which meant a limitation of the endpoint you picked read as a
      limitation of Quest Board. Summaries also now record the gaps that applied
      when they were written — shown on the session as "Summarised under a limited
      configuration" — and a run on a provider that cannot enforce a schema is
      marked degraded up front rather than only when its output came back visibly
      broken.

    • [webapp] You can now have OpenAI or Groq transcribe your sessions instead
      of the Whisper server you run yourself
      (#128, #350). No migration, and
      nothing changes unless you choose it — the bundled server stays the default
      and stays a fully tested path. Admin → Bot Settings gains a transcription
      provider select and a model field alongside the endpoint URL; point it at
      https://api.groq.com/openai/v1 or https://api.openai.com/v1 and leave the
      model blank for that host's default, or name any other server that speaks
      the same API. Read this part before you switch: with a managed provider,
      every per-speaker track of every recording is uploaded to a third party.
      If
      you self-host Quest Board precisely so your table's recordings stay on your
      own machine, this setting undoes that and no amount of accuracy is worth it.
      If that is not why you self-host, what you get is worth having: word-level
      timings that are measured rather than estimated across each sentence, so
      jumping to a quote lands on the word rather than near it; no GPU to own or
      keep running; and a three-hour session transcribed in minutes on Groq's turbo
      model. The "What your AI configuration can and cannot do" panel makes the
      difference concrete — the bundled server lists two gaps there and this lists
      none. Both services cap one request at 25 MB, which is under fourteen minutes
      of audio, so tracks are cut into speech spans and stitched back onto the
      session timeline automatically — including when you have silence trimming
      switched off, because otherwise every track would be refused unsent. A chunk
      that is refused anyway is counted, and a track that loses more than a quarter
      of itself fails the run rather than handing you a transcript quietly missing
      a third of the evening. See OPERATIONS.md → "Choosing a transcription
      provider".

    • [webapp] Quest Board now checks its own AI providers once a month, so a
      dead endpoint is found before a session needs it
      (#359). Every AI call in
      the product happens because a GM asked for something, which means a Whisper
      server that stopped answering in April is discovered by a failed
      transcription in July — with the recording already made. A new monthly
      background task transcribes one second of silence through whichever ASR you
      have configured and asks your LLM to reply with "OK", then records
      {ran_at, asr_ok, llm_ok, asr_provider, llm_provider, error} under the
      provider_canary key in app_settings, logging a warning when either side
      fails. It costs a second of audio and a handful of tokens a month; it grades
      nothing, retries nothing, and treats "not configured" as a blank rather than
      a fault. There is no admin panel for it yet — read it with the psql
      one-liner in OPERATIONS.md → Alerting. Alongside it, every AI provider
      adapter now has to pass a shared conformance suite in CI, which is what keeps
      "swap your AI provider without a rewrite" true as the code moves, and keeps
      the self-hosted fallback a tested path rather than a hopeful one.

    • [webapp] Quest Board now records what each session costs to process, in
      dollars or in GPU minutes depending on what you run
      (#357). One additive
      migration
      (b6c7d8e9fa0b) adds a new session_usage table; nothing
      existing is touched and nothing is backfilled, because there is nothing to
      backfill from — none of this was ever recorded. A new panel in Admin → Bot
      Settings shows the last 30 days by campaign, by month, and run by run: how
      many hours of per-speaker audio went in, how much of it the silence pre-pass
      removed before transcription, how long each phase took, how many tokens the
      model read and wrote, and what it is estimated to have cost. Every processing
      run gets a row, including the ones that failed — transcription is the
      expensive half and it finishes long before most failures do, so a run that
      transcribed three hours and then fell over has already been paid for, and a
      retry is a second row rather than a replacement for the first. Dollar
      figures are estimates from published list rates on a stated date
      , marked as
      such on every number: no provider tells us what it actually charged, and a
      model our rate table has never heard of gets no figure rather than a
      plausible-looking guess. If you self-host your transcription or your LLM there
      is no bill to estimate, so those runs show the compute time they occupied
      instead — which is the number to size a GPU against, and the honest answer
      where "$0.00" would imply the run was free. Nothing to configure; it starts
      recording on the next session you process.

    • [webapp] Admin → Bot Settings can now measure your hardware, and the docs
      say what to expect from three named profiles
      (#353, #354). No migration.
      A new panel, "Hardware profile", has a Run preflight button: it makes one
      health round trip to your transcription endpoint and one short generation
      against your LLM, times them, and reports what it found — a detected profile,
      tokens per second, transcription speed relative to real time, the gaps in your
      configuration, and roughly how long a 3.5-hour, five-player session will
      take on your hardware
      . Until now the only way to learn that was to record
      one and wait. The result is stored with the date it was taken, so the panel
      says "as measured on …" rather than pretending to be live, and the
      transcription speed is read from sessions you have actually processed rather
      than from a synthetic clip — a deployment that has processed nothing sees
      "not yet measured" instead of an invented number. POST and GET /api/admin/ai/preflight are the same thing as JSON. docs/OPERATIONS.md →
      "Hardware profiles" now carries a table of three profiles — CPU-only, modest
      GPU, workstation-class GPU — with the model, the hardware, the expected
      processing time and the measured accuracy scores for each, including the
      command that produced every number, plus
      docs/examples/docker-compose.cpu-llm.yml: a complete, runnable CPU-only LLM
      service to drop into the stack if you have no GPU.

    Changed

    • [webapp] Running without a GPU is now a supported configuration rather than
      a slower version of one
      (#353). No migration. Every timeout in the AI
      pipeline used to be a fixed number chosen against a GPU — five minutes for a
      structured call, ten for a transcription track, thirty seconds for the "Test
      connection" button. On CPU-only hardware those are not slow budgets, they are
      impossible ones, so a self-hoster without a GPU saw failures rather than
      patience, with a recording already made and nothing naming the cause. A new
      QB_AI_PROFILE setting (auto, gpu or cpu) scales all of them together,
      and QB_LLM_TIMEOUT_SECONDS, QB_LLM_PROSE_TIMEOUT_SECONDS and
      QB_ASR_TIMEOUT_SECONDS set any of the three directly if you have measured
      your own endpoint — a value you write down is used exactly as written. The
      Celery time limits are derived from those timeouts rather than configured
      beside them, so the limit can never be shorter than the provider calls it is
      supposed to contain. Nothing changes for an existing deployment: the
      default is auto, which is the GPU timings unless the new preflight has
      measured your box as CPU-class, and on the GPU profile the task limits are the
      same 6 hours they have always been. Two smaller consequences worth knowing:
      the monthly provider health check and the admin "Test connection" button now
      scale too, because a thirty-second cap on a CPU endpoint could only ever
      report a working provider as broken.

    Fixed

    • [webapp] A paid transcription service was being reported as free compute
      (#502). One additive migration (e9fa0b1c2d3e) adds three nullable
      columns to session_usage; nothing existing is touched and nothing is
      backfilled. Admin → AI Usage recorded the provider you configured rather
      than the one that ran — and "Detect from the URL" is the default, which
      stores nothing at all. So if you had pasted an OpenAI or Groq endpoint and
      left that select alone, every one of those runs was filed as self-hosted:
      the page showed GPU seconds and no dollars for sessions you were being
      invoiced for, which is the exact opposite of what it is for. Runs now record
      the transcription service, model and endpoint they actually used, so a
      managed one is counted as managed from here on. Older rows are left as they
      are — the endpoint a session used six weeks ago is not recoverable now, and
      guessing it would be inventing history rather than repairing it. Alongside
      that: managed transcription can be priced at all for the first time (OpenAI
      and Groq list rates, per model — the Groq figure is a floor, because their
      ten-second minimum per request is not visible from what is recorded), the
      rate table has learned the Claude 5 models the documentation recommends
      instead of showing no cost for them, and make conformance-live can now be
      pointed at a managed transcription service rather than only at the bundled
      one.
    Downloads