stenograf 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. {stenograf-0.2.0 → stenograf-0.2.2}/.claude/skills/verify/SKILL.md +28 -0
  2. {stenograf-0.2.0 → stenograf-0.2.2}/.github/workflows/release.yml +57 -5
  3. {stenograf-0.2.0 → stenograf-0.2.2}/CLAUDE.md +7 -2
  4. {stenograf-0.2.0 → stenograf-0.2.2}/PKG-INFO +23 -6
  5. {stenograf-0.2.0 → stenograf-0.2.2}/PLAN.md +205 -9
  6. {stenograf-0.2.0 → stenograf-0.2.2}/README.md +21 -5
  7. stenograf-0.2.2/install.ps1 +28 -0
  8. {stenograf-0.2.0 → stenograf-0.2.2}/native/README.md +3 -1
  9. stenograf-0.2.2/native/stenodiar/build.ps1 +14 -0
  10. {stenograf-0.2.0 → stenograf-0.2.2}/pyproject.toml +2 -1
  11. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/streaming.py +8 -3
  12. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/windows.py +6 -2
  13. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/doctor_cmd.py +39 -14
  14. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/run.py +10 -6
  15. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/settings_cmd.py +1 -0
  16. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/start.py +1 -1
  17. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/transcribe.py +4 -1
  18. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/diarization/speakrs.py +11 -5
  19. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/loaders.py +58 -22
  20. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/settings.py +24 -6
  21. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/shortcut.py +46 -2
  22. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/flow.py +5 -1
  23. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/home.py +4 -1
  24. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/meeting.py +3 -1
  25. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/setup.py +16 -13
  26. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/transcribe.py +2 -0
  27. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_capture_streaming.py +37 -5
  28. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_capture_windows.py +11 -0
  29. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_cli.py +98 -12
  30. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_cli_notes.py +40 -0
  31. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_diarization_speakrs.py +18 -1
  32. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_settings.py +11 -0
  33. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_shortcut.py +54 -7
  34. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_ui.py +70 -23
  35. {stenograf-0.2.0 → stenograf-0.2.2}/uv.lock +1 -1
  36. {stenograf-0.2.0 → stenograf-0.2.2}/.github/workflows/ci.yml +0 -0
  37. {stenograf-0.2.0 → stenograf-0.2.2}/.gitignore +0 -0
  38. {stenograf-0.2.0 → stenograf-0.2.2}/LICENSE +0 -0
  39. {stenograf-0.2.0 → stenograf-0.2.2}/PLAN-AEC.md +0 -0
  40. {stenograf-0.2.0 → stenograf-0.2.2}/PLAN-CLEANUP.md +0 -0
  41. {stenograf-0.2.0 → stenograf-0.2.2}/eval/README.md +0 -0
  42. {stenograf-0.2.0 → stenograf-0.2.2}/eval/adjudicate.py +0 -0
  43. {stenograf-0.2.0 → stenograf-0.2.2}/eval/aec_rig.py +0 -0
  44. {stenograf-0.2.0 → stenograf-0.2.2}/eval/aec_score.py +0 -0
  45. {stenograf-0.2.0 → stenograf-0.2.2}/eval/backends.py +0 -0
  46. {stenograf-0.2.0 → stenograf-0.2.2}/eval/common.py +0 -0
  47. {stenograf-0.2.0 → stenograf-0.2.2}/eval/der.py +0 -0
  48. {stenograf-0.2.0 → stenograf-0.2.2}/eval/diarize.py +0 -0
  49. {stenograf-0.2.0 → stenograf-0.2.2}/eval/extract.py +0 -0
  50. {stenograf-0.2.0 → stenograf-0.2.2}/eval/live.py +0 -0
  51. {stenograf-0.2.0 → stenograf-0.2.2}/eval/manifest.example.json +0 -0
  52. {stenograf-0.2.0 → stenograf-0.2.2}/eval/parity.py +0 -0
  53. {stenograf-0.2.0 → stenograf-0.2.2}/eval/rttm.py +0 -0
  54. {stenograf-0.2.0 → stenograf-0.2.2}/eval/scan_languages.py +0 -0
  55. {stenograf-0.2.0 → stenograf-0.2.2}/eval/score.py +0 -0
  56. {stenograf-0.2.0 → stenograf-0.2.2}/eval/transcribe.py +0 -0
  57. {stenograf-0.2.0 → stenograf-0.2.2}/hatch_build.py +0 -0
  58. {stenograf-0.2.0 → stenograf-0.2.2}/install.sh +0 -0
  59. {stenograf-0.2.0 → stenograf-0.2.2}/native/helper/Info.plist +0 -0
  60. {stenograf-0.2.0 → stenograf-0.2.2}/native/helper/build.sh +0 -0
  61. {stenograf-0.2.0 → stenograf-0.2.2}/native/helper/main.swift +0 -0
  62. {stenograf-0.2.0 → stenograf-0.2.2}/native/spike/Info.plist +0 -0
  63. {stenograf-0.2.0 → stenograf-0.2.2}/native/spike/build.sh +0 -0
  64. {stenograf-0.2.0 → stenograf-0.2.2}/native/spike/main.swift +0 -0
  65. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/.gitignore +0 -0
  66. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/Cargo.lock +0 -0
  67. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/Cargo.toml +0 -0
  68. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/build.sh +0 -0
  69. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/src/main.rs +0 -0
  70. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/VENDOR.md +0 -0
  71. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/Cargo.toml +0 -0
  72. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/LICENSE +0 -0
  73. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/README.md +0 -0
  74. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/binarize.rs +0 -0
  75. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/clustering/ahc.rs +0 -0
  76. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/clustering/plda.rs +0 -0
  77. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/clustering/vbx.rs +0 -0
  78. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/clustering.rs +0 -0
  79. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/coreml/array.rs +0 -0
  80. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/coreml/path.rs +0 -0
  81. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/coreml/runtime.rs +0 -0
  82. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/coreml.rs +0 -0
  83. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/batch.rs +0 -0
  84. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/chunk.rs +0 -0
  85. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/fbank.rs +0 -0
  86. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/load/sessions.rs +0 -0
  87. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/load.rs +0 -0
  88. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/native/loaders.rs +0 -0
  89. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/native.rs +0 -0
  90. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/paths.rs +0 -0
  91. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/run.rs +0 -0
  92. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/session.rs +0 -0
  93. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/tail.rs +0 -0
  94. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding/tensor.rs +0 -0
  95. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/embedding.rs +0 -0
  96. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation/native.rs +0 -0
  97. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation/parallel/batch.rs +0 -0
  98. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation/parallel/single.rs +0 -0
  99. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation/parallel.rs +0 -0
  100. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation/run.rs +0 -0
  101. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation/tensor.rs +0 -0
  102. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference/segmentation.rs +0 -0
  103. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/inference.rs +0 -0
  104. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/lib.rs +0 -0
  105. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/linalg.rs +0 -0
  106. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/metrics.rs +0 -0
  107. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/models.rs +0 -0
  108. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/builder.rs +0 -0
  109. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/chunk_embedding/collect.rs +0 -0
  110. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/chunk_embedding/error.rs +0 -0
  111. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/chunk_embedding/gpu.rs +0 -0
  112. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/chunk_embedding/orchestrate.rs +0 -0
  113. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/chunk_embedding/prep.rs +0 -0
  114. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/chunk_embedding.rs +0 -0
  115. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/clustering.rs +0 -0
  116. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/concurrent.rs +0 -0
  117. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/config.rs +0 -0
  118. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/post_inference.rs +0 -0
  119. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/queued.rs +0 -0
  120. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/tests.rs +0 -0
  121. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/types/data.rs +0 -0
  122. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/types/error.rs +0 -0
  123. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/types/extract.rs +0 -0
  124. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/types/layout.rs +0 -0
  125. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline/types.rs +0 -0
  126. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/pipeline.rs +0 -0
  127. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/powerset.rs +0 -0
  128. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/reconstruct.rs +0 -0
  129. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/segment.rs +0 -0
  130. {stenograf-0.2.0 → stenograf-0.2.2}/native/stenodiar/vendor/speakrs/src/utils.rs +0 -0
  131. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/__init__.py +0 -0
  132. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/__main__.py +0 -0
  133. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/aec.py +0 -0
  134. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/__init__.py +0 -0
  135. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/base.py +0 -0
  136. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/parakeet.py +0 -0
  137. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/parakeet_onnx.py +0 -0
  138. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/providers.py +0 -0
  139. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/registry.py +0 -0
  140. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/asr/tokens.py +0 -0
  141. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/audio.py +0 -0
  142. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/__init__.py +0 -0
  143. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/base.py +0 -0
  144. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/file.py +0 -0
  145. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/linux.py +0 -0
  146. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/capture/macos.py +0 -0
  147. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/__init__.py +0 -0
  148. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/format.py +0 -0
  149. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/notes.py +0 -0
  150. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/cli/profiles.py +0 -0
  151. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/config.py +0 -0
  152. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/diarization/__init__.py +0 -0
  153. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/diarization/base.py +0 -0
  154. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/diarization/sherpa.py +0 -0
  155. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/doctor.py +0 -0
  156. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/glossary.py +0 -0
  157. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/lid.py +0 -0
  158. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/live.py +0 -0
  159. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/models.py +0 -0
  160. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/__init__.py +0 -0
  161. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/backend.py +0 -0
  162. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/command.py +0 -0
  163. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/export.py +0 -0
  164. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/generate.py +0 -0
  165. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/mlx.py +0 -0
  166. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/model.py +0 -0
  167. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/ollama.py +0 -0
  168. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/notes/prompt.py +0 -0
  169. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/output.py +0 -0
  170. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/pipeline.py +0 -0
  171. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/profiles.py +0 -0
  172. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/recording.py +0 -0
  173. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/session.py +0 -0
  174. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/transcript.py +0 -0
  175. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/__init__.py +0 -0
  176. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/_fps.py +0 -0
  177. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/app.py +0 -0
  178. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/doctor.py +0 -0
  179. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/notes.py +0 -0
  180. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/settings.py +0 -0
  181. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/ui/widgets.py +0 -0
  182. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/vad.py +0 -0
  183. {stenograf-0.2.0 → stenograf-0.2.2}/src/stenograf/view.py +0 -0
  184. {stenograf-0.2.0 → stenograf-0.2.2}/tests/conftest.py +0 -0
  185. {stenograf-0.2.0 → stenograf-0.2.2}/tests/fake_parec.py +0 -0
  186. {stenograf-0.2.0 → stenograf-0.2.2}/tests/fake_stenocap.py +0 -0
  187. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_aec.py +0 -0
  188. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_asr_parakeet.py +0 -0
  189. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_asr_parakeet_onnx.py +0 -0
  190. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_asr_providers.py +0 -0
  191. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_asr_registry.py +0 -0
  192. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_asr_tokens.py +0 -0
  193. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_audio.py +0 -0
  194. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_build_hook.py +0 -0
  195. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_capture_file.py +0 -0
  196. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_capture_linux.py +0 -0
  197. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_capture_macos.py +0 -0
  198. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_config.py +0 -0
  199. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_diarization_sherpa.py +0 -0
  200. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_diarization_sherpa_unit.py +0 -0
  201. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_doctor.py +0 -0
  202. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_echo_dedup.py +0 -0
  203. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_eval_der.py +0 -0
  204. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_glossary.py +0 -0
  205. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_lid.py +0 -0
  206. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_live.py +0 -0
  207. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_live_orchestration.py +0 -0
  208. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_meeting.py +0 -0
  209. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_models.py +0 -0
  210. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_notes_backends.py +0 -0
  211. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_notes_export.py +0 -0
  212. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_notes_generate.py +0 -0
  213. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_notes_model.py +0 -0
  214. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_output.py +0 -0
  215. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_pipeline.py +0 -0
  216. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_profiles.py +0 -0
  217. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_recording.py +0 -0
  218. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_session.py +0 -0
  219. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_transcript.py +0 -0
  220. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_vad.py +0 -0
  221. {stenograf-0.2.0 → stenograf-0.2.2}/tests/test_view.py +0 -0
