The release step (softprops/action-gh-release) failed with 403 "Resource not
accessible by integration" because the default GITHUB_TOKEN is read-only. Add a
top-level permissions block so tag builds can create the Release and upload the
DMG. This is why v1.1.0/v1.2.0 built the DMG but never published a Release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump desktop app version 1.1.0 -> 1.2.0 and cut CHANGELOG [未发布] into
[1.2.0]. First release carrying PostHog analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- posthog.ts: drop redundant no-console disable (project has no no-console rule;
--report-unused-disable-directives failed CI lint)
- desktop-build.yml: pass VITE_PUBLIC_POSTHOG_KEY/HOST into the frontend build
so released DMGs actually report analytics (was no-op without the key)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bump version 1.0.0 → 1.1.0 (tauri.conf.json, Cargo.toml, Cargo.lock); the
build script now derives the DMG name from tauri.conf.json so it won't drift.
- RELEASE_NOTES.md + CHANGELOG.md: document v1.1.0 (zero-dependency desktop
install, end-to-end clip pipeline, black-screen / stuck-list / retry-spam /
stuck-pipeline fixes, opt-in faster-whisper, Gemini→google-genai, CI unify +
repo cleanup).
- Drop two stale doc links to files removed in the cleanup
(QUICK_START_GUIDE, SYSTEM_REBUILD_GUIDE).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the full clip pipeline and the Whisper-for-no-subtitle items out of
"gaps" (both verified end-to-end in the packaged app) and refresh the roadmap
and key-files list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The character class used a single-quoted raw string with embedded ASCII single
quotes (r'[...""''...]'), which Python parsed as two implicitly-concatenated
literals — silently dropping the quotes from the class and emitting a
SyntaxWarning for \s. Switch to a double-quoted raw string so the class is a
single, correctly-closed character class. Adds a unit test asserting the class
is well-formed and contains all intended CJK punctuation + whitespace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Videos without embedded subtitles (e.g. B站 without AI字幕) need Whisper to
generate subtitles, but bundling it would bloat every install. Instead let
users install it on demand from Settings → 语音识别, and pick which model.
Backend uses faster-whisper (CTranslate2, no PyTorch, ~214MB installed, several
times faster than openai-whisper, cross-platform) — chosen over mlx-whisper,
which hard-depends on torch (~2-3GB).
- whisper_runtime.py (new): pip-install faster-whisper into a user-writable dir
(<data>/whisper-runtime) using the bundled Python; add to sys.path; status +
coarse progress; uninstall. Never writes into the signed .app bundle.
- whisper_model_manager.py: tiny→large-v3 from Systran/faster-whisper-*,
background download via huggingface_hub, real status; cache under
<data>/whisper-models.
- speech_recognizer.py: subtitle generation rewritten from the `whisper` CLI to
faster-whisper's WhisperModel API → SRT; availability = runtime installed.
- speech_recognition.py API: /whisper/install, /whisper/uninstall,
/whisper/runtime-status (+ existing /whisper-models*).
- SpeechRecognitionConfig.tsx: was a stub; now a full UI (install button +
progress + log, model list with download/delete/status) wired to a new
speechApi in services/api.ts.
- build_macos_arm.sh: allowlist faster_whisper/ctranslate2/huggingface_hub in
the dependency guard (they're installed at runtime, imported lazily).
Verified end-to-end: install runtime → download tiny model → transcribe a real
video into a valid SRT, all through the API/runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Once the pipeline actually executes locally (previous commit), the multiple
dispatch entry points became a real hazard: clicking retry fires /retry while
the frontend's auto-start fires /process for the now-pending project, so two
process_video_pipeline runs start ~1s apart and race on the same DB Task row:
"Instance '<Task ...>' has been deleted, or its row is otherwise not present".
Add an in-process concurrency guard: a module-level set of active project_ids
behind a lock. process_video_pipeline skips immediately if the project already
has a pipeline running, and releases the slot in the inner finally (plus a
belt-and-suspenders release in the outer except for very-early failures).
Verified: a single retry runs exactly one pipeline (Step 1 executes); firing
/retry and /process back-to-back yields zero "has been deleted" DB races.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Desktop processing never ran — projects stuck at 0% "初始化中" forever.
Root cause: the pipeline is dispatched with Celery (process_video_pipeline.delay()
from /process, /retry, import; and send_task from auto_pipeline). core.celery_app
points at redis://localhost:6379, which doesn't exist in the desktop bundle, and
the only worker started (desktop_celery) uses a *different* (filesystem) broker.
So tasks were queued to a broker nobody consumed.
Fix (no Redis, no broker, non-blocking):
- core/celery_app.py: a DesktopAwareTask base whose apply_async, in desktop mode,
runs the task via .apply() in a background daemon thread and returns a
lightweight result. Covers every .delay()/.apply_async() call site at once.
Production (server) mode is unchanged — it falls through to the real broker.
- task_submission_utils.py: submit_video_pipeline_task (which uses app.send_task,
not Task.apply_async) gets the same local-thread path in desktop mode, and no
longer hits the hardcoded redis.Redis(localhost) debug probe.
The pipeline task itself already runs the whole flow inline
(asyncio.run(pipeline_adapter...)), so local execution is a perfect fit;
progress is written to the DB Task record for the UI to poll.
Verified: retrying a subtitle-bearing project now runs Step 1 (大纲, 6 topics via
DashScope) → Step 2 (timeline), with progress events 0%→17%→25%. Processing
works end-to-end in the packaged app for videos that have subtitles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous fix stopped the failure-toast spam but exposed a success-toast
loop. handleRetryProject calls loadProjects, which sets loading=true and swaps
the project list for HomePage's "正在加载项目列表" spinner — unmounting every
ProjectCard. That reset the useRef auto-start guard, so on remount auto-start
fired again → onRetry → handleRetryProject → loadProjects → unmount → ...
an infinite loop, one "已开始重试处理项目" toast per cycle (and the flickering
spinner in the report).
Three cuts so the loop can't form:
1. Auto-start guard moved from useRef to a module-level Set<projectId>, which
survives the remount. Project ids are unique per import → once per session.
2. Silent auto-start no longer calls onRetry — it must never drive the parent's
toast/reload path. Only user-clicked retries notify the parent.
3. handleRetryProject no longer re-issues retryProcessing (the card already
sent the request); it just toasts once and refreshes.
Frontend typecheck clean; production build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Importing a B站/YouTube video left the project `pending` while it downloaded.
ProjectCard had a useEffect that auto-called handleRetry() for any pending
project, with `isRetrying` in its deps — and handleRetry flips `isRetrying` in
its finally block. That created a tight loop: every cycle POSTed
/projects/{id}/process, which 400s with "Video file not found" until the
download finishes, and each failure popped a "重试失败,请稍后再试" toast.
Result: the screen filled with error toasts.
Frontend (ProjectCard.tsx):
- Auto-start now fires at most once per project (useRef guard, reset on id
change) and is removed from the isRetrying-driven loop.
- Auto-start failures are silent; only user-clicked retry buttons show the
toast. (Uploads still auto-start; B站 imports still get their one harmless
best-effort kick, and the backend auto-starts the pipeline when the download
actually completes.)
Backend (bilibili.py):
- When the download task throws, also mark the *project* FAILED (not just the
task). Previously it stayed `pending` forever, which is exactly what the
frontend kept trying to auto-start. Now a failed download shows a failed
card with an error_message.
Verified: the B站 download path itself works end-to-end (downloads video, finds
AI subtitle, starts pipeline). Frontend typecheck clean; bilibili.py parses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the old pre-PBS handoff notes with a single source of truth:
architecture, what's done and verified (packaging pipeline end-to-end, CI
unification, cleanup, Gemini migration), the remaining gaps (full E2E clip
run, notarization, multi-platform, dep pinning, build speed), and the roadmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the CI unification, the PyInstaller / prepare_resources route scripts
were fully orphaned (referenced only by each other and stale docs). Remove the
cluster:
build_backend.py, prepare_resources.py, check_bundle_size.py, smoke_api.py,
build_desktop.py, dev_desktop.py, test_desktop.py, quick_build_check.sh,
build_release.py
scripts/ now holds only what's live: build_macos_arm.sh (the one packaging
route), verify_desktop.sh (backend smoke), monitor_whisper.py (runtime).
Rewrite scripts/README.md and BUILD_GUIDE.md to describe the single PBS route
accurately (they previously documented the dead PyInstaller flow), and fix the
stale build command in RELEASE_CHECKLIST.md.
Also removed a 94M stale _autoclip-backend-pyinstaller-bak left in
src-tauri/resources/ (untracked build junk).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
google-generativeai is deprecated and no longer maintained. Switch
GeminiProvider to the unified google-genai SDK:
- genai.configure() + GenerativeModel(...).generate_content(...)
→ genai.Client(api_key=...).models.generate_content(model=..., contents=...)
- max-tokens hint now mapped onto types.GenerateContentConfig
- requirements.txt: google-generativeai>=0.3.0 → google-genai>=1.0.0
- install_llm_dependencies.py: same package rename
Verified the new SDK exposes Client + models.generate_content +
GenerateContentConfig, and the module parses clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The installed app opened but the project list was stuck forever on
"正在加载项目列表...". Root cause: GET /api/v1/projects/ returned HTTP 500
with `ModuleNotFoundError: No module named 'pytz'`. The portable build only
installs requirements.txt, but the backend imported packages the dev venv had
that were never listed there.
A static scan of backend imports against the portable runtime found 4 missing:
- pytz (broke the project list — hot path)
- openai } LLM provider SDKs — the AI clipping pipeline. These
- google-generativeai } lived only in install_llm_dependencies.py, so the
- dashscope } bundle shipped without them and any provider failed.
Add all 4 to requirements.txt. Verified the rebuilt app returns 200 for
/api/v1/projects/ on a clean PATH.
Also add a build-time dependency-completeness check to build_macos_arm.sh:
after installing deps + copying the backend, it AST-scans every third-party
import and fails the build if any can't be resolved in the portable runtime.
This turns "works in dev, 500s in the bundle" into a hard build error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There were 7 desktop-build workflows across two dead packaging routes
(PyInstaller via build_backend.py, and prepare_resources.py), none of which
ever produced a working installable app — they're the red builds in issue #65.
Replace all of them with a single desktop-build.yml that runs the proven
python-build-standalone route (scripts/build_macos_arm.sh): portable Python +
backend source + static ffmpeg/ffprobe → ad-hoc signed .app → DMG. Triggers on
manual dispatch and on v* tags (attaches the DMG to a GitHub Release). Caches
the PBS + ffmpeg downloads and the Rust build.
Removed:
build-all-platforms.yml, build-cross-platform.yml, build-desktop.yml,
build-linux.yml, build-universal.yml, build-windows.yml, build-x86-macos.yml
Kept: ci.yml (tests), i18n-sync.yml (docs), nightly-desktop-smoke.yml
(backend smoke via verify_desktop.sh).
Currently macOS arm64 only — the validated target. The script generalizes to
other platforms later (PBS + static ffmpeg exist for them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The packaged app opened to a pure black screen. Root cause was a frontend
bundling bug, not the Tauri packaging:
vite.config.ts manualChunks put antd in `vendor-antd` and React in
`vendor-react`. antd's top-level code runs `React.createContext` at module
eval; with the two in separate chunks the load order isn't guaranteed, so
antd evaluated before React's CJS interop was initialized:
Uncaught TypeError: Cannot read properties of undefined
(reading 'createContext') at vendor-antd-*.js
React never mounted, #root stayed empty, and the dark body background
(rgb(15,15,15)) showed as a black screen — no visible error.
Fix: remove the manual React/antd chunk split and let Rollup order chunks
itself. Verified in a headless browser against the built dist: React mounts,
"App组件已加载" logs, full UI renders, no createContext error. Rebuilt the
desktop app and confirmed it embeds the single bundle and serves
/api/v1/video-categories (200) that the home screen needs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop build started the backend fine on the build machine but would
fail on any clean Mac: video processing needs ffmpeg/ffprobe and neither was
usable for distribution.
Three compounding bugs, all fixed:
1. backend_manager.rs never set AUTOCLIP_FFMPEG_PATH, so the backend fell
back to `shutil.which("ffmpeg")` — i.e. the build machine's homebrew
ffmpeg. Now the launcher exports AUTOCLIP_FFMPEG_PATH / AUTOCLIP_FFPROBE_PATH
pointing at the bundled binaries (resources/ffmpeg/{ffmpeg,ffprobe}).
2. The build bundled only ffmpeg, not ffprobe (the backend needs both).
3. The bundled ffmpeg was homebrew's dynamic build (412KB, 57 /opt/homebrew
dylib deps) — dead on any machine without homebrew. The build now downloads
the STATIC arm64 ffmpeg+ffprobe (48MB each, zero non-system deps) from
osxexperts.net, cached under build/ffmpeg-cache, with a build-time assert
that fails if any homebrew dep reappears.
Also exclude stray *.rdb from the bundled backend source.
Verified end-to-end: full build → App+DMG; app launches with /opt/homebrew
stripped from PATH (clean-machine sim), backend starts, HTTP server responds;
bundled ffmpeg/ffprobe run under an empty environment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove .trae/ (Trae IDE notes superseded by docs/)
- Remove BUILD_SUMMARY.md, DESKTOP_ARCHITECTURE_FIX.md (one-off notes,
no references in code or docs)
- Remove src-tauri/resources/ffmpeg-unified/ffmpeg-macos (75MB binary
duplicate of src-tauri/resources/ffmpeg/, which is gitignored)
- .gitignore: also exclude src-tauri/resources/ffmpeg-unified/
Local-only cleanup not in this commit: celery.log, control/, venv_x86/.