porter-workflow 0.2.0__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 (113) hide show
  1. porter_workflow-0.2.0/.gitignore +110 -0
  2. porter_workflow-0.2.0/CHANGELOG.md +239 -0
  3. porter_workflow-0.2.0/LICENSE +21 -0
  4. porter_workflow-0.2.0/PKG-INFO +309 -0
  5. porter_workflow-0.2.0/README.md +256 -0
  6. porter_workflow-0.2.0/THIRD_PARTY_NOTICES.md +175 -0
  7. porter_workflow-0.2.0/pyproject.toml +240 -0
  8. porter_workflow-0.2.0/skills/porter-skill/README.md +70 -0
  9. porter_workflow-0.2.0/skills/porter-skill/SKILL.md +241 -0
  10. porter_workflow-0.2.0/skills/porter-skill/assets/config.example.json +59 -0
  11. porter_workflow-0.2.0/skills/porter-skill/references/ARCHITECTURE.md +147 -0
  12. porter_workflow-0.2.0/skills/porter-skill/references/CONFIG.md +154 -0
  13. porter_workflow-0.2.0/skills/porter-skill/references/MCP.md +120 -0
  14. porter_workflow-0.2.0/skills/porter-skill/scripts/bootstrap.sh +40 -0
  15. porter_workflow-0.2.0/skills/porter-skill/scripts/inspect.sh +13 -0
  16. porter_workflow-0.2.0/skills/porter-skill/scripts/porter.sh +14 -0
  17. porter_workflow-0.2.0/src/porter/__init__.py +143 -0
  18. porter_workflow-0.2.0/src/porter/asr/__init__.py +59 -0
  19. porter_workflow-0.2.0/src/porter/asr/base.py +169 -0
  20. porter_workflow-0.2.0/src/porter/asr/bcut.py +304 -0
  21. porter_workflow-0.2.0/src/porter/asr/chain.py +442 -0
  22. porter_workflow-0.2.0/src/porter/asr/google_web.py +296 -0
  23. porter_workflow-0.2.0/src/porter/asr/platform_subs.py +123 -0
  24. porter_workflow-0.2.0/src/porter/asr/videocaptioner.py +241 -0
  25. porter_workflow-0.2.0/src/porter/asr/whisper_api.py +193 -0
  26. porter_workflow-0.2.0/src/porter/asr/whisper_local.py +360 -0
  27. porter_workflow-0.2.0/src/porter/config.py +398 -0
  28. porter_workflow-0.2.0/src/porter/context.py +105 -0
  29. porter_workflow-0.2.0/src/porter/doctor/__init__.py +55 -0
  30. porter_workflow-0.2.0/src/porter/doctor/guides.py +166 -0
  31. porter_workflow-0.2.0/src/porter/doctor/probes.py +673 -0
  32. porter_workflow-0.2.0/src/porter/errors.py +138 -0
  33. porter_workflow-0.2.0/src/porter/events.py +194 -0
  34. porter_workflow-0.2.0/src/porter/jobs/__init__.py +44 -0
  35. porter_workflow-0.2.0/src/porter/jobs/records.py +695 -0
  36. porter_workflow-0.2.0/src/porter/jobs/store.py +332 -0
  37. porter_workflow-0.2.0/src/porter/logging.py +157 -0
  38. porter_workflow-0.2.0/src/porter/media/__init__.py +75 -0
  39. porter_workflow-0.2.0/src/porter/media/burn.py +479 -0
  40. porter_workflow-0.2.0/src/porter/media/encode.py +472 -0
  41. porter_workflow-0.2.0/src/porter/media/enhance.py +102 -0
  42. porter_workflow-0.2.0/src/porter/media/ffmpeg.py +367 -0
  43. porter_workflow-0.2.0/src/porter/media/prepare.py +210 -0
  44. porter_workflow-0.2.0/src/porter/media/probe.py +189 -0
  45. porter_workflow-0.2.0/src/porter/media/standardize.py +199 -0
  46. porter_workflow-0.2.0/src/porter/models/__init__.py +25 -0
  47. porter_workflow-0.2.0/src/porter/models/inspection.py +116 -0
  48. porter_workflow-0.2.0/src/porter/models/materials.py +128 -0
  49. porter_workflow-0.2.0/src/porter/models/metadata.py +47 -0
  50. porter_workflow-0.2.0/src/porter/models/plan.py +113 -0
  51. porter_workflow-0.2.0/src/porter/models/request.py +165 -0
  52. porter_workflow-0.2.0/src/porter/models/subtitle.py +120 -0
  53. porter_workflow-0.2.0/src/porter/pipeline.py +505 -0
  54. porter_workflow-0.2.0/src/porter/plan.py +373 -0
  55. porter_workflow-0.2.0/src/porter/platforms/__init__.py +60 -0
  56. porter_workflow-0.2.0/src/porter/platforms/base.py +746 -0
  57. porter_workflow-0.2.0/src/porter/platforms/bilibili.py +57 -0
  58. porter_workflow-0.2.0/src/porter/platforms/downloader.py +89 -0
  59. porter_workflow-0.2.0/src/porter/platforms/inspector.py +280 -0
  60. porter_workflow-0.2.0/src/porter/platforms/instagram.py +49 -0
  61. porter_workflow-0.2.0/src/porter/platforms/local.py +282 -0
  62. porter_workflow-0.2.0/src/porter/platforms/registry.py +231 -0
  63. porter_workflow-0.2.0/src/porter/platforms/spec.py +158 -0
  64. porter_workflow-0.2.0/src/porter/platforms/tiktok.py +43 -0
  65. porter_workflow-0.2.0/src/porter/platforms/titles.py +132 -0
  66. porter_workflow-0.2.0/src/porter/platforms/urls.py +223 -0
  67. porter_workflow-0.2.0/src/porter/platforms/x.py +37 -0
  68. porter_workflow-0.2.0/src/porter/platforms/ydl.py +431 -0
  69. porter_workflow-0.2.0/src/porter/platforms/youtube.py +47 -0
  70. porter_workflow-0.2.0/src/porter/ports.py +158 -0
  71. porter_workflow-0.2.0/src/porter/py.typed +1 -0
  72. porter_workflow-0.2.0/src/porter/subtitles/__init__.py +71 -0
  73. porter_workflow-0.2.0/src/porter/subtitles/ass.py +274 -0
  74. porter_workflow-0.2.0/src/porter/subtitles/phrasing.py +765 -0
  75. porter_workflow-0.2.0/src/porter/subtitles/srt.py +432 -0
  76. porter_workflow-0.2.0/src/porter/subtitles/transcript.py +286 -0
  77. porter_workflow-0.2.0/src/porter/translate/__init__.py +67 -0
  78. porter_workflow-0.2.0/src/porter/translate/base.py +199 -0
  79. porter_workflow-0.2.0/src/porter/translate/bing.py +467 -0
  80. porter_workflow-0.2.0/src/porter/translate/chain.py +477 -0
  81. porter_workflow-0.2.0/src/porter/translate/google.py +354 -0
  82. porter_workflow-0.2.0/src/porter/translate/llm.py +324 -0
  83. porter_workflow-0.2.0/src/porter/translate/mymemory.py +165 -0
  84. porter_workflow-0.2.0/src/porter/translate/reuse.py +176 -0
  85. porter_workflow-0.2.0/src/porter/translate/videocaptioner.py +273 -0
  86. porter_workflow-0.2.0/src/porter/utils/__init__.py +17 -0
  87. porter_workflow-0.2.0/src/porter/utils/text.py +61 -0
  88. porter_workflow-0.2.0/src/porter/utils/time.py +53 -0
  89. porter_workflow-0.2.0/src/porter_cli/__init__.py +13 -0
  90. porter_workflow-0.2.0/src/porter_cli/__main__.py +17 -0
  91. porter_workflow-0.2.0/src/porter_cli/app.py +131 -0
  92. porter_workflow-0.2.0/src/porter_cli/commands/__init__.py +14 -0
  93. porter_workflow-0.2.0/src/porter_cli/commands/config.py +131 -0
  94. porter_workflow-0.2.0/src/porter_cli/commands/doctor.py +130 -0
  95. porter_workflow-0.2.0/src/porter_cli/commands/inspect.py +86 -0
  96. porter_workflow-0.2.0/src/porter_cli/commands/jobs.py +216 -0
  97. porter_workflow-0.2.0/src/porter_cli/commands/plan.py +198 -0
  98. porter_workflow-0.2.0/src/porter_cli/commands/run.py +277 -0
  99. porter_workflow-0.2.0/src/porter_cli/render.py +226 -0
  100. porter_workflow-0.2.0/src/porter_mcp/__init__.py +22 -0
  101. porter_workflow-0.2.0/src/porter_mcp/limits.py +34 -0
  102. porter_workflow-0.2.0/src/porter_mcp/server.py +90 -0
  103. porter_workflow-0.2.0/src/porter_mcp/shutdown.py +204 -0
  104. porter_workflow-0.2.0/src/porter_mcp/stdout_guard.py +153 -0
  105. porter_workflow-0.2.0/src/porter_mcp/tools/__init__.py +49 -0
  106. porter_workflow-0.2.0/src/porter_mcp/tools/config.py +99 -0
  107. porter_workflow-0.2.0/src/porter_mcp/tools/docs.py +282 -0
  108. porter_workflow-0.2.0/src/porter_mcp/tools/doctor.py +118 -0
  109. porter_workflow-0.2.0/src/porter_mcp/tools/inspect.py +123 -0
  110. porter_workflow-0.2.0/src/porter_mcp/tools/jobs.py +435 -0
  111. porter_workflow-0.2.0/src/porter_mcp/tools/meta.py +40 -0
  112. porter_workflow-0.2.0/src/porter_mcp/tools/plan.py +106 -0
  113. porter_workflow-0.2.0/src/porter_mcp/tools/stages.py +357 -0
@@ -0,0 +1,110 @@
1
+ # ---------------------------------------------------------------------------
2
+ # porter-workflow
3
+ # ---------------------------------------------------------------------------
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+ .eggs/
15
+ wheels/
16
+ sdist/
17
+ MANIFEST
18
+
19
+ # Virtualenvs / tooling caches
20
+ .venv/
21
+ venv/
22
+ ENV/
23
+ .pytest_cache/
24
+ .mypy_cache/
25
+ .ruff_cache/
26
+ .coverage
27
+ .coverage.*
28
+ coverage.xml
29
+ htmlcov/
30
+ .tox/
31
+ .hypothesis/
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Secrets & local configuration (NEVER commit)
35
+ # ---------------------------------------------------------------------------
36
+ config.json
37
+ porter.json
38
+ porter_config.json
39
+ .env
40
+ .env.*
41
+ !.env.example
42
+ *.cookies
43
+ *.cookie
44
+ cookies.txt
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Pipeline media output & scratch
48
+ # ---------------------------------------------------------------------------
49
+ porter_output/
50
+ output/
51
+ downloads/
52
+ *.mp4
53
+ *.mkv
54
+ *.webm
55
+ *.mov
56
+ *.flv
57
+ *.wav
58
+ *.mp3
59
+ *.flac
60
+ *.aac
61
+ *.m4a
62
+ *.jpg
63
+ *.jpeg
64
+ *.png
65
+ *.webp
66
+ *.part
67
+ *.ytdl
68
+ *.ass
69
+ *.srt
70
+ *.vtt
71
+ .tmp/
72
+ tmp/
73
+ *.tmp
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Local-only working area (kept on disk, never published)
77
+ # ---------------------------------------------------------------------------
78
+ docs/
79
+
80
+ # Skill assets are the one place media is source, not output: a subtitle sample
81
+ # must be committable. Pipeline output dropped anywhere else is still ignored by
82
+ # the rules above.
83
+ !skills/**/*.png
84
+ !skills/**/*.jpg
85
+ !skills/**/*.jpeg
86
+ !skills/**/*.webp
87
+ !skills/**/*.gif
88
+ !skills/**/*.svg
89
+ !skills/**/*.srt
90
+ !skills/**/*.ass
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Agent / editor / OS
94
+ # ---------------------------------------------------------------------------
95
+ .pi/**
96
+ .memory/
97
+ .cache/
98
+ .idea/
99
+ .vscode/
100
+ *.swp
101
+ *.swo
102
+ *~
103
+ .DS_Store
104
+ Thumbs.db
105
+ desktop.ini
106
+
107
+ # Pi session exports & local scratch
108
+ pi-session-*.html
109
+ *.session.html
110
+ scratch/
@@ -0,0 +1,239 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-09-24
11
+
12
+ The first published release. v0.2 is a structural rewrite into one engine with
13
+ three frontends (CLI, MCP server, Agent Skill). The `0.1.0` entry below was a
14
+ commit, never a tag or an upload, so this is the first version anyone can install.
15
+
16
+ ### Added
17
+
18
+ - **`PORTER_CACHE_DIR`** overrides where the job registry lives. Needed for more
19
+ than tidiness: `platformdirs` ignores `XDG_CACHE_HOME` on Windows (it asks the
20
+ Known Folder API), so there was no way to point two processes at a temporary
21
+ registry — which is exactly what the cross-process cancellation test has to do.
22
+ It is also the knob for a cache directory that is shared or read-only.
23
+ - **TRANSLATE reuses its translation.** A re-run over unchanged cues skips the
24
+ backends and re-renders the subtitles from the cached text, so editing
25
+ `style.*` and re-running is free and still gives the new look. This was the one
26
+ phase with no reuse, on the grounds that its output includes the rendered
27
+ subtitle files and those depend on the style — which is true of the *files*,
28
+ and is why the **text** is cached and never the files. The cache is keyed on a
29
+ content hash of the sentences plus the target language and the engine's
30
+ identity (backend, model, endpoint), so changing the model or editing the cues
31
+ misses while changing the style hits. `--force` bypasses it.
32
+ - **The MCP server shuts down gracefully on a signal.** `SIGINT`/`SIGTERM` (and
33
+ `SIGBREAK` on Windows) now ask every running job to stop and wait briefly for
34
+ them to unwind, instead of killing them mid-write. A job that unwinds records
35
+ `cancelled` itself, so the registry does not have to infer an interruption from
36
+ a dead PID later. An in-flight FFmpeg encode cannot be interrupted -- it has no
37
+ cancellation point inside it -- so a burn finishes its current step; the wait is
38
+ bounded for that reason, and a second signal skips it.
39
+ - **Interrupting `porter run` reports what happened.** Ctrl+C records the job as
40
+ cancelled, prints one line, and exits `130`. Previously the `KeyboardInterrupt`
41
+ unwound to the top level and the user was shown the frame stack of whichever
42
+ library call was running -- ssl, socket, FFmpeg's pipe.
43
+ - **Engine/frontend split.** Business logic now lives only in the `porter`
44
+ library. `porter_cli` and `porter_mcp` are thin consumers, and the boundary is
45
+ enforced by `import-linter`.
46
+ - **`porter-mcp`**, an MCP server over stdio. Long jobs are exposed as
47
+ `start` / `status` / `result` / `cancel` rather than one blocking call.
48
+ - **Persistent job registry**, written to the platform cache directory and
49
+ visible across processes, so a job can be inspected or cancelled from another
50
+ shell.
51
+ - **New CLI subcommands**: `inspect`, `plan`, `jobs`, `doctor`, `config`.
52
+ - **Local speech recognition** via `faster-whisper` behind the `asr-local`
53
+ extra: no API key, no network, no GPL code.
54
+ - **Local media input** — a path or `file://` URL is accepted in addition to a
55
+ remote URL.
56
+ - **`porter doctor`** probes FFmpeg, yt-dlp, the JavaScript runtime and every
57
+ ASR/translation backend, and reports which route a job will actually take.
58
+ - **LLM translation backend**, alongside the key-free Bing, Google and MyMemory
59
+ backends.
60
+ - **ASS subtitle styling** and bilingual / Chinese-only release renders.
61
+ - **Launcher binaries** on GitHub Releases: a ~hundreds-of-KB bootstrap that
62
+ installs and updates the engine in `~/.porter/venv`, rather than a frozen
63
+ bundle that would pin a stale yt-dlp.
64
+
65
+ ### Changed
66
+
67
+ - **Python 3.11 is now the floor**, up from 3.10. The declared `>=3.10` was not
68
+ true: the recommended `[asr-local]` extra could not install there at all,
69
+ because `faster-whisper` pulls in `onnxruntime`, which stopped publishing
70
+ `cp310` wheels after 1.23.2 — so `pip install porter-workflow[asr-local]`
71
+ failed outright on 3.10 while `requires-python` claimed it worked. Python 3.10
72
+ reaches end of life on 2026-10-31. The `tomli` dependency and its conditional
73
+ import are gone with it, since `tomllib` is standard from 3.11.
74
+ - **Dependencies are locked.** `uv.lock` is committed and CI installs from it
75
+ with `uv sync --locked`, so the environment that is tested is the environment
76
+ that is described. Changing a dependency requires re-locking in the same
77
+ commit, which the gate enforces.
78
+ - **Distribution renamed** from `porter-skill` to `porter-workflow`. The
79
+ installable Agent Skill keeps the name `porter-skill`.
80
+ - **Library import path** changed from `porter_skill` to `porter`.
81
+ - **Configuration** is resolved from `--config`, `$PORTER_CONFIG`, environment
82
+ variables, `./porter.json`, the platform user-config directory, then built-in
83
+ defaults. v0.1's probe for `SKILL.md` to decide where to write is gone; the
84
+ engine no longer knows how it is deployed.
85
+ - **CLI is subcommand-based.** The v0.1 flags (`--config-show`, `--doctor`,
86
+ `--inspect`, `--skip-burn`, …) are replaced.
87
+ - **YouTube downloads now require an external JavaScript runtime** (Deno
88
+ recommended, Node ≥ 20 accepted), because yt-dlp ≥ 2025.11.12 needs one to
89
+ solve YouTube's JS challenges.
90
+ - **Optional backends are behind extras** (`llm`, `stt`, `asr-local`, `images`,
91
+ `mcp`, `all`), so the default install is pure Python plus FFmpeg.
92
+
93
+ ### Fixed
94
+
95
+ - **The job registry is safe for concurrent writers in one process.** The lock
96
+ claimed to make that safe and did not: `msvcrt.locking` refuses rather than
97
+ waits when the same process already holds the byte range through another
98
+ handle, raising `OSError(EDEADLK, "Resource deadlock avoided")`. That is not an
99
+ exotic case — the MCP server publishes to the registry from one thread per job —
100
+ and measured, 16 concurrent publishers lost up to 5 records. A process-local
101
+ lock now serialises threads, with the file lock still serialising processes.
102
+ - **A failed lock no longer reports the wrong error.** When taking the lock
103
+ failed, the cleanup tried to unlock a handle that had never locked it, which
104
+ raises `PermissionError` on Windows and *replaced* the `EDEADLK` that explained
105
+ the failure. The caller saw "permission denied" and never the cause.
106
+ - **`--asr-engine` works.** It was written into `JobOptions.asr_engine` by all
107
+ three frontends and read by nobody, so `porter run --asr-engine whisper-api`,
108
+ MCP `porter_job_start(asr_engine=…)` and `porter_transcribe(engine=…)` were
109
+ silently ignored while the same value in `asr.engine` worked — which is
110
+ precisely what made it hard to notice. The flag now wins over the config key.
111
+ The test that appeared to cover this set the *config key* while its docstring
112
+ described the *flag*, which is how the gap survived.
113
+ - **Platform subtitle tracks are written as UTF-8.** The `.json` (Bilibili) and
114
+ `.vtt` branches of the subtitle downloader read with `encoding="utf-8"` and
115
+ then wrote with `Path.write_text`'s default — the *locale* encoding, GBK on a
116
+ Windows console. Any cue holding an emoji or a replacement character raised
117
+ `UnicodeEncodeError` and killed a job whose subtitle had just been fetched
118
+ successfully. A mechanical test now rejects locale-encoded text writes across
119
+ `src/`, because a behavioural test cannot catch this on CI: Ubuntu's locale is
120
+ UTF-8, so the same code produces the same bytes there.
121
+ - **The two release videos are announced as themselves.** BURN emitted both the
122
+ bilingual and the Chinese-only release as the generic `video` kind, so a
123
+ consumer matching on `ArtifactKind` — which its own docstring says downstream
124
+ agents do — could not tell them apart, even though `video_bilingual` and
125
+ `video_zh` existed for exactly that.
126
+ - **`porter jobs cancel` no longer promises a checkpoint.** Its message said the
127
+ owning process "stops at the next checkpoint", referring to a resume mechanism
128
+ that was removed; it now says cancellation check.
129
+ - **`porter jobs list` no longer crashes on Windows.** `os.kill(pid, 0)` reports a
130
+ dead PID as `OSError(ERROR_INVALID_PARAMETER)` there rather than
131
+ `ProcessLookupError`, and it escaped unhandled -- so a single stale record, which
132
+ is exactly what a killed job leaves behind, aborted the whole registry read.
133
+ The command that tells you about interrupted jobs was the one they broke.
134
+ - **The recycled-PID guard now works on Windows.** `process_marker` reads the
135
+ process creation time via `GetProcessTimes` instead of falling back to a bare
136
+ PID, and checks the exit code first: `OpenProcess` keeps succeeding for a
137
+ terminated process for a moment after it dies, so the creation time alone would
138
+ have reported a dead owner as identifiable. Liveness is answered per platform
139
+ rather than through `os.kill(pid, 0)`, which has the same blind spot.
140
+ - **`ffmpeg.auto_tune` and `asr.audio_denoise` are honoured.** Both were parsed,
141
+ documented, and read by nothing. `auto_tune=false` now skips the hardware-tier
142
+ trial encode and uses `ffmpeg.preset`/`ffmpeg.crf` as configured, in BURN,
143
+ `porter_burn` and `porter doctor` alike; `audio_denoise` is the default for
144
+ `JobOptions.audio_denoise`, which the CLI and MCP override only when asked to.
145
+ - **`porter plan` no longer drops `--config`.** `plan_for` re-resolved the
146
+ configuration whenever it was given neither a context nor options, so the plan
147
+ described a different run from the one the same command line would perform --
148
+ every setting that comes from configuration (`asr.engine`, `translator`, ffmpeg,
149
+ subtitle style) was read from defaults. The CLI and the MCP tool now pass a
150
+ context built from the configuration they already resolved.
151
+ - **Naming an unavailable ASR backend is no longer silent.** Naming a backend
152
+ promotes it and keeps the rest as fallbacks, so a missing one is not fatal --
153
+ which is exactly why it needs saying: asking for VideoCaptioner's `bijian` and
154
+ quietly getting Bcut's output is indistinguishable from success. It is now
155
+ reported at assembly time, before the expensive work.
156
+ - **`porter_doctor` no longer tells agents to call a tool that does not exist.**
157
+ Its description referred to `porter_run`.
158
+ - **Truncated encodes are no longer published as finished videos.** Release
159
+ output is written to a temporary path and renamed only after `ffprobe`
160
+ confirms it is readable. In v0.1 the rename was unconditional, so a failed
161
+ encode could be mistaken for a complete render.
162
+ - **`import porter` no longer fails on Python 3.10.** `tomli` is declared for
163
+ the 3.10 floor instead of relying on the 3.11 standard-library `tomllib`.
164
+ - **Non-ASCII paths and titles no longer crash subprocess reads.** Child output
165
+ is decoded with `errors="replace"` rather than the locale codec, which is
166
+ ASCII under the `POSIX`/`C` locale.
167
+ - **CLI, MCP and skill agree on one engine**, so a fix lands in all three at
168
+ once; v0.1 duplicated the pipeline across entry points.
169
+ - **yt-dlp failures are reported as data, not as a traceback.** A removed video,
170
+ a geo-block, a bot check or a format selector that matches nothing all arrive
171
+ as `yt_dlp.utils.YoutubeDLError`; only the metadata path let one escape, so the
172
+ user got a Python stack trace for a condition the tool is meant to explain. It
173
+ is now mapped to `ExtractionError` with the URL, platform and original message.
174
+ - **Subtitles no longer cut an English word in half.** The midpoint pass in
175
+ `split_chinese_text_by_phrase` used a raw character index, serving a real
176
+ subtitle as `这里的所有内容都在 Wi` / `ndows 上本地运行` — despite the function
177
+ promising not to cut words apart. The cut now snaps to the nearest word
178
+ boundary, and a piece that is one unbreakable token is left whole.
179
+ - **`porter plan` accepts `--cookies` / `--cookies-from-browser`.** It inspects
180
+ the source, so it needs the credentials a run needs; its own blocker text told
181
+ users to pass flags the command did not have. (`inspect` always had them.)
182
+ - **Glyphs degrade to ASCII on a non-UTF-8 console.** A GBK or cp1252 terminal
183
+ cannot encode `✓`/`✗`, and `errors="backslashreplace"` printed a literal
184
+ `\u2713` at the user. Symbols the console *can* encode (`→`, `…`, `·`) are
185
+ still used; only the impossible ones fall back.
186
+
187
+ ### Removed
188
+
189
+ - **`porter_mcp/progress.py`, and the claim that went with it.** A complete
190
+ bridge from engine events to MCP progress notifications — phase weights,
191
+ monotonic percent, the lot — imported by nothing, while `server.py` listed
192
+ "progress notifications" among the things the frontend owns. It does not send
193
+ any: jobs run on background threads and are polled, and the blocking stage
194
+ tools are short enough that a token would buy nothing. The docstring now says
195
+ so instead of the module implying otherwise.
196
+ - **Dead helpers whose docstrings described capabilities they did not have.**
197
+ `JobStore._observe_cancel_now` ("exists for the watchdog's own tests"; no test
198
+ called it), `translate.base.apply_texts_to_items` (promised a `strict=True`
199
+ length check that never ran, because the chain applies text through
200
+ `_cues_from_sentences`), `doctor.guides.platform_hint`, and
201
+ `SubtitleSet.has_translation` — whose real check is
202
+ `has_chinese_translation`, since a backend echoing its input satisfies the
203
+ former.
204
+ - **Unused checkpoint scaffolding on `RunContext`** (`checkpoint_dir`,
205
+ `stage_dir`, `stage_cached`). Nothing called it, no code ever set
206
+ `checkpoint_dir`, and its docstrings advertised a resume-from-disk feature
207
+ that did not exist. Stage-output reuse is real, but it lives in the PREPARE,
208
+ TRANSCRIBE and BURN implementations instead, gated on `--force`.
209
+ - **The claimed key-free speech-to-text path.** Every key-free ASR endpoint
210
+ (Bcut, Google Web) was measured and found non-functional as of 2026-09-22.
211
+ Transcription now needs an API key or the VideoCaptioner CLI, and
212
+ `porter doctor` says so before a job starts.
213
+ - **Hard dependencies on `openai`, `pillow`, `SpeechRecognition`** in the
214
+ default install.
215
+
216
+ ### Security
217
+
218
+ - Added [`SECURITY.md`](SECURITY.md) and a private vulnerability reporting
219
+ process.
220
+ - The engine never writes to stdout (enforced by ruff rule `T20`); stdout is the
221
+ MCP JSON-RPC channel, so this is a protocol-integrity guarantee, not style.
222
+
223
+ ## [0.1.0] - 2026-08-30
224
+
225
+ Initial release, published as `porter-skill` (Python distribution and Agent
226
+ Skill of the same name).
227
+
228
+ ### Added
229
+
230
+ - Automated video localization pipeline for YouTube, X/Twitter, Instagram,
231
+ TikTok and Bilibili: download, transcribe, translate, burn.
232
+ - Multi-engine ASR chain and multi-backend subtitle translation chain.
233
+ - Bilingual and Chinese-only hard-subbed release videos, plus ASS/SRT subtitle
234
+ assets and a standardised `raw/` + `cooked/` output layout.
235
+ - Agent Skill packaging (`SKILL.md`, scripts, references, example config).
236
+
237
+ [Unreleased]: https://github.com/RolinShmily/porter-workflow/compare/v0.2.0...HEAD
238
+ [0.2.0]: https://github.com/RolinShmily/porter-workflow/compare/v0.1.0...v0.2.0
239
+ [0.1.0]: https://github.com/RolinShmily/porter-workflow/commit/c2e4286
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RoL1n_SrP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.5
2
+ Name: porter-workflow
3
+ Version: 0.2.0
4
+ Summary: Automated video localization, multi-engine ASR, subtitle translation, and dual-version FFmpeg hardsub pipeline for AI agents, CLI, and MCP hosts.
5
+ Project-URL: Homepage, https://github.com/RolinShmily/porter-workflow
6
+ Project-URL: Repository, https://github.com/RolinShmily/porter-workflow
7
+ Project-URL: Issues, https://github.com/RolinShmily/porter-workflow/issues
8
+ Project-URL: Documentation, https://github.com/RolinShmily/porter-workflow/tree/main/docs
9
+ Project-URL: Changelog, https://github.com/RolinShmily/porter-workflow/blob/main/CHANGELOG.md
10
+ Author: RoL1n_SrP
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ License-File: THIRD_PARTY_NOTICES.md
14
+ Keywords: agent-skill,asr,ffmpeg,hardsub,mcp,subtitles,translation,video-localization,youtube
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Video
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: platformdirs>=4.0
24
+ Requires-Dist: pydantic>=2.0
25
+ Requires-Dist: requests>=2.31
26
+ Requires-Dist: yt-dlp[default]>=2025.11.12
27
+ Provides-Extra: all
28
+ Requires-Dist: faster-whisper>=1.0; extra == 'all'
29
+ Requires-Dist: fastmcp>=2.11; extra == 'all'
30
+ Requires-Dist: json-repair>=0.30; extra == 'all'
31
+ Requires-Dist: openai>=1.0; extra == 'all'
32
+ Requires-Dist: pillow>=10.0; extra == 'all'
33
+ Requires-Dist: speechrecognition>=3.10; extra == 'all'
34
+ Provides-Extra: asr-local
35
+ Requires-Dist: faster-whisper>=1.0; extra == 'asr-local'
36
+ Provides-Extra: dev
37
+ Requires-Dist: import-linter>=2.0; extra == 'dev'
38
+ Requires-Dist: mypy>=1.10; extra == 'dev'
39
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
40
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
41
+ Requires-Dist: pytest>=8.0; extra == 'dev'
42
+ Requires-Dist: ruff>=0.6; extra == 'dev'
43
+ Provides-Extra: images
44
+ Requires-Dist: pillow>=10.0; extra == 'images'
45
+ Provides-Extra: llm
46
+ Requires-Dist: json-repair>=0.30; extra == 'llm'
47
+ Requires-Dist: openai>=1.0; extra == 'llm'
48
+ Provides-Extra: mcp
49
+ Requires-Dist: fastmcp>=2.11; extra == 'mcp'
50
+ Provides-Extra: stt
51
+ Requires-Dist: speechrecognition>=3.10; extra == 'stt'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # Porter Workflow
55
+
56
+ <p align="center">
57
+ <b>English</b> | <a href="README_zh.md">简体中文</a>
58
+ </p>
59
+
60
+ <p align="center">
61
+ <a href="https://github.com/RolinShmily/porter-workflow/actions/workflows/test.yml"><img alt="CI" src="https://github.com/RolinShmily/porter-workflow/actions/workflows/test.yml/badge.svg"></a>
62
+ <a href="https://pypi.org/project/porter-workflow/"><img alt="PyPI" src="https://img.shields.io/pypi/v/porter-workflow"></a>
63
+ <img alt="Python 3.11 - 3.13" src="https://img.shields.io/badge/python-3.11%20%E2%80%93%203.13-blue">
64
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green"></a>
65
+ </p>
66
+
67
+ **Porter Workflow** is an automated video localization pipeline: give it a video
68
+ URL, get back a standardised asset bundle plus hard-subbed release videos with
69
+ bilingual and Chinese-only subtitles.
70
+
71
+ It ships as **one engine with three frontends**:
72
+
73
+ | Frontend | Entry point | For |
74
+ | --- | --- | --- |
75
+ | `porter` (CLI) | `porter "<URL>"` | Humans and shell scripts |
76
+ | `porter-mcp` | `uvx --from "porter-workflow[mcp]" porter-mcp` | AI agents via Model Context Protocol |
77
+ | `porter-skill` | `npx skills add RolinShmily/porter-workflow` | Agents that use the Agent Skills spec |
78
+
79
+ All three call the same `porter` library. The engine contains no argument
80
+ parsing and never writes to stdout — a requirement, because in an MCP stdio
81
+ server stdout *is* the JSON-RPC channel.
82
+
83
+ > **Status: `v0.2.0` is the current development line on `main`.**
84
+ > The v0.2 rewrite — one engine, three frontends — has landed. The v0.1
85
+ > implementation is preserved in git history at commit `b5fd577`.
86
+
87
+ ---
88
+
89
+ ## Installation
90
+
91
+ Requires **Python ≥ 3.11**.
92
+
93
+ ```bash
94
+ # CLI, minimal install (pure-Python; translation works without a key, but
95
+ # transcription does NOT -- see "Transcription needs a key" below)
96
+ uvx porter-workflow "<URL>"
97
+
98
+ # CLI with every optional backend
99
+ uvx --from "porter-workflow[all]" porter "<URL>"
100
+
101
+ # Persistent install
102
+ pipx install "porter-workflow[all]"
103
+ ```
104
+
105
+ ### System dependencies
106
+
107
+ These are **not** Python packages and must be on `PATH`:
108
+
109
+ | Dependency | Needed for | Notes |
110
+ | --- | --- | --- |
111
+ | **FFmpeg + ffprobe** | Everything | Must be built **with `libass`** for hardsubbing. Verify: `ffmpeg -filters \| grep subtitles` |
112
+ | **Deno** (recommended) | YouTube downloads | yt-dlp ≥ 2025.11.12 needs an external JS runtime to solve YouTube's JS challenges. Node ≥ 20 also works. |
113
+
114
+ Check everything at once:
115
+
116
+ ```bash
117
+ porter doctor
118
+ ```
119
+
120
+ ### Transcription needs a key (or the VideoCaptioner CLI)
121
+
122
+ **There is no working key-free speech-to-text path.** This is measured, not
123
+ theoretical — as of 2026-09-22, on a real 10-minute video:
124
+
125
+ | ASR backend | Status |
126
+ | --- | --- |
127
+ | Whisper API | Needs `OPENAI_API_KEY` (or a compatible endpoint) |
128
+ | VideoCaptioner CLI | Needs the `videocaptioner` package installed separately |
129
+ | Bcut | Host answers, but returns **zero utterances** |
130
+ | Google Web (`[stt]`) | Returns the empty result `{"result":[]}` for **every** request |
131
+
132
+ Both key-free endpoints are reverse-engineered and neither transcribes any
133
+ more. Google Web's response is also malformed at the HTTP level, so the read
134
+ fails outright rather than merely returning nothing. Bcut's failure is the more
135
+ ambiguous of the two -- the host answers, so it could be a quota or a changed
136
+ field rather than a dead endpoint -- which is why it keeps an "unverified" label
137
+ in the source.
138
+
139
+ So a bare `uvx porter-workflow "<URL>"` will download and standardise the video
140
+ and then fail at the transcription phase with
141
+ `every speech-to-text backend failed`. To get subtitles, do one of:
142
+
143
+ ```bash
144
+ # Option A: an LLM key (also improves translation quality a lot)
145
+ export OPENAI_API_KEY=sk-...
146
+ porter "<URL>" --burn skip
147
+
148
+ # Option B: install VideoCaptioner and use its engines
149
+ pip install videocaptioner
150
+ porter "<URL>" --asr-engine bijian --burn skip
151
+ ```
152
+
153
+ **Translation is unaffected.** Bing, Google and MyMemory all still work without
154
+ a key; each was probed against its live endpoint and returned real Chinese. Only
155
+ transcription needs credentials.
156
+
157
+ `porter doctor` reports which route a job will actually take before you start it.
158
+
159
+ ### Optional extras
160
+
161
+ | Extra | Adds | Enables |
162
+ | --- | --- | --- |
163
+ | `[llm]` | `openai`, `json-repair` | LLM translation + Whisper API ASR |
164
+ | `[stt]` | `SpeechRecognition` | Google Web STT — **measured non-functional**, see below |
165
+ | `[images]` | `pillow` | Cover image handling |
166
+ | `[mcp]` | `fastmcp` | The `porter-mcp` server |
167
+ | `[all]` | all of the above | — |
168
+
169
+ `videocaptioner` is **deliberately not a dependency.** It is GPL-3.0 (this
170
+ project is MIT) and pins `python<3.13`. Porter detects it at runtime and, if
171
+ present, uses it as an extra ASR/translation backend across a process boundary.
172
+ Install it yourself if you want those engines:
173
+
174
+ ```bash
175
+ pip install videocaptioner
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Usage
181
+
182
+ ```bash
183
+ # Inspect a link without downloading anything
184
+ porter inspect "<URL>"
185
+
186
+ # Full pipeline: download → transcribe → translate → burn
187
+ porter "<URL>" -o ./porter_output
188
+
189
+ # Subtitles only, no video encoding
190
+ porter "<URL>" --burn skip
191
+
192
+ # Diagnose the environment
193
+ porter doctor
194
+
195
+ # Show resolved configuration (secrets masked)
196
+ porter config list
197
+ ```
198
+
199
+ ### Output layout
200
+
201
+ ```
202
+ <output_dir>/<video_id>_<safe_title>/
203
+ ├── raw/
204
+ │ ├── video.mp4 H.264 + AAC + faststart master
205
+ │ ├── audio.wav 16 kHz mono reference track
206
+ │ ├── audio_enhanced.wav denoised track used only for ASR
207
+ │ ├── transcript.json/.txt reconstructed sentence script book
208
+ │ ├── subtitle.srt platform-provided source subtitles, if any
209
+ │ ├── cover.jpg
210
+ │ └── metadata.json
211
+ └── cooked/
212
+ ├── subtitle_bilingual.{srt,ass}
213
+ ├── subtitle_zh.{srt,ass}
214
+ ├── video_bilingual.mp4
215
+ └── video_zh.mp4
216
+ ```
217
+
218
+ ### Configuration
219
+
220
+ Resolved in this order, highest priority first:
221
+
222
+ 1. `--config <path>`
223
+ 2. `$PORTER_CONFIG`
224
+ 3. Environment variables (`OPENAI_API_KEY`, `PORTER_ASR_ENGINE`, …)
225
+ 4. Project-level `./porter.json`
226
+ 5. User-level config directory (`platformdirs`)
227
+ 6. Built-in defaults
228
+
229
+ ---
230
+
231
+ ## MCP server
232
+
233
+ ```json
234
+ {
235
+ "mcpServers": {
236
+ "porter": {
237
+ "command": "uvx",
238
+ "args": ["--from", "porter-workflow[mcp]", "porter-mcp"]
239
+ }
240
+ }
241
+ }
242
+ ```
243
+
244
+ Long jobs are exposed as a start/status/result/cancel API rather than one
245
+ blocking call, because encoding a 1080p video can take tens of minutes.
246
+
247
+ ---
248
+
249
+ ## Development
250
+
251
+ ```bash
252
+ uv sync --extra all --extra dev # installs exactly what uv.lock pins
253
+
254
+ ruff check src # includes T20: the engine must not print()
255
+ mypy src
256
+ lint-imports # enforces the engine/frontend boundary
257
+ pytest
258
+ ```
259
+
260
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) documents the full gate, the architecture
261
+ rules those tools enforce, and the licence rules for new dependencies.
262
+
263
+ Repository layout:
264
+
265
+ ```
266
+ src/porter/ engine (library) — all business logic lives here
267
+ src/porter_cli/ CLI frontend
268
+ src/porter_mcp/ MCP frontend
269
+ skills/porter-skill/ Agent Skill assets (SKILL.md, scripts, references)
270
+ tests/{unit,regression,integration}/
271
+ ```
272
+
273
+ ---
274
+
275
+ ## License
276
+
277
+ MIT. See [LICENSE](LICENSE).
278
+
279
+ ---
280
+
281
+ ## Acknowledgements
282
+
283
+ Porter is built on other people's work, and the debt is worth stating plainly:
284
+
285
+ * **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** — downloading from five
286
+ platforms is a solved problem rather than five scrapers, because of this.
287
+ * **[VideoCaptioner](https://github.com/WEIFENG2333/VideoCaptioner)** (GPL-3.0) —
288
+ its sentence segmentation, alignment and subtitle-processing design shaped the
289
+ approach in `porter.subtitles` and the ASR chain. Ideas only: no code was
290
+ copied, and it is never imported — only run as an external process, which is
291
+ why porter can stay MIT.
292
+ * **[FFmpeg](https://ffmpeg.org/) and
293
+ [libass](https://github.com/libass/libass)** — the entire media layer:
294
+ standardisation, denoising and professional ASS rendering.
295
+
296
+ [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) records every component,
297
+ its licence, and whether porter depends on it, optionally installs it, or only
298
+ spawns it as a subprocess.
299
+
300
+ ---
301
+
302
+ ## Contributing
303
+
304
+ Contributions are welcome. [`CONTRIBUTING.md`](CONTRIBUTING.md) covers the
305
+ development setup and the gate every change must pass; [`SECURITY.md`](SECURITY.md)
306
+ explains how to report a vulnerability privately; release history lives in
307
+ [`CHANGELOG.md`](CHANGELOG.md).
308
+
309
+ This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md).