Skip to content

feat: PyInstaller packaging with user data separation#1019

Open
WEIFENG2333 wants to merge 10 commits intomasterfrom
feat/pyinstaller-build
Open

feat: PyInstaller packaging with user data separation#1019
WEIFENG2333 wants to merge 10 commits intomasterfrom
feat/pyinstaller-build

Conversation

@WEIFENG2333
Copy link
Copy Markdown
Owner

@WEIFENG2333 WEIFENG2333 commented Feb 26, 2026

Summary

Add PyInstaller packaging support adapted to the new videocaptioner/ project structure, with proper user data separation for clean upgrades.

Build System

  • VideoCaptioner.spec — PyInstaller config, entry point __main__.py (supports both CLI and GUI)
  • scripts/build.py — One-command build with auto version detection, ffmpeg/7z download (Windows), and bundle verification
  • .github/workflows/build.yml — CI for Windows + macOS with smoke tests and artifact uploads

User Data Separation

User data now lives in the system standard directory instead of the program folder, so upgrading is just replacing the program directory:

Data Location
Settings, logs, cache, models, styles Windows: %LOCALAPPDATA%/VideoCaptioner/
macOS: ~/Library/Application Support/VideoCaptioner/
Binaries (ffmpeg, 7z) Program directory resource/bin/
Work directory Program directory work-dir/

Frozen Mode Fixes

  • config.py — Three-mode path detection (frozen / dev / pip), unified platformdirs for frozen + pip
  • ui/main.py — Qt plugin path for _internal/PyQt5/Qt5/plugins
  • ass_renderer.py — Write generated images to CACHE_PATH instead of read-only RESOURCE_PATH
  • build.py — Auto-generate _version.py from git tags, retry logic for binary downloads
  • Switch to ffmpeg essentials build (~96MB vs ~192MB)

Changed Files (6 files, +671 lines)

  • .github/workflows/build.yml — New CI workflow
  • VideoCaptioner.spec — New PyInstaller spec
  • scripts/build.py — New build script
  • videocaptioner/config.py — Rewritten with data separation
  • videocaptioner/core/subtitle/ass_renderer.py — Fix read-only path write
  • videocaptioner/ui/main.py — Frozen mode Qt plugin path

Test plan

  • Local Linux build + CLI test (--help, --version, config, subtitle processing, style list)
  • CI Windows build + smoke test (App started successfully)
  • CI macOS build + smoke test (App started successfully)
  • User data directory auto-created in system standard location
  • Subtitle style presets auto-copied on first run
  • ffmpeg + 7z bundled and verified (Windows)
  • Merged with latest master (subtitle style refactor) — no conflicts

🤖 Generated with Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented Feb 26, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Comment thread app/config.py Outdated
@claude
Copy link
Copy Markdown

claude Bot commented Feb 26, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread scripts/build.py
for m in missing:
print(f" - {m}")
else:
print("All expected resources found in bundle.")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify function silently passes with missing critical resources

Medium Severity

The verify() function only prints a WARNING when critical bundled resources (like logo.png, fonts, translations) are missing from the build output, but does not exit with a non-zero code. Since verify() is called at the end of every build.py run (including CI), a build with missing resources will pass CI, get uploaded as an artifact, and crash at runtime when those resources are accessed.

Fix in Cursor Fix in Web

Comment thread scripts/build.py

# Windows binary download URLs
FFMPEG_URL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
SEVENZIP_URL = "https://7-zip.org/a/7zr.exe"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong 7-Zip variant downloaded and renamed misleadingly

Low Severity

SEVENZIP_URL points to 7zr.exe, a reduced standalone 7-Zip variant that only supports .7z archives. This binary is saved as 7z.exe, which normally refers to the full 7-Zip command-line tool supporting dozens of archive formats. While the current codebase only extracts .7z files, the renamed binary silently lacks support for other formats that 7z.exe is expected to handle.

Additional Locations (1)

Fix in Cursor Fix in Web

data = tomllib.load(f)
deps = data['project']['dependencies']
subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + deps)
"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI ignores platform-specific PyQt5-Qt5 version pin on Windows

Medium Severity

The CI installs dependencies via pip by reading pyproject.toml's [project] dependencies, but the project's [tool.uv] override-dependencies deliberately pins PyQt5-Qt5==5.15.2 on Windows. Since pip doesn't understand uv overrides, the Windows CI build installs the latest PyQt5-Qt5 (currently 5.15.16) instead of the explicitly required 5.15.2. This means the Windows artifact bundles a different Qt binary version than what developers test with, potentially causing runtime issues.

Fix in Cursor Fix in Web

- Add VideoCaptioner.spec with entry point videocaptioner/ui/main.py
- Add scripts/build.py with ffmpeg/7z download and writable resource copy
- Add .github/workflows/build.yml for Windows + macOS CI builds
- Update config.py with sys.frozen detection for PyInstaller paths
- Update ui/main.py with frozen-mode Qt plugin path resolution

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@WEIFENG2333 WEIFENG2333 force-pushed the feat/pyinstaller-build branch from 48f86b0 to 19452ca Compare March 28, 2026 18:36
@claude
Copy link
Copy Markdown

claude Bot commented Mar 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

The 7-zip.org server sometimes resets connections on CI runners.
Downloads now retry up to 3 times with backoff, and failures are
warnings instead of fatal errors.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

- ass_renderer.py: Generate default_bg.png in CACHE_PATH instead of
  RESOURCE_PATH, which is read-only in PyInstaller frozen mode
- build.py: Auto-generate videocaptioner/_version.py before building
  so the packaged app has the correct version number

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Without fetch-depth: 0, git describe only sees the commit hash
instead of the nearest tag, resulting in version like "d9d6206"
instead of "1.5.0".

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

The previous entry point (ui/main.py) only launched GUI. Using
__main__.py routes through cli/main.py which supports both CLI
subcommands and auto-launches GUI when no args are given.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

The full GPL build includes every codec (x265, svtav1, dav1d, etc.)
but the project only needs libx264, libvpx, and libass. The gyan.dev
essentials build has all of these at roughly half the size.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Move user data (settings, logs, cache, models, subtitle styles) to
the system standard data directory so upgrading the program is just
replacing the program folder.

Program directory (replaceable):
  - exe + _internal/, resource/bin/, work-dir/

User data directory (persistent across upgrades):
  - Windows: %LOCALAPPDATA%/VideoCaptioner/
  - macOS:   ~/Library/Application Support/VideoCaptioner/
  - Linux:   ~/.local/share/VideoCaptioner/
  - Contains: settings.json, logs/, cache/, models/, subtitle_style/

Also update build.py to stop copying subtitle_style to dist/ since
it's now managed by config.py first-run initialization.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 29, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Both frozen (PyInstaller) and pip install modes now use platformdirs
for user data, so settings/cache/models/logs are always in the system
standard directory. Only dev mode uses project-local AppData/.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 29, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

Weifeng and others added 2 commits March 29, 2026 15:21
The upstream refactored subtitle styles from .txt to .json format
(ass-default.json, rounded-default.json, etc.) and switched the
default font to NotoSansSC-Regular.ttf.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@claude
Copy link
Copy Markdown

claude Bot commented Mar 29, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@WEIFENG2333 WEIFENG2333 changed the title feat: Add PyInstaller packaging and CI/CD build workflow feat: PyInstaller packaging with user data separation Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant