Skip to content

v0.2.2

Choose a tag to compare

@github-actions github-actions released this 11 Jun 05:48
· 287 commits to main since this release

[v0.2.2] — 2026-06-11

Added

  • Add anthropic to requirements.txt to fix pyright CI

The anthropic package was used in gui/claude_client.py but missing from
requirements.txt, causing pyright to report reportMissingImports in CI.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(bd60038)

  • Add Modelfile.qc for fine-tuned Gemma 4 E4B quality-check model

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e283b06)

  • Add protect_named_entities setting; default to not protecting proper nouns

Previously, faction/company/ship/character/location/creature/resource/system/
ui/game_term categories were only excluded when the source language was
English. For all other source languages, the term protector would replace
these names with opaque tokens, preventing the AI from translating them
(e.g. "United Colonies" could never become "Об'єднані колонії").

Changes:

  • term_protector.py: export SOFT_CATEGORIES frozenset listing all proper-
    noun/lore categories. This is the single source of truth used everywhere.
  • app_settings.py: new protect_named_entities: bool = False field.
    Config version bumped 22→23 with migration.
  • ollama_worker.py: remove the source-lang gate; instead exclude
    SOFT_CATEGORIES unless self.protect_named_entities is True.
    Added protect_named_entities param to init and update_config.
  • claude_translation_worker.py: same logic; also fixes a latent bug
    where protect() (non-existent method) was called instead of protect_text(),
    causing term protection to silently fail with AttributeError every time.
  • settings_dialog.py: new checkbox "Protect proper nouns and lore terms"
    (unchecked by default) with tooltip explaining the trade-off. Updated
    the stale info label that listed faction names as always protected.
  • main_window.py: pass protect_named_entities when constructing both
    worker types and in update_config.

Format tags, game IDs, XML/alias tokens, structural newline tokens, and
user-added custom terms are always protected regardless of this setting.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6928b7c)

  • Add Hunspell spell-check QC check for translated strings

New gui/spell_checker.py wraps hunspell with three fallback backends:

  • hunspell pip package (C bindings, fastest)
  • spylls pip package (pure Python Hunspell)
  • hunspell CLI subprocess (no Python package needed)

Silently disables per-language when no dictionary is installed.
Fires SPELL_ERROR (warning) QC code when misspelled lowercase words are
found; skips ALL-CAPS, proper-noun-capitalised, and digit tokens to keep
false positives low in game text.

Retry hint lists the misspelled words so the model can correct them on
retranslation.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0873e67)

  • Add NexusMods BBCode description (no markdown separators)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(7ee671e)

  • Add config directory override in Settings → Storage

Reads from BSE_CONFIG_DIR env var or a bootstrap file at the fixed
default location, so the redirect is discoverable before the config
dir is known. UI shows the active path, browse/reset buttons, and a
restart-required warning when the value changes.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fe63557)

  • Add cache directory override in Settings → Storage

Same bootstrap-file pattern as the config dir override. Priority:
BSE_CACHE_DIR env var → ~/.config/BethesdaModTools/.cache_dir_override
→ /mnt/ssd when mounted → config dir fallback.

UI shows the active cache path, browse/reset buttons, and restart
warning in the Storage group.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6e2b8ff)

  • Add word checkers for DE/ES/FR/IT/PL/PT-BR languages

  • gui/_word_checker_base.py: shared WordChecker class (lazy load, thread-safe,
    handles both plain and "word count" frequency-list formats)

  • gui/{de,es,fr,it,pl,ptbr}_word_checker.py: thin wrappers per language

  • data/{german,spanish,french,italian,polish,portuguese}_words.txt: 50k-word
    frequency lists from hermitdave/FrequencyWords (MIT)

  • scripts/download_lang_dicts.py: download/refresh all 6 word lists

  • quality_checker: add _check_latin_coverage() — fires LOW_TARGET_COVERAGE
    when <20% of tokens match the expected target language; wired into check()

  • ollama_worker: preload all 6 dictionaries on worker init

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b089218)

  • Add file association support for all supported file types

  • main.py: open a file path passed as argv[1] via QTimer.singleShot so
    double-clicking a .strings/.esp/.ba2 in a file manager launches the app
    and loads the file immediately

  • aur/bethesda-strings-editor.desktop: add %f to Exec= and declare
    MimeType= for the three new custom MIME types

  • aur/bethesda-strings-editor-mime.xml: freedesktop MIME definitions for
    application/x-bethesda-strings (.strings/.dlstrings/.ilstrings),
    application/x-bethesda-plugin (.esp/.esm/.esl, magic TES4), and
    application/x-bethesda-archive (.ba2, magic BTDX)

  • aur/PKGBUILD: install MIME XML to /usr/share/mime/packages/ so the
    shared-mime-info pacman hook runs update-mime-database automatically

  • scripts/install_file_associations.sh: helper for source-checkout users
    (xdg-mime + desktop-file-install for per-user ~/.local install)

  • Modelfile: update FROM to use blob SHA256 reference

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(11e68ba)

  • Add Modelfile and config for gemma4-opus48-st

Modelfile.gemma4-opus48: Ollama model definition for the Gemma 4 12B IT
fine-tuned on Claude Opus 4.6/4.7/4.8 reasoning distillation (GGUF at
/mnt/ssd/models/gguf/gemma4-opus48-Q4_K_M.gguf). Uses Gemma 4 chat
template, think=disabled, top_k 64 / top_p 0.95 per author recommendation.

MODEL_CONFIGS["gemma4-opus48-st"]: matching runtime config in OllamaWorker
so the app sends correct parameters when this model is selected.

Also adds gemma4-opus48-st to the default model picker suggestions.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(96a1a36)

  • Add rule 3e: translate multi-sentence bracket content (publisher notes, etc.)

The four existing categories only covered single-word tokens. Long bracket
content like publisher notes and editorial remarks had no matching rule, so
the model defaulted to preserving them in English. New category (e) explicitly
instructs: translate any bracket text that is multiple words or full sentences,
keeping the [ ] delimiters, with a concrete example from the failing book string.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a9d2398)

  • Add skip-string-types setting (Book, Note, Dialogue, etc.)

Adds a row of checkboxes in Settings → Translation for each content
type (Book, Note, Terminal, Dialogue, Quest, UI, System). Checked
types are skipped during AI batch translation — strings are left
untranslated and marked as pending.

Implementation:

  • AppSettings.skip_string_types: list (default [], CONFIG_VERSION 24)
  • OllamaWorker / ClaudeTranslationWorker: skipped_types attribute;
    classify() called per-string; returns SKIP_SIGNAL when matched.
    Paragraph sub-requests (string_id==-1) are exempt from the check.
  • main_window.py passes skip_string_types to both workers on init.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f6677f9)

  • Add Find & Replace to Advanced Search dialog

Adds a "Replace (Translated text only)" group below the search criteria:

  • "Replace with:" field (supports regex back-references when regex is on)
  • "Replace All" button — substitutes in all rows matching the current
    search criteria; asks for confirmation when >20 rows are affected
  • Shows "Replaced N occurrences in M rows" feedback inline

Replace uses the same pattern/flags as search (regex, case, whole-word).
Only the Translated column is modified; Original text is never touched.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2e4e217)

  • Add error-code filter to Quality Check Results dialog

Adds an "Error code:" dropdown next to the existing severity filter.
It is populated dynamically from every issue code present in the current
reports (sorted alphabetically), so only codes that actually appear are
listed. Selecting a code hides all rows that don't contain that code.

Both filters (severity + code) are ANDed: e.g. "Errors" + "ENGLISH_LEAK"
shows only error-severity rows that have an ENGLISH_LEAK issue.

The code combo is rebuilt after Auto-Fix / retranslation refresh so
cleared codes disappear and the previous selection is preserved when
possible.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(71bd17d)

  • Add Claude API pre-flight cost estimator

Before a Claude-backend batch starts, shows a dialog with:

  • Model name and string count
  • Estimated input / output token totals
  • Estimated cost WITH prompt caching (written once, read on remaining
    strings at ~10% rate) vs WITHOUT caching
  • Cache savings percentage

Estimation uses chars/3.5 ≈ tokens (no API call, instant). The pricing
table lives in claude_client.CLAUDE_PRICING (USD/MTok for input, output,
cache_write, cache_read) and can be updated independently of the UI.

The dialog only appears for the Claude backend; Ollama is unaffected.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(12f51a3)

  • Add Weblate community translation sync

Push the local string table as a PO file to a Weblate instance so
community translators can work in a familiar web UI. Pull back their
translations and merge into the local model. Includes REST client
(weblate_client.py), push/pull/stats dialog (weblate_sync_dialog.py),
four new AppSettings fields, a settings group in the Settings dialog,
and a "Weblate Community Sync…" entry in the File menu.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f0d350a)

  • Add mamaylm MODEL_CONFIGS entry with author-recommended sampling params

top_k 64 / top_p 0.95 match the INSAIT training distribution; previously
the app fell back to DEFAULT_CONFIG and used top_k 40 / top_p 0.9.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0b5314b)

  • Add Dialogue Tree Visualizer (Quest → Topic → Response node graph)

New standalone ESP parser (bethesda_strings/dialogue_tree.py) does a focused
second-pass over QUST/DIAL/INFO records using GRUP type-7 labels to resolve
INFO→DIAL parentage and PNAM fields for chain ordering.

DialogueTreeDialog (gui/dialogue_tree_dialog.py):

  • Left panel: collapsible QTreeWidget (Quest→Topic→Response)
  • Right panel: QGraphicsScene node graph — each INFO is a clickable card
    with a blue player-prompt section and a green NPC-line section; PNAM chains
    are drawn as directed arrows; parallel threads are separated by dashed lines
  • Detail panel: full text of selected response + Jump-to-NPC-Line /
    Jump-to-Player-Line buttons that navigate the main string table
  • Background QThread loading with progress indicator
  • Filter box collapses non-matching tree branches live
  • Works with localized plugins (shows StringID hex) and non-localized

Wired into Translation menu as "Dialogue Tree Visualizer…" (Ctrl+Shift+T).
Modeless window; re-opens the same instance if already visible.
_jump_to_esp_field() locates the exact (form_id, field_sig) row in the table.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ad900d1)

  • Add Lore RAG: local SQLite FTS5 lore database with UESP downloader and prompt injection

New files:

  • bethesda_strings/lore_db.py: LoreDB wraps SQLite FTS5 (trigram tokeniser,
    unicode61 fallback) with BM25-ranked search, upsert, delete-by-source, and
    list/browse methods. Apostrophes in FTS MATCH queries are stripped so
    "Va'ruun" finds articles reliably.

  • gui/lore_rag_manager.py: LoreRAGManager queries the DB for proper-noun
    phrases extracted from the source string and formats a compact inline snippet
    ("Lore context: [House Va'ruun] … | [Akila City] …") capped at a configurable
    character limit.

  • gui/lore_rag_dialog.py: Management dialog with three tabs — Index (stats +
    live search preview), Download (UESP MediaWiki API by category, background
    QThread worker), Import (local JSON file).

Integration:

  • TranslationRequest gains lore_snippet field; to_prompt() prepends it to the
    user turn (not system prompt) so Claude's system-prompt caching is preserved.
  • OllamaWorker and ClaudeTranslationWorker both query lore_rag_manager on the
    worker thread (same pattern as glossary_manager). Sub-requests (line-by-line
    / paragraph) inherit the parent's snippet via dataclasses.replace().
  • claude_client.translate() forwards lore_snippet to TranslationRequest.
  • AppSettings: enable_lore_rag (default off until user downloads data),
    lore_rag_max_snippet_chars. CONFIG_VERSION bumped to 26.
  • Translation menu: "Lore RAG Context…" opens the management dialog.
  • Settings dialog: Lore RAG QGroupBox with enable toggle + max-chars spinner.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f94cf1c)

  • Add Font & Glyph Checker: scan translations for unsupported characters

