Repository navigation
Credit Scripture versions on translated slides #181
Description
Context
The slide agent (draftItemTranslations / runSlideTranslationAgent in nlp.ts) grounds
Scripture slides in published Bibles fetched through lookup_bible_passage (bible.ts,
BIBLE_TRANSLATIONS). Nothing records which published text a slide came from, so viewers
never see a credit. Some of these texts carry licenses that require one; for example, hatbsa
is CC BY-SA 4.0 (ebible.org/Scriptures/details.php?id=hatbsa). The goal is a small,
unobtrusive credit under the translated slide, shown only when the slide reproduces Scripture
(at least about one verse). A phrase borrowed for a song gets no credit.
Decisions made: the model declares the credit, and the viewer shows version + license
only (no reference). On the base: it was "stack on PR 152", but 169/170 now move the
pipeline too (see below). You expect 169/170 to land soon, so: build on main after 169/170. If 152 is still
open then, stack on 152 (rebased onto that main), since it owns the entry shape and the notes
UI this extends.
Design
1. Sources registry: src/textSources.ts (new, pure)
TextSource = { id, kind: 'scripture' | 'creed', name, publisher, license, licenseUrl, sourceUrl }TEXT_SOURCES: Record<string, TextSource>, keyed by helloao id (hatbsa,fra_ncl,spa_r09).
Creeds and confessions are added here later underkind: 'creed', with no schema change.formatCredit(source)returns e.g.Bib la — Société Biblique Haïtienne · CC BY-SA 4.0.- Entries store the id, not the rendered string. If a credit turns out wrong, we fix the
registry once and every stored slide picks up the fix. bible.tskeepsBIBLE_TRANSLATIONS, and a test asserts that every id in it has a
TEXT_SOURCESentry. Adding a Bible without its credit then fails CI.
2. Data model (on top of PR 152)
SlideTranslationEntryinsrc/slideTranslation.tsgets optionalsourceId?: stringand
adapted?: boolean; both ride the same path below.- It is threaded through
TranslationBlockResult(nlp.ts) →translateItem
(src/slideItemTranslation.ts) → the returned entries →slideLibrary.ts(persist, and keep
it through the record rebuild that PR 152 added) → the/api/slideLibraryupsert body. slide_translator.py_store_translations: copysourceIdwhen present.
3. Model declares it: nlp.ts
- Add two optional fields to the
set_translationssegment schema, both named generically:source, an enum. It is'bible'today and gains'creed'once a creed lookup tool
exists. The description gives the rule: set only when this slide reproduces at least
one full verse (or sentence) of text you looked up; omit it for allusions, short phrases,
and song lyrics.adapted, a boolean: whether the slide alters the published wording. We trust the agent
on this.
- The model doesn't echo a reference. I checked what a lookup returns:
{ reference: "PSA 23", passages: { French: … } }, where the reference is the requested
range. That is often wider than one slide (a chapter lookup, with the slide holding verses
1–3). So echoing it would be loose matching at best. It also isn't needed: the credit
depends only on which version, and that's fixed by the language (BIBLE_TRANSLATIONS). - Server-side guard: attach
sourceId = BIBLE_TRANSLATIONS[language]only if the
conversationmessagescontain alookup_bible_passageresponse that returned a passage
in that language. Scanningmessages, rather than a per-run list, covers follow-up runs
whose lookup happened in an earlier round. The model can't name a license, and it can't
get a credit for a version it never fetched. - Store
sourceId(andadapted) at write time, so a later change toBIBLE_TRANSLATIONS
doesn't relabel old slides. - Working-copy semantics follow PR 152's notes: a rewrite without
sourcewithdraws the
credit, andrevise_translationleaves it alone. - One sentence goes in
bibleSectionofbuildSlideTranslationPromptpointing at the fields.
4. Viewer: src/SlideTranslationViewer.tsx
- When
resolved.entry.sourceIdmaps to a source, render a footer line throughSlideText.
Add afooterprop next toheaderifSlideTextlacks one. Style ittext-xs, low
opacity, and keep it outside the fit-text measurement so it doesn't shrink the slide. - The license name links to
licenseUrl. This matters for phone viewers; on the projector it
is just text. - No label for a verbatim quote: the line itself (version · publisher · license) is
self-explanatory. There is one localized string,adaptedFrom("adapted from" / "adapté
de" / …), insrc/strings.tsfor all four locales. It is used only whenadaptedis true.
It follows the viewer's UI locale like every other string.
5. Review screen: src/SlideReview.tsx
- Show the same credit line in the cell, next to PR 152's note, so the reviewer can see the
claim. A × to clear it is optional and goes in only if it turns out trivial.
6. Follow-up issue (not built)
- "Require a Scripture credit on
BiblePassageitems".itemKindalready reaches
proclaimPresentations(yjs_publisher.py) but is not sent to/api/translateItem
(onlyitemTitleis, fromproclaim_service.pyandsrc/slideTranslationApi.ts). Per your
answer, this goes to an issue. Also include: the source-language credit (Proclaim's own
English version oncurrentSlide), and localized references if that's wanted later.
If PRs 169/170 land first (server-owned slide sync)
Most of the plan is untouched. 169 doesn't change nlp.ts (the agent, its tools, the
prompt), the viewer, the review screen, or slideLibrary.ts. The changes are:
slide_translator.pyis deleted. Its entry write moves towriteSlideTranslationsin
slideSync.ts, sosourceId/adaptedget copied there. Python is no longer involved.- One entry point instead of two. Both
/api/translateItemand the server's
translate-ahead now go throughdraftIteminserver.ts, so the credit is threaded once. - The BiblePassage rule becomes cheap. The snapshot now carries
itemKindto the server
(slideSync.ts), andproclaimPresentationsalready stores it for the review screen. So
passingitemKindintodraftItem→buildSlideTranslationPromptis a few lines: "this
item is a Bible passage; every slide must declaresource: 'bible'". This moves from
follow-up issue into scope. - Ordering conflict to watch: 169 still skips
status === 'reviewed'entries, and 152
deletesstatus. Whichever lands second has to reconcile that. This work is cleaner on
main after both than stacked on either one.
Things to settle while implementing (flagged, not assumed)
- Registry values (supplied by you; hatbsa's publisher name still gets copied verbatim
from its ebible page):fra_ncl: Sainte Bible néo-Crampon Libre, © 2022 Fraternité de Tibériade, CC BY-SA 4.0.
Its terms say an adapted text must indicate the change, which theadapted→ "adapted
from" line covers.spa_r09: Reina-Valera 1909, public domain. No credit is legally required, but we
still show "Reina-Valera 1909 · dominio público". Crediting only the licensed versions
would read as inconsistent to a viewer, and the line costs nothing.hatbsa: Bib la (Haitian Bible Society), CC BY-SA 4.0.
- ShareAlike. Under CC BY-SA an adapted slide is itself under CC BY-SA. The credit line
already names that license, so nothing extra is shown, but it's worth one line in the PR.
Files
src/textSources.ts (new), bible.ts, nlp.ts, src/slideTranslation.ts,
src/slideItemTranslation.ts, slideLibrary.ts, server.ts (upsert body),
slide_translator.py (or, after 169, slideSync.ts + draftItem in server.ts),
src/SlideTranslationViewer.tsx, src/SlideText.tsx,
src/SlideReview.tsx, src/strings.ts, and a short note in docs/slide-translations-plan.md
in PR 152's amendment style.
Verification
- Unit tests: registry covers
BIBLE_TRANSLATIONS;source: 'bible'attachessourceId
when the history holds a lookup for that language (including one from an earlier round),
and is dropped when it doesn't; a rewrite withoutsourcewithdraws it;adaptedrenders
"adapted from";slideLibraryround-tripssourceId; the viewer renders the footer
only whensourceIdresolves; Python_store_translationscopies it. npm run typecheck,npm run lint,npm test -- --no-color,uv run pytest.- Smoke §3 (Proclaim sync): replay
tests/fixtures/synthetic_service.jsonland confirm a
Scripture item shows the credit and a song doesn't. - Push to
claude/vibrant-albattani-lk70bubased on PR 152's head, and open a PR noting it
stacks on PR 152.
Originally posted by @kcarnold in #152 (comment)
Activity
kcarnold commentedon Oct 26, 2026
Two updates from splitting #152:
SlideTranslationEntry = { text, provenance, note? }, rather than on the conversation. SosourceId/adaptedwould sit next to it and follow the same rule: whatever replaces the text replaces them (a rewrite brings its own or none,revise_translationdrops them, a reviewer's Save drops them). This plan said a rewrite withoutsourcewithdraws the credit andrevise_translationleaves it alone. Under Surface the translator's per-slide notes on the entry #184's rule, a revision withdraws it too. That probably fits: an edited verse may no longer be the published wording, and the model can restate the credit withset_translations. Also,slide_translator.pyis gone, so the write isstoreTranslationsinslideSync.ts.provenance: 'bible' | 'creed'overlaps withsourceId. Those two values have never had a writer. They were kept for "the agent claims this is the published wording", and that is exactly whatsourceIdrecords, more precisely, because it names the version. I suggest this work drops them fromSlideProvenance(src/slideTranslation.ts) and keeps one way to say it.Generated by Claude Code