@@ -8,6 +8,34 @@ description: How to run and observe stenograf end-to-end (TUI, live pass, finali
8
8
  The product surface is the `steno` CLI / Textual TUI. Everything is drivable
9
9
  headlessly via `--replay` — no mic, no system tap, no native helper needed.
10
10
 
11
+ ## Headless TUI smoke — real stack, no TTY, any platform
12
+
13
+ CLI-mode green does NOT imply the launcher works: the CLI does its loader
14
+ work *before* Textual owns stdio, the launcher does it *under* a live app —
15
+ a code path no `--plain`/`--replay` run touches (this is how the win32
16
+ EBADF meeting-start crash shipped despite a fully validated CLI,
17
+ 2026-07-12). Textual's `run_test` pilot drives the real launcher without a
18
+ terminal, so "the TUI needs eyes" is true only for rendering — plumbing is
19
+ verifiable headlessly:
20
+
21
+ ```python
22
+ app = StenografApp()
23
+ async with app.run_test(size=(100, 40)) as pilot:
24
+ await pilot.click("#start") # Home -> setup form
25
+ await pilot.pause()
26
+ app.screen._submit() # form defaults -> meeting screen
27
+ # ... pilot.pause() until MeetingScreen._phase is CAPTURING (model load
28
+ # takes seconds), press "q" to stop+finalize, then inspect
29
+ # app._notifications — a "Meeting failed" toast is the failure signal.
30
+ ```
31
+
32
+ Patch `stenograf.output.default_output_home` to a scratch dir first. On a
33
+ machine with audio devices this runs the REAL provider + models end to end.
34
+ When a test must fake (CI has no audio), fake at the hardware boundary
35
+ (`capture.windows.default_devices`, the provider class), never at
36
+ `loaders.make_provider`/`load_backends` — faking the orchestration seam is
37
+ exactly what hid the EBADF crash from the UI suite.
38
+
11
39
  ## Build & launch
12
40
 
13
41
  No build step; run from the repo with uv:
@@ -1,9 +1,9 @@
1
1
  name: release
2
2
 
3
- # Tag v<version> → build the wheel matrix, smoke-test clean installs on both
4
- # OSes, publish to PyPI (Trusted Publishing/OIDC — no token secrets) and attach
5
- # the artifacts to a GitHub release. workflow_dispatch runs everything except
6
- # the publish step, as a dry run.
3
+ # Tag v<version> → build the wheel matrix, smoke-test a clean install on every
4
+ # supported OS, publish to PyPI (Trusted Publishing/OIDC — no token secrets) and
5
+ # attach the artifacts to a GitHub release. workflow_dispatch runs everything
6
+ # except the publish step, as a dry run.
7
7
 
8
8
  on:
9
9
  push:
@@ -110,8 +110,60 @@ jobs:
110
110
  # installs, the CLI runs, and the headless model download works.
111
111
  - run: steno setup --models-only
112
112
 
113
+ smoke-windows:
114
+ # Windows ships the same pure wheel Linux does (no native helpers), so there
115
+ # is nothing extra to *build* — but nothing verified a clean Windows install
116
+ # before publish either, which is how 0.2.1 shipped a launcher that died on
117
+ # every meeting start. The launcher/TUI itself is covered by the headless
118
+ # real-stack smoke in ci.yml's windows unit job; this leg is the *installed
119
+ # artifact* check: the wheel installs, the CLI renders through a redirected
120
+ # stdout, and the pipeline runs end to end from a file.
121
+ needs: build-linux
122
+ runs-on: windows-latest
123
+ timeout-minutes: 45
124
+ defaults:
125
+ run:
126
+ shell: bash # wheel globs + $HOME; the runner ships Git bash
127
+ steps:
128
+ - uses: astral-sh/setup-uv@v8.3.2
129
+ - uses: actions/download-artifact@v8
130
+ with:
131
+ name: dist-linux
132
+ path: dist
133
+ # uv installs tool executables to ~/.local/bin on Windows too.
134
+ - run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
135
+ - run: uv tool install -p 3.13 ./dist/stenograf-*py3-none-any.whl
136
+ - run: steno --version
137
+ # ffmpeg is the bundled imageio-ffmpeg binary here — nothing to install.
138
+ - run: steno setup --models-only
139
+ - name: The report renders through a redirected stdout
140
+ # doctor *exits 1* on a runner by design: it has no audio endpoint, so
141
+ # the (non-optional) capture check fails. What this asserts is that the
142
+ # report renders at all — piped Windows stdout uses the legacy code page,
143
+ # and click.echo once crashed outright on the ✓/✗ glyphs (fixed by
144
+ # reconfiguring the streams with errors="replace"; this pins it down).
145
+ run: |
146
+ steno doctor > doctor.txt 2>&1 || true
147
+ cat doctor.txt
148
+ grep -q "ASR backend" doctor.txt
149
+ grep -q "ffmpeg" doctor.txt
150
+ - name: Pipeline smoke on a synthetic WAV
151
+ # Live WASAPI capture needs hardware no runner has; --replay drives the
152
+ # same pipeline from a file.
153
+ run: |
154
+ uv run --no-project --python 3.13 python - <<'EOF'
155
+ import math, struct, wave
156
+ with wave.open("smoke.wav", "wb") as w:
157
+ w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000)
158
+ w.writeframes(b"".join(
159
+ struct.pack("<h", int(8000 * math.sin(2 * math.pi * 440 * i / 16000)))
160
+ for i in range(16000 * 3)))
161
+ EOF
162
+ steno start --replay smoke.wav --local 1 --remote 0 --lang en --out smoke-out
163
+ test -f smoke-out/transcript.json
164
+
113
165
  publish:
114
- needs: [smoke-macos, smoke-linux]
166
+ needs: [smoke-macos, smoke-linux, smoke-windows]
115
167
  if: github.ref_type == 'tag'
116
168
  runs-on: ubuntu-latest
117
169
  timeout-minutes: 15
@@ -38,8 +38,13 @@ real PipeWire); GitHub Actions Ubuntu is the stable-distro CI reference.
38
38
  import-broken, and Voxtral needs 4.x); generation must stay bound to the
39
39
  thread that imported `mlx_lm` (guarded in code). **Linux CPU fallback =
40
40
  llama-cpp-python — its wheels are OFF-PyPI** (extra index URL required);
41
- Ollama stays the default when a GPU box is available. Windows (future) =
42
- onnxruntime-genai-directml + Phi-4-mini.
41
+ Ollama stays the default when a GPU box is available. **Windows (decided
42
+ 2026-07-12): Ollama-default, no in-process backend unless Ollama proves
43
+ insufficient — then llama-cpp-python CPU GGUF (Phi-4-mini), same off-PyPI
44
+ wheel friction as Linux.** onnxruntime-genai-directml was rejected
45
+ (DX12-GPU-only on the EOL DirectML EP; coexistence with our
46
+ onnxruntime-directml would actually have been fine — it depends on the
47
+ same flavor). Details: PLAN.md §5 Phase 6 Track D.
43
48
  - **MLX on background threads**: materialize weights on the load thread or