Parses Scaleform SWF font atlases (DefineFont2/3) and TrueType/OpenType
fonts (cmap format 4/12) to build glyph coverage sets, then flags any
translated characters that will render as tofu (□) in-game.

  • bethesda_strings/font_checker.py: SWF + TTF parser, built-in Starfield
    safe set (ASCII + Windows-1252 + full Cyrillic), SUGGESTED_FIXES table
    for common unsafe chars (NBSP, modifier apostrophe, zero-width spaces,
    soft hyphen, etc.), FontChecker.check_rows() returns structured results
  • gui/font_checker_dialog.py: modal dialog with font source pickers (SWF /
    TTF / game Data directory), per-character summary table with U+ codes and
    fix suggestions, per-string affected table with Jump buttons, Auto-fix All
    applies safe replacements via fix_applied signal, HTML/TXT export
  • gui/main_window.py: Translation > Font Glyph Checker… menu action,
    _open_font_checker(), _apply_font_fixes() via set_translated_text_batch()

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(7375483)

  • Add Character Persona Profiling: per-NPC voice for AI translation

Each character profile encodes register, temperature, and a system-prompt
addendum so the AI naturally adapts its style when translating a specific
NPC's dialogue.

  • bethesda_strings/character_profiles.py: CharacterProfile dataclass,
    ProfileManager (JSON persist + CRUD), ProfileAssignments (per-file
    string_id→profile mapping in config dir), 7 built-in Starfield profiles:
    Freestar Ranger (casual/0.25), SysDef Officer (formal/0.07), Crimson
    Fleet Pirate (rough/0.30), House Va'ruun Zealot (religious/0.15),
    UC Civilian (neutral/0.15), Robot/Automaton (technical/0.05),
    Narrator/Item Text (neutral/default)
  • gui/profile_editor_dialog.py: two-panel editor — profile list with Add /
    Duplicate / Delete; edit form with name, color picker, formality,
    temperature override, contractions checkbox, free-text addendum +
    "Regenerate from fields" button; unsaved-changes guard on navigation
  • gui/profile_assign_dialog.py: compact picker shown from the right-click
    context menu; shows all profiles with color badges; Assign / Clear /
    Cancel; double-click to confirm
  • gui/string_table.py: per-row profile background tint (HSV desaturation
    for light/dark mode), Status-column tooltip shows profile name; new
    assign_profile_requested Signal; Assign Character Profile… context menu
    item using existing _selected_source_rows()
  • gui/ollama_worker.py: TranslationRequest.character_profile field;
    to_system_prompt() appends "## Character Voice" addendum; temperature
    override at the API call site; profile_manager + profile_assignments
    attributes; lazy lookup in _translate_single() via dataclasses.replace()
  • gui/claude_client.py: translate() accepts character_profile; injects
    addendum via TranslationRequest; passes temperature to API when set
  • gui/claude_translation_worker.py: profile_manager + profile_assignments
    attributes; profile lookup per-request before Claude call
  • gui/main_window.py: ProfileManager + ProfileAssignments init at startup;
    workers receive profile_manager + profile_assignments; Translation >
    Character Profiles… menu action; assign_profile_requested signal
    connection; _reload_profile_tints() rebuilds row→profile map on file
    open; _open_profile_assign() handles picker + assignment persistence

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(07ae89c)

  • Add Audio/TTS Preview: hear translation timing vs original audio

  • gui/tts_engine.py: TTS abstraction for eSpeak-NG + Piper; WAV
    duration reader; syllable-based estimate fallback; AudioFileIndex
    (form ID → audio file) for auto-locating game audio

  • gui/audio_preview_panel.py: dockable QDockWidget with original
    audio browser+player, TTS synthesizer+player (background thread),
    and _TimingBar color-coded by tts/orig ratio (green ≤110%,
    orange ≤130%, red >130%)

  • gui/app_settings.py: CONFIG_VERSION 26→27; adds enable_audio_preview,
    tts_engine_type, espeak_voice/binary/speed, piper_binary/model,
    audio_dir, tts_auto_preview

  • gui/settings_dialog.py: Audio/TTS Preview settings group with
    engine dropdown, eSpeak voice+speed, Piper binary+model pickers,
    audio directory browser, and auto-preview toggle

  • gui/main_window.py: View menu Audio Preview toggle (Ctrl+Shift+A);
    dock init + apply_settings; _on_selection_changed pushes row data
    to panel; settings dialog propagates changes to panel

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(8604493)

  • Add Zen / Focus Mode: full-screen single-string distraction-free editor

  • gui/focus_overlay.py: FocusModeOverlay(QDialog) — frameless fullscreen
    window with dark-theme UI (GitHub-dark palette), read-only source
    panel + editable translation panel, pending counter, per-string status
    badge, and approve/skip/prev navigation

  • gui/main_window.py: View → Zen/Focus Mode (F11) toggle; _enter_focus_mode
    creates overlay with signal wiring; _on_focus_translation applies edits
    to the model; _on_focus_row_navigated syncs table selection;
    _focus_overlay=None init; commits unsaved text on overlay close

Keyboard shortcuts inside the overlay:
Ctrl+Enter — approve translation and advance to next pending string
F7 — skip to next untranslated without saving
Shift+F7 — go to previous string
Esc — exit focus mode (unsaved edit is committed automatically)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a10cc1a)

  • Add Multi-Monitor / Detached Panes for multi-screen workflows

gui/translation_editor_pane.py (new QDockWidget)

  • Large source (read-only) + translation (editable QTextEdit) editing area
  • Approve (Ctrl+Enter) + Revert buttons; status badge; string ID label
  • Fully floatable — drag title bar to any monitor
  • Updates on every selection change; emits translation_approved(row, text)

