Python orchestration core for the Kardenwort desktop window. It runs kardenwort to extract lemmas and sentence translations, invokes headless IntelliFiller to fill per-lemma translation columns (ru/de/ua/ipa/morphology), composes the combined HTML view, and exports favorited rows to a kardenwort-schema TSV for Anki import.
This repo is the backend — a pure orchestration module (stdlib-only Python, no GUI/OS dependencies) with a thin CLI wrapper. The display frontend is a per-platform shim; today it is an AutoHotkey v2 script:
Display frontend (Windows, AHK v2):
U:/voothi/20240411110510-autohotkeyscript:kardenwort-window.ahk
The frontend is display-only: it captures the selected text (via a per-platform hotkey/intent), renders the HTML this core emits, lets the user mark rows as favorites / edit cells, and forwards selections/deltas back here. It contains no linguistic logic.
- Visual Showcase
- Single Architectural Contract (portability anchor)
- CLI contract (called by the AHK shell / SendTo)
- Configuration (
config.ini) - Dependencies on Sibling Projects
- Processing pipeline (file-on-disk, SendTo per-stage)
- Inflection → lemma link & bidirectional selection
- Multi-session reliability
- TSV merge utility (SendTo "Kardenwort Merge")
- Source text saving & session restore
- GoldenDict Integration
- Integration Contracts & Formats
- Hover-highlight MVP (proof-of-concept)
- Project Structure
- Kardenwort Ecosystem
- License and Acknowledgements
I press Ctrl+Alt+Shift+F2 for En.

I select words in the Source Text or in the tabular part of the lemmas. A convenient mode of bookmarks (pointers) is supported, which allows you to quickly find the place of a word in translation and back.

Favorites are supported, a separate file with highlighted lemmas is created and sent to Anki.

To Anki A deck is created with the name of the TSV file. You can also download this TSV file yourself via Ctrl+Shift+I in a few clicks.

This is what the card looks like in Anki from the added deck (file).

This is what the directory with uploads looks like, with files created for each new launch in an open and understandable format, which can be edited and loaded into Anki.

It is possible to restore a session in Kardenwort Desk (expand your desk) via Sent to.

Supports restoring multiple groups of files in a directory, then these windows open in a cascade, numbered in their Title Bar and Taskbar Icon.

