textflowkit 0.1.4__tar.gz → 0.1.5__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 (94) hide show
  1. {textflowkit-0.1.4 → textflowkit-0.1.5}/.gitattributes +22 -22
  2. {textflowkit-0.1.4 → textflowkit-0.1.5}/.github/workflows/ci.yml +4 -2
  3. textflowkit-0.1.5/.github/workflows/publish-pypi.yml +114 -0
  4. {textflowkit-0.1.4 → textflowkit-0.1.5}/.gitignore +1 -1
  5. {textflowkit-0.1.4 → textflowkit-0.1.5}/.pre-commit-config.yaml +1 -1
  6. {textflowkit-0.1.4 → textflowkit-0.1.5}/CONTRIBUTING.md +4 -1
  7. {textflowkit-0.1.4 → textflowkit-0.1.5}/LEGAL.md +1 -1
  8. {textflowkit-0.1.4 → textflowkit-0.1.5}/LICENSE +1 -1
  9. {textflowkit-0.1.4 → textflowkit-0.1.5}/PKG-INFO +48 -2
  10. {textflowkit-0.1.4 → textflowkit-0.1.5}/README.md +41 -0
  11. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/adapters.md +10 -1
  12. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/install.md +3 -2
  13. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/release-checklist.md +30 -7
  14. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/roadmap.md +11 -0
  15. {textflowkit-0.1.4 → textflowkit-0.1.5}/pyproject.toml +10 -3
  16. {textflowkit-0.1.4 → textflowkit-0.1.5}/scripts/smoke_installed_wheel.py +34 -3
  17. textflowkit-0.1.5/src/textflowkit/__init__.py +8 -0
  18. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/adapters/__init__.py +1 -1
  19. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/adapters/http_server.py +5 -4
  20. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/adapters/mcp_server.py +6 -2
  21. textflowkit-0.1.5/src/textflowkit/assets/README.md +8 -0
  22. textflowkit-0.1.5/src/textflowkit/assets/selftest-speech.wav +0 -0
  23. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/cli.py +19 -26
  24. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/__init__.py +1 -1
  25. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/cancel.py +19 -19
  26. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/engine.py +12 -4
  27. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/model.py +34 -7
  28. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/pipeline.py +13 -1
  29. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/submission.py +3 -1
  30. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/timeutil.py +2 -2
  31. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/__init__.py +15 -0
  32. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/docx.py +71 -71
  33. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/markdown.py +1 -1
  34. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/pdf.py +9 -5
  35. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/srt.py +1 -1
  36. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/txt.py +1 -1
  37. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/vtt.py +1 -1
  38. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/__init__.py +1 -1
  39. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/detect.py +200 -200
  40. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/ollama_stub.py +110 -110
  41. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_adapters.py +57 -0
  42. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_batch.py +1 -1
  43. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_engine_cache.py +21 -0
  44. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_export.py +11 -0
  45. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_jobs.py +1 -1
  46. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_model.py +11 -2
  47. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_pipeline_integrity.py +48 -3
  48. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_release_boundaries.py +17 -0
  49. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_render.py +99 -99
  50. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_sources.py +2 -2
  51. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_stdio_protocol.py +213 -213
  52. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_submission.py +26 -0
  53. textflowkit-0.1.4/src/textflowkit/__init__.py +0 -8
  54. textflowkit-0.1.4/src/textflowkit/render/fonts/NotoSans.ttf +0 -0
  55. textflowkit-0.1.4/src/textflowkit/render/fonts/NotoSansArabic.ttf +0 -0
  56. textflowkit-0.1.4/src/textflowkit/render/fonts/NotoSansSC.ttf +0 -0
  57. textflowkit-0.1.4/src/textflowkit/render/fonts/OFL-NotoSans.txt +0 -94
  58. textflowkit-0.1.4/src/textflowkit/render/fonts/OFL-NotoSansSC.txt +0 -93
  59. textflowkit-0.1.4/src/textflowkit/render/fonts/README.md +0 -19
  60. {textflowkit-0.1.4 → textflowkit-0.1.5}/.github/dependabot.yml +0 -0
  61. {textflowkit-0.1.4 → textflowkit-0.1.5}/SECURITY.md +0 -0
  62. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/.nojekyll +0 -0
  63. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/index.html +0 -0
  64. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/site-deployment.md +0 -0
  65. {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/sources.md +0 -0
  66. {textflowkit-0.1.4 → textflowkit-0.1.5}/scripts/smoke_live_youtube.py +0 -0
  67. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/batch.py +0 -0
  68. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/bind.py +0 -0
  69. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/checkpoint.py +0 -0
  70. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/diarize.py +0 -0
  71. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/executor.py +0 -0
  72. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/jobs.py +0 -0
  73. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/paths.py +0 -0
  74. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/retrieval.py +0 -0
  75. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/runner.py +0 -0
  76. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/service.py +0 -0
  77. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/sqlite_store.py +0 -0
  78. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/translate.py +0 -0
  79. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/acquire.py +0 -0
  80. {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/scratch.py +0 -0
  81. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/__init__.py +0 -0
  82. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_acquire.py +0 -0
  83. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_bind.py +0 -0
  84. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_checkpoint.py +0 -0
  85. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_cli.py +0 -0
  86. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_cli_resume_batch.py +0 -0
  87. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_diarize.py +0 -0
  88. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_executor.py +0 -0
  89. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_landing_page.py +0 -0
  90. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_live_smoke_script.py +0 -0
  91. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_paths.py +0 -0
  92. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_retrieval.py +0 -0
  93. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_service_profile.py +0 -0
  94. {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_translate.py +0 -0
@@ -1,22 +1,22 @@
1
- # Normalise line endings so a build is the same on every platform.
2
- #
3
- # Without this, core.autocrlf rewrites text files on Windows checkout, so a
4
- # wheel built from a Windows clone differs byte-for-byte from one built on
5
- # Linux - same code, different bytes - and a published hash cannot be
6
- # reproduced. Text is stored as LF in the repository and in every build.
7
- * text=auto eol=lf
8
-
9
- # Binary assets must never be rewritten.
10
- *.png binary
11
- *.jpg binary
12
- *.jpeg binary
13
- *.gif binary
14
- *.ico binary
15
- *.pdf binary
16
- *.docx binary
17
- *.whl binary
18
- *.gz binary
19
-
20
- # Windows-only scripts keep CRLF.
21
- *.bat text eol=crlf
22
- *.cmd text eol=crlf
1
+ # Normalise line endings so a build is the same on every platform.
2
+ #
3
+ # Without this, core.autocrlf rewrites text files on Windows checkout, so a
4
+ # wheel built from a Windows clone differs byte-for-byte from one built on
5
+ # Linux - same code, different bytes - and a published hash cannot be
6
+ # reproduced. Text is stored as LF in the repository and in every build.
7
+ * text=auto eol=lf
8
+
9
+ # Binary assets must never be rewritten.
10
+ *.png binary
11
+ *.jpg binary
12
+ *.jpeg binary
13
+ *.gif binary
14
+ *.ico binary
15
+ *.pdf binary
16
+ *.docx binary
17
+ *.whl binary
18
+ *.gz binary
19
+
20
+ # Windows-only scripts keep CRLF.
21
+ *.bat text eol=crlf
22
+ *.cmd text eol=crlf
@@ -57,7 +57,8 @@ jobs:
57
57
  shell: bash
58
58
  run: |
59
59
  python -m pip install --upgrade pip
60
- python -m pip install -e ".[dev,mcp,http]"
60
+ python -m pip install ./packages/textflowkit-fonts
61
+ python -m pip install -e ".[dev,mcp,http,export]"
61
62
 
62
63
  - name: Show ffmpeg version
63
64
  shell: bash
@@ -84,6 +85,7 @@ jobs:
84
85
  run: |
85
86
  python -m pip install "setuptools>=83" "build>=1.2"
86
87
  python -m build --wheel
88
+ python -m build --wheel --outdir dist packages/textflowkit-fonts
87
89
  - name: Smoke all three installed entry points
88
90
  shell: bash
89
- run: python scripts/smoke_installed_wheel.py dist/*.whl
91
+ run: python scripts/smoke_installed_wheel.py dist/textflowkit-*.whl dist/textflowkit_fonts-*.whl
@@ -0,0 +1,114 @@
1
+ name: Publish PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ if: github.repository == 'scottconverse/textflowkit'
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
16
+ with:
17
+ persist-credentials: false
18
+ fetch-depth: 0
19
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
20
+ with:
21
+ python-version: '3.12'
22
+ - run: python -m pip install packaging
23
+ - name: Verify release tag and package versions
24
+ env:
25
+ RELEASE_TAG: ${{ github.ref_name }}
26
+ run: |
27
+ python - <<'PY'
28
+ import os, tomllib
29
+ from pathlib import Path
30
+ from packaging.version import Version
31
+
32
+ tag = os.environ['RELEASE_TAG']
33
+ if not tag.startswith('v'):
34
+ raise SystemExit('Release tag must start with v')
35
+ version = str(Version(tag[1:]))
36
+ if tag != 'v' + version:
37
+ raise SystemExit('Release tag is not a normalized final version')
38
+ if Version(version).is_prerelease or Version(version).is_devrelease:
39
+ raise SystemExit('PyPI release must be a final version')
40
+ for file in (Path('pyproject.toml'), Path('packages/textflowkit-fonts/pyproject.toml')):
41
+ declared = tomllib.loads(file.read_text(encoding='utf-8'))['project']['version']
42
+ if declared != version:
43
+ raise SystemExit(f'{file}: {declared} != {version}')
44
+ PY
45
+ git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
46
+ echo 'Release tag is not merged into main' >&2
47
+ exit 1
48
+ }
49
+ - name: Build distributions
50
+ run: |
51
+ python -m pip install --upgrade build packaging
52
+ python -m build --sdist --wheel --outdir dist/main .
53
+ python -m build --sdist --wheel --outdir dist/fonts packages/textflowkit-fonts
54
+ python -m pip install twine
55
+ python -m twine check dist/main/* dist/fonts/*
56
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
57
+ with:
58
+ name: distributions
59
+ path: dist/
60
+ if-no-files-found: error
61
+
62
+ publish-fonts:
63
+ needs: build
64
+ runs-on: ubuntu-latest
65
+ environment: pypi
66
+ permissions:
67
+ id-token: write
68
+ steps:
69
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
70
+ with:
71
+ name: distributions
72
+ path: dist/
73
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
74
+ with:
75
+ packages-dir: dist/fonts/
76
+
77
+ publish-main:
78
+ needs: publish-fonts
79
+ runs-on: ubuntu-latest
80
+ environment: pypi
81
+ permissions:
82
+ id-token: write
83
+ steps:
84
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
85
+ with:
86
+ name: distributions
87
+ path: dist/
88
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
89
+ with:
90
+ packages-dir: dist/main/
91
+
92
+ publish-github-release:
93
+ needs: publish-main
94
+ runs-on: ubuntu-latest
95
+ permissions:
96
+ contents: write
97
+ steps:
98
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
99
+ with:
100
+ name: distributions
101
+ path: dist/
102
+ - name: Create draft with the exact PyPI distributions
103
+ env:
104
+ GH_TOKEN: ${{ github.token }}
105
+ RELEASE_TAG: ${{ github.ref_name }}
106
+ run: |
107
+ gh release create "$RELEASE_TAG" dist/main/* dist/fonts/* \
108
+ --repo "$GITHUB_REPOSITORY" --verify-tag --draft \
109
+ --title "$RELEASE_TAG" --generate-notes
110
+ - name: Publish complete GitHub release
111
+ env:
112
+ GH_TOKEN: ${{ github.token }}
113
+ RELEASE_TAG: ${{ github.ref_name }}
114
+ run: gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false
@@ -17,6 +17,7 @@ work/
17
17
  out/
18
18
  outputs/
19
19
  *.wav
20
+ !src/textflowkit/assets/selftest-speech.wav
20
21
  *.mp3
21
22
  *.m4a
22
23
  *.mp4
@@ -34,4 +35,3 @@ Thumbs.db
34
35
  .vscode/
35
36
  .idea/
36
37
  board/
37
-
@@ -15,4 +15,4 @@ repos:
15
15
  - id: check-yaml
16
16
  - id: check-toml
17
17
  - id: check-added-large-files
18
- args: [--maxkb=1024]
18
+ args: [--maxkb=1024]
@@ -13,7 +13,8 @@ cd textflowkit
13
13
  python -m venv .venv
14
14
  # Windows: .venv\Scripts\activate
15
15
  # POSIX: source .venv/bin/activate
16
- pip install -e ".[dev,mcp,http]"
16
+ pip install ./packages/textflowkit-fonts # local optional PDF font companion
17
+ pip install -e ".[dev,mcp,http,export]"
17
18
  ```
18
19
 
19
20
  ## Test and lint — the commands CI runs
@@ -60,6 +61,8 @@ mark it so it can be skipped in CI.
60
61
  - Add a test for a bug fix. If you cannot, say why in the PR description.
61
62
  - Run `ruff check .` and `python -m pytest` before opening the PR.
62
63
  - Do not commit media files, transcripts, or scratch output. `work/` is gitignored.
64
+ The bundled `src/textflowkit/assets/selftest-speech.wav` is an intentional,
65
+ small synthetic speech fixture; do not replace it with private recordings.
63
66
 
64
67
  ## Evidence language
65
68
 
@@ -41,4 +41,4 @@ outside the intended use of this project.
41
41
 
42
42
  "textflowkit" is an independent open-source project. It is not affiliated with,
43
43
  endorsed by, or connected to any other product or organisation using a similar
44
- name.
44
+ name.
@@ -200,4 +200,4 @@
200
200
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
201
  See the License for the specific language governing permissions and
202
202
  limitations under the License.
203
-
203
+
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: textflowkit
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Cross-platform media transcription toolkit: URL or file in, timestamped transcripts and subtitle files out.
5
- Project-URL: Homepage, https://github.com/scottconverse/textflowkit
5
+ Project-URL: Homepage, https://www.textflowkit.org/
6
6
  Project-URL: Issues, https://github.com/scottconverse/textflowkit/issues
7
+ Project-URL: Source, https://github.com/scottconverse/textflowkit
8
+ Project-URL: Documentation, https://github.com/scottconverse/textflowkit/tree/main/docs
9
+ Project-URL: Changelog, https://github.com/scottconverse/textflowkit/releases
7
10
  License-Expression: Apache-2.0
8
11
  License-File: LICENSE
9
12
  Keywords: speech-to-text,srt,subtitles,transcription,vtt,whisper,yt-dlp
@@ -27,6 +30,7 @@ Requires-Dist: pyannote-audio>=4.0; extra == 'all'
27
30
  Requires-Dist: pydantic>=2.7; extra == 'all'
28
31
  Requires-Dist: python-docx>=1.1; extra == 'all'
29
32
  Requires-Dist: reportlab>=4.0; extra == 'all'
33
+ Requires-Dist: textflowkit-fonts<0.2,>=0.1.5; extra == 'all'
30
34
  Requires-Dist: uvicorn>=0.30; extra == 'all'
31
35
  Provides-Extra: dev
32
36
  Requires-Dist: pypdf>=5; extra == 'dev'
@@ -38,6 +42,7 @@ Requires-Dist: pyannote-audio>=4.0; extra == 'diarize'
38
42
  Provides-Extra: export
39
43
  Requires-Dist: python-docx>=1.1; extra == 'export'
40
44
  Requires-Dist: reportlab>=4.0; extra == 'export'
45
+ Requires-Dist: textflowkit-fonts<0.2,>=0.1.5; extra == 'export'
41
46
  Provides-Extra: http
42
47
  Requires-Dist: fastapi>=0.115; extra == 'http'
43
48
  Requires-Dist: pydantic>=2.7; extra == 'http'
@@ -139,6 +144,47 @@ python -m pip install 'textflowkit[mcp]' # MCP server
139
144
  python -m pip install 'textflowkit[http]' # JSON HTTP API
140
145
  ```
141
146
 
147
+ PDF/DOCX export is optional. The default wheel stays small; the `export` extra
148
+ installs `textflowkit-fonts` for offline multilingual PDF rendering:
149
+
150
+ ```bash
151
+ python -m pip install 'textflowkit[export]'
152
+ ```
153
+
154
+ ## Python API
155
+
156
+ The same pipeline used by the CLI and adapters is available to Python callers:
157
+
158
+ ```python
159
+ from textflowkit import transcribe
160
+
161
+ result = transcribe(
162
+ "meeting.mp4", # also accepts supported URLs
163
+ model="small",
164
+ formats=["json", "srt", "txt"],
165
+ output_dir="transcripts", # omit to return the transcript without writing files
166
+ )
167
+ print(result.transcript.text)
168
+ print(result.transcript.duration) # full media duration in seconds
169
+ print(result.outputs) # pathlib.Path objects for written files
170
+ for segment in result.transcript.segments:
171
+ print(segment.start, segment.end, segment.speaker, segment.text)
172
+ for word in segment.words:
173
+ print(" ", word.start, word.end, word.text)
174
+ ```
175
+
176
+ `transcribe()` returns `TranscribeResult` with a canonical `Transcript` and
177
+ written output paths. `Transcript.to_dict()` / `.to_json()` preserve segment and
178
+ word timing; older transcript JSON without `words` remains readable. Pass
179
+ `input_root=` to confine local input paths for untrusted callers. See
180
+ [the install guide](https://github.com/scottconverse/textflowkit/blob/main/docs/install.md)
181
+ for ffmpeg and Windows ROCm setup.
182
+
183
+ MCP and HTTP transcript reads omit word timings by default to keep responses
184
+ small; set `include_words=true` on a JSON read to receive them. Saved files,
185
+ Python results, and durable job records still retain the source-language words,
186
+ including when segment text has been translated.
187
+
142
188
  **AMD ROCm on native Windows:** do not use the generic command in an environment
143
189
  with a working ROCm PyTorch install. Ordinary dependency resolution can replace
144
190
  that torch build. Follow the [ROCm install notes](https://github.com/scottconverse/textflowkit/blob/main/docs/install.md)
@@ -91,6 +91,47 @@ python -m pip install 'textflowkit[mcp]' # MCP server
91
91
  python -m pip install 'textflowkit[http]' # JSON HTTP API
92
92
  ```
93
93
 
94
+ PDF/DOCX export is optional. The default wheel stays small; the `export` extra
95
+ installs `textflowkit-fonts` for offline multilingual PDF rendering:
96
+
97
+ ```bash
98
+ python -m pip install 'textflowkit[export]'
99
+ ```
100
+
101
+ ## Python API
102
+
103
+ The same pipeline used by the CLI and adapters is available to Python callers:
104
+
105
+ ```python
106
+ from textflowkit import transcribe
107
+
108
+ result = transcribe(
109
+ "meeting.mp4", # also accepts supported URLs
110
+ model="small",
111
+ formats=["json", "srt", "txt"],
112
+ output_dir="transcripts", # omit to return the transcript without writing files
113
+ )
114
+ print(result.transcript.text)
115
+ print(result.transcript.duration) # full media duration in seconds
116
+ print(result.outputs) # pathlib.Path objects for written files
117
+ for segment in result.transcript.segments:
118
+ print(segment.start, segment.end, segment.speaker, segment.text)
119
+ for word in segment.words:
120
+ print(" ", word.start, word.end, word.text)
121
+ ```
122
+
123
+ `transcribe()` returns `TranscribeResult` with a canonical `Transcript` and
124
+ written output paths. `Transcript.to_dict()` / `.to_json()` preserve segment and
125
+ word timing; older transcript JSON without `words` remains readable. Pass
126
+ `input_root=` to confine local input paths for untrusted callers. See
127
+ [the install guide](https://github.com/scottconverse/textflowkit/blob/main/docs/install.md)
128
+ for ffmpeg and Windows ROCm setup.
129
+
130
+ MCP and HTTP transcript reads omit word timings by default to keep responses
131
+ small; set `include_words=true` on a JSON read to receive them. Saved files,
132
+ Python results, and durable job records still retain the source-language words,
133
+ including when segment text has been translated.
134
+
94
135
  **AMD ROCm on native Windows:** do not use the generic command in an environment
95
136
  with a working ROCm PyTorch install. Ordinary dependency resolution can replace
96
137
  that torch build. Follow the [ROCm install notes](https://github.com/scottconverse/textflowkit/blob/main/docs/install.md)
@@ -136,7 +136,7 @@ textflowkit-http --host 127.0.0.1 --port 8767
136
136
  | POST | `/jobs/{id}/resume` | resume a saved durable request/checkpoint |
137
137
  | GET | `/jobs` | list recent jobs |
138
138
  | GET | `/jobs/{id}` | job status |
139
- | GET | `/jobs/{id}/transcript?format=&offset=&limit=&start=&end=` | rendered transcript, optionally sliced |
139
+ | GET | `/jobs/{id}/transcript?format=&offset=&limit=&start=&end=&include_words=` | rendered transcript, optionally sliced; word timings are opt-in for JSON |
140
140
  | GET | `/jobs/{id}/search?q=&limit=&context=` | search a transcript |
141
141
  | POST | `/jobs/{id}/export?formats=docx&formats=pdf` | write files to disk (docx/pdf included) |
142
142
  | POST | `/jobs/{id}/cancel` | request cancellation |
@@ -338,6 +338,15 @@ question being asked.
338
338
  | `offset` | skip this many segments **within the selected range** |
339
339
  | `limit` | return at most this many |
340
340
  | `start` / `end` | restrict by time in seconds (inclusive) |
341
+ | `include_words` | include source-language word timings in JSON; default `false` on MCP and HTTP reads |
342
+
343
+ Saved JSON, SQLite job records, and resume checkpoints retain word timings even
344
+ when `include_words=false`; the option reduces response size, not storage size.
345
+ In one short reviewer sample, transcript JSON grew from 508 to 1,641 bytes
346
+ (roughly threefold); the multiplier varies with segment and word counts.
347
+ If the transcript was translated, the optional word timings still refer to the
348
+ **original spoken language**, not the translated segment text. The Python API
349
+ also retains the original word timings.
341
350
 
342
351
  Filtering is time first, then offset/limit inside that window - `offset` counts
343
352
  from the start of the requested range, not the start of the transcript. The
@@ -133,7 +133,9 @@ textflowkit selftest --skip-transcribe # compute device only, no model downloa
133
133
  ```
134
134
 
135
135
  It runs a real matmul on the selected device and then a real Whisper pass over a
136
- generated probe clip, printing PASS/FAIL for each and naming the torch build. A
136
+ bundled synthetic speech clip. It fails if Whisper returns no nonempty, timed
137
+ speech segment. A PASS proves text generation, not transcription accuracy.
138
+ It prints PASS/FAIL for each stage and names the torch build. A
137
139
  ROCm install reports `torch <ver>+rocm*` and the device name; a stock CPU wheel
138
140
  reports plain `torch <ver>`.
139
141
 
@@ -235,4 +237,3 @@ how many segments were labelled. If the backend or token is missing, the run
235
237
 
236
238
  With no GPU, the engine selects CPU automatically. Pass `--device cpu` to force it.
237
239
  CPU transcription is dramatically slower; prefer a smaller `--model`.
238
-
@@ -51,13 +51,36 @@ receipt. It does not upload cookies, media, or transcripts to GitHub.
51
51
 
52
52
  ## PyPI publication
53
53
 
54
- If this release is also published to PyPI, upload the **same** verified wheel
55
- and source archive as the GitHub release. Check package metadata before upload,
56
- then compare both PyPI SHA-256 digests with the GitHub release assets. Finally,
57
- install that exact version from the public PyPI index in a clean environment
58
- and smoke the CLI entry point. An install with `--no-deps` proves distribution
59
- and entry-point wiring only; it does not prove a full transcription run.
54
+ After merged-main CI and the local release checks pass, push an annotated final
55
+ version tag (for example `v0.1.5`) on that verified `main` commit. Do **not**
56
+ create or publish a GitHub release manually. The tag push triggers
57
+ [`publish-pypi.yml`](../.github/workflows/publish-pypi.yml). It verifies the
58
+ tag, both package versions, and ancestry on `main`, builds the four artifacts,
59
+ publishes `textflowkit-fonts` first and `textflowkit` second using PyPI Trusted
60
+ Publishing, then creates a **draft** GitHub release with those exact artifacts.
61
+ Only after the assets are attached does it make that release public.
62
+ No upload token is passed to CI. Both PyPI projects must have trusted publishers
63
+ for owner `scottconverse`, repository `textflowkit`, workflow
64
+ `publish-pypi.yml`, and environment `pypi`. The GitHub `pypi` environment
65
+ should require maintainer approval before an upload job can proceed.
66
+
67
+ This is not a transaction across two PyPI projects and GitHub. If fonts upload
68
+ succeeds but core fails, **there is no public GitHub release**, but fonts are
69
+ already on PyPI. Check whether any core files reached PyPI before retrying:
70
+ PyPI will reject duplicate filenames, and this workflow intentionally does not
71
+ silently skip them. Correct the cause and rerun only failed jobs when safe; if
72
+ some core files are present, reconcile their hashes against the original build
73
+ artifact and finish the missing files deliberately. If both PyPI uploads succeed
74
+ but draft creation or publishing fails, do not republish either PyPI project;
75
+ complete the GitHub draft and assets from the exact build artifact after hash
76
+ verification. Do not move the published version tag or claim a complete release
77
+ until all four PyPI files and all four GitHub assets match.
78
+
79
+ After publication, compare PyPI SHA-256 digests against the GitHub release assets
80
+ for **both** packages, and clean-install the exact version with
81
+ `textflowkit[export,mcp,http]`. Run `doctor`, `selftest`, and an actual PDF export.
82
+ An install with `--no-deps` only proves distribution wiring, not transcription.
60
83
 
61
84
  Never place an API token in this repository, a CI log, or a shell command line.
62
85
  An upload is a separate public release action; passing CI alone does not
63
- authorize it.
86
+ authorize it. Do not call a release complete if either package upload failed.
@@ -73,6 +73,17 @@ transcription service.
73
73
  - [x] Prepare v0.1.4 with a PyPI-first package description and install guide
74
74
  - [x] Make PyPI the default developer install path while keeping GitHub
75
75
  releases and the native-Windows ROCm dependency instructions
76
+ - [ ] Ship v0.1.5 review follow-ups: speech-bearing self-test, full media
77
+ duration, retained word timings, Python API documentation, richer PyPI
78
+ project links, a small core wheel with optional offline fonts, and a
79
+ tokenless Trusted Publishing release workflow for both packages
80
+ - [ ] Follow-up: normalize exported file permissions on POSIX to respect the
81
+ process umask. Current atomic temporary files can leave outputs mode `0600`,
82
+ preventing another account (such as a separate web server user) from reading
83
+ exported subtitles or documents. Fix and regression-test separately.
84
+ - [ ] Follow-up: decouple the fonts package's version from core releases so
85
+ unchanged font wheels are not rebuilt/uploaded every patch. Keep duplicate
86
+ PyPI uploads visible rather than relying on `skip-existing`.
76
87
 
77
88
  The 13 listed media platforms are recognised through `yt-dlp`; **only YouTube**
78
89
  has an opt-in [live URL transcription release gate](release-checklist.md), not
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "textflowkit"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Cross-platform media transcription toolkit: URL or file in, timestamped transcripts and subtitle files out."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -36,7 +36,7 @@ http = ["fastapi>=0.115", "uvicorn>=0.30", "pydantic>=2.7"]
36
36
  # no longer provides. See docs/install.md before installing - pyannote pulls
37
37
  # torch open-endedly, so pin your torch stack or you lose GPU acceleration.
38
38
  diarize = ["pyannote.audio>=4.0"]
39
- export = ["python-docx>=1.1", "reportlab>=4.0"]
39
+ export = ["python-docx>=1.1", "reportlab>=4.0", "textflowkit-fonts>=0.1.5,<0.2"]
40
40
  all = ["textflowkit[mcp,http,diarize,export]"]
41
41
 
42
42
  [project.scripts]
@@ -45,11 +45,18 @@ textflowkit-mcp = "textflowkit.adapters.mcp_server:main"
45
45
  textflowkit-http = "textflowkit.adapters.http_server:main"
46
46
 
47
47
  [project.urls]
48
- Homepage = "https://github.com/scottconverse/textflowkit"
48
+ Homepage = "https://www.textflowkit.org/"
49
49
  Issues = "https://github.com/scottconverse/textflowkit/issues"
50
+ Source = "https://github.com/scottconverse/textflowkit"
51
+ Documentation = "https://github.com/scottconverse/textflowkit/tree/main/docs"
52
+ Changelog = "https://github.com/scottconverse/textflowkit/releases"
50
53
 
51
54
  [tool.hatch.build.targets.wheel]
52
55
  packages = ["src/textflowkit"]
56
+ artifacts = ["src/textflowkit/assets/*.wav"]
57
+
58
+ [tool.hatch.build.targets.sdist]
59
+ exclude = ["/packages/textflowkit-fonts"]
53
60
 
54
61
  [tool.ruff]
55
62
  line-length = 100
@@ -19,6 +19,7 @@ import time
19
19
  import urllib.error
20
20
  import urllib.request
21
21
  import venv
22
+ import zipfile
22
23
  from pathlib import Path
23
24
 
24
25
 
@@ -151,9 +152,19 @@ def http_smoke(command: Path, cwd: Path, env: dict[str, str], *, production: boo
151
152
 
152
153
 
153
154
  def main() -> None:
154
- if len(sys.argv) != 2:
155
- raise SystemExit("usage: python scripts/smoke_installed_wheel.py PATH_TO_WHEEL")
155
+ if len(sys.argv) != 3:
156
+ raise SystemExit("usage: python scripts/smoke_installed_wheel.py MAIN_WHEEL FONTS_WHEEL")
156
157
  wheel = Path(sys.argv[1]).resolve(strict=True)
158
+ font_wheel = Path(sys.argv[2]).resolve(strict=True)
159
+ with zipfile.ZipFile(wheel) as archive:
160
+ assert not any(name.endswith(".ttf") for name in archive.namelist())
161
+ assert "textflowkit/assets/selftest-speech.wav" in archive.namelist()
162
+ with zipfile.ZipFile(font_wheel) as archive:
163
+ assert all(
164
+ f"textflowkit_fonts/fonts/{name}.ttf" in archive.namelist()
165
+ for name in ("NotoSans", "NotoSansArabic", "NotoSansSC")
166
+ )
167
+ assert any("OFL-NotoSans.txt" in name for name in archive.namelist())
157
168
  with tempfile.TemporaryDirectory(prefix="tfk-wheel-smoke-") as directory:
158
169
  root = Path(directory)
159
170
  env_root = root / "venv"
@@ -165,13 +176,33 @@ def main() -> None:
165
176
  env.pop("TEXTFLOWKIT_DB", None)
166
177
  env["TEXTFLOWKIT_OUTPUT_ROOT"] = str(root / "outputs")
167
178
  run(py, "-m", "pip", "install", "--disable-pip-version-check", "--no-deps",
168
- wheel, cwd=root, env=env)
179
+ wheel, font_wheel, cwd=root, env=env)
169
180
  run(py, "-m", "pip", "install", "--disable-pip-version-check",
170
181
  "mcp>=2.0", "fastapi>=0.115", "uvicorn>=0.30", "pydantic>=2.7",
182
+ "reportlab>=4.0", "pypdf>=5",
171
183
  cwd=root, env=env)
172
184
  installed_path = run(py, "-c", "import textflowkit; print(textflowkit.__file__)",
173
185
  cwd=root, env=env).strip()
174
186
  assert str(root).casefold() in installed_path.casefold(), installed_path
187
+ speech_fixture = run(
188
+ py, "-c",
189
+ "from importlib.resources import files; "
190
+ "p=files('textflowkit').joinpath('assets/selftest-speech.wav'); "
191
+ "assert p.read_bytes().startswith(b'RIFF'); print('speech fixture present')",
192
+ cwd=root, env=env,
193
+ )
194
+ assert "speech fixture present" in speech_fixture
195
+ pdf_smoke = run(
196
+ py, "-c",
197
+ "from textflowkit import Segment, Transcript; "
198
+ "from textflowkit.render import render_bytes; "
199
+ "from pypdf import PdfReader; import io; "
200
+ "blob=render_bytes(Transcript(source='wheel',segments=[Segment(0,1,'Hello 你好')]),'pdf'); "
201
+ "assert '你好' in PdfReader(io.BytesIO(blob)).pages[0].extract_text(); "
202
+ "print('PDF export passed')",
203
+ cwd=root, env=env,
204
+ )
205
+ assert "PDF export passed" in pdf_smoke
175
206
  cli = executable(env_root, "textflowkit")
176
207
  mcp = executable(env_root, "textflowkit-mcp")
177
208
  http = executable(env_root, "textflowkit-http")
@@ -0,0 +1,8 @@
1
+ """textflowkit - cross-platform media transcription toolkit."""
2
+
3
+ from textflowkit.core.model import Segment, Transcript, WordTiming
4
+ from textflowkit.core.pipeline import PipelineError, transcribe
5
+
6
+ __version__ = "0.1.5"
7
+
8
+ __all__ = ["PipelineError", "Segment", "Transcript", "WordTiming", "__version__", "transcribe"]
@@ -8,4 +8,4 @@ here, so the MCP server, the HTTP API, and any future frontend cannot drift.
8
8
  - `http_server` - JSON HTTP API for software products and web frontends
9
9
  """
10
10
 
11
- __all__ = ["http_server", "mcp_server"]
11
+ __all__ = ["http_server", "mcp_server"]
@@ -283,11 +283,14 @@ def get_transcript(
283
283
  limit: int | None = None,
284
284
  start: float | None = None,
285
285
  end: float | None = None,
286
+ include_words: bool = False,
286
287
  ):
287
288
  """Transcript for a completed job, optionally a slice.
288
289
 
289
290
  `offset`/`limit` page through segments; `start`/`end` select a time range in
290
- seconds. The JSON form reports total_segments and has_more.
291
+ seconds. The JSON form reports total_segments and has_more. Word timings
292
+ are omitted unless include_words is true; translated segments retain
293
+ source-language word timings.
291
294
  """
292
295
  job, tr = _finished_transcript(job_id)
293
296
  if production_enabled():
@@ -325,7 +328,7 @@ def get_transcript(
325
328
  response = {
326
329
  "job_id": job.id,
327
330
  **page.as_dict(),
328
- "transcript": sliced.to_dict(),
331
+ "transcript": sliced.to_dict(include_words=include_words),
329
332
  }
330
333
  try:
331
334
  enforce_output_limit(len(json.dumps(response, ensure_ascii=False).encode("utf-8")))
@@ -461,5 +464,3 @@ def main(argv: list[str] | None = None) -> int:
461
464
  if __name__ == "__main__":
462
465
  raise SystemExit(main())
463
466
 
464
-
465
-
@@ -248,6 +248,7 @@ def get_transcript(
248
248
  limit: int | None = None,
249
249
  start: float | None = None,
250
250
  end: float | None = None,
251
+ include_words: bool = False,
251
252
  ) -> dict[str, Any]:
252
253
  """Read the transcript for a completed job, optionally a slice of it.
253
254
 
@@ -262,6 +263,9 @@ def get_transcript(
262
263
  limit: Return at most this many segments.
263
264
  start: Only segments ending at or after this time (seconds).
264
265
  end: Only segments starting at or before this time (seconds).
266
+ include_words: Include source-language word timings in JSON output.
267
+ Defaults to false to keep model context small; translated text
268
+ does not produce translated word timings.
265
269
  """
266
270
  job, err = _resolve_job(job_id)
267
271
  if err:
@@ -307,7 +311,8 @@ def get_transcript(
307
311
  "language": tr.language,
308
312
  "platform": tr.platform,
309
313
  **page.as_dict(),
310
- "content": render(sliced, norm),
314
+ "content": sliced.to_json(include_words=include_words) if norm == "json"
315
+ else render(sliced, norm),
311
316
  }
312
317
  if page.has_more:
313
318
  payload["next"] = (
@@ -551,4 +556,3 @@ if __name__ == "__main__":
551
556
 
552
557
 
553
558
 
554
-