gui/detached_table_window.py (new Qt.Window)

  • Standalone window with a second QTableView sharing the main window's
    model AND selection model — clicking a row in either view syncs both
  • EditTriggers=NoEditTriggers keeps all editing in the main window
  • Auto-places on screen 2 when no saved geometry exists
  • Geometry saved/restored via QSettings("BSE", "BethesdaStringsEditor")
  • Safely replaces shared selection model on close so Qt doesn't delete it

gui/main_window.py

  • View menu: Editor Pane (Ctrl+Shift+E), Pop Out String List (Ctrl+Shift+L)
  • _toggle_editor_pane / _toggle_detached_table / _on_detached_table_closed
  • _on_editor_pane_approved: commits editor pane edits to model
  • _save_window_state / _restore_window_state: QMainWindow.saveState()
    persists all dock positions (docked + floating) across sessions
  • _on_selection_changed now pushes data to editor pane when visible
  • Detached table title updated on file load
  • Fix: _get_current_row, _on_focus_row_navigated, _get_focus_start_row
    no longer call mapToSource/mapFromSource (no proxy model in use)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(8cdbf8e)

Changed

  • Update README: add Ollama hub model links, spell-check, Claude backend

  • Add Ollama models table with hub links (0xra/bethesda-translate,
    0xra/bethesda-qc) and pull/cp commands at the top

  • Document Claude API backend as alternative to Ollama

  • Add Hunspell spell-check feature and installation notes

  • Add AI QC model (qcgemma4-st) to quality assurance section

  • Add UI translations section with supported locales

  • Add spell_checker.py and claude_translation_worker.py to project structure

  • Add create_qc_dataset.py to scripts section

  • Update requirements to reference requirements.txt with optional deps

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ec9a8b2)

  • Update NexusMods description: Ollama hub models, Claude backend, spell-check

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a3081cd)

  • Update system prompt bracket token rules with accurate Starfield token inventory

Replace incorrect/absent hardcoded tokens ([PLYR], [MALE], [FEMALE], [Firstname],
[Secondname], [F], [M]) with a 4-category system derived from scanning actual
Starfield txt files: (a) translate dialogue/skill tokens with Ukrainian examples,
(b) preserve input-binding tokens unchanged, (c) preserve NPC name/variable
placeholders, (d) translate ALL-CAPS status codes. Update Modelfile.gemma4-opus48
fallback SYSTEM prompt for consistency.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(787c617)

Fixed

  • Fix RST docstring formatting in detect_encoding to pass Sphinx build

Multi-line numbered list item with a continuation indent caused Sphinx to
report "Unexpected indentation" and treat warnings as errors (-W flag).
Rewrote item 5 as a single line and added blank line before the list.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d674dc4)

  • Fix README language table: switch to single-column to show all 11 codes

The two-column layout left Ukrainian's row with no right-side code or
language name (11 entries cannot fill a 2-per-row table evenly).
A flat Code | Language table has no empty cells.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(4f87e9e)

  • Fix Modelfile.qc: raise num_predict to 1024 for chain-of-thought budget

Model uses internal reasoning before emitting VERDICT; 256 tokens cut off
before the structured output block could be written.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(258064b)

  • Fix chunked translation: stop chunk markers bleeding into translated output

The context hint "[Part N/M of a longer text]" was prepended to the user
prompt text, so raw-prompt translation models (translategemma3-st) would
translate it along with the source, producing "[Частина 3/3 довший текст]"
in the output. This also confused the model into generating mostly-empty
output for that chunk.

Fix: never inject the hint into prompt_text. For raw-prompt models (no
system prompt), drop it entirely — the model is a specialized translation
model that can't handle metadata in the source text. For instruction-following
models, append it to the system prompt with an explicit "do not include this
in your output" instruction. Also strip any leaked markers from the response
as a safety net.

Also raise adaptive_num_predict cap from input_len//2 to input_len since
Ukrainian is typically ~20% wordier than English and the old cap could
truncate the last chunk.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(58ccf57)

  • Fix LOW_UKRAINIAN_COVERAGE false positives on good Ukrainian translations

Two bugs caused the check to fire on clearly-correct Ukrainian text:

  1. Dictionary only has lemmas, not inflected forms. Ukrainian is highly
    inflected (7 noun cases, adjective agreement, verb conjugation), so
    inflected forms like кредитів/оцінено/становить/відповідних never matched
    the lemmatised dictionary, consistently dropping coverage below 25%.

    Fix: also count a word as Ukrainian if it contains any Ukrainian-exclusive
    Cyrillic character (і / ї / є, including uppercase). These characters are
    absent from Russian, so their presence is a definitive Ukrainian signal
    regardless of inflection.

  2. raw[0].isupper() skipped all sentence-starting words (Усі, Сума, Ми…)
    treating them as proper nouns, shrinking the counted sample.

    Fix: change to raw.isupper() — skip only ALL-CAPS tokens (actual game
    codes / English acronyms like FRONTIER, CONSTELLATION, VEST), not ordinary
    Ukrainian words that begin a sentence.

