You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: rozšíření diktování o obnovu, profily a lokální STT - #38
AudioRecorder poskytuje koherentní snapshot hlasitosti/stavu a RecordingSnackbar zobrazuje skutečné zařízení, čekání na vzorky a úroveň
Chyba zařízení se odlišuje od tichého, ale zachyceného vstupu; po selhání startu či streamu se neodesílá částečný WAV
Bezpečný výstup a obnova
type_text, _TrayApp._deliver, HotkeyListener a RecoveryDialog ověřují cíl, evidují jednotlivé pokusy a umožňují explicitní copy/arm/retry
Typování je podmíněno shodou HWND+PID; počty SendInput neprokazují viditelný text a nic se automaticky neposílá znovu
Výsledky a historie
DictationRecord, HistoryStore a session worker oddělují raw text, rewrite, pokusy a trvalé záznamy
Opt-in historie šifruje DPAPI pouze textové výsledky; jedno audio pro STT retry zůstává v RAM
Vocabulary a profily
Normalizace slovníku, per-provider STT prompt opt-in, profily raw/clean/custom a mapování úplné cesty executable
Ovlivnění nové session je deterministické; poškozená uložená data se nepřepisují potichu
Konfigurace a proces
Migrace provenance promptu, keyless STT, single-instance lock a řízené dokončení workeru při ukončení
Zachované uživatelské prompt texty se nerozpoznávají jako default jen podle shodného obsahu
Kontext a pozadí
Rozsah odpovídá 9f78dd01bbd2e2304c153ca89a77d9998438c8dc...77f175bd27283c3fe8ba87e6212e7bde54627b35: 3b0ebd6 (mikrofon), eace21f (provenance promptů), d533a74 (zbývající roadmapa), 75a674f (font-aware meter) a 77f175b (deterministický viewport jeho regrese). V diffu je 40 souborů včetně obou dřívějších neuvolněných změn; žádná není vynechána. Původní pinned výňatky z d533a74 zachycují mechanismy, které zůstaly stejné; následná oprava metru je aktualizovaná v Detailu 1 a nová regrese v Detailu 7.
Historie zůstává defaultně vypnutá a uchovává posledních 1–100 textových záznamů přes DPAPI; jedno audio po chybě STT je pouze v RAM a lze jej explicitně zkusit znovu bez automatického výstupu. Výstup do schránky je samostatná operace. U typování se před odesláním kontroluje foreground HWND+PID; selhání/skok post-key nebo částečný send se eviduje jako pokus a nikdy se automaticky neopakuje. Úspěšná kopie na nativním Windows vyžaduje potvrzené Qt clipboard ownership. Původní schránka se nečte ani neobnovuje.
Recovery hotkey je ve výchozím stavu Ctrl+Alt+Shift+V; vložení se armuje a dialog skryje, uživatel přepne do cílové aplikace, potom zkratku stiskne a plně uvolní včetně modifierů. Escape arm zruší a nemůže být recovery triggerem. Injektované klávesy procházejí hookem bez změny fyzického shortcut state. Windows RegisterHotKey nemusí odhalit konflikt s aplikací používající pouze low-level hook.
Jazyk Auto a .env fallback zachovávají rozdíl mezi chybějícím klíčem a explicitně uloženou prázdnou hodnotou. Vocabulary je omezena na 128 termínů, 128 znaků/termín a 8 000 znaků renderovaného kontextu; poškozená hodnota se uchová do explicitní opravy. Prompt pro STT má nezávislý opt-in pro každý provider. Profily limitují custom katalog na 32; migrace respektuje default_v2 versus legacy_saved/user_saved, včetně uloženého prázdného promptu a promptu totožného s defaultem. Globální AI off vybere raw, jinak platí Manual nebo Automatic podle přesné normalizované cesty executable a nakonfigurovaného defaultu. Nastavení profilu i cíle se zmrazí na začátku session.
Roadmapová položka 4 je odstraněná a není obsahem tohoto MR. README ani přijaté detailní roadmapové specifikace nejsou nově přepsány tímto popisem. Nejde o tvrzení publikovaného release. Příčina problému s Notepadem zůstává nepotvrzena a není tvrzeno, že byla diagnostikována či opravena.
Slovník a nezávislé povolení multipart prompt pro STT endpointy
WindowIdentity(hwnd, process_id, executable_path)
injector.py
Typování porovnává HWND+PID; executable je volitelná metadata pro profile mapping
DeliveryAttempt
results.py
Rozlišuje kopii, text eventy, post-key a withheld; neprohlašuje viditelné vložení
APP_DIR/screamer.lock
main.py
Zámek jediné instance pod datovým adresářem; získán před načtením konfigurace
Detail změn
Detailní index obsahuje 376 změněných funkcí včetně vnořených callbacků/lambda a test/fixture metod. Kvůli limitu délky GitHub těla jsou samostatné výňatky, pinned source odkazy a vysvětlení každé položky publikovány v sedmi navazujících komentářích tohoto PR:
Detail 1: audio, config/migrace, hotkey, injection, recovery view, history, STT/rewrite a meter (88 položek).
Malé preference zůstávají v atomickém INI. Privátní sidecars nejsou INI klíče: uchovávají poškozené kolekce a přítomnost explicitně uloženého Auto. Primární a fallback STT prompt mají oddělené defaultně vypnuté flagy. Textová historie používá samostatný history.enc, nikdy audio/credentials/system prompt.
RewriteProfile je frozen ID/name/kind/prompt; AppMapping je executable-path/profile-ID. Catalog JSON v1 obsahuje pouze custom profily a mappings; built-ins jsou code-owned. Řízená migrace zachová každý uložený legacy/user prompt a výchozí prompt zůstává konzervativní, bez automatického povolení LLM. Je to instrukce modelu, nikoli záruka fidelity.
Frozen records oddělují modelový výsledek od DeliveryAttempt (čas/mode/copy/text count/state/post-key/reason). Nové AppError hodnoty rozlišují withheld output, clipboard, history a config chyby; bridge přidává recovery request/cancel signály. Dokumentace aktualizuje current API a akceptační stav a přidává source/runtime-pinned lokální STT návod. Odstranění roadmapové položky 4 zachovává sjednaný scope, nikoli tvrzení nové diff/inspection feature.
Ověření
Provedené ověření
Předpublikační výsledky předané pro source commit d533a74886529f5e0b0822d89b63d65246ed763d:
Linux unittest suite: 287 testů, 282 pass, 5 Windows-only skips.
Po navazující font opravě: Linux 288 testů, 283 pass, 5 Windows-only skips; ostatní gate commands opět exit 0.
python -m compileall src/ tests/ .github/scripts/: exit 0
python -c "import src; print('OK')": exit 0
ruff check src/ tests/ .github/scripts/: exit 0
ruff format --check src/ tests/ .github/scripts/: exit 0
git diff --check: exit 0
Sedm nezávislých feature review (1,2,3,5,6,7,8): všechny review findings označeny resolved.
Pinned Speaches v0.8.3 Linux CPU/int8 + veřejný JFK sample: keyless multipart smoke PASS; viz docs/LOCAL-STT.md.
Výsledek:
Jeden nefatální GC warning se objevil v linuxovém test běhu; jeho dopad nebyl označen za selhání.
První Windows CI na 3.11/3.12 odhalilo ořezávání metru; původní dvě regrese následná produkční oprava opravila. Doplněný test velkého fontu má explicitní ample viewport, aby byl nezávislý na offscreen monitoru. Finální Windows Python 3.11/3.12: 288 testů, OK v obou jobs. EXE build, lint, audit, CodeQL a Linux compile/import jsou úspěšné nad headem 77f175bd27283c3fe8ba87e6212e7bde54627b35; zaseknutá apt instalace byla zrušena a pouze nedokončený job na stejném SHA znovu prošel. Žádný neúspěšný check nebyl obejit. Skutečný desktop/hardware test ani potvrzení příčiny Notepad potíží provedeny nebyly.
Toto PR tělo a appendixy pouze popisují předané výsledky; žádné další testy, buildy ani review nebyly spuštěny.
Přesné změněné testovací metody jsou rozepsány v appendicích; jejich přítomnost v diffu není tvrzení o právě provedeném běhu nad rámec výsledků výše.
Nasazení
Bez nových third-party dependencies. Není tvrzena automatická datová migrace mimo zdrojově popsané AppConfig/QSettings změny. Historie je nová opt-in DPAPI persistence pro aktuální Windows účet. Lokální STT je volitelná konfigurace endpointu, nikoli lokální-only síťová politika. Stávající release workflow nebyl měněn; nejvyšší feat: změna od publikované v1.2.0 určovala minor verzi v1.3.0.
Dokončené nasazení: PR merged do main jako 554cedbf796af3cfee4620e0fd5e82c6af4a8624. Main CI 36926003567, CodeQL 36926003470 a Release 36926233488 uspěly. v1.3.0 je publikovaná. Stažený 89 722 553 B ZIP má SHA256 9b240ee40a345671430529bfdb6ae2caa30610050a641483f0d4e51fdafb225e, shodný s .sha256 i GitHub asset digestem; ověřeno ZIP CRC, Windows PE x64, Qt6Core/Widgets/qwindows a absence zabalených uživatelských dat. Runtime desktop/hardware gate zůstává otevřená.
Kompatibilita / dependencies
Zdrojová větev feat/dictation-roadmap → cílová main; base 9f78dd01bbd2e2304c153ca89a77d9998438c8dc, source 77f175bd27283c3fe8ba87e6212e7bde54627b35. Všech pět commitů je zahrnuto. Merge proběhne pouze po zelených CI/CodeQL; user výslovně autorizoval admin bypass povinného PR approval, nikoli bypass neúspěšných kontrol.
Worker provádí síťové STT/rewrite; Qt main thread vlastní UI, record, clipboard a konečné rozhodnutí o výstupu. Busy zámek workeru se uvolní až po terminal signal i finished event.
Raw text se zachytí před rewrite; pending audio přežívá v RAM jen do úspěšného STT nebo ukončení procesu. Recovery retry nikdy automaticky nevkládá text.
Jediná historie je opt-in textová persistence; audio, API klíče ani system prompt se do ní neukládají. Local STT nebrání cloud fallbacku nebo LLM requestům.
Reviewer focus
Při review má smysl zaměřit se hlavně na hranice stavů: správné spárování session ID při raw_ready/terminal/finished; podmínky, za kterých může být text automaticky či ručně odeslán; přesné rozlišení textových eventů od post-key; fail-closed persistence a zachování poškozených katalogů/prompt provenance. Skutečné Windows foreground, keyboard-hook konflikty a audio hardware zůstávají manuálním ověřením.
Out of scope
Tento MR záměrně neřeší:
Roadmapovou feature 4, odstraněnou ještě před tímto rozsahem.
Potvrzení nebo tvrzení o opravě Notepad root cause.
Certifikaci Windows desktop/hardware chování, Windows CI, offline Windows Speaches ani local-only privacy enforcement.
Vytvoření/publikaci PR, čekání CI/CodeQL, merge, tag nebo release; tyto kroky provede caller.
PR detail — src/main.py: session, delivery, Recovery a shutdown
Zdrojový snapshot d533a74886529f5e0b0822d89b63d65246ed763d; základní snapshot 9f78dd01bbd2e2304c153ca89a77d9998438c8dc. Níže je úplný symbolový rozsah tohoto detailu podle coverage ledgeru: každá funkce i evidovaný callback/lambda má vlastní sentinel, pinned odkaz, tělo/expression a konkrétní vysvětlení. Odkaz source ukazuje na novou verzi; u změněných/odstraněných funkcí je uveden i odpovídající základní rozsah.
Jak: Po konfiguraci inicializuje oddělená místa pro aktivní session, dva terminální checkpointy, jeden čekající WAV, poslední záznam a kandidátní rewrite; načte history a sestaví tray/hotkey. Změna: Base měl pouze _session_config, worker a základní UI. Proč: Nový lifecycle potřebuje explicitní vlastnictví session, Recovery dat i dokončení workeru; konfigurace dál načítá a případně importuje z .env stejnou cestou.
Jak: Vytváří exkluzivní radio volby a vrací vytvořené submenu, aby jej volající mohl například zakázat při poškozené konfiguraci. Callback vybírá pouze při přechodu na checked. Změna: Base funkce vracela None; nyní vrací QMenu. Proč: Tray umí znepřístupnit konkrétní submenu, když výběr rewrite profilu vyžaduje opravu.
_TrayApp._apply_rewrite_profile — efektivní profil na kopii konfigurace
Jak: Resolver vybere profil; Raw vypne LLM, Clean nastaví vestavěný prompt a custom nastaví profilový prompt. Mění pouze předanou konfiguraci a vrací ID profilu. Změna: Přibyl efektivní profil pro každou session. Proč: Výsledek resolveru se aplikuje na session snapshot, nikoli na trvalou uživatelskou konfiguraci.
_TrayApp._rebuild_menu — aktuální ovládací prvky tray
try:
custom, _mappings= (
decode_rewrite_catalog(c.rewrite_catalog)
ifc.rewrite_catalogisnotNoneelse ([], [])
)
resolve_rewrite_profile(c, None) # Validate persisted selections, even when AI is off.except (TypeError, ValueError):
submenu=self._add_choice_submenu(
"Rewrite Profile — repair required",
[],
"",
self._set_rewrite_profile,
)
Jak: Znovu skládá Enabled, režim, hotkey, jazyk, post-key, rewrite a akce Settings/Recovery/Exit. Profily validuje i při vypnutém AI; chyba zobrazí zakázanou volbu „repair required“, jinak nabídne Automatic, vestavěné i custom profily. Změna: Přibyly Recovery, stav Raw při vypnutém AI a profilový výběr. Proč: Poškozený catalog se nemá tvářit jako platná nebo automaticky prázdná volba.
_TrayApp._make_listener — instalace dictation i Recovery chordu
Jak: Převádí uložený režim a hotkey na listener; při neparsovatelném dictation hotkey použije Ctrl+Alt+Space. Předává také parsovaný recovery hotkey a spustí listener. Změna: Base předával pouze dictation chord. Proč: Recovery zkratka sdílí listener, ale má vlastní konfiguraci.
Recording, session a worker
_TrayApp._start_recording — zmrazení session před změnou UI fokusu
Jak: Na Qt vlákně nejprve kopíruje config a zachytí foreground identity, vyřeší profil podle executable path, pak vybere audio zařízení a spustí capture. Konfigurační chyba vrátí UI do Idle a oznámí CONFIG_INVALID; audio chyby také obnoví Idle. Změna: Base pořídil jen config copy před audio startem; nyní session sváže config, HWND/PID/exe, ID, čas a profil. Proč: Změny tray během síťové práce nemění politiku právě nahrané dikce.
capture=self._recorder.snapshot()
ifcapture.capture_errorisnotNone:
# stop() reports the retained error once and discards incomplete audio.self._cancel_recording()
return
Jak: Pokud se stále nahrává, čte jeden snapshot; capture error ruší a zahazuje záznam. Jinak zobrazuje skutečné jméno/unknown vstupu, označí system default podle snapshotu configu a předává clamped RMS i informaci o callback datech. Změna: Base neměl polling úrovně. Proč: UI rozlišuje čekání na data a naměřené ticho; transient audio stav se zde nemění na vlastní diagnózu.
_TrayApp._finalize_recording — uzavření capture a předání WAV
Jak: Zastaví oba timery, přepne na processing a zastaví recorder; chyba nebo prázdný WAV ukončí session bez workeru. Jinak spustí worker se session. Odložený restart hotkey provede v finally. Změna: Base vytvářel worker přímo a neměl session ID/checkpoint. Proč: Jedno startovací místo workeru sdílí normální i Recovery cesty; restart listeneru neodstřihne release starého HOLD hotkey.
Jak: Zastaví limity/polling, odstraní session a zastaví recorder; případnou chybu oznámí, dokončí čekající restart hotkey, přepne na Idle a aktualizuje Recovery. Změna: Base neměl level timer, _session ani refresh Recovery. Proč: Zrušení (například při Disable nebo chybě capture) nesmí poslat neúplný zvuk ke STT.
Jak: Vyžaduje nepřítomnost workeru, nastaví session/recovery flags a propojí raw checkpoint, terminal a finished signály; zobrazí Processing, obnoví Recovery state a spustí QThread. raw_text značí rewrite rerun, stt_only retry STT. Změna: Base připravoval worker pouze při finalize a signály succeeded/failed/cancelled obsluhovaly odděleně. Proč: Jednotný terminální výsledek usnadňuje recovery, rušení i shutdown.
Jak: Sestaví DictationRecord s ID/časem session, raw textem, warnings, jazykem, executable cíle a profile ID. Rewrite je pending jen při zapnutém LLM a ne-STT-only cestě. Změna: Nový společný záznam checkpointu. Proč: Raw může být uchován před dokončením rewrite; retry STT ani Raw profil nesmí vykazovat nevyžádaný rewrite.
_TrayApp._on_raw_ready — nezávislý checkpoint po STT
ifsessionisNoneorsession.id!=session_id:
returnself._pending_audio=None# Terminal can reconstruct raw if it is handled before this checkpoint.ifself._terminal_handled:
returnself._retain(self._record(session, raw, warnings))
Jak: Přijímá checkpoint jen pro odpovídající aktivní session, po úspěšném STT maže retry WAV a uchová raw, pokud terminální výsledek ještě nebyl zpracován. Změna: Přibývá samostatný raw_ready signál oproti base, který předával až hotový PipelineResult. Proč: Raw přežije případné selhání/rušení rewrite; terminální payload jej umí obnovit i při opačném pořadí doručení signálů.
_TrayApp._on_terminal — sessionově párovaný výsledek a output gate
Jak: Odmítá cizí/duplicitní terminal. Raw výsledek buď aktualizuje jako samostatný rewrite kandidát, nebo retainuje zdrojový záznam a doručuje jen mimo recovery-only cestu; automatický výstup vyžaduje _automatic_allowed() i necancelled terminal. STT chyba ponechá WAV v RAM, warnings/errors se hlásí a worker release začíná až po zpracování terminalu. Změna: Base rozděloval úspěch/chybu/rušení a ihned odpojoval config. Proč: Session ID, raw-first checkpoint a explicitní automatičnosti gate zachovávají výsledek bez automatického výstupu u Recovery akcí.
_TrayApp._on_worker_succeeded — odstraněný slot starého protokolu
config=self._session_configself._session_config=Noneif (
notself._cancel_event.is_set()
andself._enabledandnotself._exitingandself._settings_dlgisNone
):
# Disable and injection share the UI thread: once this check passes,# no Disable event can interleave with the batched SendInput call.assertconfigisnotNone
Jak: Base slot přímo rozhodoval, zda předat výsledek do type_text, a pak hlásil warnings/Idle. Změna: Odstraněn spolu se succeeded signálem; terminal nyní nejprve retainuje výsledek a používá společnou _deliver cestu s typem výstupu, cílovou identitou a evidencí pokusu. Proč: Úspěšná transkripce už není totožná s prokázaným doručením; explicitní stav pokusu patří do Recovery záznamu.
_TrayApp._on_worker_failed — odstraněný slot samostatné chyby
Jak: Base čistil config, zobrazil STT/error zprávu a nastavil Idle. Změna: Odstraněn; _WorkerThread nyní vloží chybu do _Terminal, kterou obslouží _on_terminal, kde STT fail může uchovat původní WAV pro Recovery. Proč: Samostatná failed větev neuměla spojit chybu s raw checkpointem, session ani jednorázovým retry zvuku.
Jak: Base při zrušení jen mazal session config a nastavoval Idle. Změna: Odstraněn; zrušení je nyní terminální payload s session ID a případným raw/warnings, který projde společným terminal/release workflow. Proč: Jedna terminální hranice umožňuje shutdownu převzít výsledek i když Qt queued callback již nepřijde.
_TrayApp._on_worker_finished — druhá podmínka uvolnění
Jak: Označí skutečný QThread finished a zkusí společný release. Změna: Base okamžitě nastavil _worker = None po finished. Proč:finished a obsluha terminálního payloadu jsou oddělené události; busy gate se nesmí uvolnit, dokud nenastaly obě.
_TrayApp._release_worker — busy gate vyžaduje terminal i finished
Jak: Dokud není zpracován terminal i dokončen QThread, nic neuvolní. Poté vyčistí session, worker buď ponechá jako retired pro shutdown join, nebo naplánuje deleteLater, obnoví stav/Recovery a případně dokončí Exit. Změna: Base čistil worker přímo ve finished slotu. Proč: Když terminal callback dorazí později, hotkey busy gate zůstane zavřený; při Exit se zachová reference pro join.
_TrayApp._automatic_allowed — podmínky automatického výstupu
Jak: Vrací povolení jen pokud není cancel, aplikace je enabled, neodchází a Settings dialog není otevřen. Změna: Nový pojmenovaný gate nahrazuje rozptýlenou podmínku úspěšného base slotu. Proč: Automatické copy/type a další kontrola před typingem mají sdílet aktuální UI/policy stav.
Jak: Cíl je stejný pouze pokud obě identity existují a shodují se HWND i PID. Změna: Nový gate pro automatický/manual injection flow. Proč: Názvy oken ani executable path samy o sobě neidentifikují zachycenou HWND/PID dvojici; ani tato kontrola nedokazuje caret či konkrétní control.
_TrayApp._deliver — evidence kopírování a guarded SendInput
Jak: Založí DeliveryAttempt; zaznamená suppression, kopíruje pro příslušné režimy a na Windows následně ověřuje clipboard ownership. Typování vyžaduje aktuální stejný HWND/PID, ne-vlastní process a (u automatického režimu) nový _automatic_allowed check; type_text dostává expected_target. Zaznamená submitted-event counts/skipped post-key nebo chybu; neslibuje viditelný text a nic automaticky neopakuje. Změna: Base volal type_text přímo ze success slotu bez target identity/clipboard/recovery evidence. Proč: Výsledek ukládá pozorovatelný pokus, nikoli nepodložené tvrzení o vložení textu.
Jak: Resetuje committed view; jen při zapnuté historii načítá/trimuje podle limitu a vytvoří množinu ID. ScreamerError hlásí jako storage failure. Změna: Base neměl history store. Proč: Trvalá historie je opt-in; chybné/nezpřístupněné úložiště nezruší RAM Recovery.
_TrayApp._persist — jediná persistence hranice záznamu
Jak: Při vypnuté historii nic nezapisuje; jinak upsertuje do HistoryStore a synchronizuje ID, při ScreamerError hlásí selhání. Změna: Nová persistence boundary. Proč: RAM záznam není podmíněn diskem a encrypted history zůstává odděleným, opt-in store.
Jak: Nastaví latest před pokusem o persistence a obnoví Recovery view. Změna: Nové společné retainování raw i terminal výsledků. Proč: Při chybě historie zůstává záznam dostupný v RAM, jak také uvádí current Implementation.
ifself._candidateisnotNoneandself._candidate.id==record.id:
self._candidate=recordelse:
# A failed commit must not lose an attempt on an older selected entry.self._latest=recordself._persist(record)
Jak: Attempt na aktuálním rewrite kandidátovi aktualizuje kandidáta jen v RAM; jiný záznam aktualizuje latest a persistuje. V obou případech obnoví Recovery. Změna: Nové oddělení candidate od latest/history. Proč: Rewrite rerun zůstává preview a pokus na starším vybraném záznamu nezmizí, pokud commit selže.
_TrayApp._refresh_recovery — sloučení committed a RAM záznamů
Jak: Bez dialogu nebo po zastavení event loopu skončí; jinak sloučí podle ID persistentní, latest a candidate záznamy a předá seřazené records, pending/busy/enabled, shortcut a candidate ID do dialogu. Změna: Nová view refresh hranice. Proč: History, latest a rewrite candidate mají odlišnou persistence sémantiku, ale jeden aktuální pohled.
Jak: Lambda čte created_at každého záznamu a třídí sestupně před voláním dialogu. Změna: Nová samostatně evidovaná callback expression; base neměl Recovery seznam. Proč: UI dostává nejnovější vytvořené záznamy jako první; nejde o řazení podle času posledního delivery attempt.
_TrayApp._open_recovery — lazy dialog a akční signály
Jak: Neotevře se během Exit ani Settings. Dialog vytvoří jednou, propojí copy/arm/rewrite/retry/discard/delete/clear, obnoví obsah a zobrazí/aktivuje okno. Změna: Přidává Recovery view, které base tray menu neobsahovalo. Proč: Explicitní uživatelské operace jsou oddělené od automatického doručování.
Jak: Ruční Recovery akce povolí jen enabled, mimo Exit/Settings, bez nahrávání a workeru. Změna: Nová společná brána. Proč: Žádná Recovery operace nevytváří frontu vedle právě běžící dikce.
Jak: Jen po Recovery gate posílá zvolený raw nebo final text do společného deliver s copy_only=True. Změna: Nová ruční cesta; neprovádí automatic target/type. Proč: Kopie je výslovná akce a může vystavit text clipboardu; její status zaznamenává delivery attempt.
_TrayApp._arm_recovery — odložené vložení po zkratce
Jak: Uloží záznam i vybraný text do RAM a skryje dialog, pokud existuje. Změna: Nová explicitní arm cesta. Proč: Insertion se provede až po recovery chordu a novém foreground target checku, ne při pouhém výběru záznamu.
Jak: Odstraní jedinou připravenou dvojici record/text. Změna: Nová operace pro Escape, nové nahrávání, Disable a Exit. Proč: Arm je přechodný stav, ne automaticky opakovaný požadavek.
Jak: Nejprve spotřebuje arm, pak znovu ověří Recovery gate a teprve poté sejme aktuální foreground target a zavolá manual _deliver. Změna: Nový Recovery chord callback. Proč: Případ neplatného/zakázaného pokusu automaticky nečeká na další stisk; _deliver stále provádí output ochrany.
Jak: Vyžaduje Recovery gate i pending WAV; použije uložený zvuk a jeho původní _Session, poté spustí worker recovery_only a stt_only. Změna: Nová cesta pro opakování po STT failu. Proč: Používá původní immutable config/profile snapshot, ne aktuální defaults; po úspěšném STT checkpoint WAV mizí. Opakování nevykonává automatic output.
Jak: Pending audio maže pouze mimo worker i nahrávání, pak aktualizuje Recovery. Změna: Nová akce pro jediný WAV držený po STT neúspěchu. Proč: Současný běh nemá přijít o zvuk, který worker právě používá.
_TrayApp._rerun_rewrite — RAM kandidát s aktuálním výchozím nastavením
config=copy.deepcopy(self._config)
# Recovery has no external target: Automatic uses the current default.profile_id=self._apply_rewrite_profile(config, None)
session=_Session(config, None, uuid4().hex, utc_now(), profile_id)
Jak: Po gate zkopíruje aktuální config, aplikuje profil bez external executable target (Automatic tedy resolverem vybírá current default) a spustí worker s raw textem z vybraného záznamu. Chybný config oznámí; při startu jde o recovery-only candidate. Změna: Nová explicitní rerun cesta. Proč: Kandidát má novou identitu a zůstává RAM preview; nepřepíše zdrojový záznam ani nepoužije automatické vložení.
_TrayApp._delete_record — odstranění RAM a committed identity
Jak: Odmítá deletion za běhu workeru/recordingu/Exit. Committed záznam maže přes HistoryStore jen při enabled historii, failuje uzavřeně při storage error; pak maže odpovídající latest/candidate/armed RAM položku a obnoví Recovery. Změna: Nové odstranění podporující persistent i RAM rekordy. Proč: Vypnutá historie sama nedovoluje měnit soubor, ale RAM-only záznamy lze odstranit.
_TrayApp._clear_history — potvrzené vymazání včetně poškozeného souboru
if (
QMessageBox.question(
self._settings_dlgorself._recovery_dlg,
"Clear recovery",
"Delete saved history and RAM text, including an unreadable history file? "
Jak: Blokuje active work, žádá potvrzení a po nested Qt event loopu znovu kontroluje busy/exit stav. Pak HistoryStore clearuje; při chybě ponechá RAM stav, při úspěchu vymaže committed/latest/candidate/arm. Změna: Nová explicitní Clear hranice. Proč: Čištění poškozeného souboru i RAM textu je nevratná lokální akce a je potvrzována až po opakované busy kontrole.
# Provider/OS exception details may contain user text or credentials.log.error("AppError: %s", code.name)
ifnotself._event_loop_stopped:
self._tray.showMessage("Screamer", msg, QSystemTrayIcon.MessageIcon.Warning, 5000)
Jak: Zobrazí enum message a případný detail v tray balloon, ale do logu zapisuje jen kód; po zastavení event loopu balloon neposílá. Změna: Base logoval i detail. Proč: Detail provider/OS výjimky může obsahovat transcript nebo credentials; cleanup po event loopu nemá používat tray UI.
_TrayApp._toggle_enabled — stop/cancel při Disable
self._enabled=checkedifnotchecked:
self._disarm()
ifself._recording:
# Disabling means "stop"; don't transcribe and type the leftovers.self._cancel_recording()
elifself._workerisnotNone:
# Mid-processing: don't type into the focused window after the# user disabled us. The main-thread success slot checks again.self._cancel_event.set()
log.info("Screamer %s", "enabled"ifcheckedelse"disabled")
Jak: Aktualizuje enabled; při vypnutí zruší připravený insert, zahodí právě nahrávané audio, nebo signalizuje cancel workeru. Poté obnoví Recovery availability. Změna: Base neměl arm ani refresh. Proč: Disable před final injection blokuje output, ale již odeslané OS eventy vrátit nelze.
_TrayApp._set_hotkey — validace, persist a rollback
Jak: Parsuje i validuje, odmítá shodu s recovery chordem, uloží kanonický string a při save exception obnoví předchozí hodnotu/menu. Po úspěchu restartuje listener; v recordingu restart zůstává odložen. Změna: Base ukládal přímo vstupní string bez validace/rollbacku/conflict gate. Proč: Dictation a Recovery chord sdílí listener a změna nesmí uložit nevalidní binding.
Jak: Změní draft runtime volby, validuje resolverem a persistuje. Při chybě vrátí oba selection fields, rebuildne menu a rozliší config invalid od storage failure. Změna: Nová tray volba profilu. Proč: Nesmí se zobrazit uložený profil, který catalog nebo resolver nepovažuje za platný.
Jak: Pamatuje předchozí stav a při save/validation chybě jej obnoví, obnoví menu a zobrazí vhodnou chybu; success menu rebuildne. Změna: Base pouze měnil config a save bez chybové obnovy. Proč: Viditelná checkbox volba i runtime config musí při selhání souhlasit.
_TrayApp._open_settings — draft dialog a reload hranice
Jak: Odmítá duplikát/Exit a active dictation; dialog dostává device refresh/calibration callbacks, stává se aktivním draftem a disarms insertion. Propojí Apply/Clear/history/hotkey capture; po exec() vždy uklidí _settings_dlg, při Exit pokračuje shutdownem, jinak načte uložený config. Změna: Base neměl explicitní refresh callback, clear history ani _last_settings_dlg. Proč: Nested event loop nesmí závodit s tray editacemi a closed dialog reference zůstává pro post-loop joiny.
_TrayApp._on_settings_applied — Apply synchronizuje i retention
Jak: Reloadne uložené settings a výslovně požádá synchronizaci o commit history retention. Změna: Nový callback oddělený od obecného reloadu po zavření dialogu. Proč: Změna limitu se commitne při Apply, nikoli pouhým otevřením nebo Cancel.
_TrayApp._sync_settings_from_disk — načtení nastavení a history policy
Jak: Reloadne config, při změně dictation/recovery hotkey či mode restartuje listener, načte history; pouze Apply při enabled history commitne retention. Latest dopersistuje při Apply nebo při přechodu history off→on, pak refreshne UI. Změna: Base synchronizoval jen hotkey/mode a menu. Proč: Historie je separátní persistence hranice; turn-off neničí staré záznamy a explicitní Apply provede retention změny.
_TrayApp._build_hotkey — Qt bridge pro tray-owned callbacks
Jak: Připojí dictation hotkey stisk/release a chyby do tray app; nové recovery signals vedou na insert/disarm a poté sestaví listener. Změna: Base připojoval pouze dictation hotkey a chybu. Proč: Listener thread komunikuje s Qt main thread přes SignalBridge, zatímco tray app zůstává vlastníkem operací Recovery.
Jak: Idempotentně nastaví Exit, zruší arm/pending WAV, zastaví timery/listener, signalizuje worker cancellation, zastaví běžící capture a zavře Settings. _finish_exit odloží Qt quit, dokud worker/dialog neskončí. Změna: Base neměl recovery state ani zrušení pending audio. Proč: Exit nesmí spustit nové output/recovery akce; active blocking network call může dokončení zpozdit.
if (
self._workerisnotNoneorself._settings_dlgisnotNoneorself._event_loop_stoppedorself._exit_complete
):
return
Jak: Guarduje aktivní worker/dialog, již zastavenou smyčku a opakované dokončení. Teprve pak uloží config s logged failure, skryje UI a zavolá Qt quit. Změna: Base kontroloval pouze worker/dialog. Proč: Post-loop synchronous cleanup nesmí znovu dokončovat Exit ani pracovat s UI po event loopu.
_TrayApp._shutdown_after_event_loop — skutečné joiny před odemčením instance
Jak: Po zastavení Qt nastaví event-loop/Exit guard, canceluje, synchronně joinuje hotkey listener, zastaví audio, ukončí a joinuje Settings child threads, čeká na worker a použije jeho raw terminal payload i bez queued signal callbacku. Čeká také retired/child QThreads a ukládá settings, pak skryje UI. Změna: Base neměl tento post-loop safeguard. Proč: Shutdown potřebuje skutečné dokončení vláken, nejen doručení queued signálu; terminal může ještě retainovat checkpoint, ale automatic output je blokován v Exit. Instance lock musí zůstat držen až do cleanupu.
instance_lock=QLockFile(str(directory/"screamer.lock"))
# Never expire a live instance's lock based on elapsed time.instance_lock.setStaleLockTime(0)
ifnotinstance_lock.tryLock(0):
Jak: Parsuje --startup, vytvoří QApplication, připraví resolved APP_DIR a odmítne nepřístupný adresář nebo obsazený lock. Po získání locku nastaví log, sestaví tray app a spustí Qt; finally synchronně dočistí tray threads, uzavře HTTP klienta a až potom odemkne. Změna: Base zamykání adresáře neměl a uzavíral HTTP klienta přímo po event loopu. Proč: Jedna instance vlastní single-writer history/config boundary; lock trvá přes thread/dialog cleanup i HTTP close.
Jak: Uchovává WAV, session config, cancel event/ID, volitelný raw text a STT-only režim; terminal payload začíná prázdný. Změna: Base worker měl pouze WAV/config/cancel. Proč: Jeden QThread obslouží běžné STT→rewrite, STT retry i rewrite rerun a uchová výsledek nezávisle na queued delivery.
"""Read on main after wait(); does not depend on queued signal delivery."""returnself._terminal_payload
Jak: Vrací poslední uložený _Terminal; komentář stanoví čtení na main po wait(). Změna: Nové API workeru. Proč: Po event-loop exit se Qt queued slot nemusí doručit, ale joinovaný worker stále poskytuje raw result pro cleanup.
_WorkerThread._publish_terminal — uložit před emitováním
Jak: Nejdříve nastaví retained payload, následně emituje Qt terminal signal. Změna: Nový terminal protocol nahrazuje tři success/fail/cancel signály base. Proč: Shutdown po joinu může načíst stejný terminal, i když signal handler neběžel.
_WorkerThread.run — raw-first, cancellation checkpoints a terminal
Jak: Kontroluje cancel před STT, mezi STT a rewrite i před terminalem; raw text může být vstupem rewrite-only. Po neprázdném STT přenese warnings, vymaže pracovní WAV a emituje raw checkpoint. LLM volá jen pokud enabled a není stt_only; prázdný/LLM_FAILED výsledek značí failed. Všechny success/cancel/error větve publikují _Terminal; exception s již známým raw označí rewrite failed a doplní warning. Změna: Base vždy volal transcribe→rewrite a vracel PipelineResult/Exception přes rozdělené signály. Proč: Checkpoint raw před rewrite umožňuje recovery; cancel se kontroluje kolem, nikoli uvnitř blokujícího HTTP call.
Zdroje a ověřené hranice
docs/IMPLEMENTATION.md: aktuální session/output/Recovery/storage/shutdown kontrakty, oddělení historie od RAM a známé Windows ověřovací limity.
docs/IMPLEMENTATION-PLAN.md: stav implementace a dosud otevřené manuální acceptance gates; tento popis netvrdí, že Windows gates prošly.
Pinned source/base snapshoty výše jsou zdrojem přesných těl a řádků. Implementační chování je zde popsáno ze zdrojového kódu, nikoli jako nové tvrzení o uživatelské garanci.
Tento přehled pokrývá 70 přidaných nebo změněných testovacích funkcí a callbacků v testech audia, konfigurace, chráněného výstupu, injectorů a profilů. Každá položka uvádí jeden symbol a vlastní hranici přípravy či ověření; pomocné funkce jsou vypsány samostatně.
Zvuk (tests/test_audio.py)
Testovací přípravky a chování při záznamu audia
Jak to funguje: Úklid kontroluje is_recording, zavolá stop() a potlačí pouze ScreamerError, aby se ukončení přípravku dokončilo i po chybě záznamu. Co je změna: Ochranná podmínka brání opakovanému zastavení již zastaveného rekordéru; chyby při úklidu nezakryjí původní aserci testu. Proč je toto potřeba: Testy rekordéru ověřují běžné i chybové stavy, takže deterministické ukončení brání přenosu stavu backendu z jednoho testu do druhého.
Jak to funguje: Každý test dostane FakeCaptureBackend, nahrazený src.audio.sd, ovladatelné monotónní hodiny a spuštěný rekordér; úklid obnoví náhrady a zavře aktivní rekordér. Co je změna: Aserce se vyhodnocují nad falešným streamem a známým časem, nikoli nad skutečným zvukovým hardwarem nebo časem běžícím na pozadí. Proč je toto potřeba: Testy záznamu potřebují opakovatelné načasování callbacků, včetně opožděných callbacků a přesných hranic minimální délky.
Jak to funguje: Nahrazené monotónní hodiny vracejí měnitelnou hodnotu self.now z přípravku. Co je změna: Test posune dobu záznamu přiřazením do self.now, aniž by čekal. Proč je toto potřeba: Výsledky závislé na délce lze ověřit přesně a rychle.
Jak to funguje: Falešný stream uchovává callbacky z produkčního kódu a inicializuje příznaky chyb start/stop/close i stav closed. Co je změna: Testy mohou callbacky vyvolat a podle potřeby simulovat chyby backendu. Proč je toto potřeba: Regrese životního cyklu záznamu se sledují přes stejnou hranici callbacků, jakou používá adaptér streamu.
Jak to funguje:feed zavolá nainstalovaný callback se vzorky, počtem snímků, bez časové značky a s nastavitelným stavovým řetězcem. Co je změna: Test může obsluze callbacku rekordéru předat přesná pole vzorků a přechodný stav backendu. Proč je toto potřeba: Data callbacku, zpracování stavu a ukládání vzorků je nutné ověřovat nezávisle na PortAudio.
Jak to funguje: Při nastavení start_error falešný stream před vyvoláním OSError("device busy") předá 160 mono vzorků s hodnotou 1200. Co je změna: K chybě dojde až poté, co dorazí data callbacku, nikoli před změnou stavu rekordéru. Proč je toto potřeba: Vrácení neúspěšného spuštění musí zahodit částečné snímky a zabránit přijetí dalších callbacků.
Jak to funguje:stop vyžaduje ignore_errors=False, během probíhajícího zastavování předá callback se 160 vzorky, oznámí dokončení streamu a poté případně vyvolá chybu. Co je změna: Falešný stream modeluje callbacky přicházející během zastavování i následnou chybu backendu při zastavení. Proč je toto potřeba: Ukončení rekordéru musí správně seřadit uzavření callbacků a úklid po chybě.
assertignore_errorsisFalse# Backends may finish an in-flight callback while stop is in progress.self.feed(np.full((160, 1), 5000, dtype=np.int16))
self.finished_callback()
ifself.stop_error:
Jak to funguje:close vyžaduje ignore_errors=False, nastaví closed a chybu vyvolá pouze při nastaveném close_error. Co je změna: Testy mohou ověřit uzavření i v případě, že samotná operace close selže. Proč je toto potřeba: Chyby při ukončení nesmějí ponechat stream ve zdánlivě aktivním stavu ani zpřístupnit částečné audio.
Jak to funguje: Backend začíná bez streamu a bez požadavku na chybu při spuštění. Co je změna: Falešný backend má od začátku deterministický stav a následně uchovává stream vytvořený metodou InputStream. Proč je toto potřeba: Jednotlivé testy záznamu přesně určují, kdy spuštění backendu selže.
Jak to funguje: Falešný backend vytvoří FakeCaptureStream s callbackem a callbackem dokončení předanými z AudioRecorder, zkopíruje nastavený příznak selhání spuštění a stream vrátí. Co je změna: Zachovává se skutečné propojení callbacků, aniž by se používal hardwarový stream. Proč je toto potřeba: Testy musí ověřovat chování rekordéru přes produkční rozhraní pro vytváření streamu.
Jak to funguje: Vyhledání zařízení vrátí pojmenovaný mikrofon USB pouze pro ID zařízení 2 a vstupní režim; u ostatních požadavků vyvolá ValueError. Co je změna: Nastavení záznamu dostane deterministickou skutečnou identitu zařízení a neplatná vyhledání backendu zůstávají rozlišitelná. Proč je toto potřeba: Aserce snímku rekordéru kontroluje identitu skutečně otevřeného zařízení, nejen požadovaný výběr.
ifdevice_id!=2orkind!="input":
raiseValueError("missing device")
return {"name": "Actual USB microphone"}
Jak to funguje: Předá vzorky se znaménkem, pořídí snímek RMS a metadat zařízení, posune falešné hodiny na jednu sekundu a prohlédne vrácené snímky WAV. Co je změna: Aserce vyžadují počáteční stav bez dat a s nulovou úrovní, skutečnou identitu (2, "Actual USB microphone"), aktualizované RMS nejprve 1200 a poté 600, výstup 16 kHz mono 16bit a vzorky zachované byte-for-byte; callbacky po zastavení neobnoví snímky ani úroveň. Proč je toto potřeba: Test chrání věrnost vzorků a odděluje průběžný stav úrovně od dokončeného záznamu.
Jak to funguje: Pro vzorky s hodnotami 0 a 2 test ověří aktivní záznam, předá 160 vzorků, zkontroluje přítomnost dat callbacku a RMS ve snímku, posune čas o sekundu a záznam zastaví. Co je změna: Očekávané hodnoty jsou výslovně has_callback_data=True, RMS rovné 0 nebo 2, prázdná data WAV při nastaveném prahu a nezměněný výchozí práh RMS. Proč je toto potřeba: Ticho i slabé vzorky jsou skutečnými daty callbacku, i když se audio následně zahodí kvůli nedosažení prahu.
Jak to funguje: Posune falešné hodiny z 10 na 11 sekund, aniž by předal callback, a zavolá stop(). Co je změna: Test vyžaduje MIC_DISCONNECTED, detail obsahující No microphone samples, žádné zachované snímky a následně neaktivní záznam. Proč je toto potřeba: Samotné uplynutí požadované doby nesmí změnit absenci zvukového callbacku v úspěšný prázdný záznam.
Jak to funguje: Zastaví aktivní rekordér po posunu času pouze o 0,1 sekundy, aniž by předal vzorky. Co je změna: Očekávaným výsledkem jsou prázdné bajty, nikoli chyba záznamu bez dat, která se používá při záznamu v plné délce. Proč je toto potřeba: Zahazování krátkého záznamu se liší od selhání zařízení po uplynutí celé doby.
Jak to funguje: Předá 160 vzorků s hodnotou 1200 a stavem input overflow, pořídí snímek stavu/chyby, posune čas o sekundu a záznam zastaví. Co je změna: Stav zůstane zaznamenán jako pozorování, capture_error zůstane None a zastavení vrátí neprázdný WAV. Proč je toto potřeba: Přechodný stav streamu se nesmí zaměnit za jeho definitivní ukončení.
Jak to funguje: Předá nenulové vzorky a poté neočekávaně zavolá callback dokončení dříve, než falešné hodiny dosáhnou platné délky. Co je změna: Snímek zaznamená chybu záznamu; stop() vyvolá MIC_DISCONNECTED a vymaže snímky. Proč je toto potřeba: Ukončený stream nelze považovat za platný částečný WAV jen proto, že callbacky předtím předaly vzorky.
Jak to funguje: Spustí samostatné dílčí testy pro stop_error a close_error, předá slyšitelné vzorky, posune čas o sekundu a pokusí se záznam zastavit. Co je změna: Obě cesty vyžadují MIC_DISCONNECTED, uzavřený stream, neaktivní rekordér a prázdný interní seznam snímků. Proč je toto potřeba: Chyba při ukončování backendu nesmí zpřístupnit částečně zaznamenané audio jako úspěšný WAV.
Jak to funguje: Zastaví záznam přípravku, zapne selhání falešného spuštění (které nejprve předá vzorky), potom zkusí nové spuštění a po chybě znovu předá vzorky. Co je změna: Test vyžaduje MIC_UNAVAILABLE, uzavřený stream, žádné snímky po opožděném callbacku a is_recording=False. Proč je toto potřeba: Vrácení chybného spuštění musí zablokovat callbacky dorazivší po částečně úspěšném startu.
Jak to funguje: Ověří platné kombinace indexu a názvu vůči explicitním n-ticím zařízení a poté v rámci subTest projde pět neplatných nebo nejednoznačných identit. Co je změna: Platný index 1 zůstane zachován po normalizaci velikosti písmen a mezer a neznámé uložené ID lze dohledat podle jednoznačného názvu; neshody, chybějící názvy a duplicitní shody vyvolají MIC_UNAVAILABLE. Proč je toto potřeba: Explicitní identita zařízení nesmí po změně seznamu zařízení tiše začít označovat jiné zařízení.
self.assertEqual(resolve_device(1, " USB MIC ", devices=devices), 1)
Jak to funguje: Nahrazuje list_devices a řeší (None, "Old microphone"). Co je změna: Očekávaným výsledkem je None a výčet zařízení se výslovně nezavolá. Proč je toto potřeba: Záměrný výběr systémového výchozího zařízení se nesmí změnit na explicitní shodu názvu podle zastaralých uložených metadat.
Jak to funguje: Falešný seznam má na indexu 0 Microphone Pro a na indexu 1 přesný název Microphone; nad ním se zavolá resolve_device(42, "Microphone"). Co je změna: Očekávaným vyřešeným indexem je 1, přestože se částečná shoda objevuje dříve. Proč je toto potřeba: Při obnově uloženého výběru zařízení má mít jedinečný přesný název přednost před dřívější částečnou shodou.
Zdroj: Zdroj; východiskové chování: Zdroj změnilo ID použité při vyhledávání z None na 42.
Jak to funguje: Metoda start() streamu vyvolá OSError("device busy"); jeho metoda close(ignore_errors=...) s argumentem pouze pojmenovaným zaznamená uzavření. Co je změna: Rekordér musí vyvolat MIC_UNAVAILABLE, částečně vytvořený stream zavřít a uvést, že nenahrává. Proč je toto potřeba: Vytvoření streamu může uspět ještě před selháním jeho spuštění, takže úklid po chybě startu musí uzavřít získané zdroje.
Zdroj: Zdroj; výchozí signatura callbacku v tomto zdroji nepřijímala požadovaný pojmenovaný argument.
Jak to funguje: Vnořený callback close falešného objektu přijímá ignore_errors pouze jako pojmenovaný argument a nastaví closed na true. Co je změna: Poskytuje pozorovatelný účinek uzavření pro nadřazený test selhání spuštění. Proč je toto potřeba: Test zjišťuje, zda se zdroj uzavřel, místo aby kontroloval pouze převedenou výjimku.
Zdroj: Zdroj; výchozí vnořená metoda měla podobu close(self) v tomto zdroji.
Jak to funguje: Nainstaluje falešný stream, jehož stop vyvolá OSError("disconnected") a jehož close zaznamená volání, a poté zavolá zastavení rekordéru. Co je změna: Očekávaným výsledkem zůstává MIC_DISCONNECTED, test však nově vyžaduje také uzavření a vymazaný stav záznamu. Proč je toto potřeba: Selhání zastavení nesmí přeskočit úklid streamu.
Zdroj: Zdroj; výchozí metoda v tomto zdroji postrádala pojmenované parametry i nové aserce stavu.
Jak to funguje: Close vnořeného falešného objektu přijímá ignore_errors jako argument pouze pojmenovaný a označí objekt za uzavřený. Co je změna: Nadřazený test může pozorovat, že close proběhl, i když stop vyvolal chybu. Proč je toto potřeba: Výjimka při stop nesmí zakrýt vynechanou operaci close.
Jak to funguje: Vnořený callback stop přijímá ignore_errors pouze jako pojmenovaný argument a vyvolá deterministickou chybu odpojení. Co je změna: Falešný objekt stále selhává při stop, nově ale odpovídá explicitnímu argumentu politiky chyb v produkčním volání. Proč je toto potřeba: Test zajišťuje pokrytí chybové cesty i po změně rozhraní close/stop streamu.
Jak to funguje: Sestaví neúplné primární nastavení s povolenou a úplnou záložní konfigurací a porovná všechny zprávy validačních problémů. Co je změna: Očekává se pouze zpráva týkající se primární konfigurace: Primary STT requires a base URL and model; API key is optional. Proč je toto potřeba: Platná záloha splňuje požadavek na dostupnost poskytovatele, nesmí však zakrýt neplatnou částečně nastavenou primární konfiguraci.
[issue.messageforissueinvalidate_config(cfg)],
["Primary STT requires a base URL and model; API key is optional."],
Zdroj: Zdroj; výchozí test očekával starou zprávu o povinném klíči v tomto zdroji.
Jak to funguje: Ověří AppConfig obsahující pouze stt_api_key="key" a získá zprávy o problémech. Co je změna: Pro primární konfiguraci se uvádí, že URL a model jsou povinné, zatímco API klíč je volitelný; druhá aserce nadále vyžaduje URL/model primární nebo povolené záložní konfigurace. Proč je toto potřeba: Povinné nastavení STT a volitelné ověření jsou samostatná validační pravidla.
self.assertIn("Primary STT requires a base URL and model; API key is optional.", messages)
Jak to funguje: Použije dočasný adresář aplikace a soubor .env s STT_LANGUAGE=cs, při ukládání vynutí selhání os.replace, poté nahradí _env_path a importuje hodnoty prostředí. Co je změna: Po neúspěšném uložení musí konfigurace v paměti zůstat způsobilá k doplnění z prostředí; jazyk výsledku je přesně cs. Proč je toto potřeba: Neúspěšná atomická náhrada nastavení nesmí zničením staré konfigurace tiše uložit prázdnou hodnotu „Auto“.
Jak to funguje: Uloží prázdný jazyk, znovu načte konfiguraci s jazykem a URL z prostředí, uloží importovanou konfiguraci a ještě jednou ji načte. Co je změna: Uložený prázdný jazyk zůstane prázdný i po restartu a uložení, zatímco chybějící URL se doplní ze souboru .env jako http://localhost:8000/v1. Proč je toto potřeba: Výslovně uložená volba Auto a chybějící konfigurace se liší; záložní hodnota z prostředí nesmí přepsat uloženou prázdnou volbu.
Jak to funguje: Načte prázdné dočasné úložiště nastavení, samostatně zapne llm_enabled v QSettings a konfiguraci znovu načte a uloží. Co je změna: Chybějící prompt se nastaví na DEFAULT_LLM_SYSTEM_PROMPT s původem default_v2; přepis zůstane ve výchozím stavu vypnutý a zapne se, pokud je výslovně uložen jako zapnutý. Proč je toto potřeba: Nastavení výchozí verze promptu nesmí přepsat samostatnou volbu povolení přepisu.
Jak to funguje: V dílčích testech projde přes načtení, nesouvisející změnu jazyka, uložení a opětovné načtení přesný starší prompt, aktuální výchozí text, prázdný text i vlastní text s mezerami a CRLF. Co je změna: U každé hodnoty se přesně zachová text promptu i jeho bajty a původ legacy_saved, včetně uloženého klíče původu. Proč je toto potřeba: Shoda textu promptu neurčuje jeho původ a zápisy nesouvisejících nastavení musí zachovat uložený obsah bez normalizace.
Jak to funguje: Uloží konfigurace s každou z hodnot legacy_saved, default_v2 a user_saved, poté pro každý původ načte konfiguraci z nového dočasného adresáře. Co je změna: Každý výslovně zadaný původ se zachová, přestože text promptu odpovídá současnému výchozímu textu. Proč je toto potřeba: Původ nelze bezpečně odvozovat z rovnosti řetězců promptu.
Jak to funguje: Ověřuje uložené původy default_v2 a unknown s přesně uloženým promptem obsahujícím CRLF, poté ověří úplnou konfiguraci poskytovatele a výslovně opraví původ. Co je změna: Načtení zachová prompt i neplatný původ; validace nahlásí kartu 2 s možností Reset to Current Default a nastavení user_saved problémy odstraní. Proč je toto potřeba: Neplatná metadata mají vyžádat opravu, nikoli překlasifikování nebo přepsání uloženého promptu uživatele.
self.assertEqual(cfg.llm_system_prompt, "Keep this saved prompt.\r\n")
self.assertEqual(cfg.llm_prompt_origin, origin)
cfg.stt_api_key="key"cfg.stt_base_url="https://example.test/v1"cfg.stt_model="stt"
Chráněný výstup a obnova klávesové zkratky (tests/test_guarded_output.py)
Jak to funguje: Definuje cíl (hwnd=100, pid=200), napodobené API user32, výchozí úspěšné SendInput, náhrady platformy Windows a WinDLL, nulovou poslední chybu a nahrazené čekání. Co je změna: Volání testují cestu injektování ve Windows bez skutečného vstupu Win32 nebo prodlevy. Proč je toto potřeba: Částečné počty a změny okna v popředí lze na hranici injektování deterministicky ověřit.
Jak to funguje: Napodobené SendInput ve výchozím stavu vrací počet předaných událostí. Co je změna: Úspěšné cesty mají referenční úplný počet; jednotlivé testy jej mění, aby simulovaly částečné odeslání. Proč je toto potřeba: Testy porovnávají úspěšné počty odeslaných událostí s výslovně částečnými výsledky.
Jak to funguje: Projde případy bez aktuálního cíle, stejného HWND s jiným PID a jiného HWND se stejným PID, přičemž očekávaným cílem je (100, 200). Co je změna: Každé volání vyvolá InjectionError; volání SendInput není povoleno. Proč je toto potřeba: Opětovné použití HWND ani částečně shodná identita nesmějí opravňovat k odeslání textu.
Jak to funguje: Identita okna v popředí má shodné HWND/PID a cestu k programu, zatímco cesta v očekávané identitě je None; test napíše a bez závěrečné klávesy. Co je změna: Výsledek vyžaduje 2 textové události, 0 událostí závěrečné klávesy a jedno nativní volání. Proč je toto potřeba: Volitelná metadata programu nesmějí způsobit neúspěch jinak shodné ověřené identity okna.
Jak to funguje: Nastaví, aby první SendInput vrátil 3 ze čtyř událostí UTF-16 vytvořených jedním emoji, a poté požádá o Enter. Co je změna: Chyba zaznamená 3 textové události a 0 událostí závěrečné klávesy; počet nativních volání zůstane 1 a požadovaná velikost dávky je 4. Proč je toto potřeba: Částečně odeslaná dávka náhradního páru se nesmí opakovat ani následovat klávesou Enter, která by mohla ovlivnit nesprávný textový stav.
Jak to funguje: Vyhledání okna v popředí nejprve vrátí ověřený cíl a před odesláním závěrečné klávesy jiné HWND; text je abc a závěrečnou klávesou Enter. Co je změna: Výsledek zachová 6 textových událostí, odešle 0 událostí závěrečné klávesy, nastaví post_key_skipped=True a provede jedno volání vstupu. Proč je toto potřeba: Změna fokusu po úspěšném napsání má zabránit pouze závěrečné akci, nikoli zneplatnit již dokončené textové události.
Jak to funguje:SendInput vrátí [6, 1]: všech šest událostí pro abc a poté jen jednu ze dvou událostí dávky Enter. Co je změna: Chyba zachová počty 6 textových a 1 události závěrečné klávesy a proběhnou přesně dvě volání API. Proč je toto potřeba: Hlášení chyby musí odlišovat úspěšné odeslání textu od částečného odeslání závěrečné klávesy.
Jak to funguje: Po ověření cíle zavolá type_text("abc", "not-a-key", ...) a očekává InjectionError. Co je změna:SendInput se nesmí zavolat. Proč je toto potřeba: Neplatná závěrečná klávesa se odmítne dříve, než lze nevratně odeslat textové události.
Jak to funguje: Připojí tři seznamy k signálům požadavku na obnovu, zrušení obnovy a stisku diktování a poté vytvoří posluchač v režimu podržení pro Ctrl+Alt+Space se zkratkou obnovy Ctrl+Alt+Shift+V. Co je změna: Každý test sleduje vyslané události přes SignalBridge a volá hranici klávesových událostí posluchače. Proč je toto potřeba: Chování zkratky obnovy a injektovaných kláves závisí na pořadí událostí ve stavu posluchače a signálech Qt.
Jak to funguje: Signál požadavku na obnovu připojí True do self.recovered. Co je změna: Seznam slouží jako konkrétní cíl aserce pro vyvolání obnovy. Proč je toto potřeba: Testy mohou odlišit vyslaný požadavek na obnovu od pohlcené nebo ignorované zkratky.
Jak to funguje: Signál zrušení obnovy připojí True do self.cancelled. Co je změna: Zrušení klávesou Escape se ověřuje prostřednictvím tohoto seznamu. Proč je toto potřeba: Signál deaktivace/zrušení musí být pozorovatelný nezávisle na aktivaci obnovy.
Jak to funguje: Signál stisku klávesové zkratky diktování připojí True do self.dictation. Co je změna: Testy mohou ověřit, zda klávesové události během obnovy nebo injektovaného výstupu omylem nespustily diktování. Proč je toto potřeba: Obnovu a zpracování injektovaných kláves je nutné odlišovat od běžného spouštěče nahrávání.
Jak to funguje: Odešle stisky Ctrl, Alt a Shift, dvakrát stiskne spouštěcí klávesu obnovy V a ověří, že před začátkem událostí uvolnění kláves nepřijde signál obnovy. Co je změna: Pomocná metoda také ověří, že oba stisky spouštěcí klávesy byly pohlceny a recovered zůstává v tomto okamžiku prázdné. Proč je toto potřeba: Sdílené nastavení zajistí, že testy pořadí uvolnění začínají ze stejného stavu stisknuté zkratky.
Jak to funguje: Stiskne sdílenou zkratku, nejprve uvolní V, poté Ctrl a Alt a nakonec Shift. Co je změna: Signál obnovy nesmí přijít před uvolněním Shift; opakované uvolnění Shift nevyšle druhý signál a seznam diktování zůstane prázdný. Proč je toto potřeba: Obnova musí čekat na uvolnění celé zkratky bez ohledu na to, že se jako první uvolní spouštěcí klávesa, a musí se spustit právě jednou.
Jak to funguje: Stiskne zkratku, nejprve uvolní všechny tři modifikační klávesy a poté V. Co je změna: Po uvolnění modifikačních kláves zůstane obnova prázdná a po uvolnění spouštěcí klávesy bude její obsah přesně [True]; signál diktování nepřijde. Proč je toto potřeba: Stavový automat musí zvládat také pořadí, kdy se nejprve uvolní modifikační klávesy.
Jak to funguje: Odešle stisk klávesy Escape, zatímco je posluchač nastaven pro obnovu. Co je změna: Klávesová událost vrátí False (propustí se dál) a zrušení vyšle signál právě jednou. Proč je toto potřeba: Escape požádá o deaktivaci, aniž by se tato klávesa pohltila aplikaci v popředí.
Jak to funguje: Podrží Ctrl+Alt, odešle injektované stisknutí a uvolnění Space s příznakem injektování 0x10 a poté fyzický stisk Space. Co je změna: Obě injektované události projdou dál a nevyvolají diktování; fyzická událost se pohltí a vyvolá jeden stisk diktování. Proč je toto potřeba: Závěrečná klávesa vytvořená injektováním textu nesmí rekurzivně spustit nový záznam.
Jak to funguje: Podrží Ctrl+Alt a fyzickou klávesu Space, připojí pozorovatel uvolnění a poté odešle injektované uvolnění Space před fyzickým uvolněním Space. Co je změna: Injektované uvolnění klávesy projde dál a seznam uvolnění zůstane prázdný; fyzické uvolnění se pohltí a vyšle právě jedno uvolnění. Proč je toto potřeba: Injektované uvolnění klávesy nesmí poškodit stav posluchače pro fyzicky podrženou spouštěcí klávesu.
Jak to funguje: Callback signálu hotkey_released připojí do seznamu released hodnotu True. Co je změna: Test podle seznamu rozliší, zda fyzické uvolnění spustilo signál; samotná lambda nepřidává jiný stav. Proč je toto potřeba: Ověření injektovaného key-up vyžaduje pozorovat, zda fyzicky držený trigger zůstal stisknutý.
UTF-16 injektoru a chování závěrečné klávesy (tests/test_injector.py)
Jak to funguje: Při psaní prázdného textu a Enter přes napodobené API Windows zaznamená počet, virtuální klávesy a příznaky v odeslané dávce. Co je změna: Přesné očekávání je jedna dávka dvou událostí: stisk Enter (0x0D, 0) a uvolnění (0x0D, 2). Proč je toto potřeba: Párové události závěrečné klávesy se musí odeslat a zkontrolovat společně, i když je text prázdný.
Jak to funguje: Napodobené SendInput ve Windows vrátí 1 pro dvouprvkovou dávku Enter s prázdným textem a poslední chybou nastavenou na nulu. Co je změna:type_text vyvolá ScreamerError s kódem INJECTION_FAILED a provede se pouze jedno nativní volání. Proč je toto potřeba: Částečná dávka stisku/uvolnění klávesy znamená selhání injektování, i když Windows nehlásí kód poslední chyby.
Zdroj: Zdroj; původní aserce v tomto zdroji rovněž kontrolovala kód chyby.
Profily přepisu a vlastnictví relace (tests/test_profiles.py)
Jak to funguje: Každému testu poskytne dočasný adresář aplikace a prostřednictvím registrovaného kontextu úklidu vypne dostupnost DPAPI. Co je změna: Testy ukládání profilů pracují s izolovaným nastavením a nezávisí na šifrování Windows. Proč je toto potřeba: Katalogové roundtripy a chování migrace lze testovat bez zásahu do nastavení uživatele a bez požadavku na DPAPI.
Jak to funguje: Zakóduje vlastní profil a mapování spustitelného souboru Windows, dekóduje je, vyhodnotí ruční i automatický režim, přepne globální přepis a poté nastavení uloží a znovu načte. Co je změna: Kontroluje přesný profil a normalizovanou cestu, ruční raw, automaticky mapovaný vlastní profil, čistý záložní profil pro nespárovaný/neznámý cíl, raw při globálně vypnutém přepisu a uložené automatické mapování. Proč je toto potřeba: Serializace katalogu a pořadí přednosti výběru určují, který prompt vstoupí do relace diktování.
Jak to funguje: Uloží čtyři hodnoty promptu včetně přesných starších a současných výchozích řetězců, prázdného textu a textu s mezerami/CRLF; každou načte, dekóduje, uloží a znovu načte. Co je změna: Každý uložený řetězec se převede právě na jeden vlastní profil se stejným promptem, stane se ručním profilem, nemá žádná mapování, ponechá přepis vypnutý a úspěšně projde roundtripem. Proč je toto potřeba: Migrace musí zachovat veškerý obsah dříve uložených promptů a nesmí odvozovat, že prázdný prompt nebo shodný s výchozím nebyl uživatelským údajem.
Jak to funguje: Načte novou konfiguraci, uloží ji a znovu dekóduje katalog. Co je změna: Katalog má před uložením i po něm hodnotu ( [], [] ), přepis zůstává vypnutý a ruční výběr je clean. Proč je toto potřeba: Nový výchozí stav v2 se nesmí zaměnit za starý uložený prompt vyžadující migraci.
Jak to funguje: Předá kodéru případy s vyhrazeným ID clean, duplicitními profily, neplatnou relativní cestou mapování, neexistujícím cílovým ID a ekvivalentními duplicitními cestami; zvlášť dekóduje JSON s duplicitními klíči. Co je změna: Každé neplatné kódování i katalog s duplicitním klíčem musí vyvolat ValueError. Proč je toto potřeba: Omezení identity a cest katalogu je nutné vynucovat na hranici serializace, nikoli ponechat nejednoznačná pro běhové vyhodnocení.
Jak to funguje: Připraví katalog verze 99, načte nastavení, změní pouze jazyk STT, uloží je, zkontroluje nezpracovaný uložený katalog a pokusí se vyhodnotit profil. Co je změna: Nesouvisející uložení zachová přesný poškozený řetězec; vyhodnocení vyvolá ValueError místo jeho tichého nahrazení. Proč je toto potřeba: Nesouvisející změna nastavení nesmí zničit nečitelný katalog profilů ani skrýt nutnost výslovné opravy.
Jak to funguje: Připraví přesný text staršího promptu, načte konfiguraci, vynutí selhání atomického os.replace a poté zkontroluje nastavení a opětovné načtení. Co je změna: Uložení vyvolá ScreamerError, nezapíše se klíč katalogu přepisu a starý prompt včetně CRLF zůstane zachován. Proč je toto potřeba: Neúspěšné první uložení migrace musí zachovat původní stav na disku pro pozdější opakování.
self.assertFalse(_get_qsettings().contains("rewrite_catalog"))
self.assertEqual(load_config().llm_system_prompt, "Exact old prompt.\r\n")
Jak to funguje: Vytvoří dočasný adresář konfigurace, vypne DPAPI a ovládání podporovaných zařízení, uloží minimální platnou místní konfiguraci STT, vytvoří SettingsDialog a zaregistruje odstranění dialogu. Co je změna: Interakce uživatelského rozhraní začínají s uloženým čistým nastavením a dialogem bez zvukových zařízení. Proč je toto potřeba: Tyto testy se týkají stavu a ukládání editoru profilů, nikoli výčtu zařízení nebo úložiště tajných údajů ve Windows.
Jak to funguje: Přes widgety přidá profil Coding, upraví jeho prompt, zvolí jej jako ruční profil a změny použije; poté změní text editoru a dialog odmítne. Co je změna: Uložený katalog obsahuje jeden profil Coding s použitým promptem a vybraným ID; původní self.cfg zůstane bez katalogu a odmítnutí nepřepíše uložený prompt. Proč je toto potřeba: Úpravy dialogu mají samostatnou autoritu vůči konfiguračnímu objektu volajícího a zrušení/odmítnutí musí zahodit nepoužité změny.
Jak to funguje: Přidá C:\Tools\Editor.exe, zkusí přidat ekvivalentní cestu s jinou velikostí písmen a lomítky c:/tools/EDITOR.exe a poté dialog odmítne. Co je změna: První mapování vytvoří jeden řádek seznamu; duplicita zobrazí jedno varování a řádek nepřidá; odmítnutí ponechá uložená mapování prázdná. Proč je toto potřeba: Normalizace mapování musí zabránit duplicitním cílům a zrušení dialogu nesmí uložit čekající mapování.
Jak to funguje: Sestaví přípravky aplikace v systémové liště s napodobenými obsluhami stavu/chyb a rekordérem; zastavení rekordéru vrátí b"wav", vyhodnocení zařízení vrátí výchozí zařízení a spuštění pracovního vlákna je nahrazeno. Co je změna: Testy životního cyklu relace mohou přímo volat obsluhy systémové lišty a pracovníka bez záznamu z mikrofonu nebo asynchronního plánování vláken. Proč je toto potřeba: Aserce se týkají zmrazené konfigurace relace a stavu doručení, nikoli hardwaru nebo časování pracovníka.
Jak to funguje: Spustí záznam s automatickým mapováním spustitelného souboru, před dokončením změní živé nastavení a spustí pracovníka s nahrazenými závislostmi přepisu, úpravy, okna v popředí a psaní. Co je změna: Snímek relace musí zachovat namapovaný profil, češtinu, slovní zásobu a vlastní prompt; přepis dostane ("raw", snapshot); změněné okno v popředí zabrání injektování; výsledek zachová původní profil/jazyk/cíl a stav zadrženého doručení. Proč je toto potřeba: Diktování musí používat jeden zachycený cíl a konfiguraci navzdory změnám nastavení během nahrávání nebo zpracování.
self.assertIs(self.tray._worker._config, snapshot)
self.assertEqual(snapshot.stt_language, "cs")
self.assertEqual(snapshot.vocabulary_entries, ["QThread"])
with (
patch("src.main.transcribe", return_value=PipelineResult("raw")),
Jak to funguje: Spustí a dokončí relaci s povoleným globálním přepisem a ručně zvoleným raw, poté spustí pracovníka s nahrazeným přepisem, úpravou textu a injektováním. Co je změna: Přepis se nikdy nezavolá; konfigurace relace vypne LLM, zatímco živá konfigurace zůstane zapnutá; nejnovější výsledek zaznamená not_requested. Proč je toto potřeba: Chování profilu Raw musí být pro každou relaci zmrazené, aniž by se změnila globální předvolba přepisu uživatele.
Jak to funguje: Povolí přepis, sestaví nabídku systémové lišty, zkontroluje přepis Raw a vynutí, aby save_config vyvolalo OSError("read-only"). Co je změna: Konfigurace zůstane ručně nastavena na clean a znovu sestavená nabídka označí jako vybranou pouze položku Clean dictation. Proč je toto potřeba: Po neúspěšném uložení se má viditelný výběr v systémové liště vrátit na účinné uložené nastavení, nikoli zobrazovat neuložený výběr.
self._refresh_devices_fn=refresh_devices_fnself._calib_thread: _CalibrateThread|None=Noneself._pending_result: int|None=None# Edit a deep copy so the original is untouched until accept.self._working=copy.deepcopy(config)
self._profile_loading=Falseself._rewrite_error: str|None=Noneself._build_ui()
self.layout().setSizeConstraint(QLayout.SetFixedSize)
self._populate(self._working)
Jaktofunguje: Vytvoří pracovní kopii konfigurace, sestaví dialog a naplní šest záložek; volitelné callbacky obsluhují kalibraci a obnovení seznamu zařízení. Cojezmena: Přibyla provenance/profilová a refresh state a clear_history_requested; konstruktor přijímá obnovovací callback. Procjetotak: Úpravy zůstávají návrhem až do Apply/OK a stav běžící kalibrace je koordinován při zavírání.
Jaktofunguje: Vrací vestavěné profily před právě načtenými vlastními profily. Cojezmena: Zavedeno společné čtení celé nabídky profilů. Procjetotak: Výběry, editor i mapování používají stejnou množinu ID.
self._refresh_devices_btn=QPushButton("Refresh")
self._refresh_devices_btn.setEnabled(self._refresh_devices_fnisnotNone)
self._refresh_devices_btn.clicked.connect(self.refresh_devices)
device_row=QHBoxLayout()
device_row.addWidget(self._device_combo, 1)
device_row.addWidget(self._refresh_devices_btn)
form.addRow("Input Device:", device_row)
policy=QLabel(
"An unavailable or ambiguous explicit input will not switch to System Default. ""Refresh and reselect after device changes. Device IDs/names do not prove physical identity."
)
Jaktofunguje: Záložka obsahuje výběr mikrofonu, explicitní Refresh, RMS threshold a kalibraci; Refresh je vypnutý bez callbacku. Cojezmena: Přibyl callbackem řízený refresh a zachování nerozřešeného explicitního zařízení. Procjetotak: Samotné ID/jméno zařízení nepotvrzuje fyzickou identitu ani neopravňuje tichý přechod na System Default.
self._output_mode_combo=QComboBox()
forvalue, labelin (
("type", "Type into target application"),
("copy", "Copy to clipboard only"),
("copy_and_type", "Copy to clipboard and type"),
):
self._output_mode_combo.addItem(label, value)
form.addRow("Output mode:", self._output_mode_combo)
self._recovery_hotkey_input=QLineEdit()
self._recovery_hotkey_input.setPlaceholderText("ctrl+alt+shift+key:0x56")
Jaktofunguje: General nabízí hotkey/režim, output mode, recovery shortcut, opt-in history, retention, explicit Clear History a startup. Cojezmena: Přibyly output/recovery/history ovládací prvky vedle původních nastavení. Procjetotak: Clear History je samostatná potvrzovaná akce, ne součást Apply/Cancel; history limit widget má rozsah 1–100.
self._llm_prompt=QPlainTextEdit()
self._llm_prompt.setMaximumHeight(120)
self._llm_prompt.setTabChangesFocus(True)
self._llm_prompt.textChanged.connect(self._on_profile_prompt_changed)
llm_form.addRow("System Prompt:", self._llm_prompt)
self._llm_reset_prompt_btn=QPushButton("Reset to Current Default")
self._llm_reset_prompt_btn.clicked.connect(self._on_reset_prompt)
Jaktofunguje: Prompt editor je napojen na profilovou změnu; reset explicitně volá handler, zatímco provider/fallback pole zůstávají v LLM skupině. Cojezmena: Původní reset tlačítko s lambda přiřazením vystřídal handler, editor promptu je synchronizován s profilem. Procjetotak: Uložený prompt se nepřepisuje automaticky při upgradu; reset je explicitní provenance volba.
Jaktofunguje: Původní callback přímo nastavoval text editoru na konstantu. Cojezmena: V source už tato lambda není; reset obsluhuje _on_reset_prompt. Procjetotak: Reset nyní aktualizuje i původ a výběr profilu, ne pouze zobrazený text.
hint=QLabel(
"AI rewrite off always uses Raw. Manual selection ignores application mappings. ""Automatic selection matches the full executable path, then uses the default. ""Raw never calls an LLM. Edit the selected profile's prompt on the LLM tab; ""editing Clean creates a custom profile. Limits: 32 custom profiles, ""100-character names, 32 KiB UTF-8 prompts."
)
hint.setWordWrap(True)
form.addRow(hint)
self._rewrite_warning=QLabel(
"Saved rewrite catalog is damaged. Its exact value is retained during unrelated ""saves. Explicitly Reset or Replace to repair; Apply/OK saves the repair, ""Cancel discards it. Rewrite resolution is unavailable until repaired."
)
Jaktofunguje: Profiles tab exposes manual/automatic/default selectors, profile CRUD, full-path application mappings and damaged-catalog controls. Cojezmena: Nová šestá záložka odděluje výběr profilů od provider konfigurace a vysvětluje raw/clean chování. Procjetotak: Poškozený katalog se nesmí potichu přepsat; jeho oprava vyžaduje explicitní volbu.
Jaktofunguje: Kliknutí předá handleru akci Replace. Cojezmena: Přidán samostatný callback pro vložení validovaného JSON katalogu. Procjetotak: Reset a nahrazení mají odlišné datové cesty, přesto používají stejnou potvrzovací hranici.
Jaktofunguje: Tlačítko volá společný writer s edit=False, tedy přidáním. Cojezmena: Přidaný callback znovu používá writer pro oba režimy. Procjetotak: Validace katalogu zůstává společná pro přidání i editaci.
Jaktofunguje: Tlačítko volá společný writer s edit=True. Cojezmena: Editace směruje do stejné validační cesty jako add. Procjetotak: Jeden writer udržuje stejná pravidla katalogu pro oba úkony.
self._stt_prompt_primary_check=QCheckBox("Send vocabulary prompt to primary STT")
self._stt_prompt_primary_check.setToolTip(
"Enable only if this endpoint accepts the multipart prompt field. ""A rejected prompt follows the normal fallback path, without retrying it removed."
)
form.addRow(self._stt_prompt_primary_check)
self._stt_lang=QComboBox()
form.addRow("Active language:", self._stt_lang)
Jaktofunguje: STT záložka obsahuje primary/fallback poskytovatele, jazyky a nezávislé opt-in přepínače pro předání vocabulary promptu. Cojezmena: Prompt checkboxy jsou doplněné pro oba endpointy; URL a key pole dostala vysvětlující tooltipy. Procjetotak: Volání promptu je závislé na podpoře multipart pole konkrétním endpointem; chyba pokračuje běžnou fallback cestou.
self._build_general_tab()
self._build_stt_tab()
self._build_llm_tab()
self._build_audio_tab()
self._build_vocabulary_tab()
self._build_profiles_tab()
# Bottom bar.btn_import=QPushButton("Import from .env")
btn_import.clicked.connect(self._on_import_env)
btn_reset=QPushButton("Reset to Defaults")
btn_reset.clicked.connect(self._on_reset)
Jaktofunguje: Sestavuje šest záložek v pořadí General, STT, LLM, Audio, Vocabulary, Profiles a společný spodní pruh akcí. Cojezmena: Připojuje dvě nové záložky při zachování pořadí prvních čtyř. Procjetotak: Stávající indexy General/STT/LLM/Audio zůstávají stabilní.
hint=QLabel(
"Preferred spellings for names, acronyms, identifiers, and project terms. ""These guide AI cleanup, not exact replacements or permission to invent content. ""STT receives them only for providers explicitly opted in on the STT tab. ""Engineering limits: 128 terms, 128 characters per term, 8,000 characters of ""rendered guidance. Blank terms are discarded; duplicate spellings keep the first."
)
hint.setWordWrap(True)
form.addRow(hint)
Jaktofunguje: Vocabulary tab nabízí term list, edit input a add/edit/remove plus repair controls. Cojezmena: Nová záložka zpřístupňuje seznam preferovaných spellingů a uvádí normalizační limity. Procjetotak: Vocabulary slouží jako guidance pro cleanup; STT ho dostane pouze při samostatném opt-in.
Jaktofunguje: Reset tlačítko volá handler s akcí Reset. Cojezmena: Přibyl explicitní callback pro opravu poškozeného sidecaru. Procjetotak: Handler zachová potvrzení před odstraněním původní hodnoty.
Jaktofunguje: Replace tlačítko volá tentýž handler se samostatným Replace režimem. Cojezmena: Přibyla větev pro nahrazení a následné zadání termů. Procjetotak: Oprava je vědomá akce, oddělená od běžného ukládání nesouvisejících polí.
Jaktofunguje: Validuje návrh serializací přes catalog encoder; chybu ukáže a vrátí False. Cojezmena: Přibyl společný validační helper pro změny profilu/mappingu. Procjetotak: Neplatný návrh se nepřenese do živého editor state.
Jaktofunguje: Přenáší widgety do _working; ukládá vocabulary jen pokud původní corrupt value není zachovávaná. Cojezmena: Sbírá nová general/STT pole i validovaný profilový návrh; při corrupt katalogu nepřepisuje catalog state. Procjetotak: Nesouvisející změny se dají uložit bez zahození přesných poškozených sidecar hodnot.
return (
QMessageBox.question(
self,
f"{action} Damaged Rewrite Catalog",
f"{action} the damaged catalog? The retained original will be replaced only when ""you Apply or OK, and cannot then be recovered by Screamer.",
QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
==QMessageBox.StandardButton.Yes
)
Jaktofunguje: Zobrazí Yes/No dialog s výchozí odpovědí No a vrátí, zda bylo potvrzeno. Cojezmena: Nová potvrzovací brána vysvětluje, že původní data budou nahrazena až při Apply/OK. Procjetotak: Oprava katalogu nemá být implicitní vedlejší účinek nesouvisejícího save.
return (
QMessageBox.question(
self,
f"{action} Damaged Vocabulary",
f"{action} the damaged vocabulary? The retained original will be replaced when ""you Apply or OK. It cannot be recovered by Screamer after that save.",
QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
==QMessageBox.StandardButton.Yes
)
Jaktofunguje: Potvrzuje Reset/Replace a defaultně volí No. Cojezmena: Nový confirmation dialog rozlišuje lokální draft od následného uložení. Procjetotak: Původní corrupt sidecar zůstává zachován, dokud uživatel změnu potvrdí a uloží.
Jaktofunguje: Odmítne callback/thread/pending kolizi, nejdřív vyřeší vybrané zařízení, pak spustí calibration worker. Cojezmena: Worker dostává zařízení po ID i jménu a identity se ověřuje před spuštěním. Procjetotak: Neplatný nebo nerozřešený explicitní výběr skončí varováním místo kalibrace jiného vstupu.
Jaktofunguje: Po potvrzení Replace načte JSON, dekóduje ho a odmítne ValueError; Reset přímo opraví na čistý katalog. Cojezmena: Přibyla obsluha obou explicitních repair režimů. Procjetotak: Vložené katalogy procházejí stejným dekodérem před změnou pracovního nastavení.
if (
QMessageBox.question(
self,
"Clear History",
"Delete all saved text history now? This is separate from ""Apply/Cancel and cannot be undone. Copies in other apps or backups are not deleted.",
QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
==QMessageBox.StandardButton.Yes
):
self.clear_history_requested.emit()
Jaktofunguje: Po výslovném Yes emituje signal; No nic nemaže. Cojezmena: Přibyl dedikovaný clear-history request mimo config save. Procjetotak: Clear je samostatná nevratná operace a nesouvisí s Apply/Cancel.
Jaktofunguje: Přepne na LLM tab, otevře skupinu a zaostří prompt editor. Cojezmena: Přidán přímý skok z výběru profilu k promptu. Procjetotak: Profilový prompt se edituje ve stávajícím LLM editoru místo duplikovaného widgetu.
withlog_duration(log, "Settings import from .env"):
self._collect()
ifself._rewrite_errorisnotNone:
self._show_validation_issue()
returnself._working=import_from_env(self._working)
self._populate(self._working)
Jaktofunguje: Nejdřív sesbírá draft; pokud katalog návrhu při serializaci selhal, zastaví import. Jinak importuje a znovu naplní UI. Cojezmena: Nová kontrola zabrání přepsání rozpracovaného nevalidního profilu importem. Procjetotak: Import nesmí zahodit změnu, kterou nelze bezpečně zakódovat.
Jaktofunguje: Odebere mapping pouze při platném selected row a překreslí seznam. Cojezmena: Přibyla CRUD akce pro mapování aplikace. Procjetotak: Zobrazená položka a datový seznam jsou udrženy ve stejném pořadí.
Jaktofunguje: Výběr seznamu načte přesnou cestu a profile ID do editačních widgetů. Cojezmena: Přidán synchronizační handler mapping editoru. Procjetotak: Path a profile ID zůstávají propojené s vybranou položkou.
Jaktofunguje: V edit režimu vyžaduje řádek, jinak přidá nový AppMapping; před přijetím návrhu validuje celý katalog. Cojezmena: Jeden handler pokrývá add i edit a aktualizuje dekódovaný seznam až po validaci. Procjetotak: Cesta aplikace i cílový profil podléhají centrálním katalogovým pravidlům.
Jaktofunguje: Vytvoří custom profile s novým UUID, validuje katalog a po přijetí vybere nový profil. Cojezmena: Přibyla CRUD tvorba s validací limitů/identity. Procjetotak: Stabilní ID umožňuje bezpečné reference z defaultu, manuální volby a mappingů.
Jaktofunguje: Zakazuje smazat built-in a každý custom profil, který je zvolen default/manual nebo odkazován mappingem. Cojezmena: Nová kontrola závislostí před CRUD odstraněním. Procjetotak: Odstranění nesmí zanechat neplatná ID ve výběrech nebo aplikačních pravidlech.
Jaktofunguje: Ignoruje programové loading změny i poškozený katalog; Raw je read-only, změna Clean vytváří custom profil a editace vlastního profilu zachovává jeho ID. Cojezmena: Přibyla baseline/provenance správa a undo stack se při aktualizaci selectorů zachová. Procjetotak: Čistý builtin prompt se nemění globálně; úprava vytvoří vlastní profil s konkrétním ID.
Jaktofunguje: Zachová ID, kind i prompt, mění pouze jméno; built-ins odmítá a návrh ověřuje encoderem. Cojezmena: Přibyla editace názvu custom profilu. Procjetotak: Reference na profil zůstávají stabilní i při přejmenování.
Jaktofunguje: Guarduje programové výběry, vyhledá profil podle ID a naplní název/prompt; Raw editor uzamkne. Cojezmena: Nový selection handler propojuje profilový selector s existujícím prompt editorem. Procjetotak: Identita se opírá o stabilní profile ID, ne zobrazované jméno.
Jaktofunguje: Při corrupt catalog i vocabulary vyžádá potvrzení před vytvořením default configu a refresh UI. Cojezmena: Reset je nově chráněn pro oba zachované poškozené sidecary. Procjetotak: Globální reset by jinak zahodil jejich původní přesné hodnoty bez explicitního souhlasu.
Jaktofunguje: Po případném potvrzeném katalog repair nastaví současný default prompt a provenance, manuální Clean výběr a aktualizuje selectors. Cojezmena: Callback nahrazuje přímé nastavení textu editoru. Procjetotak: Reset promptu synchronizuje text s uloženým profile selection a původem.
Jaktofunguje: Připojí zadaný term ke stávajícím hodnotám a před vykreslením předá celý seznam normalizátoru. Cojezmena: Přibyla add operace vocabulary. Procjetotak: Jednotná validace pokrývá seznam i jednotlivý vstup.
Jaktofunguje: Vyžaduje selected řádek, nahradí jeho text a normalizuje výsledný seznam. Cojezmena: Přibyla edit operace termu. Procjetotak: Celková normalizace odhalí konflikt/limit vzniklý změnou.
Jaktofunguje: Odebere vybranou položku a vyčistí input; bez selection nedělá nic. Cojezmena: Přibyla remove akce pro draft vocabulary list. Procjetotak: Odstranění je lokální změna seznamu až do běžného collect/save.
Jaktofunguje: Po potvrzení zahodí retained original z pracovního draftu, vyčistí termy a odemkne UI; Replace zaostří vstup. Cojezmena: Přidána oprava poškozeného vocabulary sidecaru s odlišením Reset/Replace. Procjetotak: Samotná oprava nezačne mazat původní persistovanou hodnotu; ta se nahradí až po save.
Jaktofunguje: Výběr položky zkopíruje její spelling do editačního pole. Cojezmena: Přidán list-to-input handler. Procjetotak: Editace pracuje s konkrétním vybraným termem.
Jaktofunguje: Čte config do General/STT/LLM/Audio/Vocabulary widgets a deleguje načtení katalogu profilům. Cojezmena: Nová pole a dvě tabulky doplňují původní čtyři taby; původ promptu i corrupt sidecars projdou specializovanými helpery. Procjetotak: Centralizované naplnění udržuje stejné UI po init, reset i import.
Jaktofunguje: Přestaví combo ze seznamu zařízení, ukládá čisté jméno do role UserRole+2. Cojezmena: Nyní vyčistí widget před obnovením a připojí stabilní name metadata. Procjetotak: Změna enumerace může změnit ID; jméno umožní resolveru porovnání bez předstírání fyzické identity.
Jaktofunguje: Migruje/dekóduje katalog; parse chyba uloží exact original marker a zobrazí damaged state místo profilů. Cojezmena: Přibyla načítací a provenance cesta pro katalog včetně neplatných persisted ID. Procjetotak: Corrupt data se uchová při unrelated save a vyžaduje explicitní repair.
Jaktofunguje: Překreslí mapování jako executable path → jméno cílového profilu. Cojezmena: Nový renderer pracuje s profily podle ID. Procjetotak: Uživatelské jméno je jen zobrazení; uložený mapping odkazuje na ID.
Jaktofunguje: Přestaví všechny čtyři nabídky z ID, zachová dosavadní výběry, a guardem potlačí signály při plnění. Cojezmena: Přidána společná synchronizace profilových selectorů a řízení promptu. Procjetotak: CRUD změna seznamu nemá náhodně přepsat selection nebo rozběhnout editor callback.
Jaktofunguje: Odstraní corrupt marker a založí prázdný katalog s default Clean/manual stavem; poté znovu naplní profile UI. Cojezmena: Nový helper je společný základ Reset i Replace. Procjetotak: Čistý výchozí katalog poskytuje konzistentní stav před případným importem náhradního obsahu.
Jaktofunguje: Před save normalizuje vocabulary a canonicalizuje parsovatelný recovery hotkey; chybové výjimky zobrazí, úspěch emituje applied a synchronizuje startup. Cojezmena: Přibyla normalizace nových hodnot a zachycení ValueError. Procjetotak: Persistované hodnoty se sjednotí před hranicí save; zbytek dialogu zůstává otevřen při selhání.
Jaktofunguje: Odstraní starý unresolved entry, resolverem zkusí ID/name proti aktuálním zařízením a při chybě ponechá explicitní položku „Unavailable / reselect“. Cojezmena: Nahrazuje lokální shodu názvů sdíleným resolve_device. Procjetotak: Nerozřešená explicitní preference se nemá tiše změnit na System Default.
Jaktofunguje: Vrací ID plus retained unresolved name, nebo name metadata vybraného enumerovaného zařízení. Cojezmena: Nový helper dává collect, refresh a kalibraci sdílenou reprezentaci selection. Procjetotak: Samotné ID nestačí, když se seznam zařízení změnil.
Jaktofunguje: Normalizuje celý návrh, při chybě varuje a ponechá editor zaměřený; úspěch přestaví list a vyčistí input. Cojezmena: Nový společný zápisový helper používají add/edit operace. Procjetotak: Limity a deduplikace se vyhodnotí konzistentně po každé změně.
Jaktofunguje: Nastaví prompt pod loading guardem a zapamatuje původ, origin a skutečně zobrazený text. Cojezmena: Nová baseline rozlišuje původní hodnotu od editor textu. Procjetotak: Programové naplnění widgetu nesmí být zaměněno za uživatelskou editaci ani zahodit řádkové konce.
ifself._rewrite_errorisnotNone:
QMessageBox.warning(self, "Invalid Rewrite Profiles", self._rewrite_error)
self._tabs.setCurrentIndex(5)
returnFalse# A retained damaged catalog is still a validation issue at the public boundary,# but must not prevent an unrelated settings save from preserving its source.
Jaktofunguje: Chybný draft katalogu blokuje save a přepne na Profiles; validační issue se zobrazuje s příslušnou tab index. Cojezmena: Poškozený zachovaný katalog je vyňat z blokace nesouvisejícího save. Procjetotak: UI validace nesmí vynutit ztrátu retained raw hodnoty, kterou sběr záměrně zachovává.
Jaktofunguje: Při damaged markeru ukáže warning/repair controls, označí tab a deaktivuje profil editing widgets včetně promptu. Cojezmena: Nová UI state projekce pro corrupt katalog. Procjetotak: Dokud není opraven, rezoluce katalogu není dostupná a editace běžného draftu by byla zavádějící.
Jaktofunguje: Zobrazuje repair UI a zamyká běžné term editing widgety, když je sidecar poškozený. Cojezmena: Přidaný damaged state pro vocabulary tab. Procjetotak: UI nezobrazuje prázdný term list jako validní náhradu za nečitelné uložené původní data.
Jaktofunguje: Vrací texty list widgetu v pořadí řádků. Cojezmena: Nový helper je zdrojem pro collect i add/edit. Procjetotak: Seznam termů je jediný draft state pro tuto část UI.
Jaktofunguje: Na user request zavolá enumerator; prázdný/failed výsledek ukáže warning a ponechá výběr, úspěch znovu naplní list a pokusí zachovat původní preference. Cojezmena: Přibyla explicitní runtime refresh cesta. Procjetotak: Refresh nemá při selhání ani změně seznamu smazat nerozřešenou preference.
Jaktofunguje: Worker uchová calibration callback, ID i jméno vybraného zařízení. Cojezmena: Konstruktor nově přijímá name vedle volitelného ID. Procjetotak: Worker může před spuštěním vyřešit aktuální identitu zařízení společným audio resolverem.
try:
device_id=resolve_device(self._device_id, self._device_name)
self.succeeded.emit(self._fn(device_id))
exceptExceptionase: # surface any calibration failure to the dialogself.failed.emit(str(e))
Jaktofunguje: Worker nejprve vyřeší zařízení podle zachyceného ID/jména, následně zavolá calibration function; chybu emituje jako text. Cojezmena: Resolver předchází volání callbacku na zastaralém ID. Procjetotak: UI thread zůstává volný během blokující kalibrace a nejednoznačné zařízení se projeví jako failure.
PR komentář — Detail 5: test coverage recovery, persistence a providerů
Tato část rozepisuje 63 přidaných testovacích funkcí, fixture metod a callbacků z ledgeru. Každý odkaz ukazuje na pinned diff-source snapshot d533a74886529f5e0b0822d89b63d65246ed763d; řádek odpovídá source rozsahu ledgeru. Ukázka je doslovný výřez vlastního těla symbolu (u velmi krátkých callbacků jeho celé tělo), nikoli obecný popis očekávání.
tests/test_recovery.py — checkpointy, retry a bezpečné doručení
Jak to funguje: Každý lifecycle případ dostane tray, session s oddělenou kopií konfigurace, cílové okno a uloží/restauruje clipboard; start workeru je nahrazený mockem. Co je změna: Testovací kontext izoluje vedlejší účinky Qt i worker scheduling. Proč: Assertions níže ověřují přechody main-app stavu přímo, bez spuštění worker threadu.
Jak to funguje: Zavolá _start_worker se známým WAV a session a vrátí uložený worker. Co je změna: Fixture zkracuje opakované spuštění workeru, aniž by zastírala jeho vlastnictví trayem. Proč: Další testy mohou pozorovat checkpoint a lifecycle téhož workeru.
Jak to funguje: STT vrátí raw text; při vstupu do rewrite callbacku test ověří, že raw už je v _latest a worker uvolnil WAV. Rewrite pak selže a kontrolují se failed, warning LLM, počet částečně odeslaných textových událostí i dokončení workeru. Co je změna: Raw checkpoint předchází rewrite; selhání rewrite nezahodí raw ani informaci o částečném výstupu. Proč: Obnova musí odrážet už nastalé side effecty.
Jak to funguje: Vnořený rewrite callback ověřuje vstupní raw, již uložený checkpoint a prázdnou WAV referenci, pak vyvolá testovací selhání. Co je změna: Assertion probíhá přesně na hranici mezi STT a rewrite. Proč: Pozdější error nesmí zakrýt, zda checkpoint nastal před uvolněním audio bufferu.
Jak to funguje: Simuluje signal finished před terminálním výsledkem: worker zůstává aktivní a hotkey ho nepřepíše. Po _Terminal se uloží final text; pozdní _on_raw_ready jej nesmí vrátit na raw. Co je změna: Dokončovací pořadí signálů je pokryto samostatně. Proč: Busy stav a terminální výsledek nesmějí záviset na pořadí Qt callbacků.
Jak to funguje: Rewrite callback vypne tray mezi raw checkpointem a terminalem. Test ověřuje, že se nic netypuje ani nekopíruje, raw zůstane zachován a pokus se zaznamená jako cancelled/suppressed; opětovné povolení nic zpětně nedoručí. Co je změna: Ověřena hranice mezi uložením raw a oprávněním k výstupu. Proč: Pozdější výsledek nesmí obejít zrušení.
Jak to funguje: Vnořený callback mění enabled stav přesně během rewrite boundary. Co je změna: V testu je časování přepnutí explicitní. Proč: Tím se kontroluje konkrétní závodní interval, ne pouze vypnutí před začátkem práce.
Jak to funguje: Dva STT neúspěchy ponechají stejné pending audio; retry používá session config snapshot i po změně live config. Úspěšný retry zapíše raw a vyčistí pending audio, ale nevolá rewrite ani type. Co je změna: Test pokrývá retry snapshot i pravidlo bez automatického výstupu. Proč: Opakovaný STT nesmí potichu změnit jazyk ani znovu spustit další pipeline kroky.
self.tray._rerun_rewrite(original)
with (
patch("src.main.transcribe") asstt,
patch("src.main.rewrite", return_value=PipelineResult("new candidate")),
Jak to funguje: Rerun nad uloženým záznamem volá pouze rewrite; vytvoří candidate s novým ID, zatímco _latest zůstává původní záznam. Co je změna: Ověřen je rozdíl candidate/latest a absence STT i type. Proč: Návrh nového textu nesmí přepsat záznam ani se sám odeslat.
Jak to funguje: Při vypnuté historii load/retain nevolá trim ani upsert. Po zapnutí se simulované selhání upsertu ohlásí, ale _latest stále ukazuje na RAM záznam. Co je změna: Test vymezuje opt-in storage boundary a in-memory fallback. Proč: Chyba disku nesmí vymazat poslední pokus z běžící aplikace.
Jak to funguje: STT callback vyvolá orderly exit a přesto vrátí raw; test kontroluje, že type se nevolá a history dostane raw se suppressed attempt. Co je změna: Exit nepřeruší checkpoint již dokončujícího se STT, ale potlačí doručení. Proč: Zachovat výsledek při vypnutí bez pozdního zásahu do jiné aplikace.
Jak to funguje: Fake committed store zaznamenává právě předaný záznam a vrací jej jako uložený. Co je změna: Callback zpřístupní obsah a pořadí commit pokusu pro assertion rodičovského testu. Proč: Test může ověřit raw i suppressed stav, nejen to, že metoda byla zavolána.
self.tray._exit()
returnPipelineResult("raw completed during exit")
Jak to funguje: Testovací transcribe boundary zahájí exit ještě před navrácením raw. Co je změna: Exit je vložen do přesného interleavingu STT a terminalu. Proč: Ověřit skutečnou situaci, kdy proces končí během blocking STT.
Jak to funguje: Připraví tray, target, aktuální record a obnoví původní clipboard po testu. Co je změna: Delivery případy sdílejí záznam i bezpečně izolovaný clipboard. Proč: Testují rozdílné výsledky copy/type bez závislosti na předchozím testu.
Jak to funguje: I bez foreground targetu režim copy vloží final do clipboardu; type se nevolá a attempt označí copy i post_key_state=not_requested. Co je změna: Copy-only výsledek nezávisí na focusu a nepředstírá Enter. Proč: Clipboard a aktivní okno jsou oddělené výstupní cesty.
Jak to funguje: Simuluje Windows clipboard, jehož setText nevyvolá Python výjimku, ale ownership zůstane false. Výsledek je clipboard_failed, type se nevolá a chyba je nahlášena. Co je změna: Úspěch neodvozují jen z absence exception. Proč: Win32 clipboard může odmítnout převzetí bez Python exception.
Jak to funguje: S úspěšným clipboard ownership a stejným foreground targetem copy_and_type kopíruje a pošle type_text s expected target; attempt zachycuje oba výsledky zvlášť. Co je změna: Kopírování neznamená automaticky, že typování také uspělo. Proč: Doručení je složeno z odděleně pozorovatelných side effectů.
Jak to funguje: Clipboard dostane final, ale změněná identita foreground okna způsobí vynechání type a attempt withheld. Co je změna: Výsledek copy zůstává pravdivý nezávisle na ochraně typování. Proč: Typování cílí jen na okno ověřené proti původní identitě.
Jak to funguje: Injection reportuje text events submitted, ale selhání post-key; attempt zachovává events_submitted a zvlášť reported_failed. Co je změna: Selhání Enter/koncového klíče nepřepíše stav již odeslaného textu. Proč: Historie musí rozlišit částečný/úspěšný text od následné klávesy.
self.tray._arm_recovery(self.entry, "raw")
with (
patch("src.main.get_foreground_target", return_value=WindowIdentity(100, os.getpid())),
patch("src.main.type_text") asinject,
Jak to funguje: Armovaný raw se při cíli Screameru nevloží; při běžném cíli se final vloží jednou, druhé vyvolání už ne. Co je změna: Ověřena jednorázovost i blokace self-targetu. Proč: Recovery insertion je explicitní akce s ochranou proti vložení do vlastní aplikace.
Jak to funguje: Chybující delete a potvrzený clear ponechají latest, armed i committed záznamy beze změny. Co je změna: UI akce neodstraní RAM state před úspěchem storage operace. Proč: Selhání persistence nesmí předstírat smazání, které disk nepřijal.
Jak to funguje: Po úspěšném store delete jsou matching latest a armed volba zrušeny. Co je změna: RAM reference se vyčistí až po store success. Proč: UI nemá dál nabízet recovery volbu pro potvrzeně odstraněný záznam.
Jak to funguje: Kopie staršího committed záznamu vytvoří aktuální RAM attempt; neúspěšný upsert jej ponechá v _latest, zatímco committed snapshot stále nemá delivery attempt. Co je změna: Pokus o výstup a persistovaný záznam jsou rozlišeny. Proč: Disková chyba nesmí vymazat informaci o právě provedené akci.
Jak to funguje: Copy candidate vloží jeho final text a aktualizuje candidate RAM attempt; history upsert se nevolá a _latest zůstává jiný záznam. Co je změna: Candidate preview zůstává mimo implicitní history commit. Proč: Samotná kopie návrhu není důkazem, že jej uživatel přijal jako konečný výsledek.
Jak to funguje: Nastaví history limit 1 a ověří, že delete store dostane ID i tento limit. Co je změna: Retention policy pro záznamy aplikace přechází do committed store operace. Proč: Výsledný diskový a UI seznam mají respektovat stejnou mez.
Jak to funguje: HTML-like raw/final se zobrazí jako prostý text; refresh nemění clipboard. Copy final a Arm raw jsou explicitní kliky; bez výběru jsou copy/delete disabled. Co je změna: Test ověřuje literal rendering i oddělení zobrazení od side effectu. Proč: Transcript není markup ani implicitní clipboard příkaz.
tests/test_results.py — formát a selhání lokální historie
returnDictationRecord(
id=identifier,
created_at="2026-09-30T12:00:00+00:00",
raw_text="Sensitive raw transcript",
Jak to funguje: Factory dává recordům stabilní ID, čas a transcript; changes umožňuje testům měnit jednotlivá pole. Co je změna: Sdílený input explicitně obsahuje citlivý raw text. Proč: Persistence assertions mohou ověřit, co se šifruje i co se nesmí objevit v payloadu.
Jak to funguje: Každý test má vlastní temporary history soubor; DPAPI protect/unprotect je nahrazen reverzibilním base64. Co je změna: Tyto testy cvičí formát a disk boundary, ne kryptografickou ochranu. Proč: Umožňuje deterministicky kontrolovat ciphertext bytes a simulovat storage chyby.
Jak to funguje: Uloží raw, rewritten text, warning a delivery attempt; načtení vrátí stejný record, zatímco ciphertext neobsahuje citlivý text a payload schema neobsahuje audio/config. Co je změna: Ověřen roundtrip persisted fields bez ephemeral audio či credentials. Proč: Historie má uchovávat výsledek, ne vstupní WAV ani nastavení.
Jak to funguje: Upsert stejného ID nahradí checkpoint; nové záznamy při limitu vyřadí nejstarší a explicitní nižší limit dále omezí seznam. Co je změna: Jeden record může mít raw a později final checkpoint bez duplikace ID. Proč: Persistence reflektuje průběh jedné dictation session.
Jak to funguje: Postupně zapisuje 101 záznamů s limitem 100; očekává zachování entry 001–100. Co je změna: Test potvrzuje pevnou horní mez i vyřazení nejstarší položky. Proč: Historie nemá růst neomezeně při opakovaném používání.
Jak to funguje: Nečitelné soubory zablokují běžný upsert, vrátí HISTORY_STORAGE_FAILED a původní bytes zůstanou stejné; explicitní clear pak vyčistí store. Co je změna: Corruption fail-closed, nikoli tichý overwrite. Proč: Automatický commit nesmí zničit jedinou kopii poškozených dat.
Jak to funguje: Unknown/boolean version, nevalidní raw/warnings, duplicate ID a nepodporované audio pole mají zablokovat upsert a zachovat původní encoded file. Co je změna: Schema validation se ověřuje včetně falešně truthy True version a zakázaných polí. Proč: Neznámý či poškozený obsah se nesmí normalizovat destruktivním zápisem.
Jak to funguje: Selhání atomic replace při novém upsertu ponechá dosavadní bytes beze změny a odstraní temporary file. Co je změna: Test pokrývá neúspěch v commit boundary, ne pouze exception propagation. Proč: Částečný zápis nesmí nahradit committed historii.
Jak to funguje: Selhání replace při delete a unlink při clear vyvolají ScreamerError; load stále vrací record. Následné úspěšné delete/clear odstraní soubor. Co je změna: Selhání mutace nezmění viditelný committed obsah. Proč: UI a storage musejí zůstat konzistentní s tím, co disk skutečně uložil.
Jak to funguje: Pro not-requested, pending, failed a cancelled rewrite vrací final_text raw transcript. Co je změna: Fallback finálního textu ověřen pro všechny neúspěšné/neukončené statusy. Proč: Recovery/copy musí mít použitelný text i bez rewrite outputu.
Jak to funguje: Validních 101-entry file load/trim view vrací nejvýše 100, ale trim(1) samo nemění raw bytes; explicitní bounded delete zapíše ořezaný seznam. Co je změna: Oversized legacy state je read-only dokud nenastane explicitní bounded commit. Proč: Pouhé čtení nesmí bez požadavku přepsat uživatelský soubor.
Jak to funguje: Pouze Windows spouští skutečný HistoryStore/DPAPI roundtrip; soubor neobsahuje raw bytes transcriptu, načtení vrací původní record. Co je změna: Reálné DPAPI ověřování je platform-guarded. Proč: Base64 testy jinde nepotvrzují ochranu DPAPI; tento test neposkytuje Windows průkaz mimo Windows runner.
tests/test_roadmap_backends.py — vocabulary, request prompts a keyless STT
Jak to funguje: Persistence testy směrují APP_DIR do dočasné složky a vypínají DPAPI dostupnost. Co je změna: Izolují preference serialization od Windows secret API. Proč: Vocabulary/settings roundtrip lze ověřovat i na Linuxu bez tvrzení o DPAPI.
Jak to funguje: Save/load ořízne whitespace, odstraní prázdné/case-insensitive duplikáty a zachová první display spelling i pořadí; prompt opt-ins defaultují false. Co je změna: Test zakotvuje canonical vocabulary seznam. Proč: Request guidance musí být deterministická a čitelná přes roundtrip.
damaged='["keep this original", 42]'settings.setValue("vocabulary_entries", damaged)
settings.sync()
Jak to funguje: Poškozená vocabulary se v configu projeví prázdnou, ale save jiné preference zachová přesný původní serialized value. Co je změna: Nevalidní data nejsou tiše přepsána unrelated save operací. Proč: Uživatel může opravit/obnovit data explicitně, ne ztratit je bokem.
Jak to funguje: Validation odmítá line/control chars, příliš dlouhé termy, >128 entries, neznámý output mode, history limit mimo 1..100 i unsafe recovery hotkeys. Co je změna: Config validation testuje vocabulary i output preference invariants. Proč: Nevalidní draft se nesmí uložit do persisted configu.
Jak to funguje: Potvrzuje history opt-in default off, limit 100, output type; následný save/load zachová enabled, zvolený limit i copy_and_type. Co je změna: Výchozí privacy a serializace preferencí mají explicitní assertions. Proč: Nastavení historie/výstupu nesmí být domýšleno z jiných provider voleb.
Jak to funguje: Všechny čtyři opt-in kombinace kontrolují dva STT requesty při primary connect failure; každý request zachová jazyk a přidá prompt právě podle svého provider flagu. Fallback warning zůstává. Co je změna: Prompt opt-in je per provider, ne sdílený. Proč: Endpointy se liší v podpoře vocabulary promptu.
Jak to funguje: I s primárním prompt opt-in se request bez termů odešle bez prompt field. Co je změna: Samotný enable flag nevytváří prázdný prompt. Proč: Request obsahuje guidance pouze tehdy, když ji skutečně lze sestavit.
raw="Ask ChatGPT to explain this identifier."cfg=AppConfig(
llm_enabled=True,
llm_api_key="primary",
Jak to funguje: Primary LLM failure vede k fallbacku; oba requesty sdílejí system guidance (vlastní system prompt, vocabulary spelling preference, jazyk), zatímco user message je přesně raw transcript. Co je změna: Vocabulary/language guidance patří system role, transcript zůstává literal user content. Proč: Guidance nesmí přepisovat transcript boundary; assertions nekontrolují LLM interpretaci, jen request payload.
Jak to funguje: CLI spouští bezkey primary nebo enabled fallback config z env backfillu; WAV jde do mocked endpointu, Authorization chybí a stdout obsahuje transcription. Co je změna: CLI credential resolution se ověřuje pro keyless endpoint. Proč: CLI nesmí ukončit běh jen proto, že lokální STT nemá secret.
Jak to funguje: Config validace přijme STT URL+model bez key, ale po zapnutí LLM se stejná absence credentials projeví LLM validation issue. Co je změna: Test explicitně rozlišuje keyless STT od key-required LLM. Proč: Nevyvozovat obecné pravidlo „všichni providers jsou keyless“ z lokální STT podpory.
Jak to funguje: Request normalizuje trailing slash na /audio/transcriptions, posílá WAV multipart a verbose_json; bez key není Authorization. Co je změna: Route/request body testuje keyless path beze změny OpenAI-compatible payloadu. Proč: Nesmí být vymyšlen implicitní auth header.
Jak to funguje: Primary request používá svůj Bearer key; po connection failure fallback request používá jeho vlastní case-insensitive custom Authorization místo primary secret. Co je změna: Provider auth se nepřenáší mezi primary a fallback. Proč: Keyless fallback může mít vlastní custom auth contract.
Jak to funguje: Enabled fallback URL+model bez primary konfigurace projde validací, odešle jediný request bez Authorization a vrátí fallback warning. Co je změna: Fallback může být jediný nakonfigurovaný STT provider. Proč: Aktivní local endpoint nemusí mít separátní primární cloud endpoint.
Jak to funguje: Neúspěšný local primary bez enabled fallback vyvolá ScreamerError po jediném HTTP pokusu, přestože fallback URL/key jsou vyplněné. Co je změna: Přítomnost fallback fields sama nespouští cloud request. Proč: Fallback side effect musí respektovat explicitní enable flag.
tests/test_roadmap_settings.py — Apply/Cancel draft a explicitní akce
Jak to funguje: Settings test používá temp config directory, DPAPI off a nepodporované zařízení; dialog se staví nad uloženým local STT configem. Co je změna: Apply/Cancel případy zůstávají oddělené od host machine settings. Proč: Výsledky QSettings testu nemají být ovlivněny uživatelským profilem.
Jak to funguje: Přidá vocabulary a primary opt-in, provede Apply; uložený config zachytí změny, ale původní live AppConfig zůstane nezměněn. Co je změna: Apply persistence oddělena od instance předané dialogu. Proč: Dialog edituje draft, ne sdílenou config mutaci před Apply.
self.dialog._vocabulary_input.setText("Only in draft")
self.dialog._vocabulary_add_btn.click()
self.dialog._output_mode_combo.setCurrentIndex(
Jak to funguje: Draft vocabulary, output mode, history enabled/limit se změní; reject() a následný load vrací původní persisted defaults. Co je změna: Cancel zahazuje nastavení draftu. Proč: Nepotvrzené preference se nesmějí uložit při zavření dialogu.
Jak to funguje: Signal observer zaznamenává explicitní clear request do listu. Co je změna: Callback umožňuje odlišit signal od pouhého draftu. Proč: History clear je samostatný side effect, nikoli preference podléhající Cancel.
Jak to funguje: Odmítnutí confirmation nevyšle signal; potvrzení vyšle právě jeden request, který zůstane i po reject dialogu. Co je změna: Clear je explicitní potvrzená operace mimo preference draft. Proč: Cancel nesmí vrátit zpět již samostatně potvrzené mazání.
Jak to funguje: Apply/load zachovává prázdný STT key, URL i model. Co je změna: Keyless provider settings roundtrip bez fabricated secret. Proč: Settings UI save path nesmí inject default authorization data.
settings=_get_qsettings()
damaged='{"do not discard": "on unrelated save"}'settings.setValue("vocabulary_entries", damaged)
Jak to funguje: Reset confirmation + Cancel zachová původní damaged serialized value; nová dialog instance s potvrzeným resetem ji uloží jako [] až při Apply. Co je změna: Repair draft není okamžitý destructive write. Proč: Poškozená vocabulary se smaže pouze po explicit confirmation a Apply.
tests/test_single_instance.py — lock ownership i orderly Qt shutdown
Jak to funguje: Spustí Python child process v repo rootu s temporary APP_DIR, offscreen Qt, captured output, timeout a check=True. Co je změna: Single-instance workflow se testuje přes reálnou process boundary. Proč: In-process lock mocks nemohou dokázat vzájemné vyloučení OS processes.
Jak to funguje: Držený lock childu zablokuje main před config/start hook setup; po unlock app startuje a po skončení lock file není. Co je změna: Ověřena blokace před settings/hooks, ne jen duplicate window. Proč: Druhý proces nemá provádět startup side effects.
Jak to funguje: Child process lock holder exits via os._exit; parent confirms file exists, then next app child recovers stale lock and starts. Live-lock behavior je pokryt samostatným testem. Co je změna: Dead owner recovery je procvičena přes skutečný process death. Proč: Stale lock cleanup nesmí vyžadovat ruční deletion.
deftranscribe(_audio, _config):
started.set()
assertrelease.wait(5)
returnPipelineResult('raw completed after Qt quit')
Jak to funguje: Child starts real tray worker and holds STT; external Qt quit occurs before release. Test pak ověří worker je dokončen, raw se persisted se suppressed attempt, není copied a QLockFile lze získat až po ukončení app. Co je změna: Orderly external quit čeká na worker/checkpoint před unlock. Proč: Další instance nesmí nastartovat, dokud první nedokončila final checkpoint. Test běží pouze v testovaném prostředí; nenahrazuje Windows manual run.
Rozsah důkazů
Dodané gate evidence uvádí 287 testů na Linuxu: 282 pass a 5 skip; Windows DPAPI test je skipován mimo Windows. Tato appendix dokumentuje testované assertions, nikoli Windows manual pass ani tvrzení, která testy neprokazují.
Položka 80 obsahuje navazující font/DPI opravu z commitu 75a674f44f28a05abad2640eb8eb884d87446369; ostatní výňatky jsou z původního snapshotu d533a74886529f5e0b0822d89b63d65246ed763d a jejich mechanismy zůstávají stejné.
Jak to funguje: Konstruktor inicializuje měření/stav pod zámkem vedle bufferu a streamu. Co je změna: Přibyly poslední RMS, přítomnost callbacku, capture chyba, skutečná identita a status vstupu. Proč je to tak: Main thread potřebuje pozorovat aktuální capture bez přístupu k interním frames.
Jak to funguje: Vypočte block RMS, pod lockem odmítne pozdní callback nebo uloží původní samples a pozorovací state. Co je změna: Callback status se nově ukládá k UI evidence místo warning-only cesty. Proč je to tak: Transient status má být viditelná diagnostika, ne automaticky fatální chyba či změna audio vzorků.
Jak to funguje: Finished callback při aktivním capture zaznamená chybu a zavře příjem nových frames. Co je změna: Nový stream-finished handler; při očekávaném stopu nic nemění. Proč je to tak: Následný stop nesmí interpretovat neočekávaně ukončený stream jako použitelný capture.
Jak to funguje: Pod lockem sestaví jeden immutable snapshot bez kopírování audio bufferu nebo enumeration zařízení. Co je změna: Nové API pro koherentní polling. Proč je to tak: Meter UI může číst související callback evidence jako jeden state.
Jak to funguje: Resetuje capture state, povolí callbacky, otevře stream s finished handlerem, čte skutečné stream.device metadata a spustí jej; při selhání zavře stream, zastaví callbacky a smaže partial frames. Co je změna: Přidává actual-open-device provenance a stream finish; zachovává MIC_UNAVAILABLE pro start errors. Proč je to tak: UI nemá hádat default input a neúspěšný start nesmí zanechat audio.
Jak to funguje: Zakáže callbacky, vždy provede stop/close a vyhodnotí capture error, délku, absenci vzorků a silence threshold před zakódováním WAV. Co je změna: Valid-length bez callbacku či stream failure je MIC_DISCONNECTED; short nebo tichý zachycený vstup zůstává discardem. Proč je to tak: Odděluje ticho od vadného capture a nepředává partial WAV.
Jak to funguje: None ponechá výběr na systémovém defaultu; explicitní výběr ověří ID/name consistency a dovolí migraci jen na unikátní přesný název. Co je změna: Funkce přijímá device list a odmítá ambiguitu, substring náhradu i zmizelé zařízení. Proč je to tak: Explicitně zvolené zařízení se nemá bez potvrzení změnit na jiné.
ifcfg._rewrite_catalog_corrupt_originalisnotNone:
raiseValueError("Saved rewrite catalog is damaged; explicitly Reset or Replace it")
Jak to funguje: Vybírá existující katalog, generuje legacy custom profile z uloženého/user promptu nebo prázdný katalog pro default_v2. Co je změna: Nový resolver zachovává corrupt marker a prompt origin. Proč je to tak: Chybné uložené values nesmějí být zaměněny za prázdný katalog.
ifnotisinstance(profiles, list) orlen(profiles) >MAX_REWRITE_PROFILES:
raiseValueError("Choose no more than 32 custom rewrite profiles")
Jak to funguje: Ověří custom-only profiles, IDs/name/prompt limity, validní cestu a jedinečné mappings na existující profile. Co je změna: Nová společná validace před decode/encode. Proč je to tak: Katalog omezuje velikost i dangling/duplicate selection.
Jak to funguje: Sestaví id→profile index z built-ins a custom catalogu, zkontroluje selection mode i manual/default IDs a vrací mappings. Co je změna: Nový společný validator pro resolver i save. Proč je to tak: Volba se validuje bez ohledu na to, zda je globální LLM právě zapnuté.
Jak to funguje: Vrací code-owned Raw a Clean. Co je změna: Přidává stabilní built-in profile definitions. Proč je to tak: Built-in IDs nemusí být duplicitně persistovány jako custom.
ifnotisinstance(value, str):
raiseValueError("Rewrite catalog must be JSON-encoded text")
Jak to funguje: Dekóduje jen strict v1 JSON s exact envelope/row keys, následně validuje dataclasses. Co je změna: Přidává decoder, který poškozená nebo nepodporovaná data odmítne. Proč je to tak: Neznámý formát se nemá tiše interpretovat jako prázdný catalog.
Jak to funguje: JSON object_pairs_hook skládá klíče a duplicate odmítá. Co je změna: Nová nested parse funkce pro strict decode. Proč je to tak: Ambiguous JSON nemá vést k tiché volbě jedné z duplicitních hodnot.
ifnotisinstance(value, str):
raiseValueError("Vocabulary must be JSON-encoded text")
Jak to funguje: Vyžaduje serialized JSON text, dekóduje a normalizuje terms. Co je změna: Přidává vocabulary decoder. Proč je to tak: Config loader potřebuje odlišit validní list od corrupt value, které se zachovává.
Jak to funguje: Ověří obě kolekce a zapisuje versioned JSON se zachováním Unicode. Co je změna: Nový encoder katalogu. Proč je to tak: Persistovaný obsah musí mít stejná omezení jako decoder.
Jak to funguje: Normalizuje před JSON serializací. Co je změna: Nová persist codec. Proč je to tak: Disk i runtime dostanou stejnou canonical vocabulary form.
try:
fromdotenvimportdotenv_valuesexceptImportError:
log.debug("python-dotenv not installed; skipping .env import")
returncfg
Jak to funguje: Zachovává no-op bez dotenv; import backfill polí z .env a rozlišuje saved language key absence od explicitního saved empty. Co je změna: Explicitní Auto se už nepřepíše env jazykem. Proč je to tak: Chybějící persistence key a explicitní prázdná volba mají odlišný význam.
Jak to funguje: Načte known config values/DPAPI secrets, special-case decode kolekcí, uloží damaged originals do markerů a odvodí prompt provenance podle uložené key presence. Co je změna: Přidává loading output/history/vocabulary/profiles a migraci legacy promptu; language saved-presence je zaznamenaná. Proč je to tak: Invalid collections a saved prompts zůstávají zachovány pro explicitní opravu/migraci.
Jak to funguje: Migrate jen absent/noncorrupt catalog z legacy promptu, pak nastaví manual ID, pokud vznikl legacy profile. Co je změna: Nová příprava catalogu před save. Proč je to tak: Existing catalog a damaged origin se nesmí znovu migrovat nebo přepsat.
ifnotisinstance(value, str) ornotvalueorany(ord(c) <32forcinvalue):
raiseValueError("Application path must be a full Windows absolute path")
Jak to funguje: Ověří Windows absolute drive/UNC path a executable basename, normalizuje slash, path a case. Co je změna: Nová platform-independent path codec. Proč je to tak: App mapping používá stabilní exact full path místo názvu okna nebo substring.
ifnotisinstance(entries, list):
raiseValueError("Vocabulary must be a list of terms")
Jak to funguje: Validuje types/control chars, trimuje blanks, stable casefold dedupe a odmítá limity term/count/rendered context. Co je změna: Nový central bounded normalizer; terms se netruncují. Proč je to tak: Přijmout jen celý validní spelling set pro persistence i model request.
Jak to funguje: Posílá byte payload do stávající Windows DPAPI encrypt boundary. Co je změna: Nové byte API vedle string key wrapperu. Proč je to tak: History JSON se chrání jako bytes před diskovým zápisem.
Jak to funguje: Validuje catalog; precedence: global off→raw, legacy prompt, manual ID, jinak automatic path mapping nebo configured default. Co je změna: Nový pure session-snapshot resolver. Proč je to tak: Výběr se zmrazí při startu nahrávání, než worker začne network pipeline.
Jak to funguje: Validuje provenance/selections, migruje catalog, sestaví temp INI s encrypted secrets a zachovanými corrupt raws, sync/fsync/atomic replace; odstraňuje legacy key store až po úspěchu. Co je změna: Persistuje nové preference/catalogs v existující atomic save. Proč je to tak: Unrelated save nemá zničit invalid raw collection, kterou uživatel explicitně neopravoval.
Jak to funguje: STT konfigurace vyžaduje endpoint+model, nikoli key. Co je změna: Samostatný predicate; LLM completeness zůstává API key+URL+model. Proč je to tak: Podporuje keyless local STT bez uvolnění LLM auth contractu.
Jak to funguje: Dešifruje history bytes ve stejné DPAPI boundary. Co je změna: Nové inverse API k protect_bytes, platform/storage chyby ponechává viditelné. Proč je to tak: Reader nesmí při decrypt failure přejít na plaintext fallback.
Jak to funguje: Sbírá Issues pro profile/provenance, recovery/dictation chord, output/history bounds, vocabulary, STT/LLM completeness, headers a URLs. Co je změna: Rozšířena o nová nastavení a keyless-STT rules. Proč je to tak: Startup a settings mají jediný validátor, který vrací vše potřebné k opravě místo tichého fallbacku.
Jak to funguje: Přijímá optional recovery chord a inicializuje oddělené reservation/held/matched/pending state. Co je změna: Konstruktor přidává keyword-only recovery shortcut. Proč je to tak: Recovery matching nesmí sdílet state dictation chordu.
Jak to funguje: Počká na pending chord, release triggeru a úplné uvolnění jeho modifierů, pak vyšle recovery_requested. Co je změna: Nový release barrier. Proč je to tak: Manual insert se spustí až po uvolnění celého chordu.
Jak to funguje: Vytvoří Win32 queue/ctypes callbacks, rezervuje recovery chord, instaluje keyboard/mouse hooks a pumpuje message loop; při chybě signalizuje a odinstaluje. Co je změna: Callback dostává event flags a loop integruje recovery reservation. Proč je to tak: Windows plumbing zůstává na hook threadu a matcher může být testován nezávisle.
Jak to funguje: Při HC_ACTION rozbalí VK+flags, pošle matcheru a suppressne jen True match. Co je změna: Předává kb.flags, dříve pouze key code. Proč je to tak: Matcher identifikuje injected key events.
Jak to funguje: Předá mouse data+flags a potlačí pouze matched event. Co je změna: Přidává injection flags do matcher inputu. Proč je to tak: Hook callback zachovává origin event transparency pokud žádný configured chord neodpovídá.
ifflags&0x10: # LLKHF_INJECTED; only physical keyboard events drive shortcutsreturnFalse
Jak to funguje: Ignoruje injected input, Esc vyšle cancel, sleduje modifiers, vyhodnocuje recovery chord před dictation key a vrací suppression rozhodnutí. Co je změna: flags+recovery/Escape path. Proč je to tak: Aplikací vložené klávesy se nesmějí interpretovat jako fyzická user akce.
Jak to funguje: Překládá middle/X buttons; před matcherem klasifikuje injected mouse input, řeší recovery nebo původní mouse trigger. Co je změna: Přijímá flags a recovery match. Proč je to tak: Synthetic mouse event nemá vytvořit manual recovery chord.
if {MODIFIER_VK_TO_NAME[vk] forvkinself._recovery_held} !=recovery.mods:
returnFalse
Jak to funguje: První trigger press porovnává exact modifier set, nastaví matched/pending a jinak nearmuje chord. Co je změna: Nový recovery keydown matcher. Proč je to tak: Částečný/extra modifier stisk se nemá považovat za nakonfigurovanou klávesovou zkratku.
Jak to funguje: Uvolní trigger, přečte/resetuje matched a zkusí dokončit chord. Co je změna: Nová recovery trigger keyup větev. Proč je to tak: Matched state je jednorázový a release může začít pending completion.
Jak to funguje: Reservuje keyboard chord s MOD_NOREPEAT, recovery enabling závisí na OS success; failure loguje a signalizuje conflict. Co je změna: Nová conflict gate. Proč je to tak: Konflikt s OS registered hotkey nesmí vést k nejasnému recovery inputu; hook-only foreign bindings ale nejsou zjistitelné.
Jak to funguje: Před odstraněním hooků uvolní vlastní RegisterHotKey reservation, pak odinstaluje KB/mouse hooks. Co je změna: Cleanup nově zahrnuje registered chord. Proč je to tak: Listener restart/exit nemá ponechat vlastní OS hotkey zabraný.
Jak to funguje: Win32 guard, reset lifecycle/matching state, vytvoří daemon thread a čeká na queue/hooks attempt readiness. Co je změna: Recovery state se inicializuje/resetuje spolu s listenerem. Proč je to tak: Restart nemá zdědit starou částečně stisknutou chord state.
Jak to funguje: Deklaruje ctypes ABI pro registration/unregistration spolu s hook/message APIs. Co je změna: Přidány RegisterHotKey signatures. Proč je to tak: Chord conflict check potřebuje správné Win32 argument/return typy.
Jak to funguje: Ukládá oddělené submission counts a nastaví common INJECTION_FAILED. Co je změna: Nový typed error s partial counts. Proč je to tak: Post-key fail nesmí skrýt, že text eventy už odešly.
Jak to funguje: Windows-only čte foreground HWND/PID a volitelně executable path přes query-limited-information handle, který zavírá ve finally. Co je změna: Nová target identity boundary. Proč je to tak: Session může zacílit process instance bez sběru window title/content.
Jak to funguje: Validuje post key před odesláním, posílá UTF-16 unicode text v jednom SendInput batch; cílové HWND/PID ověří před textem a po pauze před post-key. Reports event counts; partial sends fail bez retry. Co je změna: Přijímá expected_target, vrací InjectionReport a zachovává text/post-key výsledky odděleně. Proč je to tak: Typování nesmí pokračovat do změněného foreground targetu a submission count není potvrzení viditelného textu.
Jak to funguje: Připojí WinError, pokud existuje, a předá aktuální counts typed exception. Co je změna: Výjimka navíc nese text/post-key counts. Proč je to tak: Main potřebuje zaznamenat partial output bez automatického opakování.
Jak to funguje: Sestaví keyboard down/up pair, pošle ho jedním batch a vyžaduje počet 2; failure deleguje error helperu. Co je změna: Aktualizuje samostatný post-key count, nikoli text count. Proč je to tak: Post-key submission je druhá operace s vlastním částečným výsledkem.
Jak to funguje: Staví on-demand dialog s plain-text raw/final read-only editors, records list, delivery metadata a buttons napojenými na explicit signals. Co je změna: Nová recovery UI třída. Proč je to tak: Copy/arm/retry/delete jsou explicitní události předané main owneru.
Jak to funguje: Click vyšle raw variantu přes shared selected-record dispatcher. Co je změna: Nový konkrétní UI callback. Proč je to tak: Raw copy je explicitní volba odlišná od final copy.
Jak to funguje: Click vybere final variantu a deleguje emit. Co je změna: Oddělený final-copy callback. Proč je to tak: Uživatel může kopírovat zpracovaný výsledek bez automatického vložení.
Jak to funguje: Click vyšle arm request pro raw record text. Co je změna: Přidává raw manual-insert choice. Proč je to tak: Po skrytí dialogu může uživatel přepnout focus a ručně vyvolat chord.
Jak to funguje: Click vyšle final-text arm request. Co je změna: Samostatná volba pro final output. Proč je to tak: Recovery zachovává raw/final distinction i při ručním insertion.
Jak to funguje: Click předá selected record pro explicit cleanup rerun. Co je změna: Přidává záměrnou rerun action. Proč je to tak: Candidate rewrite není neviditelná náhrada původního záznamu.
Jak to funguje: Click předá selected record delete signal. Co je změna: Přidává explicit selected-record deletion. Proč je to tak: Delete se vztahuje k aktuálnímu výběru, nikoli implicitně poslednímu výsledku.
Jak to funguje: Emits record-only signal pouze při valid selection. Co je změna: Nový common adapter pro cleanup/delete actions. Proč je to tak: Empty list nepředá neplatný record.
Jak to funguje: Předá record+raw/final variantu u valid selection. Co je změna: New adapter pro copy/arm. Proč je to tak: UI nesestavuje text ani necílí OS output; main dostává chosen record and variant.
Jak to funguje: Aktualizuje literal plain text, metadata/profile/warnings and last attempt evidence for selection; no record clears fields. Co je změna: Nový detailní recovery presentation. Proč je to tak: Transcript markup se vykreslí doslova a attempt je výslovně označen jako neověřená delivery.
Jak to funguje: Gates record actions on selected row, idle/busy, and enabled; clear, STT retry, discard have separate conditions. Co je změna: New state-aware button gating. Proč je to tak: UI nemá poslat record action, když není record nebo app state dovoluje jen některé akce.
Jak to funguje: Zaktualizuje busy/enabled/pending and records, znovu postaví list, obnoví selected record, zobrazí explicit history/clipboard/deletion notice a hotkey hint. Co je změna: Nový refresh interface pro main-owned data. Proč je to tak: Dialog zobrazuje aktuální volatile/persisted state, aniž by sám četl disk.
Jak to funguje: Bounds-check row against current records. Co je změna: Nový selection accessor. Proč je to tak: UI callbacks share one safe lookup and empty selection yields None.
Jak to funguje: Returns rewritten only for succeeded/non-None; otherwise raw. Co je změna: New computed final property. Proč je to tak: Failed/cancelled/pending rewrite cannot replace recoverable transcript.
Jak to funguje: Accepts explicit path for storage owner/tests, else selects app data encrypted history. Co je změna: New store instance boundary. Proč je to tak: Disk location is centralized with optional explicit test path.
Jak to funguje: Missing file is empty history; otherwise bounds size, DPAPI decrypts/JSON-decodes, validates envelope/version/all records/unique IDs and maps any failure to HISTORY_STORAGE_FAILED. Co je změna: New strict encrypted read path. Proč je to tak: Corrupt history must fail closed rather than becoming an empty writable store.
Jak to funguje: Validates records, serializes bounded version-1 JSON, DPAPI protects before temp-file write, fsync+atomic replace and cleanup on failure. Co je změna: New ciphertext-only commit path. Proč je to tak: Plaintext transcripts are never written to persistent history file.
Jak to funguje: Explicitly unlinks without decoding first. Co je změna: New corruption-bypass clear operation. Proč je to tak: Only explicit Clear can remove unreadable ciphertext; normal mutations fail closed.
Jak to funguje: Validates limit, reads full history, sorts UTC and writes only when applying preference actually removes older entries. Co je změna: Separate explicit Apply-retention operation. Proč je to tak: Merely opening history/view must not mutate persisted data.
Jak to funguje: Ordering key parses record timestamp for chronological retention. Co je změna: New ordering callback in explicit retention commit. Proč je to tak: Limit must keep latest records by timestamp, not insertion order.
Jak to funguje: Validates limit, reads committed data, removes ID, sorts/trims, writes atomically. Co je změna: New record delete read-modify-write. Proč je to tak: Deletion is an explicit persisted change with normal corrupt-file safeguards.
Jak to funguje: Chronologically orders retained entries after delete. Co je změna: New callback within the mutation. Proč je to tak: Trim applies to latest timestamps after selected record removal.
Jak to funguje: Fully validates through _load_records, sorts and returns recent maximum 100. Co je změna: New public validated read. Proč je to tak: View is bounded even if legacy stored collection is larger.
Jak to funguje: Uses parsed timestamps as ordering key. Co je změna: Chronological sort operation in history load. Proč je to tak: The exposed view is ordered by creation time, not file order.
Jak to funguje: Validates limit and returns last records from load() without write. Co je změna: New view-only retention method. Proč je to tak: Preference view should not commit deletion until explicit Apply.
Jak to funguje: Validates limit, replaces same ID, adds record, sorts/trims and commits through _write. Co je změna: New checkpoint-friendly upsert. Proč je to tak: Raw checkpoint and terminal update share stable ID without duplicate history entries.
Jak to funguje: Orders upserted records by parsed creation time before limit selection. Co je změna: New sort key callback in upsert. Proč je to tak: Retention evicts oldest timestamp even after update/retry order differs.
ifnotisinstance(value, dict) orset(value) !=set(DictationRecord.__dataclass_fields__):
raiseValueError("Invalid record fields")
Jak to funguje: Checks exact record fields and validates strings/timestamps/rewrite status/warnings/delivery schema/counts before materializing immutable record. Co je změna: New strict schema decoder for persistent history. Proč je to tak: Unknown, malformed or incompatible state must fail closed before mutation.
Jak to funguje: Accepts explicit optional None; otherwise requires bounded string and optional nonempty condition. Co je změna: New shared history field validator. Proč je to tak: Persisted optional metadata differs from required text/IDs.
Jak to funguje: Requires nonempty string, parses ISO timestamp and requires zero UTC offset. Co je změna: New timestamp schema check. Proč je to tak: Ordering/retention expects comparable UTC timestamps.
Jak to funguje: Produces timezone-aware UTC ISO timestamp. Co je změna: New record timestamp helper. Proč je to tak: History validation and ordering use an explicit UTC contract.
Jak to funguje: Normalizuje entries; returns empty if none, else creates bounded quoted spelling-only wrapper. Co je změna: Nový context renderer. Proč je to tak: Guidance remains separate from literal transcript and cannot be treated as untrusted instruction text.
Jak to funguje: Preserves disabled pass-through; composes saved prompt, vocabulary context and language hint; tries primary/fallback and returns raw with warning on failed output. Co je změna: Adds vocabulary to system prompt, not the transcript user field. Proč je to tak: Typographic hints guide spelling while keeping the transcript as distinct literal input.
Jak to funguje: Creates overlay/animations plus input device/level/data fields. Co je změna: Adds capture status state to existing indicator. Proč je to tak: Recording pill can distinguish no samples from quiet samples.
ifself._input_labelisnotNone:
prefix_w=fm.horizontalAdvance("Waiting for samples - ")
status_padding=4+2*self._PAD_X# Match the separate prefix/device advances used by input_status_text.status_w=prefix_w+fm.horizontalAdvance(self._input_label) +status_paddingmax_status_w=prefix_w+fm.horizontalAdvance("M"*32) +status_paddingscreen=QGuiApplication.primaryScreen()
ifscreenisnotNone:
max_status_w=min(
max_status_w, screen.availableGeometry().width() -2*self._MARGIN
)
width=max(
width,
prefix_w+status_padding,
min(status_w, max_status_w),
)
height+=text_h+14
Jak to funguje: Titulek i druhý řádek měří aktivním fontem; rozpočet jména používá 32 šířek M a dostupnou šířku obrazovky s marginem. Plný observation prefix má minimum, dlouhé jméno se nad rozpočet eliduje. Co je změna: Původní baseline neměl meter řádek; navazující CI oprava nahrazuje jeho počáteční pevný cap 420 px font-aware limitem. Proč je to tak: Windows CI ukázalo ořezávání i krátkých názvů při jiných font metrics. Regrese s 18bodovým fontem selže před opravou a projde po ní v explicitním dostatečně velkém viewportu; elision dlouhých názvů zůstává zachována.
Jak to funguje: Stops pulse and clears all capture UI evidence before no-op or fade. Co je změna: Hide clears new meter state too. Proč je to tak: Stale level/device must not be presented in the next recording.
Jak to funguje: Exposes last clamped level. Co je změna: New getter for inspection/UI tests. Proč je to tak: Current RMS meter value is observable without reading paint internals.
Jak to funguje: Returns empty without device; otherwise creates waiting/input-level prefix and elides device label to allocated width. Co je změna: New formatted status getter. Proč je to tak: Waiting/quiet state remains visible when hardware name is long.
Jak to funguje: Paints pill/pulsing dot/label and, when recording has input state, second-line text plus level bar. Co je změna: Adds meter and status drawing. Proč je to tak: Recording overlay exposes captured level and device observation in the existing widget.
Jak to funguje: Ignores updates outside recording; stores device label, clamps full-scale RMS if samples exist, resizes/repositions and repaints. Co je změna: Adds capture snapshot consumer. Proč je to tak: UI does not show input level in processing/idle state; RMS is not gain or speech confidence.
Jak to funguje: Updates label/color; resets meter state and initializes an unknown device label for recording; cancels pending fade-out before showing. Co je změna: State transition resets/initializes live input evidence. Proč je to tak: UI cannot carry stale prior-run meter into a newly shown recording state.
Jak to funguje: Requires URL+model predicate, emits multipart WAV/model/format/language/optional prompt; Bearer auth only when key exists; parses response and no-speech contract. Co je změna: No longer treats blank STT key as unconfigured. Proč je to tak: Supports local keyless server without fabricating auth or changing LLM key policy.
Jak to funguje: Builds primary/fallback configs, requires configured primary or enabled fallback, constructs bounded vocabulary spelling prompt, chooses it independently by provider flags and preserves fallback warning/error flow. Co je změna: Adds keyless endpoint readiness and provider-specific prompt field. Proč je to tak: A local STT API can omit credentials; prompt opt-in must not be assumed for every compatible service.
Jak to funguje: Test porovnává případ, kdy nalezené ID stále patří „USB mic“, s případem, kdy se zařízení pod stejným ID jmenuje jinak; čeká na calibrační thread a zpracuje Qt eventy. Co je změna: Přidává kontrolu aktuální identity zařízení před použitím vybraného ID. Proč je to tak: Kalibrace smí použít jen zařízení odpovídající uloženému výběru, ne jiné zařízení, které mezitím převzalo stejné ID.
Jak to funguje: Zkouší seznam s odlišným zařízením na uloženém ID i seznam se dvěma shodnými názvy; ověřuje unavailable stav, zobrazení ID a zachování uložené dvojice při collect. Co je změna: Pokrytí nejednoznačných názvů místo spoléhání na výběr podle jména. Proč je to tak: Shodný nebo konfliktní název sám o sobě nedokazuje totožnost mikrofonu, proto test hlídá, že dialog svévolně nepřesměruje volbu.
Jak to funguje: Stejná kontrola běží pro prázdný výsledek enumerace i vyhozenou výjimku; po varování ověřuje, že collect stále vrátí dříve vybrané ID a jméno. Co je změna: Přidává test obou cest selhání refresh. Proč je to tak: Neúspěšné obnovení seznamu nemá potichu nahradit platnou uloženou volbu ani se tvářit jako úspěšné obnovení.
Jak to funguje: Ověří, že výpis zařízení je dodán konstruktoru a refresh nastane až po kliknutí; apply zachová unplugged ID/jméno, zatímco explicitní výběr nového ID změní pouze pracovní config. Co je změna: Pokrytí odlišuje uloženou unavailable volbu od nové uživatelské volby. Proč je to tak: Otevření nebo Apply nemá znamenat souhlas s automatickou náhradou mikrofonu; Cancel navíc nepropíše pracovní změnu do původního configu.
Jak to funguje: Původní seznam obsahuje „USB mic Pro“, obnovený seznam přesnou „USB mic“; před refresh je volba unavailable, poté test ověřuje vybrané nové ID 7 a stejný přesný název. Co je změna: Přidává rozlišení přesné shody názvu od substringu. Proč je to tak: Podobný, delší název nestačí k nahrazení chybějícího zařízení; unikátní přesná shoda po replug je naopak v testovaném toku rozpoznána.
cfg, devices=[(1, "USB mic Pro")], refresh_devices_fn=lambda: [(7, "USB mic")]
Jak to funguje: Inline callback vrací při refresh jiné ID se stejným přesným jménem, přičemž počáteční zařízení má pouze delší podobný název. Co je změna: Tento fake je samostatná coverage položka a izoluje vstupní podmínky exact-replug testu. Proč je to tak: Test tím rozlišuje přesnou shodu po odpojení od nebezpečné substituce podle podřetězce.
Jak to funguje: Konfigurace ukazuje na unplugged USB mic, zatímco dostupný seznam obsahuje laptop mic; test volá kalibraci a kontroluje varování i nulové volání callbacku. Co je změna: Přidává negativní test kalibrace pro unavailable volbu. Proč je to tak: Kalibrace nesmí spadnout na výchozí vstup a změřit něco jiného, než co dialog ukazuje.
Jak to funguje: Každý test skupiny získá dočasné APP_DIR, vypne DPAPI a podporu startup registrace, uloží prompt s whitespace/CRLF a otevře dialog. Co je změna: Sdílený setup nastavuje izolované persistence prostředí a legacy původ pro prompt testy. Proč je to tak: Assertions o přesných uložených bajtech a původu nemají záviset na uživatelském nastavení ani Windows DPAPI.
Jak to funguje: Uživatel explicitně vloží aktuální výchozí text do editoru a přijme dialog; test porovná uložený text i llm_prompt_origin. Co je změna: Výchozí text zadaný jako editace dostane původ user_saved, nikoli default_v2. Proč je to tak: Rovnost obsahu sama neprokazuje, že uživatel použil akci Reset; původ zachycuje cestu editace.
Jak to funguje: Test explicitně resetuje prompt, přidá další editaci, vyvolá chybu atomického replace, vrátí pozdější editaci undo a znovu použije Apply. Co je změna: Kontroluje zachování resetu přes neúspěšné Apply i undo následujícího textového zásahu. Proč je to tak: Stav původu resetu nesmí být chybně odvozen pouze z posledního textového undo; retry má uložit default s default_v2.
Jak to funguje: Subtests kombinují legacy/default prompt s validačním či save selháním, provedou dočasnou editaci, kontrolují varování, počet replace a stav uloženého promptu/provenance; po undo a opravě modelu opakují Apply. Co je změna: Přidává kontrolu přesného stavu před i po retry pro různé hranice selhání. Proč je to tak: Neplatný config nemá dojít k nahrazení souboru a ani neúspěšné uložení nemá ztratit původní text nebo jeho původ.
Jak to funguje: Dialog s novým configem provede běžné Apply a test znovu načte uložený původ i přepínač rewrite. Co je změna: Rutinní uložení bez explicitní editace zachovává default_v2 a vypnutý LLM. Proč je to tak: Otevřít a uložit nastavení nemá být interpretováno jako uživatelská změna promptu ani jako zapnutí rewrite.
Jak to funguje: Test vytvoří rozpor mezi default_v2 a uloženým promptem, očekává blokované přijetí a varování, poté klikne Reset a opakuje accept. Co je změna: Pokrývá odmítnutí neplatného původu i explicitní obnovu do validního defaultu; ověřuje, že checkbox zůstává vypnutý. Proč je to tak: Neplatná provenance se nemá tiše normalizovat jako běžná editace; reset je v testovaném toku výslovný opravný krok.
Jak to funguje: Opakované Apply následuje po Resetu, pak po editaci se zachovaným whitespace; závěrečný reset a Cancel kontrolují poslední uloženou hodnotu a původ. Co je změna: Test odlišuje provenance default_v2 od user_saved přes opakované uložení a Cancel. Proč je to tak: Původ se nemá měnit jen proto, že Apply proběhlo vícekrát, a Cancel nemá vrátit již potvrzené Apply.
Jak to funguje: Reset změní editor, následné reject dialogu je porovnáno s uloženou konfigurací i původním live objektem. Co je změna: Přidává explicitní Cancel scénář po resetu promptu. Proč je to tak: Změna pracovního editoru se před Apply nemá propsat do uložených nebo caller-owned hodnot.
Jak to funguje: Test provede Apply dvakrát a poté OK; po načtení z persistence porovná přesný prompt včetně CRLF/whitespace, původ a recording mode. Co je změna: Přidává regresní kontrolu nezměněného legacy promptu přes obě potvrzovací cesty. Proč je to tak: Rutinní uložení nemá normalizovat původní řádky nebo přepsat legacy_saved na jiný původ.
self.assertTrue(waiting_text.startswith("Waiting for samples - "))
Jak to funguje: Dlouhý název se zobrazí při stavu bez callback dat a poté s callbackem; test kontroluje zachování status prefixu, odlišnost obou textů a render obou stavů. Co je změna: Přidává kontrolu, že zkrácení názvu zařízení nepřekryje informaci o čekání ani o pozorované úrovni. Proč je to tak: Samotná nula RMS nerozliší ticho od chybějících vzorků; status prefix je zde pozorovatelný kontrakt.
self.assertEqual(bar.input_status_text(), "Waiting for samples - USB microphone")
Jak to funguje: Test porovná waiting s callbackem a quiet zero, ověří ořez RMS do 0..1 a paint; při processingu ignoruje pozdní sample a při novém recording resetuje status, po hide jej odstraní. Co je změna: Pokrývá celý cyklus meteru včetně stavů a resetu. Proč je to tak: Nulová úroveň může být měřené ticho, zatímco chybějící callback znamená čekání; mimo recording pozdní vzorek nemá obnovit starý meter.
Jak to funguje: Test simuluje fatální capture výjimku během pollingu a sleduje chybu, timer, stav recording i to, že nezačne processing. Co je změna: Přidává propojený test cesty selhání mezi capture a snackbar poll timerem. Proč je to tak: Po fatální chybě nesmí zůstat aktivní pravidelné čtení snapshotu ani být záznam předán ke zpracování jako úspěšné audio.
Jak to funguje: Vytvořený bare app používá mock recorder snapshot, spustí poll a ověří předání jeho hodnot snackbar; po deaktivaci ověřuje, že další poll nepokračuje. Co je změna: Přidává přímé pokrytí timeru nad skutečnou snapshot hodnotou, nejen uložené UI hodnoty. Proč je to tak: Meter má odrážet callback data recorderu a nečinný záznam nemá provádět další poll.
Jak to funguje: Helper sestaví minimální _TrayApp stav pro wiring testy bez plného startupu a připraví Qt application. Co je změna: Bare app fixture má rozšířený inicializovaný stav vůči base verzi. Proč je to tak: Testy timeru a poll callbacku potřebují stabilní QObject/Qt kontext, aniž by spouštěly audio zařízení nebo celou tray inicializaci.
Jak to funguje: Mock HTTP POST zachytí JSON messages pro default i uložený prompt; test porovná system content a doslovný transcript jako user content. Co je změna: Přidává kontrolu tvaru požadavku na hranici klienta. Proč je to tak: Text transkriptu nemá být zaměněn za systémový prompt ani sloučen s instrukcemi před odesláním.
Jak to funguje: Zavolá rewrite s vypnutou volbou a kontroluje zachování raw textu i nulové volání HTTP boundary. Co je změna: Explicitní negative assertion nad síťovým requestem. Proč je to tak: Vypnutý rewrite má být skutečně přeskočen, ne jen skrýt výsledek již odeslaného požadavku.
Jak to funguje: Test pokrývá prázdný rewrite i výjimku z POST a porovnává přesný návratový raw text spolu s počtem requestů. Co je změna: Přidává regresní pokrytí fallbacku v obou výsledkových případech. Proč je to tak: Neplatný výstup nebo chyba rewrite nemají zahodit již získaný transcript ani vyvolávat opakované requesty v rámci tohoto volání.
Jak to funguje: Test připraví primary/fallback endpointy, simuluje primary selhání a kontroluje requesty pro stejný vybraný prompt i jazykovou nápovědu. Co je změna: Přidává porovnání fallback requestu s primárním nastavením. Proč je to tak: Přechod backendu má měnit endpoint, ne instrukce určené pro daný rewrite.
Jak to funguje: Spustí recording, ověří aktivní level i single-shot limit timer a vynutí jeho timeout; dále ověřuje zastavení capture, vypnutí timerů a jediný worker nad uloženou session config. Co je změna: Rozšířený test potvrzuje i meter timer a jeho deaktivaci při limitu. Proč je to tak: Časový limit musí uzavřít tentýž záznam jednou a ukončit polling, aniž by pozdější release audio zpracoval podruhé.
Jak to funguje: Helper vybírá menu radio, ověřuje checked stav, persistence Auto/Czech/English a session language před/po změně během aktivního záznamu. Co je změna: Test zahrnuje explicitní pozorování checked radio vedle uložené a session hodnoty. Proč je to tak: Změna tray nastavení má přetrvat pro další session, ale právě běžící session používá snapshot pořízený při startu.
Jak to funguje: Po startu recorderu simuluje stop() chybu MIC_DISCONNECTED a kontroluje idle recording/timer/session, žádné vytvoření workeru a předání konkrétní chyby. Co je změna: Přidává kontrolu chyby při získávání audio dat po aktivním capture. Proč je to tak: Audio bez vzorků není transcriptový vstup; meter se zastaví a STT se nemá spouštět.
Jak to funguje: Startup callback vrací config beze změn v rámci test setupu, který jinak zakládá tray aplikaci. Co je změna: Tato lambda je samostatná ledger položka v těle nového error-path testu. Proč je to tak: Izoluje chybu capture od environment importu a nevyžaduje změnu configu během inicializace.
Jak to funguje: Pro release, disable a exit spustí nahrávání a limit timer, provede příslušnou akci a zpracuje Qt eventy; kontroluje oba timery i nulové worker spuštění. Co je změna: Rozšířená kontrola ověřuje také zastavení level timeru po každém ukončovacím toku. Proč je to tak: Žádný z těchto toků nemá ponechat aktivní limit ani pokračovat v měření po zastavení/odhození audio.
Jak to funguje:resolve_device vyhodí chybu před startem capture; test ověřuje recording false, žádnou session config, neaktivní level timer a přechod tray state do IDLE. Co je změna: Přidává assertion nad timerem k původnímu idle rollback testu. Proč je to tak: Neúspěšný start nesmí vytvořit aktivní pollovací činnost ani zdánlivě aktivní session.
Jak to funguje: Nastaví vlastní Settings dialog jako aktivní UI, doručí terminal s výsledným textem a kontroluje, že type_text neběží, přičemž raw výsledek se zachová. Co je změna: Zpevňuje test zachování výsledku vedle negace automatického psaní. Proč je to tak: Psaní do vlastního nastavení by mohlo vložit transcript do nevhodného pole; odložení automatického výstupu nemá zahodit dostupný raw text.
Jak to funguje: Nejdříve vyřadí aplikaci z enabled stavu a poté zpracuje queued terminal; assertion kontroluje absenci type_text a uchování latest raw. Co je změna: Upravené tělo explicitně zachovává výsledek i při zakázaném automatickém výstupu. Proč je to tak: Změna enabled stavu před obsluhou fronty musí zablokovat psaní, ale nevymazat výsledek workeru.
Jak to funguje: Po startu mění live language/rewrite/post-key, finalizuje audio a ověřuje session snapshot; při terminal delivery ověřuje konkrétní target předaný type_text. Co je změna: Výstupní assertion nově vyžaduje expected_target odpovídající zachycenému cílovému oknu. Proč je to tak: Změny během nahrávání nemají změnit session nastavení a typing má být svázán s oknem zachyceným pro tuto session.
Jak to funguje: Při otevřeném nastavení zaznamená enabled stav menu před a po simulovaném Apply, které menu znovu sestaví, a poté zaznamená stav po zavření. Co je změna: Test kontroluje zachování uzamčení tray config ovládání napříč vnitřním rebuildem a změněným menu widgetem. Proč je to tak: Apply může synchronizovat config za běhu dialogu, ale souběžné tray volby nemají během této interakce měnit jeho stav; Enabled/Exit zůstávají odděleně dostupné.
Jak to funguje: Vnořený callback projde aktuálně vytvořené menu, získá widget akce a uloží enabled stavy pro record mode, hotkey, language, post-key, AI Rewrite, Enabled a Exit. Co je změna: Inspect helper nyní pracuje i s vloženými widgety a identifikuje AI Rewrite podle prefixu labelu. Proč je to tak: Test musí pozorovat skutečně vykreslené checkbox/widget ovládání a jeho stav po rebuild, ne pouze QMenuAction text.
Jak to funguje: Mock recorder vyhodí MIC_UNAVAILABLE při startu; test kontroluje neaktivní level i recording limit timer, false recording, žádnou session a přesně nahlášenou chybu. Co je změna: Přidává zvláštní test chyby při otevření streamu. Proč je to tak: Timer a session vznikají až po úspěšném startu streamu; jejich ponechání po výjimce by znamenalo pollování neexistujícího capture.
Jak to funguje: No-op config callback je součástí startup izolace před vyvoláním chyby startu streamu. Co je změna: Lambda má vlastní položku v ledgeru vedle funkce testu. Proč je to tak: Zachovává setup beze změny configu, takže pozorovaným selháním zůstává pouze stream start.
Jak to funguje: Vytvoří session s konkrétní WindowIdentity, nastaví tentýž foreground target a po terminal výsledku ověří volání type_text s post-key i expected target. Co je změna: Upravený assertion zahrnuje očekávanou identitu cílového okna. Proč je to tak: Test ověřuje, že úspěšný výsledek stále prochází main-thread delivery boundary a předává jí guard na okno dané session.
Jak to funguje: Mock server odmítá primary, přijímá fallback STT i rewrite; test změní live config po vytvoření session a ověří jazyk i prompt v zachycených requestech. Co je změna: Změněný test používá session snapshot pro požadavky a současně ověřuje další worker s novými live hodnotami. Proč je to tak: Fallback i rewrite jednoho audio záznamu mají sdílet jeho zmrazenou language konfiguraci; změna nastavení se uplatní až v nové session.
Jak to funguje: Konstruktor fixture zajišťuje QApplication a sestaví _TrayApp přes __new__ plus QObject init, vyplňující ručně testovaná pole. Co je změna: Fixture má rozšířená pole včetně session, recovery a recording/level timerů oproti base. Proč je to tak: Jednotkové testy mohou volat dílčí tray handlery bez plného startupu, ale musí dodat nový stav, který tyto handlery čtou.
Detail 7: Regrese Windows font metrics a geometrie
Navazuje na původní zdrojový snapshot d533a74886529f5e0b0822d89b63d65246ed763d. Produkční _resize_to_content je aktualizován přímo v Detailu 1; tato příloha přidává jedinou novou testovací funkci. Aktuální head PR je 77f175bd27283c3fe8ba87e6212e7bde54627b35.
font=bar.font()
font.setPointSize(18)
bar.setFont(font)
screen=self.enterContext(patch("src.snackbar.QGuiApplication.primaryScreen"))
screen.return_value.availableGeometry.return_value=QRect(0, 0, 3840, 2160)
bar.show_state("Recording", (229, 57, 53))
bar.set_input_status("USB microphone", 0.0, False)
self.assertEqual(bar.input_status_text(), "Waiting for samples - USB microphone")
bar.set_input_status("System Default (Actual USB mic)", 0.0, True)
self.assertEqual(bar.input_status_text(), "Input level - System Default (Actual USB mic)")
Jak to funguje: Reálný widget, QFontMetrics a elision používají 18bodový font. Pouze externí geometrie monitoru je řízená na dostatečně velký viewport, aby se nemíchala regrese pevného capu s legitimním ořezáním na malém offscreen monitoru. Test vyžaduje přesný waiting i input-level text; dále dlouhé jméno eliduje, hlídá rozměr a skutečně vykreslí widget přes grab().
Co je změna: Nový deterministický test. Počáteční Windows CI mělo dvě skutečné chyby s normálním systémovým fontem; font-aware sizing je opravil. Dodatečný test velkého fontu musí mít nezávisle řízený dostatečně velký prostor, proto navazující test commit kontroluje viewport. Původní integrační assertions nejsou oslabené.
Proč je to tak: Regrese se starou implementací skutečně selhala na Linuxu i s dostupným prostorem, opravená implementace prošla. Větší font nesmí sám způsobit předčasnou elision krátkého názvu, zatímco dlouhá jména zůstávají omezená. Tato automatická kontrola nenahrazuje Windows desktop/hardware acceptance.
Dokumentace současně zaznamenává font-aware, screen-bounded sizing a aktuální Linux výsledek 288 testů / 5 Windows-only skipů. Nativní Windows CI je nadále samostatný merge gate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MR Overview
Overview
AudioRecorderposkytuje koherentní snapshot hlasitosti/stavu aRecordingSnackbarzobrazuje skutečné zařízení, čekání na vzorky a úroveňtype_text,_TrayApp._deliver,HotkeyListeneraRecoveryDialogověřují cíl, evidují jednotlivé pokusy a umožňují explicitní copy/arm/retrySendInputneprokazují viditelný text a nic se automaticky neposílá znovuDictationRecord,HistoryStorea session worker oddělují raw text, rewrite, pokusy a trvalé záznamyraw/clean/custom a mapování úplné cesty executableKontext a pozadí
Rozsah odpovídá
9f78dd01bbd2e2304c153ca89a77d9998438c8dc...77f175bd27283c3fe8ba87e6212e7bde54627b35:3b0ebd6(mikrofon),eace21f(provenance promptů),d533a74(zbývající roadmapa),75a674f(font-aware meter) a77f175b(deterministický viewport jeho regrese). V diffu je 40 souborů včetně obou dřívějších neuvolněných změn; žádná není vynechána. Původní pinned výňatky zd533a74zachycují mechanismy, které zůstaly stejné; následná oprava metru je aktualizovaná v Detailu 1 a nová regrese v Detailu 7.Historie zůstává defaultně vypnutá a uchovává posledních 1–100 textových záznamů přes DPAPI; jedno audio po chybě STT je pouze v RAM a lze jej explicitně zkusit znovu bez automatického výstupu. Výstup do schránky je samostatná operace. U typování se před odesláním kontroluje foreground HWND+PID; selhání/skok post-key nebo částečný send se eviduje jako pokus a nikdy se automaticky neopakuje. Úspěšná kopie na nativním Windows vyžaduje potvrzené Qt clipboard ownership. Původní schránka se nečte ani neobnovuje.
Recovery hotkey je ve výchozím stavu
Ctrl+Alt+Shift+V; vložení se armuje a dialog skryje, uživatel přepne do cílové aplikace, potom zkratku stiskne a plně uvolní včetně modifierů.Escapearm zruší a nemůže být recovery triggerem. Injektované klávesy procházejí hookem bez změny fyzického shortcut state. WindowsRegisterHotKeynemusí odhalit konflikt s aplikací používající pouze low-level hook.Jazyk
Autoa.envfallback zachovávají rozdíl mezi chybějícím klíčem a explicitně uloženou prázdnou hodnotou. Vocabulary je omezena na 128 termínů, 128 znaků/termín a 8 000 znaků renderovaného kontextu; poškozená hodnota se uchová do explicitní opravy. Prompt pro STT má nezávislý opt-in pro každý provider. Profily limitují custom katalog na 32; migrace respektujedefault_v2versuslegacy_saved/user_saved, včetně uloženého prázdného promptu a promptu totožného s defaultem. Globální AI off vybereraw, jinak platíManualneboAutomaticpodle přesné normalizované cesty executable a nakonfigurovaného defaultu. Nastavení profilu i cíle se zmrazí na začátku session.Roadmapová položka 4 je odstraněná a není obsahem tohoto MR. README ani přijaté detailní roadmapové specifikace nejsou nově přepsány tímto popisem. Nejde o tvrzení publikovaného release. Příčina problému s Notepadem zůstává nepotvrzena a není tvrzeno, že byla diagnostikována či opravena.
Stavové hodnoty a důležité proměnné
CaptureSnapshot(level_rms,has_callback_data,capture_error,input_status,device)audio.pydevicenese skutečné ID/jméno, RMS není důkaz srozumitelné řeči_Session(id, config snapshot, target,profile_id)main.pyDictationRecord(raw_text,rewritten_text,rewrite_status, warnings,last_delivery)results.py_terminal_handled,_thread_finishedmain.py_pending_audiomain.pyhistory_enabled,history_limitAppConfigllm_prompt_originconfig.pydefault_v2,legacy_saved,user_saved; chrání migraci promptu před chybným přeznačenímrewrite_catalog,rewrite_selection_mode,rewrite_manual_profile_id,rewrite_default_profile_idAppConfigvocabulary_entries,stt_prompt_{primary,fallback}_enabledAppConfigpromptpro STT endpointyWindowIdentity(hwnd, process_id, executable_path)injector.pyDeliveryAttemptresults.pyAPP_DIR/screamer.lockmain.pyDetail změn
Detailní index obsahuje 376 změněných funkcí včetně vnořených callbacků/lambda a test/fixture metod. Kvůli limitu délky GitHub těla jsou samostatné výňatky, pinned source odkazy a vysvětlení každé položky publikovány v sedmi navazujících komentářích tohoto PR:
Strukturální a persistence změny mimo funkce
AppConfig, výstup/recovery/history/vocabulary:Malé preference zůstávají v atomickém INI. Privátní sidecars nejsou INI klíče: uchovávají poškozené kolekce a přítomnost explicitně uloženého Auto. Primární a fallback STT prompt mají oddělené defaultně vypnuté flagy. Textová historie používá samostatný
history.enc, nikdy audio/credentials/system prompt.Profily a provenance:RewriteProfileje frozen ID/name/kind/prompt;AppMappingje executable-path/profile-ID. Catalog JSON v1 obsahuje pouze custom profily a mappings; built-ins jsou code-owned. Řízená migrace zachová každý uložený legacy/user prompt a výchozí prompt zůstává konzervativní, bez automatického povolení LLM. Je to instrukce modelu, nikoli záruka fidelity.WindowIdentity:Volitelné executable metadata nejsou součást rovnosti output targetu.
InjectionReportodděluje text/post-key počty a skipped post-key;CaptureSnapshotodděluje callback RMS/data/status/error a skutečnou otevřenou audio identitu.DictationRecord:Frozen records oddělují modelový výsledek od
DeliveryAttempt(čas/mode/copy/text count/state/post-key/reason). NovéAppErrorhodnoty rozlišují withheld output, clipboard, history a config chyby; bridge přidává recovery request/cancel signály. Dokumentace aktualizuje current API a akceptační stav a přidává source/runtime-pinned lokální STT návod. Odstranění roadmapové položky 4 zachovává sjednaný scope, nikoli tvrzení nové diff/inspection feature.Ověření
Provedené ověření
Výsledek:
77f175bd27283c3fe8ba87e6212e7bde54627b35; zaseknutá apt instalace byla zrušena a pouze nedokončený job na stejném SHA znovu prošel. Žádný neúspěšný check nebyl obejit. Skutečný desktop/hardware test ani potvrzení příčiny Notepad potíží provedeny nebyly.Coverage
Ověřeno zejména:
QThread.finishedpořadí, jeden busy worker, history fail-closed a DPAPI boundaries.SendInput, separátní clipboard/post-key a ruční recovery chord.Nasazení
Bez nových third-party dependencies. Není tvrzena automatická datová migrace mimo zdrojově popsané
AppConfig/QSettings změny. Historie je nová opt-in DPAPI persistence pro aktuální Windows účet. Lokální STT je volitelná konfigurace endpointu, nikoli lokální-only síťová politika. Stávající release workflow nebyl měněn; nejvyššífeat:změna od publikovanév1.2.0určovala minor verziv1.3.0.Dokončené nasazení: PR merged do
mainjako554cedbf796af3cfee4620e0fd5e82c6af4a8624. Main CI36926003567, CodeQL36926003470a Release36926233488uspěly. v1.3.0 je publikovaná. Stažený 89 722 553 B ZIP má SHA2569b240ee40a345671430529bfdb6ae2caa30610050a641483f0d4e51fdafb225e, shodný s.sha256i GitHub asset digestem; ověřeno ZIP CRC, Windows PE x64, Qt6Core/Widgets/qwindows a absence zabalených uživatelských dat. Runtime desktop/hardware gate zůstává otevřená.Kompatibilita / dependencies
Zdrojová větev
feat/dictation-roadmap→ cílovámain; base9f78dd01bbd2e2304c153ca89a77d9998438c8dc, source77f175bd27283c3fe8ba87e6212e7bde54627b35. Všech pět commitů je zahrnuto. Merge proběhne pouze po zelených CI/CodeQL; user výslovně autorizoval admin bypass povinného PR approval, nikoli bypass neúspěšných kontrol.Data-flow diagram
Jak číst tok:
Reviewer focus
Při review má smysl zaměřit se hlavně na hranice stavů: správné spárování session ID při
raw_ready/terminal/finished; podmínky, za kterých může být text automaticky či ručně odeslán; přesné rozlišení textových eventů od post-key; fail-closed persistence a zachování poškozených katalogů/prompt provenance. Skutečné Windows foreground, keyboard-hook konflikty a audio hardware zůstávají manuálním ověřením.Out of scope
Tento MR záměrně neřeší: