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.
- porter_workflow-0.2.0/.gitignore +110 -0
- porter_workflow-0.2.0/CHANGELOG.md +239 -0
- porter_workflow-0.2.0/LICENSE +21 -0
- porter_workflow-0.2.0/PKG-INFO +309 -0
- porter_workflow-0.2.0/README.md +256 -0
- porter_workflow-0.2.0/THIRD_PARTY_NOTICES.md +175 -0
- porter_workflow-0.2.0/pyproject.toml +240 -0
- porter_workflow-0.2.0/skills/porter-skill/README.md +70 -0
- porter_workflow-0.2.0/skills/porter-skill/SKILL.md +241 -0
- porter_workflow-0.2.0/skills/porter-skill/assets/config.example.json +59 -0
- porter_workflow-0.2.0/skills/porter-skill/references/ARCHITECTURE.md +147 -0
- porter_workflow-0.2.0/skills/porter-skill/references/CONFIG.md +154 -0
- porter_workflow-0.2.0/skills/porter-skill/references/MCP.md +120 -0
- porter_workflow-0.2.0/skills/porter-skill/scripts/bootstrap.sh +40 -0
- porter_workflow-0.2.0/skills/porter-skill/scripts/inspect.sh +13 -0
- porter_workflow-0.2.0/skills/porter-skill/scripts/porter.sh +14 -0
- porter_workflow-0.2.0/src/porter/__init__.py +143 -0
- porter_workflow-0.2.0/src/porter/asr/__init__.py +59 -0
- porter_workflow-0.2.0/src/porter/asr/base.py +169 -0
- porter_workflow-0.2.0/src/porter/asr/bcut.py +304 -0
- porter_workflow-0.2.0/src/porter/asr/chain.py +442 -0
- porter_workflow-0.2.0/src/porter/asr/google_web.py +296 -0
- porter_workflow-0.2.0/src/porter/asr/platform_subs.py +123 -0
- porter_workflow-0.2.0/src/porter/asr/videocaptioner.py +241 -0
- porter_workflow-0.2.0/src/porter/asr/whisper_api.py +193 -0
- porter_workflow-0.2.0/src/porter/asr/whisper_local.py +360 -0
- porter_workflow-0.2.0/src/porter/config.py +398 -0
- porter_workflow-0.2.0/src/porter/context.py +105 -0
- porter_workflow-0.2.0/src/porter/doctor/__init__.py +55 -0
- porter_workflow-0.2.0/src/porter/doctor/guides.py +166 -0
- porter_workflow-0.2.0/src/porter/doctor/probes.py +673 -0
- porter_workflow-0.2.0/src/porter/errors.py +138 -0
- porter_workflow-0.2.0/src/porter/events.py +194 -0
- porter_workflow-0.2.0/src/porter/jobs/__init__.py +44 -0
- porter_workflow-0.2.0/src/porter/jobs/records.py +695 -0
- porter_workflow-0.2.0/src/porter/jobs/store.py +332 -0
- porter_workflow-0.2.0/src/porter/logging.py +157 -0
- porter_workflow-0.2.0/src/porter/media/__init__.py +75 -0
- porter_workflow-0.2.0/src/porter/media/burn.py +479 -0
- porter_workflow-0.2.0/src/porter/media/encode.py +472 -0
- porter_workflow-0.2.0/src/porter/media/enhance.py +102 -0
- porter_workflow-0.2.0/src/porter/media/ffmpeg.py +367 -0
- porter_workflow-0.2.0/src/porter/media/prepare.py +210 -0
- porter_workflow-0.2.0/src/porter/media/probe.py +189 -0
- porter_workflow-0.2.0/src/porter/media/standardize.py +199 -0
- porter_workflow-0.2.0/src/porter/models/__init__.py +25 -0
- porter_workflow-0.2.0/src/porter/models/inspection.py +116 -0
- porter_workflow-0.2.0/src/porter/models/materials.py +128 -0
- porter_workflow-0.2.0/src/porter/models/metadata.py +47 -0
- porter_workflow-0.2.0/src/porter/models/plan.py +113 -0
- porter_workflow-0.2.0/src/porter/models/request.py +165 -0
- porter_workflow-0.2.0/src/porter/models/subtitle.py +120 -0
- porter_workflow-0.2.0/src/porter/pipeline.py +505 -0
- porter_workflow-0.2.0/src/porter/plan.py +373 -0
- porter_workflow-0.2.0/src/porter/platforms/__init__.py +60 -0
- porter_workflow-0.2.0/src/porter/platforms/base.py +746 -0
- porter_workflow-0.2.0/src/porter/platforms/bilibili.py +57 -0
- porter_workflow-0.2.0/src/porter/platforms/downloader.py +89 -0
- porter_workflow-0.2.0/src/porter/platforms/inspector.py +280 -0
- porter_workflow-0.2.0/src/porter/platforms/instagram.py +49 -0
- porter_workflow-0.2.0/src/porter/platforms/local.py +282 -0
- porter_workflow-0.2.0/src/porter/platforms/registry.py +231 -0
- porter_workflow-0.2.0/src/porter/platforms/spec.py +158 -0
- porter_workflow-0.2.0/src/porter/platforms/tiktok.py +43 -0
- porter_workflow-0.2.0/src/porter/platforms/titles.py +132 -0
- porter_workflow-0.2.0/src/porter/platforms/urls.py +223 -0
- porter_workflow-0.2.0/src/porter/platforms/x.py +37 -0
- porter_workflow-0.2.0/src/porter/platforms/ydl.py +431 -0
- porter_workflow-0.2.0/src/porter/platforms/youtube.py +47 -0
- porter_workflow-0.2.0/src/porter/ports.py +158 -0
- porter_workflow-0.2.0/src/porter/py.typed +1 -0
- porter_workflow-0.2.0/src/porter/subtitles/__init__.py +71 -0
- porter_workflow-0.2.0/src/porter/subtitles/ass.py +274 -0
- porter_workflow-0.2.0/src/porter/subtitles/phrasing.py +765 -0
- porter_workflow-0.2.0/src/porter/subtitles/srt.py +432 -0
- porter_workflow-0.2.0/src/porter/subtitles/transcript.py +286 -0
- porter_workflow-0.2.0/src/porter/translate/__init__.py +67 -0
- porter_workflow-0.2.0/src/porter/translate/base.py +199 -0
- porter_workflow-0.2.0/src/porter/translate/bing.py +467 -0
- porter_workflow-0.2.0/src/porter/translate/chain.py +477 -0
- porter_workflow-0.2.0/src/porter/translate/google.py +354 -0
- porter_workflow-0.2.0/src/porter/translate/llm.py +324 -0
- porter_workflow-0.2.0/src/porter/translate/mymemory.py +165 -0
- porter_workflow-0.2.0/src/porter/translate/reuse.py +176 -0
- porter_workflow-0.2.0/src/porter/translate/videocaptioner.py +273 -0
- porter_workflow-0.2.0/src/porter/utils/__init__.py +17 -0
- porter_workflow-0.2.0/src/porter/utils/text.py +61 -0
- porter_workflow-0.2.0/src/porter/utils/time.py +53 -0
- porter_workflow-0.2.0/src/porter_cli/__init__.py +13 -0
- porter_workflow-0.2.0/src/porter_cli/__main__.py +17 -0
- porter_workflow-0.2.0/src/porter_cli/app.py +131 -0
- porter_workflow-0.2.0/src/porter_cli/commands/__init__.py +14 -0
- porter_workflow-0.2.0/src/porter_cli/commands/config.py +131 -0
- porter_workflow-0.2.0/src/porter_cli/commands/doctor.py +130 -0
- porter_workflow-0.2.0/src/porter_cli/commands/inspect.py +86 -0
- porter_workflow-0.2.0/src/porter_cli/commands/jobs.py +216 -0
- porter_workflow-0.2.0/src/porter_cli/commands/plan.py +198 -0
- porter_workflow-0.2.0/src/porter_cli/commands/run.py +277 -0
- porter_workflow-0.2.0/src/porter_cli/render.py +226 -0
- porter_workflow-0.2.0/src/porter_mcp/__init__.py +22 -0
- porter_workflow-0.2.0/src/porter_mcp/limits.py +34 -0
- porter_workflow-0.2.0/src/porter_mcp/server.py +90 -0
- porter_workflow-0.2.0/src/porter_mcp/shutdown.py +204 -0
- porter_workflow-0.2.0/src/porter_mcp/stdout_guard.py +153 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/__init__.py +49 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/config.py +99 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/docs.py +282 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/doctor.py +118 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/inspect.py +123 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/jobs.py +435 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/meta.py +40 -0
- porter_workflow-0.2.0/src/porter_mcp/tools/plan.py +106 -0
- 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).
|