Verified on all false-positive cases from quality_report_20260603_211806:
rows 11/21/22/33/49/71/82 all now score 42–70% (vs 5–22% before), well above
the 25% threshold.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(23d8f0e)

  • Fix three translation bugs identified in quality report 20260603
  1. %.0f (and other precision format specs) dropped by model
    term_protector STRUCTURAL_PATTERNS had %[sdfoxXc%] which misses
    format specifiers with width/precision modifiers like %.0f, %.2f,
    %5d, %-10s. Extended to %[-+0 #]\d(?:.\d+)?[sdfoxXciuFeEgGp%]
    so all printf-style format tags survive into the translation and are
    restored unchanged. Fixes MISSING_TAG on rows 24, 30, 48, 85.

  2. \r\n in source causing \n\n in translation output
    The structural newline tokenizer replaced \n with [[STRUCT_BREAK_SGL_N]]
    but left the preceding \r floating, producing \r[[…]] in the prompt.
    The model would then output two newlines or none at that position.
    Fix: normalize \r\n → \n (and bare \r → \n) before any tokenization,
    in both OllamaWorker and ClaudeTranslationWorker. Fixes NEWLINE_COUNT_
    MISMATCH on row 31 and similar strings.

  3. _fix_newlines auto-fix couldn't collapse excess newlines
    When a translation had MORE newlines than the original the method
    returned early ("already has enough"), leaving the surplus in place.
    Now collapses \n\n → \n (up to the excess count) before falling
    through to the existing insertion logic. Fixes auto-fix for the
    too-many-newlines case (rows 31, 61).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3f83bf6)

  • Fix three chunked-translation bugs for large strings
  1. num_predict in _call_ollama_chunk was capped at input_len — Ukrainian
    and other inflected languages are 20-50 % wordier than English, causing
    hard truncation of the last tokens in each chunk. Now 2× input_len.

  2. Separator lines (___…) stripped before sending to the model were never
    restored when _translate_chunked returned early, so they disappeared
    from the saved translation.

  3. restore_line_structure (fallback for when the model drops
    [[STRUCT_BREAK
    *]] tokens) was only wired into the non-chunked path;
    chunked results that lost paragraph breaks were never repaired.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(453cd75)

  • Fix [[STRUCT_BREAK_*]] token leaking into translated output

Root cause: structural newline tokens ([[STRUCT_BREAK_SGL_N]] /
[[STRUCT_BREAK_DBL_N]]) were injected unconditionally but only restored
through restore_text(), which is gated on term_protector being active.
When term protection is disabled the tokens survived verbatim in the
saved translation.

Three-layer fix:

  1. Explicit fallback in both _translate_single and _translate_chunked:
    when term_protector is None but token_map has STRUCT_BREAK entries,
    replace them with their actual newline values.
  2. Safety net at the top of _clean_translation: converts any remaining
    tokens regardless of how they survived — covers edge cases where
    restore_text()'s anchor algorithm places them in the result string
    verbatim before the iterative replacement pass runs.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c5b65cc)

  • Fix CI: move hunspell out of requirements.txt, install via apt in CI

hunspell needs libhunspell-dev system headers to compile its C extension,
so it cannot be unconditionally listed in requirements.txt — the build
fails on any environment that doesn't have that header.

  • requirements.txt: comment it out (with install instructions)
  • test.yml: install libhunspell-dev + pip install hunspell in the Linux
    system-deps step so CI still exercises the spell-checker code path

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(9dc6174)

  • Fix Pyright: suppress reportMissingImports for optional hunspell/spylls

Both imports are runtime-optional (inside init methods, not at module
level) and will not be present in all environments, so type: ignore is
the correct suppression rather than adding them to pyrightconfig.json.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b135995)

  • Fix three chunked-translation bugs causing truncation and lost paragraphs
  1. Paragraph separator loss: split_text_into_chunks puts the boundary
    delimiter (\n\n / [[STRUCT_BREAK
    *]]) at the START of the next chunk.
    _clean_translation then strips it via .strip(), so "".join() collapsed
    every paragraph break in multi-chunk book/log strings. Fix: peel the
    leading delimiter before sending each chunk, reattach it after translation.

  2. Context window too small: adaptive_num_ctx was calculated without a
    system-prompt budget (~500-1200 tokens), causing mid-word truncation when
    system prompt + chunk + Ukrainian output exceeded the 4096-token window.
    Fix: add 1200-token system-prompt allowance to the estimate; raise the
    chunk-path minimum ctx from 4096 to 8192.

  3. num_predict undercount: chunk path used input_len×2 while non-chunked
    used input_len×4. Ukrainian/Cyrillic output needs more tokens per source
    character than English. Raised to ×4 with a 512-token minimum.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5b9ae13)

  • Fix large-text translation: raise chunk threshold to avoid fragmenting book strings

Fine-tuned models (translategemma3-st) were trained on complete strings and produce
garbled output when given fragments with a "Part X of Y" prompt — echoing structural
tokens ([[STRUCT_BREAK_*]]) without the content between them, leaving blank lines and
isolated protected tokens (H20, O2) in the output.

Root cause: _CHUNK_TRANSLATE_THRESHOLD was 4000 chars, just below the ~4300-char
tokenized length of typical Bethesda book strings. Those strings were split into 3
chunks; the fine-tuned model does not handle partial context well.

Fix:

  • Raise _CHUNK_TRANSLATE_THRESHOLD 4000 → 12000 chars. Bethesda books are ≤8000
    chars; all fit in one 16384-token ctx call (the app default) without chunking.
  • Raise _MAX_CHUNK_CHARS 2000 → 4000 for the rare texts that genuinely exceed 12000
    chars (player-written notes with engine-lifted limits, modded content).
  • Add 1200-token system-prompt allowance to both the chunked and non-chunked ctx
    estimates so the context window is never undersized for rich prompts with glossaries.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(21c6060)

  • Fix thinking-model truncation for large strings (gemma4-opus48-st)

Root cause: the Gemma 4 thinking template was missing from Modelfile.gemma4-opus48
so think=false sent by the app had no effect — the model generated 2000-3000
reasoning tokens before the translation, exhausting the 4096-token num_predict
budget and truncating output mid-string.

Three-part fix:

  1. Modelfile.gemma4-opus48: Add {{ if .Think }}<|think|>{{ end }} guard to the
    template. Ollama sets .Think=false when the app sends think=false, so
    <|think|> is never injected and the model stays out of thinking mode.

  2. MODEL_CONFIGS num_predict 4096→8192 for "gemma4" and "gemma4-opus48-st":
    Even with thinking suppressed, these models are verbose. 8192 is the safe
    floor; the adaptive calculation may use more based on input length.

  3. Non-chunked path: model_config["num_predict"] is now used as a floor for
    adaptive_num_predict so any model config with a high num_predict value
    automatically gets that budget regardless of the app-level setting.

  4. _clean_translation: add a second greedy regex to strip unclosed <|think|>
    blocks (where generation was cut off before the closing tag was generated).