44
49
  inference dies with "no Stream(gpu, 0)"; verify MLX-threading changes
45
50
  against the real backend, not mocks.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stenograf
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Accuracy-first local meeting transcription for German and English. Audio stays in RAM; only text is written.
5
5
  Project-URL: Repository, https://github.com/daniel-om-weber/stenograf
6
6
  Project-URL: Issues, https://github.com/daniel-om-weber/stenograf/issues
@@ -11,6 +11,7 @@ Keywords: diarization,local,meetings,privacy,speech-to-text,transcription
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
14
15
  Classifier: Operating System :: POSIX :: Linux
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
@@ -103,6 +104,19 @@ installs the current main branch; building from the repository compiles the
103
104
  capture helper on your machine, which needs the Xcode command-line tools
104
105
  (`xcode-select --install`).
105
106
 
107
+ ### Windows
108
+
109
+ Same one command, run from PowerShell — installs uv if missing, then stenograf,
110
+ then the launcher:
111
+
112
+ ```powershell
113
+ powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/daniel-om-weber/stenograf/main/install.ps1 | iex"
114
+ ```
115
+
116
+ Windows never prompts for the microphone, so capture stays silent until you flip
117
+ the toggle under Settings > Privacy & security > Microphone. Windows Terminal is
118
+ recommended for the live-caption TUI.
119
+
106
120
  ### From a checkout
107
121
 
