textflowkit 0.1.6__tar.gz → 0.1.7__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 (170) hide show
  1. textflowkit-0.1.7/CHANGELOG.md +100 -0
  2. {textflowkit-0.1.6 → textflowkit-0.1.7}/CONTRIBUTING.md +7 -0
  3. {textflowkit-0.1.6 → textflowkit-0.1.7}/PKG-INFO +37 -12
  4. {textflowkit-0.1.6 → textflowkit-0.1.7}/README.md +36 -11
  5. {textflowkit-0.1.6 → textflowkit-0.1.7}/SECURITY.md +57 -5
  6. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/adapters.md +178 -21
  7. textflowkit-0.1.7/docs/architecture.md +357 -0
  8. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/index.html +3 -3
  9. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/install.md +42 -22
  10. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/release-checklist.md +4 -4
  11. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/roadmap.md +44 -5
  12. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/site-deployment.md +9 -9
  13. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/sources.md +8 -0
  14. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/user-manual.md +72 -16
  15. {textflowkit-0.1.6 → textflowkit-0.1.7}/pyproject.toml +4 -1
  16. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/__init__.py +1 -1
  17. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/adapters/http_server.py +107 -23
  18. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/adapters/mcp_server.py +154 -40
  19. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/cli.py +39 -6
  20. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/batch.py +22 -4
  21. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/checkpoint.py +40 -10
  22. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/engine.py +67 -0
  23. textflowkit-0.1.7/src/textflowkit/core/executor.py +791 -0
  24. textflowkit-0.1.7/src/textflowkit/core/jobs.py +675 -0
  25. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/paths.py +18 -2
  26. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/pipeline.py +147 -15
  27. textflowkit-0.1.7/src/textflowkit/core/runner.py +264 -0
  28. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/service.py +22 -9
  29. textflowkit-0.1.7/src/textflowkit/core/sqlite_store.py +585 -0
  30. textflowkit-0.1.7/src/textflowkit/core/startup.py +136 -0
  31. textflowkit-0.1.7/src/textflowkit/core/submission.py +540 -0
  32. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/translate.py +21 -0
  33. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/__init__.py +399 -29
  34. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/sources/acquire.py +100 -4
  35. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_adapters.py +22 -10
  36. textflowkit-0.1.7/tests/test_admission_ownership.py +375 -0
  37. textflowkit-0.1.7/tests/test_atomic_resume.py +615 -0
  38. textflowkit-0.1.7/tests/test_atomic_resume_aba.py +478 -0
  39. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_batch.py +30 -14
  40. textflowkit-0.1.7/tests/test_batch_item_boundary.py +645 -0
  41. textflowkit-0.1.7/tests/test_cancellation_finalization.py +1470 -0
  42. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_checkpoint.py +1 -1
  43. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_checkpoint_single_copy.py +1 -1
  44. textflowkit-0.1.7/tests/test_cli_batch_feedback.py +234 -0
  45. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_cli_resume_batch.py +12 -2
  46. textflowkit-0.1.7/tests/test_cli_stage_feedback.py +373 -0
  47. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_cli_stdout_format.py +12 -0
  48. textflowkit-0.1.7/tests/test_confined_decoder_boundary.py +248 -0
  49. textflowkit-0.1.7/tests/test_confined_indirect_media.py +211 -0
  50. textflowkit-0.1.7/tests/test_confined_resume_boundary.py +153 -0
  51. textflowkit-0.1.7/tests/test_docx_publication_identity.py +994 -0
  52. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_done_output_integrity.py +1 -1
  53. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_empty_formats_resume.py +1 -1
  54. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_engine_adapters.py +59 -16
  55. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_engine_faster.py +12 -3
  56. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_engine_quiet.py +15 -2
  57. textflowkit-0.1.7/tests/test_executor_store_failure.py +1000 -0
  58. textflowkit-0.1.7/tests/test_export_preflight.py +683 -0
  59. textflowkit-0.1.7/tests/test_mcp_protocol_cleanliness.py +215 -0
  60. textflowkit-0.1.7/tests/test_mcp_terminal_guidance.py +222 -0
  61. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_mcp_tool_descriptions.py +6 -2
  62. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_partial_output_resume.py +1 -1
  63. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_partial_pdf_resume.py +1 -1
  64. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_pipeline_integrity.py +1 -1
  65. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_production_browser_cookies.py +34 -5
  66. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_release_boundaries.py +13 -4
  67. textflowkit-0.1.7/tests/test_stage_reuse_resume.py +1315 -0
  68. textflowkit-0.1.7/tests/test_startup_recovery.py +622 -0
  69. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_submission.py +32 -14
  70. textflowkit-0.1.7/tests/test_submission_validation.py +311 -0
  71. textflowkit-0.1.7/tests/test_translate_completeness.py +351 -0
  72. textflowkit-0.1.6/src/textflowkit/core/executor.py +0 -297
  73. textflowkit-0.1.6/src/textflowkit/core/jobs.py +0 -241
  74. textflowkit-0.1.6/src/textflowkit/core/runner.py +0 -173
  75. textflowkit-0.1.6/src/textflowkit/core/sqlite_store.py +0 -233
  76. textflowkit-0.1.6/src/textflowkit/core/submission.py +0 -279
  77. {textflowkit-0.1.6 → textflowkit-0.1.7}/.dockerignore +0 -0
  78. {textflowkit-0.1.6 → textflowkit-0.1.7}/.env.example +0 -0
  79. {textflowkit-0.1.6 → textflowkit-0.1.7}/.gitattributes +0 -0
  80. {textflowkit-0.1.6 → textflowkit-0.1.7}/.github/dependabot.yml +0 -0
  81. {textflowkit-0.1.6 → textflowkit-0.1.7}/.github/workflows/ci.yml +0 -0
  82. {textflowkit-0.1.6 → textflowkit-0.1.7}/.github/workflows/publish-pypi.yml +0 -0
  83. {textflowkit-0.1.6 → textflowkit-0.1.7}/.gitignore +0 -0
  84. {textflowkit-0.1.6 → textflowkit-0.1.7}/.pre-commit-config.yaml +0 -0
  85. {textflowkit-0.1.6 → textflowkit-0.1.7}/Dockerfile +0 -0
  86. {textflowkit-0.1.6 → textflowkit-0.1.7}/LEGAL.md +0 -0
  87. {textflowkit-0.1.6 → textflowkit-0.1.7}/LICENSE +0 -0
  88. {textflowkit-0.1.6 → textflowkit-0.1.7}/compose.yaml +0 -0
  89. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/.nojekyll +0 -0
  90. {textflowkit-0.1.6 → textflowkit-0.1.7}/docs/404.html +0 -0
  91. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/fetch_published_fonts.py +0 -0
  92. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/smoke_installed_wheel.py +0 -0
  93. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/smoke_live_youtube.py +0 -0
  94. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/verify_readme_release.py +0 -0
  95. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/verify_release_ci.py +0 -0
  96. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/verify_release_versions.py +0 -0
  97. {textflowkit-0.1.6 → textflowkit-0.1.7}/scripts/write_release_manifest.py +0 -0
  98. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/adapters/__init__.py +0 -0
  99. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/assets/README.md +0 -0
  100. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/assets/selftest-speech.wav +0 -0
  101. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/__init__.py +0 -0
  102. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/bind.py +0 -0
  103. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/cancel.py +0 -0
  104. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/diarize.py +0 -0
  105. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/model.py +0 -0
  106. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/retrieval.py +0 -0
  107. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/core/timeutil.py +0 -0
  108. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/_cue_layout.py +0 -0
  109. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/_cue_text.py +0 -0
  110. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/_rename_excl.py +0 -0
  111. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/_rename_noreplace.py +0 -0
  112. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/docx.py +0 -0
  113. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/markdown.py +0 -0
  114. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/pdf.py +0 -0
  115. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/srt.py +0 -0
  116. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/txt.py +0 -0
  117. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/render/vtt.py +0 -0
  118. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/sources/__init__.py +0 -0
  119. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/sources/detect.py +0 -0
  120. {textflowkit-0.1.6 → textflowkit-0.1.7}/src/textflowkit/sources/scratch.py +0 -0
  121. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/__init__.py +0 -0
  122. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/ollama_stub.py +0 -0
  123. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_acquire.py +0 -0
  124. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_batch_resume_shared.py +0 -0
  125. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_bind.py +0 -0
  126. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_cli.py +0 -0
  127. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_container_example.py +0 -0
  128. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_cue_layout.py +0 -0
  129. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_diarize.py +0 -0
  130. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_diarize_example_flags.py +0 -0
  131. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_diarize_token_docs.py +0 -0
  132. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_engine_cache.py +0 -0
  133. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_executor.py +0 -0
  134. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_export.py +0 -0
  135. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_export_file_mode.py +0 -0
  136. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_faster_whisper_docs.py +0 -0
  137. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_fetch_published_fonts.py +0 -0
  138. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_font_notices.py +0 -0
  139. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_http_host_boundary.py +0 -0
  140. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_http_trusted_proxy.py +0 -0
  141. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_job_progress.py +0 -0
  142. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_jobs.py +0 -0
  143. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_landing_commands.py +0 -0
  144. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_landing_page.py +0 -0
  145. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_legal_summary.py +0 -0
  146. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_linux_no_hardlink.py +0 -0
  147. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_live_smoke_script.py +0 -0
  148. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_macos_no_hardlink.py +0 -0
  149. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_mcp_http_boundary.py +0 -0
  150. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_mcp_work_root.py +0 -0
  151. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_model.py +0 -0
  152. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_paths.py +0 -0
  153. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_publish_ci_gate.py +0 -0
  154. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_publish_fonts_wiring.py +0 -0
  155. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_publish_manifest_wiring.py +0 -0
  156. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_publish_workflow_gate.py +0 -0
  157. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_readme_layout.py +0 -0
  158. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_readme_release_guard.py +0 -0
  159. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_release_manifest.py +0 -0
  160. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_release_surfaces.py +0 -0
  161. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_render.py +0 -0
  162. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_retrieval.py +0 -0
  163. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_sdist_allowlist.py +0 -0
  164. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_service_profile.py +0 -0
  165. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_site_not_found.py +0 -0
  166. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_sources.py +0 -0
  167. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_stdio_protocol.py +0 -0
  168. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_translate.py +0 -0
  169. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_verify_release_versions.py +0 -0
  170. {textflowkit-0.1.6 → textflowkit-0.1.7}/tests/test_windows_no_hardlink.py +0 -0
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ ## v0.1.7 — 2026-10-01
4
+
5
+ The 2026-10-01 audit repair release. Core version: 0.1.7; unchanged optional fonts: 0.1.6. Release artifacts and publication status are recorded on [GitHub Releases](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.7). Local source-candidate verification does not establish installed-package or individual harness compatibility.
6
+
7
+ ### Engineering fixes
8
+
9
+ - **Confined decoder boundary.** With `TEXTFLOWKIT_INPUT_ROOT` set, a confined
10
+ local input is staged and its decode is restricted to FFmpeg's self-contained
11
+ demuxers (`-format_whitelist`), and the `ffprobe` duration probe carries the
12
+ same whitelist. A playlist/manifest that would make the decoder open referenced
13
+ files is refused. The restriction covers a resume that re-decodes; a stage with
14
+ durable output is reused, not re-decoded. Owner runs without an input root stay
15
+ unrestricted.
16
+ - **Atomic attempt ownership.** A worker pins the attempt generation it claimed;
17
+ every progress write, checkpoint write, and terminal finalize is guarded on it,
18
+ so a stale run cannot overwrite a newer attempt's label or verdict. The reopen
19
+ of a terminal row for resume is an atomic claim pinned to the observed attempt —
20
+ two callers racing one job id cannot both reopen it.
21
+ - **Atomic worker startup and cancellation.** Worker-thread startup is atomic, and
22
+ the completion/cancellation race is decided in one place against the owned
23
+ attempt: the row becomes CANCELLED only if that generation carries an accepted
24
+ cancellation, DONE otherwise, so a finished DONE row can never still carry an
25
+ accepted cancel.
26
+ - **DOCX identity.** Completed-output reuse for DOCX compares the archive bytes
27
+ with only the local and central ZIP time/date pairs blanked; every other byte
28
+ (member names and order, compression, flags, extra fields, comments, compressed
29
+ content) must be equal, and the comparison never inflates a member.
30
+ - **Completed-stage reuse.** A run leaves a durable per-stage marker for each
31
+ expensive stage it finishes; resume reuses only stages finished for the
32
+ **requested configuration**, and a legacy coarse `postprocess` marker is honoured
33
+ only for the optional stages that record actually requested. The rule keys on
34
+ the marker, never on transcript metadata.
35
+ - **Aggregate export preflight.** The whole requested format set is validated
36
+ (unsupported/duplicate names, then every dependency) and the aggregate rendered
37
+ bytes are bounded **before any destination file is written**, for every surface
38
+ that publishes output.
39
+ - **Translation completeness.** The translator's result length is checked against
40
+ its input and every result is validated **before mutating any segment**: a
41
+ blank result for a non-blank segment raises and leaves the transcript exactly as
42
+ it was, rather than half-writing a set.
43
+ - **Per-item batch validation.** Each batch entry is constructed and admitted
44
+ independently (HTTP and MCP): a non-object entry, a non-string/empty source, an
45
+ unsupported format, or an unusable engine is **that** entry's error, carrying its
46
+ original zero-based `index` and best-effort `source`, while the other entries
47
+ still queue, in submitted order. There is no whole-batch engine preflight.
48
+ - **Terminal feedback.** A non-quiet CLI run streams live stage progress to
49
+ **stderr, flushed** (stdout stays clean for `--stdout`); quiet suppresses it.
50
+ Batch prints each item's result as it finishes. Job status exposes the recorded
51
+ stage.
52
+
53
+ ### Documentation corrections (DOC-001 – DOC-006)
54
+
55
+ - **DOC-001** — the user manual now states that canonical saved/exported JSON
56
+ retains hidden segments and is an archive, not a redaction mechanism, and adds
57
+ a worked safe-publication-copy example that filters the copy without mutating
58
+ the stored transcript.
59
+ - **DOC-002** — the adapter guide and manual now distinguish response shapes:
60
+ HTTP JSON returns a `transcript` object with page counts but no `next`, MCP
61
+ returns a JSON string under `content` and adds a `next` instruction, and HTTP
62
+ text reads carry no paging metadata. Time selection is described as
63
+ overlapping rather than clipped.
64
+ - **DOC-003** — "same options" is replaced with a per-surface parameter matrix
65
+ (HTTP `formats` array vs MCP comma-separated string; HTTP batch options are
66
+ **per item**, MCP batch options are shared and expose **no**
67
+ `cookies_from_browser` parameter), and the batch contract now states that 202
68
+ means admission, not completion, with per-item indexed errors. MCP
69
+ `sources` is documented as a list of **strings**: a non-string entry (including
70
+ a nested object or a number) is that entry's own error, not a whole-call
71
+ refusal, while a string remains valid.
72
+ - **DOC-004** — a full production settings reference (names, defaults, units,
73
+ required fields) is added to the adapter guide, and the production refusal of
74
+ browser-cookie submissions is stated across the adapter guide, the sources
75
+ guide, and `SECURITY.md`, including resume.
76
+ - **DOC-005** — the install guide chooses the diarization install path (preserve
77
+ a working ROCm stack vs fresh CPU) **before** the first pip command, and the
78
+ manual's ROCm deep link now resolves to the actual heading.
79
+ - **DOC-006** — new [architecture and lifecycle guide](docs/architecture.md)
80
+ maps the core/CLI/MCP/HTTP doors, single-owning-process SQLite, startup
81
+ recovery via the HTTP/MCP lifespans and the lazy executor, checkpoint reuse,
82
+ and output publication; the adapter guide's "reaped at startup" wording is
83
+ qualified to match. Linked from the README and `CONTRIBUTING.md`.
84
+
85
+ The production settings reference lives in
86
+ [docs/adapters.md](docs/adapters.md#production-settings-reference).
87
+
88
+ ### Deferred audit follow-ups — later fixes
89
+
90
+ On 2026-10-01, the owner explicitly deferred the following findings from the
91
+ full five-role audit. They remain open; they are not completed fixes, new release
92
+ claims, or additions to the current core-bug repair goal. No release date is set.
93
+
94
+ See [Later fixes in the roadmap](docs/roadmap.md#later-fixes--2026-10-01-audit)
95
+ for the complete list and completion criteria. The current repair goal still
96
+ covers decoder confinement, job lifecycle, publication/resume, translation and
97
+ batch correctness, major terminal guidance/documentation, and its final gate.
98
+
99
+ This changelog starts with the v0.1.7 release notes; published historical
100
+ release notes remain on [GitHub Releases](https://github.com/scottconverse/textflowkit/releases).
@@ -55,6 +55,13 @@ build of torch; see [docs/install.md](docs/install.md) for the torch-pinning tra
55
55
  Most tests do **not** need a GPU or a model download. If you add one that does,
56
56
  mark it so it can be skipped in CI.
57
57
 
58
+ ## How the pieces fit together
59
+
60
+ [docs/architecture.md](docs/architecture.md) maps the core, CLI, MCP, and HTTP
61
+ doors, the single-owning-process job lifecycle (including startup recovery),
62
+ checkpoint reuse, and output publication. Read it before changing submission,
63
+ executor, runner, or checkpoint behaviour.
64
+
58
65
  ## Pull requests
59
66
 
60
67
  - Keep the change scoped; state what it fixes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: textflowkit
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Cross-platform media transcription toolkit: URL or file in, timestamped transcripts and subtitle files out.
5
5
  Project-URL: Homepage, https://www.textflowkit.org/
6
6
  Project-URL: Issues, https://github.com/scottconverse/textflowkit/issues
@@ -64,7 +64,7 @@ Cross-platform media transcription toolkit. **One core, one CLI, thin adapters.*
64
64
  [GitHub releases](https://github.com/scottconverse/textflowkit/releases) ·
65
65
  [User manual](https://github.com/scottconverse/textflowkit/blob/main/docs/user-manual.md)
66
66
 
67
- **Current release: [v0.1.6](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.6).**
67
+ **Current release: [v0.1.7](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.7).**
68
68
 
69
69
  The [static site deployment](https://github.com/scottconverse/textflowkit/blob/main/docs/site-deployment.md) is hosted on Cloudflare
70
70
  Pages. GitHub remains the source and CI host; the website does not run the
@@ -140,6 +140,9 @@ the core; the interfaces are thin.
140
140
  Because the core owns the pipeline, adding a door is cheap — and adding a platform
141
141
  means writing one source adapter, not another tool.
142
142
 
143
+ For the job lifecycle, process ownership, checkpoints, and output publication,
144
+ see [docs/architecture.md](https://github.com/scottconverse/textflowkit/blob/main/docs/architecture.md).
145
+
143
146
  ## Recognized sources
144
147
 
145
148
  YouTube · TikTok · Facebook · Instagram · Vimeo · Twitch · Bilibili · Rumble ·
@@ -216,7 +219,7 @@ to preserve it.
216
219
 
217
220
  The same version's wheel and source archive are also on the
218
221
  [GitHub release page](https://github.com/scottconverse/textflowkit/releases/latest).
219
- Starting with the next release, every release published by this project's release
222
+ Starting with v0.1.6, every release published by this project's release
220
223
  workflow carries a `SHA256SUMS` asset listing the SHA-256 hash of each wheel and
221
224
  source archive it contains, so you can check a download with
222
225
  `sha256sum -c SHA256SUMS`. Releases published before that change, v0.1.5
@@ -335,21 +338,43 @@ web frontend share the job contract without blocking a request.
335
338
 
336
339
  ## Status
337
340
 
338
- **v0.1.6 release.** Core, CLI, MCP, and HTTP have automated
339
- coverage. This release is the post-v0.1.5 review repair set: security hardening
341
+ **v0.1.7 release.** Core, CLI, MCP, and HTTP have automated
342
+ coverage. This release is the 2026-10-01 audit repair set — the post-v0.1.6
343
+ review follow-ups the owner accepted on 2026-10-01: a confined decoder boundary
344
+ for rooted local inputs, atomic job-attempt ownership with atomic worker startup
345
+ and cancellation finalization, DOCX completed-output identity, per-request
346
+ completed-stage reuse, an aggregate export preflight, translation completeness
347
+ and per-item batch validation, live terminal feedback, and major documentation
348
+ corrections. Implementation was verified on a native-Windows source candidate with real CLI, HTTP and MCP speech, seven-format exports and completed resume. Release publication uses the tag workflow, which requires successful exact-commit main CI before PyPI uploads and creates the public GitHub release only afterward. Check the linked release for artifacts and workflow status; local source verification is not a fresh PyPI-install or individual-harness receipt.
349
+
350
+ The v0.1.6 release was the post-v0.1.5 review repair set: security hardening
340
351
  for media acquisition and the HTTP and MCP adapters, safer subtitle wrapping and
341
352
  output-file publication, job and checkpoint storage corrections, an opt-in
342
353
  faster-whisper engine, a container example, and fail-closed release guards for
343
354
  tagged versions, README claims, and exact-commit CI.
344
355
  The v0.1.5 release added a speech-bearing self-test, full-media duration,
345
- retained word timings, optional PDF fonts, and tokenless PyPI publishing, and its
346
- live evidence is the only live evidence recorded on this page.
347
- Windows-native ROCm and a local Windows YouTube run on the v0.1.5 release commit were verified.
348
- That run's receipt records the clip, timestamps, and hashes; the checklist's
349
- recognized-speech assertion was added after it, so treat it as shape evidence for
350
- that commit rather than proof of what was said.
356
+ retained word timings, optional PDF fonts, and tokenless PyPI publishing.
357
+ Windows-native ROCm and a local Windows YouTube run on the v0.1.5 release commit
358
+ were verified. That run's receipt records the clip, timestamps, and hashes; the
359
+ checklist's recognized-speech assertion was added after it, so treat it as shape
360
+ evidence for that commit rather than proof of what was said.
361
+ The v0.1.6 release is public: the
362
+ [v0.1.6 GitHub release](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.6)
363
+ and both [core](https://pypi.org/project/textflowkit/0.1.6/) and
364
+ [fonts](https://pypi.org/project/textflowkit-fonts/0.1.6/) PyPI projects are
365
+ live — that is the previous release's evidence, not a receipt for v0.1.7.
366
+ Everything from here to the end of this section is the v0.1.6 release's
367
+ historical record as published: it is not re-verified for v0.1.7, whose separate release evidence is not supplied by these historical paragraphs.
368
+ Merged-main CI passed 16/16 on the tagged commit; the published wheel and
369
+ sdist digests match the GitHub release assets and their SHA-256 list; and a fresh
370
+ Windows Python 3.12 install of `textflowkit[export,mcp,http]==0.1.6` from PyPI
371
+ passed `doctor`, `selftest`, and a tiny CPU transcription to JSON and PDF. A
372
+ local native-Windows YouTube run on the same commit returned 3 timestamped
373
+ segments and a whole-word `elephants` match; its receipt is stored outside this
374
+ repository. One clip is not proof of the other 12 recognized platforms or of
375
+ general accuracy.
351
376
  The GitHub-hosted YouTube attempt was blocked by a bot challenge, so hosted
352
- live transcription is not verified. No v0.1.6 tag, upload, or live run is verified here.
377
+ live transcription is not verified.
353
378
  This does not imply that all 13 platforms or every harness workflow has been
354
379
  tested end to end. See the [user manual](https://github.com/scottconverse/textflowkit/blob/main/docs/user-manual.md)
355
380
  and [roadmap](https://github.com/scottconverse/textflowkit/blob/main/docs/roadmap.md).
@@ -7,7 +7,7 @@ Cross-platform media transcription toolkit. **One core, one CLI, thin adapters.*
7
7
  [GitHub releases](https://github.com/scottconverse/textflowkit/releases) ·
8
8
  [User manual](https://github.com/scottconverse/textflowkit/blob/main/docs/user-manual.md)
9
9
 
10
- **Current release: [v0.1.6](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.6).**
10
+ **Current release: [v0.1.7](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.7).**
11
11
 
12
12
  The [static site deployment](https://github.com/scottconverse/textflowkit/blob/main/docs/site-deployment.md) is hosted on Cloudflare
13
13
  Pages. GitHub remains the source and CI host; the website does not run the
@@ -83,6 +83,9 @@ the core; the interfaces are thin.
83
83
  Because the core owns the pipeline, adding a door is cheap — and adding a platform
84
84
  means writing one source adapter, not another tool.
85
85
 
86
+ For the job lifecycle, process ownership, checkpoints, and output publication,
87
+ see [docs/architecture.md](https://github.com/scottconverse/textflowkit/blob/main/docs/architecture.md).
88
+
86
89
  ## Recognized sources
87
90
 
88
91
  YouTube · TikTok · Facebook · Instagram · Vimeo · Twitch · Bilibili · Rumble ·
@@ -159,7 +162,7 @@ to preserve it.
159
162
 
160
163
  The same version's wheel and source archive are also on the
161
164
  [GitHub release page](https://github.com/scottconverse/textflowkit/releases/latest).
162
- Starting with the next release, every release published by this project's release
165
+ Starting with v0.1.6, every release published by this project's release
163
166
  workflow carries a `SHA256SUMS` asset listing the SHA-256 hash of each wheel and
164
167
  source archive it contains, so you can check a download with
165
168
  `sha256sum -c SHA256SUMS`. Releases published before that change, v0.1.5
@@ -278,21 +281,43 @@ web frontend share the job contract without blocking a request.
278
281
 
279
282
  ## Status
280
283
 
281
- **v0.1.6 release.** Core, CLI, MCP, and HTTP have automated
282
- coverage. This release is the post-v0.1.5 review repair set: security hardening
284
+ **v0.1.7 release.** Core, CLI, MCP, and HTTP have automated
285
+ coverage. This release is the 2026-10-01 audit repair set — the post-v0.1.6
286
+ review follow-ups the owner accepted on 2026-10-01: a confined decoder boundary
287
+ for rooted local inputs, atomic job-attempt ownership with atomic worker startup
288
+ and cancellation finalization, DOCX completed-output identity, per-request
289
+ completed-stage reuse, an aggregate export preflight, translation completeness
290
+ and per-item batch validation, live terminal feedback, and major documentation
291
+ corrections. Implementation was verified on a native-Windows source candidate with real CLI, HTTP and MCP speech, seven-format exports and completed resume. Release publication uses the tag workflow, which requires successful exact-commit main CI before PyPI uploads and creates the public GitHub release only afterward. Check the linked release for artifacts and workflow status; local source verification is not a fresh PyPI-install or individual-harness receipt.
292
+
293
+ The v0.1.6 release was the post-v0.1.5 review repair set: security hardening
283
294
  for media acquisition and the HTTP and MCP adapters, safer subtitle wrapping and
284
295
  output-file publication, job and checkpoint storage corrections, an opt-in
285
296
  faster-whisper engine, a container example, and fail-closed release guards for
286
297
  tagged versions, README claims, and exact-commit CI.
287
298
  The v0.1.5 release added a speech-bearing self-test, full-media duration,
288
- retained word timings, optional PDF fonts, and tokenless PyPI publishing, and its
289
- live evidence is the only live evidence recorded on this page.
290
- Windows-native ROCm and a local Windows YouTube run on the v0.1.5 release commit were verified.
291
- That run's receipt records the clip, timestamps, and hashes; the checklist's
292
- recognized-speech assertion was added after it, so treat it as shape evidence for
293
- that commit rather than proof of what was said.
299
+ retained word timings, optional PDF fonts, and tokenless PyPI publishing.
300
+ Windows-native ROCm and a local Windows YouTube run on the v0.1.5 release commit
301
+ were verified. That run's receipt records the clip, timestamps, and hashes; the
302
+ checklist's recognized-speech assertion was added after it, so treat it as shape
303
+ evidence for that commit rather than proof of what was said.
304
+ The v0.1.6 release is public: the
305
+ [v0.1.6 GitHub release](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.6)
306
+ and both [core](https://pypi.org/project/textflowkit/0.1.6/) and
307
+ [fonts](https://pypi.org/project/textflowkit-fonts/0.1.6/) PyPI projects are
308
+ live — that is the previous release's evidence, not a receipt for v0.1.7.
309
+ Everything from here to the end of this section is the v0.1.6 release's
310
+ historical record as published: it is not re-verified for v0.1.7, whose separate release evidence is not supplied by these historical paragraphs.
311
+ Merged-main CI passed 16/16 on the tagged commit; the published wheel and
312
+ sdist digests match the GitHub release assets and their SHA-256 list; and a fresh
313
+ Windows Python 3.12 install of `textflowkit[export,mcp,http]==0.1.6` from PyPI
314
+ passed `doctor`, `selftest`, and a tiny CPU transcription to JSON and PDF. A
315
+ local native-Windows YouTube run on the same commit returned 3 timestamped
316
+ segments and a whole-word `elephants` match; its receipt is stored outside this
317
+ repository. One clip is not proof of the other 12 recognized platforms or of
318
+ general accuracy.
294
319
  The GitHub-hosted YouTube attempt was blocked by a bot challenge, so hosted
295
- live transcription is not verified. No v0.1.6 tag, upload, or live run is verified here.
320
+ live transcription is not verified.
296
321
  This does not imply that all 13 platforms or every harness workflow has been
297
322
  tested end to end. See the [user manual](https://github.com/scottconverse/textflowkit/blob/main/docs/user-manual.md)
298
323
  and [roadmap](https://github.com/scottconverse/textflowkit/blob/main/docs/roadmap.md).
@@ -50,10 +50,53 @@ user accounts or multi-tenancy.
50
50
  `TEXTFLOWKIT_OUTPUT_ROOT` to confine less-trusted callers. With those roots
51
51
  set, paths outside them (including `..` and symlink escapes) are rejected.
52
52
  See `resolve_output_dir` and `resolve_input_path` in
53
- `src/textflowkit/core/paths.py`. Confined local input is copied from a
54
- path-verified open handle into isolated scratch before ffmpeg. Output paths
55
- are rechecked at file publication. Keep configured roots non-writable by
56
- untrusted local users to prevent races.
53
+ `src/textflowkit/core/paths.py`. Output paths are rechecked at file
54
+ publication. Keep configured roots non-writable by untrusted local users to
55
+ prevent races.
56
+
57
+ - **The decoder boundary for confined input.** A local file can be a
58
+ *reference* to other files rather than media itself: an HLS/M3U playlist, an
59
+ ffmpeg concat script, a DASH manifest. The demuxer opens every path such a
60
+ file names. Confined local input is handle-verified and copied into isolated
61
+ scratch, but only the top-level file is copied, so a playlist inside the root
62
+ can name a file outside it — an absolute reference survives the copy
63
+ unchanged, and a relative one resolves against the scratch directory, where
64
+ `..` walks out. When an input root is set, every confined input is therefore
65
+ decoded under FFmpeg's `-format_whitelist`, listing only the demuxers this
66
+ product supports as *self-contained* starting formats:
67
+
68
+ wav, mp3, mov,mp4,m4a,3gp,3g2,mj2, matroska,webm, ogg, flac, aac
69
+
70
+ These are FFmpeg's own demuxer names (`ffmpeg -demuxers`), not the extension
71
+ list. A file whose content is not one of them — including a playlist or
72
+ manifest however it is named — is refused by FFmpeg itself, before any
73
+ reference is followed. The same restriction is applied to the `ffprobe`
74
+ duration check, which otherwise opens the staged file the same way.
75
+
76
+ This is the guarantee, stated exactly:
77
+
78
+ - **Confined local input cannot make FFmpeg open another file.** The demuxer
79
+ that would follow a reference is not selectable, so there is no reference to
80
+ check and none is followed. This is a decoder-format restriction, not a
81
+ filesystem sandbox.
82
+ - **It applies only to confined input** (an input root set) and only to the
83
+ media decode. Without an input root there is no boundary to protect, and the
84
+ documented workflow decodes what FFmpeg already decoded.
85
+ - **It rests on the demuxer list above.** A file that this build classifies as
86
+ one of those demuxers, but which in another FFmpeg build (or through a
87
+ container's own external-data mechanism) opens other files, is not covered.
88
+ The `-format_whitelist` names are checked against `ffmpeg -demuxers`; a
89
+ build whose demuxers differ is not verified here.
90
+ - **It is not a sandbox of FFmpeg or yt-dlp at large** (see "Not defended").
91
+
92
+ - **Leading-byte manifest refusal (defense in depth).** On top of the decoder
93
+ whitelist, a confined input whose leading bytes are an HLS/M3U playlist
94
+ (`#EXT`), an MPEG-DASH manifest (`<`), or an ffmpeg concat script
95
+ (`ffconcat`) is refused before the staged copy is written, so a recognised
96
+ manifest leaves nothing behind. This is an extra layer, deliberately *not*
97
+ the boundary: it covers only the signatures in its list, and detection is by
98
+ content rather than extension. The decoder whitelist above is what holds if
99
+ this check is widened, narrowed, or removed.
57
100
 
58
101
  ### Not defended (by design — read before deploying)
59
102
 
@@ -64,10 +107,19 @@ user accounts or multi-tenancy.
64
107
  Streamable-HTTP MCP is separate and not covered by this Bearer middleware.
65
108
  - **No sandboxing of `ffmpeg` / `yt-dlp`.** They run as your user on input you
66
109
  supply. Media is untrusted data; treat a malformed file as you would any
67
- untrusted input to those tools.
110
+ untrusted input to those tools. The confined-input decoder boundary above
111
+ limits which *formats* a confined input may be, and so which files the demuxer
112
+ can open; it does not confine the decoder process, its codecs, or anything
113
+ else on the system.
68
114
  - **Cookies are passed through, not stored.** `--cookies-from-browser` hands
69
115
  browser cookies to `yt-dlp` for content you are authorised to access. Using it
70
116
  to reach content you are not authorised to access is outside the intended use.
117
+ Browser-cookie input is a **developer/owner-profile** feature: while
118
+ `TEXTFLOWKIT_PROFILE=production`, every submission carrying the option is
119
+ refused before a job is written — on the CLI, MCP, and HTTP surfaces, and when
120
+ a saved request is resumed — because the server cannot distinguish an
121
+ owner-run session from a remote caller. See the [production settings
122
+ reference](docs/adapters.md#production-settings-reference).
71
123
  - **Transcript accuracy is not a security property.** Speech-to-text output
72
124
  contains errors and must not be relied on for legal, medical, safety-critical,
73
125
  or evidentiary purposes without human review.
@@ -1,6 +1,6 @@
1
1
  # Adapters and integration
2
2
 
3
- Current published release: [v0.1.6](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.6).
3
+ Current published release: [v0.1.7](https://github.com/scottconverse/textflowkit/releases/tag/v0.1.7).
4
4
  Start with the [user manual](user-manual.md) for everyday use. Harness versions
5
5
  in the compatibility table below identify **historical test sessions**, not
6
6
  the version of TextFlowKit currently published.
@@ -95,9 +95,52 @@ Tools: `list_sources`, `transcribe_media`, `submit_batch_media`, `resume_job`,
95
95
  Read-only tools carry `readOnlyHint: true`. Submission and export tools carry
96
96
  `openWorldHint: true`; resume and cancellation are marked as mutating.
97
97
 
98
- `transcribe_media` and `submit_batch_media` take the same options as the HTTP
99
- bodies above, `engine` included; a name that is unknown, or whose optional
100
- package is missing, comes back as `{"error": ...}` before any job is queued.
98
+ The MCP tools share the **core** options with the HTTP bodies above, but the
99
+ request *shapes* differ between surfaces - they are not interchangeable
100
+ payloads. The table below is the contract; a caller must not build one from an
101
+ HTTP batch item by hand.
102
+
103
+ | Option | HTTP single (`POST /jobs`) / batch item | MCP `transcribe_media` | MCP `submit_batch_media` |
104
+ |---|---|---|---|
105
+ | Input | `source: string`, required | `source: string`, required | `sources: list[string]`, required |
106
+ | `formats` | **list** of strings (e.g. `["json","srt"]`); default `json`,`srt`,`txt` | **comma-separated string** `"json,srt,txt"` | **comma-separated string** |
107
+ | `language`, `output_dir`, `model`, `device`, `engine`, `diarize`, `translate_to` | same fields on the single item; on HTTP batch each item carries its **own** copy, applied only to that item | same | shared across all sources |
108
+ | `cookies_from_browser` | accepted on the HTTP single `POST /jobs` and on each HTTP batch item | accepted | **not a supported parameter** - callers must not supply it |
109
+ | Resume | batch wrapper `resume: bool`; single `POST /jobs/{id}/resume` | `resume_job` tool | `resume: bool` |
110
+
111
+ The MCP batch tool's `cookies_from_browser` row is the one place the surfaces
112
+ genuinely differ: `submit_batch_media`'s signature has no such parameter, so a
113
+ caller cannot pass browser cookies through it — only through `transcribe_media`
114
+ or an HTTP submission. On HTTP, batch options are **per item**: each entry of
115
+ `jobs` is a full `TranscribeRequest`, so two items can name different models or
116
+ languages. The MCP batch tool instead takes its options **once** and applies them
117
+ to every source.
118
+
119
+ `output_dir` omitted writes no rendered files, but a durable transcript is still
120
+ kept: with `TEXTFLOWKIT_DB` set the transcript survives the process, so "no
121
+ output files" is not "memory only."
122
+
123
+ On `transcribe_media` an unknown engine name or a missing optional package comes
124
+ back as `{"error": ...}` before any job is queued. On `submit_batch_media` each
125
+ source is admitted independently, so such a refusal is that source's error in
126
+ the returned `jobs` list - carrying its zero-based `index` and best-effort
127
+ `source` - and the other sources are still queued, reported in submitted order.
128
+ The MCP batch tool takes engine (and every other option) **once** for the whole
129
+ call, so an unusable engine is not a per-source refusal: it means every item
130
+ carries an admission error. The response `count` and each item's
131
+ `state`/`error` describe **admission**, not completion: a queued item still has
132
+ to be polled to a terminal state.
133
+
134
+ **`sources` entries are expected to be strings.** The tool's schema types the
135
+ argument as `list[Any]`, not `list[str]`, precisely so that a *bad entry* is that
136
+ entry's own item error rather than a whole-call schema rejection: an entry that
137
+ is not a string - a nested object, a number, or any other non-string value - is
138
+ raised as a `TypeError` inside the per-item boundary and reported at its `index`
139
+ while the remaining entries are still queued. A string is valid whether or not
140
+ it is empty-but-shaped like a source; the error is *non-string*, not
141
+ *non-object*. So the parameter is typed permissively but the contract is "one
142
+ string per source" - pass `["https://...", "C:\\media\\clip.mp4"]`, not nested
143
+ objects or numbers. A malformed entry is not a reason the whole batch fails.
101
144
 
102
145
  ### Harness configuration
103
146
 
@@ -152,19 +195,43 @@ textflowkit-http --host 127.0.0.1 --port 8767
152
195
 
153
196
  Both submission bodies (`POST /jobs` and each item of `POST /jobs/batch`) accept
154
197
  the same options: `source`, `language`, `formats`, `output_dir`, `model`,
155
- `device`, `engine`, `cookies_from_browser`, `diarize`, and `translate_to`.
198
+ `device`, `engine`, `cookies_from_browser`, `diarize`, and `translate_to`. On
199
+ `/jobs/batch` these are **per item**: each entry is its own full request, so an
200
+ item's model/language/device apply only to that item (unlike the MCP batch tool,
201
+ which takes one shared set for every source).
156
202
  `engine` defaults to `whisper` — openai-whisper on the torch stack: ROCm on AMD,
157
203
  CUDA on NVIDIA, CPU otherwise — and may be set to `faster-whisper`, the opt-in
158
204
  CTranslate2 engine for CPU and Apple Silicon that needs
159
205
  `pip install "textflowkit[faster-whisper]"`. The default is unchanged, and no
160
- speed or accuracy comparison between the engines is claimed. An unknown engine
161
- name, or a missing extra, is answered with **422 before a job record is written**;
162
- on `/jobs/batch` that refusal covers the whole request, so a fresh batch is never
163
- partly queued for one unusable engine. The choice is stored on the durable
164
- request and checked again when the job is resumed - except for a job that already
165
- finished, which is answered from its stored transcript and needs no engine. A
166
- resume refused for an unusable engine leaves the job's terminal state, error, and
167
- cancellation flag untouched, so nothing is left queued-less in `pending`.
206
+ speed or accuracy comparison between the engines is claimed. On a **single**
207
+ submission (`POST /jobs`) an unknown engine name or a missing extra is answered
208
+ with **422 before a job record is written**. On `/jobs/batch` each item is
209
+ admitted independently: an unusable engine (or any other per-item refusal - an
210
+ unsupported format, an empty source) is reported as that item's error, in place,
211
+ carrying its zero-based `index` and best-effort `source`, and the other items are
212
+ still queued and reported in the submitted order. The envelope shape is the
213
+ whole-request case: a batch body that fails `BatchRequest` — most obviously a
214
+ `jobs` value that is not a list at all, but also a request body that does not
215
+ decode to that model (a missing or non-object top-level body, or an invalid
216
+ `resume`) — is a whole-request 422. An
217
+ individual entry that is not an object (or carries a malformed field) is that
218
+ entry's item error, never a whole-request refusal. The choice is stored on the
219
+ durable request and checked again when the job is resumed - except for a job that
220
+ already finished, which is answered from its stored transcript and needs no
221
+ engine. A resume refused for an unusable engine leaves the job's terminal state,
222
+ error, and cancellation flag untouched, so nothing is left queued-less in
223
+ `pending`.
224
+
225
+ **202 means admitted, not completed.** Both `POST /jobs` and `POST /jobs/batch`
226
+ return a job handle as soon as work is queued; the response does not mean the
227
+ transcription succeeded. `/jobs/batch` returns `{count, jobs}` where each entry
228
+ carries its `index`, `source`, and either a `job_id`/`state` or an `error`, and
229
+ the entry count always matches the number of submitted items. Poll each returned
230
+ job to a terminal state (`done`, `error`, or `cancelled`) before treating it as
231
+ finished. A missing or unusable **engine** is a per-item error on the batch route
232
+ (HTTP 422 on the single route); a batch body that fails the envelope model — a
233
+ `jobs` value that is not a list, or a body missing its required shape — is the
234
+ whole-request 422 case.
168
235
 
169
236
  ### Developer mode and production profile
170
237
 
@@ -249,6 +316,48 @@ boundary described above and stands it down on `--allow-remote` /
249
316
  `TEXTFLOWKIT_ALLOW_REMOTE=1`, and should remain on loopback or behind a gateway;
250
317
  the JSON HTTP production token does not automatically secure it.
251
318
 
319
+ **Browser cookies are refused in the production profile.** `core.service`
320
+ rejects any submission carrying `cookies_from_browser` while
321
+ `TEXTFLOWKIT_PROFILE=production`, before a job record is written. This applies to
322
+ **every** surface that reaches the shared submission contract - a single or
323
+ batch HTTP submission, an MCP submission, a CLI run, and the **resume** of a
324
+ request saved earlier with the option - because the server cannot tell an
325
+ owner-run CLI from a remote caller. Keep cookie-assisted jobs in the deliberate
326
+ owner developer profile.
327
+
328
+ #### Production settings reference
329
+
330
+ Every numeric limit below is read by `core.service` at request time and must be a
331
+ positive integer; a bad value fails the request closed rather than being ignored.
332
+ The **five** "required" settings - `TEXTFLOWKIT_API_TOKEN`, `TEXTFLOWKIT_INPUT_ROOT`,
333
+ `TEXTFLOWKIT_OUTPUT_ROOT`, `TEXTFLOWKIT_WORK_ROOT`, and `TEXTFLOWKIT_DB` - must be
334
+ present (see the checks in `validate_production_config`) or the profile refuses to
335
+ serve at all.
336
+
337
+ | Environment variable | Default / requirement | Applies to |
338
+ |---|---|---|
339
+ | `TEXTFLOWKIT_PROFILE` | `developer`; set to `production` explicitly | Shared profile checks; JSON HTTP auth middleware |
340
+ | `TEXTFLOWKIT_API_TOKEN` | **required**, ≥ 16 characters | JSON HTTP, every request including `/health` |
341
+ | `TEXTFLOWKIT_INPUT_ROOT` | **required**, must be an existing directory | Local-input boundary |
342
+ | `TEXTFLOWKIT_OUTPUT_ROOT` | **required**, created if absent | Rendered-output boundary |
343
+ | `TEXTFLOWKIT_WORK_ROOT` | **required**, created if absent | Per-job scratch directory |
344
+ | `TEXTFLOWKIT_DB` | **required**, on-disk SQLite (not `:memory:`) | Durable jobs; one owning process |
345
+ | `TEXTFLOWKIT_MAX_REQUEST_BYTES` | 65,536 bytes (64 KiB) | Request body cap |
346
+ | `TEXTFLOWKIT_RATE_PER_MINUTE` | 60 per client per process | Per-process rate limit, not a distributed quota |
347
+ | `TEXTFLOWKIT_MAX_DURATION_SECONDS` | 14,400 s (4 hours) | Source and decoded duration |
348
+ | `TEXTFLOWKIT_MAX_OUTPUT_BYTES` | 52,428,800 bytes (50 MiB) | Rendered output and read response |
349
+ | `TEXTFLOWKIT_MAX_MEDIA_BYTES` | 1,073,741,824 bytes (1 GiB) | Download, media, and decoded-audio size |
350
+ | `TEXTFLOWKIT_MAX_PENDING_JOBS` | 100 | Executor pending queue capacity |
351
+ | `TEXTFLOWKIT_MAX_CONCURRENCY` | 1 | Executor worker count |
352
+ | `TEXTFLOWKIT_EGRESS_PROXY` | **required for URL input**; absent means URL jobs fail closed | Operator SSRF-filtering egress |
353
+ | `TEXTFLOWKIT_TRUSTED_PROXY_IPS` | unset = the TCP peer is the identity | Comma-separated IPs/CIDRs; a malformed value fails closed (503) |
354
+ | `TEXTFLOWKIT_FFMPEG_TIMEOUT_SECONDS` | 600 s | Decode wall-clock; **all profiles**, not production-only |
355
+
356
+ Two production read caps are numeric but are not environment variables: HTTP
357
+ transcript paging defaults to `limit=100` and rejects `limit > 500` (HTTP 422),
358
+ and HTTP search caps `limit` at 500 and `context` at 20. These are HTTP checks;
359
+ MCP is a separate surface and does not share this token or these caps.
360
+
252
361
  ### Container example (Dockerfile and Compose)
253
362
 
254
363
  The repository root carries a `Dockerfile` and a `compose.yaml` that run this
@@ -400,10 +509,16 @@ TEXTFLOWKIT_DB=/var/lib/textflowkit/jobs.db textflowkit-mcp --transport http
400
509
 
401
510
  Backed by SQLite (WAL). Transcripts, outputs, and job states survive a restart.
402
511
 
403
- **Orphaned work is reaped at startup.** A job left in `pending` or `running` by a
404
- previous process has no worker, so it is failed with a reason rather than reported
405
- as a job that will never finish. This assumes **one owning process per store** -
406
- two processes sharing one `TEXTFLOWKIT_DB` would reap each other's live jobs.
512
+ **Orphaned work is reaped before reads are served.** A job left in `pending` or
513
+ `running` by a previous process has no worker, so it is failed with a reason
514
+ rather than reported as a job that will never finish. The reap runs once per
515
+ store from each server's startup lifespan (HTTP and Streamable-HTTP MCP both
516
+ enter it) and, on the lazy path, the first time the executor starts from
517
+ submission/enqueue - so a restarted server that only answers status/`/health`
518
+ requests after the reap still reports the orphaned rows correctly. This assumes
519
+ **one owning process per store** - two processes sharing one `TEXTFLOWKIT_DB`
520
+ would reap each other's live jobs. See the [architecture guide](architecture.md)
521
+ for the full lifecycle, ownership, and checkpoint map.
407
522
 
408
523
  ## Concurrency
409
524
 
@@ -578,10 +693,26 @@ If the transcript was translated, the optional word timings still refer to the
578
693
  also retains the original word timings.
579
694
 
580
695
  Filtering is time first, then offset/limit inside that window - `offset` counts
581
- from the start of the requested range, not the start of the transcript. The
582
- response reports `total_segments`, `returned`, and `has_more`, and when more
583
- remain it includes a `next` hint naming the offset to continue from. Truncation
584
- is never silent.
696
+ from the start of the requested range, not the start of the transcript. Time
697
+ selection keeps whole segments whose intervals **overlap** the inclusive window;
698
+ it never trims a segment or a subtitle cue to the exact `start`/`end` seconds.
699
+ `offset` counts **visible** segments inside the window, after hidden filtering.
700
+
701
+ A slice reports `total_segments` (visible segments in the window), `returned`,
702
+ and `has_more`. **What carries the "keep going" information differs by surface:**
703
+
704
+ - **MCP** (`get_transcript`) adds a human-readable `next` string when more
705
+ segments remain.
706
+ - **HTTP JSON** (`format=json`) returns `{job_id, total_segments, offset,
707
+ returned, has_more, start, end, transcript}` but **no `next` field**; a client
708
+ continues by requesting `offset + returned` within the same `start`/`end`.
709
+ - **HTTP text** (`format=txt|srt|vtt|md`) returns the rendered text as the
710
+ response body with **no paging metadata at all**. Discover page boundaries with
711
+ `format=json` first, then fetch the text-format slice.
712
+
713
+ So counts are never silent, but only MCP supplies a `next` instruction: an HTTP
714
+ client that waits for one will stall, and one that ignores `has_more` will drop
715
+ the remainder.
585
716
 
586
717
  ```jsonc
587
718
  // MCP
@@ -592,6 +723,7 @@ is never silent.
592
723
  ```bash
593
724
  # HTTP
594
725
  curl "http://127.0.0.1:8767/jobs/$ID/transcript?format=srt&start=300&end=320"
726
+ curl "http://127.0.0.1:8767/jobs/$ID/transcript?format=json&offset=20&limit=20"
595
727
  curl "http://127.0.0.1:8767/jobs/$ID/search?q=neural%20network&context=1"
596
728
  ```
597
729
 
@@ -617,6 +749,31 @@ account:
617
749
  which is the point: an agent running on your behalf can reach your files.
618
750
  - Once `TEXTFLOWKIT_INPUT_ROOT` is set, paths outside that root, including
619
751
  `..` and symlink escapes, are rejected before the file is read.
752
+ - A confined input must be **self-contained media**. With an input root set, the
753
+ decode is restricted to FFmpeg's self-contained starting formats — `wav`, `mp3`,
754
+ `mov`/`mp4`/`m4a`, `matroska`/`webm`, `ogg`, `flac`, `aac` (FFmpeg's demuxer
755
+ names, see `ffmpeg -demuxers`) — via `-format_whitelist`. A file that is
756
+ instead a playlist or manifest (HLS/M3U, MPEG-DASH, an ffmpeg concat script)
757
+ is refused by FFmpeg, even when it is inside the root: it names other files
758
+ that the decoder would open, and those references cannot be checked against
759
+ the root once the input has been copied into scratch. Detection is not by
760
+ extension — renaming the file does not change it. **Both** decode tools are
761
+ restricted this way: FFmpeg's decode and the `ffprobe` duration check carry the
762
+ same self-contained demuxer whitelist. The restriction holds on a **resume**
763
+ whose reacquired audio is decoded again, because the resume path stages the
764
+ confined input and applies the same whitelist; a stage whose output is already
765
+ recorded durable is reused and never re-decoded at all.
766
+
767
+ **What this is not.** The whitelist bounds what a confined input's *decode* can
768
+ open; it is not an OS sandbox, and it does not make an untrusted root safe to
769
+ point at. The root's own directories must still be kept from untrusted local
770
+ mutation, and the process keeps the full access of the account that started it
771
+ outside the decode. With no input root set there is no boundary at all: the
772
+ owner's own runs (CLI, MCP, HTTP under the developer profile) are deliberately
773
+ unconfined and decode playlists, manifests, and anything else FFmpeg already
774
+ decoded, exactly as before. Nothing here is a claim of complete URL-egress
775
+ protection; that belongs to the production egress-proxy requirement below, and
776
+ a local-file job needs no egress.
620
777
 
621
778
  ```bash
622
779
  TEXTFLOWKIT_INPUT_ROOT=/srv/media textflowkit-mcp --transport http