IMPORTANT: you must re-create the Ollama model after pulling:
ollama create gemma4-opus48-st -f Modelfile.gemma4-opus48

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2c9dfb3)

  • Fix two translation issues: bracketed text and lore terms (Settled Systems)
  1. system prompt rule 3: clarify that ALL content inside square brackets must
    be translated — dialogue choices, narrative notes, publisher remarks.
    Only the short game-engine codes ([PLYR], [MALE], [FEMALE], [Firstname],
    [Secondname], [F], [M]) are preserved unchanged. Fixes editorial notes
    like "[The second part of the book...]" being left in English.

  2. term_protector.py: add "lore" to SOFT_CATEGORIES so lore-tagged terms
    (including "Settled Systems") are not protected by default. They are only
    preserved when the user explicitly enables "Protect proper nouns and lore
    terms" in Settings. Previously "lore" was silently treated as a hard
    category, causing "Settled Systems" to always pass through untranslated.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d474672)

  • Fix bracket token categories against actual Starfield file inventory

Corrections from scanning all 130+ unique bracket tokens in actual Starfield
txt files:

  • Category (a) TRANSLATE: fix [Diplomacy]→[Diplomat] (wrong name); remove
    [Sneak] (key binding, not dialogue); add 17 missing dialogue/skill tokens:
    [Friendship], [Commitment], [Evidence], [Security], [Engineering], [Soldier],
    [Surveying], [Scavenging], [Xenobiologist], [Industrialist], [Robotics],
    [Cyberneticist], [MedPack], [Chems], [Syringe], [Wave], [Strikers]
  • Category (b) PRESERVE: add [Sneak], [Move], [Look], [Boosters], [Cruise],
    [Steady], [Accept], [Activate], [Cancel], [Confirm], [Reject], [Submit],
    [Edit], [Exit], [Play], [Leave], [Add], [All], [Yes], [No], [Click],
    [DataMenu], [Monocle], [SHMonocle], [Crew], vehicle/outpost/ship bindings;
    remove duplicate [Sprint]
  • Category (d) TRANSLATE: add sentence-case labels [Restored], [Decrypted],
    [Redacted], [Deleted], [Signed], [Optional], [Unfinished], [Temporary],
    [Unknown], [Maintenance]; add type labels [Volcanic], [Forest], [Desert],
    [Albino], [Common]

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2acd8b0)

  • Fix three structural newline bugs: garbled token, missing \n, English char leak
  1. [[STRUCT_STRUCT_BREAK_DBL_N]] leak: models sometimes double the STRUCT_
    prefix. Add re.sub with (?:STRUCT_)+ pattern in both the pre-restore path
    (so _restore_line_structure sees correct \n count) and _clean_translation
    safety net. The old exact-string check missed the garbled form entirely.

  2. Sections stuck together (missing single \n): _restore_line_structure was
    called with an under-count of \n because garbled tokens hadn't been
    converted yet, causing wrong proportional splits. Fixed by running the
    garbled-token fix before the \n count check.

  3. English character leak in Cyrillic words (e.g. "будь-dе"): single Latin
    char in a 600-char Cyrillic text falls well below the 75% distribution
    threshold. Add a mixed-script word scan to _needs_en_to_uk_retry — flags
    any whitespace-delimited token containing both Cyrillic and Latin alpha
    chars. All-Latin words (proper nouns, game tags) are never false-positives.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2057266)

  • Fix large-text truncation for thinking models (num_ctx and num_predict too small)

Root cause: think=false does not fully prevent chain-of-thought token generation
in Gemma 4 models. For the ~4000-char book text, the model was generating
~5000-6000 reasoning tokens before the actual translation started, consuming
most of the 8192-token num_predict budget and the 6492-token output window
(num_ctx=8192 - 1700 input tokens).

Fix: for any model with think_disabled=True, add a per-string thinking overhead
to both num_predict and required_ctx:
think_extra = max(2000, input_len // 2)

For the 4000-char book text (thinking model):

  • num_predict: 8192+2000 = 10192 (was 8192)
  • required_ctx: 6378+2000 = 8378 → picks 16384 ctx (was 8192)
  • available output: 16384 - 1700 = 14684 tokens
  • thinking 6000 + translation 2000 = 8000 → fits easily

Applied to both the non-chunked path and _call_ollama_chunk (chunked path).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f34a161)

  • Fix multi-sentence bracket content not translated (editorial note pre-protection)

Root cause: the [[STRUCT_BREAK_DBL_N]] token injected inside [The second part...]
(from the blank line between its two paragraphs) made the model treat the entire
bracket block as a formatting-preservation target rather than translatable text,
despite rule 3e in the system prompt.

Fix: protect the bracket delimiters themselves as [[EDITORIAL_OPEN_N]] /
[[EDITORIAL_CLOSE_N]] tokens before sending to the API. The AI then sees the
content as plain translatable paragraphs (no confusing [...] wrappers), and
restore_text() re-adds the [ and ] after translation.

Detection heuristic: all game bracket tokens are single words with no spaces
([Attack], [Lie], [CANCELED]). Any [content with spaces or newlines] is
editorial text. Applied before STRUCT_BREAK tokenization so raw newlines
inside the bracket block are visible to the regex.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ae9c6f6)

  • Fix editorial bracket translation: remove counterproductive EDITORIAL_OPEN/CLOSE tokens