108
122
  ```sh
@@ -236,11 +250,14 @@ dir = "~/Documents/Obsidian/Meetings" # optional: also write one combined
236
250
  With `[notes.export]` set, every summarized meeting also produces a single
237
251
  self-contained markdown note (frontmatter, summary, action items, collapsible
238
252
  transcript) — drop the dir inside an Obsidian vault and meetings file
239
- themselves. Two more levers in `[notes]`: `instructions = "~/style.md"`
240
- appends your house style to the built-in prompt, and `thinking = false` skips
241
- the mlx model's reasoning pass (faster, less careful). Notes never run unless
242
- you ask (`--notes` or `steno notes`), and a notes failure never touches the
243
- transcript.
253
+ themselves. Three more levers in `[notes]`: `instructions = "~/style.md"`
254
+ appends your house style to the built-in prompt, `thinking = false` skips the
255
+ mlx model's reasoning pass (faster, less careful), and `auto = true` makes
256
+ notes the default for every meeting `steno start` summarizes without
257
+ `--notes`, and the launcher's "Generate notes after the meeting" switch starts
258
+ on. Out of the box notes never run unless you ask (`--notes`, the launcher
259
+ switch, or `steno notes` afterwards); with `auto = true`, `--no-notes` still
260
+ skips them for one run. A notes failure never touches the transcript.
244
261
 
245
262
  ## Naming speakers across meetings
246
263
 
@@ -938,15 +938,211 @@ SIGINT-based macOS-provider teardown now skipped on win32) and **one real produc
938
938
  bug**: on Windows, piped/redirected output uses the legacy code page and `click.echo`
939
939
  crashed on ✓/← — the CLI entry now reconfigures stdout/stderr with
940
940
  `errors="replace"` on win32. `livekit` win_amd64 wheels confirmed (1.1.13 installs;
941
- AEC behaviour on Windows hardware still to be exercised). Still open in Phase 6: real
942
- `steno transcribe`/`steno start` with downloaded models, the `windows-latest` CI job +
943
- Windows classifier, setup messaging, the DirectML notes backend, the stenodiar port,
944
- and the Windows leg of the Phase-7 install story (deferred there 2026-07-12): an
945
- `install.ps1` one-liner (uv ships `irm https://astral.sh/uv/install.ps1 | iex`) plus
946
- the win32 branch of `stenograf/shortcut.py` a Desktop `.lnk`, or simpler a `.cmd`
947
- wrapper, since `steno.exe` is a console app and any shortcut to it opens its own
948
- console window; `install_shortcut()` currently returns `None` on win32 so `steno
949
- setup` stays silent there.
941
+ AEC behaviour on Windows hardware still to be exercised).
942
+
943
+ **Closed since the capture track (recorded 2026-07-12):**
944
+
945
+ - **ASR GPU acceleration (2026-07-11, commit 293545f):** `[asr] provider` setting /
946
+ `STENOGRAF_ASR_PROVIDER` (`cpu` default | `dml` | `cuda` | `auto`) with an
947
+ availability pre-check + canary decode + CPU fallback (`asr/parakeet_onnx.py`,
948
+ `asr/providers.py`). pyproject ships `onnxruntime-directml` on win32 via marker —
949
+ every ORT flavor installs the same package directory and silently clobbers the
950
+ others (verified), so extras can't express the swap. Measured on an RTX 4080 SUPER:
951
+ DML transcripts byte-identical to CPU, 107× vs 16× realtime. Gotcha for future EPs:
952
+ ORT does **not** raise on an unlisted provider — it warns and runs on the rest, so
953
+ always pre-check `get_available_providers()`.
954
+ - The `windows-latest` CI job (unit suite; pyright stays macOS-only for the
955
+ marker-dep imports) and a `cargo check` leg for stenodiar's `cuda` feature.
956
+ - The `Operating System :: Microsoft :: Windows` classifier (2026-07-12).
957
+ - `steno setup` messaging (2026-07-12): a win32 branch reads `mic_access_blocked()`
958
+ up front and fails before the model download, mirroring the macOS grant step —
959
+ and, when allowed, tells the user Windows will never show a prompt.
960
+ - The `shortcut.py` win32 branch (2026-07-12): a Desktop `Stenograf.cmd` — the
961
+ "simpler" option from the two scoped here, since `steno` is a console app (a
962
+ `.lnk` would open a console window anyway) and a text wrapper regenerates without
963
+ COM. The Desktop is resolved through the shell's `User Shell Folders` registry key
964
+ (OneDrive folder backup redirects it on a large share of Windows 11 machines;
965
+ `~/Desktop` would be an invisible decoy there), and the wrapper ends in
966
+ `if errorlevel 1 pause` so a startup crash stays readable instead of vanishing
967
+ with the console window.
968
+
969
+ What remains for **full Windows support** is planned in the next subsection.
970
+
971
+ ### Phase 6 — remaining-work plan (planned 2026-07-12)
972
+
973
+ **Definition of done:** a fresh Windows 11 machine goes one-liner install →
974
+ `steno setup` → live meeting → diarized transcript + notes in
975
+ `Documents\Meetings`, at parity with Linux, with no manual step beyond the
976
+ mic privacy toggle. Four tracks: **A gates the "supported" claim**; B, C, D
977
+ are independent of each other and of A (but A first — everything else builds
978
+ on a verified core).
979
+
980
+ **Track A — real-hardware validation (the gate).**
981
+
982
+ State (desktop, 2026-07-12, Ryzen 9 5900X + RTX 4080 SUPER, Sharkoon Gaming
983
+ DAC Pro S, output = headphones): **the automatable core is verified on the
984
+ desktop; items 1–2 pass, item 3's live path smoke-passes, the ≥30-min/AEC gate
985
+ and the notebook re-run stay open.** Measured on a 56.4 s piper-TTS clip
986
+ (en_US-lessac-medium, the ci.yml sentence with "provisioning"/"October"):
987
+
988
+ - **Item 1 — transcribe, PASS.** Warm CPU run (weights pre-cached): 8.3 s wall
989
+ → 6.8× RT end-to-end, but that is model-load-bound on a short clip; the
990
+ tool's own transcribe-only figure is **14.9× RT** (3.8 s). Sentinels present;
991
+ "backend"→"backhand" is the known TTS mishearing. `provider = "dml"`
992
+ (`STENOGRAF_ASR_PROVIDER=dml`) engaged ("asr: accelerated (DirectML)"), no
993
+ silent CPU fallback: transcript **byte-identical** to CPU (SHA-256 match on
994
+ transcript.{txt,md,json}), transcribe-only **35.3× RT** (1.6 s) →
995
+ **2.37× DML speedup** over CPU compute. Output files are clean UTF-8/ASCII,
996
+ no code-page mangling (the only mojibake seen — a UTF-16 BOM and a `✓`→`?`
997
+ glyph — is PowerShell's `*>` redirect + console codepage, not the tool).
998
+ - **Item 2 — `steno start --replay`, PASS.** End-to-end via the `verify` flow
999
+ (mic replay, in-room): live caption emitted, finalize + transcript written,
1000
+ both sentinels present.
1001
+ - **Item 3 (desktop smoke) — live WASAPI capture, PASS.** `--local 0 --remote
1002
+ 1` captured the Sharkoon **loopback** render stream (default output =
1003
+ headphones, yet loopback still taps render); both sentinels present on a
1004
+ single clean playback. Back-to-back double playback reorders/garbles the
1005
+ ASR (a playback-overlap harness artifact, not a capture bug). Bonus
1006
+ `--local 1 --remote 1` mic+system smoke: **both channels captured**, **AEC
1007
+ armed** ("echo cancellation: on (mic cancelled against system audio)"),
1008
+ stderr clean — **no far-end-missing / armed-backstop warnings**, and the
1009
+ **silent-mic watchdog stayed quiet** (no false-positive). With headphones
1010
+ output the mic heard nothing intelligible and all content arrived via the
1011
+ system channel — expected on this box.
1012
+ - **Launcher meeting start was broken on Windows — found by the by-eye check,
1013
+ fixed 2026-07-12:** every meeting died instantly with `[Errno 9] Bad file
1014
+ descriptor` — click.echo under Textual's redirected stdio trips click's
1015
+ Windows-only console probe (`msvcrt.get_osfhandle` on the proxy fd → EBADF)
1016
+ at `make_provider`'s capture-device line. The loader seams now take
1017
+ `announce=` (None = click CLI behavior; the TUIs pass their status sinks),
1018
+ which also surfaces real loader progress in the meeting header. Verified by
1019
+ driving the real launcher headlessly: capture → finalize → saved.
1020
+ - **Still open (needs the notebook, real speakers):** the ≥30-min
1021
+ **speakers-not-headphones** AEC meeting (far-end re-anchor / echo-leak across
1022
+ long system-silence gaps — item 3's AEC bullet and item 4's long run); the
1023
+ **TUI-by-eye** check in Windows Terminal (live captions, resize, clean Ctrl-C);
1024
+ and re-running items 1–3 on the **notebook's GPU** (different DML vendor tier).
1025
+
1026
+ 1. Real `steno transcribe` on a meeting-length recording with downloaded
1027
+ models (fp32 ONNX, CPU): correctness + RTF on the notebook. Repeat with
1028
+ `[asr] provider = "dml"` — byte-identity + speedup were verified on the
1029
+ desktop's RTX 4080; re-confirm on the notebook's GPU (different vendor
1030
+ tier is exactly what DML is for).
1031
+ 2. `steno start --replay` end to end (the `verify` skill flow) on Windows.
1032
+ 3. A real live meeting on the notebook — **speakers, not headphones**, both
1033
+ channels captured. Checks, in order of risk:
1034
+ - **AEC**: livekit's APM is installed but unexercised on Windows, and
1035
+ laptop mics genuinely hear the speakers. Watch `far_end_missing_ticks`
1036
+ and the armed-backstop warning across WASAPI loopback silence gaps; a
1037
+ 0.5 s re-anchor moves the far-end alignment, so confirm the AEC
1038
+ re-converges after long system silence instead of leaking echo.
1039
+ - TUI in Windows Terminal: live captions, resize, clean Ctrl-C shutdown.
1040
+ - The silent-mic watchdog must not false-positive on real hardware.
1041
+ - Finalize with diarization (sherpa known-count path) + notes via Ollama;
1042
+ transcript + notes land in `Documents\Meetings`, no code-page mangling.
1043
+ 4. Run the checklist **twice**: one short smoke, one ≥30-min meeting with
1044
+ long system-silence stretches — the gap/zero-fill/re-anchor path only
1045
+ shows up in the long one. Record results here; anything broken becomes a
1046
+ task in this section.
1047
+
1048
+ **Track B — stenodiar (estimated speaker counts).**
1049
+
1050
+ State: **the patched CPU build is measured on Windows (2026-07-12, desktop
1051
+ Ryzen 9 5900X 12C/24T).** Prior state (2026-07-11): CPU 3.0× RT but still
1052
+ ~1.1 cores, which **predated** the two vendored speakrs patches; CUDA 60× RT
1053
+ works via pip `nvidia-*-cu12` DLLs on PATH + `onnxruntime_providers_cuda.dll`
1054
+ beside the exe; rustup + VS 2022 Build Tools are installed there. The CPU exe
1055
+ is self-contained (~40 MB, ORT statically linked).
1056
+
1057
+ 1. **Done.** Rebuilt from the patched vendor tree (`native/stenodiar/vendor/`,
1058
+ threads + model-list patches; CPU default features, `cargo build --release
1059
+ --locked`) and re-measured on a 300 s mono-16 kHz cut of AMI ES2004a
1060
+ Mix-Headset: **34.6–35.1 s wall/segment → 8.6× RT, ~6.0 cores** (208 s
1061
+ CPU-time / 34.6 s wall), 3 speakers / 46 turns, byte-identical across the
1062
+ two timed back-to-back runs. Right at the Linux **8.2×** figure (the
1063
+ intra-op-thread patch is the same lever; embedding threads still cap at 8),
1064
+ and a ~2.9× jump over the pre-patch Windows 3.0× — well past the ≥4× bar.
1065
+ 2. Shipping decision (recommendation, not yet implemented): **GitHub Releases
1066
+ attachment + a documented drop location**, not wheel bundling — a 40 MB
1067
+ binary for an optional, sherpa-fallback feature does not belong in every
1068
+ platform wheel, and `hatch_build.py` already keeps stenodiar off non-mac
1069
+ wheels. `find_stenodiar` now handles the `.exe` suffix on win32 (via
1070
+ `_HELPER_FILENAME`) and, as a dev/drop fallback, also checks
1071
+ `native/stenodiar/target/release/`; the packaged-bin lookup
1072
+ (`stenograf/bin/stenodiar.exe`) and `STENOGRAF_DIAR_HELPER` override are the
1073
+ two Release-install landing spots.
1074
+ 3. **Done.** `build.ps1` twin of `build.sh` added (CPU/ORT default, `--locked`,
1075
+ copies `stenodiar.exe` next to the script; CUDA stays a manual `--features
1076
+ cuda`); `native/README.md` and the `find_stenodiar` not-found message point
1077
+ Windows at it.
1078
+
1079
+ Exit: **met** — `steno doctor` finds the helper on Windows (the target/release
1080
+ fallback discovers a plain `cargo build`), and estimated-count diarization runs
1081
+ at 8.6× RT (≥4× bar cleared).
1082
+
1083
+ **Track C — install story (the Phase-7 Windows leg).**
1084
+
1085
+ 1. `install.ps1` mirroring `install.sh`: install uv via the astral installer
1086
+ (`irm https://astral.sh/uv/install.ps1 | iex` — **not** winget, whose uv
1087
+ lands off-PATH), then `uv tool install --upgrade stenograf` (the
1088
+ `<3.14` python cap already steers uv to 3.12/3.13), then `steno setup`.
1089
+ Documented invocation is the piped one-liner — a downloaded, double-clicked
1090
+ `.ps1` hits execution policy.
1091
+ 2. CI parity with `install.sh`'s `sh -n` test: syntax-check via
1092
+ `[ScriptBlock]::Create((Get-Content -Raw install.ps1))` (runs on the
1093
+ windows-latest job, skipped elsewhere) + the same content asserts
1094
+ (`tool install --upgrade stenograf`, ends in `setup`).
1095
+ 3. README: a Windows install section — the one-liner, the privacy-toggle
1096
+ note, Windows Terminal recommended.
1097
+ 4. **Done (2026-07-13).** `release.yml` gained a `smoke-windows` leg and
1098
+ `publish` now waits on it: a clean `uv tool install` of the pure wheel on
1099
+ `windows-latest`, then `steno --version`, `setup --models-only`, a doctor
1100
+ render, and a `--replay` pipeline run asserting `transcript.json`. Windows
1101
+ builds nothing of its own (it ships the same pure wheel as Linux), so the
1102
+ gap was never in *building* — it was that **no Windows install was exercised
1103
+ before PyPI**, which is how 0.2.1 published a launcher that died on every
1104
+ meeting start. Two notes for whoever touches this job: `steno doctor` exits
1105
+ **1** on a runner by design (no audio endpoint → the non-optional capture
1106
+ check fails), so the step tolerates that and asserts only that the report
1107
+ *renders* — piped Windows stdout is legacy-code-page, the bug class that once
1108
+ crashed `click.echo` on the ✓ glyph; and the replay clip is a 440 Hz tone, so
1109
+ it finalizes to **0 entries** — the assertion is that the transcript is
1110
+ written at all, not its content. The launcher/TUI stays covered by ci.yml's
1111
+ windows unit job (headless real-stack smoke), not here.
1112
+
1113
+ Exit: on a fresh VM (or fresh user account), the one-liner reaches
1114
+ "setup complete" and drops the Desktop launcher.
1115
+
1116
+ **Track D — on-device notes backend — DECIDED 2026-07-12 (no code; the
1117
+ build gate stays "Ollama proves insufficient").**
1118
+
1119
+ Re-researched against the DirectML EOL. The clobber fear was unfounded:
1120
+ `onnxruntime-genai-directml` (0.14.1, 2026-06-02, still released) does not
1121
+ bundle its own ORT — it pip-depends on **`onnxruntime-directml`**, the very
1122
+ flavor the ASR already ships, so it coexists cleanly (the CPU flavor
1123
+ `onnxruntime-genai` depends on base `onnxruntime` and IS disqualified by the
1124
+ one-flavor rule). **Rejected anyway**: DX12-GPU-only with no CPU fallback,
1125
+ built entirely on the EOL DirectML EP — the wrong bet when Microsoft's
1126
+ investment moved to Windows ML. The successors aren't pip-reachable today:
1127
+ Windows ML genai is NuGet/C#-only Preview (no Python wheel); Foundry Local
1128
+ (GA 2026-06) requires a DX12 GPU and pulls `onnxruntime-core`/-genai (real
1129
+ conflict risk), an Ollama-class service rather than an in-process analog.
1130
+
1131
+ **Decision: stay Ollama-default on Windows. If an in-process pip backend is
1132
+ ever needed, use llama-cpp-python (CPU GGUF)** — the same fallback CLAUDE.md
1133
+ already records for Linux CPU, same off-PyPI extra-index friction, zero ORT
1134
+ entanglement. Caveat: the official wheel index's Windows cp313 CPU wheel is
1135
+ stale (0.3.2) — build from source (VS Build Tools installed on the desktop)
1136
+ or a third-party index. Model: Phi-4-mini stays fine as Q4 GGUF (Qwen2.5-3B /
1137
+ Llama-3.2-3B as smaller CPU-friendly alternatives). Revisit the ONNX route
1138
+ only when Windows ML genai ships an out-of-preview Python wheel.
1139
+
1140
+ **Watch-items (not tasks):** `onnxruntime-directml` wheels ceasing to track
1141
+ ORT patch releases (swap the marker back to plain `onnxruntime`; the provider
1142
+ setting degrades to CPU with a warning — designed in); mid-meeting default-
1143
+ device switches stay unfollowed on Windows (WASAPI has no `@DEFAULT_MONITOR@`
1144
+ alias — accepted asymmetry vs Linux); Windows ML as the eventual multi-vendor
1145
+ EP path for ASR, once dynamic-shape conformer models are validated there.
950
1146
 
951
1147
  ---
952
1148
 
@@ -68,6 +68,19 @@ installs the current main branch; building from the repository compiles the
68
68
  capture helper on your machine, which needs the Xcode command-line tools
69
69
  (`xcode-select --install`).
70
70
 
71
+ ### Windows
72
+
73
+ Same one command, run from PowerShell — installs uv if missing, then stenograf,
74
+ then the launcher:
75
+
76
+ ```powershell
77
+ powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/daniel-om-weber/stenograf/main/install.ps1 | iex"
78
+ ```
79
+
80
+ Windows never prompts for the microphone, so capture stays silent until you flip
81
+ the toggle under Settings > Privacy & security > Microphone. Windows Terminal is
82
+ recommended for the live-caption TUI.
83
+
71
84
  ### From a checkout
72
85
 
73
86
  ```sh
@@ -201,11 +214,14 @@ dir = "~/Documents/Obsidian/Meetings" # optional: also write one combined
201
214
  With `[notes.export]` set, every summarized meeting also produces a single
202
215
  self-contained markdown note (frontmatter, summary, action items, collapsible
203
216
  transcript) — drop the dir inside an Obsidian vault and meetings file
204
- themselves. Two more levers in `[notes]`: `instructions = "~/style.md"`
205
- appends your house style to the built-in prompt, and `thinking = false` skips
206
- the mlx model's reasoning pass (faster, less careful). Notes never run unless
207
- you ask (`--notes` or `steno notes`), and a notes failure never touches the
208
- transcript.
217
+ themselves. Three more levers in `[notes]`: `instructions = "~/style.md"`
218
+ appends your house style to the built-in prompt, `thinking = false` skips the
219
+ mlx model's reasoning pass (faster, less careful), and `auto = true` makes
220
+ notes the default for every meeting `steno start` summarizes without
221
+ `--notes`, and the launcher's "Generate notes after the meeting" switch starts
222
+ on. Out of the box notes never run unless you ask (`--notes`, the launcher
223
+ switch, or `steno notes` afterwards); with `auto = true`, `--no-notes` still
224
+ skips them for one run. A notes failure never touches the transcript.
209
225
 
210
226
  ## Naming speakers across meetings
211
227
 
@@ -0,0 +1,28 @@
1
+ # stenograf installer — one command sets up everything:
2
+ #
3
+ # powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/daniel-om-weber/stenograf/main/install.ps1 | iex"
4
+ #
5
+ # Installs uv if missing, installs stenograf as a uv tool, then runs
6
+ # `steno setup` (permission prompts, desktop launcher, model downloads).
7
+ # Safe to re-run: every step is idempotent and re-running upgrades stenograf.
8
+ $ErrorActionPreference = "Stop"
9
+
10
+ $uvHome = Join-Path $env:USERPROFILE ".local\bin\uv.exe"
11
+ if (-not (Get-Command uv -ErrorAction SilentlyContinue) -and -not (Test-Path $uvHome)) {
12
+ Write-Host "installing uv (https://docs.astral.sh/uv/) ..."
13
+ powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"
14
+ }
15
+ $uv = (Get-Command uv -ErrorAction SilentlyContinue).Source
16
+ if (-not $uv) { $uv = $uvHome }
17
+
18
+ & $uv tool install --upgrade stenograf
19
+
20
+ # A freshly created uv bin dir isn't on this shell's PATH yet — ask uv where it is.
21
+ $steno = (Get-Command steno -ErrorAction SilentlyContinue).Source
22
+ if (-not $steno) { $steno = Join-Path (& $uv tool dir --bin) "steno.exe" }
23
+
24
+ & $steno setup
25
+
26
+ Write-Host ""
27
+ Write-Host "stenograf is installed. Start it from the desktop launcher above,"
28
+ Write-Host "or run: steno"
@@ -11,7 +11,9 @@ VBx can; stenograf routes estimate-mode diarization through it and known
11
11
  counts through sherpa (see `stenograf/diarization/speakrs.py`). Raw mono
12
12
  16 kHz s16le PCM goes in on stdin, JSON speaker turns come out on stdout.
13
13
  Build with `stenodiar/build.sh` (needs a Rust toolchain: `brew install rust`);
14
- no signing needed it touches no TCC-guarded resource. The first run per
14
+ on Windows use `stenodiar/build.ps1` (rustup + VS Build Tools; CPU/ORT default,
15
+ CUDA a manual `--features cuda` opt-in); no signing needed — it touches no
16
+ TCC-guarded resource. The first run per
15
17
  machine downloads models and compiles them for CoreML (minutes; `--warmup`
16
18
  does it eagerly). Without the binary, stenograf silently falls back to
17
19
  sherpa-only diarization.
@@ -0,0 +1,14 @@
1
+ # Build the stenodiar diarization helper and drop the binary next to this
2
+ # script, where stenograf's dev fallback looks for it (twin of build.sh for
3
+ # Windows). Needs a Rust toolchain (rustup) and the VS Build Tools linker. No
4
+ # signing: stenodiar touches no guarded resource, so an unsigned binary is fine.
5
+ $ErrorActionPreference = "Stop"
6
+ Set-Location $PSScriptRoot
7
+
8
+ # Default features = ORT CPU (matches the shipped Linux/Windows path); CUDA is a
9
+ # manual opt-in — cargo build --release --features cuda (nvidia-*-cu12 DLLs +
10
+ # onnxruntime_providers_cuda.dll on PATH). CoreML is macOS-only (build.sh).
11
+ cargo build --release --locked
12
+ Copy-Item target\release\stenodiar.exe stenodiar.exe -Force
13
+
14
+ Write-Output "built: $(Join-Path (Get-Location) 'stenodiar.exe')"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stenograf"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Accuracy-first local meeting transcription for German and English. Audio stays in RAM; only text is written."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -15,6 +15,7 @@ classifiers = [
15
15
  "Development Status :: 3 - Alpha",
16
16
  "Environment :: Console",
17
17
  "Operating System :: MacOS",
18
+ "Operating System :: Microsoft :: Windows",
18
19
  "Operating System :: POSIX :: Linux",
19
20
  "Programming Language :: Python :: 3.12",
20
21
  "Topic :: Multimedia :: Sound/Audio :: Speech",
@@ -193,9 +193,14 @@ class QueueStreamingProvider[TransportT](CaptureProvider):
193
193
  try:
194
194
  self._pump(channel, transport)
195
195
  finally:
196
- if not self._stop_event.is_set():
197
- self.stop()
198
- self._queue.put(channel)
196
+ try:
197
+ if not self._stop_event.is_set():
198
+ self.stop()
199
+ finally:
200
+ # The sentinel is the only way frames() learns this channel is
201
+ # done, so it must survive a teardown that raises — otherwise
202
+ # frames() waits on an empty queue with every pump already dead.
203
+ self._queue.put(channel)
199
204
 
200
205
  def _emit(self, channel: Channel, samples: np.ndarray) -> None:
201
206
  """Stamp a frame onto the session clock and hand it to ``frames()``."""
@@ -245,10 +245,14 @@ class WindowsCaptureProvider(QueueStreamingProvider[None]):
245
245
  # Pumps notice the stop event within one frame read (~frame_ms +
246
246
  # WASAPI's silence threshold) and release their devices on the way
247
247
  # out. Skip the current thread: stop() also runs *from* a pump on an
248
- # unexpected stream death.
248
+ # unexpected stream death. Skip threads that have not started either:
249
+ # a pump dying this early can land inside start()'s register-then-start
250
+ # window, and joining an unstarted thread raises. Nothing is lost by
251
+ # skipping — the stop event is already set, so a pump that starts after
252
+ # this exits on its first loop check.
249
253
  current = threading.current_thread()
250
254
  for thread in self._threads.values():
251
- if thread is not current:
255
+ if thread is not current and thread.ident is not None:
252
256
  thread.join(timeout=5)
253
257
 
254
258
 
@@ -35,18 +35,22 @@ def doctor() -> None:
35
35
  help="Skip the permission prompts and only download the models (headless machines, CI).",
36
36
  )
37
37
  def setup(models_only: bool) -> None:
38
- """One-time setup: permission prompts, desktop launcher, model downloads.
39
-
40
- Launches the capture helper so macOS shows both permission prompts (mic +
41
- system audio) now instead of at the start of your first meeting — nothing
42
- is recorded then installs a double-clickable launcher and downloads
43
- every model the first meeting would otherwise stop to fetch. macOS scopes
44
- the grant to the app the helper was launched from, so re-run this from
45
- each terminal app (or IDE) you will run meetings from; the models are
46
- cached machine-wide.
38
+ """One-time setup: capture permissions, desktop launcher, model downloads.
39
+
40
+ macOS: launches the capture helper so both permission prompts (mic +
41
+ system audio) appear now instead of at the start of your first meeting —
42
+ nothing is recorded; the grant is scoped to the app the helper was
43
+ launched from, so re-run this from each terminal app (or IDE) you will
44
+ run meetings from. Windows: no prompt exists, so the microphone privacy
45
+ toggle is checked and named instead. Then installs a double-clickable
46
+ launcher and downloads every model the first meeting would otherwise
47
+ stop to fetch; the models are cached machine-wide.
47
48
  """
48
- if not models_only and sys.platform == "darwin":
49
- _grant_capture_permissions() # only macOS gates capture behind TCC prompts
49
+ if not models_only:
50
+ if sys.platform == "darwin":
51
+ _grant_capture_permissions() # only macOS gates capture behind TCC prompts
52
+ elif sys.platform == "win32":
53
+ _check_windows_mic_access() # no prompt exists — read the privacy toggle now
50
54
 
51
55
  # The launcher lands before the model download: the download can fail (and
52
56
  # models fetch on first use anyway), the shortcut shouldn't be lost to that.
@@ -56,10 +60,10 @@ def setup(models_only: bool) -> None:
56
60
 
57
61
  if (shortcut := install_shortcut()) is not None:
58
62
  click.echo(click.style("✓", fg="green") + f" launcher installed: {shortcut}")
59
- if sys.platform == "darwin":
60
- click.echo(" Double-click it to start stenograf — no terminal needed.")
61
- else:
63
+ if sys.platform.startswith("linux"): # menu entry; macOS/Windows land on the Desktop
62
64
  click.echo(' Look for "Stenograf" in your application menu.')
65
+ else:
66
+ click.echo(" Double-click it to start stenograf — no terminal needed.")
63
67
 
64
68
  # Permissions first (they need the user at the keyboard), then the long
65
69
  # unattended part: everything a first meeting would otherwise stop to fetch.
@@ -73,6 +77,27 @@ def setup(models_only: bool) -> None:
73
77
  click.echo(click.style("✓", fg="green") + " setup complete.")
74
78
 
75
79
 
80
+ def _check_windows_mic_access() -> None:
81
+ """Fail setup loud when the Windows mic privacy toggle denies capture.
82
+
83
+ Windows never prompts desktop apps for the microphone (no TCC
84
+ equivalent) — a denied toggle just makes the stream deliver zeros — so
85
+ setup reads the consent store up front, mirroring the macOS grant step's
86
+ fail-before-models behavior, and tells the user no prompt is coming.
87
+ """
88
+ from stenograf.capture.windows import mic_access_blocked
89
+
90
+ if (blocked := mic_access_blocked()) is not None:
91
+ raise click.ClickException(f"{blocked}, then re-run `steno setup`")
92
+ click.echo(
93
+ click.style("✓", fg="green") + " microphone access is allowed in Windows privacy settings."
94
+ )
95
+ click.echo(
96
+ " Windows shows no permission prompt — if the mic ever records only silence, "
97
+ "check Settings > Privacy & security > Microphone."
98
+ )
99
+
100
+
76
101
  def _grant_capture_permissions() -> None:
77
102
  if sys.platform != "darwin":
78
103
  raise click.ClickException(