The boundary between frontend and backend is a stable, transport-agnostic contract: CLI today (argv → stdout), HTTP later (same payloads/responses). The orchestration logic — tokenizer, TSV, providers, merge, edit-race prevention — is written once here and never rewritten. Porting to Linux/Mac reuses this core unchanged (same stdlib); porting to mobile/web wraps the same module in an HTTP entrypoint. Only the frontend shim (hotkey/intent capture + HTML rendering) is rewritten per platform.
For command line transport, multi-line payloads (like render mode's HTML output)
are Base64-encoded to avoid shell quoting and UTF-8 encoding issues. This uses the
Base64 transport helper b64util.py on the backend and Lib/B64Util.ahk on the frontend (mirroring ftca's s_b64 pattern).
Render mode (produces HTML to stdout):
python kardenwort_desk.py render --text "<selected>" --language en --zid <session> [--config <path>] [--verbose | --debug]
Export mode (writes favorites TSV to the configured output dir):
python kardenwort_desk.py export --selection-manifest <path> --language en [--config <path>]
Edit-save mode (applies cell-edit deltas to the word TSV, atomic write):
python kardenwort_desk.py edit-save --deltas <deltas.json> --zid <session> [--language en] [--config <path>]
Merge mode (combines multiple TSV files into one, ordered by ZID):
python kardenwort_desk.py merge --files <f1.tsv> <f2.tsv> --target new [--config <path>]
Reprocess mode (clears AI fields for selected rows and runs IntelliFiller again):
python kardenwort_desk.py reprocess --selection-manifest <path> --language en [--config <path>]
Restore mode (opens a .txt or .tsv file and reconstitutes the desk window state):
python kardenwort_desk.py restore --file <ZID>-<slug>.txt [--config <path>]
All modes accept --config <path> (default: config.ini next to kardenwort_desk.py).
All paths, the favorites output directory, the headless IntelliFiller entrypoint
path, provider slots, and schema mapping are read from config.ini.
All sibling-project paths in config.ini are relative, resolved from the
location of this config.ini (mirroring kardenwort's own convention). The
sibling repos all live on the same level under U:/voothi/, so they are
referenced via ../:
[environment]
; Python interpreter for kardenwort (spacy-env venv)
kardenwort_python = ../20250825231214-spacy-env/Scripts/python.exe
; Kardenwort project (script + data files)
kardenwort_workspace = ../20241223170748-kardenwort
; Deep-translator (Google + DeepL providers)
deep_translator_python = ../20241122093311-deep-translator/venv/Scripts/python.exe
translate_google_script = ../20241122093311-deep-translator/translate_google.py
translate_deepl_script = ../20241122093311-deep-translator/translate_deepl.py
; DeepL API key secrets — point at translate-selection's settings.ini which
; contains [Security] SecretsPath (the actual secrets.ini) + Salt.
; The desk core reads the salt + secrets path from settings.ini, then
; deobfuscates the key (base64+XOR with salt, %%SEC%% marker), mirroring
; translate-selection.ahk's GetDeepLKey pattern.
deepl_settings_file = ../20240411110510-autohotkey/translate-selection/settings.ini
; IntelliFiller headless entrypoint (resolve to the installed entrypoint module)
intellifiller_headless = ../20251206123938-intellifiller-ai-addon-for-anki/IntelliFiller/headless_entrypoint.py
[translation_providers]
; [DEPRECATED] Use [pipeline] section instead.
; Each slot: google | intellifiller | deepl | combined
main_text_translation = combined
lemmas_translation = combined
; Pass --use-local-fork to translate scripts (default: true)
use_local_fork = true
[pipeline]
; The base translation provider (google, deepl)
base_provider = google
; The enrichment provider (intellifiller, none)
enrichment_provider = intellifiller
[triggers]
; Trigger for base translation. Options: auto (on load), manual (user must trigger)
run_base_translation = auto
; Trigger for enrichment. Options: auto (on load), manual (user must trigger)
run_enrichment = manual
[rendering]
; UI rendering mode. Options: progressive (stream stages as ready), monolithic (wait for all stages)
display_mode = progressive
[settings]
default_language = en
default_target_language = ru
; [DEPRECATED] Use [rendering] display_mode instead.
progressive_loading = true
; [DEPRECATED] Use [triggers] instead.
lazy_processing = llm_only
; Maximum source-word index distance allowed between parts of a split/separable verb construct
split_gap_limit = 60
; Favorites TSV output directory (relative or absolute)
favorites_output_dir = ./favorites
; Standalone schema-mapping file (same pattern as kardenwort-mpv's anki-mapping.ini)
; NOTE: kardenwort-mpv's anki-mapping.ini has WordSource=source_word (older mapping);
; the desk core needs col_lemma=WordSource (kardenwort.py's mapping: WordSource=lemma).
; Either point at a desk-local copy with the [desk_columns] section added, or
; update the shared file to add [desk_columns] + [desk_editable].
anki_mapping_file = ./anki-mapping.ini
save_source_text = true
merge_delete_sources = false
; NOTE: file_watcher_interval_ms is a display concern and lives in the AHK-side
; config.ini [Settings] section (FileWatcherIntervalMs), not here.
[languages]
; per-language lemma data files (under kardenwort_workspace) + IntelliFiller prompt names
en_lemma_index = data/en/en-news-2023-1m-words.csv
en_lemma_override = data/en/lemma_override_en.tsv
en_prompt = English Vocabulary Analysis and Translation JSON
de_lemma_index = data/de/deu-mixed-typical-2011-1m-words.csv
de_lemma_override = data/de/lemma_override_de.tsv
de_prompt = <TBD>
[timeouts]
; Subprocess timeout in seconds (kills the subprocess on expiry; never hangs)
translation_timeout = 60
intellifiller_timeout = 120Paths are validated at startup; a missing path produces a clear error naming the offending key.
This orchestration core coordinates and depends on several sister projects within the Kardenwort ecosystem:
-
Kardenwort Parser (
U:/voothi/20241223170748-kardenwort)- Repository: voothi/20241223170748-kardenwort
- Role: Core linguistic engine.
- Dependency: The desk core calls
kardenwort.pyin a subprocess to run morphological parsing, tokenization, lemma analysis, and generation of the initial lemmatized HTML structure.
-
IntelliFiller (
U:/voothi/20251206123938-intellifiller-ai-addon-for-anki)- Repository: voothi/20251206123938-intellifiller-ai-addon-for-anki
- Role: Headless LLM translation and details enrichment.
- Dependency: Called in Stage 2/Export to query LLMs and fill localized vocabulary parameters (e.g. IPA, transcription, translation variants, morphology) directly into the generated TSV schema.
-
AutoHotkey Frontend (
U:/voothi/20240411110510-autohotkey)- Repository: voothi/20240411110510-autohotkey
- Role: User Interface and Interaction shim.
- Dependency: Provides the physical desktop interface (
kardenwort-window.ahk). Captures system text selections, hosts the WebView component to render the desk core's HTML outputs, coordinates inline grid edits, and manages the sequence number cascade.
-
Deep-Translator (
U:/voothi/20241122093311-deep-translator)- Repository: voothi/20241122093311-deep-translator
- Role: Translation services broker.
- Dependency: Provides the backend integration script (
translate_google.py,translate_deepl.py) used by the pipeline to fetch baseline target translations of the selection block.
Each stage operates on files on disk and is independently re-runnable via
Windows SendTo. All state between stages is carried exclusively by the
standard kardenwort TSV/JSON files (schema from kardenwort.py +
config.ini [anki_fields]) — no hidden/in-memory state crosses a stage
boundary. Pipeline composition (which stages run, in which order) is
config-driven, so the final Anki import can be dropped (e.g.
stages = extract, fill) leaving the filled TSV+JSON on disk. If Import
fails, the files remain on disk for a manual retry via SendTo.
Stage 1 Extract (kardenwort) .txt/.srt → triple.word.<lang>.tsv (+ .json)
Stage 1b Edit (desk window) inline Excel-like cell editing → atomic save of the word TSV
Stage 2 Fill (headless IntelliFiller) word TSV → columns filled in place
Stage 3 Import (kardenwort_runner --import-only) filled TSV + .json → Anki
This core (kardenwort-desk) drives the same stages for the live window, and its "export favorites" produces a TSV that re-enters at Stage 2 or 3. Inline lemma/translation edits are held in-memory until an explicit Save (button / Ctrl+S), then written atomically by this core so a crash never corrupts the TSV.
The core tokenizes the original text (Python port of the kardenwort-mpv
tokenizer) and exposes a token→lemma map derived from kardenwort's
Quotation/WordSource/WordSourceInflectedForm columns. The window uses
this for bidirectional selection: selecting word(s) in the original text
highlights the corresponding lemma row(s) in the table, and vice versa.
For lemma rows with multi-word inflected forms (e.g., German separable verbs like ankommen with inflected form kommt an), the highlighter anchors highlights to the specific token positions that form the split construct in the source text rather than highlighting all string-matching candidate occurrences.
- Greedy Minimum-Span Selection: It recursively finds strictly-increasing, order-preserving tuples of candidate word occurrences within the text, selecting the combination with the minimum source-word index distance (greedy, non-overlapping).
- Three-State Highlighting:
highlight-purple(Purple): Applied to the specific token positions forming the successfully anchored construct (e.g., the specifickommtandanforming the construct).highlight-orange(Yellow/Gold): Applied to candidates mapped to single-word lemma rows (retaining all-occurrences highlighting).not-connected(Gray/No Highlight): Applied to candidate occurrences of multi-word rows that are not part of the anchored tuple (e.g., a strayanin a sentence that is not part of theankommenconstruct).
- Bidirectional Isolation: The
token_manifestrow_idsmetadata and JavaScriptfindTokenDatalookup are filtered by visual index position. Clicks or hovers on unanchored stray particles do not select or trigger active bidirectional highlighting for the multi-word lemma row.
The lemma table is frequency-ordered (most frequent lemma first, per the
language's --lemma-index-file).
Desk window layout:
Original text. ← tokenized, selectable
Translation of the original text. ← sentence translation
Table with lemmas and their translation. ← frequency-ordered, editable, bidirectional link
Many desk windows can run simultaneously, each with its own text and
independent save/close lifecycle — they never conflict. Each window session
gets a unique ZID (14-digit timestamp + process-unique suffix if needed);
its working TSV is ZID-prefixed, so two windows never write the same file.
All TSV writes are atomic (temp → backup-rename → atomic promote →
rollback-on-failure, per kardenwort-quiz's save_tsv), and an stdlib
advisory file lock guards the rare same-file case. The session ZID is the
end-to-end trace key across window, working TSV, temp/backup files, and logs.
Combines multiple selected triple.word.<lang>.tsv files into one, ordered by
ZID (timestamp) — for grouping parts/individual files into one logically
complete study unit (easier to study). Installed via install.py as a SendTo
entrypoint.
Merge target options:
- Create new (default):
<current-ZID>-merged.<lang>.tsv - Append to first: earliest-ZID file becomes the target
- Append to chosen: pick the target from a dropdown in the desk window
Source files are kept by default (non-destructive); merge_delete_sources
in config enables deletion after a verified merge. The merged file is written
atomically; mismatched schemas are refused.
When a session produces its working TSV, the desk core also saves the original
source text as a .txt file with the same ZID prefix
(<ZID>-<slug>.txt next to <ZID>-<slug>.<lang>.tsv). This is gated by config
option save_source_text (default: enabled).
The "Kardenwort Desk Restore" SendTo entrypoint (via install.py) opens
a .txt or .tsv file, finds its sibling (same ZID prefix, other extension),
and reconstitutes the desk window's working state (source text + lemma table +
translations + edit state) for continued work. If the sibling file is missing,
it opens with what's available and warns.
Kardenwort Desk provides a lookup subcommand that integrates directly with GoldenDict as an external program. It extracts, translates, and formats the given text on the fly.
The [goldendict] section in config.ini controls the lookup behavior:
lookup_ttl_seconds = 3600: How long to cache lookups (based on a SHA1 hash of the input text). The cache file is saved aslookup-<lang>-<hash>.tsvin the configuredgenerated_results_dir.run_intellifiller = false: Whether to run Headless IntelliFiller to populate IPA and Morphology. Enabling this adds significant latency but provides richer data.sections = source,translation,lemmas: Comma-separated list of sections to render in order.heading_source = Source Text: Heading label for the source text section.lemma_columns = inflected,lemma,ipa,morphology,translation: Columns to include in the lemma table.
Add these command lines as "Programs" in GoldenDict (Edit > Dictionaries > Programs).
English (HTML format, dark theme):
python "U:/voothi/20260629183335-kardenwort-desk/kardenwort_desk.py" lookup --text "%GDWORD%" --language en --format html --theme dark(This replaces the current En kW program)
English (Plain Text format):
python "U:/voothi/20260629183335-kardenwort-desk/kardenwort_desk.py" lookup --text "%GDWORD%" --language en --format text(This replaces the current dT-g En-Ru text program)
German (Combined format, light theme):
python "U:/voothi/20260629183335-kardenwort-desk/kardenwort_desk.py" lookup --text "%GDWORD%" --language de --format combined --theme light(This replaces the current German program)
The lookup-<lang>-<hash>.tsv cache files are stored in your generated_results_dir. They skip the extraction and provider lookup phases completely if the requested text hits the cache (TTL expires after lookup_ttl_seconds). You can clear the cache by deleting these files. Setting run_intellifiller = true will synchronously block the lookup until IntelliFiller completes, which can add a few seconds of latency; consider keeping it false for instant lookups if IPA/morphology are not strictly needed.
If you encounter issues with the new lookup command, the revert path is to swap the GoldenDict Program commands back to their previous raw scripts (En kW, dT-g En-Ru, or German scripts). The desk change is strictly additive and doesn't break or replace the underlying standalone extraction scripts.
Headless IntelliFiller is executed as:
python <intellifiller_headless> --tsv <tsv_path> --prompt <prompt_name>- Inputs: A word TSV containing vocabulary at
<tsv_path>and the prompt name (e.g.English Vocabulary Analysis and Translation JSON). - Outputs: Headless IntelliFiller updates the columns (e.g.
WordSourceMorphologyAI,WordSourceIPA,WordDestination) in-place.
kardenwort.py is invoked as:
python <kardenwort_workspace>/src/kardenwort/core/kardenwort.py --type word --stdout-format html --text <text> --lemma-index-file <lemma_index> --lemma-override-file <lemma_override> --sentence-context-size 0- Inputs: Selected text, target language index and overrides.
- Outputs:
- HTML content printed to stdout (containing lemmatized spans).
- A TSV file (
<ZID>-<slug>.<lang>.tsv) and optional JSON metadata produced via--output-file.
For exporting favorites, the frontend creates a JSON file containing the selected row indices/lemma mappings:
{
"tsv_path": "U:/voothi/...",
"selected_lemma_ids": ["lemma1", "lemma2"]
}The desk core reads this manifest, extracts the matching lines from the session's working TSV, and outputs a subset TSV into the favorites directory.
This is an opt-in, client-side-only proof of concept to validate bidirectional word hover highlighting and click bookmarks before adopting the full server-side tokenization and theme-color integration.
In kardenwort-window/config.ini, under [Settings]:
HoverHighlightMvp = 1(default0) — Enables the WebView overlay injection. Set to0to completely disable and revert to standard rendering.HoverHighlightMvpBookmarks = N(default3) — The capacity of the rotating bookmark click-to-pin ring buffer.
- Word Hover: Hovering over a word in either the source text or the translation block will highlight matching tokens on the other side using cyan (
#39c5ffon dark/white themes,#0969daon light themes). - Word Bookmark Pin: Clicking a word pins it with an amber highlight (
#f0883edark,#bf8700light). Up toNbookmarks can be active; theN+1-th click evicts the oldest pin. Clicking a pinned word unpins it. - Escape Key: Pressing
Escclears all currently pinned bookmarks.
- Tokenization Drift: Client-side JavaScript tokenization using a simple regex (
\p{L}) may occasionally drift from the desk's server-sidetext_tokenizer.pyon exotic Unicode boundaries. - Fixed Colors: Unlike the main plan which supports dynamic themes via
theme_colorsfrom the desk, this MVP uses fixed cyan/amber colors with branches on dark/light/whitebodyclasses.
This MVP overlay is designed as a host-side overlay. When the full 20260702154120-translation-source-highlight change is implemented, you can retire this MVP by setting HoverHighlightMvp = 0 in config.ini. No code reverts are needed on the backend.
.
├── .agent/ # Agentic configurations and workflows
├── docs/ # Documentation and conversation logs
├── kardenwort_desk.py # Main CLI entrypoint
├── b64util.py # Base64 transport utility
├── requirements.in # Unpinned Python dependencies list
├── requirements.txt # Hashed, pip-compile generated lockfile
├── vendor/ # Vendored third-party dependencies
├── config.ini.template # Configuration template
├── release-notes.md # Release changelog and history
└── README.md # Project documentation
This project natively supports a hybrid dependency model. You can either use a standard Python environment or a local vendored package for a zero-install runtime.
1. Package Environment (Standard)
If you add or update packages in requirements.in, regenerate the secure hashes for requirements.txt by running:
pip install pip-tools
pip-compile --generate-hashes requirements.in -o requirements.txtTo install: pip install --require-hashes -r requirements.txt
If you plan to run the test suite locally, you must also install the Playwright browsers by running:
playwright install2. Vendoring (Zero-Install)
To vendor dependencies locally (bypassing pip at runtime), install them directly into the vendor/ directory:
pip install -r requirements.txt --target vendor/The application will automatically prioritize loading packages from the local vendor/ directory if it exists.
This project is part of the Kardenwort environment, designed to create a focused and efficient learning ecosystem.
This project was created by and is maintained by Denis Novikov (voothi).
It is licensed under the MIT License. See the LICENSE file for details.