Using [[EDITORIAL_OPEN_N]]/[[EDITORIAL_CLOSE_N]] tokens caused the model to treat
the entire bracket block as a preserve region (correctly following rule 2 "preserve
[[TK_…]] tokens unchanged"), keeping the content in English.

Fix: remove the token-wrapping approach entirely. Instead, update rule 3e with an
explicit multi-paragraph example showing [[STRUCT_BREAK_DBL_N]] inside brackets so
the model knows that embedded newline tokens don't make the bracket content a game
token — it still needs translation.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b994fd2)

  • Fix bracket content translation: skip STRUCT_BREAK tokenisation inside [spans]

Root cause: the model correctly follows rule 2 ("preserve [[TK_…]] tokens unchanged")
when it sees [[STRUCT_BREAK_DBL_N]] inside a [...] bracket — it treats the entire
block as a formatting token and leaves the English content untranslated.

Fix: apply STRUCT_BREAK tokenisation only to text between bracket spans.
Inside any [...] block, raw \n characters are left as-is so the model sees plain
translatable prose with no embedded formatting tokens. The regex skips both
[[double-bracket-tokens]] (TK/STRUCT_BREAK already injected) and all [...] spans
(including those that contain [[TK_...]] tokens from term protection inside them).

System prompt rule 3e simplified — the [[STRUCT_BREAK_DBL_N]]-inside-brackets
example is no longer applicable.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e759b00)

  • Fix untranslated multi-paragraph bracket spans: two-pass sub-translation

Extract [content\n\ncontent] spans as [[BRACKET_N]] placeholders before
STRUCT_BREAK so the main model call preserves them (rule 2), then
sub-translate each bracket's inner text in a separate _translate_one call
after restoration and replace the placeholder with [Ukrainian translation].

Reverts selective STRUCT_BREAK back to simple global replacement since
bracket spans are now extracted first and contain no raw newlines.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(868d6cb)

  • Fix sub-translation: use _translate_single (not non-existent _translate_one)

The AttributeError from calling the wrong method name caused the entire
_translate_single call to fail silently, leaving the string with its old
(English bracket) translation. Also set string_id=-1 on sub-requests to
prevent a false translation-memory hit returning the full book text.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(846b122)

  • Fix Pyright false positive: use unique local import name for bracket sub-req

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(44fee58)

  • Fix English bracket spans: post-translation scan instead of placeholder

Replace the placeholder ([[BRACKET_N]]) approach with a post-translation
scan. After restore_text(), search translated output for any [bracket spans]
with newlines and no Cyrillic characters (still English), then sub-translate
each one via a nested _translate_single call.

The placeholder approach failed because rule 2 only lists [[TK_]] and
[[STRUCT_BREAK_
]] — the model didn't know to preserve [[BRACKET_N]] and
would modify or drop it. The post-translation scan has no such dependency:
it operates on the final restored text, so inner content has actual terms
(not [[TK_*]] tokens) and the sub-call's term protection works correctly.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(67260c0)

  • Fix leaked/garbled [[...]] tokens: comprehensive post-restore cleanup

Replace the narrow (?:STRUCT_)+ regex with fuzzy patterns that catch all
model-invented STRUCT_BREAK name variants:

  • [[STRUCT__BREAK_DBL_N]], [[STRUCT_BREAK_DOUBLE_NEWLINE]] → \n\n
  • [[STRUCT_SGL_N]]], [[STRUCT__BREAK_SGL_N]] → \n
  • [[STRUCT_REDACTED]] and any other [[STRUCT_*]] → \n

Also add a restore pass for [[TK_*]] tokens that restore_text() missed,
followed by a final strip of any remaining [[word]] artifacts the model
hallucinated (e.g. [[TK_2e96fe_0]] not in token_map).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fb119d8)

  • Fix 5 model artifact leaks in _clean_translation

Strip artifacts the model generates that should never appear in game strings:

  • <image|> and other invented image tags
  • [Redacted] hallucinations (model censoring content instead of translating)
  • Extra close tags beyond what the original had
  • Hallucinated <Alias=...> tags not present in the original text

Also update the safety-net STRUCT_BREAK cleanup to use the same fuzzy
patterns as the main post-restore sweep (catches all model name variants).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c3d3739)

  • Fix newline/paragraph structure: translate paragraphs independently

Split multi-paragraph strings on \n\n before sending to the model,
translate each paragraph separately, and rejoin. This guarantees correct
paragraph breaks without relying on the model to preserve
[[STRUCT_BREAK_DBL_N]] tokens (which it routinely drops).

Single-newline structure within paragraphs still uses STRUCT_BREAK_SGL_N
with the existing _restore_line_structure fallback.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(df5c86c)

  • Fix leaked % format specifiers in _clean_translation

Model sometimes hallucinates %s/%d/%1$s etc. not present in the source.
Counts printf specifiers in original vs translated; removes the rightmost
extras (model artifacts tend to appear at the end of output).
Bare % (percentage signs) are unaffected.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ac558f0)

  • Fix SignalOverflow: use Signal(int, str, object) for translation_ready

Bethesda FormIDs are unsigned 32-bit (up to 0xFFFFFFFF). Declaring the
string_id parameter as int mapped it to C++ signed int (max 0x7FFFFFFF),
causing libshiboken RuntimeWarning on FormIDs in the 0x80000000–0xFFFFFFFF
range. Using object bypasses the C++ type coercion and passes the value
as a Python integer with no size limit.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c03c090)

  • Fix ruff lint errors: remove unused QMutexLocker import, drop extraneous f-prefix

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5e7a600)

  • Fix three translation bugs: tag forgetting, newline corruption, line count mismatch

  • Tag forgetting (<Alias=…>, <Global=…> etc.): add _restore_dropped_tags()
    static method that re-inserts any Bethesda XML alias/variable tags the model
    dropped, using proportional position tracking. Called after _clean_translation
    in both the chunked and single-string paths.

  • Newline corruption (\н instead of \n): models translate the N in
    [[STRUCT_BREAK_SGL_N]] to Cyrillic н, producing literal \н in the output.
    Fixed by replacing \нн → \n\n and \н → \n after the existing garbled-token
    cleanup, conditioned on whether STRUCT_BREAK tokens were actually injected.

  • Sentence/line count mismatch for multi-line format strings (%.2f, etc.):
    add line-by-line translation for strings with 3+ short lines separated by
    single \n. Each line is translated independently so the app controls
    structure and the model works on a minimal context where format specifiers
    are far less likely to be dropped.

  • System prompt rule 2 updated to list %.2f, %f, %g, %e, %i etc. explicitly
    and to instruct the model never to convert [[STRUCT_BREAK_*]] tokens.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5d57ee7)

  • Fix false SENTENCE_COUNT_MISMATCH on printf format-spec strings

_SENT_END_RE matched the '.' inside %.2f, %+.3g etc., counting each
format specifier as a sentence terminator. For the reported string
(6 × %.2f lines in original, 2 × %.2f in translation) this gave
ratio 0.33 < 0.4 and a spurious mismatch warning.

Add _FORMAT_SPEC_RE to strip printf-style format specifiers before
the sentence count is measured, and replace the two direct findall
calls with _count_sentences(). Both strings now score 0 sentence-end
marks → ratio 1.0 → no issue fired.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(596c869)

Other

  • Integrate qcgemma4-st AI quality check into QC workflow

  • gui/ai_qc_worker.py: new AiQcWorker(QThread) calls qcgemma4-st via
    Ollama HTTP API in parallel (ThreadPoolExecutor, 4 workers); emits
    result/progress/finished signals

  • gui/quality_checker.py: add QualityChecker.parse_ai_verdict() — parses
    VERDICT: GOOD / ISSUES_FOUND blocks from model output, handles
    chain-of-thought prefix by searching for last VERDICT: occurrence

  • gui/app_settings.py: add enable_ai_qc (bool, default False) and
    ai_qc_model (str, default "qcgemma4-st") settings; bump config to v22

  • gui/settings_dialog.py: add "AI Quality Check" group box with toggle
    checkbox and model name field

  • gui/main_window.py: _run_quality_check runs AiQcWorker when enabled,
    shows a cancellable QProgressDialog, merges AI issues into rule-based
    reports before opening QualityDialog

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c367ecf)

  • Extend quality checks to all supported target languages

Previously most language-aware checks were hard-coded for Ukrainian only.
Now they cover all 11 languages the app can translate into.

_check_untranslated
Added a second branch for EN→(non-EN) pairs: fires when the translation
is identical to the English source and contains ≥3 alphabetic words.
Short proper-noun-style strings (1-2 words) are left alone to avoid
false positives on brand names and abbreviations that legitimately stay
in English.

_check_english_leak
Removed the hard-coded "Ukrainian target only" guard. Now runs for any
non-English target when source is English — catches untranslated English
words in DE/FR/ES/IT/PL/PTBR/JA/ZH translations.

_check_source_leak
Extended beyond Ukrainian:
• Ukrainian target (existing): Russian-only chars + Russian vocabulary.
• Latin-script targets (DE/FR/ES/IT/PL/PTBR/CS) from a Cyrillic source
(RU/UK): flags ≥3 Cyrillic characters in the output.

_check_script_coverage (new)
For CJK target languages:
• Japanese (ja): warns when output contains no Hiragana, Katakana, or
CJK characters despite having ≥6 non-whitespace characters.
• Chinese Simplified (zhhans): same check for CJK block only.
Catches cases where the model returned Latin/Cyrillic text instead of
the expected script. Registered as LOW_SCRIPT_COVERAGE in RETRANSLATE_CODES
with a retry hint.

build_retry_hint
• SOURCE_LANGUAGE_LEAK hint generalised (was Ukrainian-specific wording).
• Added LOW_SCRIPT_COVERAGE hint.

Module-level constants added: _LATIN_SCRIPT_TARGETS, _CYRILLIC_SOURCES,
_CJK_TARGETS — single source of truth for language group membership.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5f42fd2)

  • Allow any Ollama model: editable combo + Refresh button

The model picker was locked to 2 hardcoded names. Now it is an editable
combo so any model name can be typed directly. A new Refresh button fetches
all installed models from /api/tags and populates the dropdown, preserving
the user's current selection. Removed Claude API model entries that did not
belong in the Ollama section.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fddc23c)

  • Handle Gemma 4 thinking mode in translation pipeline

Gemma 4 IT models emit … reasoning blocks by default which
would corrupt translation output. Two-layer fix:

  • Add "gemma4" to MODEL_CONFIGS with think_disabled:True so Ollama's
    think=false flag is sent for any gemma4:* model variant.
  • Strip /<|think|> blocks in post-processing as a safety net for
    any reasoning-capable model not in MODEL_CONFIGS (QwQ, future models, etc.).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(9a5536e)

  • Increase timeouts for thinking models on large text

Thinking models (gemma4, gemma4-opus48-st) generate 2000-4000 reasoning
tokens before the translation output, making 300s too short for large
strings (books, long descriptions).

  • Add "timeout": 600 to gemma4 and gemma4-opus48-st model configs
  • Scale model_timeout dynamically: +1s per 20 chars of protected_text
    (e.g. a 3400-char book gets 600 + 170 = 770s)
  • Apply the same scaling to the per-chunk timeout in _call_ollama_chunk
    so chunked paths also get sufficient time per chunk

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c058db1)

  • Warn about unsaved changes when closing settings dialog

Tracks a _dirty flag via findChildren connections (QLineEdit, QComboBox,
QSpinBox, QCheckBox, QSlider, QKeySequenceEdit). On Cancel or window
close, if dirty shows a "Discard / Cancel" confirmation.
Clicking OK clears the flag so the dialog closes silently.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(67ef051)

  • Release v0.2.2

Bump version in pyproject.toml; add v0.2.2 and v0.2.1 CHANGELOG entries.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(17263bf)

Removed

  • Remove nexusmods_description.html

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(48f0337)