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.
- {textflowkit-0.1.4 → textflowkit-0.1.5}/.gitattributes +22 -22
- {textflowkit-0.1.4 → textflowkit-0.1.5}/.github/workflows/ci.yml +4 -2
- textflowkit-0.1.5/.github/workflows/publish-pypi.yml +114 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/.gitignore +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/.pre-commit-config.yaml +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/CONTRIBUTING.md +4 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/LEGAL.md +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/LICENSE +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/PKG-INFO +48 -2
- {textflowkit-0.1.4 → textflowkit-0.1.5}/README.md +41 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/adapters.md +10 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/install.md +3 -2
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/release-checklist.md +30 -7
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/roadmap.md +11 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/pyproject.toml +10 -3
- {textflowkit-0.1.4 → textflowkit-0.1.5}/scripts/smoke_installed_wheel.py +34 -3
- textflowkit-0.1.5/src/textflowkit/__init__.py +8 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/adapters/__init__.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/adapters/http_server.py +5 -4
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/adapters/mcp_server.py +6 -2
- textflowkit-0.1.5/src/textflowkit/assets/README.md +8 -0
- textflowkit-0.1.5/src/textflowkit/assets/selftest-speech.wav +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/cli.py +19 -26
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/__init__.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/cancel.py +19 -19
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/engine.py +12 -4
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/model.py +34 -7
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/pipeline.py +13 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/submission.py +3 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/timeutil.py +2 -2
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/__init__.py +15 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/docx.py +71 -71
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/markdown.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/pdf.py +9 -5
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/srt.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/txt.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/render/vtt.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/__init__.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/detect.py +200 -200
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/ollama_stub.py +110 -110
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_adapters.py +57 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_batch.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_engine_cache.py +21 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_export.py +11 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_jobs.py +1 -1
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_model.py +11 -2
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_pipeline_integrity.py +48 -3
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_release_boundaries.py +17 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_render.py +99 -99
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_sources.py +2 -2
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_stdio_protocol.py +213 -213
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_submission.py +26 -0
- textflowkit-0.1.4/src/textflowkit/__init__.py +0 -8
- textflowkit-0.1.4/src/textflowkit/render/fonts/NotoSans.ttf +0 -0
- textflowkit-0.1.4/src/textflowkit/render/fonts/NotoSansArabic.ttf +0 -0
- textflowkit-0.1.4/src/textflowkit/render/fonts/NotoSansSC.ttf +0 -0
- textflowkit-0.1.4/src/textflowkit/render/fonts/OFL-NotoSans.txt +0 -94
- textflowkit-0.1.4/src/textflowkit/render/fonts/OFL-NotoSansSC.txt +0 -93
- textflowkit-0.1.4/src/textflowkit/render/fonts/README.md +0 -19
- {textflowkit-0.1.4 → textflowkit-0.1.5}/.github/dependabot.yml +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/SECURITY.md +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/.nojekyll +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/index.html +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/site-deployment.md +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/docs/sources.md +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/scripts/smoke_live_youtube.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/batch.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/bind.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/checkpoint.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/diarize.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/executor.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/jobs.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/paths.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/retrieval.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/runner.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/service.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/sqlite_store.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/core/translate.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/acquire.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/src/textflowkit/sources/scratch.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/__init__.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_acquire.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_bind.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_checkpoint.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_cli.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_cli_resume_batch.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_diarize.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_executor.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_landing_page.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_live_smoke_script.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_paths.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_retrieval.py +0 -0
- {textflowkit-0.1.4 → textflowkit-0.1.5}/tests/test_service_profile.py +0 -0
- {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 -
|
|
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
|
|
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
|
|
@@ -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 -
|
|
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
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: textflowkit
|
|
3
|
-
Version: 0.1.
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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://
|
|
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) !=
|
|
155
|
-
raise SystemExit("usage: python scripts/smoke_installed_wheel.py
|
|
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"]
|
|
@@ -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":
|
|
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
|
-
|