diff --git a/.github/workflows/motion-readback-qualification.yml b/.github/workflows/motion-readback-qualification.yml new file mode 100644 index 00000000..34fb076a --- /dev/null +++ b/.github/workflows/motion-readback-qualification.yml @@ -0,0 +1,72 @@ +name: Motion readback qualification + +on: + push: + branches: ['release/**'] + paths: + - 'crates/opentake-motion/**' + - '.github/workflows/motion-readback-qualification.yml' + workflow_dispatch: + inputs: + commit_sha: + description: Exact candidate commit for Motion readback qualification + required: true + type: string + +permissions: + contents: read + +jobs: + motion-readback: + name: Motion readback (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2022] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + env: + CARGO_BUILD_JOBS: 2 + OPENTAKE_MOTION_TRACE: '1' + steps: + - name: Validate immutable source input + shell: pwsh + env: + SOURCE_SHA: ${{ inputs.commit_sha || github.sha }} + run: | + if ($env:SOURCE_SHA -cnotmatch '^[0-9a-f]{40}$') { throw 'Expected exact 40-hex source SHA' } + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ inputs.commit_sha || github.sha }} + persist-credentials: false + - name: Cache qualification dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: motion-readback-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + - name: Assert source and run complete Motion qualification + shell: pwsh + env: + SOURCE_SHA: ${{ inputs.commit_sha || github.sha }} + run: | + if ((git rev-parse HEAD).Trim() -cne $env:SOURCE_SHA) { throw 'Source SHA mismatch' } + rustc --version + cargo --version + $output = cargo test --locked -p opentake-motion --features chromium --lib --test chromium --jobs 2 -- --nocapture --test-threads=1 2>&1 + $testExit = $LASTEXITCODE + $output | Set-Content -Encoding utf8 motion-readback-qualification.log + $output | ForEach-Object { Write-Output $_ } + if ($testExit -ne 0) { throw 'Motion readback qualification failed' } + $text = $output -join "`n" + if ($text -notmatch 'test result: ok\. 97 passed; 0 failed; 0 ignored;') { throw 'Expected all 97 Motion unit tests' } + if ($text -notmatch 'test result: ok\. 7 passed; 0 failed; 0 ignored;') { throw 'Expected all 7 Chromium integration tests' } + if ($text -notmatch '4K opaque single-frame elapsed_ms=\d+' -or $text -notmatch '4K transparent single-frame elapsed_ms=\d+') { throw 'Both real 4K paths must execute' } + - name: Preserve qualification evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: motion-readback-${{ matrix.os }}-${{ inputs.commit_sha || github.sha }} + path: motion-readback-qualification.log diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f340e6c..38a9f613 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,8 +160,8 @@ jobs: expected_version = "1.0.0-beta.4" expected_wix_version = "1.0.0.4" else: - expected_version = "1.0.0-beta.5" - expected_wix_version = "1.0.0.5" + expected_version = "1.0.0-beta.6" + expected_wix_version = "1.0.0.6" if "+" in tag: raise SystemExit("SemVer build metadata is unsupported for updater asset URLs") if SEMVER_RE.fullmatch(tag) is None: @@ -188,8 +188,8 @@ jobs: if not notes.is_file() or not notes.read_text(encoding="utf-8").strip(): raise SystemExit(f"release notes are missing or empty: {notes}") prerelease = "-" in version.split("+", 1)[0] - if version == "1.0.0-beta.5" and not prerelease: - raise SystemExit("OpenTake 1.0.0-beta.5 must remain a prerelease") + if version == "1.0.0-beta.6" and not prerelease: + raise SystemExit("OpenTake 1.0.0-beta.6 must remain a prerelease") if not prerelease: raise SystemExit("this release workflow publishes prereleases only") @@ -333,7 +333,7 @@ jobs: > "$tooling_root/test_provision_ffmpeg_sidecars.py" git cat-file blob "$RELEASE_TOOLING_SHA:.github/workflows/release.yml" \ > "$tooling_root/release.yml" - git cat-file blob "$RELEASE_TOOLING_SHA:docs/releases/1.0.0-beta.5.md" \ + git cat-file blob "$RELEASE_TOOLING_SHA:docs/releases/1.0.0-beta.6.md" \ > "$tooling_root/release-notes.md" test -s "$tooling_root/check_release_workflow.py" test -s "$tooling_root/test_check_release_workflow.py" @@ -372,7 +372,7 @@ jobs: run: | set -euo pipefail case "$OPENTAKE_EXPECTED_RELEASE_VERSION" in - 1.0.0-beta.5) + 1.0.0-beta.6) python3 -B -m unittest discover -s scripts -p 'test_check_license_inventory.py' python3 -B scripts/check_license_inventory.py ;; diff --git a/.github/workflows/semantic-model-qualification.yml b/.github/workflows/semantic-model-qualification.yml new file mode 100644 index 00000000..3f767227 --- /dev/null +++ b/.github/workflows/semantic-model-qualification.yml @@ -0,0 +1,65 @@ +name: Semantic model qualification + +on: + push: + branches: ['release/**'] + workflow_dispatch: + inputs: + commit_sha: + description: Exact candidate commit for real model qualification + required: true + type: string + +permissions: + contents: read + +jobs: + real-model: + name: Real SigLIP2 (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [macos-15, windows-2022] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Validate immutable source input + shell: pwsh + env: + SOURCE_SHA: ${{ inputs.commit_sha || github.sha }} + run: | + if ($env:SOURCE_SHA -cnotmatch '^[0-9a-f]{40}$') { throw 'Expected exact 40-hex source SHA' } + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ inputs.commit_sha || github.sha }} + persist-credentials: false + - name: Cache qualification dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: semantic-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + - name: Assert source and export production manifest + shell: pwsh + env: + SOURCE_SHA: ${{ inputs.commit_sha || github.sha }} + run: | + if ((git rev-parse HEAD).Trim() -cne $env:SOURCE_SHA) { throw 'Source SHA mismatch' } + cargo run --locked -p opentake-media --features model-download --example search_manifest --quiet > search-manifest.json + if ($LASTEXITCODE -ne 0) { throw 'Production manifest export failed' } + - name: Fetch checksum-pinned model and image fixtures + shell: pwsh + run: | + python scripts/prepare_search_model_fixtures.py --manifest search-manifest.json --output "$env:RUNNER_TEMP/search-fixtures" + if ($LASTEXITCODE -ne 0) { throw 'Fixture verification failed' } + - name: Exercise real installed model, embeddings and ranking + shell: pwsh + run: | + $env:OPENTAKE_SEARCH_MODEL_TEST_DIR = "$env:RUNNER_TEMP/search-fixtures" + $output = cargo test --locked -p opentake-media --features model-download,ort-backend --lib real_model_offline_install_embeddings_and_ranking -- --ignored --nocapture --test-threads=1 2>&1 + $testExit = $LASTEXITCODE + $output | ForEach-Object { Write-Output $_ } + if ($testExit -ne 0) { throw 'Real model qualification failed' } + if (-not ($output -match 'test result: ok\. 1 passed; 0 failed; 0 ignored;')) { throw 'Expected exactly one executed real-model test' } diff --git a/.github/workflows/windows-export-qualification.yml b/.github/workflows/windows-export-qualification.yml new file mode 100644 index 00000000..ddb87e1f --- /dev/null +++ b/.github/workflows/windows-export-qualification.yml @@ -0,0 +1,101 @@ +name: Windows export qualification + +on: + push: + branches: ['release/**'] + paths: + - 'src-tauri/src/export.rs' + - '.github/workflows/windows-export-qualification.yml' + workflow_dispatch: + inputs: + commit_sha: + description: Exact candidate commit for Windows export qualification + required: true + type: string + +permissions: + contents: read + +jobs: + windows-export: + runs-on: windows-2022 + timeout-minutes: 60 + env: + SOURCE_SHA: ${{ inputs.commit_sha || github.sha }} + CARGO_BUILD_JOBS: 2 + defaults: + run: + shell: pwsh + steps: + - name: Validate immutable source input + run: | + if ($env:SOURCE_SHA -cnotmatch '^[0-9a-f]{40}$') { throw 'Expected exact 40-hex source SHA' } + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ env.SOURCE_SHA }} + persist-credentials: false + - name: Cache Cargo target and dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: windows-export-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + - name: Bind source and provision locked Windows sidecars + run: | + $actual = (git rev-parse HEAD).Trim() + if ($actual -cne $env:SOURCE_SHA) { throw 'Source SHA mismatch' } + "source_sha=$actual" | Set-Content -Encoding utf8 windows-export-qualification.log + $provision = python scripts/provision_ffmpeg_sidecars.py --target x86_64-pc-windows-msvc 2>&1 + $provisionExit = $LASTEXITCODE + $provision | Set-Content -Encoding utf8 windows-export-provision.log + $provision | ForEach-Object { Write-Output $_ } + if ($provisionExit -ne 0) { throw 'Pinned FFmpeg provisioning failed' } + $toolsDir = Join-Path $env:RUNNER_TEMP 'opentake-export-ffmpeg' + New-Item -ItemType Directory -Force $toolsDir | Out-Null + foreach ($tool in @('ffmpeg', 'ffprobe')) { + $sidecar = Join-Path $env:GITHUB_WORKSPACE "src-tauri/binaries/$tool-x86_64-pc-windows-msvc.exe" + if (-not (Test-Path -LiteralPath $sidecar -PathType Leaf)) { throw "Missing pinned sidecar: $tool" } + $envName = 'OPENTAKE_' + $tool.ToUpperInvariant() + "$envName=$sidecar" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + # Legacy test helpers use bare executable names; keep them on the + # same provisioned bytes rather than the runner's unrelated FFmpeg. + Copy-Item -LiteralPath $sidecar -Destination (Join-Path $toolsDir "$tool.exe") -Force + } + $toolsDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Run debug Tauri export tests and verify required regressions + run: | + foreach ($sidecar in @($env:OPENTAKE_FFMPEG, $env:OPENTAKE_FFPROBE)) { + $version = & $sidecar -version 2>&1 + $versionExit = $LASTEXITCODE + $version | Add-Content -Encoding utf8 windows-export-sidecars.log + $version | ForEach-Object { Write-Output $_ } + if ($versionExit -ne 0) { throw 'Pinned FFmpeg sidecar is not runnable' } + } + $output = cargo test --locked -p opentake-tauri --lib export::tests --jobs 2 -- --nocapture --test-threads=1 2>&1 + $testExit = $LASTEXITCODE + $lines = @($output | ForEach-Object { "$_" }) + $lines | Add-Content -Encoding utf8 windows-export-qualification.log + $lines | ForEach-Object { Write-Output $_ } + if ($testExit -ne 0) { throw 'Tauri export qualification failed' } + $text = [regex]::Replace(($lines -join "`n"), '\x1b\[[0-?]*[ -/]*[@-~]', '') + foreach ($name in @( + 'export_output_cleanup_removes_active_partial_output_on_drop', + 'export_output_probe_reads_the_retained_file_before_cleanup' + )) { + $pattern = '(?m)^test export::tests::' + [regex]::Escape($name) + ' \.\.\. ok\s*$' + if ($text -notmatch $pattern) { throw "Required regression did not actually pass: $name" } + } + if ($text -notmatch '(?m)^test result: ok\. [1-9][0-9]* passed; 0 failed; 0 ignored;') { + throw 'Expected a nonempty export test group with zero failures and zero ignored tests' + } + - name: Preserve qualification evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: windows-export-${{ env.SOURCE_SHA }} + path: | + windows-export-qualification.log + windows-export-provision.log + windows-export-sidecars.log diff --git a/.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-2-report.md b/.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-2-report.md new file mode 100644 index 00000000..6e8991a1 --- /dev/null +++ b/.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-2-report.md @@ -0,0 +1,118 @@ +# Task 2 Report — Preview Temporal Remap Parity + +## Scope + +- Task: restore Preview controls for the native temporal-remap route after Task 1 routed compositor temporal timelines to Rust. +- Baseline commit: `ad0bd70` +- Task 1 dependency acknowledged: + - route work landed in `a833764` + - follow-up fix landed in `1013355` +- Modified files: + - `web/src/components/preview/Preview.tsx` + - `web/src/components/preview/Preview.test.tsx` + - `web/src/components/preview/previewEngine.test.ts` + +## Constraints Followed + +- Only changed the brief-listed Preview files plus this required task report. +- Did not modify Rust, upstream Swift, docs, or user audit files. +- Did not dispatch subagents or reviewers. +- Added/updated tests before touching production code. +- Reused the existing native playback surface, controller identity, clock, and capture flow. + +## Root Cause + +Task 1 already changed the route contract so temporal compositor timelines no longer return `unsupported` when Rust playback is available. + +That left two Preview-side gaps: + +- stale UI tests still treated `reversed` compositor timelines as unsupported +- the native timeline surface had no explicit `data-playback-surface="native"` marker, so the Preview contract could not assert the Rust surface directly + +No Rust/engine production route change was needed for temporal remap playback itself. + +## Red Phase + +Added temporal compositor coverage with a `text` clip using: + +- `reversed: true` +- `speed: 1.5` + +Focused command run before implementation: + +```bash +NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-localstorage-preview-remap-ui.json pnpm exec vitest run src/components/preview/Preview.test.tsx src/components/preview/previewEngine.test.ts +``` + +Observed failures before implementation: + +- old unsupported tests failed because the current route now correctly renders the Rust surface for temporal compositor timelines +- the new native-surface assertion failed because Preview did not emit `data-playback-surface="native"` on the timeline Rust path + +This confirmed the engine route was already correct, while the Preview test contract and one surface marker were outdated/incomplete. + +## Green Phase + +Production change in `Preview.tsx`: + +- wrapped the timeline `RustFrameBuffer` in a minimal container that emits `data-playback-surface="native"` only when `playbackRoute.kind === "rust"` + +Test updates: + +- added a shared temporal compositor fixture in `Preview.test.tsx` +- asserted that Rust-capable Preview shows: + - no `unsupported-playback-surface` + - `data-playback-surface="native"` + - enabled play/capture controls +- asserted that the same temporal compositor timeline still shows typed unsupported UI when Rust capability is unavailable +- updated the old generic unsupported Preview cases to use a genuinely unsupported `lottie` clip instead of the now-supported temporal compositor case +- updated `previewEngine.test.ts` so the temporal compositor route now expects: + - native playback start when capability resolves + - transport stop without native start when capability is unavailable + +## Verification + +Focused Preview/engine tests after the change: + +```bash +NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-localstorage-preview-remap-ui.json pnpm exec vitest run src/components/preview/Preview.test.tsx src/components/preview/previewEngine.test.ts +``` + +Result: + +- `2` test files passed +- `44` tests passed +- exit code `0` + +Build check: + +```bash +pnpm build +``` + +Result: + +- `tsc -b && vite build` passed +- Vite emitted pre-existing bundle-size / ineffective-dynamic-import warnings only +- exit code `0` + +## Commit + +Commit message: + +```text +feat(preview): enable temporal compositor controls +``` + +## Notes / Concerns + +- The worktree `.git` file still points at a stale absolute path under `/Users/lvbaiqing/...`, so plain `git` fails with `fatal: not a git repository: (null)`. +- Git operations for this task must use explicit: + +```bash +GIT_DIR='/Users/trip/TRUE 开发/PRIMARY-CN/OpenTake/.git/worktrees/OpenTake-generation' +GIT_COMMON_DIR='/Users/trip/TRUE 开发/PRIMARY-CN/OpenTake/.git' +GIT_WORK_TREE='/Users/trip/TRUE 开发/PRIMARY-CN/OpenTake-generation' +``` + +- The repository contains many unrelated user changes outside this task; they were left untouched and excluded from the staged commit. diff --git a/.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-3-report.md b/.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-3-report.md new file mode 100644 index 00000000..a5552ef0 --- /dev/null +++ b/.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-3-report.md @@ -0,0 +1,71 @@ +# Task 3 Report — Preview Temporal Remap Parity + +## Scope + +- Task: prove RenderPlan/playback temporal parity at the native publication boundary. +- Baseline commit: `69a958fc816c2a7dafda87b479e58fd5c59383d1` +- Allowed edit surface honored: + - `crates/opentake-render/src/plan/tests.rs` + - `src-tauri/src/playback/engine.rs` + - `src-tauri/tests/playback_transport_integration.rs` + +## What I changed + +1. Added `source_frame_video_with_trim_and_fractional_speed` to `crates/opentake-render/src/plan/tests.rs`. + - Covers `trim_start_frame = 2`, `speed = 1.5`, timeline frames `0 / 3 / 5`. + - Verifies `source_frame_index` resolves to source frames `2 / 7 / 10`. + +2. Replaced the earlier native proof in `src-tauri/tests/playback_transport_integration.rs` with stricter first-publication checks. + - `frame_route_first_publication_matches_fractional_speed_plan` + - Uses a deterministic CFR ffmpeg fixture. + - Fails explicitly if ffmpeg cannot run or if `RenderLoop::new` cannot acquire a GPU. + - Reuses `build_render_plan` + `source_frame_index` to map timeline frames `0 / 3 / 5` to source frames `2 / 7 / 10`. + - Asserts the first `RenderLoop::render_frame` result matches the exact decoded source frame. + - Verifies `/frame` returns decodable JPEGs and native publication payloads remain monotonic: + - frames: `0, 3, 5` + - sequences: `1, 2, 3` + - terminal flags: `false, false, true` + - `frame_route_first_publication_matches_reversed_plan` + - Reuses the same RenderPlan/source-frame mapping for `reversed = true`. + - Verifies source frames `7 / 2` for timeline frames `0 / 5`. + - Verifies the first native publication JPEGs decode and publication order remains monotonic: + - frames: `0, 5` + - sequences: `1, 2` + - terminal flags: `false, true` + +3. Fixed the reversed publication stale-frame bug in `src-tauri/src/playback/engine.rs`. + - `RenderLoop` now tracks the last planned video `source_frame` per active clip. + - When the next frame plan rewinds a clip's `source_frame`, playback clears continuous decode streams before syncing the frame. + - This keeps reversed playback on the exact plan-mapped source frame for the first publication instead of reusing a stale later frame from the forward-only queue. + - Added engine unit coverage for the rewind detection helper. + +## Investigation notes + +- Existing pure `RenderPlan` tests already covered `reversed` and non-`1.0` speed cases, but not the exact `1.5x` fractional case from this task. +- Fractional-speed playback already matched the plan on the first render/publication once the test stopped warming up in a loop. +- The stricter reversed native test exposed a real bug: timeline frames advanced monotonically while the clip's planned `source_frame` moved backward, and the forward decode stream reused stale cached/pending data on the first later publication. +- The previous warmup helper hid that behavior and was removed. + +## Verification + +Red/green checkpoints: + +- `cargo test -p opentake-render source_frame_video_with_trim_and_fractional_speed` +- `cargo test -p opentake-tauri --features playback-engine --test playback_transport_integration frame_route_first_publication_matches_fractional_speed_plan -- --nocapture` +- `cargo test -p opentake-tauri --features playback-engine --test playback_transport_integration frame_route_first_publication_matches_reversed_plan -- --nocapture` + +Fresh verification after the fix: + +- `cargo test -p opentake-render plan` +- `cargo test -p opentake-tauri --features playback-engine --test playback_transport_integration` +- `cargo test -p opentake-tauri --features playback-engine playback::engine` +- `cargo test -p opentake-tauri --features playback-engine playback::transport` +- `cargo fmt --all` +- `cargo fmt --all -- --check` + +All commands passed in this environment. + +## Concerns + +- The worktree `.git` file still points at an old absolute path under `/Users/lvbaiqing/...`; git operations succeed only when driven with explicit `GIT_DIR` / `GIT_COMMON_DIR` / `GIT_WORK_TREE`. +- The worktree contains unrelated user changes under `docs/audit/` and `.playwright-cli/`; they were left untouched and must stay out of this fix commit. diff --git a/AGENTS.md b/AGENTS.md index df0a698f..6e8bac98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,118 +1,27 @@ - -# OpenTake — AI Agent 协作指南 +# OpenTake — 项目入口 -OpenTake 是 Palmier Pro 的跨平台社区分支:Rust core(Tauri 2 + React)桌面端,媒体引擎 FFmpeg + wgpu,GPL-3.0 开源。 +> 状态:canonical · 阶段:implementation-backed · 同步日期:2026-09-07 -## 项目结构 +OpenTake 是 Palmier Pro 的 GPL-3.0-or-later 社区项目,以 Rust + Tauri 2 + React/TypeScript 构建视频编辑器和 Agent 工作流。编辑逻辑对照只读上游移植。 -``` -PRIMARY-CN/ -├── palmier-pro-upstream/ # 上游只读参考(Swift macOS 视频编辑器,GPL-3.0) -│ └── Sources/PalmierPro/ # 209 .swift,~43K 行,编辑逻辑的真理来源 -└── OpenTake/ # 本项目 - ├── docs/ # 文档树(入口 docs/INDEX.md) - │ ├── modules/ # ★ 按 crate/前端的模块文档(总览+目录+子系统) - │ ├── architecture/ # 跨切面:架构/路线图/移植图/gap/bug/编辑自动化 DOS - │ └── upstream-analysis/ # 上游拆解报告 - ├── crates/ # Rust workspace(9 个 crate,依赖只能向下) - ├── src-tauri/ # Tauri 2 桌面壳 - └── web/ # React + TypeScript 前端 -``` +- **活动工作树**:`OpenTake-generation/`;同级 `OpenTake/` 仅作旧代码对照,`palmier-pro-upstream/` 只读。每个独立 Git 仓库仅保留根目录这一份 `AGENTS.md` 自动入口。 +- **源码基线**:本轮同步起点为 `release/v1.0.0-beta.5` / `33ee8e2`,版本清单已进入 `1.0.0-beta.6`。公开发行状态以 [GitHub Releases](https://github.com/appergb/OpenTake/releases) 为唯一事实来源;进入任务先读取实际分支、HEAD、工作区与发行状态。 +- **远端**:`origin` = `https://github.com/appergb/OpenTake.git`。 +- **技术栈**:10 个库 crate + `src-tauri`(11 个 Cargo 成员),React 18 / TypeScript / Vite / Zustand;FFmpeg、wgpu、cpal;Agent 使用 rmcp 与本地/BYOK 能力。 +- **用户意图**:同步全项目文档并完成可验证的 Beta 候选。源码接线、原生 GUI 验证和实际发布分开记录;不把候选写成已发布或全功能完成。 +- **风险**:保留用户未提交改动;平台、provider、安装包验收按真实环境与候选提交记录。新包贴纸、搜索和导出已实测;剩余发布检查见当前验收记录,不把局部通过写成已公开发布。 -## 从何处开始 +## 当前活动计划与路由 -| 你要做什么 | 先看这个 | +| 内容 | 唯一来源 / 入口 | |---|---| -| 了解项目全局 | [README.md](README.md) | -| 理解目标架构 | [docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md) | -| 知道当前阶段 + 下一步做什么 | [docs/architecture/ROADMAP.md](docs/architecture/ROADMAP.md) | -| 理解 Agent 如何与软件协作 | [docs/modules/opentake-agent/AGENT-CONTEXT-SIGNAL.md](docs/modules/opentake-agent/AGENT-CONTEXT-SIGNAL.md) | -| 移植某个上游模块 | [docs/architecture/MODULE-PORT-MAP.md](docs/architecture/MODULE-PORT-MAP.md) | -| 了解为何选了 Rust / Tauri / GPL-3.0 | [DECISIONS.md](DECISIONS.md) | -| 查找某个上游模块的源码 | `palmier-pro-upstream/Sources/PalmierPro/` | - -## 核心设计原则(来自上游拆解) - -1. **单一可观测状态容器**:Rust 持有权威 `Timeline`,前端只持只读镜像 + 版本号。 -2. **纯函数编辑算法**:OverwriteEngine / RippleEngine / SnapEngine 全部纯函数,无副作用,可全单测。 -3. **命令层 = 唯一编辑入口**:所有 UI 手势、Agent、MCP 工具归一到一个 `EditCommand` 枚举。 -4. **撤销栈在 Rust**:整树快照(`Timeline` derive `Clone`),前端不做撤销。 -5. **预览与导出共享 RenderPlan**:纯函数 `Timeline → 每帧属性`,保证预览与导出像素一致。 - -## 技术栈(已定) - -| 关注点 | 选型 | -|---|---| -| 核心语言 | Rust(workspace,多 crate) | -| 桌面壳 | Tauri 2 | -| 前端 | React + TypeScript + Vite | -| 状态管理 | Zustand(前端只读镜像) | -| 编解码 | ffmpeg-next(libav*) | -| 帧合成 | wgpu(自写合成器) | -| 音频播放 | cpal | -| MCP server | rmcp(streamable-http-server) | - -## 移植法则 - -编辑算法从 Swift → Rust 时的转换铁律: - -- **一切以整数帧为单位**,`secondsToFrame` 用截断(`Int(s * fps)`),非四舍五入。 -- **关键帧存储用 clip 相对帧偏移**,公开 API 用绝对时间线帧。 -- **`round()` 方向与上游一致**:Swift `.rounded()` = Rust `f64::round()`(.5 向偶取整),MODULE-PORT-MAP 中有标注差异处。 -- **smoothstep(t) = t*t*(3-2t)**,不要换公式。 -- **所有 serde 模型加 `#[serde(default)]` + `Option`**,保证读旧工程不破坏。 - -## Rust 代码风格 - -- 用 `Result` 做内部错误,边界层转 Tauri 的 `Err(String)`。 -- `crates/opentake-domain/` 零依赖叶子 crate,不允许 `std::fs` 或网络调用。 -- 单测用 `#[cfg(test)]`,每个命令一个 test module,覆盖率 ≥80%。 -- 保持注释最小,只在 why 不显然时写一条短行。 - -## React / TypeScript 代码风格 - -- 组件不持有领域逻辑,只渲染 Tauri 命令返回的快照。 -- Timeline 的像素↔帧换算放前端,帧↔秒换算放 Rust。 -- 所有数值常量走 `AppTheme`,不硬编码。 -- 悬停态用 CSS `:hover` + 圆角背景,图标用 lucide-react。 - -## 构建(全部在 `OpenTake/` 内运行) - -```bash -# Rust core -cargo build -cargo test -cargo clippy - -# 前端 -cd web && pnpm install && pnpm build - -# 启动 Tauri 开发模式 -cargo tauri dev -``` - -当前状态与发布门槛以 `docs/releases/1.0.0-beta.2.md` 为准,验证证据记录在 -`docs/audit/2026-08-02/beta-functional-verification.md`。`CLAUDE.md`、 -`docs/architecture/HANDOFF-2026-07.md` 与 `PORT-1TO1-GAP.md` 均为历史快照或设计来源。 - -## 上游参考 - -上游克隆 `palmier-pro-upstream/` 只读。查找编辑逻辑时直接在该目录 grep。禁止修改上游文件。 - -常用查找路径: -- 领域模型:`palmier-pro-upstream/Sources/PalmierPro/Models/` -- 编辑算法:`palmier-pro-upstream/Sources/PalmierPro/Editor/` -- Agent/MCP 工具:`palmier-pro-upstream/Sources/PalmierPro/Agent/` - - -## Agent Context Signal — 软件主动发信号 - -OpenTake 的核心创新之一是 **软件主动向 Agent 发送剪辑指引**,而不是让 Agent 自己去读技能文件。 - -当 Agent 通过 MCP 操作时间线和轨道时,软件会在每次工具返回中附带 `context_signal`: -- 视频类型判定(口播 / Vlog / 混剪 / 采访 / 短剧 / 长视频) -- 每个轨道的角色和用途(主画面 / B-roll / 旁白 / BGM / SFX / 文字) -- 当前剪辑阶段和下一步建议 -- 该视频类型适用的剪辑规则 - -这些指引内化自 ClipSkills 技能套件([appergb/ClipSkills](https://github.com/appergb/ClipSkills),MIT 许可)。详见 [docs/modules/opentake-agent/AGENT-CONTEXT-SIGNAL.md](docs/modules/opentake-agent/AGENT-CONTEXT-SIGNAL.md) 和 [docs/modules/opentake-agent/WORKFLOW-PLUGIN-SYSTEM.md](docs/modules/opentake-agent/WORKFLOW-PLUGIN-SYSTEM.md)。 +| 当前执行与发布门槛 | [公开 Beta 活动计划](docs/plans/active/2026-09-06-public-beta.md) | +| Beta 6 版本说明与发布流程 | [版本发布文档](docs/releases/1.0.0-beta.6.md) | +| 文档总目录 | [docs/INDEX.md](docs/INDEX.md) | +| 开发规范、构建与验证 | [docs/project/conventions.md](docs/project/conventions.md) | +| 意图与工作区约束 | [docs/project/intent.md](docs/project/intent.md) | +| 模块源码路由 | [docs/modules/INDEX.md](docs/modules/INDEX.md) | +| 能力与证据 | [能力账本](docs/capabilities/CAPABILITY-LEDGER.md) | +| 文档同步范围与遗留 | [本次同步报告](docs/documentation-sync-2026-09-06.md) | + +`CLAUDE.md` 是历史快照,不是当前交接或额外自动入口。按路由读取所需文档,禁止递归加载整棵文档树。 diff --git a/CHANGELOG.md b/CHANGELOG.md index f150a0cf..80033446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ 本文件记录 OpenTake 的重要改动。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。 +## [Unreleased] — 1.0.0-beta.6 候选 + +> 状态:draft · 阶段:partial-implementation。当前版本清单已切到 Beta 6,尚未发布。范围核对起点:`aae0ae6..33ee8e2`(74 个提交);后续源码修复及门槛以[候选说明](docs/releases/1.0.0-beta.6.md)和[活动计划](docs/plans/active/2026-09-06-public-beta.md)为准。 + +### 新增与修复 + +- 多素材预览 tab 与旧布局状态归一;文件夹、平铺、分组媒体视图;原生文件导入对话框修复。 +- temporal compositor 播放路由、倒放发布流重置、时间控制和 Lottie 原生时间线播放;不支持或缺失素材组合显式拒绝。 +- 透明 Motion 发布/再次编辑保留 alpha;ProRes 4444 透明导出。 +- 范围选择、按选区分割/播放头修剪、链接移动与 Option 修剪一致性。 +- 导出失败清理、取消传播与输出身份检查;镜像刷新后约束播放头范围。 +- 文本面板接入添加文字;特效面板向单个选中视觉片段追加预设,保留链接视频既有特效。Sticker 本地图片/Lottie 展示、导入、预览、拖拽/落轨与工程身份隔离已实现,待新包原生验收。 +- 文档入口、三语 README、模块路由和历史状态同步,新增无依赖本地 Markdown 链接审计。 + +### 候选边界 + +功能接线不等于本候选 GUI/安装包已验收。语义搜索模型分发仍在修复验证中;Web 全量重跑、Clippy 与依赖审计修复由主任务记录。本文不声明整仓检查通过或跨平台已验证发行。 + ## [1.0.0-beta.5] — 2026-08-14 ### 新增(Added) @@ -25,7 +43,15 @@ ### Beta 已知边界 - macOS 候选包仍为 ad-hoc 签名且未公证;Windows 安装器仍未使用 Authenticode。平台安装与升级证据必须来自候选 exact-SHA CI/实机,不能由 macOS 或浏览器 fallback 替代。 -- 在最终打包 `.app` GUI 验收、远端 main CI 和签名 secret 预检完成前,不创建或发布 `v1.0.0-beta.5`。 +- 以下为 Beta 5 发布时的范围与边界;Beta 5 已发布,其历史验收不替代 Beta 6 候选门槛。 + +## [1.0.0-beta.4] — 2026-08-10 + +时间与转场持久化、导出一致性、签名更新流程和 Windows tract 安全升级。具体发布范围与证据见 [Beta 4 历史说明](docs/releases/1.0.0-beta.4.md)。 + +## [1.0.0-beta.3] — 2026-08-09 + +全局空格播放/暂停、原生 HEVC 素材预览和发布流水线修复。具体发布范围与证据见 [Beta 3 历史说明](docs/releases/1.0.0-beta.3.md)。 ## [1.0.0-beta.2] — 2026-08-03 diff --git a/CLAUDE.md b/CLAUDE.md index e531f491..8d9fb3d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,10 @@ # OpenTake — 历史工作交接 / 状态快照 -> **2026-08-03 状态裁决:**本文件保留早期开发过程,不再是当前状态真值。Beta 2 -> 的当前范围与发布门槛见 `docs/releases/1.0.0-beta.2.md`,执行证据见 +> 状态:historical · 阶段:partial-implementation · 历史日期与正文保留。 +> 当前路由:[项目入口](AGENTS.md) / [活动计划](docs/plans/active/2026-09-06-public-beta.md)。本文件不作为自动指令入口。 + +> **历史裁决(2026-08-03):**本文件保留早期开发过程,不再是当前状态真值。Beta 2 +> 当时的范围与发布门槛见 `docs/releases/1.0.0-beta.2.md`,执行证据见 > `docs/audit/2026-08-02/beta-functional-verification.md`。`HANDOFF-2026-07.md` 和 > `PORT-1TO1-GAP.md` 同样仅作历史/设计参考。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28a1c84f..3b9b5112 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,31 @@ # Contributing to OpenTake -We welcome contributions! Please open an [Issue](https://github.com/appergb/OpenTake/issues) for discussion before submitting large PRs. +> Status: canonical · Stage: implementation-backed · Updated: 2026-09-06 -## Development Setup +Contributions are welcome at [appergb/OpenTake](https://github.com/appergb/OpenTake). Discuss large changes in an issue before implementation. In this multi-worktree workspace, use `OpenTake-generation/`; a fresh clone may use its chosen directory name. + +Start with [AGENTS.md](AGENTS.md), then read the relevant [module overview and index](docs/modules/INDEX.md). Development rules, upstream references, frame/serialization contracts and build commands have one source: [development conventions](docs/project/conventions.md). + +## Validation + +Run checks appropriate to the changed behavior from the repository root: ```bash -# Prerequisites: Rust >= 1.96, Node.js >= 20, pnpm, FFmpeg >= 6.0 -cargo build +cargo fmt --all -- --check +cargo clippy cargo test -cd web && pnpm install && pnpm build +pnpm --dir web install --frozen-lockfile +pnpm --dir web test +pnpm --dir web build +python3 scripts/check_docs.py ``` -## Code Style +The frontend currently has no `lint` script; its build includes TypeScript checking. Record commands actually run. Native playback, GPU, audio, providers and installer changes need environment-specific evidence; a browser fallback does not validate a desktop package. Keep user edits and retained audit assets intact. + +## Documentation and releases -- Rust: `cargo fmt` + `cargo clippy` -- TypeScript: `pnpm run lint` +Update the relevant module documentation with code changes. Keep historical dates and release versions. Current work follows the [public Beta plan](docs/plans/active/2026-09-06-public-beta.md); [Beta 6](docs/releases/1.0.0-beta.6.md) is a candidate pending validation and publication. Release/tag/signing operations belong to the release owner. ## License -By contributing, you agree that your contributions will be licensed under GPL-3.0. +Contributions use the repository's GPL-3.0-or-later license; retain upstream attribution and third-party notices. diff --git a/Cargo.lock b/Cargo.lock index 0e1f5886..92ba5370 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3690,7 +3690,7 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "opentake-agent" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "anyhow", "async-trait", @@ -3725,7 +3725,7 @@ dependencies = [ [[package]] name = "opentake-core" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "opentake-domain", "opentake-ops", @@ -3739,7 +3739,7 @@ dependencies = [ [[package]] name = "opentake-domain" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "serde", "serde_json", @@ -3747,7 +3747,7 @@ dependencies = [ [[package]] name = "opentake-gen" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "anyhow", "async-trait", @@ -3765,7 +3765,7 @@ dependencies = [ [[package]] name = "opentake-media" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "anyhow", "byteorder", @@ -3795,6 +3795,7 @@ dependencies = [ "tokenizers", "tokio", "tracing", + "tract-onnx", "unicode-normalization", "whisper-rs", "windows-sys 0.61.2", @@ -3803,7 +3804,7 @@ dependencies = [ [[package]] name = "opentake-motion" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "base64 0.22.1", "hex", @@ -3821,7 +3822,7 @@ dependencies = [ [[package]] name = "opentake-ops" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "opentake-domain", "serde_json", @@ -3829,7 +3830,7 @@ dependencies = [ [[package]] name = "opentake-process-tree" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3837,7 +3838,7 @@ dependencies = [ [[package]] name = "opentake-project" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "cap-fs-ext", "cap-std", @@ -3856,7 +3857,7 @@ dependencies = [ [[package]] name = "opentake-render" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "bytemuck", "cosmic-text", @@ -3873,7 +3874,7 @@ dependencies = [ [[package]] name = "opentake-tauri" -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" dependencies = [ "axum", "base64 0.22.1", @@ -3927,6 +3928,7 @@ dependencies = [ "uuid", "velato", "windows-sys 0.61.2", + "zip", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 54f11498..a4fa72a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ ] [workspace.package] -version = "1.0.0-beta.5" +version = "1.0.0-beta.6" edition = "2021" license = "GPL-3.0-or-later" repository = "https://github.com/appergb/OpenTake" diff --git a/README.ja.md b/README.ja.md index a42529ef..5aef34c4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -12,6 +12,12 @@

+**ソースバージョン:`1.0.0-beta.6`。** 公開状況とダウンロードは [GitHub Releases](https://github.com/appergb/OpenTake/releases) を参照してください。[実行計画](docs/plans/active/2026-09-06-public-beta.md)、[バージョンノート](docs/releases/1.0.0-beta.6.md)、[文書とソースの確認記録](docs/documentation-sync-2026-09-06.md)に実装と検証を記録しています。 + +ソースには、複数のメディアプレビュータブ、フォルダー/フラット/グループ表示、temporal compositor、透明 Motion 出力、ProRes 4444 書き出しが含まれます。Text パネルはテキストクリップを追加し、Effect パネルは既存の効果を保持してプリセットを追加します。Sticker は画像/Lottie 素材の読み込み、選択、プレビュー、タイムライン配置に対応しています。単体テストと macOS パッケージの GUI 操作は[検証記録](docs/audit/2026-09-06/public-beta-validation.md)を参照してください。 + +意味検索は固定バージョンの約 1.5 GB のモデルを使用し、チェックサム検証とオフラインインストールに対応しています。macOS と Windows で実際の画像・テキスト推論とランキングを検証しました。Windows は固定入力を指定する Tract エンジンを使用します。macOS パッケージではモデルのダウンロード、索引作成、中国語検索も実行済みです。利用前にモデルをインストールしてください。[モデル監査記録](docs/audit/2026-09-06/semantic-search-model.md)に詳細があります。 + - [プロジェクトについて](#-プロジェクトについて) - [なぜOpenTakeか](#-なぜopentakeか) - [競合との違い](#-競合との違い) @@ -29,7 +35,7 @@ ## 📖 プロジェクトについて -**OpenTake** は、**Rust + Tauri 2** で構築された**クロスプラットフォームの動画制作エンジン**です。macOS / Windows / Linux の 3 プラットフォームで動作し、プロの映像編集ワークフローに AI Agent を深く統合することを目的としています。 +**OpenTake** は、**Rust + Tauri 2** で構築された**クロスプラットフォームの動画制作エンジン**です。macOS / Windows / Linux の 3 プラットフォームを対象とし、プロの映像編集ワークフローに AI Agent を深く統合することを目的としています。 > 🌟 **革新的な点**: Agent に長大なスキルドキュメントを読ませるのではなく、OpenTake は**編集ガイダンス(Context Signal)を Agent に能動的に送信**します——各トラックの役割、各クリップの適切な編集方法、各段階で適用すべきルールを、ソフトウェアが Agent に直接伝えます。 @@ -44,9 +50,9 @@ OpenTake は CapCut / DaVinci Resolve / Final Cut Pro の代替品ではあり | 課題 | 従来の手法 | OpenTakeの手法 | |:--|:--|:--| | Agentが素材の編集方法を知らない | Agentが自らスキルドキュメントを読む | ソフトウェアがContext Signalを発信 — 「このトラックはA-roll、トーキングヘッドのリズムでカット」 | -| クロスプラットフォームに3つのコードベースが必要 | macOS: Swift/AVFoundation、Windows: C++/DirectShow | Rustの単一コードベース、FFmpeg + wgpu、全プラットフォームで同一体験 | +| クロスプラットフォームに3つのコードベースが必要 | macOS: Swift/AVFoundation、Windows: C++/DirectShow | Rustの単一コードベース、FFmpeg + wgpu、各プラットフォームを個別検証 | | 自分のAIキーを使いたい | ベンダーのクラウドサービスにロックイン | BYOK — fal.ai / Replicate / OpenAI に直接接続、バックエンド不要、運用コストゼロ | -| Agentはチャットできるが操作できない | CLI Agentがテキスト出力を読むだけ | MCP Server 31ツール — Agentが直接 add_clips / split_clip / set_keyframes を実行 | +| Agentはチャットできるが操作できない | CLI Agentがテキスト出力を読むだけ | 能力に応じた MCP Server — Agentが直接 add_clips / split_clip / set_keyframes を実行 | | 動画タイプごとに毎回プロンプトを書き直す | 「あなたは製品レビューを編集しています…」を毎回繰り返す | ワークフロープラグインシステム: レビュー/チュートリアル/ゲーム/ウェディング、各タイプの手法を事前パッケージ化 | | 新しいツールの学習コストが高い | 複雑なUI、長いオンボーディング | Agentが代わりに操作 — 「このインタビューを3分のハイライトに編集して」と言うだけ | @@ -54,16 +60,7 @@ OpenTake は CapCut / DaVinci Resolve / Final Cut Pro の代替品ではあり ## ⚡ 競合との違い -| 項目 | CapCut | DaVinci Resolve | Final Cut Pro | **OpenTake** | -|:--|:--|:--|:--|:--| -| **Agentネイティブ統合** | ❌ | ❌ | ❌ | ✅ MCP 31ツール + Context Signal | -| **クロスプラットフォーム** | ✅ macOS / Win | ✅ macOS / Win / Linux | ❌ macOSのみ | ✅ macOS / Win / Linux | -| **BYOK AI生成** | 有料テンプレート | ❌ | ❌ | ✅ fal.ai / Replicate / OpenAI に直接接続 | -| **ローカル文字起こし** | ❌ クラウドのみ | ❌ プラグイン必要 | ❌ プラグイン必要 | ✅ whisper-rs デバイス上推論 | -| **ローカル意味検索** | ❌ | ❌ | ❌ | ✅ SigLIP2 + Ort デバイス上 | -| **ワークフロープラグイン** | ❌ 固定テンプレート | ❌ | ❌ | ✅ JSON+MD コミュニティプラグイン | -| **オープンソース** | ❌ | ❌ | ❌ | ✅ GPL-3.0 | -| **Agent操作可能なキーフレーム** | ❌ | ❌ | ❌ | ✅ 全6 kfトラックをMCP経由で制御 | +OpenTake は Rust が保持するタイムライン、コマンドによる undo、認証 MCP、Context Signal、ローカル/BYOK メディア処理を組み合わせます。[能力台帳](docs/capabilities/CAPABILITY-LEDGER.md)は実装と検証を区別し、他の編集ソフトとの全機能同等性を保証するものではありません。 --- @@ -84,7 +81,7 @@ OpenTake は CapCut / DaVinci Resolve / Final Cut Pro の代替品ではあり ### 🔌 Agent ツールサーフェス -OpenTake は45個の互換 Agent ツールを提供し、メディア・生成・provider の実行可能性に +OpenTake は互換 Agent ツールを提供し、メディア・生成・provider の実行可能性に 応じて動的に公開します。利用できない機能は fail closed になります。 | グループ | 主要ツール | @@ -95,9 +92,7 @@ OpenTake は45個の互換 Agent ツールを提供し、メディア・生成 | ライブラリ | `create_folder`, `move_to_folder`, `rename_media` | | リソース | `models/video`, `models/image` | -公式 Codex / ChatGPT は、ターンごとにランダムな loopback ポート、256-bit Bearer、 -現在のプロジェクト ID を持つ一時 MCP を使用します。Beta 2 では旧来の未認証固定 -`127.0.0.1:19789` エンドポイントを無効化しています。 +公式 Codex / ChatGPT は現在のプロジェクトに紐づく認証済み loopback エンドポイントをターンごとに使用します。Beta 5 では外部 MCP クライアントの明示的なペアリング、認証情報の失効、再起動後の設定保持も追加しました。旧未認証エンドポイントは無効のままです。[MCP 実装](docs/modules/opentake-agent/mcp-server.md)を参照してください。 ### 🎬 クロスプラットフォームメディアエンジン @@ -107,7 +102,7 @@ OpenTake は45個の互換 Agent ツールを提供し、メディア・生成 | コンポジター | wgpu カスタムコンポジター | | 音声再生 | cpal | | 文字起こし | whisper-rs | -| 意味検索 | candle / ort + SigLIP2 | +| 意味検索 | SigLIP2(固定 revision のモデル導入と macOS の実 Rust 推論を検証済み) | ### 🌐 BYOK AI生成 @@ -123,12 +118,12 @@ OpenTake は45個の互換 Agent ツールを提供し、メディア・生成 ## 🖥️ 対応プラットフォーム -| プラットフォーム | 状態 | 備考 | -|:--|:--|:--| -| **macOS** (Apple Silicon + Intel) | ✅ 主要開発環境 | ネイティブARM64 + x86_64; Metal経由GPU (wgpu) | -| **Windows** (10/11 x86_64) | ✅ サポート | Vulkan / DX12 (wgpu); Tauri 2完全サポート | -| **Linux** (x86_64) | ✅ サポート | Vulkan; AppImage / deb | -| **バックエンド / ヘッドレス** | ✅ サポート | GUIなしで純Rustコア実行可能 | +| プラットフォーム | 検証範囲 | +|:--|:--| +| macOS | 主な開発環境。日付付きのネイティブ/パッケージ検証はリリース記録を参照。Intel と Apple Silicon は個別に検証します。 | +| Windows | ビルド/インストーラーの対象。候補コミットの CI と実機インストール/UI 検証が必要です。 | +| Linux | ビルド対象。新しい配布物やネイティブ GUI の検証完了は宣言していません。 | +| Headless core | Rust ライブラリとテストは GUI なしで利用可能。メディア/GPU/ブラウザー機能には実行環境が必要です。 | --- @@ -136,6 +131,7 @@ OpenTake は45個の互換 Agent ツールを提供し、メディア・生成 ``` crates/ +├── opentake-process-tree # Cross-platform child-process lifecycle ├── opentake-domain # Timeline / Track / Clip / Keyframe ├── opentake-ops # OverwriteEngine / RippleEngine / SnapEngine ├── opentake-project # プロジェクト永続化 / バンドル / エクスポート @@ -196,16 +192,16 @@ crates/ ```bash # OpenTake の兄弟ディレクトリに上流を clone -cd .. # from OpenTake/ +cd .. # from OpenTake-generation/ git clone https://github.com/palmier-io/palmier-pro.git palmier-pro-upstream -cd OpenTake +cd OpenTake-generation ``` ディレクトリ構成: ``` PRIMARY-CN/ -├── OpenTake/ # このリポジトリ +├── OpenTake-generation/ # このリポジトリ └── palmier-pro-upstream/ # 上流 Swift ソース (GPL-3.0) ``` @@ -238,9 +234,6 @@ cd web && pnpm install && pnpm build cd .. && cargo tauri dev ``` -> **現在の状態**: `1.0.0-beta.2` 候補版。ローカル編集、プレビュー、保存、 -> 書き出し、Agent、Motion Canvas、レビュー可能な AI ワークフローを実装済みです。 -> 検証範囲と制限は [Beta リリースノート](docs/releases/1.0.0-beta.2.md) を参照してください。 --- @@ -251,6 +244,10 @@ cd .. && cargo tauri dev | `0.1.0-dev` | 2026-06 | Phase 0+1: Cargo workspace + Domain models + Edit ops | | `1.0.0-beta.1` | 2026-08-01 | 初回インストール可能 Beta:ローカル編集、Agent、Motion、レビュー可能な AI ワークフロー | | `1.0.0-beta.2` | 2026-08-03 | 公式 Codex ログイン、原子的タイムライン操作、認証 MCP、操作性の強化 | +| `1.0.0-beta.3` | 2026-08-09 | Space 再生、HEVC ネイティブプレビューと配布パイプライン | +| `1.0.0-beta.4` | 2026-08-10 | 時間/トランジションの保存、書き出し整合性とアップデーター | +| `1.0.0-beta.5` | 2026-08-14 | 外部 MCP ペアリング、Agent 会話順序と Motion Studio | +| `1.0.0-beta.6` | [バージョン記録](docs/releases/1.0.0-beta.6.md) | 透明 Motion、ProRes 4444、複数プレビュー、メディア表示、Text/Effect/Sticker、実モデル意味検索 | | *(planned)* `1.0.0` | TBD | Phase 10: フルリリース | 📖 [完全なロードマップ](docs/architecture/ROADMAP.md) diff --git a/README.md b/README.md index 95cbaa43..2065d589 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- Platforms + Platforms License Stars Discord EN @@ -26,6 +26,12 @@

+**Source version: `1.0.0-beta.6`.** Published versions and downloads are maintained in [GitHub Releases](https://github.com/appergb/OpenTake/releases). See the [execution plan](docs/plans/active/2026-09-06-public-beta.md), [version notes](docs/releases/1.0.0-beta.6.md) and [documentation/source review](docs/documentation-sync-2026-09-06.md). + +The source includes multiple media preview tabs, folder/flat/grouped media views, temporal compositor routing, transparent Motion publishing and ProRes 4444 export. The Text panel adds text clips; the Effect panel applies presets to one selected visual clip while preserving existing effects. Sticker supports project image/Lottie assets, local import, selection/preview and placement on the timeline. Focused tests and macOS package GUI checks are recorded in the [validation report](docs/audit/2026-09-06/public-beta-validation.md); provider and platform coverage remains tied to the corresponding evidence. + +Semantic search uses fixed-revision assets (about 1.5 GB) with checksum validation and offline installation. Real image/text embedding and ranking passed on macOS and Windows; Windows uses fixed input facts with the locked Tract engine. The macOS package also passed actual model download, indexing and Chinese visual queries. A model installation is required before use; see the [semantic model audit](docs/audit/2026-09-06/semantic-search-model.md). + ## Table of Contents - [About](#-about) @@ -45,7 +51,7 @@ ## 📖 About -**OpenTake** is a **cross-platform video production engine** built on **Rust + Tauri 2**, running on macOS / Windows / Linux, designed to deeply integrate AI Agents into professional video editing workflows. +**OpenTake** is a **cross-platform video production engine** built on **Rust + Tauri 2**, targeting macOS / Windows / Linux, designed to deeply integrate AI Agents into professional video editing workflows. > 🌟 **Core Innovation**: Instead of making the Agent parse lengthy skill documents, OpenTake **actively pushes editing guidance (Context Signal)** to the Agent — telling it exactly what each track does, how each clip should be cut, and what rules apply at every stage. @@ -60,9 +66,9 @@ OpenTake is not a replacement for CapCut / DaVinci Resolve / Final Cut Pro — i | Pain Point | Traditional Approach | OpenTake Approach | |:--|:--|:--| | Agent doesn't know how to edit | Agent reads skill docs on its own | Software pushes Context Signal — "this track is A-roll, cut with talking-head rhythm" | -| Cross-platform needs 3 codebases | macOS: Swift/AVFoundation, Windows: C++/DirectShow | Single Rust codebase, FFmpeg + wgpu, identical experience on all 3 platforms | +| Cross-platform needs 3 codebases | macOS: Swift/AVFoundation, Windows: C++/DirectShow | Single Rust codebase, FFmpeg + wgpu, platform-specific validation | | I want to use AI directly | Locked into vendor cloud services | Official Codex / ChatGPT sign-in for Agent, plus BYOK for fal.ai / Replicate / OpenAI | -| Agent can chat but can't act | CLI agent reads text output | MCP Server with 31 tools — Agent directly runs add_clips / split_clip / set_keyframes | +| Agent can chat but can't act | CLI agent reads text output | Capability-filtered MCP Server — Agent directly runs add_clips / split_clip / set_keyframes | | Rewriting prompts for every video type | "You are editing a product review..." every time | Workflow Plugin System: review/tutorial/gaming/wedding, each pre-packaged with methodology | | Steep learning curve for new tools | Complex UI, long onboarding | Agent operates for you — just say "edit this interview into a 3-minute highlight" | @@ -70,16 +76,7 @@ OpenTake is not a replacement for CapCut / DaVinci Resolve / Final Cut Pro — i ## ⚡ Competitive Edge -| Dimension | CapCut | DaVinci Resolve | Final Cut Pro | **OpenTake** | -|:--|:--|:--|:--|:--| -| **Agent-Native Integration** | ❌ | ❌ | ❌ | ✅ MCP 31 tools + Context Signal | -| **Cross-Platform** | ✅ macOS / Win | ✅ macOS / Win / Linux | ❌ macOS only | ✅ macOS / Win / Linux | -| **BYOK AI Generation** | Paid templates | ❌ | ❌ | ✅ Direct fal.ai / Replicate / OpenAI | -| **Local Transcription** | ❌ Cloud-only | ❌ Plugins needed | ❌ Plugins needed | ✅ whisper-rs on-device | -| **Local Semantic Search** | ❌ | ❌ | ❌ | ✅ SigLIP2 + Ort on-device | -| **Workflow Plugins** | ❌ Fixed templates | ❌ | ❌ | ✅ JSON+MD community plugins | -| **Open Source** | ❌ | ❌ | ❌ | ✅ GPL-3.0 | -| **Agent-Controllable Keyframes** | ❌ | ❌ | ❌ | ✅ All 6 kf tracks via MCP | +OpenTake combines a Rust-owned timeline, command-based undo, authenticated MCP, Context Signal and local/BYOK media workflows. The [capability ledger](docs/capabilities/CAPABILITY-LEDGER.md) tracks implementation and evidence; it does not imply feature parity with other editors. --- @@ -100,7 +97,7 @@ Knowledge source: [ClipSkills](https://github.com/appergb/ClipSkills) — 12-vol ### 🔌 Agent Tool Surface -OpenTake exposes 44 compatible Agent tools, filtered at runtime so unavailable media, +OpenTake exposes compatible Agent tools, filtered at runtime so unavailable media, generation, or provider capabilities fail closed instead of being advertised: | Group | Key Tools | @@ -111,10 +108,7 @@ generation, or provider capabilities fail closed instead of being advertised: | Library | `create_folder`, `move_to_folder`, `rename_media` | | Resources | `models/video`, `models/image` | -Official Codex / ChatGPT turns use a fresh loopback endpoint with a random port, -256-bit Bearer token, and current-project identity for that turn only. The legacy -fixed unauthenticated `127.0.0.1:19789` endpoint is disabled in Beta 2; external -Claude/Cursor pairing will return only with an authenticated opt-in flow. +Official Codex / ChatGPT turns use an authenticated per-turn loopback endpoint bound to the current project. Beta 5 also introduced explicit pairing for external MCP clients, with credentials, revocation and persistent client configuration. The old unauthenticated endpoint remains disabled; see [MCP behavior](docs/modules/opentake-agent/mcp-server.md). Built-in Agent chat panel shares tool definitions and system prompt with MCP. It can use direct OpenAI/Anthropic BYOK or the user-installed official Codex CLI's ChatGPT sign-in; OpenTake never @@ -127,8 +121,8 @@ reads or stores the Codex credential. | Codec | FFmpeg (`ffmpeg-next`) — battle-tested Rust bindings | | Compositor | wgpu custom compositor — multi-track layering + per-frame property sampling + affine/crop/blend | | Audio Playback | cpal | -| Transcription | whisper-rs (word/segment timestamps) | -| Semantic Search | candle / ort + SigLIP2 dual-encoder | +| Transcription | whisper-rs (requires installed model) | +| Semantic Search | SigLIP2; fixed-revision model installation and real macOS Rust inference validated | Playback routing is capability-based. Ordinary media uses WebKit playback; timelines that require supported compositing use the Rust compositor; and @@ -154,14 +148,12 @@ Community-authored JSON + Markdown plugins per video genre — review / tutorial ## 🖥️ Platforms -| Platform | Status | Notes | -|:--|:--|:--| -| **macOS** (Apple Silicon + Intel) | ✅ Primary dev platform | Native ARM64 + x86_64; GPU via Metal (wgpu) | -| **Windows** (10/11 x86_64) | ✅ Supported | Vulkan / DX12 backend (wgpu); full Tauri 2 support | -| **Linux** (x86_64) | ✅ Supported | Vulkan backend; AppImage / deb packaging | -| **Backend / Headless** | ✅ Supported | Pure Rust core runs without GUI for CI / server rendering / Agent batch processing | - -📋 macOS ≥12.0 (Monterey), Windows ≥10 (1809+), Linux glibc ≥2.31 +| Platform | Evidence boundary | +|:--|:--| +| macOS | Primary development platform; dated native/package evidence is linked from release notes. Intel and Apple Silicon artifacts require their own validation. | +| Windows | Build/installer target; candidate CI and native installer/UI evidence are required before claiming a verified distribution. | +| Linux | Build target; no new Linux distribution or native GUI verification is claimed here. | +| Headless core | Rust library APIs and tests can run without the desktop UI; media/GPU/browser capabilities still have runtime requirements. | --- @@ -169,19 +161,20 @@ Community-authored JSON + Markdown plugins per video genre — review / tutorial ``` crates/ +├── opentake-process-tree # Cross-platform child-process lifecycle ├── opentake-domain # Timeline / Track / Clip / Keyframe — pure value semantics ├── opentake-ops # OverwriteEngine / RippleEngine / SnapEngine — edit algorithm layer ├── opentake-project # Project persistence / bundle / archive / export ├── opentake-media # FFmpeg codec / thumbnails / waveform / transcription / semantic search ├── opentake-render # wgpu compositor + text rasterizer -├── opentake-motion # Native motion fallback: RGBA frame cache / alpha source scaffold +├── opentake-motion # Motion rendering, RGBA cache and transparent publishing ├── opentake-agent # MCP Server + Agent chat + context signal system ├── opentake-gen # Generative AI clients (fal.ai / Replicate / OpenAI) ├── opentake-core # Session management / DI / event bus └── src-tauri # Tauri 2 desktop shell ``` -Planned external plugin: +Motion Canvas plugin (implemented constrained runner): ``` plugins/ @@ -251,14 +244,14 @@ When porting editing logic, compare against the original Palmier Pro Swift sourc # Clone upstream alongside OpenTake (sibling directory) cd .. git clone https://github.com/palmier-io/palmier-pro.git palmier-pro-upstream -cd OpenTake +cd OpenTake-generation ``` Expected layout: ``` PRIMARY-CN/ -├── OpenTake/ # This repo +├── OpenTake-generation/ # This repo └── palmier-pro-upstream/ # Upstream Swift source (GPL-3.0) ``` @@ -303,11 +296,6 @@ cd .. cargo tauri dev ``` -> **Current Status**: `1.0.0-beta.5` candidate. The local editing, preview, -> persistence, export, authenticated external MCP, ordered Agent conversation, -> Motion Studio, and reviewed AI workflow verticals are implemented. See the -> [Beta release notes](docs/releases/1.0.0-beta.5.md) -> for validation scope and platform/provider limits. The sibling directory `palmier-pro-upstream/` contains upstream Swift sources for reference during porting. @@ -323,6 +311,7 @@ The sibling directory `palmier-pro-upstream/` contains upstream Swift sources fo | `1.0.0-beta.3` | 2026-08-09 | Playback Beta: app-wide Space transport, native HEVC source preview and release-pipeline hardening | | `1.0.0-beta.4` | 2026-08-10 | Release candidate: timing and transition persistence, export consistency, signed updater and Windows tract security upgrade | | `1.0.0-beta.5` | 2026-08-14 | Agent workflow Beta: persistent authenticated MCP, ordered inline tools, Motion Studio, project previews and interface polish | +| `1.0.0-beta.6` | [Version record](docs/releases/1.0.0-beta.6.md) | Transparent Motion, ProRes 4444, preview tabs, media views, text/effects/stickers and verified semantic search | | *(planned)* `1.0.0` | TBD | Phase 10: Full release — CapCut parity + deep Agent integration | 📖 [Full Roadmap](docs/architecture/ROADMAP.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 3224a165..ce32b0d9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,6 +12,12 @@

+**源码版本:`1.0.0-beta.6`。** 公开版本与下载以 [GitHub Releases](https://github.com/appergb/OpenTake/releases) 为准。执行与验证见[活动计划](docs/plans/active/2026-09-06-public-beta.md)、[版本说明](docs/releases/1.0.0-beta.6.md)和[文档/源码核对记录](docs/documentation-sync-2026-09-06.md)。 + +源码包含多素材预览 tab、文件夹/平铺/分组媒体视图、temporal compositor 路由、透明 Motion 发布和 ProRes 4444 导出。文本面板可以添加文字片段;特效面板为单个选中视觉片段追加预设并保留既有特效。贴纸支持项目图片/Lottie 素材、本地导入、选择/预览、拖拽与落轨;定向测试与 macOS 安装包实际操作见[验证记录](docs/audit/2026-09-06/public-beta-validation.md)。平台与 provider 的验证范围按对应证据记录。 + +语义搜索使用约 1.5 GB 的固定版本模型,经过校验并支持离线安装。macOS 与 Windows 已通过真实图文推理和排名验证,Windows 使用固定输入的锁定 Tract 引擎;macOS 安装包还实际完成了模型下载、索引和中文画面搜索。使用前需安装模型,详见[语义模型专项审计](docs/audit/2026-09-06/semantic-search-model.md)。 + - [项目介绍](#-项目介绍) - [为什么选 OpenTake](#-为什么选-opentake) - [竞品对比优势](#-竞品对比优势) @@ -29,7 +35,7 @@ ## 📖 项目介绍 -**OpenTake** 是一个基于 **Rust + Tauri 2** 构建的**跨平台视频制作引擎**,可在 macOS / Windows / Linux 三大平台上运行,旨在将 AI Agent 与专业视频编辑工作流深度集成。 +**OpenTake** 是一个基于 **Rust + Tauri 2** 构建的**跨平台视频制作引擎**,面向 macOS / Windows / Linux 三大平台,旨在将 AI Agent 与专业视频编辑工作流深度集成。 > 🌟 **核心创新**: 我们不让 Agent 去翻技能文档。OpenTake 会**主动向 Agent 发送编辑指导(Context Signal)**——时间线的每条轨道、每段素材、每个剪辑阶段,软件都能精准告知 Agent「这段该怎么做」。 @@ -44,9 +50,9 @@ OpenTake 不是剪映 / DaVinci Resolve / Final Cut Pro 的替代品——它是 | 痛点 | 传统做法 | OpenTake 的做法 | |:--|:--|:--| | Agent 不知道素材怎么剪 | Agent 自己去读 Skill 文档 | 软件主动发射 Context Signal,告诉 Agent「这条轨是主画面,素材该用口播手法剪」 | -| 跨平台需要三套代码 | macOS 用 Swift/AVFoundation,Windows 用 C++/DirectShow | Rust 单一代码库,FFmpeg + wgpu 跨平台编译,三平台体验一致 | +| 跨平台需要三套代码 | macOS 用 Swift/AVFoundation,Windows 用 C++/DirectShow | Rust 单一代码库,FFmpeg + wgpu 跨平台编译,各平台独立验证 | | 我想用自己的 AI Key | 被锁定在厂商的云服务里 | BYOK(自带 Key)直连 fal.ai / Replicate / OpenAI,零后端、零运营成本 | -| Agent 只能聊不能操作 | CLI Agent 读文本输出 | MCP Server 31 个工具——Agent 直接在时间线上 add_clips / split_clip / set_keyframes | +| Agent 只能聊不能操作 | CLI Agent 读文本输出 | 按能力发布的 MCP Server——Agent 直接在时间线上 add_clips / split_clip / set_keyframes | | 每个视频类型都要重新写提示词 | 每次重复「你要剪一个评测视频...」 | 工作流插件系统:评测/科普/游戏/婚礼每种类型封装好方法论,Agent 开机即用 | | 学新软件成本高 | 界面复杂,学习曲线陡 | Agent 替你操作,你只需要告诉它「帮我把这个采访剪成 3 分钟的精华」 | @@ -54,16 +60,7 @@ OpenTake 不是剪映 / DaVinci Resolve / Final Cut Pro 的替代品——它是 ## ⚡ 竞品对比优势 -| 维度 | 剪映 / CapCut | DaVinci Resolve | Final Cut Pro | **OpenTake** | -|:--|:--|:--|:--|:--| -| **Agent 原生集成** | ❌ | ❌ | ❌ | ✅ MCP 31 工具 + Context Signal | -| **跨平台** | ✅ macOS / Win | ✅ macOS / Win / Linux | ❌ macOS only | ✅ macOS / Win / Linux | -| **BYOK AI 生成** | 内置模板付费 | ❌ | ❌ | ✅ 直连 fal.ai / Replicate / OpenAI | -| **本地语音转写** | ❌ 云端 | ❌ 需插件 | ❌ 需插件 | ✅ whisper-rs 端侧推理 | -| **本地语义搜索** | ❌ | ❌ | ❌ | ✅ SigLIP2 + Ort 本地索引 | -| **工作流插件** | ❌ 固定模板 | ❌ | ❌ | ✅ JSON+MD 社区插件系统 | -| **开源** | ❌ | ❌ | ❌ | ✅ GPL-3.0 | -| **Agent 可操控所有关键帧属性** | ❌ | ❌ | ❌ | ✅ opacity / position / scale / rotation / crop / volume | +OpenTake 的项目重点是 Rust 权威时间线、命令式撤销、认证 MCP、Context Signal 和本地/BYOK 媒体工作流。[能力账本](docs/capabilities/CAPABILITY-LEDGER.md)区分实现与验证证据,不代表已经达到其他编辑器的全功能对等。 --- @@ -84,7 +81,7 @@ Agent 操作时间线时,每次工具返回附带 `context_signal`: ### 🔌 Agent 工具面 -OpenTake 提供 45 个兼容 Agent 工具,并按当前媒体、生成能力和 provider 授权动态发布; +OpenTake 提供兼容 Agent 工具,并按当前媒体、生成能力和 provider 授权动态发布; 未就绪能力会 fail closed,不会被虚假宣传为可执行: | 分组 | 代表工具 | @@ -95,9 +92,7 @@ OpenTake 提供 45 个兼容 Agent 工具,并按当前媒体、生成能力和 | 素材库组织 | `create_folder`, `move_to_folder`, `rename_media` | | 资源 | `models/video`, `models/image` | -官方 Codex / ChatGPT 每轮使用独立的随机回环端口、256-bit Bearer 和当前工程身份, -轮次结束即销毁。Beta 2 已关闭旧的未认证固定 `127.0.0.1:19789` 入口;Claude、 -Cursor 等外部客户端将在后续带认证、显式配对流程完成后重新开放。 +官方 Codex / ChatGPT 每轮使用绑定当前工程的认证回环端点。Beta 5 已加入外部 MCP 客户端显式配对、凭据撤销和重启保留配置;旧的未认证入口仍关闭。详见 [MCP 实现](docs/modules/opentake-agent/mcp-server.md)。 内置 Agent chat panel,与 MCP 共享工具定义和系统提示词。 @@ -109,7 +104,7 @@ Cursor 等外部客户端将在后续带认证、显式配对流程完成后重 | 帧合成 | wgpu 自写合成器 — 多轨叠加 + 逐帧属性采样 + 仿射/裁剪/混合 | | 音频播放 | cpal | | 语音转写 | whisper-rs (word/segment 时间戳) | -| 语义搜索 | candle / ort + SigLIP2 图文双编码器 | +| 语义搜索 | SigLIP2;固定 revision 模型安装与 macOS/Windows 真实推理已验证 | ### 🌐 BYOK 生成式 AI @@ -125,12 +120,12 @@ Cursor 等外部客户端将在后续带认证、显式配对流程完成后重 ## 🖥️ 支持平台 -| 平台 | 状态 | 说明 | -|:--|:--|:--| -| **macOS** (Apple Silicon + Intel) | ✅ 主要开发平台 | 原生 ARM64 + x86_64,GPU 加速 via Metal (wgpu) | -| **Windows** (10/11 x86_64) | ✅ 支持 | Vulkan / DX12 backend (wgpu),完整 Tauri 2 支持 | -| **Linux** (x86_64) | ✅ 支持 | Vulkan backend,AppImage / deb 打包 | -| **Backend / Headless** | ✅ 支持 | 纯 Rust 核心可在无 GUI 环境下运行,用于 CI / 服务端渲染 / Agent 批量处理 | +| 平台 | 证据边界 | +|:--|:--| +| macOS | 主要开发平台;原生与安装包的日期化证据见发布记录。Intel/Apple Silicon 产物分别验证。 | +| Windows | 构建与安装器目标;候选提交 CI 和真实安装/UI 验证完成后才能称为已验证发行。 | +| Linux | 构建目标;本文不声明新增 Linux 发行或原生 GUI 验证。 | +| Headless core | Rust 库和测试可脱离桌面 UI 运行,媒体/GPU/浏览器能力仍有环境依赖。 | --- @@ -138,6 +133,7 @@ Cursor 等外部客户端将在后续带认证、显式配对流程完成后重 ``` crates/ +├── opentake-process-tree # Cross-platform child-process lifecycle ├── opentake-domain # Timeline / Track / Clip / Keyframe — 纯函数式值语义 ├── opentake-ops # OverwriteEngine / RippleEngine / SnapEngine — 编辑算法层 ├── opentake-project # 项目持久化 / bundle / archive / export @@ -213,16 +209,16 @@ plugins/ ```bash # 在 OpenTake 同级目录 clone 上游 -cd .. # from OpenTake/ +cd .. # from OpenTake-generation/ git clone https://github.com/palmier-io/palmier-pro.git palmier-pro-upstream -cd OpenTake +cd OpenTake-generation ``` 目录结构: ``` PRIMARY-CN/ -├── OpenTake/ # 本项目 +├── OpenTake-generation/ # 本项目 └── palmier-pro-upstream/ # 上游 Swift 源码 (GPL-3.0) ``` @@ -263,9 +259,6 @@ cd web && pnpm install && pnpm build cd .. && cargo tauri dev ``` -> **当前状态**:`1.0.0-beta.2` 候选版。本地剪辑、预览、持久化、导出、Agent、 -> Motion Canvas 与可审阅 AI 工作流竖切均已实现。验证范围及平台/provider 限制见 -> [Beta 发布说明](docs/releases/1.0.0-beta.2.md)。 --- @@ -276,6 +269,10 @@ cd .. && cargo tauri dev | `0.1.0-dev` | 2026-06 | Phase 0+1: Cargo workspace + Domain models + Edit ops + Tauri scaffold | | `1.0.0-beta.1` | 2026-08-01 | 首个可安装 Beta:本地编辑闭环、Agent、Motion 与可审阅 AI 工作流 | | `1.0.0-beta.2` | 2026-08-03 | 官方 Codex 登录、原子时间线手势、安全 MCP 与交互加固 | +| `1.0.0-beta.3` | 2026-08-09 | 空格播放、HEVC 原生预览与发布流水线修复 | +| `1.0.0-beta.4` | 2026-08-10 | 时间/转场持久化、导出一致性与更新器 | +| `1.0.0-beta.5` | 2026-08-14 | 外部 MCP 配对、有序 Agent 对话与 Motion Studio | +| `1.0.0-beta.6` | [版本记录](docs/releases/1.0.0-beta.6.md) | 透明 Motion、ProRes 4444、多预览、媒体视图、文本/特效/贴纸与真实语义搜索 | | *(planned)* `1.0.0` | TBD | Phase 10: 全功能发布 — 对标剪映 + Agent 深度集成 | 📖 [完整路线图](docs/architecture/ROADMAP.md) diff --git a/crates/opentake-agent/src/mcp/dispatch.rs b/crates/opentake-agent/src/mcp/dispatch.rs index 7e4a9e4f..7c4cc23c 100644 --- a/crates/opentake-agent/src/mcp/dispatch.rs +++ b/crates/opentake-agent/src/mcp/dispatch.rs @@ -32,8 +32,8 @@ use opentake_media::analysis::{ }; use opentake_media::{PcmFormat, PcmSpec}; use opentake_ops::{ - ClipEntry, ClipMove, ClipProperties, ClipPropertyAssignment, EditCommand, FrameRange, - KeyframePayload, KeyframeProperty, RenameEntry, TextEntry, + ClipEntry, ClipMove, ClipProperties, ClipPropertyAssignment, EditCommand, FolderCreateEntry, + FolderMoveEntry, FrameRange, KeyframePayload, KeyframeProperty, RenameEntry, TextEntry, }; use serde_json::Value; @@ -709,8 +709,8 @@ impl Dispatcher { ToolName::SetKeyframes => self.set_keyframes(args), ToolName::RippleDeleteRanges => self.ripple_delete_ranges(args, before, op), ToolName::AddTexts => self.add_texts(args, before), - ToolName::CreateFolder => self.create_folder(args), - ToolName::MoveToFolder => self.move_to_folder(args), + ToolName::CreateFolder => self.create_folder(args, manifest), + ToolName::MoveToFolder => self.move_to_folder(args, manifest), ToolName::SetClipProperties => self.set_clip_properties(args, before, manifest), ToolName::SetColorGrade => self.set_color_grade(args), ToolName::ChromaKey => self.chroma_key(args), @@ -1853,16 +1853,65 @@ impl Dispatcher { fn move_clips(&self, args: &Value, before: &Timeline) -> Result { let a: MoveClipsArgs = decode_tool_args(args, "")?; - let mut moves = Vec::with_capacity(a.moves.len()); - for (i, raw) in a.moves.iter().enumerate() { - let m: MoveEntry = decode_tool_args(raw, &format!("moves[{i}]"))?; + let entries = a + .moves + .iter() + .enumerate() + .map(|(i, raw)| decode_tool_args::(raw, &format!("moves[{i}]"))) + .collect::, _>>()?; + let requested_ids: BTreeSet = + entries.iter().map(|entry| entry.clip_id.clone()).collect(); + let mut moves = Vec::with_capacity(entries.len()); + for m in entries { // Optional to_track / to_frame default to the clip's current location. let (cur_track, cur_frame) = clip_location(before, &m.clip_id); + let to_frame = m.to_frame.or(cur_frame).unwrap_or(0); moves.push(ClipMove { - clip_id: m.clip_id, + clip_id: m.clip_id.clone(), to_track: m.to_track.or(cur_track).unwrap_or(0), - to_frame: m.to_frame.or(cur_frame).unwrap_or(0), + to_frame, }); + + // Match the upstream move_clips contract: an explicit start-frame + // move propagates its delta to linked partners, while a track-only + // move leaves the partners on their own tracks and frames. If the + // caller listed a partner explicitly, its requested move is the + // authoritative one and we do not synthesize a duplicate payload. + let Some(lead) = find_clip(before, &m.clip_id) else { + continue; + }; + let Some(link_group_id) = lead.link_group_id.as_deref() else { + continue; + }; + let Some(current_frame) = cur_frame else { + continue; + }; + let Some(delta) = m.to_frame.and_then(|_| to_frame.checked_sub(current_frame)) else { + continue; + }; + if delta == 0 { + continue; + } + for (track_index, track) in before.tracks.iter().enumerate() { + for partner in &track.clips { + if partner.id == m.clip_id + || partner.link_group_id.as_deref() != Some(link_group_id) + || requested_ids.contains(partner.id.as_str()) + { + continue; + } + let partner_frame = partner + .start_frame + .checked_add(delta) + .ok_or_else(|| ToolError::new("linked partner move frame overflow"))? + .max(0); + moves.push(ClipMove { + clip_id: partner.id.clone(), + to_track: track_index, + to_frame: partner_frame, + }); + } + } } let res = self.apply(EditCommand::MoveClips { moves })?; Ok(ToolResult::ok(res.summary)) @@ -2515,18 +2564,67 @@ impl Dispatcher { Ok(ToolResult::ok(res.summary)) } - fn create_folder(&self, args: &Value) -> Result { + fn create_folder( + &self, + args: &Value, + manifest: &MediaManifest, + ) -> Result { let a: CreateFolderArgs = decode_tool_args(args, "")?; - // Single form (name / parentFolderId) only; the batch `entries` form is - // not yet wired (one CreateFolder command per call). - if a.entries.is_some() { - return Ok(ToolResult::error( - "create_folder: batch 'entries' form not yet implemented; pass name/parentFolderId", + if let Some(raw_entries) = a.entries { + if a.name.is_some() || a.parent_folder_id.is_some() { + return Err(ToolError::new( + "create_folder: pass either entries or name/parentFolderId, not both", + )); + } + if raw_entries.is_empty() { + return Err(ToolError::new("create_folder: entries must not be empty")); + } + let mut entries = Vec::with_capacity(raw_entries.len()); + for (index, raw) in raw_entries.iter().enumerate() { + let entry: args::CreateFolderEntry = + decode_tool_args(raw, &format!("entries[{index}]"))?; + if let Some(parent) = entry.parent_folder_id.as_deref() { + if !manifest.folders.iter().any(|folder| folder.id == parent) { + return Err(ToolError::new(format!( + "entries[{index}]: parentFolderId not found: {parent}" + ))); + } + } + entries.push(FolderCreateEntry { + name: entry.name, + parent_folder_id: entry.parent_folder_id, + }); + } + let response_entries = entries.clone(); + let res = self.apply(EditCommand::CreateFolders { entries })?; + let folders = response_entries + .iter() + .zip(res.affected_clip_ids.iter()) + .map(|(entry, id)| { + let mut folder = serde_json::json!({ + "id": id, + "name": entry.name, + }); + if let Some(parent) = &entry.parent_folder_id { + folder["parentFolderId"] = serde_json::json!(parent); + } + folder + }) + .collect::>(); + return Ok(ToolResult::ok( + serde_json::json!({ "folders": folders }).to_string(), )); } let Some(name) = a.name else { return Err(ToolError::new("arguments: missing required field 'name'")); }; + if let Some(parent) = a.parent_folder_id.as_deref() { + if !manifest.folders.iter().any(|folder| folder.id == parent) { + return Err(ToolError::new(format!( + "create_folder: parentFolderId not found: {parent}" + ))); + } + } let res = self.apply(EditCommand::CreateFolder { name, parent_folder_id: a.parent_folder_id, @@ -2534,18 +2632,74 @@ impl Dispatcher { Ok(ToolResult::ok(res.summary)) } - fn move_to_folder(&self, args: &Value) -> Result { + fn move_to_folder( + &self, + args: &Value, + manifest: &MediaManifest, + ) -> Result { let a: MoveToFolderArgs = decode_tool_args(args, "")?; - if a.entries.is_some() { - return Ok(ToolResult::error( - "move_to_folder: batch 'entries' form not yet implemented; pass assetIds/folderId", - )); + if let Some(raw_entries) = a.entries { + if a.asset_ids.is_some() || a.folder_id.is_some() { + return Err(ToolError::new( + "move_to_folder: pass either entries or assetIds/folderId, not both", + )); + } + if raw_entries.is_empty() { + return Err(ToolError::new("move_to_folder: entries must not be empty")); + } + let mut entries = Vec::with_capacity(raw_entries.len()); + for (index, raw) in raw_entries.iter().enumerate() { + let entry: args::MoveToFolderEntry = + decode_tool_args(raw, &format!("entries[{index}]"))?; + if entry.asset_ids.is_empty() { + return Err(ToolError::new(format!( + "entries[{index}]: assetIds must not be empty" + ))); + } + for asset_id in &entry.asset_ids { + if !manifest.entries.iter().any(|asset| asset.id == *asset_id) { + return Err(ToolError::new(format!( + "entries[{index}]: media asset not found: {asset_id}" + ))); + } + } + if let Some(folder_id) = entry.folder_id.as_deref() { + if !manifest.folders.iter().any(|folder| folder.id == folder_id) { + return Err(ToolError::new(format!( + "entries[{index}]: folderId not found: {folder_id}" + ))); + } + } + entries.push(FolderMoveEntry { + asset_ids: entry.asset_ids, + folder_id: entry.folder_id, + }); + } + let res = self.apply(EditCommand::MoveToFolders { entries })?; + return Ok(ToolResult::ok(res.summary)); } let Some(asset_ids) = a.asset_ids else { return Err(ToolError::new( "arguments: missing required field 'assetIds'", )); }; + if asset_ids.is_empty() { + return Err(ToolError::new("arguments: 'assetIds' must not be empty")); + } + for asset_id in &asset_ids { + if !manifest.entries.iter().any(|asset| asset.id == *asset_id) { + return Err(ToolError::new(format!( + "move_to_folder: media asset not found: {asset_id}" + ))); + } + } + if let Some(folder_id) = a.folder_id.as_deref() { + if !manifest.folders.iter().any(|folder| folder.id == folder_id) { + return Err(ToolError::new(format!( + "move_to_folder: folderId not found: {folder_id}" + ))); + } + } let res = self.apply(EditCommand::MoveToFolder { asset_ids, folder_id: a.folder_id, @@ -5313,6 +5467,22 @@ mod tests { Arc::new(StateHandle::new(tl, m)) } + fn linked_move_handle() -> Arc { + let mut tl = Timeline::new(); + let mut video_track = Track::new("video-track", ClipType::Video); + let mut video = Clip::new("video-1", "video-asset", 20, 30); + video.link_group_id = Some("av-group".into()); + video_track.clips.push(video); + let mut audio_track = Track::new("audio-track", ClipType::Audio); + let mut audio = Clip::new("audio-1", "audio-asset", 14, 30); + audio.media_type = ClipType::Audio; + audio.source_clip_type = ClipType::Audio; + audio.link_group_id = Some("av-group".into()); + audio_track.clips.push(audio); + tl.tracks = vec![video_track, audio_track]; + Arc::new(StateHandle::new(tl, MediaManifest::new())) + } + fn seeded_transform_handle( transform: Transform, media_size: Option<(i32, i32)>, @@ -5374,6 +5544,36 @@ mod tests { assert_eq!(handle.timeline().tracks[0].clips[0].start_frame, 0); } + #[test] + fn move_clips_dispatch_propagates_frame_delta_to_linked_partner() { + let handle = linked_move_handle(); + let dispatcher = dispatcher_with(handle.clone()); + let result = dispatcher.dispatch( + "move_clips", + serde_json::json!({"moves":[{"clipId":"video-1","toFrame":35}]}), + ); + + assert!(!result.is_error, "{}", result.text_joined()); + let timeline = handle.timeline(); + assert_eq!(timeline.tracks[0].clips[0].start_frame, 35); + assert_eq!(timeline.tracks[1].clips[0].start_frame, 29); + } + + #[test] + fn move_clips_track_only_does_not_move_linked_partner() { + let handle = linked_move_handle(); + let dispatcher = dispatcher_with(handle.clone()); + let result = dispatcher.dispatch( + "move_clips", + serde_json::json!({"moves":[{"clipId":"video-1","toTrack":0}]}), + ); + + assert!(!result.is_error, "{}", result.text_joined()); + let timeline = handle.timeline(); + assert_eq!(timeline.tracks[0].clips[0].start_frame, 20); + assert_eq!(timeline.tracks[1].clips[0].start_frame, 14); + } + #[test] fn poisoned_agent_undo_mutex_does_not_break_later_edit_or_undo() { let handle = seeded_handle(); @@ -6493,6 +6693,69 @@ mod tests { assert_eq!(h.media().entries[0].name, "Hero Shot"); } + #[test] + fn create_folder_batch_returns_records_and_uses_one_undo_step() { + let h = Arc::new(StateHandle::new(Timeline::new(), MediaManifest::new())); + let d = dispatcher_with(h.clone()); + + let result = d.dispatch( + "create_folder", + serde_json::json!({ + "entries": [{"name": "A-Roll"}, {"name": "B-Roll"}] + }), + ); + + assert!(!result.is_error, "{}", result.text_joined()); + let body = first_json(&result); + let folders = body["folders"].as_array().expect("batch folders"); + assert_eq!(folders.len(), 2); + assert_eq!(folders[0]["name"], "A-Roll"); + assert_eq!(folders[1]["name"], "B-Roll"); + assert_eq!(h.media().folders.len(), 2); + + let undo = d.dispatch("undo", serde_json::json!({})); + assert!(!undo.is_error, "{}", undo.text_joined()); + assert!(h.media().folders.is_empty(), "batch must undo atomically"); + } + + #[test] + fn move_to_folder_batch_moves_each_entry_and_uses_one_undo_step() { + let mut manifest = MediaManifest::new(); + manifest.entries.push(entry("asset-a", "A")); + manifest.entries.push(entry("asset-b", "B")); + manifest + .folders + .push(opentake_domain::MediaFolder::new("folder-a", "A-Roll")); + manifest + .folders + .push(opentake_domain::MediaFolder::new("folder-b", "B-Roll")); + let h = Arc::new(StateHandle::new(Timeline::new(), manifest)); + let d = dispatcher_with(h.clone()); + + let result = d.dispatch( + "move_to_folder", + serde_json::json!({ + "entries": [ + {"assetIds": ["asset-a"], "folderId": "folder-a"}, + {"assetIds": ["asset-b"], "folderId": "folder-b"} + ] + }), + ); + + assert!(!result.is_error, "{}", result.text_joined()); + let media = h.media(); + assert_eq!(media.entries[0].folder_id.as_deref(), Some("folder-a")); + assert_eq!(media.entries[1].folder_id.as_deref(), Some("folder-b")); + + let undo = d.dispatch("undo", serde_json::json!({})); + assert!(!undo.is_error, "{}", undo.text_joined()); + let restored = h.media(); + assert!(restored + .entries + .iter() + .all(|asset| asset.folder_id.is_none())); + } + #[test] fn delete_media_cascades_referencing_clip() { let h = seeded_handle(); diff --git a/crates/opentake-agent/src/mcp/motion_documents.rs b/crates/opentake-agent/src/mcp/motion_documents.rs index acb74395..3434e36f 100644 --- a/crates/opentake-agent/src/mcp/motion_documents.rs +++ b/crates/opentake-agent/src/mcp/motion_documents.rs @@ -113,6 +113,10 @@ pub struct MotionDocumentPublishRequest { pub duration_frames: i32, pub start_frame: Option, pub track_index: Option, + /// Transparent output is supported for a new published clip. Existing + /// Motion clip replacement remains opaque until alpha-preserving edit is + /// implemented end to end. + pub transparent: bool, pub clip_id: Option, } @@ -303,6 +307,8 @@ struct PublishArgs { duration_frames: i32, start_frame: Option, track_index: Option, + #[serde(default)] + transparent: bool, clip_id: Option, } impl ToolArgs for PublishArgs { @@ -315,6 +321,7 @@ impl ToolArgs for PublishArgs { "durationFrames", "startFrame", "trackIndex", + "transparent", "clipId", ]; } @@ -440,6 +447,11 @@ pub(crate) fn decode_request( "trackIndex must be omitted when clipId is provided", )); } + if args.clip_id.is_some() && args.transparent { + return Err(ToolError::new( + "transparent is supported only when publishing a new Motion clip", + )); + } Ok(MotionDocumentRequest::Publish( MotionDocumentPublishRequest { document_id: args.document_id, @@ -450,6 +462,7 @@ pub(crate) fn decode_request( duration_frames: args.duration_frames, start_frame: args.start_frame, track_index: args.track_index, + transparent: args.transparent, clip_id: args.clip_id, }, )) @@ -764,6 +777,33 @@ mod tests { }), ); assert!(publish.is_err()); + + let transparent = decode_request( + ToolName::PublishMotionDocument, + &serde_json::json!({ + "documentId": ID, "revisionHash": HASH, + "width": 1920, "height": 1080, "fps": 60, + "durationFrames": 120, "startFrame": 0, "transparent": true + }), + ) + .expect("transparent Motion document add is part of the publish contract"); + let MotionDocumentRequest::Publish(transparent) = transparent else { + panic!("expected publish request"); + }; + assert!(transparent.transparent); + + let transparent_edit = decode_request( + ToolName::PublishMotionDocument, + &serde_json::json!({ + "documentId": ID, "revisionHash": HASH, + "width": 1920, "height": 1080, "fps": 60, + "durationFrames": 120, "clipId": "clip", "transparent": true + }), + ); + assert!(transparent_edit + .expect_err("alpha edit is not implemented yet") + .to_string() + .contains("new Motion clip")); } #[test] diff --git a/crates/opentake-agent/src/tools/descriptions.rs b/crates/opentake-agent/src/tools/descriptions.rs index 62f5aa98..63b3d3e4 100644 --- a/crates/opentake-agent/src/tools/descriptions.rs +++ b/crates/opentake-agent/src/tools/descriptions.rs @@ -106,7 +106,7 @@ pub fn description(tool: ToolName) -> &'static str { ToolName::ApplyEffect => "Sets the effect chain on one or more clips in one undoable action. The closed effect registry is grayscale, sepia, and invert; each accepts an optional amount from 0 to 1 (default 1). Effects execute in list order in the shared preview/export GPU compositor. Pass enabled:false to retain a disabled effect. The list replaces the current chain; pass an empty array to clear it. Unknown names, parameters, non-finite values, and out-of-range values are rejected instead of rendering unchanged. Applies to every clip in clipIds.", // --- OpenTake deterministic motion graphics (Issue #34 fallback vertical) --- - ToolName::AddMotionGraphic => "Renders a deterministic motion graphic to MP4, imports it, and places it on the timeline as one durable undoable workflow. Returns the new clipId. The packaged Beta uses the pinned Motion Canvas 3.17.2 runner for 'title-card'; it also supports the local 'lower-third.glass' template and self-contained HTML/CSS/JS fallback (animated through OpenTake.onSeek). Raw TypeScript/TSX and transparent output are reported as unsupported instead of being accepted as placeholders.\n\nstartFrame/durationFrames are project frames (from get_timeline). trackIndex is optional — omit to auto-create a new visual track; set it to target an existing non-audio track.", + ToolName::AddMotionGraphic => "Renders a deterministic motion graphic, imports it, and places it on the timeline as one durable undoable workflow. Output is MP4 by default; set transparent=true for a ProRes 4444 .mov with alpha. Returns the new clipId. The packaged Beta uses the pinned Motion Canvas 3.17.2 runner for 'title-card'; it also supports the local 'lower-third.glass' template and self-contained HTML/CSS/JS fallback (animated through OpenTake.onSeek). Raw TypeScript/TSX is not supported by the packaged renderer.\n\nstartFrame/durationFrames are project frames (from get_timeline). trackIndex is optional — omit to auto-create a new visual track; set it to target an existing non-audio track.", ToolName::EditMotionGraphic => "Re-renders an existing OpenTake motion graphic as one durable undoable workflow while preserving its timeline clipId and placement. Pass the clipId and either replacement self-contained HTML/CSS/JS for a code-authored graphic or parameter overrides for a template-authored graphic. Ordinary video clips and unsupported source types are rejected with typed errors.", @@ -120,7 +120,7 @@ pub fn description(tool: ToolName) -> &'static str { ToolName::PreviewMotionDocument => "Renders one bounded frame from the exact saved Motion Studio revision with the production Chromium renderer. Returns a PNG and diagnostics; it never changes the document or timeline.", - ToolName::PublishMotionDocument => "Publishes the exact saved Motion Studio revision through the production Chromium/FFmpeg atomic timeline path. Omit clipId and provide startFrame to add a clip; provide clipId and omit startFrame to replace that existing Motion clip. Returns committed clip/media ids and source revision.", + ToolName::PublishMotionDocument => "Publishes the exact saved Motion Studio revision through the production Chromium/FFmpeg atomic timeline path. Omit clipId and provide startFrame to add a clip; set transparent=true for a ProRes 4444 .mov with alpha. Provide clipId and omit startFrame to replace that existing Motion clip; omit transparent for edits, and an existing transparent Motion clip preserves its alpha format. Returns committed clip/media ids and source revision.", ToolName::TrackMotion => "Analyzes a bounded source region and returns editable position keyframes that follow the subject. Defaults to preview-only; set apply=true only after reviewing confidence and samples. Applying is one undoable edit. The tool is advertised only when a production tracking backend is available.", ToolName::GenerateMatte => "Generates a frame-aligned reusable alpha matte for one clip without modifying the source asset. Defaults to preview-only and reports model/version/progress metadata. Applying the matte is one undoable edit. The tool is advertised only when an installed compatible model is available.", @@ -721,7 +721,7 @@ pub fn input_schema(tool: ToolName) -> Value { }, "startFrame": {"type": "integer", "description": "Timeline frame position to place the graphic (project frames)."}, "durationFrames": {"type": "integer", "description": "Clip length on the timeline, in project frames (>= 1)."}, - "transparent": {"type": "boolean", "description": "Forward-compatible alpha intent. The current MP4 path rejects true with a typed unsupported-capability error."}, + "transparent": {"type": "boolean", "description": "When true, publish a transparent ProRes 4444 .mov with alpha instead of the default opaque MP4."}, "trackIndex": {"type": "integer", "description": "Optional. Existing non-audio track index (0-based) to place the graphic on. Omit to auto-create a new video track at the top."} }), &["source", "startFrame", "durationFrames"], @@ -807,6 +807,7 @@ pub fn input_schema(tool: ToolName) -> Value { "durationFrames": {"type": "integer", "minimum": 1, "maximum": 3600}, "startFrame": {"type": "integer", "minimum": 0, "description": "Required for add; omit for edit."}, "trackIndex": {"type": "integer", "minimum": 0, "description": "Optional existing visual track for add."}, + "transparent": {"type": "boolean", "description": "For a new clip, publish a transparent ProRes 4444 .mov with alpha instead of opaque MP4. Omit for edits; existing alpha is preserved."}, "clipId": {"type": "string", "description": "Existing Motion clip to replace; omit for add."} }), &[ diff --git a/crates/opentake-core/src/lib.rs b/crates/opentake-core/src/lib.rs index b9eb7f20..ab0a470f 100644 --- a/crates/opentake-core/src/lib.rs +++ b/crates/opentake-core/src/lib.rs @@ -52,7 +52,8 @@ pub use crate::core::{ pub use session::{ importable_clip_type, DerivedStemProvenance, EditorSession, GenerationJobCommit, GenerationStateUpdate, PreparedGenerationJob, PreparedGenerationOutput, ProbedMedia, - SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_VIDEO_EXTENSIONS, + SUPPORTED_AUDIO_EXTENSIONS, SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_LOTTIE_EXTENSIONS, + SUPPORTED_VIDEO_EXTENSIONS, }; // --- Events --- diff --git a/crates/opentake-core/src/session.rs b/crates/opentake-core/src/session.rs index 79f6bcf9..7aa953b5 100644 --- a/crates/opentake-core/src/session.rs +++ b/crates/opentake-core/src/session.rs @@ -143,9 +143,9 @@ pub(crate) struct GenerationStateCheckpoint { /// list. OpenTake's importer routes every decode through the system `ffmpeg`, /// which handles a much wider set of containers/codecs cross-platform, so the /// white-list is widened to the formats ffmpeg reads well rather than mirroring -/// upstream's narrow macOS-native list. The Lottie/JSON special-case is still -/// excluded (it needs a content sniff the bare extension can't provide, so JSON -/// files are not auto-imported here). +/// upstream's narrow macOS-native list. Lottie JSON and `.lottie` containers +/// are admitted by extension and validated by the Tauri render/materializer +/// boundary before they are shown as usable media. pub const SUPPORTED_VIDEO_EXTENSIONS: [&str; 14] = [ "mov", "mp4", "m4v", "mkv", "webm", "avi", "mts", "m2ts", "mpg", "mpeg", "3gp", "wmv", "flv", "ts", @@ -158,10 +158,13 @@ pub const SUPPORTED_AUDIO_EXTENSIONS: [&str; 11] = [ pub const SUPPORTED_IMAGE_EXTENSIONS: [&str; 9] = [ "png", "jpg", "jpeg", "tiff", "heic", "webp", "bmp", "gif", "avif", ]; +/// Accepted Lottie document extensions. Validation happens in the desktop +/// renderer because the zero-dependency core must not link the Velato parser. +pub const SUPPORTED_LOTTIE_EXTENSIONS: [&str; 2] = ["json", "lottie"]; /// The [`ClipType`] for `path` if its (lowercased) extension is on the import -/// white-list, else `None`. JSON/Lottie are intentionally excluded (see -/// [`SUPPORTED_VIDEO_EXTENSIONS`]). +/// white-list, else `None`. Lottie files map to [`ClipType::Lottie`] and are +/// validated by the host before registration. pub fn importable_clip_type(path: &Path) -> Option { let ext = path.extension()?.to_str()?.to_ascii_lowercase(); if SUPPORTED_VIDEO_EXTENSIONS.contains(&ext.as_str()) { @@ -170,6 +173,8 @@ pub fn importable_clip_type(path: &Path) -> Option { Some(ClipType::Audio) } else if SUPPORTED_IMAGE_EXTENSIONS.contains(&ext.as_str()) { Some(ClipType::Image) + } else if SUPPORTED_LOTTIE_EXTENSIONS.contains(&ext.as_str()) { + Some(ClipType::Lottie) } else { None } @@ -1506,8 +1511,14 @@ mod tests { importable_clip_type(Path::new("/x/pic.JPG")), Some(ClipType::Image) ); - // JSON/Lottie is intentionally not auto-importable here. - assert_eq!(importable_clip_type(Path::new("/x/anim.json")), None); + assert_eq!( + importable_clip_type(Path::new("/x/anim.json")), + Some(ClipType::Lottie) + ); + assert_eq!( + importable_clip_type(Path::new("/x/anim.lottie")), + Some(ClipType::Lottie) + ); assert_eq!(importable_clip_type(Path::new("/x/notes.txt")), None); assert_eq!(importable_clip_type(Path::new("/x/noext")), None); } diff --git a/crates/opentake-domain/src/media.rs b/crates/opentake-domain/src/media.rs index 26efee3b..e428f03d 100644 --- a/crates/opentake-domain/src/media.rs +++ b/crates/opentake-domain/src/media.rs @@ -129,6 +129,10 @@ pub struct GenerationInput { /// Video-only. #[serde(default, skip_serializing_if = "Option::is_none")] pub generate_audio: Option, + /// Whether the generated media carries straight alpha that must be + /// premultiplied exactly once before compositing. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transparent: Option, #[serde( rename = "referenceImageURLs", default, @@ -253,7 +257,9 @@ impl MediaManifestEntry { /// to request one premultiplication before blending. pub fn carries_straight_alpha(&self) -> bool { self.generation_input.as_ref().is_some_and(|input| { - input.provider.as_deref() == Some("opentake-matting") && input.model.starts_with("rvm-") + input.transparent == Some(true) + || (input.provider.as_deref() == Some("opentake-matting") + && input.model.starts_with("rvm-")) }) } } @@ -788,6 +794,29 @@ mod tests { assert!(!json.contains("voice")); } + #[test] + fn motion_transparent_generation_input_marks_straight_alpha() { + let entry: MediaManifestEntry = serde_json::from_value(serde_json::json!({ + "id": "motion-alpha", + "name": "motion.mov", + "type": "video", + "source": {"external": {"absolutePath": "/tmp/motion.mov"}}, + "duration": 1.0, + "generationInput": { + "prompt": "{}", + "model": "opentake.motion-v1", + "duration": 30, + "aspectRatio": "16:9", + "provider": "opentake-motion", + "transparent": true, + "status": "ready" + } + })) + .expect("transparent motion manifest entry"); + + assert!(entry.carries_straight_alpha()); + } + // --- MediaManifestEntry --- #[test] diff --git a/crates/opentake-media/Cargo.toml b/crates/opentake-media/Cargo.toml index 378c6cdd..2bfd2270 100644 --- a/crates/opentake-media/Cargo.toml +++ b/crates/opentake-media/Cargo.toml @@ -81,7 +81,7 @@ test-faults = [] # tract backend so the installed product does not depend on Server-only # DirectX/ONNX Runtime entry points. Other platforms keep the pinned native # ONNX Runtime backend. Both remain off in the default, fully-offline build. -ort-backend = ["dep:ort", "dep:ort-tract"] +ort-backend = ["dep:ort", "dep:ort-tract", "dep:tract-onnx"] # Real on-device transcription via whisper.cpp (compiles native C++ on enable). whisper-backend = ["dep:whisper-rs"] # Model weight download/verify/unzip (reqwest + zip + sha1). Off by default so @@ -105,6 +105,12 @@ optional = true version = "=0.2.0" optional = true +# Fixed-shape search encoders need input-fact binding, which ort-tract's ORT +# compatibility API does not expose. Reuse the exact engine already in Cargo.lock. +[target.'cfg(windows)'.dependencies.tract-onnx] +version = "=0.22.3" +optional = true + [dependencies.whisper-rs] version = "0.14" default-features = false diff --git a/crates/opentake-media/examples/search_manifest.rs b/crates/opentake-media/examples/search_manifest.rs new file mode 100644 index 00000000..fb3f2e34 --- /dev/null +++ b/crates/opentake-media/examples/search_manifest.rs @@ -0,0 +1,19 @@ +//! Export the production manifest for opt-in model qualification tooling. + +#[cfg(feature = "model-download")] +fn main() { + use opentake_media::search::config; + println!( + "{}", + serde_json::json!({ + "base_url": config::MODEL_DOWNLOAD_BASE_URL, + "manifest": config::manifest(), + }) + ); +} + +#[cfg(not(feature = "model-download"))] +fn main() { + eprintln!("search_manifest requires --features model-download"); + std::process::exit(2); +} diff --git a/crates/opentake-media/src/analysis/matting.rs b/crates/opentake-media/src/analysis/matting.rs index 226d0c2c..0694d047 100644 --- a/crates/opentake-media/src/analysis/matting.rs +++ b/crates/opentake-media/src/analysis/matting.rs @@ -240,7 +240,7 @@ impl RvmMattingSession { let width = frame.width as usize; let height = frame.height as usize; let mut src = vec![0.0_f32; 3 * width * height]; - for (index, pixel) in frame.rgba.chunks_exact(4).enumerate() { + for (index, pixel) in frame.rgba.as_chunks::<4>().0.iter().enumerate() { src[index] = pixel[0] as f32 / 255.0; src[width * height + index] = pixel[1] as f32 / 255.0; src[2 * width * height + index] = pixel[2] as f32 / 255.0; diff --git a/crates/opentake-media/src/analysis/stems.rs b/crates/opentake-media/src/analysis/stems.rs index 9a7233ee..d614686f 100644 --- a/crates/opentake-media/src/analysis/stems.rs +++ b/crates/opentake-media/src/analysis/stems.rs @@ -264,7 +264,7 @@ pub fn separate_stems( .try_reserve_exact(input.len()) .map_err(|error| MediaError::Decode(format!("stem_audio_allocation_failed: {error}")))?; let mut side_energy = 0.0_f64; - for (index, frame) in input.chunks_exact(2).enumerate() { + for (index, frame) in input.as_chunks::<2>().0.iter().enumerate() { if index.is_multiple_of(8 * 1024) { if cancel.checkpoint() { return Err(MediaError::Cancelled); diff --git a/crates/opentake-media/src/cancel.rs b/crates/opentake-media/src/cancel.rs index d51ffc61..54beda9c 100644 --- a/crates/opentake-media/src/cancel.rs +++ b/crates/opentake-media/src/cancel.rs @@ -1,18 +1,52 @@ -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::Arc; +use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, Weak}; -#[derive(Default)] struct CancelState { - cancelled: AtomicBool, + /// 0 = active, 1 = cancelled, 2 = committed. A committed operation no + /// longer accepts cancellation, which gives callers a final linearization + /// point after all output validation has passed. + status: AtomicU8, spawned_children: AtomicUsize, active_readers: AtomicUsize, checkpoints: AtomicUsize, } +impl Default for CancelState { + fn default() -> Self { + Self { + status: AtomicU8::new(0), + spawned_children: AtomicUsize::new(0), + active_readers: AtomicUsize::new(0), + checkpoints: AtomicUsize::new(0), + } + } +} + +struct CancellationCoordinator { + parent: Arc, + active_phase: Mutex>>, +} + /// Cloneable cooperative cancellation shared by media workers and FFmpeg jobs. -#[derive(Clone, Default)] +#[derive(Clone)] pub struct MediaCancelToken { state: Arc, + coordinator: Arc, + is_phase: bool, +} + +impl Default for MediaCancelToken { + fn default() -> Self { + let state = Arc::new(CancelState::default()); + Self { + state: Arc::clone(&state), + coordinator: Arc::new(CancellationCoordinator { + parent: state, + active_phase: Mutex::new(None), + }), + is_phase: false, + } + } } impl MediaCancelToken { @@ -20,12 +54,93 @@ impl MediaCancelToken { Self::default() } + /// Create a cancellation scope whose cancellation inherits from this + /// token, while a final commit on the child does not commit the parent + /// workflow. Long-running workflows use this for independently committed + /// phases such as timeline preprocessing followed by upload. + pub fn child(&self) -> Self { + let state = Arc::new(CancelState::default()); + { + let mut active_phase = self + .coordinator + .active_phase + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + match self.coordinator.parent.status.load(Ordering::Acquire) { + 0 => *active_phase = Some(Arc::downgrade(&state)), + 1 => state.status.store(1, Ordering::Release), + 2 => state.status.store(2, Ordering::Release), + _ => unreachable!("cancel status is a three-state contract"), + } + } + Self { + state, + coordinator: Arc::clone(&self.coordinator), + is_phase: true, + } + } + pub fn cancel(&self) { - self.state.cancelled.store(true, Ordering::Release); + let mut active_phase = self + .coordinator + .active_phase + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if self.is_phase { + let _ = self + .state + .status + .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire); + if active_phase + .as_ref() + .and_then(Weak::upgrade) + .is_some_and(|phase| Arc::ptr_eq(&phase, &self.state)) + { + *active_phase = None; + } + return; + } + let parent = &self.coordinator.parent; + let _ = parent + .status + .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire); + if let Some(phase) = active_phase.take().and_then(|phase| phase.upgrade()) { + let _ = phase + .status + .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire); + } + } + + /// Atomically claim the final success boundary. Returns false when + /// cancellation won the race; later `cancel()` calls cannot invalidate a + /// committed operation. + pub fn try_commit(&self) -> bool { + let mut active_phase = self + .coordinator + .active_phase + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if self.is_phase && self.coordinator.parent.status.load(Ordering::Acquire) == 1 { + self.state.status.store(1, Ordering::Release); + *active_phase = None; + return false; + } + let committed = self + .state + .status + .compare_exchange(0, 2, Ordering::AcqRel, Ordering::Acquire) + .is_ok(); + if committed && self.is_phase { + *active_phase = None; + } + committed } pub fn is_cancelled(&self) -> bool { - self.state.cancelled.load(Ordering::Acquire) + self.state.status.load(Ordering::Acquire) == 1 + || (self.is_phase + && self.state.status.load(Ordering::Acquire) == 0 + && self.coordinator.parent.status.load(Ordering::Acquire) == 1) } pub(crate) fn child_spawned(&self) { @@ -66,3 +181,85 @@ impl MediaCancelToken { Arc::ptr_eq(&self.state, &other.state) } } + +#[cfg(test)] +mod tests { + use super::MediaCancelToken; + use std::sync::{Arc, Barrier}; + use std::thread; + + #[test] + fn commit_blocks_later_cancellation() { + let token = MediaCancelToken::new(); + assert!(token.try_commit()); + token.cancel(); + assert!(!token.is_cancelled()); + assert!(!token.try_commit()); + } + + #[test] + fn cancellation_blocks_commit() { + let token = MediaCancelToken::new(); + token.cancel(); + assert!(!token.try_commit()); + assert!(token.is_cancelled()); + } + + #[test] + fn child_commit_does_not_commit_parent_workflow() { + let parent = MediaCancelToken::new(); + let child = parent.child(); + assert!(child.try_commit()); + assert!(!parent.is_cancelled()); + assert!(parent.try_commit()); + } + + #[test] + fn parent_cancel_and_child_commit_have_one_linearized_winner() { + for _ in 0..32 { + let parent = Arc::new(MediaCancelToken::new()); + let child = parent.child(); + let child_observer = child.clone(); + let barrier = Arc::new(Barrier::new(3)); + let commit_barrier = Arc::clone(&barrier); + let cancel_barrier = Arc::clone(&barrier); + let commit = thread::spawn(move || { + commit_barrier.wait(); + child.try_commit() + }); + let cancel_parent = Arc::clone(&parent); + let cancel = thread::spawn(move || { + cancel_barrier.wait(); + cancel_parent.cancel(); + }); + barrier.wait(); + let committed = commit.join().expect("child commit thread"); + cancel.join().expect("parent cancel thread"); + assert_eq!(committed, !child_observer.is_cancelled()); + } + } + + #[test] + fn commit_and_cancel_have_one_linearized_winner() { + for _ in 0..32 { + let token = Arc::new(MediaCancelToken::new()); + let barrier = Arc::new(Barrier::new(3)); + let commit_token = Arc::clone(&token); + let cancel_token = Arc::clone(&token); + let commit_barrier = Arc::clone(&barrier); + let cancel_barrier = Arc::clone(&barrier); + let commit = thread::spawn(move || { + commit_barrier.wait(); + commit_token.try_commit() + }); + let cancel = thread::spawn(move || { + cancel_barrier.wait(); + cancel_token.cancel(); + }); + barrier.wait(); + let committed = commit.join().expect("commit thread"); + cancel.join().expect("cancel thread"); + assert_eq!(committed, !token.is_cancelled()); + } + } +} diff --git a/crates/opentake-media/src/decode/audio_stream.rs b/crates/opentake-media/src/decode/audio_stream.rs index bbc467ea..eaaaff8f 100644 --- a/crates/opentake-media/src/decode/audio_stream.rs +++ b/crates/opentake-media/src/decode/audio_stream.rs @@ -60,12 +60,16 @@ fn raw_to_interleaved_f32(bytes: &[u8], spec: &PcmSpec) -> Result> { match spec.format { PcmFormat::F32 => out.extend( bytes - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])), ), PcmFormat::S16Le => out.extend( bytes - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0), ), } diff --git a/crates/opentake-media/src/decode/pcm.rs b/crates/opentake-media/src/decode/pcm.rs index 8a3b5d6e..dbc07b4f 100644 --- a/crates/opentake-media/src/decode/pcm.rs +++ b/crates/opentake-media/src/decode/pcm.rs @@ -72,22 +72,12 @@ fn allocation_error(detail: impl std::fmt::Display) -> MediaError { MediaError::Decode(format!("audio_allocation_failed: {detail}")) } -fn expected_pcm_bytes(path: &Path, spec: &PcmSpec, range: Option<(f64, f64)>) -> Result { +fn expected_pcm_bytes_for_duration(duration_secs: f64, spec: &PcmSpec) -> Result { if spec.sample_rate == 0 || spec.channels == 0 { return Err(MediaError::Decode( "PCM sample rate and channel count must be non-zero".to_string(), )); } - let duration_secs = match range { - Some((lo, hi)) => (hi - lo.max(0.0)).max(0.0), - None => { - let media = probe::probe(path)?; - if !media.has_audio { - return Err(MediaError::no_track("audio", path)); - } - media.duration_secs - } - }; if !duration_secs.is_finite() { return Err(audio_buffer_too_large("non-finite duration")); } @@ -103,6 +93,20 @@ fn expected_pcm_bytes(path: &Path, spec: &PcmSpec, range: Option<(f64, f64)>) -> .ok_or_else(|| audio_buffer_too_large("PCM output byte count overflow")) } +fn expected_pcm_bytes(path: &Path, spec: &PcmSpec, range: Option<(f64, f64)>) -> Result { + let duration_secs = match range { + Some((lo, hi)) => (hi - lo.max(0.0)).max(0.0), + None => { + let media = probe::probe(path)?; + if !media.has_audio { + return Err(MediaError::no_track("audio", path)); + } + media.duration_secs + } + }; + expected_pcm_bytes_for_duration(duration_secs, spec) +} + fn read_stdout( mut stdout: ChildStdout, cap: usize, @@ -301,6 +305,28 @@ fn pcm_args(path: &Path, spec: &PcmSpec, range: Option<(f64, f64)>) -> Vec, + max_frames: usize, +) -> Vec { + let mut args = pcm_args(path, spec, range); + args.pop(); // stdout destination follows all output options + args.extend([ + "-af".into(), + // AAC decoders may retain a padded final packet beyond the container's + // duration. Enforce the same frame budget as the stdout reader, after + // conversion to the requested sample rate, without relaxing that cap. + format!( + "aresample={},atrim=end_sample={max_frames}", + spec.sample_rate + ), + "-".into(), + ]); + args +} + /// Convert interleaved raw PCM bytes to mono f32, averaging `channels`. #[cfg(test)] fn raw_to_mono_f32(bytes: &[u8], spec: &PcmSpec) -> Result> { @@ -424,23 +450,39 @@ pub(super) fn decode_raw_pcm_cancellable( } else { None }; - let effective_range = match range { - Some(range) => Some(range), + let expected_bytes = match range { + Some(range) => expected_pcm_bytes(path, spec, Some(range))?, None => { let media = probed.unwrap_or(probe::probe(path)?); - Some((0.0, media.duration_secs)) + expected_pcm_bytes_for_duration(media.duration_secs, spec)? } }; - let expected_bytes = expected_pcm_bytes(path, spec, effective_range)?; let frame_bytes = usize::from(spec.channels) .checked_mul(spec.format.bytes_per_sample()) .ok_or_else(|| audio_buffer_too_large("PCM frame byte count overflow"))?; + // Full-track extraction must preserve decoder output, including padding + // excluded from a container's presentation duration. Admit at most one + // extra second for that discrepancy; explicit ranges retain one-frame + // rounding slack and are trimmed at the requested output sample rate. + let slack_frames = if range.is_none() { + spec.sample_rate as usize + } else { + 1 + }; + let slack_bytes = slack_frames + .checked_mul(frame_bytes) + .ok_or_else(|| audio_buffer_too_large("PCM padding budget overflow"))?; let reader_cap = expected_bytes - .checked_add(frame_bytes) + .checked_add(slack_bytes) .ok_or_else(|| audio_buffer_too_large("PCM reader cap overflow"))?; + let args = if range.is_some() { + bounded_pcm_args(path, spec, range, expected_bytes / frame_bytes) + } else { + pcm_args(path, spec, None) + }; let mut child = ff::ffmpeg() - .args(pcm_args(path, spec, effective_range)) + .args(args) .spawn() .map_err(|e| MediaError::Ffmpeg(format!("spawn: {e}")))?; cancel.child_spawned(); @@ -544,6 +586,31 @@ mod tests { assert_eq!(cancel.spawned_child_count(), 0); } + #[test] + fn output_frame_limit_trims_padding_after_resampling() { + assert!(crate::ff::ffmpeg_available(), "requires runnable FFmpeg"); + let temp = tempfile::tempdir().unwrap(); + let input = temp.path().join("padded.wav"); + // Supply 2 seconds at 48k, but permit only the reported 1 second at + // 16k. Trimming before resampling would return just 5,333 samples. + write_silence_wav(&input, 48_000, 96_000); + let spec = PcmSpec { + sample_rate: 16_000, + channels: 1, + format: PcmFormat::F32, + }; + let output = std::process::Command::new(crate::ff::ffmpeg_path()) + .args(bounded_pcm_args(&input, &spec, None, 16_000)) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(output.stdout.len(), 16_000 * 4); + } + #[test] fn pcm_decode_reports_non_terminal_progress_for_multiple_stdout_chunks() { assert!( diff --git a/crates/opentake-media/src/encode/mod.rs b/crates/opentake-media/src/encode/mod.rs index 8ee4ef72..daf6153c 100644 --- a/crates/opentake-media/src/encode/mod.rs +++ b/crates/opentake-media/src/encode/mod.rs @@ -134,11 +134,18 @@ struct PendingAudio { impl VideoEncoder { /// Start an encoder writing to `out`. `w`/`h` must already be even. pub fn new(out: &Path, w: u32, h: u32, fps: i32, preset: &ExportPreset) -> Result { - reject_link_output(out)?; - let output = open_output_nofollow(out)?; + let output = Self::open_output_file(out)?; Self::new_with_file(out, output, w, h, fps, preset) } + /// Open and truncate a regular, non-link output file without following a + /// symlink. Callers that need an identity-safe cleanup guard can retain + /// this handle and pass a clone into `new_with_file`. + pub fn open_output_file(out: &Path) -> Result { + reject_link_output(out)?; + open_output_nofollow(out) + } + pub fn new_with_file( out_hint: &Path, mut output: File, diff --git a/crates/opentake-media/src/frame.rs b/crates/opentake-media/src/frame.rs index 9042e45a..02dcb2ec 100644 --- a/crates/opentake-media/src/frame.rs +++ b/crates/opentake-media/src/frame.rs @@ -25,7 +25,7 @@ impl RgbaFrame { /// for tests). pub fn black(width: u32, height: u32) -> Self { let mut rgba = vec![0u8; width as usize * height as usize * 4]; - for px in rgba.chunks_exact_mut(4) { + for px in rgba.as_chunks_mut::<4>().0.iter_mut() { px[3] = 255; } RgbaFrame { @@ -64,7 +64,7 @@ mod tests { assert_eq!(f.rgba.len(), 3 * 2 * 4); assert_eq!(f.pixel_count(), 6); // every pixel is (0,0,0,255) - for px in f.rgba.chunks_exact(4) { + for px in f.rgba.as_chunks::<4>().0.iter() { assert_eq!(px, &[0, 0, 0, 255]); } } diff --git a/crates/opentake-media/src/ort_worker/mod.rs b/crates/opentake-media/src/ort_worker/mod.rs index c4139307..f301de8f 100644 --- a/crates/opentake-media/src/ort_worker/mod.rs +++ b/crates/opentake-media/src/ort_worker/mod.rs @@ -135,6 +135,7 @@ pub enum JobState { #[derive(Clone, Debug, PartialEq, Eq)] pub enum WorkerError { QueueFull, + QueueTimeout, Cancelled, Shutdown, Panicked, @@ -147,6 +148,7 @@ impl std::fmt::Display for WorkerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::QueueFull => f.write_str("inference queue is full"), + Self::QueueTimeout => f.write_str("inference worker is busy"), Self::Cancelled => f.write_str("inference job was cancelled"), Self::Shutdown => f.write_str("inference worker is shut down"), Self::Panicked => f.write_str("inference job panicked"), @@ -326,6 +328,27 @@ where } } } + + /// Bound admission latency for interactive callers. A job that has already + /// started may finish normally; an expired queued job is cancelled while + /// holding the same status lock used by `set_running`, so it cannot start + /// after this method reports a timeout. + pub fn wait_with_queue_timeout(&self, timeout: Duration) -> Result { + let status = self.shared.status.lock().unwrap_or_else(|e| e.into_inner()); + let (status, _) = self + .shared + .changed + .wait_timeout_while(status, timeout, |status| { + status.state == JobState::Queued && status.result.is_none() + }) + .unwrap_or_else(|e| e.into_inner()); + if status.state == JobState::Queued && status.result.is_none() { + self.shared.cancel.cancel(); + return Err(WorkerError::QueueTimeout); + } + drop(status); + self.wait() + } } /// Single-worker model cache. A worker task can lazily install a typed model by diff --git a/crates/opentake-media/src/search/config.rs b/crates/opentake-media/src/search/config.rs index 22893206..2b6b715c 100644 --- a/crates/opentake-media/src/search/config.rs +++ b/crates/opentake-media/src/search/config.rs @@ -1,7 +1,7 @@ //! Search index configuration constants and the model manifest — port of //! `Search/SearchIndexConfig.swift`. The model is the ONNX build of -//! `siglip2-base-patch16-256` (dim 768, image 256, context 64); upstream's -//! CoreML hashes/bytes are replaced when the ONNX assets are hosted (SPEC T8.0). +//! `siglip2-base-patch16-256` (dim 768, image 256, context 64). The +//! ONNX Community FP32 export is pinned by revision, byte size and SHA-256. use crate::search::embedder::EmbedderSpec; use crate::search::model_download::{Manifest, ManifestFile}; @@ -15,25 +15,18 @@ pub const SEARCH_LIMIT: usize = 20; /// SigLIP2 model identity. pub const MODEL_NAME: &str = "siglip2-base-patch16-256"; -pub const MODEL_VERSION: i32 = 1; +// v2 invalidates embeddings made with the old uncalibrated export contract. +pub const MODEL_VERSION: i32 = 2; pub const EMBEDDING_DIM: usize = 768; pub const IMAGE_SIZE: u32 = 256; pub const CONTEXT_LENGTH: usize = 64; -/// Base URL the ONNX model files are fetched from (`{base}/{file}`), mirroring -/// `WhisperModel.base_url`. Placeholder until the ONNX build is hosted (SPEC -/// T8.0): the download command constructs `{base}/image_encoder.onnx` etc., and -/// SHA-256-verifies each against [`manifest`]'s (currently placeholder) hashes, -/// so a real download only succeeds once both this URL and the manifest -/// hashes/bytes are filled in. The Hugging Face `resolve/main` raw-file endpoint -/// is the intended host (same shape as the whisper model URL). +/// Public ONNX Community export of Google's model, pinned to an immutable commit. +/// Provenance and verified hashes: docs/knowledge/2026-09-06-semantic-search-model.md. pub const MODEL_DOWNLOAD_BASE_URL: &str = - "https://huggingface.co/opentake/siglip2-base-patch16-256-onnx/resolve/main"; + "https://huggingface.co/onnx-community/siglip2-base-patch16-256-ONNX/resolve/d1114256522a37ffa257a0a58017348ab0058db2"; -/// The [`EmbedderSpec`] for the configured SigLIP2 model. `normalized` defaults -/// to `false` to match upstream's assumption that the exported model L2- -/// normalizes internally (SPEC §0.8); flip it only if calibration proves the -/// embeddings need external normalization. +/// The separate encoder graphs return unnormalized pooled features. pub fn embedder_spec() -> EmbedderSpec { EmbedderSpec { model: MODEL_NAME.to_string(), @@ -45,9 +38,7 @@ pub fn embedder_spec() -> EmbedderSpec { } } -/// The download manifest for the ONNX model assets. The `sha256`/`bytes` are -/// placeholders until the ONNX build is hosted (SPEC T8.0); they are validated -/// at download time and must be filled before enabling real downloads. +/// FP32 encoder assets (no external-data sidecars) and the matching tokenizer. pub fn manifest() -> Manifest { Manifest { model: MODEL_NAME.to_string(), @@ -56,19 +47,19 @@ pub fn manifest() -> Manifest { image_size: IMAGE_SIZE, context_length: CONTEXT_LENGTH, image_encoder: ManifestFile { - name: "image_encoder.onnx".to_string(), - sha256: String::new(), - bytes: 0, + name: "onnx/vision_model.onnx".to_string(), + sha256: "f5cb16728a704703f05516ded628397e11dbca4de2eb5db04b0c0bcee988aa7a".into(), + bytes: 371_992_072, }, text_encoder: ManifestFile { - name: "text_encoder.onnx".to_string(), - sha256: String::new(), - bytes: 0, + name: "onnx/text_model.onnx".to_string(), + sha256: "d3de4a6bbbfcb429b6615ac496790353cf4a4fc0f19fbbe7179e523ae60daaef".into(), + bytes: 1_129_469_657, }, tokenizer: ManifestFile { - name: "tokenizer.zip".to_string(), - sha256: String::new(), - bytes: 0, + name: "tokenizer.json".to_string(), + sha256: "cb9140fae3ac5122c972d37adf83e1248471a38147ad76f8215c8872c6fd8322".into(), + bytes: 34_363_039, }, } } @@ -104,4 +95,19 @@ mod tests { assert_eq!(m.version, MODEL_VERSION); assert_eq!(m.embedding_dim, EMBEDDING_DIM); } + #[test] + fn public_manifest_has_pinned_revision_and_complete_checksums() { + let revision = MODEL_DOWNLOAD_BASE_URL.rsplit('/').next().unwrap(); + assert_eq!(revision.len(), 40); + assert!(revision.bytes().all(|b| b.is_ascii_hexdigit())); + let m = manifest(); + let files = [&m.image_encoder, &m.text_encoder, &m.tokenizer]; + for file in files { + assert!(file.bytes > 0); + assert_eq!(file.sha256.len(), 64); + assert!(file.sha256.bytes().all(|b| b.is_ascii_hexdigit())); + } + assert!(files.iter().map(|f| f.bytes).sum::() < 2_000_000_000); + assert_eq!(m.spec(), embedder_spec()); + } } diff --git a/crates/opentake-media/src/search/embedder.rs b/crates/opentake-media/src/search/embedder.rs index 926d6b9a..6ff872d0 100644 --- a/crates/opentake-media/src/search/embedder.rs +++ b/crates/opentake-media/src/search/embedder.rs @@ -128,7 +128,7 @@ pub(crate) mod test_support { // Average channel intensities → 4-vec [r,g,b,brightness], normalized. let mut acc = [0.0f64; 4]; let mut n = 0.0f64; - for px in frame.rgba.chunks_exact(4) { + for px in frame.rgba.as_chunks::<4>().0.iter() { acc[0] += px[0] as f64; acc[1] += px[1] as f64; acc[2] += px[2] as f64; diff --git a/crates/opentake-media/src/search/frame_sampler.rs b/crates/opentake-media/src/search/frame_sampler.rs index 2f330834..bce96849 100644 --- a/crates/opentake-media/src/search/frame_sampler.rs +++ b/crates/opentake-media/src/search/frame_sampler.rs @@ -288,7 +288,7 @@ mod tests { // Pure red (255,0,0) → luma 0.299*255 ≈ 76.245. let f = RgbaFrame::new(8, 8, { let mut v = vec![0u8; 8 * 8 * 4]; - for px in v.chunks_exact_mut(4) { + for px in v.as_chunks_mut::<4>().0.iter_mut() { px[0] = 255; px[3] = 255; } diff --git a/crates/opentake-media/src/search/mod.rs b/crates/opentake-media/src/search/mod.rs index 8b6218c0..3ff44218 100644 --- a/crates/opentake-media/src/search/mod.rs +++ b/crates/opentake-media/src/search/mod.rs @@ -52,7 +52,7 @@ mod integration_tests { let bright = RgbaFrame::new(8, 8, vec![240; 8 * 8 * 4]); let dark = RgbaFrame::new(8, 8, { let mut v = vec![0u8; 8 * 8 * 4]; - for px in v.chunks_exact_mut(4) { + for px in v.as_chunks_mut::<4>().0.iter_mut() { px[3] = 255; } v diff --git a/crates/opentake-media/src/search/model_download.rs b/crates/opentake-media/src/search/model_download.rs index 05c057d8..e5b4ec82 100644 --- a/crates/opentake-media/src/search/model_download.rs +++ b/crates/opentake-media/src/search/model_download.rs @@ -69,23 +69,100 @@ pub fn install_dir(models_dir: &Path, m: &Manifest) -> PathBuf { models_dir.join(format!("{}-v{}", m.model, m.version)) } -/// Return the installed model if all three artifacts (and `tokenizer.json`) -/// exist. Port of `installed(for:)` adapted to ONNX filenames. +/// Fast installed-state check. A receipt is written only after all checks pass. +/// Full hashes are rechecked by `verify_installed` before loading a model. pub fn installed(models_dir: &Path, m: &Manifest) -> Option { let dir = install_dir(models_dir, m); + let receipt: Manifest = + serde_json::from_slice(&std::fs::read(dir.join("manifest.json")).ok()?).ok()?; + if receipt != *m { + return None; + } let image = dir.join("image_encoder.onnx"); let text = dir.join("text_encoder.onnx"); let tokenizer = dir.join("tokenizer"); - if image.exists() && text.exists() && tokenizer.join("tokenizer.json").exists() { - Some(InstalledModel { - image_encoder: image, - text_encoder: text, - tokenizer_folder: tokenizer, - spec: m.spec(), - }) - } else { - None + for (path, file) in [(&image, &m.image_encoder), (&text, &m.text_encoder)] { + if !matches_size(path, file) { + return None; + } + } + let token = tokenizer.join("tokenizer.json"); + if !token.is_file() + || (m.tokenizer.name.ends_with(".json") && !matches_size(&token, &m.tokenizer)) + { + return None; + } + Some(InstalledModel { + image_encoder: image, + text_encoder: text, + tokenizer_folder: tokenizer, + spec: m.spec(), + }) +} + +fn matches_size(path: &Path, file: &ManifestFile) -> bool { + file.bytes > 0 + && std::fs::metadata(path) + .is_ok_and(|meta| meta.is_file() && meta.len() == file.bytes as u64) +} + +/// Verify the actual installed bytes before inference, including offline copies. +pub fn verify_installed(models_dir: &Path, m: &Manifest) -> Result { + let model = installed(models_dir, m) + .ok_or_else(|| MediaError::ModelInstall("model missing or incomplete".into()))?; + verify_sha256(&model.image_encoder, &m.image_encoder.sha256)?; + verify_sha256(&model.text_encoder, &m.text_encoder.sha256)?; + if m.tokenizer.name.ends_with(".json") { + verify_sha256( + &model.tokenizer_folder.join("tokenizer.json"), + &m.tokenizer.sha256, + )?; + } + Ok(model) +} + +#[cfg(feature = "model-download")] +fn validate_manifest(m: &Manifest) -> Result<()> { + use std::path::Component; + if m.model.is_empty() + || !m + .model + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') + { + return Err(MediaError::ModelInstall("invalid model name".into())); + } + for f in [&m.image_encoder, &m.text_encoder, &m.tokenizer] { + if f.name.is_empty() + || !Path::new(&f.name) + .components() + .all(|p| matches!(p, Component::Normal(_))) + || f.name.contains('\\') + || f.bytes <= 0 + || f.sha256.len() != 64 + || !f + .sha256 + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + { + return Err(MediaError::ModelInstall(format!( + "invalid manifest entry: {}", + f.name + ))); + } } + Ok(()) +} + +#[cfg(feature = "model-download")] +fn verify_file(path: &Path, file: &ManifestFile) -> Result<()> { + if !matches_size(path, file) { + return Err(MediaError::ModelInstall(format!( + "size mismatch: {} (expected {})", + file.name, file.bytes + ))); + } + verify_sha256(path, &file.sha256) } /// Streaming SHA-256 verification (1 MiB chunks). `Err(Checksum)` on mismatch. @@ -130,8 +207,8 @@ pub fn sha256_hex(bytes: &[u8]) -> String { hex } -/// Download, verify, unzip, and install the model. Requires the `model-download` -/// feature (HTTP + zip). Idempotent: returns immediately if already installed. +/// Download and verify all artifacts before atomically publishing the directory. +/// Temporary files are removed on errors and cancellation. #[cfg(feature = "model-download")] pub async fn install( models_dir: &Path, @@ -140,74 +217,142 @@ pub async fn install( on_progress: impl Fn(f64), ) -> Result { use futures_util::StreamExt; - - if let Some(existing) = installed(models_dir, m) { + validate_manifest(m)?; + if let Ok(existing) = verify_installed(models_dir, m) { + on_progress(1.0); return Ok(existing); } - - let staging = std::env::temp_dir().join(format!("opentake-model-{}", uuid_like())); - std::fs::create_dir_all(&staging)?; - + std::fs::create_dir_all(models_dir)?; + let staging = tempfile::Builder::new() + .prefix(".search-download-") + .tempdir_in(models_dir)?; let files = [&m.image_encoder, &m.text_encoder, &m.tokenizer]; - let total_bytes: i64 = files.iter().map(|f| f.bytes).sum(); - let mut done_bytes: i64 = 0; - let client = reqwest::Client::new(); - + let total_bytes: f64 = files.iter().map(|f| f.bytes as f64).sum(); + let mut done_bytes = 0.0; + let client = reqwest::Client::builder() + .connect_timeout(std::time::Duration::from_secs(30)) + .timeout(std::time::Duration::from_secs(3600)) + .build() + .map_err(|e| MediaError::ModelInstall(format!("HTTP client: {e}")))?; for file in files { let url = format!("{}/{}", base_url.trim_end_matches('/'), file.name); let resp = client .get(&url) .send() .await - .map_err(|e| MediaError::ModelInstall(format!("GET {url}: {e}")))?; + .map_err(|e| MediaError::ModelInstall(format!("GET {}: {e}", file.name)))?; if !resp.status().is_success() { return Err(MediaError::ModelInstall(format!( - "GET {url} -> {}", + "GET {} -> {}", + file.name, resp.status() ))); } - let dest = staging.join(&file.name); + let dest = staging.path().join(&file.name); + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } let mut out = std::fs::File::create(&dest)?; let mut stream = resp.bytes_stream(); - let base = done_bytes as f64; - let mut file_done: i64 = 0; + let mut file_done = 0u64; while let Some(chunk) = stream.next().await { - let chunk = chunk.map_err(|e| MediaError::ModelInstall(format!("stream: {e}")))?; + let chunk = chunk + .map_err(|e| MediaError::ModelInstall(format!("stream {}: {e}", file.name)))?; + file_done += chunk.len() as u64; + if file_done > file.bytes as u64 { + return Err(MediaError::ModelInstall(format!( + "size exceeded: {}", + file.name + ))); + } use std::io::Write; out.write_all(&chunk)?; - file_done += chunk.len() as i64; - if total_bytes > 0 { - on_progress((base + file_done as f64) / total_bytes as f64); - } + on_progress(((done_bytes + file_done as f64) / total_bytes).min(0.99)); } + out.sync_all()?; drop(out); - verify_sha256(&dest, &file.sha256)?; - done_bytes += file.bytes; - if total_bytes > 0 { - on_progress(done_bytes as f64 / total_bytes as f64); - } + verify_file(&dest, file)?; + done_bytes += file.bytes as f64; } + let model = publish_staging(models_dir, m, staging.path())?; + on_progress(1.0); + Ok(model) +} - // Encoders are plain .onnx; the tokenizer ships as a zip with one top-level - // folder. Unzip it. - let tokenizer_zip = staging.join(&m.tokenizer.name); - let tokenizer_extracted = unzip_single_top_level(&tokenizer_zip, &staging)?; +/// Offline installation from the same repository-relative files as the download. +/// Does not contact the network. Checks source hashes before copying, then checks +/// the staged copies; an existing installation is untouched on verification errors. +#[cfg(feature = "model-download")] +pub fn install_from_directory( + models_dir: &Path, + m: &Manifest, + source: &Path, +) -> Result { + validate_manifest(m)?; + let files = [&m.image_encoder, &m.text_encoder, &m.tokenizer]; + for file in files { + verify_file(&source.join(&file.name), file)?; + } + if let Ok(existing) = verify_installed(models_dir, m) { + return Ok(existing); + } + std::fs::create_dir_all(models_dir)?; + let staging = tempfile::Builder::new() + .prefix(".search-offline-") + .tempdir_in(models_dir)?; + for file in files { + let dest = staging.path().join(&file.name); + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::copy(source.join(&file.name), &dest)?; + verify_file(&dest, file)?; + } + publish_staging(models_dir, m, staging.path()) +} - let dir = install_dir(models_dir, m); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir)?; +#[cfg(feature = "model-download")] +fn publish_staging(models_dir: &Path, m: &Manifest, staging: &Path) -> Result { + let prepared = staging.join("installed"); + std::fs::create_dir(&prepared)?; std::fs::rename( staging.join(&m.image_encoder.name), - dir.join("image_encoder.onnx"), + prepared.join("image_encoder.onnx"), )?; std::fs::rename( staging.join(&m.text_encoder.name), - dir.join("text_encoder.onnx"), + prepared.join("text_encoder.onnx"), )?; - std::fs::rename(tokenizer_extracted, dir.join("tokenizer"))?; - std::fs::write(dir.join("spec.json"), serde_json::to_vec(&m.spec())?)?; - - let _ = std::fs::remove_dir_all(&staging); + if m.tokenizer.name.ends_with(".json") { + std::fs::create_dir(prepared.join("tokenizer"))?; + std::fs::rename( + staging.join(&m.tokenizer.name), + prepared.join("tokenizer/tokenizer.json"), + )?; + } else { + let extracted = unzip_single_top_level(&staging.join(&m.tokenizer.name), staging)?; + if !extracted.join("tokenizer.json").is_file() { + return Err(MediaError::ModelInstall( + "tokenizer.json missing from archive".into(), + )); + } + std::fs::rename(extracted, prepared.join("tokenizer"))?; + } + std::fs::write(prepared.join("spec.json"), serde_json::to_vec(&m.spec())?)?; + std::fs::write(prepared.join("manifest.json"), serde_json::to_vec(m)?)?; + let dir = install_dir(models_dir, m); + // Preserve a previous (incomplete/corrupt) directory until replacement succeeds. + let backup = staging.join("previous"); + let previous = dir.exists(); + if previous { + std::fs::rename(&dir, &backup)?; + } + if let Err(error) = std::fs::rename(&prepared, &dir) { + if previous { + std::fs::rename(&backup, &dir)?; + } + return Err(error.into()); + } installed(models_dir, m).ok_or_else(|| MediaError::ModelInstall("post-install missing".into())) } @@ -253,17 +398,6 @@ fn unzip_single_top_level(zip_path: &Path, into: &Path) -> Result { Ok(out_root.join(top_levels.into_iter().next().unwrap())) } -/// Tiny unique suffix without pulling a uuid dependency. -#[cfg(feature = "model-download")] -fn uuid_like() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0); - format!("{nanos:x}") -} - #[cfg(test)] mod tests { use super::*; @@ -309,9 +443,12 @@ mod tests { #[test] fn installed_detected_when_all_artifacts_present() { let dir = tempfile::tempdir().unwrap(); - let m = manifest(); + let mut m = manifest(); + m.image_encoder.bytes = 1; + m.text_encoder.bytes = 1; let id = install_dir(dir.path(), &m); std::fs::create_dir_all(id.join("tokenizer")).unwrap(); + std::fs::write(id.join("manifest.json"), serde_json::to_vec(&m).unwrap()).unwrap(); std::fs::write(id.join("image_encoder.onnx"), b"i").unwrap(); std::fs::write(id.join("text_encoder.onnx"), b"t").unwrap(); std::fs::write(id.join("tokenizer/tokenizer.json"), b"{}").unwrap(); @@ -359,4 +496,174 @@ mod tests { let back: Manifest = serde_json::from_str(&json).unwrap(); assert_eq!(m, back); } + #[test] + fn installed_rejects_unverified_loose_files() { + let root = tempfile::tempdir().unwrap(); + let m = manifest(); + let dir = install_dir(root.path(), &m); + std::fs::create_dir_all(dir.join("tokenizer")).unwrap(); + std::fs::write(dir.join("image_encoder.onnx"), b"i").unwrap(); + std::fs::write(dir.join("text_encoder.onnx"), b"t").unwrap(); + std::fs::write(dir.join("tokenizer/tokenizer.json"), b"{}").unwrap(); + assert!(installed(root.path(), &m).is_none()); + } + + #[cfg(feature = "model-download")] + #[tokio::test] + async fn downloads_nested_onnx_and_raw_tokenizer() { + use std::io::Read; + let server = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let url = format!("http://{}", server.local_addr().unwrap()); + let mut m = manifest(); + for (f, name, bytes) in [ + ( + &mut m.image_encoder, + "onnx/vision_model.onnx", + b"image".as_slice(), + ), + ( + &mut m.text_encoder, + "onnx/text_model.onnx", + b"text".as_slice(), + ), + (&mut m.tokenizer, "tokenizer.json", b"{}".as_slice()), + ] { + f.name = name.into(); + f.sha256 = sha256_hex(bytes); + f.bytes = bytes.len() as i64; + } + let serving = std::thread::spawn(move || { + for (path, body) in [ + ("onnx/vision_model.onnx", "image"), + ("onnx/text_model.onnx", "text"), + ("tokenizer.json", "{}"), + ] { + let (mut stream, _) = server.accept().unwrap(); + let mut request = [0; 4096]; + let n = stream.read(&mut request).unwrap(); + assert!( + String::from_utf8_lossy(&request[..n]).starts_with(&format!("GET /{path} ")) + ); + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .unwrap(); + } + }); + let root = tempfile::tempdir().unwrap(); + let result = install(root.path(), &m, &url, |_| {}).await; + // If the downloader fails early, do not block waiting for unused requests. + assert!(result.is_ok(), "{result:?}"); + serving.join().unwrap(); + let got = result.unwrap(); + assert_eq!( + std::fs::read(got.tokenizer_folder.join("tokenizer.json")).unwrap(), + b"{}" + ); + assert_eq!( + install(root.path(), &m, "http://127.0.0.1:1", |_| {}) + .await + .unwrap(), + got + ); + } + + #[cfg(feature = "model-download")] + fn offline_fixture(source: &Path) -> Manifest { + let mut m = manifest(); + for (file, name, bytes) in [ + ( + &mut m.image_encoder, + "onnx/vision_model.onnx", + b"image".as_slice(), + ), + ( + &mut m.text_encoder, + "onnx/text_model.onnx", + b"text".as_slice(), + ), + (&mut m.tokenizer, "tokenizer.json", b"{}".as_slice()), + ] { + file.name = name.into(); + file.sha256 = sha256_hex(bytes); + file.bytes = bytes.len() as i64; + let path = source.join(name); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, bytes).unwrap(); + } + m + } + + #[cfg(feature = "model-download")] + #[test] + fn offline_install_verifies_bytes_and_preserves_existing_on_failure() { + let source = tempfile::tempdir().unwrap(); + let models = tempfile::tempdir().unwrap(); + let m = offline_fixture(source.path()); + let got = install_from_directory(models.path(), &m, source.path()).unwrap(); + assert_eq!(verify_installed(models.path(), &m).unwrap(), got); + std::fs::write(source.path().join(&m.image_encoder.name), b"wrong").unwrap(); + assert!(matches!( + install_from_directory(models.path(), &m, source.path()), + Err(MediaError::Checksum(_)) + )); + assert!(verify_installed(models.path(), &m).is_ok()); + std::fs::write(&got.image_encoder, b"wrong").unwrap(); + assert!(verify_installed(models.path(), &m).is_err()); + std::fs::write(&got.image_encoder, b"i").unwrap(); + assert!(installed(models.path(), &m).is_none()); + } + + #[cfg(feature = "model-download")] + #[test] + fn offline_install_rejects_bad_size_hash_and_traversal() { + let source = tempfile::tempdir().unwrap(); + let models = tempfile::tempdir().unwrap(); + let m = offline_fixture(source.path()); + for mutation in 0..3 { + let mut bad = m.clone(); + match mutation { + 0 => bad.image_encoder.bytes += 1, + 1 => bad.image_encoder.sha256.clear(), + _ => bad.image_encoder.name = "../outside.onnx".into(), + } + assert!(install_from_directory(models.path(), &bad, source.path()).is_err()); + } + assert_eq!(std::fs::read_dir(models.path()).unwrap().count(), 0); + } + + #[cfg(feature = "model-download")] + #[tokio::test] + async fn http_failure_cleans_staging_and_does_not_install() { + use std::io::Read; + for (status, body, expected_size) in [ + ("404 Not Found", "", 5), + ("200 OK", "bad", 5), + ("200 OK", "wrong", 5), + ("200 OK", "toolong", 5), + ] { + let server = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let url = format!("http://{}", server.local_addr().unwrap()); + let serving = std::thread::spawn(move || { + let (mut stream, _) = server.accept().unwrap(); + let mut request = [0; 4096]; + let _ = stream.read(&mut request).unwrap(); + write!( + stream, + "HTTP/1.1 {status}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .unwrap(); + }); + let source = tempfile::tempdir().unwrap(); + let models = tempfile::tempdir().unwrap(); + let mut m = offline_fixture(source.path()); + m.image_encoder.bytes = expected_size; + assert!(install(models.path(), &m, &url, |_| {}).await.is_err()); + serving.join().unwrap(); + assert_eq!(std::fs::read_dir(models.path()).unwrap().count(), 0); + } + } } diff --git a/crates/opentake-media/src/search/ort_embedder.rs b/crates/opentake-media/src/search/ort_embedder.rs index 6ae1859e..589e3901 100644 --- a/crates/opentake-media/src/search/ort_embedder.rs +++ b/crates/opentake-media/src/search/ort_embedder.rs @@ -1,4 +1,4 @@ -//! ONNX Runtime SigLIP2 embedder (feature `ort-backend`). Real implementation of +//! SigLIP2 embedder using native ORT or fixed-shape tract on Windows. Implements //! the [`Embedder`] trait; the default build and tests use the mock instead. //! //! Image input is `NCHW` f32 `(1,3,256,256)` (mean/std from @@ -6,16 +6,20 @@ //! right-padded with 0. Output is a `(1, embedding_dim)` f32 vector; we assert //! the length matches the spec, mirroring upstream `vector(from:dim:)`. //! -//! IO tensor names default to the SigLIP CoreML names (`image`/`tokens` → -//! `embedding`); ONNX exports often use `pixel_values`/`input_ids` → -//! `image_embeds`/`text_embeds`. `IoNames` makes them configurable. +//! IO defaults match the pinned ONNX Community graphs: `pixel_values` / +//! `input_ids` → `pooler_output`. Features are L2-normalized for cosine ranking. +//! Windows binds the known input shapes in memory; downloaded assets stay intact. use std::path::Path; use std::sync::Mutex; -use ndarray::{Array2, Array4}; -use ort::session::Session; -use ort::value::Tensor; +#[cfg(not(target_os = "windows"))] +use ndarray::Array2; +use ndarray::Array4; +#[cfg(not(target_os = "windows"))] +use ort::{session::Session, value::Tensor}; +#[cfg(target_os = "windows")] +use tract_backend::Encoder as Session; use super::embedder::{ l2_normalize, preprocess_image, Embedder, EmbedderSpec, SIGLIP_MEAN, SIGLIP_STD, @@ -35,12 +39,12 @@ pub struct IoNames { impl Default for IoNames { fn default() -> Self { - // SigLIP CoreML names (upstream `VisualEmbedder`). + // ONNX Community separate encoder graphs expose pooled, unnormalized features. IoNames { - image_input: "image".into(), - image_output: "embedding".into(), - text_input: "tokens".into(), - text_output: "embedding".into(), + image_input: "pixel_values".into(), + image_output: "pooler_output".into(), + text_input: "input_ids".into(), + text_output: "pooler_output".into(), } } } @@ -52,6 +56,7 @@ pub struct OrtEmbedder { text: Mutex, tokenizer: SiglipTokenizer, spec: EmbedderSpec, + #[cfg(not(target_os = "windows"))] io: IoNames, } @@ -80,34 +85,59 @@ impl OrtEmbedder { spec: EmbedderSpec, io: IoNames, ) -> Result { + #[cfg(not(target_os = "windows"))] let image = build_session(image_encoder)?; + #[cfg(not(target_os = "windows"))] let text = build_session(text_encoder)?; + #[cfg(target_os = "windows")] + let image = Session::load( + image_encoder, + &io.image_input, + &io.image_output, + tract_onnx::prelude::DatumType::F32, + &[1, 3, spec.image_size as usize, spec.image_size as usize], + spec.embedding_dim, + )?; + #[cfg(target_os = "windows")] + let text = Session::load( + text_encoder, + &io.text_input, + &io.text_output, + tract_onnx::prelude::DatumType::I64, + &[1, spec.context_length], + spec.embedding_dim, + )?; let tokenizer = SiglipTokenizer::from_file(tokenizer_json, spec.context_length)?; Ok(OrtEmbedder { image: Mutex::new(image), text: Mutex::new(text), tokenizer, spec, + #[cfg(not(target_os = "windows"))] io, }) } - fn finalize(&self, mut v: Vec) -> Result> { - if v.len() != self.spec.embedding_dim { - return Err(MediaError::BadModelOutput); - } - if self.spec.normalized { - // Model already normalizes — leave as-is. - } else { - // Upstream default assumes in-graph normalization; only normalize - // when the spec explicitly requests external normalization (kept - // here for the calibration path, SPEC §0.8). Currently a no-op. - let _ = &mut v; - } - Ok(v) + fn finalize(&self, v: Vec) -> Result> { + finalize_embedding(v, &self.spec) + } +} + +fn finalize_embedding(mut v: Vec, spec: &EmbedderSpec) -> Result> { + if v.len() != spec.embedding_dim || v.iter().any(|x| !x.is_finite()) { + return Err(MediaError::BadModelOutput); + } + let norm_squared = v.iter().map(|x| x * x).sum::(); + if !norm_squared.is_finite() || norm_squared <= f32::EPSILON { + return Err(MediaError::BadModelOutput); } + if !spec.normalized { + l2_normalize(&mut v); + } + Ok(v) } +#[cfg(not(target_os = "windows"))] fn build_session(path: &Path) -> Result { crate::initialize_ort_backend(); let builder = Session::builder().map_err(|e| MediaError::ModelInstall(format!("ort: {e}")))?; @@ -124,6 +154,7 @@ fn build_session(path: &Path) -> Result { .map_err(|e| MediaError::ModelInstall(format!("ort load {}: {e}", path.display()))) } +#[cfg(not(target_os = "windows"))] fn extract_vec(value: &ort::value::Value) -> Result> { let (_, data) = value .try_extract_tensor::() @@ -139,42 +170,249 @@ impl Embedder for OrtEmbedder { fn encode_image(&self, frame: &RgbaFrame) -> Result> { let tensor: Array4 = preprocess_image(frame, self.spec.image_size, SIGLIP_MEAN, SIGLIP_STD); - let input = Tensor::from_array(tensor) - .map_err(|e| MediaError::Decode(format!("ort tensor: {e}")))?; - let mut session = self.image.lock().unwrap(); - let outputs = session - .run(ort::inputs![self.io.image_input.as_str() => input]) - .map_err(|e| MediaError::Decode(format!("ort run image: {e}")))?; - let value = outputs - .get(self.io.image_output.as_str()) - .ok_or(MediaError::BadModelOutput)?; - let v = extract_vec(value)?; + #[cfg(not(target_os = "windows"))] + let v = { + let input = Tensor::from_array(tensor) + .map_err(|e| MediaError::Decode(format!("ort tensor: {e}")))?; + let mut session = self.image.lock().unwrap(); + let outputs = session + .run(ort::inputs![self.io.image_input.as_str() => input]) + .map_err(|e| MediaError::Decode(format!("ort run image: {e}")))?; + let value = outputs + .get(self.io.image_output.as_str()) + .ok_or(MediaError::BadModelOutput)?; + extract_vec(value)? + }; + #[cfg(target_os = "windows")] + let v = self + .image + .lock() + .map_err(|_| MediaError::ModelInstall("image session poisoned".into()))? + .run( + tensor.shape(), + tensor.as_slice().ok_or(MediaError::BadModelOutput)?, + )?; self.finalize(v) } fn encode_text(&self, text: &str) -> Result> { let ids = self.tokenizer.tokenize(text)?; - let arr = Array2::from_shape_vec((1, ids.len()), ids) - .map_err(|e| MediaError::Decode(format!("ort text shape: {e}")))?; - let input = - Tensor::from_array(arr).map_err(|e| MediaError::Decode(format!("ort tensor: {e}")))?; - let mut session = self.text.lock().unwrap(); - let outputs = session - .run(ort::inputs![self.io.text_input.as_str() => input]) - .map_err(|e| MediaError::Decode(format!("ort run text: {e}")))?; - let value = outputs - .get(self.io.text_output.as_str()) - .ok_or(MediaError::BadModelOutput)?; - let v = extract_vec(value)?; + #[cfg(not(target_os = "windows"))] + let v = { + let arr = Array2::from_shape_vec((1, ids.len()), ids) + .map_err(|e| MediaError::Decode(format!("ort text shape: {e}")))?; + let input = Tensor::from_array(arr) + .map_err(|e| MediaError::Decode(format!("ort tensor: {e}")))?; + let mut session = self.text.lock().unwrap(); + let outputs = session + .run(ort::inputs![self.io.text_input.as_str() => input]) + .map_err(|e| MediaError::Decode(format!("ort run text: {e}")))?; + let value = outputs + .get(self.io.text_output.as_str()) + .ok_or(MediaError::BadModelOutput)?; + extract_vec(value)? + }; + #[cfg(target_os = "windows")] + let v = self + .text + .lock() + .map_err(|_| MediaError::ModelInstall("text session poisoned".into()))? + .run(&[1, ids.len()], &ids)?; self.finalize(v) } } -// Keep `l2_normalize` referenced for the calibration path even while the default -// (in-graph-normalized) configuration does not call it. -#[allow(dead_code)] -fn _normalize_for_calibration(v: &mut [f32]) { - l2_normalize(v); +// The ort-tract adapter cannot bind input dimensions and rejects ORT thread +// options. Use its same pinned tract engine directly for these fixed encoders. +#[cfg(target_os = "windows")] +mod tract_backend { + use super::*; + use tract_onnx::prelude::*; + use tract_onnx::tract_hir::infer::{Factoid, ShapeFactoid}; + + pub(super) struct Encoder { + plan: TypedRunnableModel, + shape: Vec, + datum_type: DatumType, + embedding_dim: usize, + } + + impl Encoder { + pub(super) fn load( + path: &Path, + input_name: &str, + output_name: &str, + datum_type: DatumType, + shape: &[usize], + embedding_dim: usize, + ) -> Result { + let result = (|| -> TractResult { + let mut model = tract_onnx::onnx().model_for_path(path)?; + bind_input(&mut model, input_name, datum_type, shape)?; + let model = model.with_output_names([output_name])?.into_optimized()?; + let output = model.output_fact(0)?; + anyhow::ensure!( + output.datum_type == DatumType::F32 + && output.shape.as_concrete() == Some(&[1, embedding_dim]), + "expected pooled float32 output [1, {embedding_dim}], got {output:?}" + ); + Ok(Self { + plan: model.into_runnable()?, + shape: shape.to_vec(), + datum_type, + embedding_dim, + }) + })(); + result.map_err(|e| { + MediaError::ModelInstall(format!("tract load {}: {e:#}", path.display())) + }) + } + + pub(super) fn run(&self, shape: &[usize], data: &[T]) -> Result> { + if shape != self.shape || T::datum_type() != self.datum_type { + return Err(MediaError::BadModelOutput); + } + let input = tract_onnx::prelude::Tensor::from_shape(shape, data) + .map_err(|e| MediaError::Decode(format!("tract input: {e:#}")))?; + let output = self + .plan + .run(tvec!(input.into())) + .map_err(|e| MediaError::Decode(format!("tract run: {e:#}")))?; + if output.len() != 1 || output[0].shape() != [1, self.embedding_dim] { + return Err(MediaError::BadModelOutput); + } + output[0] + .as_slice::() + .map(|v| v.to_vec()) + .map_err(|_| MediaError::BadModelOutput) + } + } + + fn bind_input( + model: &mut InferenceModel, + input_name: &str, + datum_type: DatumType, + shape: &[usize], + ) -> TractResult<()> { + let inputs = model.input_outlets()?; + anyhow::ensure!( + inputs.len() == 1 && model.node(inputs[0].node).name == input_name, + "expected a single input named {input_name}" + ); + let input = model.input_fact(0)?; + anyhow::ensure!( + input.datum_type.concretize() == Some(datum_type), + "unexpected input type" + ); + anyhow::ensure!( + input.shape.rank().concretize() == Some(shape.len() as i64), + "unexpected input rank" + ); + for (dimension, expected) in input.shape.dims().zip(shape) { + if let Some(dimension) = dimension.concretize().and_then(|d| d.to_i64().ok()) { + anyhow::ensure!( + dimension == *expected as i64, + "input dimension conflicts with model spec" + ); + } + } + // The exporter includes symbolic expressions in value_info. Tract 0.22 + // parses their division precedence incorrectly. Re-infer only dynamic + // dimensions from the fixed input; retain dtype, rank and static checks. + for node in model.nodes_mut() { + for output in &mut node.outputs { + let dims = output + .fact + .shape + .dims() + .map(|dimension| { + if dimension.concretize().is_some_and(|d| d.to_i64().is_ok()) { + dimension.clone() + } else { + Default::default() + } + }) + .collect(); + output.fact.shape = if output.fact.shape.is_open() { + ShapeFactoid::open(dims) + } else { + ShapeFactoid::closed(dims) + }; + } + } + model.set_input_fact(0, TypedFact::dt_shape(datum_type, shape).into())?; + Ok(()) + } + + #[cfg(test)] + mod tests { + use super::*; + + fn image_model() -> InferenceModel { + let mut model = InferenceModel::default(); + let batch = model.symbols.sym("batch_size"); + let shape: [TDim; 4] = [batch.into(), 3.into(), 256.into(), 256.into()]; + model + .add_source("pixel_values", f32::fact(shape).into()) + .unwrap(); + model + } + + #[test] + fn binds_dynamic_input_without_relaxing_static_dimensions() { + let mut model = image_model(); + bind_input( + &mut model, + "pixel_values", + DatumType::F32, + &[1, 3, 256, 256], + ) + .unwrap(); + assert_eq!( + model + .input_fact(0) + .unwrap() + .shape + .as_concrete_finite() + .unwrap() + .unwrap() + .as_slice(), + &[1, 3, 256, 256] + ); + assert!(bind_input( + &mut image_model(), + "pixel_values", + DatumType::F32, + &[1, 3, 128, 128] + ) + .is_err()); + } + + #[test] + fn rejects_wrong_input_name_type_and_rank() { + assert!(bind_input( + &mut image_model(), + "wrong", + DatumType::F32, + &[1, 3, 256, 256] + ) + .is_err()); + assert!(bind_input( + &mut image_model(), + "pixel_values", + DatumType::I64, + &[1, 3, 256, 256] + ) + .is_err()); + assert!(bind_input( + &mut image_model(), + "pixel_values", + DatumType::F32, + &[1, 768] + ) + .is_err()); + } + } } #[cfg(test)] @@ -182,10 +420,96 @@ mod tests { use super::*; #[test] - fn default_io_names_match_upstream_coreml() { + fn default_io_names_match_pinned_onnx() { let io = IoNames::default(); - assert_eq!(io.image_input, "image"); - assert_eq!(io.text_input, "tokens"); - assert_eq!(io.image_output, "embedding"); + assert_eq!(io.image_input, "pixel_values"); + assert_eq!(io.text_input, "input_ids"); + assert_eq!(io.image_output, "pooler_output"); + } + #[test] + fn pooled_features_are_normalized_and_invalid_outputs_rejected() { + let mut spec = crate::search::config::embedder_spec(); + spec.embedding_dim = 2; + let v = finalize_embedding(vec![3.0, 4.0], &spec).unwrap(); + assert!((v[0] - 0.6).abs() < 1e-6); + assert!((v[1] - 0.8).abs() < 1e-6); + for invalid in [vec![f32::NAN, 1.0], vec![0.0, 0.0], vec![1.0]] { + assert!(finalize_embedding(invalid, &spec).is_err()); + } + } + + /// Opt-in real-asset check; never downloads as part of the default suite. + /// The directory contains the three pinned repository-relative files plus + /// cats.png and parrots.png from the audit's public fixtures. + #[cfg(feature = "model-download")] + #[test] + #[ignore = "requires OPENTAKE_SEARCH_MODEL_TEST_DIR with verified public model assets and two images"] + fn real_model_offline_install_embeddings_and_ranking() { + use crate::search::{config, embed_store, model_download, ranker}; + let source = std::path::PathBuf::from( + std::env::var("OPENTAKE_SEARCH_MODEL_TEST_DIR").expect("model test directory"), + ); + let models = tempfile::tempdir().unwrap(); + let manifest = config::manifest(); + let installed = + model_download::install_from_directory(models.path(), &manifest, &source).unwrap(); + let verified = model_download::verify_installed(models.path(), &manifest).unwrap(); + assert_eq!(installed, verified); + let embedder = OrtEmbedder::new( + &installed.image_encoder, + &installed.text_encoder, + &installed.tokenizer_folder.join("tokenizer.json"), + installed.spec, + ) + .unwrap(); + let check_vector = |v: &[f32]| { + assert_eq!(v.len(), 768); + assert!(v.iter().all(|x| x.is_finite())); + assert!((v.iter().map(|x| x * x).sum::() - 1.0).abs() < 1e-4); + }; + let mut indexes = Vec::new(); + for name in ["cats", "parrots"] { + let image = image::open(source.join(format!("{name}.png"))) + .unwrap() + .to_rgba8(); + let frame = RgbaFrame::new(image.width(), image.height(), image.into_raw()); + let vector = embedder.encode_image(&frame).unwrap(); + check_vector(&vector); + let header = embed_store::Header { + model: manifest.model.clone(), + model_version: manifest.version, + sampler_version: 1, + dim: 768, + count: 1, + }; + let rows = [embed_store::Row { + time: 0.0, + shot_start: 0.0, + shot_end: 0.0, + }]; + // Exercise the same f16 representation used by persisted search indexes. + let index = embed_store::decode(&embed_store::encode(&header, &rows, &vector).unwrap()) + .unwrap(); + indexes.push((name.to_owned(), index)); + } + for (query, expected) in [ + ("a photo of two cats on a couch", Some("cats")), + ("a photo of colorful parrots", Some("parrots")), + ("沙发上的两只猫", Some("cats")), + ("彩色鹦鹉", Some("parrots")), + ("a photo of an airplane", None), + ] { + let vector = embedder.encode_text(query).unwrap(); + check_vector(&vector); + let hits = ranker::search( + &vector, + &indexes, + config::SEARCH_LIMIT, + config::RELATIVE_CUTOFF, + Some(config::VISUAL_MATCH_COSINE_FLOOR), + ); + println!("query={query:?} hits={hits:?}"); + assert_eq!(hits.first().map(|h| h.asset_id.as_str()), expected); + } } } diff --git a/crates/opentake-media/src/search/tokenizer.rs b/crates/opentake-media/src/search/tokenizer.rs index 2162cf74..0f78a8dc 100644 --- a/crates/opentake-media/src/search/tokenizer.rs +++ b/crates/opentake-media/src/search/tokenizer.rs @@ -1,14 +1,12 @@ //! SigLIP text tokenizer wrapper. Port of `Search/Models/TextTokenizer.swift`. //! //! SigLIP was trained on `max_length`-padded sequences with **no attention -//! mask**, so we must reproduce the Python reference exactly: encode, truncate to -//! `context_length`, then right-pad with `pad_token = 0` to the fixed length. We -//! disable the tokenizer's own padding/truncation and do it manually so the -//! behavior is deterministic and unit-testable. +//! mask**. Truncate before special-token processing so EOS survives long +//! queries, then right-pad with `pad_token = 0` to the fixed context length. use std::path::Path; -use tokenizers::Tokenizer; +use tokenizers::{Tokenizer, TruncationParams}; use crate::error::{MediaError, Result}; @@ -26,9 +24,18 @@ impl SiglipTokenizer { pub fn from_file(path: &Path, context_length: usize) -> Result { let mut inner = Tokenizer::from_file(path) .map_err(|e| MediaError::ModelInstall(format!("tokenizer load: {e}")))?; - // We pad/truncate manually; turn off the tokenizer's own behavior. + if context_length == 0 { + return Err(MediaError::ModelInstall( + "tokenizer context length must be positive".into(), + )); + } inner.with_padding(None); - let _ = inner.with_truncation(None); + inner + .with_truncation(Some(TruncationParams { + max_length: context_length, + ..Default::default() + })) + .map_err(|e| MediaError::ModelInstall(format!("tokenizer truncation: {e}")))?; Ok(SiglipTokenizer { inner, context_length, @@ -41,7 +48,7 @@ impl SiglipTokenizer { /// Tokenize `text`: encode (with special tokens), truncate to /// `context_length`, right-pad with `PAD_TOKEN`. Output length is always - /// exactly `context_length`. Verbatim port of `TextTokenizer.tokenize`. + /// exactly `context_length`; truncation preserves the model's EOS token. pub fn tokenize(&self, text: &str) -> Result> { let encoding = self .inner @@ -97,4 +104,35 @@ mod tests { fn pad_token_is_zero() { assert_eq!(PAD_TOKEN, 0); } + #[test] + fn truncation_preserves_eos_in_the_last_context_slot() { + let mut inner = Tokenizer::new( + tokenizers::models::wordlevel::WordLevel::builder() + .vocab( + [("".into(), 0), ("cat".into(), 2), ("".into(), 1)] + .into_iter() + .collect(), + ) + .unk_token("".into()) + .build() + .unwrap(), + ); + inner.with_pre_tokenizer(Some(tokenizers::pre_tokenizers::whitespace::Whitespace {})); + inner.with_post_processor(Some( + tokenizers::processors::template::TemplateProcessing::builder() + .try_single("$A ") + .unwrap() + .special_tokens(vec![("", 1)]) + .build() + .unwrap(), + )); + let file = tempfile::NamedTempFile::new().unwrap(); + inner.save(file.path(), false).unwrap(); + let tokenizer = SiglipTokenizer::from_file(file.path(), 4).unwrap(); + assert_eq!( + tokenizer.tokenize("cat cat cat cat cat").unwrap(), + vec![2, 2, 2, 1] + ); + assert_eq!(tokenizer.tokenize("cat").unwrap(), vec![2, 1, 0, 0]); + } } diff --git a/crates/opentake-media/src/thumbnail/sprite.rs b/crates/opentake-media/src/thumbnail/sprite.rs index dbe179f8..f5a5e274 100644 --- a/crates/opentake-media/src/thumbnail/sprite.rs +++ b/crates/opentake-media/src/thumbnail/sprite.rs @@ -206,7 +206,7 @@ mod tests { fn thumb(t: f64, w: u32, h: u32, fill: u8) -> VideoThumb { let mut rgba = vec![0u8; (w * h * 4) as usize]; - for px in rgba.chunks_exact_mut(4) { + for px in rgba.as_chunks_mut::<4>().0.iter_mut() { px[0] = fill; px[1] = fill; px[2] = fill; @@ -346,7 +346,7 @@ mod tests { fn avg(f: &RgbaFrame) -> f64 { let mut sum = 0.0f64; let mut n = 0.0f64; - for px in f.rgba.chunks_exact(4) { + for px in f.rgba.as_chunks::<4>().0.iter() { sum += (px[0] as f64 + px[1] as f64 + px[2] as f64) / 3.0; n += 1.0; } diff --git a/crates/opentake-media/tests/facade_contract.rs b/crates/opentake-media/tests/facade_contract.rs index 7fd1876f..5061e70d 100644 --- a/crates/opentake-media/tests/facade_contract.rs +++ b/crates/opentake-media/tests/facade_contract.rs @@ -134,7 +134,12 @@ fn all_services_are_reachable_only_through_facade_and_dependencies_stay_acyclic( }; let pcm = engine.extract_pcm(&source, &pcm_spec, None).unwrap(); assert_eq!(pcm.spec, pcm_spec); - assert!((15_000..=16_500).contains(&pcm.samples_f32.len())); + assert!( + (15_000..=16_500).contains(&pcm.samples_f32.len()), + "expected ~1s of 16k mono, got {} samples ({:.6}s)", + pcm.samples_f32.len(), + pcm.duration_secs() + ); let transcript_cache = opentake_media::TranscriptCache::new(temp.path().join("cache")); let transcript = engine diff --git a/crates/opentake-media/tests/ffmpeg_integration.rs b/crates/opentake-media/tests/ffmpeg_integration.rs index 72ea035b..390cc021 100644 --- a/crates/opentake-media/tests/ffmpeg_integration.rs +++ b/crates/opentake-media/tests/ffmpeg_integration.rs @@ -11,7 +11,7 @@ use std::path::Path; use std::process::Command; use opentake_media::decode::spawn_video_stream; -use opentake_media::ffmpeg_status::{ffmpeg_available, ffprobe_available}; +use opentake_media::ffmpeg_status::{ffmpeg_available, ffmpeg_path, ffprobe_available}; use opentake_media::{ decode_frame_at, encode, extract_pcm, probe, video_thumbnails, waveform, ExportPreset, ExportResolution, FrameRequest, PcmFormat, PcmSpec, RgbaFrame, VideoCodec, VideoEncoder, @@ -157,6 +157,64 @@ fn extract_pcm_yields_16k_mono() { assert!((pcm.duration_secs() - 2.0).abs() < 0.5); } +#[test] +fn extract_pcm_without_explicit_range_matches_full_track_decode() { + if !ffmpeg_available() { + return; + } + let dir = tempfile::tempdir().unwrap(); + let av = dir.path().join("aac-full-track.mp4"); + let ffmpeg = ffmpeg_path(); + let generated = Command::new(&ffmpeg) + .args([ + "-v", + "error", + "-f", + "lavfi", + "-i", + "color=c=0x336699:s=32x18:r=4", + "-f", + "lavfi", + "-i", + "sine=frequency=440:sample_rate=16000", + "-t", + "1", + "-c:v", + "mpeg4", + "-c:a", + "aac", + "-y", + ]) + .arg(&av) + .status() + .map(|status| status.success()) + .unwrap_or(false); + if !generated { + return; + } + + let direct = Command::new(&ffmpeg) + .args(["-v", "error", "-i"]) + .arg(&av) + .args(["-vn", "-ac", "1", "-ar", "16000", "-f", "f32le", "-"]) + .output() + .unwrap(); + assert!(direct.status.success(), "direct ffmpeg decode must succeed"); + assert_eq!(direct.stdout.len() % 4, 0, "f32 PCM byte count must align"); + + let spec = PcmSpec { + sample_rate: 16_000, + channels: 1, + format: PcmFormat::F32, + }; + let pcm = extract_pcm(&av, &spec, None).unwrap(); + assert_eq!( + pcm.samples_f32.len(), + direct.stdout.len() / 4, + "range=None must decode the full track, not an implicit [0,duration] window" + ); +} + #[test] fn extract_pcm_range_is_shorter() { if !ffmpeg_available() { @@ -330,7 +388,7 @@ fn encode_roundtrip_produces_playable_video() { // Push 10 frames of solid color. for i in 0..10u8 { let mut rgba = vec![0u8; (w * h * 4) as usize]; - for px in rgba.chunks_exact_mut(4) { + for px in rgba.as_chunks_mut::<4>().0.iter_mut() { px[0] = i * 20; px[1] = 100; px[2] = 200; @@ -363,7 +421,7 @@ fn encode_codec_roundtrip(codec: VideoCodec, extension: &str, expected_codec: &s let mut encoder = VideoEncoder::new(&out, width, height, 10, &preset).unwrap(); for index in 0..6_u8 { let mut rgba = vec![0_u8; (width * height * 4) as usize]; - for pixel in rgba.chunks_exact_mut(4) { + for pixel in rgba.as_chunks_mut::<4>().0.iter_mut() { pixel.copy_from_slice(&[index.saturating_mul(30), 80, 180, 255]); } encoder @@ -458,7 +516,9 @@ fn prores_4444_roundtrip_preserves_alpha_plane() { .1; let alpha = decoded .rgba - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|pixel| pixel[3]) .collect::>(); for (actual, expected) in alpha.iter().zip([0_u8, 85, 170, 255]) { @@ -490,7 +550,9 @@ fn continuous_decode_scales_real_main10_frames_without_corruption() { let neon_green = decoded .frame .rgba - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .filter(|pixel| pixel[0] < 32 && pixel[1] > 224 && pixel[2] < 32) .count(); assert!( diff --git a/crates/opentake-media/tests/hdr.rs b/crates/opentake-media/tests/hdr.rs index 8b40a438..70548dd6 100644 --- a/crates/opentake-media/tests/hdr.rs +++ b/crates/opentake-media/tests/hdr.rs @@ -127,7 +127,9 @@ fn packaged_hdr_decode_path_materializes_bt709_rgba_pixels() { assert_eq!((frame.width, frame.height), (160, 90)); let (min, max) = frame .rgba - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .flat_map(|pixel| pixel[..3].iter().copied()) .fold((u8::MAX, u8::MIN), |(min, max), value| { (min.min(value), max.max(value)) diff --git a/crates/opentake-motion/src/renderer.rs b/crates/opentake-motion/src/renderer.rs index 3ad66f8a..36aefdb6 100644 --- a/crates/opentake-motion/src/renderer.rs +++ b/crates/opentake-motion/src/renderer.rs @@ -2152,7 +2152,6 @@ mod chromium_backend { json!({"policy": "pause"}), Some(&capture_session), )?; - self.set_host_background(&capture_session, seed)?; self.command( "Page.startScreencast", json!({ @@ -2165,8 +2164,40 @@ mod chromium_backend { )?; started = true; self.ensure_no_blocked_url()?; + // StartCapture may fill Chromium's send window with old frames + // before any new paint is delivered. ACK that window before the + // first mutation; an ACK alone does not replay a dropped paint. + trace(format!( + "frame {frame_index}: {background} startup drain start" + )); + drop(self.receive_and_ack_screencast_png(&capture_session, frame_index)?); + self.ack_pending_screencast_frames(&capture_session)?; + trace(format!( + "frame {frame_index}: {background} startup drain complete" + )); + self.set_host_background(&capture_session, seed)?; + trace(format!( + "frame {frame_index}: {background} seed guard start" + )); + self.settle_compositor(&capture_session)?; + let seed_image = + self.receive_guarded_generation(&capture_session, seed, pass, None)?; + drop(seed_image); + trace(format!( + "frame {frame_index}: {background} seed guard complete" + )); if let Some((fence, marker)) = author_marker { + // One producer mutation per acknowledged fence also keeps + // the OOPIF marker paint out of the host seed's send window. self.set_author_marker(fence, marker)?; + self.settle_compositor(&capture_session)?; + let marked_seed = self.receive_guarded_generation( + &capture_session, + seed, + pass, + Some(marker), + )?; + drop(marked_seed); } trace(format!( "frame {frame_index}: {background} transition guard start" @@ -2237,8 +2268,11 @@ mod chromium_backend { height, index: frame_index, } = frame; + let started = Instant::now(); + let mut received = 0usize; loop { self.check_abort()?; + trace(format!("frame {frame_index}: {background} screencast wait expected={expected_guard:?} received={received} elapsed_ms={}", started.elapsed().as_millis())); let png = self.receive_and_ack_screencast_png(session, frame_index)?; let image = decode_viewport_png(&png, background, frame_index)?; let expected_dimensions = ( @@ -2255,9 +2289,15 @@ mod chromium_backend { image.dimensions() ))); } - if external_guard_matches(&image, width, height, expected_guard) - && author_marker.is_none_or(|marker| author_marker_matches(&image, marker)) - { + received += 1; + let guard_matches = external_guard_matches(&image, width, height, expected_guard); + let marker_matches = + author_marker.is_none_or(|marker| author_marker_matches(&image, marker)); + trace(format!("frame {frame_index}: {background} screencast received n={received} png_bytes={} corner={:?} guard={guard_matches} marker={marker_matches} elapsed_ms={}", png.len(), image.get_pixel(width, height).0, started.elapsed().as_millis())); + if guard_matches && marker_matches { + // Frames queued before the accepted fence belong to this + // or an older generation; no next mutation has been sent. + self.ack_pending_screencast_frames(session)?; return Ok(image); } } @@ -2280,11 +2320,17 @@ mod chromium_backend { "Chromium screencast frame has no integer sessionId: {event}" )) })?; + trace(format!( + "frame {frame_index}: screencast event received; ack {screencast_session_id} start" + )); self.command( "Page.screencastFrameAck", json!({"sessionId": screencast_session_id}), Some(session), )?; + trace(format!( + "frame {frame_index}: screencast ack {screencast_session_id} complete" + )); self.ensure_no_blocked_url()?; let encoded = required_string(params, "data")?; let png = base64::engine::general_purpose::STANDARD @@ -2509,13 +2555,12 @@ mod chromium_backend { fn capture_stable_background( &mut self, target_id: &str, - session: &str, + _session: &str, author_fence: &AuthorPaintFence, rgb: [u8; 3], pass: CapturePass<'_>, ) -> MotionResult { let CapturePass { frame, background } = pass; - self.set_host_background(session, rgb)?; let markers = self.next_author_marker_plan(author_fence)?; let first = self.capture_isolated_viewport( target_id, @@ -3059,7 +3104,12 @@ mod chromium_backend { } let mut recovered = Vec::with_capacity(black.len()); - for (black, white) in black.chunks_exact(4).zip(white.chunks_exact(4)) { + for (black, white) in black + .as_chunks::<4>() + .0 + .iter() + .zip(white.as_chunks::<4>().0) + { let mut deltas = [ white[0].saturating_sub(black[0]), white[1].saturating_sub(black[1]), @@ -3821,7 +3871,7 @@ mod chromium_backend { } #[test] - fn guarded_candidate_requires_transition_then_desired_generation() { + fn guarded_candidate_drains_old_startup_frames_before_publishing_seed() { fn guarded_png(author: [u8; 4], guard: [u8; 3]) -> String { let mut image = image::RgbaImage::from_pixel( 2, @@ -3832,10 +3882,54 @@ mod chromium_backend { base64::engine::general_purpose::STANDARD .encode(encode_viewport_png(image, 0).unwrap()) } - + fn send(socket: &mut WebSocket, value: Value) { + socket.send(Message::text(value.to_string())).unwrap(); + } + fn exchange(socket: &mut WebSocket, method: &str, params: Value) { + let command = match socket.read().unwrap() { + Message::Text(text) => serde_json::from_str::(&text).unwrap(), + other => panic!("expected CDP command, got {other:?}"), + }; + assert_eq!(command["method"], method); + assert_eq!(command["params"], params); + let expected_session = if method.starts_with("Target.") { + None + } else { + Some("capture-session") + }; + assert_eq!( + command.get("sessionId").and_then(Value::as_str), + expected_session + ); + let result = match method { + "Target.attachToTarget" => json!({"sessionId":"capture-session"}), + "Runtime.evaluate" => json!({"result":{"type":"boolean","value":true}}), + _ => json!({}), + }; + send(socket, json!({"id":command["id"],"result":result})); + } + fn advance(socket: &mut WebSocket) { + exchange( + socket, + "Emulation.setVirtualTimePolicy", + json!({"policy":"advance","budget":1,"maxVirtualTimeTaskStarvationCount":10_000}), + ); + send( + socket, + json!({"method":"Emulation.virtualTimeBudgetExpired","params":{},"sessionId":"capture-session"}), + ); + } + fn frame(socket: &mut WebSocket, id: u64, data: &str) { + send( + socket, + json!({"method":"Page.screencastFrame","params":{"data":data,"metadata":{},"sessionId":id},"sessionId":"capture-session"}), + ); + } let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let client_stream = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); let (server_stream, _) = listener.accept().unwrap(); + client_stream.set_nodelay(true).unwrap(); + server_stream.set_nodelay(true).unwrap(); let client_socket = WebSocket::from_raw_socket( MaybeTlsStream::Plain(client_stream), Role::Client, @@ -3845,190 +3939,83 @@ mod chromium_backend { let seed = [0, 0, 90]; let transition = [0, 0, 165]; let desired = [0, 0, 0]; - let wrong = guarded_png([1, 2, 3, 255], [17, 18, 19]); - let transition_frame = guarded_png([4, 5, 6, 255], transition); + let stale = guarded_png([1, 2, 3, 255], [17, 18, 19]); + let seeded = guarded_png([4, 5, 6, 255], seed); + let transitioned = guarded_png([4, 5, 6, 255], transition); let desired_frame = guarded_png([128, 0, 0, 255], desired); - let host_background = |rgb: [u8; 3]| { - json!({ - "expression": format!( - "(() => {{ const layer = document.getElementById('opentake-host-background'); if (!layer) return false; layer.style.backgroundColor = 'rgb({} {} {})'; return true; }})()", - rgb[0], rgb[1], rgb[2] - ), - "returnByValue": true - }) - }; + let background = + |rgb| json!({"expression":host_background_expression(rgb),"returnByValue":true}); let server = thread::spawn(move || { - let read_json = |socket: &mut WebSocket| match socket.read().unwrap() { - Message::Text(text) => serde_json::from_str::(text.as_ref()).unwrap(), - other => panic!("expected CDP command, got {other:?}"), - }; - let send_json = |socket: &mut WebSocket, value: Value| { - socket.send(Message::text(value.to_string())).unwrap(); - }; - - let attach = read_json(&mut server_socket); - assert_eq!(attach["method"], "Target.attachToTarget"); - send_json( + exchange( &mut server_socket, - json!({"id": 1, "result": {"sessionId": "capture-session"}}), + "Target.attachToTarget", + json!({"targetId":"target-id","flatten":true}), ); - for (id, method, params) in [ - (2, "Page.enable", json!({})), - ( - 3, - "Emulation.setVirtualTimePolicy", - json!({"policy": "pause"}), - ), - (4, "Runtime.evaluate", host_background(seed)), - ( - 5, - "Page.startScreencast", - json!({ - "format": "png", - "maxWidth": 2, - "maxHeight": 2, - "everyNthFrame": 1 - }), - ), - (6, "Runtime.evaluate", host_background(transition)), - ] { - let command = read_json(&mut server_socket); - assert_eq!( - command, - json!({ - "id": id, - "method": method, - "params": params, - "sessionId": "capture-session" - }) - ); - let result = if method == "Runtime.evaluate" { - json!({"result": {"type": "boolean", "value": true}}) - } else { - json!({}) - }; - send_json(&mut server_socket, json!({"id": id, "result": result})); - } - - let transition_fence = read_json(&mut server_socket); - assert_eq!( - transition_fence, - json!({ - "id": 7, - "method": "Emulation.setVirtualTimePolicy", - "params": { - "policy": "advance", - "budget": 1, - "maxVirtualTimeTaskStarvationCount": 10_000 - }, - "sessionId": "capture-session" - }) + exchange(&mut server_socket, "Page.enable", json!({})); + exchange( + &mut server_socket, + "Emulation.setVirtualTimePolicy", + json!({"policy":"pause"}), ); - send_json(&mut server_socket, json!({"id": 7, "result": {}})); - send_json( + exchange( &mut server_socket, - json!({ - "method": "Emulation.virtualTimeBudgetExpired", - "params": {}, - "sessionId": "capture-session" - }), + "Page.startScreencast", + json!({"format":"png","maxWidth":2,"maxHeight":2,"everyNthFrame":1}), ); - - for (id, data) in [(8, wrong), (9, transition_frame.clone())] { - send_json( + // The Windows failure contains three OLD frames: even seed + // must not be published until the startup window is drained. + frame(&mut server_socket, 70, &stale); + frame(&mut server_socket, 71, &stale); + frame(&mut server_socket, 72, &stale); + for id in 70..=72 { + exchange( &mut server_socket, - json!({ - "method": "Page.screencastFrame", - "params": {"data": data, "metadata": {}, "sessionId": 70 + id}, - "sessionId": "capture-session" - }), + "Page.screencastFrameAck", + json!({"sessionId":id}), ); - let ack = read_json(&mut server_socket); - assert_eq!(ack["id"], id); - assert_eq!(ack["method"], "Page.screencastFrameAck"); - send_json(&mut server_socket, json!({"id": id, "result": {}})); } - - let desired_command = read_json(&mut server_socket); - assert_eq!( - desired_command, - json!({ - "id": 10, - "method": "Runtime.evaluate", - "params": host_background(desired), - "sessionId": "capture-session" - }) - ); - send_json( + exchange(&mut server_socket, "Runtime.evaluate", background(seed)); + advance(&mut server_socket); + frame(&mut server_socket, 77, &seeded); + exchange( &mut server_socket, - json!({"id": 10, "result": {"result": {"type": "boolean", "value": true}}}), - ); - - let desired_fence = read_json(&mut server_socket); - assert_eq!( - desired_fence, - json!({ - "id": 11, - "method": "Emulation.setVirtualTimePolicy", - "params": { - "policy": "advance", - "budget": 1, - "maxVirtualTimeTaskStarvationCount": 10_000 - }, - "sessionId": "capture-session" - }) + "Page.screencastFrameAck", + json!({"sessionId":77}), ); - send_json(&mut server_socket, json!({"id": 11, "result": {}})); - send_json( + exchange( &mut server_socket, - json!({ - "method": "Emulation.virtualTimeBudgetExpired", - "params": {}, - "sessionId": "capture-session" - }), + "Runtime.evaluate", + background(transition), ); - - for (id, data) in [(12, transition_frame), (13, desired_frame)] { - send_json( + advance(&mut server_socket); + for (id, data) in [(73, &seeded), (74, &transitioned)] { + frame(&mut server_socket, id, data); + exchange( &mut server_socket, - json!({ - "method": "Page.screencastFrame", - "params": {"data": data, "metadata": {}, "sessionId": 70 + id}, - "sessionId": "capture-session" - }), + "Page.screencastFrameAck", + json!({"sessionId":id}), ); - let ack = read_json(&mut server_socket); - assert_eq!(ack["id"], id); - assert_eq!(ack["method"], "Page.screencastFrameAck"); - send_json(&mut server_socket, json!({"id": id, "result": {}})); } - - for (id, method, params, session) in [ - ( - 14, - "Page.stopScreencast", - json!({}), - Some("capture-session"), - ), - ( - 15, - "Target.detachFromTarget", - json!({"sessionId": "capture-session"}), - None, - ), - ] { - let command = read_json(&mut server_socket); - assert_eq!(command["id"], id); - assert_eq!(command["method"], method); - assert_eq!(command["params"], params); - assert_eq!(command.get("sessionId").and_then(Value::as_str), session); - send_json(&mut server_socket, json!({"id": id, "result": {}})); + exchange(&mut server_socket, "Runtime.evaluate", background(desired)); + advance(&mut server_socket); + for (id, data) in [(75, &transitioned), (76, &desired_frame)] { + frame(&mut server_socket, id, data); + exchange( + &mut server_socket, + "Page.screencastFrameAck", + json!({"sessionId":id}), + ); } + exchange(&mut server_socket, "Page.stopScreencast", json!({})); + exchange( + &mut server_socket, + "Target.detachFromTarget", + json!({"sessionId":"capture-session"}), + ); }); - let mut cdp = Cdp::new( client_socket, - SandboxPolicy::default(), + SandboxPolicy::offline_with_timeout(Duration::from_secs(1)), MotionCancellationToken::new(), Instant::now() + Duration::from_secs(1), ); @@ -4143,9 +4130,9 @@ mod chromium_backend { json!({"id": 1, "result": {"sessionId": "capture-session"}}).to_string(), )) .unwrap(); - for id in 2..=6 { + for id in 2..=4 { let command = read_json(&mut server_socket); - if id == 5 { + if id == 4 { assert_eq!(command["method"], "Page.startScreencast"); } let result = if command["method"] == "Runtime.evaluate" { @@ -4159,22 +4146,6 @@ mod chromium_backend { )) .unwrap(); } - let transition_fence = read_json(&mut server_socket); - assert_eq!(transition_fence["method"], "Emulation.setVirtualTimePolicy"); - assert_eq!(transition_fence["params"]["policy"], "advance"); - server_socket - .send(Message::text(json!({"id": 7, "result": {}}).to_string())) - .unwrap(); - server_socket - .send(Message::text( - json!({ - "method": "Emulation.virtualTimeBudgetExpired", - "params": {}, - "sessionId": "capture-session" - }) - .to_string(), - )) - .unwrap(); server_socket .send(Message::text( json!({ @@ -4188,13 +4159,13 @@ mod chromium_backend { let ack = read_json(&mut server_socket); assert_eq!(ack["method"], "Page.screencastFrameAck"); server_socket - .send(Message::text(json!({"id": 8, "result": {}}).to_string())) + .send(Message::text(json!({"id": 5, "result": {}}).to_string())) .unwrap(); let stop = read_json(&mut server_socket); assert_eq!(stop["method"], "Page.stopScreencast"); server_socket .send(Message::text( - json!({"id": 9, "error": {"code": -1, "message": "stop-secondary"}}) + json!({"id": 6, "error": {"code": -1, "message": "stop-secondary"}}) .to_string(), )) .unwrap(); @@ -4202,7 +4173,7 @@ mod chromium_backend { assert_eq!(detach["method"], "Target.detachFromTarget"); server_socket .send(Message::text( - json!({"id": 10, "error": {"code": -2, "message": "detach-secondary"}}) + json!({"id": 7, "error": {"code": -2, "message": "detach-secondary"}}) .to_string(), )) .unwrap(); @@ -4726,6 +4697,10 @@ mod chromium_backend { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let client_stream = TcpStream::connect(listener.local_addr().unwrap()).unwrap(); let (server_stream, _) = listener.accept().unwrap(); + // Match tungstenite::connect: protocol tests must not accumulate + // Nagle/delayed-ACK stalls between consecutive tiny CDP messages. + client_stream.set_nodelay(true).unwrap(); + server_stream.set_nodelay(true).unwrap(); let client_socket = WebSocket::from_raw_socket( MaybeTlsStream::Plain(client_stream), Role::Client, @@ -4761,25 +4736,6 @@ mod chromium_backend { (1_u64, [255, 255, 255], [255, 127, 127, 255]), ] { let markers = author_marker_plan(&server_fence.nonce, marker_generation); - let background = read_json(&mut server_socket); - assert_eq!( - background, - json!({ - "id": next_id, - "method": "Runtime.evaluate", - "params": { - "expression": host_background_expression(rgb), - "returnByValue": true - }, - "sessionId": "main-session" - }) - ); - send_json( - &mut server_socket, - json!({"id": next_id, "result": {"result": {"type": "boolean", "value": true}}}), - ); - next_id += 1; - let mut previous_capture_session = None::; for marker in markers { let capture_session = format!("capture-{capture_index}"); @@ -4815,13 +4771,6 @@ mod chromium_backend { for (method, params) in [ ("Page.enable", json!({})), ("Emulation.setVirtualTimePolicy", json!({"policy": "pause"})), - ( - "Runtime.evaluate", - json!({ - "expression": host_background_expression(seed), - "returnByValue": true - }), - ), ( "Page.startScreencast", json!({ @@ -4851,6 +4800,71 @@ mod chromium_backend { next_id += 1; } + // Fill the initial stream with stale frames before any + // requested color or author mutation may be published. + for session_id in 1..=3 { + send_json( + &mut server_socket, + json!({ + "method":"Page.screencastFrame", + "params":{"data":encoded(current,[255,255,255],marker),"metadata":{},"sessionId":session_id}, + "sessionId":capture_session + }), + ); + } + for session_id in 1..=3 { + let ack = read_json(&mut server_socket); + assert_eq!( + ack, + json!({"id":next_id,"method":"Page.screencastFrameAck","params":{"sessionId":session_id},"sessionId":capture_session}) + ); + send_json(&mut server_socket, json!({"id":next_id,"result":{}})); + next_id += 1; + } + let seed_command = read_json(&mut server_socket); + assert_eq!( + seed_command, + json!({ + "id":next_id,"method":"Runtime.evaluate", + "params":{"expression":host_background_expression(seed),"returnByValue":true}, + "sessionId":capture_session + }) + ); + send_json( + &mut server_socket, + json!({"id":next_id,"result":{"result":{"type":"boolean","value":true}}}), + ); + next_id += 1; + let host_fence = read_json(&mut server_socket); + assert_eq!( + host_fence, + json!({ + "id":next_id,"method":"Emulation.setVirtualTimePolicy", + "params":{"policy":"advance","budget":1,"maxVirtualTimeTaskStarvationCount":10_000},"sessionId":capture_session + }) + ); + send_json(&mut server_socket, json!({"id":next_id,"result":{}})); + next_id += 1; + send_json( + &mut server_socket, + json!({"method":"Emulation.virtualTimeBudgetExpired","params":{},"sessionId":capture_session}), + ); + send_json( + &mut server_socket, + json!({ + "method":"Page.screencastFrame", + "params":{"data":encoded(current,seed,AuthorMarker {rgb:[marker.rgb[0]^0xff,marker.rgb[1],marker.rgb[2]],..marker}),"metadata":{},"sessionId":4}, + "sessionId":capture_session + }), + ); + let seed_ack = read_json(&mut server_socket); + assert_eq!( + seed_ack, + json!({"id":next_id,"method":"Page.screencastFrameAck","params":{"sessionId":4},"sessionId":capture_session}) + ); + send_json(&mut server_socket, json!({"id":next_id,"result":{}})); + next_id += 1; + let author_generation = read_json(&mut server_socket); assert_eq!( author_generation, @@ -4871,6 +4885,54 @@ mod chromium_backend { ); next_id += 1; + let seed_fence = read_json(&mut server_socket); + assert_eq!( + seed_fence, + json!({ + "id": next_id, + "method": "Emulation.setVirtualTimePolicy", + "params": {"policy":"advance","budget":1,"maxVirtualTimeTaskStarvationCount":10_000}, + "sessionId": capture_session + }) + ); + send_json(&mut server_socket, json!({"id":next_id,"result":{}})); + next_id += 1; + send_json( + &mut server_socket, + json!({ + "method":"Emulation.virtualTimeBudgetExpired", + "params":{},"sessionId":capture_session + }), + ); + // A seed with a stale author surface must not release + // the startup fence, even when the external guard matches. + for (session_id, seed_marker) in [ + ( + 5, + AuthorMarker { + rgb: [marker.rgb[0] ^ 0xff, marker.rgb[1], marker.rgb[2]], + ..marker + }, + ), + (6, marker), + ] { + send_json( + &mut server_socket, + json!({ + "method":"Page.screencastFrame", + "params":{"data":encoded(current,seed,seed_marker),"metadata":{},"sessionId":session_id}, + "sessionId":capture_session + }), + ); + let ack = read_json(&mut server_socket); + assert_eq!( + ack, + json!({"id":next_id,"method":"Page.screencastFrameAck","params":{"sessionId":session_id},"sessionId":capture_session}) + ); + send_json(&mut server_socket, json!({"id":next_id,"result":{}})); + next_id += 1; + } + let transition_command = read_json(&mut server_socket); assert_eq!( transition_command, @@ -5116,7 +5178,7 @@ mod chromium_backend { let mut cdp = Cdp::new( client_socket, - SandboxPolicy::default(), + SandboxPolicy::offline_with_timeout(Duration::from_secs(1)), MotionCancellationToken::new(), Instant::now() + Duration::from_secs(1), ); diff --git a/crates/opentake-ops/src/command.rs b/crates/opentake-ops/src/command.rs index 01e14b10..4017d418 100644 --- a/crates/opentake-ops/src/command.rs +++ b/crates/opentake-ops/src/command.rs @@ -642,6 +642,20 @@ pub struct RenameEntry { pub name: String, } +/// One folder creation entry for [`EditCommand::CreateFolders`]. +#[derive(Clone, Debug)] +pub struct FolderCreateEntry { + pub name: String, + pub parent_folder_id: Option, +} + +/// One asset-folder assignment for [`EditCommand::MoveToFolders`]. +#[derive(Clone, Debug)] +pub struct FolderMoveEntry { + pub asset_ids: Vec, + pub folder_id: Option, +} + /// A text overlay entry for [`EditCommand::AddTexts`]. The transform is supplied /// fully resolved (text measurement is a media/UI concern this leaf doesn't do). #[derive(Clone, Debug)] @@ -1109,11 +1123,15 @@ pub enum EditCommand { name: String, parent_folder_id: Option, }, + /// Create several media-library folders in one undoable transaction. + CreateFolders { entries: Vec }, /// Move media assets into a folder (or to root with `None`). MoveToFolder { asset_ids: Vec, folder_id: Option, }, + /// Apply several asset-folder assignments in one undoable transaction. + MoveToFolders { entries: Vec }, /// Rename media assets (single = one-element vec). Library-only; clip /// references are unaffected. RenameMedia { entries: Vec }, @@ -1428,10 +1446,12 @@ pub fn apply( name, parent_folder_id, } => create_folder(state, name, parent_folder_id, ids), + EditCommand::CreateFolders { entries } => create_folders(state, entries, ids), EditCommand::MoveToFolder { asset_ids, folder_id, } => move_to_folder(state, asset_ids, folder_id), + EditCommand::MoveToFolders { entries } => move_to_folders(state, entries), EditCommand::RenameMedia { entries } => rename_media(state, entries), EditCommand::RenameFolder { entries } => rename_folder(state, entries), EditCommand::DeleteMedia { asset_ids } => delete_media(state, asset_ids), @@ -1675,7 +1695,9 @@ fn edit_nested_sequence( | EditCommand::DissolveNestedSequence { .. } | EditCommand::PlaceMedia { .. } | EditCommand::CreateFolder { .. } + | EditCommand::CreateFolders { .. } | EditCommand::MoveToFolder { .. } + | EditCommand::MoveToFolders { .. } | EditCommand::RenameMedia { .. } | EditCommand::RenameFolder { .. } | EditCommand::DeleteMedia { .. } @@ -6142,6 +6164,54 @@ fn create_folder( ) } +fn create_folders( + state: &mut EditorState, + entries: Vec, + ids: &dyn IdGen, +) -> Result { + if entries.is_empty() { + return Err(EditError::Invalid( + "create_folder: entries must not be empty".into(), + )); + } + if entries.iter().any(|entry| entry.name.is_empty()) { + return Err(EditError::Invalid("folder name is required".into())); + } + for entry in &entries { + if let Some(parent) = entry.parent_folder_id.as_deref() { + if !state + .manifest + .folders + .iter() + .any(|folder| folder.id == parent) + { + return Err(EditError::Invalid(format!( + "parentFolderId not found: {parent}" + ))); + } + } + } + let count = entries.len(); + transact( + state, + "New Folders", + move |created| format!("Created {count} folder(s): {}", created.join(", ")), + |st| { + let mut created = Vec::with_capacity(entries.len()); + for entry in &entries { + let id = ops::create_folder( + &mut st.manifest, + entry.name.clone(), + entry.parent_folder_id.clone(), + ids, + ); + created.push(id); + } + Ok(created) + }, + ) +} + fn move_to_folder( state: &mut EditorState, asset_ids: Vec, @@ -6166,6 +6236,70 @@ fn move_to_folder( ) } +fn move_to_folders( + state: &mut EditorState, + entries: Vec, +) -> Result { + if entries.is_empty() { + return Err(EditError::Invalid( + "move_to_folder: entries must not be empty".into(), + )); + } + if entries.iter().any(|entry| entry.asset_ids.is_empty()) { + return Err(EditError::Invalid( + "move_to_folder: assetIds is required".into(), + )); + } + for entry in &entries { + for asset_id in &entry.asset_ids { + if !state + .manifest + .entries + .iter() + .any(|asset| asset.id == *asset_id) + { + return Err(EditError::Invalid(format!( + "media asset not found: {asset_id}" + ))); + } + } + if let Some(folder_id) = entry.folder_id.as_deref() { + if !state + .manifest + .folders + .iter() + .any(|folder| folder.id == folder_id) + { + return Err(EditError::Invalid(format!( + "folderId not found: {folder_id}" + ))); + } + } + } + let asset_count = entries + .iter() + .map(|entry| entry.asset_ids.len()) + .sum::(); + let operation_count = entries.len(); + transact( + state, + "Move to Folder", + move |_| { + format!("Moved {asset_count} asset(s) across {operation_count} folder operation(s)") + }, + |st| { + for entry in &entries { + ops::move_to_folder( + &mut st.manifest, + &entry.asset_ids.iter().cloned().collect(), + entry.folder_id.clone(), + ); + } + Ok(Vec::new()) + }, + ) +} + fn rename_media( state: &mut EditorState, entries: Vec, diff --git a/crates/opentake-ops/src/lib.rs b/crates/opentake-ops/src/lib.rs index 14bb499c..0ed65663 100644 --- a/crates/opentake-ops/src/lib.rs +++ b/crates/opentake-ops/src/lib.rs @@ -32,8 +32,8 @@ pub use engines::{ // --- Command layer --- pub use command::{ apply, CaptionEntry, CaptionTranslationChange, ClipEntry, ClipProperties, - ClipPropertyAssignment, EditCommand, EditError, EditResult, KeyframePayload, KeyframeProperty, - KeyframeValue, RenameEntry, TextAutoTrackEntry, TextEntry, + ClipPropertyAssignment, EditCommand, EditError, EditResult, FolderCreateEntry, FolderMoveEntry, + KeyframePayload, KeyframeProperty, KeyframeValue, RenameEntry, TextAutoTrackEntry, TextEntry, }; pub use editor_state::{DocSnapshot, EditorState}; pub use id::{IdGen, SeqIdGen}; diff --git a/crates/opentake-render/src/gpu/text_engine.rs b/crates/opentake-render/src/gpu/text_engine.rs index d22e4353..f864014f 100644 --- a/crates/opentake-render/src/gpu/text_engine.rs +++ b/crates/opentake-render/src/gpu/text_engine.rs @@ -456,7 +456,7 @@ mod tests { assert_eq!(frame.rgba.len(), (1024 * 108 * 4) as usize); // With fonts available, "Hello" must paint some non-transparent pixels. if r.has_fonts() { - let painted = frame.rgba.chunks_exact(4).any(|px| px[3] > 0); + let painted = frame.rgba.as_chunks::<4>().0.iter().any(|px| px[3] > 0); assert!( painted, "expected visible text pixels when fonts are present" @@ -473,7 +473,7 @@ mod tests { }; let frame = r.rasterize(&req("Hi", &style)).expect("frame"); // Background is opaque, so every pixel has full alpha regardless of fonts. - let opaque = frame.rgba.chunks_exact(4).all(|px| px[3] == 255); + let opaque = frame.rgba.as_chunks::<4>().0.iter().all(|px| px[3] == 255); assert!(opaque, "opaque background should fill the whole box"); } diff --git a/crates/opentake-render/src/plan/tests.rs b/crates/opentake-render/src/plan/tests.rs index a4d94f62..d5fd23e1 100644 --- a/crates/opentake-render/src/plan/tests.rs +++ b/crates/opentake-render/src/plan/tests.rs @@ -525,6 +525,20 @@ fn source_frame_video_with_trim_and_speed() { assert_eq!(source_frame_index(cp, 110), 25); } +#[test] +fn source_frame_video_with_trim_and_fractional_speed() { + let mut clip = video_clip("c0", 0, 6); + clip.trim_start_frame = 2; + clip.speed = 1.5; + let tl = single_video_timeline(clip); + let plan = build_render_plan(&tl, RS, &TestMetrics::default()); + let cp = &plan.clip_plans[0]; + + assert_eq!(source_frame_index(cp, 0), 2); + assert_eq!(source_frame_index(cp, 3), 7); + assert_eq!(source_frame_index(cp, 5), 10); +} + #[test] fn source_frame_video_reversed_respects_trim_window() { let mut clip = video_clip("c0", 0, 20); diff --git a/crates/opentake-render/tests/gpu_downscaled_nat.rs b/crates/opentake-render/tests/gpu_downscaled_nat.rs index 4ee0e79c..e61ae798 100644 --- a/crates/opentake-render/tests/gpu_downscaled_nat.rs +++ b/crates/opentake-render/tests/gpu_downscaled_nat.rs @@ -45,7 +45,7 @@ struct SmallTexResolver<'d> { impl TextureResolver for SmallTexResolver<'_> { fn resolve(&mut self, _s: &TextureSource, _f: i64) -> Option> { let mut buf = vec![0u8; 4 * 4 * 4]; - for px in buf.chunks_exact_mut(4) { + for px in buf.as_chunks_mut::<4>().0.iter_mut() { px.copy_from_slice(&self.rgba); } let frame = DecodedFrame::new(4, 4, buf, true); // premultiplied solid @@ -102,7 +102,7 @@ fn downscaled_texture_full_canvas_still_fills_whole_frame() { // source color. Before the fix the content rendered only in the bottom-left // 4×4 quarter (quad scaled by tex 4 instead of nat 16), leaving the rest // opaque black — so any pixel being black catches the regression. - for (i, px) in frame.rgba.chunks_exact(4).enumerate() { + for (i, px) in frame.rgba.as_chunks::<4>().0.iter().enumerate() { assert_eq!( px, &[40, 200, 80, 255], diff --git a/crates/opentake-render/tests/gpu_effects.rs b/crates/opentake-render/tests/gpu_effects.rs index 91d72380..afa81cf8 100644 --- a/crates/opentake-render/tests/gpu_effects.rs +++ b/crates/opentake-render/tests/gpu_effects.rs @@ -81,7 +81,7 @@ impl TextureResolver for SolidResolver<'_> { fn resolve(&mut self, _source: &TextureSource, _frame: i64) -> Option> { if self.cached.is_none() { let mut buf = vec![0u8; 16 * 16 * 4]; - for px in buf.chunks_exact_mut(4) { + for px in buf.as_chunks_mut::<4>().0.iter_mut() { px.copy_from_slice(&self.rgba); } let frame = DecodedFrame::new(16, 16, buf, true); diff --git a/crates/opentake-render/tests/gpu_smoke.rs b/crates/opentake-render/tests/gpu_smoke.rs index 28849356..f6781331 100644 --- a/crates/opentake-render/tests/gpu_smoke.rs +++ b/crates/opentake-render/tests/gpu_smoke.rs @@ -41,7 +41,7 @@ impl TextureResolver for SolidResolver<'_> { fn resolve(&mut self, _source: &TextureSource, _frame: i64) -> Option> { if self.cached.is_none() { let mut buf = vec![0u8; 16 * 16 * 4]; - for px in buf.chunks_exact_mut(4) { + for px in buf.as_chunks_mut::<4>().0.iter_mut() { px.copy_from_slice(&self.rgba); } let frame = DecodedFrame::new(16, 16, buf, true); // already premultiplied @@ -109,7 +109,7 @@ fn empty_plan_clears_to_opaque_black() { .expect("render"); // Every pixel opaque black. - for px in frame.rgba.chunks_exact(4) { + for px in frame.rgba.as_chunks::<4>().0.iter() { assert_eq!(px, &[0, 0, 0, 255], "clear color must be opaque black"); } } @@ -285,7 +285,7 @@ fn read_back_round_trips_through_png() { fn make_solid(device: &wgpu::Device, queue: &wgpu::Queue, rgba: [u8; 4]) -> Rc { let mut buf = vec![0u8; 16 * 16 * 4]; - for px in buf.chunks_exact_mut(4) { + for px in buf.as_chunks_mut::<4>().0.iter_mut() { px.copy_from_slice(&rgba); } let frame = DecodedFrame::new(16, 16, buf, true); diff --git a/crates/opentake-render/tests/gpu_text.rs b/crates/opentake-render/tests/gpu_text.rs index 3b0b811e..e8ae249a 100644 --- a/crates/opentake-render/tests/gpu_text.rs +++ b/crates/opentake-render/tests/gpu_text.rs @@ -138,7 +138,9 @@ fn text_clip_composites_visible_pixels() { if rasterizer.has_fonts() { let any_lit = frame .rgba - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .any(|px| px[0] > 8 || px[1] > 8 || px[2] > 8); assert!( any_lit, @@ -153,7 +155,13 @@ fn text_clip_composites_visible_pixels() { } fn lit_count(frame: &DecodedFrame) -> u32 { - frame.rgba.chunks_exact(4).filter(|px| px[3] > 0).count() as u32 + frame + .rgba + .as_chunks::<4>() + .0 + .iter() + .filter(|px| px[3] > 0) + .count() as u32 } fn x_centroid(frame: &DecodedFrame) -> f64 { diff --git a/crates/opentake-render/tests/gpu_y_orientation.rs b/crates/opentake-render/tests/gpu_y_orientation.rs index cb334686..325f0fbd 100644 --- a/crates/opentake-render/tests/gpu_y_orientation.rs +++ b/crates/opentake-render/tests/gpu_y_orientation.rs @@ -61,7 +61,7 @@ impl TextureResolver for SolidResolver<'_> { fn resolve(&mut self, _source: &TextureSource, _frame: i64) -> Option> { if self.cached.is_none() { let mut buf = vec![0u8; 64 * 64 * 4]; - for px in buf.chunks_exact_mut(4) { + for px in buf.as_chunks_mut::<4>().0.iter_mut() { px.copy_from_slice(&self.rgba); } let frame = DecodedFrame::new(64, 64, buf, true); diff --git a/crates/opentake-render/tests/pixel_diff.rs b/crates/opentake-render/tests/pixel_diff.rs index f522b811..4064ce67 100644 --- a/crates/opentake-render/tests/pixel_diff.rs +++ b/crates/opentake-render/tests/pixel_diff.rs @@ -138,7 +138,7 @@ fn ssim(a: &[u8], b: &[u8], w: u32, h: u32) -> f64 { fn make_solid(rgba: [u8; 4]) -> DecodedFrame { let mut buf = vec![0u8; (Q * Q * 4) as usize]; - for px in buf.chunks_exact_mut(4) { + for px in buf.as_chunks_mut::<4>().0.iter_mut() { px.copy_from_slice(&rgba); } DecodedFrame::new(Q, Q, buf, true) @@ -638,7 +638,9 @@ fn text_overlay_visible_above_video() { if rasterizer.has_fonts() { let any_text = frame .rgba - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .any(|px| px[0] > 30 && px[1] > 30); assert!(any_text, "expected visible text pixels above blue video"); } else { diff --git a/crates/opentake-render/tests/transitions.rs b/crates/opentake-render/tests/transitions.rs index 51f59b2f..4be2c92a 100644 --- a/crates/opentake-render/tests/transitions.rs +++ b/crates/opentake-render/tests/transitions.rs @@ -49,7 +49,7 @@ impl TextureResolver for PairResolver<'_> { other => panic!("unexpected transition source {other}"), }; let mut rgba = vec![0; 16 * 16 * 4]; - for pixel in rgba.chunks_exact_mut(4) { + for pixel in rgba.as_chunks_mut::<4>().0.iter_mut() { pixel.copy_from_slice(&color); } let frame = DecodedFrame::new(16, 16, rgba, true); diff --git a/docs/INDEX.md b/docs/INDEX.md index 5e60acd4..fb13c744 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,17 +1,28 @@ # OpenTake 文档总目录 -> 全项目文档的**唯一入口**。文档按「模块」组织成超链接树: +> 状态:canonical · 阶段:implementation-backed · 同步日期:2026-09-06 +> 全项目文档的**唯一索引入口**(自动入口为根 AGENTS.md)。文档按「模块」组织成超链接树: > **要开发某个模块,只需读该模块的 `OVERVIEW.md`(总览)+ `INDEX.md`(目录)**,目录里再链到该模块的各子系统文档与规格。 --- +## 当前工作与状态 + +源码清单版本为 `1.0.0-beta.6`;公开版本与下载以 [GitHub Releases](https://github.com/appergb/OpenTake/releases) 为准。源码、GUI 和平台安装包验收分别绑定实际证据。 + +- [公开 Beta 活动计划](plans/active/2026-09-06-public-beta.md):当前主计划。 +- [Beta 6 版本说明](releases/1.0.0-beta.6.md):本轮变化、交付与发布流程。 +- [当日运行验证](audit/2026-09-06/public-beta-validation.md):主代理维护,未执行门槛仍标为待验证。 +- [Sticker 专项记录](audit/2026-09-06/sticker-panel.md):源码和定向检查;安装包实际操作见当日总验收。 +- [项目规范与意图](project/index.md) · [文档同步报告](documentation-sync-2026-09-06.md)。 + ## 🧭 如何使用本文档树 ``` docs/ ├── INDEX.md ← 你在这里(总目录) ├── modules/ ← ★ 按 crate / 前端分的模块文档树 -│ ├── INDEX.md ← 模块总目录(11 个模块一览) +│ ├── INDEX.md ← 模块总目录(含进程树支持模块) │ └── <模块>/ │ ├── OVERVIEW.md ← 模块总览:职责 / 依赖 / 数据流 / 完成状态 / 对应上游 │ ├── INDEX.md ← 模块目录:链到本模块所有子系统文档 + 规格 + 源码 @@ -29,12 +40,13 @@ docs/ | 层 | 模块 | 一句话 | 入口 | |---|---|---|---| | 领域 | `opentake-domain` | Timeline/Track/Clip/Keyframe 纯值语义(叶子 crate) | [总览](modules/opentake-domain/OVERVIEW.md) · [目录](modules/opentake-domain/INDEX.md) | +| 平台基础 | `opentake-process-tree` | 子进程树生命周期与取消 | [总览](modules/opentake-process-tree/OVERVIEW.md) · [目录](modules/opentake-process-tree/INDEX.md) | | 引擎 | `opentake-ops` | 纯引擎(Overwrite/Ripple/Snap) + EditCommand + 撤销栈 | [总览](modules/opentake-ops/OVERVIEW.md) · [目录](modules/opentake-ops/INDEX.md) | | 能力 | `opentake-project` | 工程持久化 / bundle / archive / 导出 | [总览](modules/opentake-project/OVERVIEW.md) · [目录](modules/opentake-project/INDEX.md) | | 能力 | `opentake-render` | wgpu 合成器 + 文本栅格化(预览/导出共享 RenderPlan) | [总览](modules/opentake-render/OVERVIEW.md) · [目录](modules/opentake-render/INDEX.md) | | 能力 | `opentake-media` | FFmpeg 编解码 / 缩略图 / 波形 / 转写 / 语义搜索 | [总览](modules/opentake-media/OVERVIEW.md) · [目录](modules/opentake-media/INDEX.md) | | 能力 | `opentake-motion` | Lottie / web 动态图形 | [总览](modules/opentake-motion/OVERVIEW.md) · [目录](modules/opentake-motion/INDEX.md) | -| 能力 | `opentake-agent` | MCP server(当前 44 个可发布工具:38 基础 + 4 生成 + 2 动效,能力门控后按能力发布) + 内置 Agent + Context Signal | [总览](modules/opentake-agent/OVERVIEW.md) · [目录](modules/opentake-agent/INDEX.md) | +| 能力 | `opentake-agent` | MCP server(当前 44 个可发布工具:基础 + 生成 + 动效,能力门控后按能力发布) + 内置 Agent + Context Signal | [总览](modules/opentake-agent/OVERVIEW.md) · [目录](modules/opentake-agent/INDEX.md) | | 能力 | `opentake-gen` | 生成式 AI 客户端(BYOK,无后端) | [总览](modules/opentake-gen/OVERVIEW.md) · [目录](modules/opentake-gen/INDEX.md) | | 装配 | `opentake-core` | 会话管理 / DI / 事件总线(命令路由层) | [总览](modules/opentake-core/OVERVIEW.md) · [目录](modules/opentake-core/INDEX.md) | | 装配 | `src-tauri` | Tauri 2 桌面壳 + Tauri 命令 | [总览](modules/src-tauri/OVERVIEW.md) · [目录](modules/src-tauri/INDEX.md) | @@ -46,6 +58,18 @@ docs/ 跨切面、不属单一模块的设计/规划/报告:总体架构、路线图、1:1 移植图与差距、剪映 gap、已知 Bug、编辑自动化 DOS。 +长期背景计划:[OpenTake 全功能与上游收敛计划](superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md)。 + +已记录的 Preview 对齐切片:[Preview tabs parity plan](superpowers/plans/2026-08-21-preview-tabs-parity.md)。 + +已记录的 Preview temporal parity 切片:[Preview temporal remap parity plan](superpowers/plans/2026-08-21-preview-temporal-remap-parity.md)。 + +已记录的媒体导入切片:[File import dialog reliability plan](superpowers/plans/2026-08-21-file-import-dialog.md)。 + +已记录的媒体视图切片:[Media view modes parity plan](superpowers/plans/2026-08-21-media-view-modes.md)。 + +能力清单与证据:[Capability ledger](capabilities/CAPABILITY-LEDGER.md) · [Desktop functional matrix](audit/2026-08-21/full-desktop-functional-matrix.md)。 + ## 📐 上游拆解参考 → [upstream-analysis/README.md](upstream-analysis/README.md) 上游 Palmier Pro(Swift)的架构、Apple 框架可移植性、闭源云边界、MCP/Agent 工具拆解。 @@ -57,11 +81,11 @@ docs/ | 文档 | 用途 | |---|---| | [README.md](../README.md) · [README.zh-CN.md](../README.zh-CN.md) · [README.ja.md](../README.ja.md) | 项目概览(多语言) | -| [CLAUDE.md](../CLAUDE.md) | 工作交接状态文档(压缩上下文后先读) | +| [CLAUDE.md](../CLAUDE.md) | historical:早期交接快照,非当前任务入口 | | [AGENTS.md](../AGENTS.md) | AI Agent 协作指南 | | [DECISIONS.md](../DECISIONS.md) | 技术选型决策(为何 Rust/Tauri/GPL-3.0) | | [CHANGELOG.md](../CHANGELOG.md) | 变更历史 | | [CONTRIBUTING.md](../CONTRIBUTING.md) | 贡献指南 | | [Specs Index](specs/INDEX.md) | 已批准/历史规格目录 | -| [Beta 发布与验证](releases/1.0.0-beta.5.md) · [Motion Studio 验证](audit/2026-08-13/beta5-motion-studio.md) | ★ 当前 Beta 5 范围、发布门槛与逐项执行证据 | +| [Beta 发布与验证](releases/1.0.0-beta.5.md) · [Motion Studio 验证](audit/2026-08-13/beta5-motion-studio.md) | Beta 5 发布时范围与历史执行证据 | | [Superpowers Recovery](superpowers/specs/2026-07-08-opentake-recovery-integration-design.md) | 历史恢复集成设计与计划入口(Beta 2 已收口) | diff --git a/docs/architecture/ADVANCED-FEATURES.md b/docs/architecture/ADVANCED-FEATURES.md index b71dd571..9d0173cb 100644 --- a/docs/architecture/ADVANCED-FEATURES.md +++ b/docs/architecture/ADVANCED-FEATURES.md @@ -1,5 +1,9 @@ # OpenTake 进阶能力设计(对标剪映模块 1–5 的差距深化) +> 状态:draft · 阶段:partial-implementation · 路由同步:2026-09-06。本文包含设计目标与已有实现;须按源码和日期化证据逐项判断。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > **2026-08-01 Beta 对账附录(覆盖下表“现状”列)**:本文的 `missing` / `partial` > 是实现前的历史快照,继续保留作为设计来源,不再表示当前代码状态。首个 Beta 已闭环: > 通用特效、交叉溶解、线性光调色、Lift/Gamma/Gain、HSL、3D LUT、绿幕、圆形/线性/ diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 8b21fc41..9a9ecfe1 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -1,8 +1,12 @@ # OpenTake 架构设计 +> 状态:draft · 阶段:partial-implementation · 路由同步:2026-09-06。本文包含设计目标与已有实现;须按源码和日期化证据逐项判断。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > 基于对 palmier-pro-upstream(Swift macOS 视频编辑器)的逐模块拆解综合得出。 > 目标:忠实复刻其编辑逻辑,做成跨平台(macOS/Windows/Linux)、开源(GPL-3.0)、内置更强提示词与能力的版本。 -> 详见 `docs/MODULE-PORT-MAP.md`(20 模块逐项规格)与 `docs/_analysis/`(4 份横切报告)。 +> 详见 `docs/architecture/MODULE-PORT-MAP.md`(20 模块逐项规格)与 `docs/_analysis/`(4 份横切报告)。 ## 0. 一句话洞察 @@ -64,7 +68,7 @@ ## 3. Cargo workspace 布局 ``` -OpenTake/ +OpenTake-generation/ ├── crates/ │ ├── opentake-domain/ # 值类型模型:Timeline/Track/Clip/Keyframe/Transform/Crop/TextStyle/MediaAsset │ │ # + 派生函数(end_frame/source_frames_consumed/*_at 采样/fade)。零 IO,纯逻辑,可全单测 @@ -85,9 +89,9 @@ OpenTake/ └── docs/ ``` -依赖法则(经上游验证):`domain` 零依赖叶子;`ops` 只依赖 `domain`;`command` 是唯一编辑入口;UI/Agent/MCP 是命令层三个对等客户端。 +依赖法则(经上游验证):`domain` 为依赖 serde 的无 I/O 叶子;`ops` 只依赖 `domain`;`command` 是唯一编辑入口;UI/Agent/MCP 是命令层三个对等客户端。 -> Motion / AI Video v1 已由 `plugins/motion-canvas-studio/` 落地:锁定 Motion Canvas 3.17.2(MIT),渲染 materialized mp4 后由 OpenTake 当普通媒体原子导入并落轨。`crates/opentake-motion/` 同时提供离线 Chromium 宿主与 HTML/CSS fallback;透明 alpha / PNG sequence 留给后续版本。 +> Motion / AI Video v1 已由 `plugins/motion-canvas-studio/` 落地:锁定 Motion Canvas 3.17.2(MIT),渲染 materialized mp4 后由 OpenTake 当普通媒体原子导入并落轨。`crates/opentake-motion/` 同时提供离线 Chromium 宿主与 HTML/CSS fallback;透明 alpha 发布与 ProRes 4444 已进入候选;通用 PNG sequence 仍是独立后续范围。 ## 4. 领域模型(可直接复刻,见 MODULE-PORT-MAP.md「Models」) diff --git a/docs/architecture/BUGS.md b/docs/architecture/BUGS.md index 194184b9..8900a3eb 100644 --- a/docs/architecture/BUGS.md +++ b/docs/architecture/BUGS.md @@ -1,5 +1,9 @@ # OpenTake — 已知 Bug 与问题 +> 状态:historical · 阶段:partial-implementation。原日期、问题与对照结论保留;不作为当前缺口清单。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > 本文记录扫描确认的所有真实 Bug 和有问题部分。不包括规划中已承认的"未实现功能"(那些在 [PORT-1TO1-GAP.md](PORT-1TO1-GAP.md) 和 [ROADMAP.md](ROADMAP.md) 中)。 > 最后更新:2026-06-26 diff --git a/docs/architecture/CAPCUT-GAP.md b/docs/architecture/CAPCUT-GAP.md index 77d05ecd..00f7853e 100644 --- a/docs/architecture/CAPCUT-GAP.md +++ b/docs/architecture/CAPCUT-GAP.md @@ -1,5 +1,9 @@ # OpenTake vs 剪映 特性差距报告(模块 1–5) +> 状态:historical · 阶段:partial-implementation。原日期、问题与对照结论保留;不作为当前缺口清单。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > 由 5 个 max-思考子 Agent 对照 OpenTake 设计稿 + 上游源码逐特性核对。状态:has=已覆盖 / partial=部分 / missing=缺失。已排除剪映模块 6(自然语言交互/语音助手,OpenTake 已有 Agent)与模块 7(云生态/企业协作)。 ## 模块1:基础剪辑与时间线管理 diff --git a/docs/architecture/EDITING-ENGINE-PLAN.md b/docs/architecture/EDITING-ENGINE-PLAN.md index bb5805aa..5577f4d4 100644 --- a/docs/architecture/EDITING-ENGINE-PLAN.md +++ b/docs/architecture/EDITING-ENGINE-PLAN.md @@ -1,5 +1,9 @@ # 剪辑引擎实现现状与规划(EDITING-ENGINE-PLAN) +> 状态:draft · 阶段:partial-implementation · 路由同步:2026-09-06。本文包含设计目标与已有实现;须按源码和日期化证据逐项判断。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > **2026-08-03 状态注记:**本文是剪辑引擎的测绘文档。剪辑「算法核」(`ops/*` + `engines/*`) > 已 1:1 写通并随 Beta 1 交付;本文所述「前端接线层缺口」已收口(时间线手势单事务、原子分割 / > Transform、撤销/重做)。Beta 2 范围与发布门槛见 `docs/releases/1.0.0-beta.2.md`。 diff --git a/docs/architecture/FULL_PROJECT_SCAN_REPORT.md b/docs/architecture/FULL_PROJECT_SCAN_REPORT.md index e2f97542..b6b53813 100644 --- a/docs/architecture/FULL_PROJECT_SCAN_REPORT.md +++ b/docs/architecture/FULL_PROJECT_SCAN_REPORT.md @@ -1,5 +1,9 @@ # OpenTake vs palmier-pro-upstream 全项目扫描报告 +> 状态:historical · 阶段:partial-implementation。原日期、问题与对照结论保留;不作为当前缺口清单。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + **扫描日期**: 2026-06-26 **最后更新**: 2026-06-26 (经代码验证修正) **指令**: 仅只读扫描(read-only),使用多 Agent 模式并行探索 + 直接工具调用。**禁止任何代码修改**。 diff --git a/docs/architecture/HANDOFF-2026-07.md b/docs/architecture/HANDOFF-2026-07.md index 14bf8a52..cd6a4b5f 100644 --- a/docs/architecture/HANDOFF-2026-07.md +++ b/docs/architecture/HANDOFF-2026-07.md @@ -1,5 +1,9 @@ # 交接 · 未完成工作规划(2026-07-04) +> 状态:historical · 阶段:partial-implementation。原日期、问题与对照结论保留;不作为当前缺口清单。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > **2026-08-03 状态裁决:**本文件是历史交接记录,不再是当前状态真值。Beta 1 > 已发布(`v1.0.0-beta.1`,起点 `c73c192`),Beta 2 收尾进行中;当前范围与发布 > 门槛见 `docs/releases/1.0.0-beta.2.md`,逐项执行证据见 diff --git a/docs/architecture/INDEX.md b/docs/architecture/INDEX.md index 1b90bc26..8deac148 100644 --- a/docs/architecture/INDEX.md +++ b/docs/architecture/INDEX.md @@ -1,11 +1,18 @@ # 架构与规划 — 目录 +> 状态:canonical · 阶段:implementation-backed · 路由同步:2026-09-06。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + > 上级:[docs 总目录](../INDEX.md) · 同级:[模块文档树](../modules/INDEX.md) · [上游拆解](../upstream-analysis/README.md) > > 这里收录**跨切面、不属单一模块**的设计、规划与报告文档。具体某个模块的实现文档见 [模块文档树](../modules/INDEX.md)。 --- +## 当前执行入口 + +[公开 Beta 活动计划](../plans/active/2026-09-06-public-beta.md) · [Beta 6 候选(待验证、未发布)](../releases/1.0.0-beta.6.md) · [同步报告](../documentation-sync-2026-09-06.md)。旧交接、差距和扫描报告保留原时点结论,不能作为当前待办真值。 + ## 总体设计 | 文档 | 内容 | @@ -18,7 +25,7 @@ | 文档 | 内容 | |---|---| -| [../releases/1.0.0-beta.2.md](../releases/1.0.0-beta.2.md) | **当前 Beta 2 范围、11 阶段验收顺序与发布门槛** | +| [../releases/1.0.0-beta.2.md](../releases/1.0.0-beta.2.md) | 历史 Beta 2 范围与验收记录 | | [HANDOFF-2026-07.md](HANDOFF-2026-07.md) | 2026-07-04 交接时点快照;保留作历史参考,不是当前 TODO 真值 | | [ROADMAP.md](ROADMAP.md) | 分阶段路线图(Phase 0 脚手架 → Motion Canvas 插件) | | [EDITING-ENGINE-PLAN.md](EDITING-ENGINE-PLAN.md) | 剪辑引擎现况与规划:已移植的 ops 层 + 待收口 gap | diff --git a/docs/architecture/MODULE-PORT-MAP.md b/docs/architecture/MODULE-PORT-MAP.md index 634ba218..5128ee36 100644 --- a/docs/architecture/MODULE-PORT-MAP.md +++ b/docs/architecture/MODULE-PORT-MAP.md @@ -1,5 +1,9 @@ # OpenTake 模块移植地图 +> 状态:draft · 阶段:partial-implementation · 路由同步:2026-09-06。本文包含设计目标与已有实现;须按源码和日期化证据逐项判断。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + > 由 20 个 max-思考子 Agent 对 palmier-pro-upstream 逐模块拆解生成。verdict 含义:direct-port=Rust直写 / needs-replacement=换跨平台库 / ui-rebuild=React重建 / cloud-rebuild=自建后端。 ## 总览 @@ -1097,7 +1101,7 @@ MCPService 端口 19789 与工具注册属 Agent/MCP 子系统,App 层只做开 - [图片编码主流程 ImageEncoder.encode] 输入文件 URL。先算 fileStamp(path+size+mtime);若 stamp 命中内存缓存直接返回。否则走两条路径取其一:(1) passthrough 直通:要求能 sniff 出 MIME(仅 png/jpeg/gif/webp)且 文件字节<=3_500_000 且 像素 max(w,h)<=1568,满足则把原文件字节(mmap 读取)原样返回、保留原 MIME;(2) 否则 downscaled:用 ImageIO 生成最长边=1568 的缩略图(自动应用 EXIF 方向),再依次以 JPEG 质量 [0.85,0.7,0.55,0.4] 编码,取第一个字节数<=3_500_000 的结果,MIME 固定 image/jpeg;四档都超限则返回 nil。成功结果写回缓存;写前若缓存条目数>=32 则整表清空(粗暴全清,非 LRU)。复刻要点:1568 与 3.5MB 两个阈值、质量四档序列、缓存满清空策略、直通仅限四种格式、像素判定用 max(宽,高)。Rust 用 image crate 解码+缩放(Lanczos3),mozjpeg/jpeg-encoder 编码并循环试质量;EXIF 方向需用 kamadak-exif 手动旋转(ImageIO 的 CreateThumbnailWithTransform 在 Rust 无等价,必须显式处理)。 - [图片元数据/缩略图 ImageEncoder] metadata(url, thumbnailMaxPixelSize?):用 CGImageSourceCopyPropertiesAtIndex 读 kCGImagePropertyPixelWidth/Height;若给了 thumbnailMaxPixelSize 则同时生成缩略图。makeThumbnail 选项:CreateThumbnailFromImageAlways=true、WithTransform=true(应用方向)、ShouldCacheImmediately=true、ThumbnailMaxPixelSize=给定值;imageSource 创建时 ShouldCache=false。encodeJPEG(CGImage,quality):用 CGImageDestination 以 LossyCompressionQuality 编码为 JPEG Data。 - [时间码格式化 formatTimecode] 输入 frame:Int、fps:Int,输出 'HH:MM:SS:FF'(冒号分隔,帧号而非毫秒)。fps<=0 时返回 '00:00:00:00'。取 absFrame=abs(frame);ff=absFrame%fps;totalSeconds=absFrame/fps(整除);ss=totalSeconds%60;mm=(totalSeconds/60)%60;hh=totalSeconds/3600;负 frame 前缀 '-'。twoDigit:仅当 0<=value<10 时补前导零,>=10 或负数原样输出(即只保证个位数补零,不截断大于两位的值如小时)。复刻要点:这是非 drop-frame 时间码,帧字段用整数取模 fps;小时不会被两位截断。 -- [帧秒换算] frameToSeconds(frame,fps)= fps>0 ? Double(frame)/Double(fps) : 0。secondsToFrame(seconds,fps)= Int(seconds*Double(fps)),注意是向零截断(Int() 截断小数,不是四舍五入)——复刻必须用 truncate 而非 round,否则边界帧会差一。Double.rounded(toPlaces:)= 乘 10^places 后 .rounded()(就近,.5 向偶/远取决于平台默认 schoolbook)再除回。 +- [帧秒换算] frameToSeconds(frame,fps)= fps>0 ? Double(frame)/Double(fps) : 0。secondsToFrame(seconds,fps)= Int(seconds*Double(fps)),注意是向零截断(Int() 截断小数,不是四舍五入)——复刻必须用 truncate 而非 round,否则边界帧会差一。Double.rounded(toPlaces:)= 乘 10^places 后 .rounded()(就近,中点默认远离零;显式规则另按上游实现)再除回。 - [JSON 浮点规整 roundJSONFloatingPointNumbers] 递归遍历 Any:字典则 mapValues 递归;数组则 map 递归;若是 NSNumber 且非布尔(用 CFGetTypeID==CFBooleanGetTypeID 判定布尔)且 objCType 为 'd' 或 'f'(double/float),取 doubleValue:有限值用 NSDecimalNumber 以 .plain 模式、scale=places 四舍五入返回 NSDecimalNumber;非有限(NaN/Inf)返回 NSNull()。其它类型原样返回。复刻要点:只规整浮点,整数 NSNumber 不动;布尔不能被当数字;NaN/Inf -> null。Rust 中对 serde_json::Value 递归,仅处理 Number 里的 f64,用 (x*10^p).round()/10^p,非有限置 Null。 - [Keychain 存取 KeychainStore] service 固定为 Bundle.main.bundleIdentifier(回退 'io.palmier.pro')。save:先构造 query(class=GenericPassword,service,account),attrs 含 ValueData=utf8 数据 + Accessible=AfterFirstUnlock;先 SecItemUpdate,若返回 errSecItemNotFound 则把 attrs 合并进 query 后 SecItemAdd(典型 upsert 模式)。load:query 加 ReturnData=true、MatchLimit=One;成功后把 Data 转 utf8 字符串并 trim 空白换行,空串视为 nil。delete:按 service+account 删除。复刻要点:upsert 语义、AfterFirstUnlock 可见性、读出后 trim 且空串当无。Rust 用 keyring crate(macOS 后端即 Security framework),跨平台时 Windows=Credential Manager、Linux=Secret Service。 - [崩溃处理 CrashHandler] install():创建 crash.log 父目录;以 O_WRONLY|O_CREAT|O_APPEND 模式 open 文件得 fd(失败为 -1);NSSetUncaughtExceptionHandler 注册 C 函数指针;对 SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE/SIGTRAP 六个信号注册 signalHandler。未捕获异常处理器:写入时间戳、异常名、reason、callStackSymbols 到 fd。信号处理器(必须 async-signal-safe,只用 write/backtrace/backtrace_symbols_fd/fsync/raise):写 '*** FATAL SIGNAL ***' 头,用 backtrace 抓最多 64 帧并 backtrace_symbols_fd 写出,fsync,然后 signal(sig,SIG_DFL) 复位并 raise(sig) 重新触发以产生系统崩溃报告。复刻要点:信号处理里绝不能分配堆/加锁/用非 async-signal-safe 调用。Rust 用 libc::sigaction + backtrace crate(注意 backtrace 在信号处理中不完全 async-signal-safe,生产可改用第三方如 sentry-rust 的 native crash 或 minidump),异常对应到 Rust panic hook。 diff --git a/docs/architecture/PLAYBACK-ENGINE.md b/docs/architecture/PLAYBACK-ENGINE.md index 0ae49639..44ebf720 100644 --- a/docs/architecture/PLAYBACK-ENGINE.md +++ b/docs/architecture/PLAYBACK-ENGINE.md @@ -1,21 +1,18 @@ # Playback engine architecture -> Current reviewed state: 2026-08-10. The original 2026-07-04 default-off -> MJPEG design is historical; this document records the Wave 1A implementation. +> Status: draft · Stage: implementation-backed · Source review: 2026-09-06. +> Earlier dated QA remains historical. Current candidate evidence is in the [public Beta validation record](../audit/2026-09-06/public-beta-validation.md). ## Capability route is the sole authority `resolveTimelinePlaybackRoute` selects exactly one route before runtime preference or fallback is considered: -- `webkit`: one ordinary visible video track plus image/audio, including - temporal-only reverse or speed changes that do not require compositing. -- `rust`: content requiring the implemented compositor path: text, color grade, - chroma key, up to four linear/circle masks, or multiple visible video tracks. - A WebKit video decode failure may also retry the exact project revision here. -- `unsupported`: Lottie, enabled generic effects, polygon masks, more than four - masks, or any timeline that combines compositing with reverse or a non-unit - speed. Rust unavailable/disabled is also unsupported for Rust-only content. +- `webkit`: ordinary video playback, including single-track reverse/speed changes where no compositor-only property requires Rust. +- `rust`: text, Lottie, color grade, chroma key, stabilization, supported masks/effects and native video stacks when the runtime supports them. Composited temporal remapping is implemented; it is no longer rejected merely because speed differs from one or the clip is reversed. +- `unsupported`: unknown effect names, more than four masks, or a required Rust path that is unavailable/disabled. A multi-video temporal stack requires Rust and fails closed if unavailable. + +The exact branching, including ordinary multi-video runtime fallback behavior, is defined by [playbackRoute.ts](../../web/src/components/preview/playbackRoute.ts). Missing media/source failures are also handled by the native session, rather than silently omitting authored layers. Unsupported capability is fail closed. It receives a localized explanation and disabled Play/Capture/Space controls; it is not silently rendered by an engine diff --git a/docs/architecture/PORT-1TO1-GAP.md b/docs/architecture/PORT-1TO1-GAP.md index a7608080..35853a54 100644 --- a/docs/architecture/PORT-1TO1-GAP.md +++ b/docs/architecture/PORT-1TO1-GAP.md @@ -1,5 +1,9 @@ 我先确认一下任务范围。这是一份纯粹的综合写作任务:你已经给了 5 份详尽的研读 JSON,要求我把它们综合成一份中文 Markdown 文档。我不需要读代码库或验证——所有素材都在 JSON 里。下面直接输出文档本身。 +> 状态:historical · 阶段:partial-implementation。原日期、问题与对照结论保留;不作为当前缺口清单。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + # OpenTake 1:1 复刻差距与实现计划 ## 〇、现状真话:现在能不能完成一次真实剪辑闭环? diff --git a/docs/architecture/ROADMAP.md b/docs/architecture/ROADMAP.md index 80eb6fc1..019c7c01 100644 --- a/docs/architecture/ROADMAP.md +++ b/docs/architecture/ROADMAP.md @@ -1,14 +1,12 @@ # OpenTake 分阶段实施路线图 -> **2026-08-03 状态裁决:**Phase 0–10 已全部走完并交付 Beta 1(`v1.0.0-beta.1`, -> 2026-08-01 发布)。当前处于 **Beta 2 收尾与发布验证**阶段:范围与门槛见 -> `docs/releases/1.0.0-beta.2.md`,逐项执行证据见 `docs/audit/2026-08-02/beta-functional-verification.md`。 -> 下文各 Phase 的「进度」注记为历史里程碑,不再代表阻塞项;剩余工作集中在收尾 -> (未决 finding 清零、真机/Windows 验证、候选包验收),不涉及新的大功能开发。 +> 状态:historical · 阶段:partial-implementation · 原阶段日期保留。 +> 本文件保留早期 Phase 0–10 规划及其里程碑,不作为全功能完成证明。当前工作已进入 Beta 6 未发布候选,见[活动计划](../plans/active/2026-09-06-public-beta.md)、[候选说明](../releases/1.0.0-beta.6.md)及[当日验证](../audit/2026-09-06/public-beta-validation.md)。 +> 后续源码已加入透明 Motion/ProRes 4444、多预览 tab、temporal compositor 和媒体视图;语义搜索仍须真实模型安装/推理证据。下文早期“待做”不直接等于当前缺口。 > 原则:先把「纯逻辑层」做扎实并与上游对拍,再攻媒体引擎 blocker,最后接 UI / Agent / 生成后端。 -> 每个阶段都有明确「交付物」与「验证标准」。详见 `docs/ARCHITECTURE.md`、`docs/MODULE-PORT-MAP.md`。 -> 对标剪映的进阶能力(特效/转场/调色/蒙版/AI/音频工程等)穿插在下列阶段中,完整规划见 `docs/ADVANCED-FEATURES.md`。 +> 每个阶段都有明确「交付物」与「验证标准」。详见 `docs/architecture/ARCHITECTURE.md`、`docs/architecture/MODULE-PORT-MAP.md`。 +> 对标剪映的进阶能力(特效/转场/调色/蒙版/AI/音频工程等)穿插在下列阶段中,完整规划见 `docs/architecture/ADVANCED-FEATURES.md`。 ## Phase 0 — 工程脚手架 - **做**:Cargo workspace(空 crates 骨架)+ Tauri 2 app + React/Vite 前端 + CI(fmt/clippy/test/前端 build)+ GPL-3.0 LICENSE + README(标明「基于 Palmier Pro 的社区分支」)。 @@ -138,7 +136,7 @@ --- -**2026-07-14 完成度审计校准:** 路线条目已逐项归入 `docs/audit/2026-07-14/requirements.json`;当前裁决、证据与验收条件以同目录 `document-reconciliation.md` 为准,阶段文字不再单独作为完成证明。 +**2026-07-14 完成度审计校准:** 路线条目已逐项归入 `docs/audit/2026-07-14/requirements.json`;当时裁决、证据与验收条件以同目录 `document-reconciliation.md` 为准,阶段文字不再单独作为完成证明。 **当前有效条目的 ledger 绑定:** diff --git a/docs/architecture/STEM-SEPARATION.md b/docs/architecture/STEM-SEPARATION.md index f8a655b5..da414ba2 100644 --- a/docs/architecture/STEM-SEPARATION.md +++ b/docs/architecture/STEM-SEPARATION.md @@ -1,5 +1,9 @@ # Stem separation execution boundaries +> 状态:draft · 阶段:partial-implementation · 路由同步:2026-09-06。本文包含设计目标与已有实现;须按源码和日期化证据逐项判断。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + OpenTake currently exposes two explicit execution choices. Only the local path is executable in this release; the hosted path is intentionally fail-closed. ## Local execution diff --git a/docs/architecture/UPDATER.md b/docs/architecture/UPDATER.md index 2e78a9e5..b1cf9bea 100644 --- a/docs/architecture/UPDATER.md +++ b/docs/architecture/UPDATER.md @@ -1,5 +1,9 @@ # OpenTake 自动更新架构 +> 状态:draft · 阶段:partial-implementation · 路由同步:2026-09-06。本文包含设计目标与已有实现;须按源码和日期化证据逐项判断。 +> 当前执行与验收:[公开 Beta 计划](../plans/active/2026-09-06-public-beta.md) · [当日验证](../audit/2026-09-06/public-beta-validation.md)。 + + OpenTake 使用 Tauri v2 官方 updater 完成「检查 → 下载 → 签名验证 → 安装 → 安全重启」。更新源只允许独立仓库 `appergb/OpenTake`,不复用 OpenLess 的 URL 或签名密钥。 ## 发布与发现契约 diff --git a/docs/audit/2026-07-14/document-reconciliation.md b/docs/audit/2026-07-14/document-reconciliation.md index 219d72df..e7b950d4 100644 --- a/docs/audit/2026-07-14/document-reconciliation.md +++ b/docs/audit/2026-07-14/document-reconciliation.md @@ -148,7 +148,7 @@ Every row below is one current `incomplete` requirement from the normative ledge | agent-settings-generation | `doc-995da31233ec3c16` | docs/architecture/CAPCUT-GAP.md:219 | Talking-head cleanup removes both configured silence and filler words through reviewable commands. | Generate reviewable cut ranges for both configured silence thresholds and a defined filler-word lexicon using transcript timestamps.; Expose per-cut accept/reject, apply as one command, and one-step undo without altering unaccepted ranges.; On a fixed 30-second transcript/audio fixture, remove the expected silence and filler ranges within one frame, keep speech order/A-V sync, and verify save/reopen/export. | open-gap:acceptance-criteria-required | | agent-settings-generation | `doc-99b9674a4dd12fc7` | docs/specs/agent/10-implementation.md:82 | The Agent implementation checklist is complete, including all tools, safety checks, and cross-system coverage. | Bind only loopback, enforce Host/Origin/content-type/body-size limits, deny unknown/nonfinite/path inputs, and redact credentials/message content from logs.; Store BYOK secrets only in the OS-backed secret path and reject traversal, symlink escape, remote URL import, and unauthorized provider/tool requests fail closed.; Run DNS-rebinding/Origin, oversized body, traversal/symlink, secret-redaction, malformed JSON-RPC, concurrency, and graceful-shutdown tests before release. | open-gap:acceptance-criteria-required | | agent-settings-generation | `doc-9b90bbb501eeed4a` | docs/modules/opentake-agent/SPEC.md:189 | Generate an image asynchronously through GenClient and finalize it as a durable media asset. | generate_image validates model/reference capabilities, submits with authorized credentials, returns a placeholder id, and atomically finalizes result bytes plus generation metadata.; Tests cover reference inputs, restart recovery, provider failure/cancel, and add-to-timeline after completion. | crates/opentake-agent/src/mcp/dispatch.rs::tests::stub_tool_reports_not_implemented | -| agent-settings-generation | `doc-9bc7c9cdadaaa081` | docs/modules/opentake-agent/prompt.md:32 | At docs/modules/opentake-agent/prompt.md:32 under “base.rs:分段 base 提示” (gap-marker), the source “- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” requires this exact behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. | Source binding: docs/modules/opentake-agent/prompt.md:32; signal=gap-marker; heading=base.rs:分段 base 提示; candidate=- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效); Expected behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. This closes only the promise expressed by “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” in “base.rs:分段 base 提示”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” with the scenario below and register test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl; Initial state/input/event: create an isolated temporary project fixture representing the source state in “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust Agent/MCP dispatch, typed argument boundary, and any named Tauri/provider bridge, apply “Tell the model to pass shortened IDs back exactly without padding or guessing.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl. | active-gap:agent-settings-generation: docs/modules/opentake-agent/prompt.md:32 lacks strict implementation and automated-test proof | +| agent-settings-generation | `doc-9bc7c9cdadaaa081` | docs/modules/opentake-agent/prompt.md:32 | At docs/modules/opentake-agent/prompt.md:32 under “base.rs:分段 base 提示” (gap-marker), the source “- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../modules/opentake-agent/dispatch-tools.md) 契约失效)” requires this exact behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. | Source binding: docs/modules/opentake-agent/prompt.md:32; signal=gap-marker; heading=base.rs:分段 base 提示; candidate=- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../modules/opentake-agent/dispatch-tools.md) 契约失效); Expected behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. This closes only the promise expressed by “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../modules/opentake-agent/dispatch-tools.md) 契约失效)” in “base.rs:分段 base 提示”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../modules/opentake-agent/dispatch-tools.md) 契约失效)” with the scenario below and register test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl; Initial state/input/event: create an isolated temporary project fixture representing the source state in “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../modules/opentake-agent/dispatch-tools.md) 契约失效)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust Agent/MCP dispatch, typed argument boundary, and any named Tauri/provider bridge, apply “Tell the model to pass shortened IDs back exactly without padding or guessing.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl. | active-gap:agent-settings-generation: docs/modules/opentake-agent/prompt.md:32 lacks strict implementation and automated-test proof | | agent-settings-generation | `doc-9bd256cf84a82d70` | docs/specs/agent/1-mcp-server.md:47 | At docs/specs/agent/1-mcp-server.md:47, the source “### 1.2.1 绑定 + 幂等开关(照搬上游语义)” requires this exact behavior: Honor the io.opentake.mcp.enabled preference with default enabled semantics and idempotent startup while binding exactly 127.0.0.1:19789 and never 0.0.0.0. | Source binding: docs/specs/agent/1-mcp-server.md:47; signal=heading; heading=1.2.1 绑定 + 幂等开关(照搬上游语义); candidate=### 1.2.1 绑定 + 幂等开关(照搬上游语义); Expected behavior: Honor the io.opentake.mcp.enabled preference with default enabled semantics and idempotent startup while binding exactly 127.0.0.1:19789 and never 0.0.0.0.; Deterministic test: test:crates/opentake-agent/tests/spec_agent_1_line_47_9bd256cf84a82d70.rs#spec_agent_1_line_47_9bd256cf84a82d70_loopback_preference_and_idempotent_start; Initial state/input/event: run startup with the preference missing, false, and true, then call startup twice while the first listener is active and inspect the bound SocketAddr.; Code/store/API/Rust effect: default a missing preference to enabled, skip all binding when disabled, return the existing running handle on repeated start, and create at most one listener at 127.0.0.1:19789.; Visible/returned assertion: assert default enabled starts once, disabled starts zero times, repeated calls are idempotent, the address is 127.0.0.1:19789, and 0.0.0.0 is never attempted.; Evidence required: record the owning code:<tracked-file>#<declared-symbol> and the passing test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-agent/tests/spec_agent_1_line_47_9bd256cf84a82d70.rs#spec_agent_1_line_47_9bd256cf84a82d70_loopback_preference_and_idempotent_start. | active-gap:agent-settings-generation: docs/specs/agent/1-mcp-server.md:47 lacks strict implementation and automated-test proof | | agent-settings-generation | `doc-a466c25e65ff8024` | docs/architecture/editing-automation/EDITING-AUTOMATION/acceptance-tests.md:30 | The documented automation checks pass against live tool implementations. | Dispatch smart_reframe against decoded fixture frames and assert it returns an in-bounds 9:16 crop proposal rather than needs-vision-backend.; Apply the proposal to one clip, verify only crop/transform properties change, then undo to byte-equal timeline JSON.; Cover subject left/center/right, no-subject fallback, offline media, invalid ratio, cancellation, and save/reopen determinism. | open-gap:acceptance-criteria-required | | agent-settings-generation | `doc-a95bdb175b3dfc95` | docs/architecture/CAPCUT-GAP.md:225 | Avatar generation executes a configured provider and imports the result. | Route avatar generation through a configured provider with explicit consent, provider/model/request provenance, and source-audio identity.; Expose preview, start, progress, cancel, retry, import, and undo; cancellation/failure must import no asset.; Mock provider contracts and run one consented fixture asserting output duration matches narration within one frame and the imported result previews/exports. | open-gap:acceptance-criteria-required | @@ -425,7 +425,7 @@ Every row below is one current `incomplete` requirement from the normative ledge | media-render-playback-export | `doc-3336fb8bef6f3a49` | docs/specs/media/9-domain-contract.md:1 | At docs/specs/media/9-domain-contract.md:1 under “跨平台与合规要点” (heading), the source “# 跨平台与合规要点” requires this exact behavior: Media code follows cross-platform, frame-time, cache, and domain-boundary contracts. | Source binding: docs/specs/media/9-domain-contract.md:1; signal=heading; heading=跨平台与合规要点; candidate=# 跨平台与合规要点; Expected behavior: Media code follows cross-platform, frame-time, cache, and domain-boundary contracts. This closes only the promise expressed by “跨平台与合规要点” in “跨平台与合规要点”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “跨平台与合规要点” with the scenario below and register test:crates/opentake-render/tests/completion_3336fb8bef6f3a49.rs#completion_3336fb8bef6f3a49_media_code_follows_cross_platform_frame_time_cac; Initial state/input/event: load a deterministic local media fixture with fixed dimensions, frame rate, timestamps, tracks, and the parameters stated by “跨平台与合规要点”, then invoke the named preview, playback, render, or export event.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, route “Media code follows cross-platform, frame-time, cache, and domain-boundary contracts.” through the real decode/composition/audio/export pipeline with no placeholder success and without mutating source media.; Visible/returned assertion: assert the exact returned status plus deterministic frame/audio/container properties or typed unsupported/error output, and verify preview/export parity when both paths are named.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-render/tests/completion_3336fb8bef6f3a49.rs#completion_3336fb8bef6f3a49_media_code_follows_cross_platform_frame_time_cac. | active-gap:media-render-playback-export: docs/specs/media/9-domain-contract.md:1 lacks strict implementation and automated-test proof | | media-render-playback-export | `doc-376576e91988107c` | docs/specs/media/2-ffmpeg.md:80 | At docs/specs/media/2-ffmpeg.md:80 under “2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用)” (heading), the source “## 2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用)” requires this exact behavior: The named FFmpeg probe/decode/PCM/encode contract is implemented with integration fixtures. | Source binding: docs/specs/media/2-ffmpeg.md:80; signal=heading; heading=2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用); candidate=## 2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用); Expected behavior: The named FFmpeg probe/decode/PCM/encode contract is implemented with integration fixtures. This closes only the promise expressed by “2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用)” in “2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用)” with the scenario below and register test:crates/opentake-project/tests/completion_376576e91988107c.rs#completion_376576e91988107c_the_named_ffmpeg_probe_decode_pcm_encode_contrac; Initial state/input/event: create an isolated temporary project fixture representing the source state in “2.4 编码 / 导出预设(供 `opentake-render` 导出后端调用)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “The named FFmpeg probe/decode/PCM/encode contract is implemented with integration fixtures.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_376576e91988107c.rs#completion_376576e91988107c_the_named_ffmpeg_probe_decode_pcm_encode_contrac. | active-gap:media-render-playback-export: docs/specs/media/2-ffmpeg.md:80 lacks strict implementation and automated-test proof | | media-render-playback-export | `doc-379e138c3e674f1b` | docs/modules/opentake-motion/integration.md:32 | At docs/modules/opentake-motion/integration.md:32 under “解码器注入(为什么不硬接 PNG 库)” (gap-marker), the source “解码器对缺失/损坏文件返回 `None`——合成器把该帧当"缺帧"处理(与视频解码失败同语义)。” requires this exact behavior: Treat a missing/corrupt decoded motion frame as an absent source frame. | Source binding: docs/modules/opentake-motion/integration.md:32; signal=gap-marker; heading=解码器注入(为什么不硬接 PNG 库); candidate=解码器对缺失/损坏文件返回 `None`——合成器把该帧当"缺帧"处理(与视频解码失败同语义)。; Expected behavior: Treat a missing/corrupt decoded motion frame as an absent source frame. This closes only the promise expressed by “解码器对缺失/损坏文件返回 `None`——合成器把该帧当"缺帧"处理(与视频解码失败同语义)。” in “解码器注入(为什么不硬接 PNG 库)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “解码器对缺失/损坏文件返回 `None`——合成器把该帧当"缺帧"处理(与视频解码失败同语义)。” with the scenario below and register test:crates/opentake-project/tests/completion_379e138c3e674f1b.rs#completion_379e138c3e674f1b_treat_a_missing_corrupt_decoded_motion_frame_as_; Initial state/input/event: start from the smallest valid fixture for “Treat a missing/corrupt decoded motion frame as an absent source frame.”, then issue both the valid request and the exact malformed, missing, boundary, or hostile input named by “解码器对缺失/损坏文件返回 `None`——合成器把该帧当"缺帧"处理(与视频解码失败同语义)。”.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, on invalid input, reject before any store, project, filesystem, provider, or timeline mutation; on valid input, execute exactly the typed operation “Treat a missing/corrupt decoded motion frame as an absent source frame.” once.; Visible/returned assertion: assert the exact success payload for the valid case and a stable typed error for the invalid case, including zero partial side effects and no leaked internal path or credential.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_379e138c3e674f1b.rs#completion_379e138c3e674f1b_treat_a_missing_corrupt_decoded_motion_frame_as_. | active-gap:media-render-playback-export: docs/modules/opentake-motion/integration.md:32 lacks strict implementation and automated-test proof | -| media-render-playback-export | `doc-3a71829a7b489aae` | docs/modules/opentake-motion/sandbox.md:67 | At docs/modules/opentake-motion/sandbox.md:67 under “谁在调用” (gap-marker), the source “- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” requires this exact behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. | Source binding: docs/modules/opentake-motion/sandbox.md:67; signal=gap-marker; heading=谁在调用; candidate=- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。; Expected behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. This closes only the promise expressed by “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” in “谁在调用”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” with the scenario below and register test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s; Initial state/input/event: create an isolated temporary project fixture representing the source state in “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s. | active-gap:media-render-playback-export: docs/modules/opentake-motion/sandbox.md:67 lacks strict implementation and automated-test proof | +| media-render-playback-export | `doc-3a71829a7b489aae` | docs/modules/opentake-motion/sandbox.md:67 | At docs/modules/opentake-motion/sandbox.md:67 under “谁在调用” (gap-marker), the source “- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../modules/opentake-motion/renderer.md))。” requires this exact behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. | Source binding: docs/modules/opentake-motion/sandbox.md:67; signal=gap-marker; heading=谁在调用; candidate=- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../modules/opentake-motion/renderer.md))。; Expected behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. This closes only the promise expressed by “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../modules/opentake-motion/renderer.md))。” in “谁在调用”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../modules/opentake-motion/renderer.md))。” with the scenario below and register test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s; Initial state/input/event: create an isolated temporary project fixture representing the source state in “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../modules/opentake-motion/renderer.md))。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s. | active-gap:media-render-playback-export: docs/modules/opentake-motion/sandbox.md:67 lacks strict implementation and automated-test proof | | media-render-playback-export | `doc-3a72ee4de1f4a46f` | docs/specs/media/0-principles.md:1 | At docs/specs/media/0-principles.md:1 under “设计原则与移植铁律(本 crate 必须遵守)” (heading), the source “# 设计原则与移植铁律(本 crate 必须遵守)” requires this exact behavior: Media code follows cross-platform, frame-time, cache, and domain-boundary contracts. | Source binding: docs/specs/media/0-principles.md:1; signal=heading; heading=设计原则与移植铁律(本 crate 必须遵守); candidate=# 设计原则与移植铁律(本 crate 必须遵守); Expected behavior: Media code follows cross-platform, frame-time, cache, and domain-boundary contracts. This closes only the promise expressed by “设计原则与移植铁律(本 crate 必须遵守)” in “设计原则与移植铁律(本 crate 必须遵守)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “设计原则与移植铁律(本 crate 必须遵守)” with the scenario below and register test:crates/opentake-render/tests/completion_3a72ee4de1f4a46f.rs#completion_3a72ee4de1f4a46f_media_code_follows_cross_platform_frame_time_cac; Initial state/input/event: load a deterministic local media fixture with fixed dimensions, frame rate, timestamps, tracks, and the parameters stated by “设计原则与移植铁律(本 crate 必须遵守)”, then invoke the named preview, playback, render, or export event.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, route “Media code follows cross-platform, frame-time, cache, and domain-boundary contracts.” through the real decode/composition/audio/export pipeline with no placeholder success and without mutating source media.; Visible/returned assertion: assert the exact returned status plus deterministic frame/audio/container properties or typed unsupported/error output, and verify preview/export parity when both paths are named.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-render/tests/completion_3a72ee4de1f4a46f.rs#completion_3a72ee4de1f4a46f_media_code_follows_cross_platform_frame_time_cac. | active-gap:media-render-playback-export: docs/specs/media/0-principles.md:1 lacks strict implementation and automated-test proof | | media-render-playback-export | `doc-41a8a7a62b46a85e` | docs/architecture/HANDOFF-2026-07.md:191 | The listed renderer, packaging, settings, solo, and stub debt is closed with automated evidence. | Implement Lottie/motion materialization, track-solo playback/export semantics, settings/model/provider persistence, and all residual InspectMedia/generate/upscale/motion/batch library dispatch paths.; Bundle verified FFmpeg/ffprobe sidecars for packaged macOS/Windows and return typed unsupported errors for any renderer/tool capability still unavailable; advertise no placeholder success.; Pass motion/Lottie pixel fixtures, solo A/V mix tests, settings secret/restart/list_models tests, per-tool schema/failure/undo matrix, and installed-app probe/decode/playback/export smoke on both targets. | open-gap:acceptance-criteria-required | | media-render-playback-export | `doc-428f02e756802f9f` | docs/architecture/PLAYBACK-ENGINE.md:88 | At docs/architecture/PLAYBACK-ENGINE.md:88 under “Project/source identity and prewarm/cache” (heading), the source “## Project/source identity and prewarm/cache” requires this exact behavior: The playback subsystem implements project/source identity and prewarm/cache with focused route/lifecycle tests. | Source binding: docs/architecture/PLAYBACK-ENGINE.md:88; signal=heading; heading=Project/source identity and prewarm/cache; candidate=## Project/source identity and prewarm/cache; Expected behavior: The playback subsystem implements project/source identity and prewarm/cache with focused route/lifecycle tests. This closes only the promise expressed by “Project/source identity and prewarm/cache” in “Project/source identity and prewarm/cache”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “Project/source identity and prewarm/cache” with the scenario below and register test:crates/opentake-project/tests/completion_428f02e756802f9f.rs#completion_428f02e756802f9f_the_playback_subsystem_implements_project_source; Initial state/input/event: load a deterministic local media fixture with fixed dimensions, frame rate, timestamps, tracks, and the parameters stated by “Project/source identity and prewarm/cache”, then invoke the named preview, playback, render, or export event.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, route “The playback subsystem implements project/source identity and prewarm/cache with focused route/lifecycle tests.” through the real decode/composition/audio/export pipeline with no placeholder success and without mutating source media.; Visible/returned assertion: assert the exact returned status plus deterministic frame/audio/container properties or typed unsupported/error output, and verify preview/export parity when both paths are named.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_428f02e756802f9f.rs#completion_428f02e756802f9f_the_playback_subsystem_implements_project_source. | active-gap:media-render-playback-export: docs/architecture/PLAYBACK-ENGINE.md:88 lacks strict implementation and automated-test proof | @@ -464,7 +464,7 @@ Every row below is one current `incomplete` requirement from the normative ledge | media-render-playback-export | `doc-e36487856d25a4d1` | docs/specs/media/2-ffmpeg.md:30 | At docs/specs/media/2-ffmpeg.md:30 under “2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座)” (heading), the source “## 2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座)” requires this exact behavior: The named FFmpeg probe/decode/PCM/encode contract is implemented with integration fixtures. | Source binding: docs/specs/media/2-ffmpeg.md:30; signal=heading; heading=2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座); candidate=## 2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座); Expected behavior: The named FFmpeg probe/decode/PCM/encode contract is implemented with integration fixtures. This closes only the promise expressed by “2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座)” in “2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座)” with the scenario below and register test:crates/opentake-render/tests/completion_e36487856d25a4d1.rs#completion_e36487856d25a4d1_the_named_ffmpeg_probe_decode_pcm_encode_contrac; Initial state/input/event: load a deterministic local media fixture with fixed dimensions, frame rate, timestamps, tracks, and the parameters stated by “2.2 解一帧 `decode_frame_at`(缩略图/采样/取帧共用底座)”, then invoke the named preview, playback, render, or export event.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, route “The named FFmpeg probe/decode/PCM/encode contract is implemented with integration fixtures.” through the real decode/composition/audio/export pipeline with no placeholder success and without mutating source media.; Visible/returned assertion: assert the exact returned status plus deterministic frame/audio/container properties or typed unsupported/error output, and verify preview/export parity when both paths are named.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-render/tests/completion_e36487856d25a4d1.rs#completion_e36487856d25a4d1_the_named_ffmpeg_probe_decode_pcm_encode_contrac. | active-gap:media-render-playback-export: docs/specs/media/2-ffmpeg.md:30 lacks strict implementation and automated-test proof | | media-render-playback-export | `doc-e423ae30effded27` | docs/architecture/CAPCUT-GAP.md:39 | Optical-flow interpolation produces deterministic preview/export frames. | Add an optical-flow interpolation mode with explicit source/target frame-rate and fallback policy in the render model.; Convert a 24 fps motion fixture to 60 fps with exactly the expected output-frame count and unchanged first/last timestamps.; Add pixel/temporal regression tests plus a deterministic unsupported-device fallback; preview and export must select the same interpolation mode. | open-gap:acceptance-criteria-required | | media-render-playback-export | `doc-ed457b544b875446` | docs/architecture/CAPCUT-GAP.md:109 | Transitions are editable and render with preview/export parity. | Persist a transition with kind, duration, and both adjacent clip IDs, rejecting overlaps longer than either available handle.; Expose add/change/remove transition actions in the enabled transition surface with undo/redo.; For cut, midpoint, and end frames of each advertised transition, assert preview/export pixels match and save/reopen preserves the transition exactly. | open-gap:acceptance-criteria-required | -| media-render-playback-export | `doc-ee5f3f6b2ccb9cc6` | docs/modules/opentake-media/OVERVIEW.md:55 | At docs/modules/opentake-media/OVERVIEW.md:55 under “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)” (gap-marker), the source “**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” requires this exact behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. | Source binding: docs/modules/opentake-media/OVERVIEW.md:55; signal=gap-marker; heading=FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差); candidate=**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。; Expected behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. This closes only the promise expressed by “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” in “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” with the scenario below and register test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t; Initial state/input/event: create an isolated temporary project fixture representing the source state in “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t. | active-gap:media-render-playback-export: docs/modules/opentake-media/OVERVIEW.md:55 lacks strict implementation and automated-test proof | +| media-render-playback-export | `doc-ee5f3f6b2ccb9cc6` | docs/modules/opentake-media/OVERVIEW.md:55 | At docs/modules/opentake-media/OVERVIEW.md:55 under “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)” (gap-marker), the source “**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../modules/opentake-media/probe-ff.md)。” requires this exact behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. | Source binding: docs/modules/opentake-media/OVERVIEW.md:55; signal=gap-marker; heading=FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差); candidate=**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../modules/opentake-media/probe-ff.md)。; Expected behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. This closes only the promise expressed by “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../modules/opentake-media/probe-ff.md)。” in “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../modules/opentake-media/probe-ff.md)。” with the scenario below and register test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t; Initial state/input/event: create an isolated temporary project fixture representing the source state in “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../modules/opentake-media/probe-ff.md)。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t. | active-gap:media-render-playback-export: docs/modules/opentake-media/OVERVIEW.md:55 lacks strict implementation and automated-test proof | | media-render-playback-export | `doc-ef91c43e6cb92a53` | docs/architecture/PLAYBACK-ENGINE.md:117 | Playback/export is release-ready across packaged macOS and Windows with all declared capabilities rendered or explicitly rejected. | Complete installed-app export UI artifact and packaged FFmpeg/sidecar validation.; Close Windows WebView2/CSP/sidecar and signing/notarization acceptance.; Implement or fail closed for Lottie, polygon masks, unsupported effects, composited reverse/speed, and complete ProRes/A/V device probes. | open-gap:acceptance-criteria-required | | media-render-playback-export | `doc-f06463b86285d17a` | docs/specs/media/2-ffmpeg.md:1 | FFmpeg is resolved as a verified bundled sidecar in packaged macOS and Windows builds. | Package target-specific FFmpeg/ffprobe binaries and verify checksum/version.; Resolve packaged sidecar paths without relying on developer PATH.; Run installed-app probe/decode/encode smoke tests on macOS and Windows. | open-gap:acceptance-criteria-required | | media-render-playback-export | `doc-f2992815147bed14` | docs/modules/opentake-project/OVERVIEW.md:110 | At docs/modules/opentake-project/OVERVIEW.md:110 under “移植铁律(本模块重点)” (gap-marker), the source “1. **serde 向后兼容是第一铁律**:所有序列化模型加 `#[serde(default)]` + `Option<T>`,保证**读旧工程不破坏**。新增字段必须有缺省值;缺失键降级而非报错。`media.json` 的 `version` 缺省按 1(结构体默认 2,但缺省回退 1),`generation-log.json` 的 `version` 默认与回退**都是 1**——二者不同,别混。” requires this exact behavior: Decode persisted project/domain models compatibly, defaulting absent optional fields and preserving explicit version migrations. | Source binding: docs/modules/opentake-project/OVERVIEW.md:110; signal=gap-marker; heading=移植铁律(本模块重点); candidate=1. **serde 向后兼容是第一铁律**:所有序列化模型加 `#[serde(default)]` + `Option<T>`,保证**读旧工程不破坏**。新增字段必须有缺省值;缺失键降级而非报错。`media.json` 的 `version` 缺省按 1(结构体默认 2,但缺省回退 1),`generation-log.json` 的 `version` 默认与回退**都是 1**——二者不同,别混。; Expected behavior: Decode persisted project/domain models compatibly, defaulting absent optional fields and preserving explicit version migrations. This closes only the promise expressed by “1. **serde 向后兼容是第一铁律**:所有序列化模型加 `#[serde(default)]` + `Option<T>`,保证**读旧工程不破坏**。新增字段必须有缺省值;缺失键降级而非报错。`media.json` 的 `version` 缺省按 1(结构体默认 2,但缺省回退 1),`generation-log.json` 的 `version` 默认与回退**都是 1**——二者不同,别混。” in “移植铁律(本模块重点)”; adjacent headings or similarly named features remain independently adjudicated.; Deterministic test: exercise “1. **serde 向后兼容是第一铁律**:所有序列化模型加 `#[serde(default)]` + `Option<T>`,保证**读旧工程不破坏**。新增字段必须有缺省值;缺失键降级而非报错。`media.json` 的 `version` 缺省按 1(结构体默认 2,但缺省回退 1),`generation-log.json` 的 `version` 默认与回退**都是 1**——二者不同,别混。” with the scenario below and register test:crates/opentake-agent/tests/completion_f2992815147bed14.rs#completion_f2992815147bed14_decode_persisted_project_domain_models_compatibl; Initial state/input/event: create an isolated temporary project fixture representing the source state in “1. **serde 向后兼容是第一铁律**:所有序列化模型加 `#[serde(default)]` + `Option<T>`,保证**读旧工程不破坏**。新增字段必须有缺省值;缺失键降级而非报错。`media.json` 的 `version` 缺省按 1(结构体默认 2,但缺省回退 1),`generation-log.json` 的 `version` 默认与回退**都是 1**——二者不同,别混。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable.; Code/store/API/Rust effect: at the Rust Agent/MCP dispatch, typed argument boundary, and any named Tauri/provider bridge, apply “Decode persisted project/domain models compatibly, defaulting absent optional fields and preserving explicit version migrations.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata.; Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result.; Evidence required: after the deterministic test passes, record code:<tracked-file>#<declared-symbol> and test:<tracked-test-file>#<exact-test-name>; proposed concrete evidence is test:crates/opentake-agent/tests/completion_f2992815147bed14.rs#completion_f2992815147bed14_decode_persisted_project_domain_models_compatibl. | active-gap:media-render-playback-export: docs/modules/opentake-project/OVERVIEW.md:110 lacks strict implementation and automated-test proof | diff --git a/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-design.md b/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-design.md index cc6fa6ed..e39199aa 100644 --- a/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-design.md +++ b/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-design.md @@ -901,8 +901,8 @@ Close each record as the smallest end-to-end vertical slice while preserving Rus - `crates/opentake-agent/src/tools/short_id.rs#expand_unique_prefix_resolves_to_full` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. - `crates/opentake-agent/src/tools/short_id.rs#shared_prefix_extends_until_unique` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. - `crates/opentake-agent/src/tools/short_id.rs#unique_id_shortens_to_floor` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. -- Expected behavior: At docs/modules/opentake-agent/prompt.md:32 under “base.rs:分段 base 提示” (gap-marker), the source “- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” requires this exact behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. -- Acceptance criteria: Source binding: docs/modules/opentake-agent/prompt.md:32; signal=gap-marker; heading=base.rs:分段 base 提示; candidate=- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效) Expected behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. This closes only the promise expressed by “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” in “base.rs:分段 base 提示”; adjacent headings or similarly named features remain independently adjudicated. Deterministic test: exercise “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” with the scenario below and register test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl Initial state/input/event: create an isolated temporary project fixture representing the source state in “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. Code/store/API/Rust effect: at the Rust Agent/MCP dispatch, typed argument boundary, and any named Tauri/provider bridge, apply “Tell the model to pass shortened IDs back exactly without padding or guessing.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl. +- Expected behavior: At docs/modules/opentake-agent/prompt.md:32 under “base.rs:分段 base 提示” (gap-marker), the source “- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)” requires this exact behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. +- Acceptance criteria: Source binding: docs/modules/opentake-agent/prompt.md:32; signal=gap-marker; heading=base.rs:分段 base 提示; candidate=- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效) Expected behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. This closes only the promise expressed by “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)” in “base.rs:分段 base 提示”; adjacent headings or similarly named features remain independently adjudicated. Deterministic test: exercise “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)” with the scenario below and register test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl Initial state/input/event: create an isolated temporary project fixture representing the source state in “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. Code/store/API/Rust effect: at the Rust Agent/MCP dispatch, typed argument boundary, and any named Tauri/provider bridge, apply “Tell the model to pass shortened IDs back exactly without padding or guessing.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl. ### requirement-58cdfb5cd8505898 diff --git a/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-implementation.md b/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-implementation.md index 81f8aca3..0a26d127 100644 --- a/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-implementation.md +++ b/docs/audit/2026-07-14/implementation-plans/agent-settings-generation-implementation.md @@ -1881,13 +1881,13 @@ and frame-sequence sources remain separately scoped post-Beta work. #### requirement-b3c9690870310f25 - Candidate/source: `doc-9bc7c9cdadaaa081` at `docs/modules/opentake-agent/prompt.md:32` (requirement) -- Expected behavior: At docs/modules/opentake-agent/prompt.md:32 under “base.rs:分段 base 提示” (gap-marker), the source “- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” requires this exact behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. +- Expected behavior: At docs/modules/opentake-agent/prompt.md:32 under “base.rs:分段 base 提示” (gap-marker), the source “- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)” requires this exact behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. - Resolution: `validated-ledger-evidence:AG-short-id-prompt-contract` — Both slices cover one no-padding/no-guessing short-ID prompt and resolver contract with the same implementation and focused tests. - Exact acceptance contract: - - Source binding: docs/modules/opentake-agent/prompt.md:32; signal=gap-marker; heading=base.rs:分段 base 提示; candidate=- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效) - - Expected behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. This closes only the promise expressed by “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” in “base.rs:分段 base 提示”; adjacent headings or similarly named features remain independently adjudicated. - - Deterministic test: exercise “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)” with the scenario below and register test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl - - Initial state/input/event: create an isolated temporary project fixture representing the source state in “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](dispatch-tools.md) 契约失效)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. + - Source binding: docs/modules/opentake-agent/prompt.md:32; signal=gap-marker; heading=base.rs:分段 base 提示; candidate=- 短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效) + - Expected behavior: Tell the model to pass shortened IDs back exactly without padding or guessing. This closes only the promise expressed by “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)” in “base.rs:分段 base 提示”; adjacent headings or similarly named features remain independently adjudicated. + - Deterministic test: exercise “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)” with the scenario below and register test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl + - Initial state/input/event: create an isolated temporary project fixture representing the source state in “短 id:`Pass them back exactly as given — never pad, complete, or guess a longer form.`(缺失则 [short_id](../../../modules/opentake-agent/dispatch-tools.md) 契约失效)”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. - Code/store/API/Rust effect: at the Rust Agent/MCP dispatch, typed argument boundary, and any named Tauri/provider bridge, apply “Tell the model to pass shortened IDs back exactly without padding or guessing.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. - Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. - Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-agent/tests/completion_9bc7c9cdadaaa081.rs#completion_9bc7c9cdadaaa081_tell_the_model_to_pass_shortened_ids_back_exactl. diff --git a/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-design.md b/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-design.md index d2b00671..eb083065 100644 --- a/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-design.md +++ b/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-design.md @@ -427,8 +427,8 @@ Close each record as the smallest end-to-end vertical slice while preserving Rus - Test ownership: - `crates/opentake-media/src/ff.rs#env_override_is_respected_for_ffmpeg` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. - `crates/opentake-media/src/ff.rs#default_ffprobe_is_ffprobe` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. -- Expected behavior: At docs/modules/opentake-media/OVERVIEW.md:55 under “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)” (gap-marker), the source “**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” requires this exact behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. -- Acceptance criteria: Source binding: docs/modules/opentake-media/OVERVIEW.md:55; signal=gap-marker; heading=FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差); candidate=**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。 Expected behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. This closes only the promise expressed by “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” in “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)”; adjacent headings or similarly named features remain independently adjudicated. Deterministic test: exercise “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” with the scenario below and register test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t Initial state/input/event: create an isolated temporary project fixture representing the source state in “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t. +- Expected behavior: At docs/modules/opentake-media/OVERVIEW.md:55 under “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)” (gap-marker), the source “**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。” requires this exact behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. +- Acceptance criteria: Source binding: docs/modules/opentake-media/OVERVIEW.md:55; signal=gap-marker; heading=FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差); candidate=**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。 Expected behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. This closes only the promise expressed by “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。” in “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)”; adjacent headings or similarly named features remain independently adjudicated. Deterministic test: exercise “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。” with the scenario below and register test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t Initial state/input/event: create an isolated temporary project fixture representing the source state in “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t. ### requirement-d7bd977b01924877 @@ -561,8 +561,8 @@ Close each record as the smallest end-to-end vertical slice while preserving Rus - Test ownership: - `crates/opentake-motion/src/sandbox.rs#document_size_ceiling_enforced` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. - `crates/opentake-motion/src/renderer.rs#chromium_applies_sandbox_size_before_unavailable` (existing-owned): Exact named test already exists in the reviewed owning runner and records current boundary behavior. -- Expected behavior: At docs/modules/opentake-motion/sandbox.md:67 under “谁在调用” (gap-marker), the source “- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” requires this exact behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. -- Acceptance criteria: Source binding: docs/modules/opentake-motion/sandbox.md:67; signal=gap-marker; heading=谁在调用; candidate=- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。 Expected behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. This closes only the promise expressed by “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” in “谁在调用”; adjacent headings or similarly named features remain independently adjudicated. Deterministic test: exercise “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” with the scenario below and register test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s Initial state/input/event: create an isolated temporary project fixture representing the source state in “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s. +- Expected behavior: At docs/modules/opentake-motion/sandbox.md:67 under “谁在调用” (gap-marker), the source “- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。” requires this exact behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. +- Acceptance criteria: Source binding: docs/modules/opentake-motion/sandbox.md:67; signal=gap-marker; heading=谁在调用; candidate=- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。 Expected behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. This closes only the promise expressed by “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。” in “谁在调用”; adjacent headings or similarly named features remain independently adjudicated. Deterministic test: exercise “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。” with the scenario below and register test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s Initial state/input/event: create an isolated temporary project fixture representing the source state in “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s. ### requirement-3c0728a9184da517 diff --git a/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-implementation.md b/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-implementation.md index 5bddd852..d2c2e1e4 100644 --- a/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-implementation.md +++ b/docs/audit/2026-07-14/implementation-plans/media-render-playback-export-implementation.md @@ -1341,13 +1341,13 @@ preceding unchanged-code workspace gate passed in Task16. #### requirement-e71abff7d9b41127 - Candidate/source: `doc-ee5f3f6b2ccb9cc6` at `docs/modules/opentake-media/OVERVIEW.md:55` (requirement) -- Expected behavior: At docs/modules/opentake-media/OVERVIEW.md:55 under “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)” (gap-marker), the source “**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” requires this exact behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. +- Expected behavior: At docs/modules/opentake-media/OVERVIEW.md:55 under “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)” (gap-marker), the source “**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。” requires this exact behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. - Resolution: `reviewed-mapping-report:MR-cli-sidecar-boundary-complete` — The media crate deliberately uses ffmpeg-sidecar and does not link a libav ABI binding. - Exact acceptance contract: - - Source binding: docs/modules/opentake-media/OVERVIEW.md:55; signal=gap-marker; heading=FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差); candidate=**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。 - - Expected behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. This closes only the promise expressed by “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” in “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)”; adjacent headings or similarly named features remain independently adjudicated. - - Deterministic test: exercise “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。” with the scenario below and register test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t - - Initial state/input/event: create an isolated temporary project fixture representing the source state in “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](probe-ff.md)。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. + - Source binding: docs/modules/opentake-media/OVERVIEW.md:55; signal=gap-marker; heading=FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差); candidate=**实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。 + - Expected behavior: Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding. This closes only the promise expressed by “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。” in “FFmpeg sidecar 编解码(与 SPEC 的关键实现偏差)”; adjacent headings or similarly named features remain independently adjudicated. + - Deterministic test: exercise “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。” with the scenario below and register test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t + - Initial state/input/event: create an isolated temporary project fixture representing the source state in “*实现走 `ffmpeg` / `ffprobe` 命令行二进制(ffmpeg-sidecar),不链接 `libav*`。** 原因:本机工具链为 ffmpeg 8.1(libavcodec 62),C 绑定 crate(`ffmpeg-next` / `ffmpeg-the-third`)不支持,且 `pkg-config` 缺失。`ff.rs` 封装二进制发现与一次性 ffprobe JSON 查询,上层解码模块用裸 stdin/stdout 管道交换原始像素/PCM。这与多处架构文档([ARCHITECTURE.md](../../../architecture/ARCHITECTURE.md) §1、[ROADMAP.md](../../../architecture/ROADMAP.md) Phase 2、本模块 [SPEC.md](../../../modules/opentake-media/SPEC.md) §1.2 仍写 `ffmpeg-next`)存在偏差——以代码为准,详见 [probe-ff.md](../../../modules/opentake-media/probe-ff.md)。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. - Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Use the ffmpeg/ffprobe sidecar boundary rather than a libav ABI binding.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. - Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. - Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-project/tests/completion_ee5f3f6b2ccb9cc6.rs#completion_ee5f3f6b2ccb9cc6_use_the_ffmpeg_ffprobe_sidecar_boundary_rather_t. @@ -1691,13 +1691,13 @@ not the separately owned desktop motion/Lottie materialization. #### requirement-69723d1c0bd77577 - Candidate/source: `doc-3a71829a7b489aae` at `docs/modules/opentake-motion/sandbox.md:67` (requirement) -- Expected behavior: At docs/modules/opentake-motion/sandbox.md:67 under “谁在调用” (gap-marker), the source “- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” requires this exact behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. +- Expected behavior: At docs/modules/opentake-motion/sandbox.md:67 under “谁在调用” (gap-marker), the source “- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。” requires this exact behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. - Resolution: `reviewed-mapping-report:MR-motion-sandbox-complete` — Both stub and unavailable Chromium paths enforce document size before rendering or returning unavailable. - Exact acceptance contract: - - Source binding: docs/modules/opentake-motion/sandbox.md:67; signal=gap-marker; heading=谁在调用; candidate=- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。 - - Expected behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. This closes only the promise expressed by “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” in “谁在调用”; adjacent headings or similarly named features remain independently adjudicated. - - Deterministic test: exercise “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。” with the scenario below and register test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s - - Initial state/input/event: create an isolated temporary project fixture representing the source state in “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](renderer.md))。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. + - Source binding: docs/modules/opentake-motion/sandbox.md:67; signal=gap-marker; heading=谁在调用; candidate=- `StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。 + - Expected behavior: Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path. This closes only the promise expressed by “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。” in “谁在调用”; adjacent headings or similarly named features remain independently adjudicated. + - Deterministic test: exercise “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。” with the scenario below and register test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s + - Initial state/input/event: create an isolated temporary project fixture representing the source state in “`StubRenderer` 与 `HeadlessChromiumRenderer` 都在 `render()` 里对 `MotionSource::Code` 调 `check_document_size`——连 stub 与 "renderer unavailable" 路径都不放过(见 [renderer.md](../../../modules/opentake-motion/renderer.md))。”, perform the named open/edit/save/reopen event, and include its absent, malformed, and existing-file boundary where applicable. - Code/store/API/Rust effect: at the Rust media/render/playback pipeline and its typed preview/export adapter, apply “Apply sandbox document-size checks before both stub rendering and the unavailable Chromium path.” atomically in the Rust project/core layer, changing only the named project files and in-memory session state while preserving unrelated assets and metadata. - Visible/returned assertion: assert the returned project/error variant, exact post-operation files and decoded state, and save-then-reopen equality or the specified fail-closed no-write result. - Evidence required: after the deterministic test passes, record code:# and test:#; proposed concrete evidence is test:crates/opentake-project/tests/completion_3a71829a7b489aae.rs#completion_3a71829a7b489aae_apply_sandbox_document_size_checks_before_both_s. diff --git a/docs/audit/2026-08-21/full-desktop-functional-matrix.md b/docs/audit/2026-08-21/full-desktop-functional-matrix.md new file mode 100644 index 00000000..c81586c6 --- /dev/null +++ b/docs/audit/2026-08-21/full-desktop-functional-matrix.md @@ -0,0 +1,168 @@ +--- +id: audit.desktop-functional-matrix.2026-08-21 +title: OpenTake Desktop Functional Matrix — 2026-08-21 +summary: 记录安装版 OpenTake 在约 1331×768 小屏上的首轮真实 UI 巡检,以及后续模块化回归的入口和判定规则。 +kind: audit +status: draft +content_stage: partial-implementation +scope: + - packaged-macos-app + - home + - editor + - timeline + - preview + - media + - settings + - agent + - motion +triggers: + - 真机验收 + - 屏幕过大 + - 时间轴回归 + - 预览回归 +read_when: + - 修改窗口几何或桌面 UI + - 修改时间轴、预览、媒体导入、设置、Agent、Motion +skip_when: + - 仅修改零 IO 的 domain 算法且不改变 UI 合同 +priority: must +freshness_class: project +last_verified: 2026-08-22T01:03:46+08:00 +owners: + - OpenTake-generation +source_of_truth: + - ../../src/ + - ../../web/src/ +related: + prerequisites: + - ../../superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md + next: + - ../../capabilities/CAPABILITY-LEDGER.md +supersedes: [] +tags: + - audit + - desktop + - ui + - playback +--- + +# 首轮真实巡检 + +## 环境 + +- 安装应用:`/Applications/OpenTake.app` +- 观测方式:Computer Use `@oai/sky`,每次动作后重新获取 AX tree 和截图。 +- 截图逻辑尺寸:首轮首页约 `1331×768`;切换紧凑窗口后约 `1066×666`。 +- 本轮没有改上游、没有永久删除文件、没有发送 Agent 消息;导出测试使用新文件名,未覆盖既有 QA 产物。 + +## 布局与窗口 + +| 场景 | 期望 | 实际 | 状态 | 证据 | +|---|---|---|---|---| +| Home / 标准档 / 小屏 | 不超过工作区,内容有合理最大宽度 | 最新包标准档 Home 截图无白边、卡片不溢出;标准档工作区完整可见;浏览器 fallback 在 1280×720 视口实测 bodyScrollWidth/Height 与视口一致,无横向/纵向溢出 | 通过(窗口)/部分(Home 内容密度) | `src-tauri/tauri.conf.json`;`web/src/store/settingsStore.ts`;Task 2 5 files / 50 tests;最新包 Computer Use 截图;Browser fallback DOM metrics | +| 设置 → 外观 → 紧凑 | 窗口收缩且内容仍可操作 | 最新包切换后约 `1066×666`,紧凑 radio 状态正确,Home、时间线、预览、Inspector 和 Motion Studio 均可见 | 通过 | `settingsStore` + Home/SplitPane/Settings 50 tests;最新包 Computer Use 紧凑档截图 | +| Home → 设置 | 设置面板可见、可操作 | 复现时曾抓到 AX 已更新但截图未重绘;关闭/重开后及当前复测设置面板可见,判定为 Computer Use 重绘时序风险,不先当产品黑屏根因 | 部分 | 当前复测 AX + 设置截图 | + +## 功能场景 + +| 模块 | 场景 | 实际结果 | 状态 | 下一步 | +|---|---|---|---|---| +| Home | 新建项目 | 原生保存 sheet 可打开,可取消 | 通过 | 在 QA fixture 中补成功落盘/重开 | +| Home | 打开项目 | 原生打开 sheet 可打开,可取消 | 通过 | 用复制的 QA 工程补成功打开 | +| Home | 示例项目 | `产品演示` 可进入编辑器,AX 可读完整结构 | 通过 | 保存真实截图和工程路径 | +| Media | 导入文件 | 新构建 app 中 MP4、PNG、双文件 Cmd 多选后 Open 均可用,导入数从 1→3 | 通过 | `fix(media): make native file import selectable`;Computer Use 原生 dialog;`mediaActions`/`MediaPanel` 59 tests | +| Media | 导入文件夹 | 选择 `editor-core-after-fix-assets` 后媒体库出现目录,AX 提示已跳过 64 个 unsupported 文件 | 通过 | 新构建 app Computer Use 12:xx;文件夹 dialog 未改 filters | +| Media | 文件夹 / 平铺 / 分组 | 安装版 `媒体组织方式` 菜单可访问;真实文件夹导入后文件夹模式显示目录卡片,平铺模式显示 37 个媒体且隐藏目录卡片,分组模式显示“全部 / editor-core-after-fix-assets”两组;分组网格/列表均可切换 | 通过 | `mediaViewModes` + `MediaPanel` 64 tests;安装版 Computer Use 13:xx;二次 code review Ready | +| Media | Relink | 选择原始 MP4 后离线媒体恢复,Preview tab 和 Inspector 路径更新 | 通过 | 新构建 app packaged relink 验收 | +| Media | 素材预览 | 导入后真实 MP4 缩略图/画面可见,PNG 也能作为 source preview 打开 | 通过 | Computer Use:素材 tab、Preview tab、Inspector 来源/尺寸/路径 | +| Media | Lottie JSON / `.lottie` 导入 | 普通媒体入口和 MCP path 入口现在接受 `.json` / `.lottie`;Velato 校验 JSON,ZIP 容器读取 `animations/*.json`,导入后保存宽高、帧率和时长;坏 Lottie 在批量入口跳过、MCP 单文件返回明确错误;安装版 JSON 卡片、素材预览、双击落轨和最近项目重开均通过;`.lottie` 容器仍以自动化为主 | 部分(JSON 屏幕 + 容器代码/自动化) | 补 `.lottie` 容器安装版落轨/重开对拍 | +| Media | 主标签与 OpenTake 扩展占位 | 对照上游 `MediaPanelView.swift`,上游真实面板只有 Media/Captions/Music;OpenTake 当前额外显示 Text/Sticker/Effect/Transition/Smart Pack。Text 已接入现有 `addTextClip()` 可撤销命令并提供“添加文本”面板;Effect 已接入 grayscale/sepia/invert 预设并追加到当前片段效果链;最新包普通音视频 QA 中 linked V1/A1 选择后三个效果按钮可见且启用,但点击后 AX 未观察到撤销状态,截图同时出现全黑,故不升级为完整通过;Sticker 仍是置灰占位 | 部分(Text 自动化通过;Effect 代码/入口通过,实机提交待复验;Sticker 未实现) | 解锁后复测 Effect 实际提交/Undo;继续为 Sticker 建立明确实现切片 | +| Timeline | 播放/暂停 | 播放头从 0 推进到约 54,时间从 00:00:00 推进到约 00:01:24 | 通过 | 增加暂停/恢复/seek/尾帧证据 | +| Timeline | 选中片段 | 选中 `sample-text-0` 后 Inspector 切换到文本属性 | 通过 | 补选区、拖拽、删除和 undo | +| Timeline | 播放头处分割 | 初始在帧 0 或片段外尝试时无变化;补齐有效前置条件(选中 `sample-text-0`、播放头推进到帧 15)后成功新增片段、撤销变可用 | 通过(有效前置条件) | 新构建 app AX:分割后出现 UUID 片段;`cargo test -p opentake-ops split_clip_distributes_keyframes_at_cut` 通过 | +| Timeline | 入点/出点范围与范围边缘 | `TimelineContainer` 已接入 Shift+标尺 range mark、已有范围 start/end 边缘命中与拖动、clip-edge/playhead snap、pointercancel 回滚和同帧清除;Option trim 改为只修剪主 clip,普通 trim 保持 linked propagation;安装版普通右边缘修剪让联动 V1/A1 从 07:21 变为 07:04,Undo 恢复;最新包按上游顺序完成 I/O 标记整段范围→再选 V1 锚点→Shift+Backspace,V1/A1 联动删除、Motion 保留,删除后播放头从 7.7s 合法收敛到 3.0s,Undo 恢复三条轨道;Web 全量 152 files / 1425 tests 通过 | 通过(普通 trim、范围删除/Undo/播放头屏幕对拍;Option 修饰键仍 partial) | 继续补可复现的安装版 Option trim;桌面驱动器不提供按住 Option 拖拽接口,altKey 自动化测试已覆盖 | +| Inspector | 片段属性与 AI 编辑 | 选中 V1 后视频 Inspector 展开 131 项,覆盖变换、裁剪、翻转、淡入淡出、运动追踪、防抖、RVM 抠像、速度和调色;水平翻转可切换并由 Undo 恢复;音频页能显示音量/响度/降噪/人声分离;AI 编辑页能生成本地建议,应用后显示“可撤销编辑命令”,撤销成功 | 通过(入口/可撤销行为) | 数值控件逐项边界、关键帧落盘重开和本地模型分析仍待 | +| Preview | 带音频 fixture 的导入/波形/seek/暂停与增益 | `nested-timeline-compound-export-2026-07-31.mp4`:7.700s、H.264 1280×720/30fps/231帧 + AAC 48kHz 单声道;安装版出现 V1+A1、A1 波形和音频 Inspector,已操作起点/中点/终点 seek,连续暂停状态稳定;WebKit 预览新增 GainNode 路由,>0 dB 增益不再被 HTMLMediaElement.volume 截断,预览相关 58 tests 与 Web 全量 1423 tests 通过 | 部分 | Mac 解锁后补实时听感级同步、最新包增益听感和 cleanup 包导出复测 | +| Preview | compositor temporal 预览 | 最新安装版在 QA 工程中设置 `speed=1.5` 和曝光 `0.50` compositor 属性后不再出现 unsupported surface;画面可见,播放头可到尾帧,暂停后抓帧按钮保持可用;speed/reversed 的首次 native render、JPEG publication、source-frame 映射由 Rust integration tests 锁定 | 通过(代码/native/屏幕组合证据) | 预览与导出起/中/尾帧、音频同步和取消仍在后续闭环 | +| Preview | Lottie 时间轴合成与播放 | 修复前端 route 将 Lottie 误判为 unsupported 的缺口;Rust playback resolver 已有 `TextureSource::Lottie`,现在安装版时间轴走 native surface,02:40 首帧真实显示 Lottie 画面,播放头推进到 02:50,播放/截帧按钮可用,重启后最近项目仍保留落轨状态;路由/Preview 定向 20 files / 196 tests、Web 全量 152 files / 1425 tests 通过 | 通过(JSON 安装版屏幕;`.lottie` 容器屏幕仍待) | 补 `.lottie` 容器屏幕对拍,并继续验证 Lottie 与视频叠加/导出一致性 | +| Preview | 多素材 tabs | 打开 `audit-4k60-h264` 与 `export-h264-frame30` 后显示 Timeline/两个媒体 tab;关闭第二个回退第一个 | 通过 | 安装版 AX:两个 Close 按钮;Preview slice 4 files / 91 tests | +| Agent | 面板、对话标签和本地边界 | Agent 面板可打开;新建/关闭对话标签成功;无配置通道时输入框存在但发送按钮禁用;用 `⌘⌥A` 可收起并恢复编辑器布局 | 通过(入口/本地边界) | 不发送外部消息;MCP 握手、失败/取消、真实工具调用仍待 | +| Agent / MCP | 文件夹批量工具 | `create_folder.entries` 返回创建出的 folder records,`move_to_folder.entries` 支持多个资产到不同目录;两者都通过单一 EditCommand 事务和单步 Undo,417 个 Agent lib 测试全绿 | 通过(代码/自动化) | 屏幕层无独立入口;继续用安装包 Agent/MCP 本地工具调用做一次不发送外部消息的验收 | +| Subtitles | 字幕入口与导出 | 字幕页可打开,来源/样式/位置/翻译同意项均可见;点击生成字幕后明确提示需下载约 141 MB 的 multilingual 转写模型,并提供“下载模型”入口;未同意外部 Provider 时翻译按钮保持禁用;安装版 SRT 与 VTT 原生 SavePanel 分别带出 `.srt` / `.vtt` 扩展并可取消;domain 字幕格式 16/16、Tauri 实际 SRT/VTT 写文件 5/5、Captions/TitleBar 前端 10/10 通过 | 部分(入口/SavePanel/失败边界/代码自动化) | 补模型下载、转写结果编辑和真实字幕文件写出;翻译真实闭环仍受 provider 凭据边界限制 | +| Motion | Studio 入口 | HTML/CSS 编辑器、预览、参数检查器、关键帧时间线出现;播放帧推进 | 通过(既有桌面证据) | 用最新包补发布、保存重开和导入时间线 | +| Motion | 透明发布与透明导出 | Motion Studio Inspector 可点击“透明背景(ProRes 4444)”;真实发布进度出现,完成后素材库新增 Motion Graphic、时间线新增 V2 片段,预览/Inspector 均可见;QA 工程内真实媒体叶子为 ProRes 4444 `ap4h` / `yuva444p12le`,90 帧/3.000s;新增导出面板 `ProRes 4444 透明 / .mov`,Rust GPU/FFmpeg integration 验证透明清屏、ProRes、alpha 平面 0→非零;manifest 的 `transparent: true` 与项目媒体引用一致 | 部分(发布屏幕/文件通过;透明专用导出屏幕因 Mac 锁屏待复验) | 解锁后补透明专用导出 SavePanel/状态/文件屏幕对拍;opaque clip 编辑时切换透明仍待 | +| Export | 导出面板 | 导出视频面板可打开,格式/分辨率/取消/导出可见 | 通过 | 复制目标路径后完成导出并 ffprobe | +| Export | temporal 视频实际导出 | 修复 SavePanel filters 后,原生 Save 按钮可用;H.264 temporal QA 导出成功,应用显示 3840×2160 / 160 帧,ffprobe 显示 H.264、60fps、2.666667s | 通过(H.264 视频) | H.265/ProRes、带音频/字幕、取消和 preview/export 像素对拍仍待矩阵化 | +| Export | 带音频 H.264/AAC 实际导出 | 最新包 `918eac84…9551b` 通过 SavePanel 导出 `/private/tmp/opentake-audio-desktop-qa-L8Nvwh/opentake-latest-smoke.mp4`;ffprobe:H.264 1280×720/30fps/231 帧、AAC 48kHz 单声道/362 包、7.700s;完整 `ffmpeg -xerror` 通过 | 通过(最新包) | H.265/ProRes、字幕、取消和实时音画同步仍待矩阵化 | +| Export | 带音频 H.265/HEVC 实际导出 | 最新包 `893b6ed0…d0ba` 通过视频导出面板选择 `H.265 / .mp4`,SavePanel 保存 `/private/tmp/opentake-audio-desktop-qa-L8Nvwh/audio-preview-export-qa.mp4`;ffprobe:HEVC `hev1` Main、1280×720/30fps/231 帧、AAC 48kHz/362 包、7.700s;完整 `ffmpeg -xerror` 通过;安装版状态显示“导出完成 · 1280×720 · 231 帧” | 通过(屏幕/文件) | 取消、首中尾帧对拍和实时音画同步仍待 | +| Export | 带音频 ProRes 422 HQ 实际导出 | 最新包选择 `ProRes 422 / .mov` 后 SavePanel 保存 `/private/tmp/opentake-audio-desktop-qa-L8Nvwh/audio-preview-export-qa.mov`;ffprobe:ProRes HQ `apch`、`yuv422p10le`、1280×720/30fps/231 帧、PCM `sowt` 16-bit、7.700s;完整 `ffmpeg -xerror` 通过;应用返回编辑器 | 通过(屏幕/文件) | 透明 ProRes 4444 专用导出、取消和首中尾帧对拍仍待 | +| Export | 失败/取消输出清理 | `0bae80e` 增加普通输出 identity-safe guard、replacement race、双 cancel source fail-closed;`a7d98d6` 增加 symlink/reparse 最终路径拒绝;本轮又修复前端 progress listener 尚未返回时的 cancel race,避免取消意图被静默丢弃;export unit 72/72、media cancel 18/18、Shell export 39/39;安装版短工程新文件名导出完成并显示 `1280×720 · 160 帧`,但渲染期间桌面状态暂时无响应,未能点到取消按钮,也未覆盖既有文件 | 部分(代码/自动化 + 导出完成屏幕) | 需要可交互的长渲染取消场景,确认 UI toast 和部分输出文件清理;不把自动化取消证据当成桌面取消证据 | +| Help | 菜单 | 快捷键/MCP 说明可展开;教程/反馈显示 Beta 禁用 | 通过 | 记录禁用原因和可用边界 | +| Library | 全局素材库 | AX 可读分类、搜索、排序;部分截图曾未重绘,当前复测仍需重复 | 部分 | 在窗口修复后重跑可见性和空态 | + +## 初始回归顺序 + +1. 先完成窗口尺寸裁剪和小屏布局,避免大窗口影响所有 UI 判断。 +2. 修复/验证媒体导入状态,准备视频、音频、图片、文字和字幕 fixture。(文件/文件夹/relink 已通过,音频独立预览仍待补。) +3. 修复时间轴分割命令的真实选区/命令链路。(有效前置条件已通过,拖拽/删除/复杂音视频仍待补。) +4. 重跑预览、播放、Inspector、导出和保存重开。 +5. Media folder/flat/grouped 已通过;继续验 Motion alpha、Settings taxonomy 和 generation/account envelope。 + +## 证据边界 + +AX tree 只能证明节点存在,不能证明用户看到或能操作;截图只能证明当次绘制,不能替代导出文件/工程重开/帧级结果。后续条目必须同时记录动作、结果和可定位证据,不能把历史 Beta 审计文字直接升级为当前 verified。 + +## Task 2 复核结果 + +- 代码 review:首轮 F1(无 monitor fallback)、F2(fresh install 默认档位)、F3(Home 真实 DOM/CSS contract)、F4(Settings 高度 contract)均已在 review loop 收口;最终 scoped review 无新 Critical/Important。 +- App 构建:release `.app` 已生成并安装;DMG bundle 脚本因超过一分钟无输出被停止,DMG 不在本轮交付证据内。 +- 本轮主线 fresh verification:Preview/Store/Media 4 files / 91 tests passed;MediaActions/MediaPanel 2 files / 59 tests passed;`pnpm build` exit 0。 +- 全量 Web 串行门禁:`NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-range-refresh-full.json pnpm exec vitest run --pool=forks --maxWorkers=1` → 151 files / 1415 tests passed;`pnpm build` exit 0。 +- Rust workspace:此前顺序全量曾通过;本轮受影响门禁仍全绿:Agent lib 417/417、ops lib 209/209、Tauri lib 726/726,字幕 16/16 + 5/5,Lottie JSON/容器/MCP path 定向测试通过,`cargo fmt --all -- --check` 通过。2026-08-22 再跑 `cargo test --workspace --jobs 1 -- --test-threads=1` 在既有 `opentake-motion/tests/chromium.rs::four_k_single_frame_opaque_and_transparent_budget_smoke` 处超时 180s,随后同 gate 的 3 个测试因 poison 连带失败;本次 diff 未修改 `opentake-motion`,保留为当前机器 Chromium/GPU/环境风险,不宣称 workspace 全量本轮通过。全 workspace clippy 仍被既有 `chunks_exact`/`chunks_exact_mut` lint 阻塞,当前输出涉及 `opentake-media`、`opentake-motion` 等旧模块。 +- Web:Lottie、导出取消竞态和 Text/Effect 面板接线后的串行全量 `152 files / 1429 tests` 通过,`pnpm build`/tsc 通过;MediaTabBar/MediaPanel 定向 56/56,Preview 目录为 `20 files / 196 tests`,Shell export 3 files / 39 tests 通过。 +- 最终 `.app`:`web/node_modules/.bin/tauri build --bundles app` exit 0;当前安装包二进制 SHA-256 `eaad6fb35b43e39884b8b756090548eace9559e366c1bd3eedcc37b6fb599e57`,包含 Lottie native timeline route、导出取消竞态和 Text/Effect 面板接线;本轮新增 Text/Effect 自动化证据及 Effect linked V1/A1 入口屏幕证据,但效果点击后的截图/Undo 仍未闭环。透明 ProRes 4444 SavePanel 已打开并取消,字幕 SRT/VTT SavePanel 已打开并取消;Option 修饰键、实时音画级同步、可交互取消屏幕、`.lottie` 容器屏幕仍保持 partial。 + +## Related Documents + +- Parent index: [Docs index](../../INDEX.md) +- Plan: [Full UI and upstream convergence plan](../../superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md) +- Capability ledger: [CAPABILITY-LEDGER.md](../../capabilities/CAPABILITY-LEDGER.md) + +## Change Log + +- `2026-08-21T10:06:00+08:00` — 首轮安装版 UI 巡检;记录窗口、导入、分割和真实回归边界。 +- `2026-08-22T11:19:00+08:00` — 修复前端将 Lottie 时间轴误判为 unsupported 的路由缺口:Lottie 现在走 Rust native compositor;新增 route/Preview 回归,Web 全量 152/1424、tsc/build 和安装包均通过;Computer Use 在 QA 工程中验证 Lottie 素材预览、双击落轨、最近项目重开、时间轴画面和播放头推进。 +- `2026-08-22T11:33:00+08:00` — 修复 ExportDialog 在进度订阅尚未返回时丢失取消意图的竞态;新增 pending-listener RED→GREEN 回归,Shell export 39/39、Web 全量 152/1425、tsc/build 和新安装包 `2a5f8d72…0034e` 通过。Mac 仍锁屏,导出中途取消保留为屏幕待验收。 +- `2026-08-22T11:51:58+08:00` — Mac 仍锁屏期间使用项目 Browser fallback 做布局补证:1280×720 视口 `bodyScrollWidth=1280`、`bodyScrollHeight=720`,无横向/纵向溢出;示例项目和文本添加仍明确依赖 Tauri,未替代原生屏幕验收。 +- `2026-08-22T11:58:52+08:00` — 复用已有 `addTextClip()` 接通 MediaPanel 的 Text 标签和添加按钮;MediaTabBar/MediaPanel 54/54、Web 全量 152/1427、tsc/build 和新安装包 `61dbb3e4…703e0` 通过。Sticker/Effect 仍保持置灰占位。 +- `2026-08-22T12:05:54+08:00` — 接通 Effect 标签的 grayscale/sepia/invert 预设,追加到选中片段效果链并复用 `setEffects()` 撤销命令;MediaTabBar/MediaPanel 56/56、Web 全量 152/1429、tsc/build 和新安装包 `8031c640…3fd44` 通过。Sticker 仍保持置灰占位。 +- `2026-08-22T12:22:40+08:00` — 修复 Effect 面板对 linked V1/A1 选择的错误禁用:只筛选视觉片段;定向 56/56、Web 全量 152/1429、tsc/build 和新安装包 `eaad6fb3…9e57` 通过。原生入口按钮可见/启用,但点击后截图全黑且 Undo 未观察到,继续保留实机闭环风险。 +- `2026-08-21T12:09:35+08:00` — 写回 Preview tabs、文件/文件夹/relink 导入和主线 fresh verification 结果。 +- `2026-08-21T13:12:31+08:00` — 写回媒体 folder/flat/grouped 三态、网格/列表密度、文件夹导航及音频子页的安装版验收结果。 +- `2026-08-21T14:51:41+08:00` — 写回 audio full-track 解码修复、compositor temporal preview/native parity、最新安装包和全量 Web/Rust 门禁结果。 +- `2026-08-21T15:32:21+08:00` — 写回 SavePanel 修复、H.264 temporal 实际导出和 ffprobe 结果;带音频对拍仍保留为未闭环项。 +- `2026-08-21T16:08:41+08:00` — 写回时间轴范围交互代码切片、85 个时间轴测试、最新安装包哈希,以及带音频 fixture 的 V1/A1 波形、seek、暂停证据;范围坐标拖动和带音频导出仍保持 partial。 +- `2026-08-21T16:29:21+08:00` — 补充真实 happy-dom PointerEvent 覆盖,时间轴回归更新为 8 files / 88 tests;最终 range 修复包 `1e271571…` 已重新构建并安装,范围桌面拖动证据仍受 Computer Use 坐标接口限制。 +- `2026-08-21T16:36:24+08:00` — 修复最终端点 sticky snap 时序和 Shift range 原始起点,补齐反向/同帧/mark cancel/lost-capture PointerEvent 覆盖;最终包 `03bdcef3…` 已安装。 +- `2026-08-21T16:46:00+08:00` — 全量 Web 串行门禁更新为 151 files / 1410 tests passed;时间轴 range slice 仍保持 Ready、整体桌面 QA 仍为 partial。 +- `2026-08-21T17:46:17+08:00` — 写回带音频 H.264/AAC 安装版导出、ffprobe、完整解码和首中尾 SSIM;新增 export cleanup/external cancel 代码与 workspace 门禁。最新 cleanup 包的 SavePanel 复测仍记录为 partial,不把旧包成功静默升级。 +- `2026-08-21T19:32:27+08:00` — 写回 `a7d98d6` 的 symlink/reparse identity 防护、70 个 export 单测、顺序 workspace 全量门禁和最新安装包 `12e1db…9455d9`;并发 motion 超时保留为环境风险。 +- `2026-08-21T19:48:03+08:00` — 写回 `f05bac8`/`478e1b4` 的无选区 split/trim parity、Web 151/1412 全量门禁和最新安装包 `de4cb52b…25405`;Computer Use 因 Mac 锁定未完成屏幕 smoke。 +- `2026-08-21T20:09:19+08:00` — 写回 `741ff07` 的缺失媒体 Preview/Playback fail-closed、RenderLoop 集成 8 项和顺序 Rust workspace 全量结果;最新安装包 `40c21ce0…e7f08`,屏幕 smoke 仍受 Mac 锁定阻塞。 +- `2026-08-21T20:19:15+08:00` — 写回当前提交 `6b31449` 的顺序 workspace 全量、clippy 既有 lint 边界和最新安装包 `7ad988f3…a9799`;屏幕 smoke 仍受 Mac 锁定阻塞。 +- `2026-08-21T20:57:59+08:00` — 写回 `OT-MOTION-ALPHA` 首条透明发布纵切:ProRes 4444 `.mov`、alpha manifest provenance、Motion Studio 开关和 151/1413 Web + Rust workspace 全量证据;最新安装包 `7634979e…607f00`。Mac 锁屏仍阻塞透明发布桌面对拍。 +- `2026-08-21T21:21:26+08:00` — Agent Motion 文档新增透明发布参数,重新构建并安装最新 app `38c814f4…bc838`;Computer Use 再次返回 Mac locked,屏幕验收边界不变。 +- `2026-08-21T21:31:52+08:00` — alpha-preserving document edit 修复后重新构建并安装最新 app `d5f8aa46…87ae7`;真实 FFmpeg add→edit integration 通过,Computer Use 仍返回 Mac locked。 +- `2026-08-21T22:15:18+08:00` — 最新包 `918eac84…9551b` 解锁后完成真实桌面 smoke:紧凑/标准 Home、媒体预览 tab、播放到尾帧、有效分割与撤销、导出面板、Motion 透明开关和透明发布落轨均通过;Option trim 坐标操作返回 `noWindowsAvailable`,保留为未完成屏幕证据。 +- `2026-08-21T22:27:41+08:00` — 当前包完成 H.264/AAC SavePanel 导出;输出文件 ffprobe、完整 `ffmpeg -xerror` 和 7.700s A/V 流事实通过。透明 ProRes 专用导出、H.265/字幕/取消仍待。 +- `2026-08-21T22:32:49+08:00` — 最新包完成 linked V1/A1 的 Shift+Backspace ripple delete 和 Undo 屏幕对拍:V1/A1 同时移除、Motion clip 保留、撤销恢复三条 clip;Option trim 坐标接口仍未提供可用窗口。 +- `2026-08-21T22:37:23+08:00` — 对透明 Motion 发布产物做文件级复核:QA 工程中的 `.mov` 为 ProRes 4444 `ap4h` / `yuva444p12le`、90 帧/3.000s;`alphaextract` 成功读取非全黑 alpha;`media.json` 保留 `generationInput.transparent: true`。透明专用导出 UI、opaque→transparent 编辑和其它编码格式仍保持待验证。 +- `2026-08-21T22:47:48+08:00` — 最新包完成 Inspector 视频/音频/AI 编辑页、可撤销翻转和本地 AI 建议应用/撤销;字幕页的缺失转写模型提示;Agent 对话标签新建/关闭、发送禁用和 `⌘⌥A` 收起布局恢复。模型下载、真实转写/字幕导出、MCP/Agent 工具调用仍保持待验证。 +- `2026-08-21T23:09:09+08:00` — 最新安装包 `893b6ed0…d0ba` 完成范围删除屏幕对拍:I/O 标记全范围后再选 V1 锚点,Shift+Backspace 删除 V1/A1 并保留 Motion,预览时长和播放头同步到 3.000s,Undo 恢复三条轨道;同步刷新新增 playhead clamp,Web 全量更新为 151 files / 1415 tests。 +- `2026-08-21T23:19:12+08:00` — 最新安装包完成 H.265/AAC 与 ProRes 422 HQ/PCM 的 SavePanel 导出和文件级复核:HEVC `hev1` 与 ProRes `apch` 均通过完整 `ffmpeg -xerror`;透明 ProRes 4444、字幕和取消导出仍保持待验证。 +- `2026-08-21T23:47:02+08:00` — 新增透明 ProRes 4444 导出纵切:前端暴露 `ProRes 4444 透明 / .mov`,Rust 将透明 codec 映射到 `yuva444` + alpha 清屏;Rust workspace 722 tests、导出 integration 6/6、Web 151/1416 通过。最新包 `3ce3c1d0…c7eef` 已安装,但 Computer Use 当前被 Mac 锁屏阻塞,透明导出屏幕证据保留 partial。 +- `2026-08-22T00:06:00+08:00` — 复跑字幕纵切:domain 16/16、Tauri 实际 SRT/VTT 写文件 5/5、Captions/TitleBar 前端 10/10 通过;字幕能力保持“代码/自动化通过,安装版模型下载与 SavePanel 导出屏幕待解锁”。同时将导出取消状态明确区分为“代码/自动化通过,最新安装版 UI toast/清理仍待”。 +- `2026-08-22T00:46:36+08:00` — 上游 parity agent 确认并完成两项缺口:Agent 文件夹批量 entries 单步 Undo;Lottie JSON/`.lottie` 导入、Velato 校验和 metadata。Agent 417/417、ops 209/209、Lottie JSON/容器/MCP path 定向测试通过;最新安装版屏幕路径因 Mac 锁屏保持待验证。 +- `2026-08-22T01:03:46+08:00` — 受影响 Tauri lib 726/726、Web 151 files / 1416 tests、tsc、build 通过;workspace 全量复跑在既有 4K Chromium budget smoke 180s 超时,poison gate 连带 3 项失败,未把环境失败归因到本次媒体/Agent 改动。 +- `2026-08-22T01:08:00+08:00` — 基于 `c1db732` 重新执行 `web/node_modules/.bin/tauri build --bundles app` 并安装 `/Applications/OpenTake.app`;二进制 SHA-256 `acd7b105…c319`。Mac 仍锁屏,因此只记录构建/安装成功,不升级任何屏幕验收状态。 +- `2026-08-22T01:49:23+08:00` — 修复 WebKit 预览 >0 dB 增益被 `HTMLMediaElement.volume` 截断的问题:GainNode 节点复用、临时静音/恢复、卸载断开和不可用回退均有测试;预览 58/58、Web 152 files / 1423 tests、tsc/build 通过;基于新包安装 hash `d454bccc…b4b6`,Mac 锁屏仅记录构建/安装,不升级听感或屏幕验收。 diff --git a/docs/audit/2026-09-06/media-card-thumbnail-recovery.md b/docs/audit/2026-09-06/media-card-thumbnail-recovery.md new file mode 100644 index 00000000..b15a1579 --- /dev/null +++ b/docs/audit/2026-09-06/media-card-thumbnail-recovery.md @@ -0,0 +1,107 @@ +# 媒体卡片缩略图加载恢复 + +> 状态:generated · 阶段:implementation-backed · 日期:2026-09-06 +> 范围:MediaCard 图片加载恢复及小 poster 原子发布;Rust 补充更新:2026-09-07。原生 QA 复测与重新打包由主代理负责。 + +## 用户报告与只读证据 + +主代理在独立 Beta 6 `target/release/bundle/macos/OpenTake.app` 中报告:导入 `output/public-beta-2026-09-06/fixtures/playback-10s.mp4` 后,左侧卡片始终为蓝色问号破图,源预览与 V1 sprite 正常;完整播放 10 秒后破图仍在。 + +本任务没有控制电脑,也没有写入 QA 工程或素材。只读核对: + +- QA 工程 `output/public-beta-2026-09-06/Beta6-Desktop-QA.opentake/media.json` 指向上述视频,asset id 为 `edb35602-6523-4979-b995-dc1a1cfc6784`。 +- 按既有 `visual_file_identity_key` 的 path/size/mtime 规则定位到 cache key `9d7a7d4837ec2e05a65d12bb89028aca`。 +- `/Users/trip/Library/Caches/com.opentake.desktop/media-cache/MediaVisualCache/9d7a7d4837ec2e05a65d12bb89028aca.thumb.png` 存在,3059 字节、100×68;PNG signature、IHDR/IDAT/IEND chunk CRC、IDAT zlib 解压通过。 +- 同目录 `.preview.png` 存在,281760 字节、1584×1080,同样通过 PNG 结构/解压检查。卡片使用小 poster,源预览使用独立大 poster,不共用 React 图片节点。 +- `assetUrl` 通过 `convertFileSrc(path, "opentake-asset")` 构造 URL。静态 scope 包含 `$APPCACHE/**/*`,协议将 app_cache_dir 列为 application-owned root,并允许 PNG MIME。未找到仅对该卡片 PNG 缺少授权的静态证据。 +- 原生协议使用 `request.uri().path()` 解析并授权实际路径,URL 查询参数不会改变被授权文件。 + +## 已确认原因与证据边界 + +已确认的**持续破图原因**:MediaCard 直接挂载 ``,没有 onError 恢复;`item.thumbnail` 已有路径时跳过 lazy thumbnail 请求;即使列表刷新或预热完成,路径字符串不变也不会主动重新加载失败图片。因此一次资源加载失败会一直留下 WebKit 破图。 + +首次 asset 请求具体返回的状态码/错误尚未采集。上述检查不能证明首次失败属于权限、暂时忙、文件发布时序或其他协议错误;没有据此修改 Rust 或放宽授权。最终是否恢复原生 QA 中的实际图片,需要主代理用更新 Web 资源后的候选包复测。 + +## 最小修复 + +在 `MediaPanel.tsx` 内新增私有 `MediaCardThumbnail`,只用于原 MediaCard 的图片节点: + +- 图片 error 后移除破图节点,显示原素材类型图标。 +- 延迟 250ms、1000ms,最多重新加载两次同一 asset URL;附加 `opentake-thumbnail-retry=1/2` 查询参数,避免继续使用失败的请求结果。 +- 重试不调用 generateThumbnail、不重新解码、不触发预览/落轨,也不修改源文件、缓存或媒体镜像。 +- 两次仍失败后停在类型图标,不无限循环;卡片选择与源预览仍可使用。 +- 恢复组件的 key 包含 projectEpoch、媒体 thumbnailKey 与实际 URL。工程、来源或 URL 变化时重建恢复状态;卸载会清理等待中的定时器。 + +实际写集仅: + +1. `web/src/components/media/MediaPanel.tsx` +2. `web/src/components/media/MediaPanel.test.tsx` +3. 本记录 + +没有修改 asset helper、Rust、Windows 模型适配、PCM、QA 工程或其他 UI。未 commit/push。 + +## 验证 + +先新增 4 项回归。旧实现有 3 项明确失败:错误后仍显示坏 img、没有重试上限后的占位、没有可清理的恢复定时器。修复后覆盖:恢复后的新 URL、两次上限、卡片仍可预览、工程/源变化隔离、卸载清理。 + +执行: + +```bash +pnpm --dir web test --run src/components/media/MediaPanel.test.tsx src/components/media/StickerPanel.test.tsx +pnpm --dir web exec tsc --noEmit --incremental false +git diff --check -- web/src/components/media/MediaPanel.tsx web/src/components/media/MediaPanel.test.tsx +``` + +结果:2 个文件 / 70 项通过(MediaPanel 51 项,Sticker 19 项);TypeScript 检查与 diff 检查通过。没有运行全量测试、Cargo 构建或重新打包。 + +GUI 留给主代理:在更新后的候选包重现同一素材导入,确认卡片图片恢复;若仍停在类型图标,则需捕获该 `.thumb.png` 的原生 asset 请求错误,再决定是否需要 Rust 协议层修复。浏览器 DOM 的人工 error 事件验证了恢复流程,不替代这项原生验收。 + + +## 2026-09-07:双生产者发布竞态的 Rust 修复 + +主代理授权扩展写集后,完成 `src-tauri/src/media.rs` 的 poster 写入/缓存读取及 `src-tauri/src/media/prewarm.rs` 的 GridPoster 发布修复。保留主代理原有两处 `ExportGuard<'_>` → `ExportGuard` 改动。未改 safe asset 协议、sprite 生产逻辑、export/transcribe、Windows 模型适配或 PCM。 + +### 复现证据 + +冷缓存导入后台 GridPoster 与 Web `generate_thumbnail(includeSprite=false)` 使用同一个 `{key}.thumb.png`。旧同步写入直接使用最终路径,后台预热则 rename 覆盖同一路径。 + +受控 channel 屏障先暂停后台生产者,再让同步 `write_png` 发布有效 PNG,记录其文件身份,最后放行后台提交。旧实现测试失败,实际读到的 inode 从 24552253 变为 24552255;另一个测试证明旧后台发布会覆盖已存在的损坏目标。旧 `write_png` 对损坏目标直接返回成功、列表暴露损坏路径的用例也先确认失败。 + +### 最小实现与现有缓存规则 + +- `StagedPoster` 使用既有 tempfile 依赖,在目标同目录生成唯一临时文件,完整验证/写入 PNG 后调用 `persist_noclobber` 发布。当前依赖在支持的 macOS/Linux 文件系统使用 no-replace rename,Windows 使用不覆盖移动;没有引入依赖或新协调器。 +- 同步 `write_png` 和后台 `GridPoster` 复用此路径。已有有效同 key PNG 直接复用;其他生产者赢得发布时也只验证并复用,不能替换已发布 inode。 +- GridPoster 在既有 scheduler epoch/token 锁内完成最后发布,取消任务不发布;临时文件由 RAII 清理。 +- sprite 及其他原有可覆盖的 `commit_staged_bytes` 分支保持原行为。测试确认已有 sprite 仍可从 partial 更新为 complete。 +- `cached_poster_dimensions` 检查 resident regular file、拒绝 symlink/reparse,并复用既有 safe asset 的保留句柄打开方式,对 PNG 完整解码验证。未修改或减弱 safe asset 验证。 +- 损坏、截断、目录/符号链接等非法目标:保留原物,返回错误,不自动删除、修复覆盖或读取符号链接目标;媒体列表不返回其 thumbnail 路径,显式生成/缓存读取返回错误。恢复这类已损坏缓存需要通过既有缓存清理流程重新生成。 +- 既有大 preview poster 也通过共用 `write_png` 获得完整、不覆盖发布,但路径与小 poster 仍分离;没有扩大预览 UI。 + +### 受控发布测试与结果 + +新增 7 项 Rust 回归、更新 1 项现有缓存列表用例,覆盖: + +1. 完整临时文件写好后暂停于发布之前:最终路径不存在,`read_cached_poster` 无结果,媒体列表 thumbnail 为空;放行后为完整 PNG,临时文件已清理。 +2. 同步生产者先发布、后台 GridPoster 后提交:文件身份、字节、Unix inode/ctime/ctime_nsec 全部保持不变。 +3. 另一生产者在本生产者暂存后赢得发布:后者复用赢家,清理自身临时文件。 +4. 损坏/截断 PNG 与符号链接拒绝且不修改原物;无效暂存内容不进入最终路径。 +5. GridPoster 拒绝损坏目标,同时 sprite 的覆盖更新仍成功。 + +实际执行: + +```bash +cargo test -p opentake-tauri --lib poster -- --nocapture +cargo test -p opentake-tauri --lib media::prewarm::tests -- --nocapture +cargo test -p opentake-tauri --lib media_item_uses_existing_cached_thumbnail_without_decoding -- --nocapture +cargo test -p opentake-tauri --lib cached_thumbnail -- --nocapture +rustfmt --edition 2021 --check src-tauri/src/media.rs src-tauri/src/media/prewarm.rs +git diff --check -- src-tauri/src/media.rs src-tauri/src/media/prewarm.rs +``` + +结果:poster 集合 12/12、prewarm 集合 13/13、缓存列表 1/1 通过;集合有重叠,共覆盖 23 个不同测试。`cached_thumbnail` 过滤器再次选中同一缓存列表用例,不计为额外覆盖。格式与 diff 检查通过。Cargo 仅报告既有 `block v0.1.6` future-incompatibility 警告。 + +早期测试草稿曾误用不存在的 fixture 构造函数,已在生产实现前修正;随后记录的红灯均为上述实际行为断言失败。没有跳过或关闭测试。未运行全 workspace 测试,也未重新打包/操作 GUI/修改 QA 工程。 + +### 验收边界 + +已复现并修复双生产者发布竞态,前端两次受限重试继续保留。原生首次蓝问号问题仍须主代理用新包、冷缓存导入及同文件新工程复验;当前自动化结果不能替代首次 GUI 请求的闭环证据。Windows/Linux 发布分支没有在本机实测。 diff --git a/docs/audit/2026-09-06/public-beta-validation.md b/docs/audit/2026-09-06/public-beta-validation.md new file mode 100644 index 00000000..05a11ad3 --- /dev/null +++ b/docs/audit/2026-09-06/public-beta-validation.md @@ -0,0 +1,94 @@ +--- +status: draft +content_stage: partial-implementation +updated: 2026-09-06 +--- + +# Public Beta 候选验证记录 + +本记录只报告本轮实际执行结果。候选基线为 `33ee8e2` 加本任务改动,分支 `release/v1.0.0-beta.6`,本轮首个候选快照为 `3c68cf7`,GitHub 候选 [PR #249](https://github.com/appergb/OpenTake/pull/249),后续修复与最终发布 SHA 按执行记录更新。 + +## 环境 + +macOS ARM64;Node 26.7.0、pnpm 10.33.4。默认 `/usr/bin/python3` 缺少 `tomllib`,脚本改用 Codex bundled Python。初始 `web/node_modules` 缺失,已按锁文件安装。Rust工具链与构建日志保存在 `output/public-beta-2026-09-06/`。 + +## 当前结果 + +| Gate | 本轮结果 | +|---|---| +| Rust fmt | 通过 | +| Rust workspace clippy | 修复 constant chunks/manual contains 后通过,exit 0;第三方 `block 0.1.6` 有 future-incompat notice | +| Rust workspace tests | 最终整合:80组、2899通过、0失败、10个显式ignored;包含Motion握手、poster发布、导出后台/提前取消和PCM完整轨修复 | +| Rust minimal clippy | 无默认feature构建检查通过 | +| Web tests/build | 最终整合后153files/1466tests通过,生产构建通过;保留既有chunk/dynamic-import warnings | +| 发布/Windows workflow结构 | Beta 6 identity 更新后通过;只修改version/WiX/notes入口及对应现有合同摘要,历史Beta4恢复链不变 | +| 脚本单测 | Beta6更新后209/209通过 | +| sidecar provisioner 单测 | 7/7 通过 | +| C1B/打包sidecar | CI/evidence Ruby合同及macOS内置FFmpeg实际执行通过 | +| Motion Canvas | 2/2 测试、123项许可证记录、构建通过;runner.html与锁定输出一致 | +| Motion dependency audit | xmldom升级后0项漏洞 | +| Web dependency audit | nanoid升级后0项已知漏洞 | +| 真实媒体集成 | FFmpeg导出、Motion命令/合成、播放传输全通过;严格真实音频探针 frames=83、playhead=90、clock=audio、非黑比例1.000、neon-green=0.000 | +| 真实Keychain/MCP | 2项通过:鉴权、Host/Origin、重启/撤销、端口冲突、禁用/退出清理;敏感token未进入日志 | +| 新安装包GUI | macOS release app构建中,原生GUI待执行 | +| 宣传视频 | 独立GPT-6交付60s/1080p/30fps/1800帧H.264+AAC,主代理ffprobe、封面/联系表及全片解码通过;当前为注明日期的历史实拍剪辑,不算新包GUI证据 | +| 远端CI/Release | PR #249 已建立;两轮CI发现并定位的PCM问题均已修复、本地原测试转绿;真实模型资格34043674623双平台成功;最终候选远端CI待启动,尚未公开发布 | + +## 缺口与证据边界 + +- 语义搜索manifest/模型链路已完成并通过实际Rust推理;模型v2索引过滤、同尺寸损坏修复路径和真实下载大小正在独立复核后的收尾。Windows ort-tract真实运行发现线程配置未实现及动态shape推理错误,正在修复,尚未达发布条件。 +- 既有安装包可读取AX但窗口截图全黑;系统`IOConsoleLocked=No`,Raise及窗口zoom后仍黑。尚不能区分应用显示与窗口采集问题,已向用户异步询问可见状态;不能把AX按钮存在当成视觉通过。 +- 历史 GUI 记录不自动继承为当前候选的稳定性证明。 +- 用户已有 audit 工程、截图删除和视频素材改动均保留;本轮使用独立输出目录。 +- [执行计划](../../plans/active/2026-09-06-public-beta.md)与[候选说明](../../releases/1.0.0-beta.6.md)持续写回。 + +## 独立复核及新增资格工具 + +只读审查确认贴纸/模型恢复/有界排队修复无新增确定缺陷。`prepare_search_model_fixtures.py` 已实测成功下载、损坏缓存修复、错误hash与超长内容拒绝,并确认旧目标文件保留和临时文件清理;workflow YAML 可解析。真实模型清单由 Rust 生产常量导出,不维护另一套模型revision/字节数/hash。新流程后续必须在真实Windows/macOS runner实际成功,工具测试不能代替平台结论。 + +当前旧安装包窗口已能正常采集,但其中打开了其他工程;本次没有修改该工程,也没有将其素材用于宣传片。之前全黑采集现象不再持续,不能据此推定新包已验收。 + +## 首轮远端失败与修复 + +CI `34041435244` 的 Linux Rust 格式/clippy成功,workspace在实际 `facade_contract` 音频解码失败:AAC尾部输出65,536bytes超过64,004预算。已补目标采样率之后的精确采样裁剪,保留stdout保护;本地13项PCM+1项facade通过,远端待复验。详见 [FFmpeg边界记录](../../knowledge/2026-09-06-ffmpeg-pcm-boundary.md)。 + +首个本地release app已构建并启动独立进程,Info.plist为1.0.0-beta.6,二进制SHA256 `b59cb93b06f166ef74e7d36b52ba9dc21520edb40c5051be72eaa8ca6b7dabc2`。旧安装包另一个进程保持原工程;新包GUI已创建独立QA工程、原生导入10秒MP4并通过双击加入V1/A1链接轨,进一步验收进行中。此首包早于最终PCM修复重打包,不能作为最终发布源绑定。 + +- 首轮CI最终为7项成功、Linux Rust与Windows full-product两项失败,二者都在同一AAC facade测试触发相同超限,已有本地修复待新SHA复验。 +- Windows模型固定输入适配完成并独立审查通过:同产品分支的宿主真实双编码器+五条查询14秒/RSS1.89GiB,4项Windows分支单测与72项macOS模块实测通过;正式Windows/MSVC仍待qualification workflow。 +- 新增模型/PCM修复后的workspace clippy与minimal clippy通过。workspace最终重跑在4K Chromium捕获出现180秒超时,后续同组3项因共享锁poison未执行成功;已启动独立Chromium组诊断重跑,未放宽180秒门槛。 + +## 2026-09-07 收尾进展 + +- 精确候选 `64dce596d2d724f08339d469e1233601e927bace` 的 [真实模型平台资格](https://github.com/appergb/OpenTake/actions/runs/34043674623) 已成功:macOS与Windows各1项真正执行、0失败、0忽略,五条图文查询结果一致。相关日志和JSON保存本轮output目录。 +- 首包原生GUI贴纸导入/预览/落轨、文本创建并保存OpenTake Beta 6、视频+音频+图片+文本组合导出已走通。输出 `Beta6-GUI-export-720p.mp4` 为1056×720 H.264/30fps/450帧 + AAC48kHz单声道,15秒,1,095,344bytes,完整解码成功。分割实际发生在第299帧;AX slider setValue未触发播放头变化,不把它记为中点定位成功。 +- GUI导出耗时期间界面阻塞,sample92542明确主AppKit/WebKit IPC线程同步执行export_video→run_export_with_control→decode_frame。已改async+spawn_blocking,先claim lease和snapshot,再由worker拥有lease到真正结束;ExportControl共享Arc状态,保留更新安装互斥和取消generation。新增跨thread lease取消测试先RED借用非static,后export73项通过。当前首包尚未包含该修复,需新包验证进度和取消。 +- transcribe_media存在同样的sync-worker错误假设,已沿现有有界推理worker改async调度,活动lease由任务持有;4项既有转写测试通过。文档与最终评审继续同步。 + +- 提前取消回归确实RED:旧路径返回取消但已有输出文件已被删除;common export preflight修复后74项export测试通过。无range PCM已恢复完整轨语义,追加最多一秒的有界padding;原full-track equality测试未修改,media facade与FFmpeg集成15项通过/1项原有ignored。此前统一裁剪方案不再是当前实现。 + +## 最终整合本地验收(2026-09-07) + +Rust fmt、workspace clippy、minimal clippy均通过;80组workspace测试合计2899 passed/0 failed/10 ignored,全部命令exit0。Web153文件1466测试、TypeScript和生产构建通过。源码最后只读审查关闭,Motion/Poster两片无确定P1/P2。Release与Windows工作流合同通过,408篇Markdown本地链接检查无错误。新包仍须复验poster冷导入及后台导出进度/取消;旧首包的完成导出不能代替这两项。 + +独立首包QA已通过原生Open选择器重新打开保存工程,15秒、V2文本+V1视频/图片+A1音频四片段全部保留。未将Home最近工程tile双击无变化记为重开成功,也未把AX range setter当成成功seek。 + +## aa40672 新包与远端结果 + +新包二进制 SHA256 为 `80e9018cd85a6cef822de5a268dc7cc1633bd3f39e3417dbeec06fdf0d2f5099`,原测试进程 92542 已退出,新进程 25806;用户旧安装包进程 59956 保持运行。新建 `Beta6-Final-QA` 后冷导入 `cold-playback-2s.mp4`,卡片实际显示完整缩略图。2 秒和 10 秒 1080p H.264/AAC 导出已完成,分别为 60/300 帧、724357/3621960 bytes。随后在 1% 进行中状态立即取消,UI 显示“已取消导出”,`Beta6-final-cancel-confirmed.mp4` 不存在。前两次点击晚于导出结束,未冒充取消通过;文件已改名为 `short-completed` / `long-completed`。 + +桌面 UI 已实际下载安装 1.54 GB 视觉模型并进入“建立索引”阶段。新候选模型资格 [34047980719](https://github.com/appergb/OpenTake/actions/runs/34047980719) 再次在 macOS/Windows 成功。常规 CI [34047983006](https://github.com/appergb/OpenTake/actions/runs/34047983006) 仍有 Motion 跨平台阻塞:Windows 实际 4K opaque 180 秒超时;Linux 模拟透明协议用例等待 ACK 超时。其余 7 项通过,PCM 原失败场景已通过。Carver 继续同一协议切片;当前仍为候选,未创建 tag 或公开 Release。 + +最终安装包还完成了原生双文件导入、真实视觉索引和查询:`semantic-cats.png` / `semantic-parrots.png` 经模型实际处理,“沙发上的两只猫”在“画面”组返回 `semantic-cats`,“彩色鹦鹉”返回 `semantic-parrots`,`a photo of an airplane` 显示无匹配。中文查询不匹配英文文件名,不能将其误当作 Files 回退结果。卡片和实际画面均已查看。完成/取消导出文件已分别通过 probe、完整解码或不存在检查。测试工程已保存,测试进程 25806 已退出,用户旧安装实例不变。 + +## Motion 第四轮候选 + +第三轮 Windows trace 的三个启动帧全部是旧帧,seed 也可能被丢弃。新补丁在任何变更之前启动并消费/ACK 旧帧,然后逐项提交 seed、作者 marker、transition、desired,每个匹配检查后排空当前 session 的已排队 ACK。Linux 模拟测试的真实 deadline 为 1 秒;原 socket 未启用生产连接的 TCP_NODELAY,连续小消息的延迟累积导致失败。测试传输已对齐,deadline 未延长,错误预算标签同步为 1 秒。 + +作者完成 97 项 Motion 单测与 7 项实际 Chromium 集成,4K opaque/transparent 为 7.31/12.09 秒,trace 关闭,原 4K、alpha、CSP 验收文件未修改。独立只读复核通过。新增独立 Windows/Linux 资格 workflow 绑定精确 SHA,强制实际执行 97+7 和两个 4K 路径并保存日志,原 CI/release 合同未改;提交后的远端结果待回读。 + +第四轮 `d1a020e` 的 Motion 专项 `34052875635` 已真实双平台通过 97+7(0 failed/ignored):Linux 4K 10.200/17.805 秒,Windows 13.821/23.327 秒。模型资格 `34052875624` 也双平台成功,完整 CI 的 Rust 及其余 7 个任务通过。Windows full-product 在最后 Tauri 单测仅普通输出清理失败(689 passed/1 failed),Motion 阻塞已经解除。 + +当前处理这项 Windows 清理缺口:普通输出补 DELETE 访问权,保留 deny-delete-sharing;验证复用打开的文件而非重开路径。详细依据见 [Windows 输出句柄记录](../../knowledge/2026-09-07-windows-export-handles.md)。未关闭测试、未绕过失败,未创建公开 tag。 + +这项修复的本地 Tauri clippy、75 项 export 单测(含 reserved 外层所有权断言)和 6 项真实 export_integration 已通过,后者覆盖完整视频、音频 mux、HDR、文字、4K 和 ProRes 4444 alpha。新增精确 SHA 的 Windows export qualification,使用锁定 sidecars,强制两个句柄回归实际通过并保存日志;最终 Windows 结果待回读。 diff --git a/docs/audit/2026-09-06/search-model-recovery.md b/docs/audit/2026-09-06/search-model-recovery.md new file mode 100644 index 00000000..1a942512 --- /dev/null +++ b/docs/audit/2026-09-06/search-model-recovery.md @@ -0,0 +1,69 @@ +# 语义搜索模型恢复与下载大小提示 + +> 状态:generated · 阶段:implementation-backed · 日期:2026-09-06 +> 工作树:OpenTake-generation · 分支:release/v1.0.0-beta.6 +> 范围:Web 搜索界面与 IPC 类型接线;原生模型、下载与 worker 验证由主线负责。 + +## 目标与契约 + +本切片修复首次模型下载大小占位、损坏模型缺少修复入口,以及修复成功后当前查询不刷新的问题。保留现有搜索 hook、共享操作 token、Rust 只读镜像与 Files/Spoken 独立结果组。 + +主线确认的契约: + +- `SearchResults.visualError?: string | null` 向后兼容;未安装模型继续空 Moments 且无 visualError。 +- 模型验证损坏使用稳定 marker `SEARCH_MODEL_REPAIR_REQUIRED:`。查询返回直接前缀,索引 rejection 可能由 WorkerError 包装为 `model error: SEARCH_MODEL_REPAIR_REQUIRED: ...`,因此 Web 在整个错误消息中查找 marker。 +- 普通推理/索引错误不带该 marker,不能误导用户重新下载模型。 +- `searchModelStatus().bytes` 是 manifest 下载文件合计;成功 DTO 与现有 `downloadSearchModel`、`searchIndexStart(epoch, path)`、`searchQuery(query)` 调用约定不变。 +- Tauri 查询/索引等待移出 UI 主线程、复用 bounded ORT worker 属于主线 Rust 工作,不在本切片修改。 + +## 实现 + +1. 读取既有 `searchModelStatus()`,下载 tooltip 使用实际合计按十进制 MB/GB 展示。1535824768 bytes 显示 `1.54 GB`。等待状态、零值、无效数值或状态读取失败时显示“暂时无法确定下载大小”,移除 380 MB 默认值。 +2. 索引操作结算事件保留错误文字。含修复 marker 的错误导向“修复并重新下载模型”并调用 `downloadSearchModel`;普通索引失败显示实际错误,使用索引重试说明并继续调用 `searchIndexStart`。 +3. 消费可选 visualError。即使所有素材已经建立索引,损坏查询也显示修复说明及入口;普通视觉推理错误显示失败提示。Files 和 Spoken 继续渲染,不把视觉失败显示为无匹配结果。 +4. 同工程下载/索引成功后沿现有刷新通道读取最新索引状态,并递增查询 revision,重跑此时的查询文字;新查询仍使用 250ms debounce、请求序列与取消保护,成功后移除旧视觉错误。下载完成后若仍需建立索引,继续显示原“建立索引”按钮,不新增自动索引流程。 +5. 保留共享下载/索引 operation token,重复修复点击不会重复下载。同工程卸载/重挂载可继续显示同一 pending 操作并接收完成通知;卸载后无订阅者时不发起刷新/查询。旧工程操作完成可使当前工程重新读取权威状态(原有行为),但不会重跑新工程查询、套用旧失败或自动建立索引。 +6. manifest 大小返回、查询返回和操作后的查询重跑均检查现有生命周期/工程身份。修复时的新下载错误不会被旧 visualError 遮盖,网络失败可见且能重试下载。 + +## Read Set 与写集 + +已核对根 AGENTS.md、`docs/project/conventions.md`、既有 Web 路由与 MediaSearch hook/测试、SearchResults/SearchModelStatus/SearchIndexStatus 类型,以及 api.ts 中对应只读契约。只读参考上游 `MediaPanel/MediaTab/MediaTab+IndexStatus.swift`;核对主线新增 Rust visual_error DTO,仅阅读未修改。 + +本切片实际写入: + +- `web/src/components/media/MediaSearch.tsx` +- `web/src/components/media/MediaSearch.test.ts` +- `web/src/lib/types.ts`(仅 SearchResults 的可选字段与说明) +- `web/src/i18n/dict.ts`(搜索恢复所需中英文文案,保留既有贴纸内容) +- 本记录 + +Sticker 组件及其测试保持冻结。没有修改 Rust、API 调用参数、依赖、测试配置、版本或其他 MD,没有 commit/push。 + +## 验证证据 + +TDD 过程: + +- 最初新增 9 项用例均在旧实现失败,包括真实大小/未知大小、损坏索引修复、普通索引重试、完整索引下的 query 损坏与结果保留、操作完成重查及工程边界;最小接线后通过。 +- 增补卸载/重挂载、卸载后无刷新、迟到旧查询与下载失败可见性用例;“旧 query 损坏错误遮盖新下载失败”先失败,修正显示优先级后通过。 +- 收到主线 WorkerError 包装补充后,增加包装 marker 用例,先确认 startsWith 判断失败,再改为 includes 后通过。 + +最终执行: + +```bash +pnpm --dir web test --run src/components/media/MediaSearch.test.ts src/components/media/MediaPanel.test.tsx +pnpm --dir web build +git diff --check -- web/src/components/media/MediaSearch.tsx web/src/components/media/MediaSearch.test.ts web/src/lib/types.ts web/src/i18n/dict.ts +``` + +结果:2 个测试文件 / 76 项通过;其中 MediaSearch 29 项(原 15 项 + 新增 14 项)。TypeScript 与 Vite 构建通过。构建仍有动态/静态 import 无法分块和大 chunk 的既有告警。本任务没有运行全量测试或 Rust 测试。 + +测试使用真实 React 搜索面板与 Zustand,替换外部媒体 API 和下载/索引 promise,以验证用户动作、显示、请求次数及异步隔离。没有下载真实 1.5GB 模型,也没有运行原生 GUI,不能据此声明主线 worker 或真模型恢复验收通过。 + +## 主线原生待验收 + +1. 模型未安装时下载提示来自 manifest 合计,下载/索引期间 UI 响应正常。 +2. 同尺寸损坏模型:索引 worker 包装错误与完整索引下 query 校验错误均显示修复;Files/Spoken 保留。 +3. 修复下载成功后当前查询自动重跑;仍缺索引时按现有按钮建立索引,再自动重查;恢复后的画面结果正确,错误提示消失。 +4. 普通推理错误不触发重新下载;网络失败给出实际错误和下载重试;快速重复点击、跨项目以及切走再回的操作不会重复提交或显示旧工程错误。 + +这些 GUI/真实模型场景仍待主线验收;本记录不表示 Beta 6 已发布。 diff --git a/docs/audit/2026-09-06/semantic-search-model.md b/docs/audit/2026-09-06/semantic-search-model.md new file mode 100644 index 00000000..9a89ca63 --- /dev/null +++ b/docs/audit/2026-09-06/semantic-search-model.md @@ -0,0 +1,266 @@ +--- +status: canonical +stage: final-verified +updated_at: 2026-09-06 +--- + +# 语义搜索模型首次安装修复 + +## 目标与写入范围 + +完成真实可下载、固定版本校验、离线安装、真实图文 embedding 与语义排名。保留主线已有 `embedder.rs` 的 `as_chunks` 修改。不提交、推送、发布,不修改其他 Markdown、marketing 或 output。 + +代码改动:`crates/opentake-media/src/search/{config,model_download,tokenizer,ort_embedder}.rs`、`src-tauri/src/search.rs`。其中 `ort_embedder.rs` 是调查后提前告知的必要新增写入范围。知识来源仅写 [模型知识记录](../../knowledge/2026-09-06-semantic-search-model.md),详细模型 SHA/长度事实统一在那里维护。 + +读取范围:工作区和项目 AGENTS、docs/INDEX、opentake-media 和 src-tauri 的 OVERVIEW/INDEX、semantic-search 模块页、对应 Rust 文件,以及只读上游 `Search/SearchIndexConfig.swift`、`Search/Models/*.swift`、`Search/Query/VisualSearch.swift`。未修改上游。 + +## 根因与修复 + +1. 原 URL 指向未托管的 OpenTake ONNX 仓库,3 个 SHA-256 为空、长度为零。改用可信 ONNX Community 对应 Google 原模型的固定 revision FP32 资产,3 个文件共 1,535,824,768 字节,匿名下载后逐一验证通过。 +2. 真实资产使用 `onnx/` 子目录和原始 `tokenizer.json`;旧下载器只支持根路径及 `tokenizer.zip`。现支持相对路径、原始 JSON,保留旧 zip 兼容性,不增加依赖。 +3. 默认 I/O 名仍是 CoreML 的 `image/tokens/embedding`。改为真实图的 `pixel_values/input_ids/pooler_output`。 +4. 原归一化分支为空操作;真实池化输出未归一化。现校验维度、有限值、非零范数,并按 `spec.normalized == false` 做 L2 归一化。 +5. 原 tokenizer 编码后硬截断会丢失长查询末尾 EOS。现让 tokenizer 在特殊 token 后处理前执行长度限制,再按 0 右填到 64。 +6. 原 `installed` 只判断三个路径存在,错误或部分文件也能被当作已安装。现要求与当前 manifest 相同的安装回执和准确尺寸;Tauri 加载前重新验证实际 SHA-256。安装目录与索引版本升至 2,使 v1 索引自动失效。 + +安装使用模型目录内的临时目录,下载结束逐个验证长度和 SHA-256,所有文件准备好后再发布整个目录;临时目录 RAII 清理覆盖请求失败和取消。替换旧目录失败时尝试恢复原目录。进度只在安装成功后到 1.0,超出 manifest 预期大小即拒绝写入更多内容。 + +## 实际外部证据 + +- 获取时间:2026-09-06;固定 revision 与逐个校验值见知识记录。Python 使用公开 HTTPS 请求,不读取 Hugging Face 用户配置或凭据。 +- Python ONNX Runtime 1.23.2 CPU 加载两个真实图成功,不需要外部 `.onnx_data` 文件。 +- 视觉输入 `(1,3,256,256)`,文本输入 `(1,64)`,二者 `pooler_output` 均为 `(1,768)`。 +- 未归一化输出范数:全零像素张量约 14.08375,示例文本约 24.39208。归一化后再排名,不能将裸点积当余弦。 +- 图片来源:[猫图](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/coco_sample.png)、[鹦鹉图](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/hub/parrots.png),只放任务临时目录,不加入仓库。 + +Python 真实图文余弦分数: + +| 查询 | 猫图 | 鹦鹉图 | 预期 | +|---|---:|---:|---| +| a photo of two cats on a couch | 0.155847 | -0.012872 | 猫图第一 | +| a photo of colorful parrots | -0.004647 | 0.133400 | 鹦鹉图第一 | +| 沙发上的两只猫 | 0.115520 | 0.013697 | 猫图第一 | +| 彩色鹦鹉 | -0.043940 | 0.115521 | 鹦鹉图第一 | +| a photo of an airplane | -0.018885 | 0.006155 | 均低于 0.05 门槛 | + +## Rust 验证与复现 + +配置修复前,针对公开 API 数据的 manifest 回归检查失败,原因是缺少固定 revision。修复后通过。Tokenizer 长序列回归在修改前实际失败:`[2,2,2,2]`,期望 `[2,2,2,1]`;修改后通过。 + +为避免争用主线 Cargo,先使用 `/tmp` 中的 `rustc --test` harness 引用实际模块并只读复用已编译依赖。最初 harness 误选 reqwest 0.13(项目用 0.12)导致 TLS provider 初始化失败;按项目 0.12 重新链接后 34 项通过。这是测试装配错误,没有为其修改产品依赖或添加 TLS 绕过。 + +主线随后明确释放 Cargo 锁,执行正式命令: + +```sh +cargo test -p opentake-media --features model-download,ort-backend --lib search:: -- --test-threads=1 +``` + +最终结果:84 passed、0 failed、1 ignored(真实资产测试默认 opt-in);包括模型下载、HTTP 404/截断/超长/错误 hash、离线安装、回执、实际字节损坏拒绝、tokenizer 与排名/存储现有回归。 + +真实模型 Cargo 复现命令(本轮真实模型实际使用下述独立 Rust harness 执行;此命令供标准 Cargo 重跑,无下载、无凭据): + +```sh +OPENTAKE_SEARCH_MODEL_TEST_DIR=/tmp/opentake-semantic-model \ + cargo test -p opentake-media --features model-download,ort-backend --lib \ + real_model_offline_install_embeddings_and_ranking -- --ignored --nocapture --test-threads=1 +``` + +该目录应包含 `onnx/vision_model.onnx`、`onnx/text_model.onnx`、`tokenizer.json`、`cats.png`、`parrots.png`。测试从固定 manifest 校验离线来源,安装至临时模型目录,以产品 OrtEmbedder 加载,运行实际图片与中英文查询,断言 768 维/有限值/单位范数,将图像 embedding 经 PALMEMB1 f16 编码解码,再运行产品 ranker 并检查五组结果。 + +真实 Rust 模块回归已完成:`/tmp/opentake-semantic-model/rust_tests.py` 用 `rustc --test` 引用本工作树实际 `config/embedder/tokenizer/model_download/ort_embedder/embed_store/ranker` 及依赖模块,启用 `model-download` 与 `ort-backend`,连接项目已编译的 `ort 2.0.0-rc.11` 原生运行库。macOS 初始化函数与库内一致为空函数。结果 **72 passed、0 failed、0 ignored**,包含真实模型 opt-in 测试,耗时 301.84 秒(debug 构建对模型源、暂存副本、安装文件的重复 SHA-256 校验占主要时间)。没有以 Python 推理代替 Rust 实测,也没有为测试关闭 checksum。 + +产品 Rust 路径经历真实图像预处理、768 维向量归一化、f16 索引编码解码和 ranker 门槛后的结果: + +| 查询 | 第一名 | 分数 | +|---|---|---:| +| a photo of two cats on a couch | cats | 0.15572338 | +| a photo of colorful parrots | parrots | 0.13344961 | +| 沙发上的两只猫 | cats | 0.11564892 | +| 彩色鹦鹉 | parrots | 0.11663653 | +| a photo of an airplane | 无结果 | 所有候选低于门槛 | + +Tauri 正式验证: + +```sh +cargo test -p opentake-tauri --lib search:: --jobs 1 -- --test-threads=1 +``` + +结果 **14 passed、0 failed**;跨 crate 的 `verify_installed` 已正常编译链接。原主线编译中间态错误不再出现。仅有已有依赖 `block 0.1.6` 的未来 Rust 兼容性提示。 + +最后执行受限文件 `rustfmt --check` 与 `git diff --check`,均通过。主线原有 `embedder.rs` 的 `as_chunks` 改动保留,本任务未再编辑该文件。Cargo 本轮完成后已明确通知主线继续。 + +本地证据保留在 `/tmp/opentake-semantic-model/`:`api.json`、三个原始模型文件、两张公开图片、`python-evidence.json`、`rust-red.log`、`rust-unit.log`、`rust-real.log`、`cargo-search.log`、`tauri-tests.log` 及复现脚本。它们不加入 Git;真实测试生成的安装副本已随 tempfile 清理。 + +## 离线使用方式 + +- 用户可在可联网机器完成一次应用内模型下载,然后将完整 `siglip2-base-patch16-256-v2/` 目录复制至离线机器的应用 `Models` 目录(`MediaEngine::models_dir()`)。目录包含两个 ONNX、`tokenizer/tokenizer.json`、`spec.json`、`manifest.json`。应用识别回执和尺寸,并在推理加载前重新验 hash;不需要联网或账号。 +- 对预先下载的原始资产,调用公开 Rust 函数 `model_download::install_from_directory(models_dir, &config::manifest(), source_dir)`。它读取上述三个 repository-relative 文件,验证源和暂存副本,再创建同一安装回执。真实模型 opt-in 测试覆盖此路径。 +- 没有增加离线导入 UI 或额外 Tauri 命令;也没有新增前端类型或注册项。不可只放几个未校验的空文件伪造“已安装”。 + +## 剩余边界 + +- 此记录验证本任务纵向链路,不宣称 workspace、完整 UI、发布包、Windows ort-tract 已通过;主线仍负责公开 Beta 发布门槛。 +- Hugging Face 在受限网络可能不可达,应用返回请求错误,离线复制/导入保留相同校验。 +- 模型净资产约 1.43 GiB;离线安装测试需要源与安装目录同时存在,逻辑峰值约 2.86 GiB,临时安装目录测试结束后清理。未下载整个模型仓库。 +- 加载前重新计算两个大 ONNX 的 SHA-256 有 CPU/IO 成本;快速状态查询只读回执和尺寸。原有查询构造新 embedder 的行为未在本任务扩为跨请求缓存。 + +## 2026-09-06 独立 ort-tract 资格验证:失败,阻塞当前 Windows 语义搜索 + +获取/执行时间:2026-09-06T06:58:44Z 起完成汇总。范围仅为临时 harness 和本节审计;冻结产品代码没有在本轮修改,主 workspace target 未使用。主线另行处理查询缓存版本过滤、模型失败恢复与下载大小显示,这些修改不属于本次后端结论。 + +### 环境与可重复性 + +- 实际宿主:macOS ARM64,16 GiB RAM,`rustc 1.97.1 (8bab26f4f 2026-07-14)`。 +- 锁定后端:`ort 2.0.0-rc.11`、`ort-sys 2.0.0-rc.11`、`ort-tract 0.2.0+0.22`、`tract-onnx/tract-core 0.22.3`。 +- 临时 Cargo.lock 的所有 registry 包版本及 checksum 均与主 workspace 当时锁文件一致,差异列表为空,证据 `lock-comparison.json`。本地原先没有 tract 源码,只获取构建该后端所需的锁定 Rust 依赖。 +- Cargo 实际 feature:`ort = [alternative-backend, ndarray, std]`;`ort-sys = [disable-linking, std]`;没有启用 native ONNX Runtime 的 download-binaries、copy-dylibs 或 TLS feature。测试中 `ort::set_api(ort_tract::api())` 断言成功。 +- 复制产品相关模块到 `/tmp/opentake-tract-qualification/src/imported/` 作为稳定快照,复用实际 `OrtEmbedder`、`Embedder` trait、预处理、tokenizer、f16 index 与 ranker。初始化函数逐句使用产品 Windows 分支的 `Once + ort::set_api`,没有人为设置 `cfg windows`。 +- ort-tract 可以在 macOS 原生编译。其 `api.rs:66-72` 的 Windows cfg 只分支文件路径 UTF-16 解码;后续模型解析、类型推导和执行走共同代码。本次结果是相同后端的兼容性预检,**不是 Windows OS/MSVC 实机通过证据**。 +- 模型与两张图继续使用 `/tmp/opentake-semantic-model/` 中前轮已校验的同一批文件。未额外下载或重写模型、未固化 ONNX 维度、未关闭 shape/finite/dim/normalization/ranking 断言。 + +临时构建: + +```sh +CARGO_TARGET_DIR=/tmp/opentake-tract-qualification/target \ + cargo build --manifest-path /tmp/opentake-tract-qualification/Cargo.toml \ + --locked --offline --jobs 1 +``` + +最终构建成功。临时 harness 的初版日志曾误用 ort Session 私有字段,改为锁定 API 的 `inputs()` / `outputs()` 后通过;修正仅发生在 `/tmp`。为节省资源使用 `debug=0`、`incremental=false`、`opt-level=1`、单编译任务。所有临时构建/日志约 731 MiB;本轮开始可用磁盘约 25 GiB,结束约 23 GiB。前轮模型目录仍约 1.7 GiB,没有模型复制增量。 + +### 三个真实运行阻塞 + +| 路径 | 实际失败阶段及结果 | 判定 | +|---|---|---| +| 原样产品 OrtEmbedder → Windows init → tract | `model install: ort threads: Unimplemented`;图像、文本独立入口也均返回 `Unimplemented`,进程退出码 1 | 模型尚未加载即失败,当前 Windows 产品路径必需先处理 | +| 临时诊断模式仅省略线程数量性能选项,加载原图像图 | `Failed to parse model: Failed analyse for node #232 "/vision_model/embeddings/patch_embedding/Conv" ConvHir`,退出码 1 | 即使处理线程选项,原图像图仍无法加载 | +| 同一临时诊断模式加载原文本图并用真实 tokenizer 输入 `(1,64)` | Session 加载成功,执行失败:`Failed to run session: Evaluating #11 "/text_model/embeddings/Slice" StridedSlice`,退出码 1 | 原文本图无法输出 embedding | + +线程选项的具体源码链: + +- 产品 `crates/opentake-media/src/search/ort_embedder.rs::build_session` 调用 `.with_intra_threads(...)`。 +- 锁定 crate `ort-tract-0.2.0+0.22/api.rs:531-590` 构造 API 表,没有覆盖 `SetIntraOpNumThreads`,其余字段来自 `..ort_sys::stub::api()`。 +- `ort-sys-2.0.0-rc.11/src/stub.rs:194` 的 `SetIntraOpNumThreads` 明确返回 `OrtErrorCode::ORT_NOT_IMPLEMENTED`、`"Unimplemented"`。 + +为了得到 ort-tract API 错误包装省略的完整原因,临时诊断又直接调用了**同版本 tract 的相同解析/into_typed/into_runnable/run 流程**,没有修改输入事实或模型: + +```text +Vision: +Failed analyse for node #232 "/vision_model/embeddings/patch_embedding/Conv" ConvHir: +Infering facts: +Applying rule inputs[0].shape[1] == 1*{inputs[1].shape[1]}: +Impossible to unify Sym(num_channels) with Val(3). + +Text (typed graph 成功,实际执行失败): +Evaluating #11 "/text_model/embeddings/Slice" StridedSlice: +Running legacy eval: +Evaluating #5 "adhoc" Slice: +Undetermined symbol in expression: +``` + +`ort-tract/session.rs:57-64` 在加载时 `model_for_proto_model` 后立即 `into_typed()`(或启用优化时 `into_optimized()`);`session.rs:35-43` 的执行计划构造只设置 input names,没有根据运行时 tensor 绑定 input shape。该代码与上述错误相互印证。这里只记录已观察到的动态 shape 失败,不将它泛称为整个 Conv/Slice 算子不受支持,也不预先声称某一种修复方案已可行。 + +### 内存与验收结果 + +使用 `/usr/bin/time -l` 测量,每个图在独立进程顺序运行,没有 OOM 或进程被杀: + +| 诊断 | 最大 RSS | 结果 | +|---|---:|---| +| ort-tract 图像加载(省略线程选项) | 1,500,954,624 bytes(约 1.40 GiB) | shape 推导失败 | +| ort-tract 文本加载 + `(1,64)` 执行(省略线程选项) | 2,383,986,688 bytes(约 2.22 GiB) | Slice 动态符号失败 | +| 直接 tract 文本完整原因链诊断 | 2,925,232,128 bytes(约 2.72 GiB) | 同一 Slice 失败;time 另报 peak memory footprint 4,105,866,744 bytes | + +**没有产生有效 tract 图像/文本 embedding 或排名结果。** 原样产品排名入口实际运行并在 `OrtEmbedder::new` 返回线程设置错误,因此不能以先前 native-ORT 的正确猫/鹦鹉排名替代本后端验收。由于执行提前失败,也不能据此宣称完整模型推理的内存上限已通过。 + +结论:**阻塞当前 Windows 语义搜索发布资格。** 同一个锁定 tract 后端的共享代码已在宿主预检稳定复现三个阻塞;不能仅修复 `.with_intra_threads` 就宣布 Windows 支持。修复需要主线协调产品后端/API 或 shape 适配策略;本轮没有修改 `ort_embedder.rs`、`model_download.rs` 或模型文件。 + +### 最小 Windows 原生 qualification + +在真实 `windows-2022`/Windows x64 runner、VS C/C++ Build Tools + Windows SDK、项目 stable Rust MSVC 工具链上运行。无需 Python ONNX Runtime、native onnxruntime.dll、FFmpeg、Whisper 或付费服务。fixture 目录预先准备前文三个固定 SHA 模型文件以及 cats.png/parrots.png,测试本身不联网下载;需保留足够空间供源文件与临时安装副本并存。 + +```powershell +$env:OPENTAKE_SEARCH_MODEL_TEST_DIR = Join-Path $env:RUNNER_TEMP 'semantic-model-fixtures' +$env:CARGO_TARGET_DIR = Join-Path $env:RUNNER_TEMP 'semantic-qualification-target' +cargo test --locked -p opentake-media ` + --target x86_64-pc-windows-msvc ` + --features model-download,ort-backend --lib ` + search::ort_embedder::tests::real_model_offline_install_embeddings_and_ranking ` + -- --exact --ignored --nocapture --test-threads=1 +if ($LASTEXITCODE -ne 0) { throw 'Windows semantic model qualification failed' } +``` + +该命令自然选中产品 Windows `ort-tract` 依赖和初始化分支,覆盖实际 hash/安装、预处理、768 维单位向量、f16 索引和五条查询。需在协调修复后真正通过,并保留资源峰值及日志,才能解除 Windows 资格阻塞。这里提供命令,未提交/运行 CI 或改工作流。 + +本轮原始证据:`/tmp/opentake-tract-qualification/{Cargo.toml,Cargo.lock,metadata.json,lock-comparison.json,build.log,vision.log,text.log,ranking.log,diagnostic-vision.log,diagnostic-text.log,direct-vision.log,direct-text.log}`;可执行文件为 `target/debug/opentake-tract-qualification`,模式分别是 `vision`、`text`、`ranking`、`diagnostic-vision`、`diagnostic-text`、`direct-vision`、`direct-text`,运行时设置 `OPENTAKE_SEARCH_MODEL_TEST_DIR=/tmp/opentake-semantic-model`。 + +## 2026-09-06 最小 Windows 搜索适配:宿主真实推理通过,等待原生 CI + +本节取代上一节“修复前阻塞”作为最新实现状态。代码已冻结待主线统一提交/push;本任务没有打 tag 或发布 Release,原生 Windows 系统资格仍由主线新增平台 workflow 验证。 + +### 修复选择与实证 + +1. 线程配置和维度覆盖两类 ORT API 在锁定 ort-tract 包装层都没有实现。`ort::SessionBuilder::with_dimension_override` 调用的 `AddFreeDimensionOverrideByName` 同样落入 `ort-sys` 的 Unimplemented stub,不能用该接口完成固定输入绑定。 +2. 仅设置输入 fact 仍会被中间 `value_info` 的动态 shape 注解阻塞。继续实测发现导出注解被 Tract 0.22.3 解析成 `Mul([Mul([Sym(batch_size), Div(Sym(height), 16)]), Div(Sym(width), 4096)])`;256×256 输入得出错误 batch=0,随后与 Reshape 推导出的 batch=1 冲突。证据 `binding-debug.log`。 +3. 最小修复是在 Windows 搜索编码器内部直接使用**同一锁定的 tract-onnx 0.22.3 引擎**,无需修改 ort-tract 包、其他推理功能或上游模型。固定输入图像 `[1,3,256,256]`、文本 `[1,64]`,只将中间 shape 的动态维度恢复为待推导维度,保留张量类型、rank 与静态维度,随后由 Tract 完成类型/shape 推导及优化。 +4. 加载前检查原图单输入名称、dtype、rank、已有静态尺寸;加载后要求目标 `pooler_output` 是 float32 `[1,768]`;运行时再次检查输入 dtype/shape、输出数量/shape,再进入既有有限值、非零范数、L2 归一化与排名逻辑。没有启用 `ignore_output_types` 或关闭输出检查,没有修改任何算子/权重或落盘模型。 + +产品写集: + +- `crates/opentake-media/src/search/ort_embedder.rs`:Windows 内部 `tract_backend::Encoder` 与固定输入推导;macOS/Linux 保留原生 ORT 实现。 +- `crates/opentake-media/Cargo.toml`:`ort-backend` 开启 Windows-only optional `tract-onnx = "=0.22.3"`,只引用已锁定依赖。 +- `Cargo.lock`:仅为 opentake-media 增加 `tract-onnx` 依赖边,包版本与 checksum 未升级。 +- 本审计与既有模型知识记录。 + +本轮未修改 config、tokenizer、model_download、src-tauri/search、worker、模型 manifest/哈希/版本、下载资产。主线拥有的查询恢复/缓存过滤/平台 workflow 继续独立处理。 + +### 测试分支与实际运行 + +临时 harness 继续使用 `/tmp/opentake-tract-qualification/`,没有使用主 workspace target,Cargo jobs≤2。 + +为在 macOS 编译**产品文件的 Windows 搜索分支**,只对 `/tmp` 的产品源码副本进行守卫选择转换: + +```python +source = product_ort_embedder_rs.read_text() +source = source.replace('target_os = "windows"', 'feature = "qualification-tract"') +``` + +临时 Cargo manifest 定义 `qualification-tract=[]`,开启该 feature 后构造的就是产品 `OrtEmbedder` 及其同一 `tract_backend::Encoder` 实现。没有设置 rustc `cfg(windows)`,没有改标准库目标平台,没有改函数体/模型运算来取得通过;这个选择仅存在临时 harness,产品仍由真实 `target_os="windows"` 选择。新的直接 tract 路径不调用 ORT API,因而不会碰未实现的线程配置。 + +两个真实编码器**同时驻留**,产品 `OrtEmbedder` 完成图像预处理、tokenizer、768 维特征检查与归一化,图像向量经 f16 PALMEMB1 编码/解码后交给产品 ranker: + +| 查询 | 实际第一名 | 分数 | +|---|---|---:| +| a photo of two cats on a couch | cats | 0.15572256 | +| a photo of colorful parrots | parrots | 0.13344985 | +| 沙发上的两只猫 | cats | 0.115648665 | +| 彩色鹦鹉 | parrots | 0.11663527 | +| a photo of an airplane | 无结果 | 均低于门槛 | + +结果 `RESULT=PASS`,退出码 0,内部耗时 13.91 秒;`/usr/bin/time -l` real 14.85 秒、最大 RSS **2,025,848,832 bytes(约 1.89 GiB)**、peak memory footprint 2,653,620,840 bytes。前一版单独加载两个图的原型也通过,但验收采用本次同时持有双编码器的产品路径。两者保留同一公开 FP32 模型,分数与原生 ORT 前轮结果接近,没有换模型或量化。 + +验证命令和结果: + +```sh +# product-ranking.log,复用上述 feature 构建的产品分支 +OPENTAKE_SEARCH_MODEL_TEST_DIR=/tmp/opentake-semantic-model \ + /usr/bin/time -l /tmp/opentake-tract-qualification/target/debug/opentake-tract-qualification ranking + +# product-unit.log +CARGO_TARGET_DIR=/tmp/opentake-tract-qualification/target \ + cargo test --manifest-path /tmp/opentake-tract-qualification/Cargo.toml \ + --locked --offline --features qualification-tract --jobs 2 \ + search::ort_embedder -- --nocapture +``` + +- Windows 分支相关单测:**4 passed、0 failed**。新增两项分别验证动态维度绑定不放宽静态尺寸、拒绝错误输入名称/类型/rank;另两项验证 I/O 名和向量输出检查。 +- macOS 原生分支:只读复用已编译依赖的独立 `rustc --test`,**72 passed、0 failed**,本轮不重复前轮已完成的原生真实模型测试。 +- `cargo metadata --locked --offline --no-deps` 通过;macOS `cargo tree --locked --offline ... --target aarch64-apple-darwin` 确认未引入 tract。Windows 离线依赖树检查因为本机未缓存 `winapi 0.3.9` 源码停止,不作为 Windows 编译失败或成功的依据。 +- 最后受限文件 rustfmt 与 diff 空白检查通过。产品代码冻结后未再启动 Cargo。 + +新证据包括 `fixed-ranking.log`、`binding-debug.log`、`product-build.log`、`product-ranking.log`、`product-unit.log`、`native-tests.log`、`tree-macos.log`、`tree-windows.log`。模型仍在原来的 1.7 GiB 临时目录,没有额外下载。独立 tract 临时编译目录复用原有 target。 + +### 合入复核与发布门槛 + +复核重点是:Windows cfg 是否只选择新搜索编码器;直接依赖是否仅激活 Windows 且版本仍为 0.22.3;动态 shape 重推导是否保留 dtype/rank/静态尺寸,并在优化后及运行后检查 `[1,768]`;manifest 和模型字节是否保持不变。 + +**本机已实证收敛三个已知后端阻塞。** 允许进入主线统一 push 后的平台资格流程;仍须在真实 Windows runner 跑主线的 `real_model_offline_install_embeddings_and_ranking` opt-in 测试,并检查确实 `1 passed`,同时满足 macOS 原生资格和主线 release 验收,才能宣布 Windows 发布资格通过。临时 harness 的通过不能替代 OS/MSVC 实测。 diff --git a/docs/audit/2026-09-06/sticker-panel.md b/docs/audit/2026-09-06/sticker-panel.md new file mode 100644 index 00000000..23d543b3 --- /dev/null +++ b/docs/audit/2026-09-06/sticker-panel.md @@ -0,0 +1,79 @@ +# Sticker 面板纵切记录 + +> 状态:generated · 阶段:implementation-backed · 日期:2026-09-06 +> 工作树:OpenTake-generation · 分支:release/v1.0.0-beta.6 +> 范围:本地贴纸面板接线与定向 Web 验证;不是原生安装包或发布验收。 + +## 实现结果 + +- Sticker 主标签已启用,在 MediaPanel 内提供独立贴纸内容区。 +- 「导入贴纸」打开有贴纸格式筛选的原生多选文件对话框。接受 PNG/JPG/JPEG/TIFF/HEIC/WebP、Lottie JSON、`.lottie`;扩展名集合与既有 Rust `ClipType::from_file_extension` 一致,JSON/Lottie 内容校验继续由 Rust 负责。 +- 面板从现有 `useMediaStore` 镜像筛选整个工程的 `image` / `lottie`,包括子文件夹素材。不新增贴纸模型或分类持久化字段,不引入商店、网络请求、生成素材或依赖。 +- 复用 `MediaCard` 的缩略图、单击/键盘选中及源预览、拖拽 MIME、收藏和重新链接。增加可键盘访问的「将选中贴纸加入时间线」按钮;双击经同一受控回调调用既有 `addMediaToTimeline`,拖拽继续交由现有时间线接收。 +- 落轨继续经既有 `placeMedia` 单事务处理,前端没有新增 Undo 栈或领域编辑逻辑。定向测试核对入口与既有事务行为;真实桌面 Undo 仍待主代理验收。 +- 空态及状态使用 `role=status`,失败使用 `role=alert`,按钮通过 `aria-describedby` 关联提示。无工程、只读工程、素材离线、生成/下载中及生成失败/取消时给出原因并禁用相应操作。 +- 整个导入手势(包括文件选择器)复用 `beginMediaImport` / `endMediaImport` token。重复点击被拦截;工程 epoch/path 变化后的选择结果、刷新和错误不会写回新工程,旧操作结束不会清除新工程的导入状态。 +- StickerPanel 同工程跨标签保持挂载,切走时仅卸载卡片视图,保留 pending/ref/错误;贴纸局部状态以 epoch/path 为 React key 重建,旧工程落轨失败不进入新面板。共享方向键导航尊重卡片 `aria-disabled`,不会通过方向键预览不可用贴纸。 + +## Read Set 与修改范围 + +已阅读根 AGENTS.md、`docs/project/conventions.md`、Web OVERVIEW/INDEX 路由,以及现有 MediaPanel、MediaTabBar、mediaActions/mediaStore/editActions、类型、对话框/API 与 Rust 图片类型/导入实现。 + +只读上游参考: + +- `../palmier-pro-upstream/Sources/PalmierPro/MediaPanel/MediaPanelView.swift` +- `../palmier-pro-upstream/Sources/PalmierPro/MediaPanel/MediaTab/AssetThumbnailView.swift` +- `../palmier-pro-upstream/Sources/PalmierPro/Editor/ViewModel/EditorViewModel+MediaLibrary.swift` + +实际修改仅以下文件: + +1. `web/src/components/media/MediaPanel.tsx`:贴纸内容、导入/落轨协调;为共享卡片增加可选回调与禁用原因。 +2. `web/src/components/media/MediaTabBar.tsx`:启用标签。 +3. `web/src/components/media/MediaTabBar.test.tsx`:新增可导航标签后的转场导航步数。 +4. `web/src/components/media/StickerPanel.test.tsx`:新增真实 MediaPanel/MediaCard DOM 交互测试。 +5. `web/src/i18n/dict.ts`:贴纸中英文文案。 +6. 本记录。 + +没有修改测试配置、版本、Rust、其他文档;没有 commit/push/release。现有并行未提交改动保留。 + +## 验证证据 + +TDD:先写 11 项 Sticker 交互用例,旧面板下 11 项失败(置灰、缺少卡片和动作入口);实现后通过。补键盘导航/工程失败隔离等边界至 16 项;方向键用例先复现失败,再补网格导航容器与禁用预览判断后通过。另补用例复现旧导入错误遮盖新落轨错误,改为同时展示后通过。 + +最终执行: + +```bash +pnpm --dir web test --run src/components/media/StickerPanel.test.tsx src/components/media/MediaTabBar.test.tsx src/components/media/MediaPanel.test.tsx src/store/editActions.test.ts +pnpm --dir web build +git diff --check -- web/src/components/media/MediaPanel.tsx web/src/components/media/MediaTabBar.tsx web/src/components/media/MediaTabBar.test.tsx web/src/i18n/dict.ts +``` + +- P2 修复后的最终结果:4 个测试文件 / 117 项通过,其中 Sticker 19 项(原 16 项 + 跨标签/工程隔离 3 项)。 +- `tsc -b && vite build` 成功。仍有动态/静态 import 不能分块和大于 500 kB chunk 的构建告警;本切片未做打包重构。 +- 最终 diff 已检查。未重复全量测试,留给主代理统一验证。 +- 测试使用真实 React 卡片、Zustand 镜像和对话框/媒体 API 边界替身;Sticker DOM 用例中落轨 action 被 mock 来检验手势次数/失败门控,另运行既有真实 editActions 定向测试。不能把这些测试写成真实原生解码/撤销验收。 + +## 冻结 patch 审查 P2 修复 + +审查指出并经本地 DOM 用例复现:原 `active ? : null` 在切换标签时卸载组件,重置 `placing` / `placementPending`,旧落轨尚未完成时返回已可再次提交;旧实例 `alive=false` 也会丢弃失败反馈。 + +最小修复:MediaPanel 对 StickerPanel 保持稳定挂载,传入 `active`;StickerPanel 完成所有 hooks 后在非活动状态返回 null,仅卸载卡片视图,不丢弃该工程的操作状态。父级继续用 epoch/path key 隔离工程,不新增全局 store。 + +本次新增 3 项回归: + +- 可见期间失败和隐藏期间失败两种情形:落轨中切走再返回,导入/落轨按钮仍禁用,双击不重复提交;失败返回后可见;点击重试只新增一次调用,成功后解除 pending。 +- 旧工程落轨在隐藏状态切换到新工程,新工程可独立提交;旧 promise 失败不显示到新工程,也不清除新工程的 pending。 + +TDD 证据:两个跨标签用例先在“返回后按钮仍禁用”断言失败;修复后原定向集合及新增用例全部通过(117 项),`pnpm --dir web build` 成功,原动态 import/大 chunk 告警仍在。测试 setup 对落轨 mock 使用 `mockReset`,防止失败用例未消费的 once promise 污染后续测试。 + +本次追加修改只有 `MediaPanel.tsx`、`StickerPanel.test.tsx` 和本记录,未改 Rust、配置、其他 MD;未运行全量测试。 + +## 待主代理候选包 GUI 验收 + +1. 在新 Beta 6 候选原生应用通过「导入贴纸」各导入透明 PNG、有效 Lottie JSON、`.lottie`,核对名称、缩略图/源预览、透明效果与动画播放;确认取消和无效 JSON 提示。 +2. 单击、方向键、Tab 操作与选中按钮落轨;双击与拖拽各落轨一次,核对轨道/时长/合成画面;每次落轨执行一次 Undo、Redo,确认完整撤销/恢复本次操作。 +3. 嵌套文件夹素材可见;将素材移走后确认离线状态及重新链接恢复。检查只读项目、生成/下载中及失败卡片提示。 +4. 落轨 pending 时切走/返回,确认禁用持续、失败可见及重试一次;导入进行中切换面板/工程,核对旧结果/错误不覆盖新工程,连续点击不重复提交;检查中文/英文、窄面板滚动和屏幕阅读器导航。 +5. 保存重开工程,确认贴纸仍来自同一媒体镜像,落轨片段及资源引用正常。 + +本任务未运行原生 GUI;不据此声明 Beta 6 已发布或全平台已验收。 diff --git a/docs/audit/2026-09-07/motion-4k-screencast.md b/docs/audit/2026-09-07/motion-4k-screencast.md new file mode 100644 index 00000000..dbbbcfff --- /dev/null +++ b/docs/audit/2026-09-07/motion-4k-screencast.md @@ -0,0 +1,128 @@ +--- +status: canonical +stage: implementation-backed +updated_at: 2026-09-07 +--- + +# macOS Motion 4K 截帧超时专项 + +## 范围与验收 + +用户授权仅修改 `crates/opentake-motion/src/renderer.rs`、必要的 `tests/chromium.rs` 与本专项 audit/knowledge。实际产品修改集中在 renderer,现有 Chromium 集成测试保持原断言。模型文件、Tauri/worker/export、浏览器启动参数、180 秒预算和其他文档均不在写集。 + +Read Set:当前 AGENTS;Motion OVERVIEW/INDEX/renderer 文档;renderer 与 tests/chromium;主线 `output/public-beta-2026-09-06/rust-final-tests.log`、`chromium-isolated-retry.log`;Chrome 对应版本公开 page_handler.cc。 + +验收顺序:单 4K 原样复现 → 最小 trace 定位 → 源码解释 → 协议顺序修复 → 单 4K opaque/transparent → 协议回归单测 → 完整 Motion 单测和 Chromium 组 → 定向 fmt/clippy。全程不操作电脑 GUI;浏览器只由授权的 headless 测试进程启动。量测前通知主代理暂缓 Cargo/GUI验收,空档由其独立处理导出主线程问题;等待到 build 锁释放后才进入最终测试。 + +## 修复前证据 + +- 主线全量:4K opaque 在 `tests/chromium.rs:432` 报 `Timeout(180s)`,随后三个 Chromium 测试因共享 test gate 被 poison 连带失败。 +- 主线独立 trace 重跑:opaque 10688 ms 通过,transparent 在 line456 命中 180 秒预算。最后停在 black transition guard 等待,virtual-time budget 已正常结束。 +- 本任务仅加 opt-in trace 的单 4K 复现仍失败,0 passed / 1 failed,180.18 秒。trace 显示期望 transition `[1,0,165]`,实际依次收到白色、seed `[1,0,90]`、seed `[1,0,90]`;三个帧的 ACK 均成功,随后没有新帧到达。不是 PNG 解码或 ACK 本身卡住。 +- 实际 Chrome 进程版本 152.0.7977.76;GPU trace 为 Apple M4 / ANGLE Metal,GPU compositing enabled。没有换浏览器或关闭 GPU。 + +## 根因与最小修复 + +对应 Chrome 版本的 screencast 窗口实际容纳三个未 ACK 帧;窗口满时新的 compositor 帧直接丢弃,ACK 只释放额度,不重放被丢弃画面。官方来源、版本和有效期统一记录在[专项知识记录](../../knowledge/2026-09-07-motion-4k-screencast.md)。 + +原协议在开始消费之前连续发布 seed、作者 marker、transition。启动旧画面和 seed 更新可能先占满发送窗口,使唯一的 transition 绘制被丢弃。之后页面静止,即使逐一 ACK,等待目标 guard 的循环也不会再得到对应像素。 + +修复在 `capture_isolated_viewport` 中增加 **seed + 作者 marker 的一次提交握手**。真正收到且 ACK seed 代际后,才发布 transition。原有 transition→desired 检查、三次作者 marker 采样/一致性、黑白透明度重建、CSP/Fetch、取消与清理路径保持。没有增加重试、加长 timeout 或放宽像素检查。 + +trace 仅在 `OPENTAKE_MOTION_TRACE` 开启时输出等待/ACK/guard/marker 状态;角点来自作者画布外的宿主 guard,不输出作者画面像素或 nonce。 + +## 回归覆盖 + +- 模拟 Chrome 三个未 ACK 启动帧的满窗口,严格要求全部 ACK 后才能发布 transition,并继续拒绝旧 guard。 +- 透明协议 mock 增加 seed 阶段;seed 颜色正确而作者 marker 过期时仍必须拒绝。原有两种背景、每种三个作者代际、错误 marker/旧 session 检查和逐像素 alpha 重建断言保留。 +- pre-start / post-start 错误仍测试 detach、stop 以及原始错误优先级;CSP late-request、帧 ACK、作者隔离、稳定读回、取消和缓存测试保留。 + +## 执行记录 + +```sh +OPENTAKE_MOTION_TRACE=1 cargo test -p opentake-motion --features chromium \ + --test chromium --jobs 2 four_k_single_frame_opaque_and_transparent_budget_smoke \ + -- --exact --nocapture --test-threads=1 +``` + +修复前:0 passed / 1 failed,180.18 秒。修复后:**1 passed / 0 failed**,总计108.02秒;opaque **39567 ms**,transparent **64435 ms**,每段均保持原180秒预算,透明段全像素非平凡 alpha 断言通过。这是共享宿主下的 debug/trace 运行记录,不是稳定性能基准。 + +```sh +cargo test -p opentake-motion --features chromium --lib guarded_candidate \ + --jobs 2 -- --nocapture --test-threads=1 +``` + +三项 guard/清理协议单测通过。 + +初次全模块运行有96项通过、1项透明协议 mock 失败:mock 尚按旧顺序期待 transition,而产品已先执行 seed 握手。仅补齐该 mock 的 seed/错误作者 marker 事件,不改其 alpha/CSP/三代际验算标准。 + +最终关闭 `OPENTAKE_MOTION_TRACE`,避免把日志输出改变时序误当成修复: + +```sh +cargo test -p opentake-motion --features chromium --lib --test chromium \ + --jobs 2 -- --nocapture --test-threads=1 +cargo clippy -p opentake-motion --features chromium --lib --test chromium \ + --jobs 2 -- -D warnings +rustfmt --check --edition 2021 crates/opentake-motion/src/renderer.rs \ + crates/opentake-motion/tests/chromium.rs +``` + +结果: + +| 验证 | 实际结果 | +|---|---| +| Motion 完整单测 | **97 passed、0 failed**,5.72秒 | +| Chromium 完整组 | **7 passed、0 failed、0 ignored**,130.72秒 | +| 全组中的 4K opaque | **18870 ms**,原全像素 opaque 断言通过 | +| 全组中的 4K transparent | **30293 ms**,原全像素非平凡 alpha 断言通过 | +| 定向 clippy `-D warnings` | 通过;仅有已有依赖 block 0.1.6 的 future-incompatibility 提示 | +| 格式 / 最终 diff 空白检查 | 通过 | + +完整 Chromium 组包含浏览器池失效/复用、并发错误、host/CSP/guard、预览确定性、网络/虚拟时间/超时清理/帧身份。没有只凭 opaque 通过收尾,也没有 poison 后跳过余下测试。 + +结论:本机可重复的 4K 截帧阻塞已在原浏览器上收敛。属于产品对 Chromium 发送背压的顺序缺陷,宿主/4K 时序是触发条件,不应简单归为主机环境故障。代价是每个隔离读回增加一次 seed 提交握手;原有每次 render 的时间预算不变。以上是 debug 构建的实测记录,不承诺稳定吞吐。 + +实际写集仅 `renderer.rs` 与两份专项文档;`tests/chromium.rs` 没有修改。2026-09-07 本轮定向检查结束后已通知主代理恢复 Cargo/GUI,产品代码冻结。最终发布仍应以主线整合提交及其跨平台 CI 为准;修复后的 Windows/Linux 本轮未原生重跑,不沿用修复前 Windows 4K gate 当作新提交的通过证据。 + +本地日志:`/tmp/opentake-motion-4k-20260906/{single-4k-red.log,single-4k-seed.log,guard-unit.log,motion-full.log,motion-full-final.log,clippy.log}`;对应版本 Chromium 源码为同目录 `page_handler.cc`。未写主线 output 目录,未提交/push/tag/release。 + +## 第三轮 CI 后的残余问题(候选 aa40672 / run 34047983006) + +上一轮仅证明 macOS 本机通过,不能视为跨平台收敛。主线保留的 `output/public-beta-2026-09-06/ci-third-failures.log` 提供了两个新的确定触发点: + +- Windows 第一个 opaque capture 在 **seed 阶段**收到三张旧白色帧,外部 guard 与作者 marker 均为 false,ACK 全正常;此后 seed 永不到达,180秒超时。不是后续 capture 的旧 session 泄漏才能触发,第一轮启动已足以复现。 +- Linux 的透明协议 mock 在 capture-4 提前清理。mock 构造的是 `Instant::now() + 1s`,但 `SandboxPolicy::default()` 报错标签为60秒;`check_abort_state` 按传入 deadline 判断、按传入 timeout 展示,因此 `Timeout(60s)` 实际来自1秒测试 deadline,并非真实运行60秒。每次分开发送的响应/帧出现约41ms延迟,累计到第五次 capture 跨过1秒。 + +### 本轮最小改动 + +实际源码写集仍只有 `renderer.rs`,`tests/chromium.rs` 与全部实际像素/alpha/CSP验收保持原样。 + +1. capture 启动时不再先改 seed。先 startScreencast,消费/ACK 第一张启动 PNG,并 ACK 已经排队的旧启动帧;这些只用于释放传输窗口,绝不作为输出图像。 +2. 然后单独发布 seed、等待外部 guard;再单独更新作者 marker,等待同一 seed 与新 marker 同时成立;最后沿用 transition→desired 与三代际/双背景像素校验。每次通过一个 fence 后 ACK 已排队的同代或旧代帧,再发布下一次修改。 +3. 删除 `capture_stable_background` 在流启动前的冗余颜色写入,避免它额外占用未确认的启动窗口。最终背景仍由各 capture 的 desired 阶段设定并验证。 +4. 不重写 attach/detach 或 start/stop 总生命周期。第三轮首个 capture 已失败,现有证据不足以把反复创建 PageHandler 本身定为根因;独立 session 和晚到事件隔离保持。 +5. Linux mock 的 TCP 两端启用 `TCP_NODELAY`。生产 `tungstenite 0.29.0::connect_with_config` 在 `client.rs:68` 本就启用 NoDelay,raw-socket mock 原先没有。测试仍用原1秒 deadline,仅把 policy 标签也改为1秒,没有增加任何测试/产品预算。 + +新回归把启动窗口设为“三张全旧”,严格要求在第一个 seed 修改前全部 ACK。修复前0 passed/1 failed,错误为原代码先发送 Runtime.evaluate、mock期待Page.startScreencast;修复后该测试与两项原始错误/清理测试全部通过。透明 mock 继续覆盖旧 session、错误 guard、错误作者 marker、三代际与全像素 alpha;没有删除断言。 + +### 本轮本机结果 + +| 检查 | 结果 | +|---|---| +| Linux同构 raw-socket mock 修正 NoDelay 后 | 1 passed / 0 failed,0.03秒;原1秒 deadline不变 | +| 全部 Motion 单测 | 97 passed / 0 failed,2.99秒 | +| trace开启的单4K | 1 passed / 0 failed,20.16秒;opaque7521ms、transparent11892ms | +| trace关闭的完整 Chromium 组 | 7 passed / 0 failed,51.62秒;opaque7308ms、transparent12093ms | +| 定向 clippy `-D warnings`、rustfmt、diff检查 | 通过 | + +日志位于 `/tmp/opentake-motion-ci-round4/`:`linux-mock-nodelay.log`、`startup-red.log`、`startup-green.log`、`unit-final.log`、`real-4k.log`、`chromium-final.log`、`clippy.log`。两项平台原因与生产传输配置来源补充在同名 knowledge。 + +**当前状态是本机候选已冻结,仍待真实 Windows/Linux 对这份新补丁的资格结果。** 已通知主线提交/push,未自行 push,未沿用 aa40672 的失败 run 或上一轮本机成功来宣布跨平台通过。 + +### 独立临时资格 workflow 提案 + +可审查 YAML 已准备在 `/tmp/opentake-motion-ci-round4/motion-readback-qualification.yml`,尚未写入 `.github/workflows/`,由主线决定纳入新候选。YAML 已解析检查,使用现有仓库已固定的 checkout/cache/upload-artifact action revision。 + +提案仅新增 `.github/workflows/motion-readback-qualification.yml`:Windows-2022 与 Ubuntu-24.04 两个 job,精确40位提交SHA校验、只读 contents 权限、jobs=2、trace日志归档。运行完整 `cargo test --locked -p opentake-motion --features chromium --lib --test chromium --jobs 2 -- --nocapture --test-threads=1`,要求确实出现97项单测通过、7项Chromium通过且opaque/transparent两段计时存在。没有额外重试、没有改180秒测试预算,不替换浏览器,不修改已有ci.yml/release.yml合同。 + +触发方式为 release 分支相关路径 push(可对新增workflow首次执行),或手动dispatch指定不可变SHA。主线提交并触发后,须读取两个真实job日志确认结果;如仍失败,以新trace继续诊断,不把本机绿灯当成最终结果。 diff --git a/docs/capabilities/CAPABILITY-LEDGER.md b/docs/capabilities/CAPABILITY-LEDGER.md new file mode 100644 index 00000000..1da134e5 --- /dev/null +++ b/docs/capabilities/CAPABILITY-LEDGER.md @@ -0,0 +1,209 @@ +--- +id: capabilities.ledger +title: OpenTake Capability Ledger +summary: 首轮上游对齐和安装版 UI 实测的模块级状态;详细证据按能力 ID 继续累积。 +kind: engineering +status: draft +content_stage: partial-implementation +scope: + - all-modules +triggers: + - parity + - 上游对齐 + - 功能完成度 + - 真实验收 +read_when: + - 修改公共能力或判断模块是否完成 +skip_when: + - 仅查看单个内部函数 +priority: must +freshness_class: project +last_verified: 2026-09-06 +owners: + - OpenTake-generation +source_of_truth: + - ./requirements.json + - ../audit/2026-08-21/full-desktop-functional-matrix.md +related: + prerequisites: + - ./README.md + next: + - ../superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md +supersedes: [] +tags: + - capabilities + - parity + - evidence +--- + +# Beta 6 当前状态 + +本轮规范来源仍为 [`requirements.json`](./requirements.json),实际执行证据见 [2026-09-06 验证记录](../audit/2026-09-06/public-beta-validation.md)。下方八月记录保留历史属性。 + +- 新增贴纸面板:图片/Lottie 导入、预览、选择、落轨及跨标签请求状态保留,Web 交互验证通过;macOS 新包已实际完成图片贴纸导入、预览、落轨和组合导出。 +- 语义搜索:固定公开模型资产与校验清单,真实 macOS 模型推理、图文检索和旧索引隔离已验证;损坏模型提供修复入口,文件名/口播结果不受视觉失败影响。Windows 同版本 Tract 的线程接口和动态形状问题已通过固定输入适配修复,真实共享后端双模型及中英查询在宿主测试通过;Windows/MSVC 实机资格待远端流程确认。 +- 真实媒体集成、原生音频时钟播放探针、真实 Keychain/MCP 生命周期验证已通过;这些证据各自有范围,不能代替安装包 GUI 或全平台验证。 +- 测试现在使用按测试文件隔离的 Storage;无须历史 NODE_OPTIONS localStorage 文件参数。 + +# 2026-08-21–22 历史状态 + +这不是最终完成声明,而是 2026-08-21 的可恢复基线。机器可读条目见 [`requirements.json`](./requirements.json);安装版 UI 场景见 [`full-desktop-functional-matrix.md`](../audit/2026-08-21/full-desktop-functional-matrix.md)。首轮上游 agent 按实际代码和两条定向测试落锚,未把历史 handoff 当成当前证据。 + +## 已有代码链路 + +- Models、Timeline、Track、Clip、Keyframe、Transform、TextStyle:domain crate 已有实现,需继续做完整行为级枚举。 +- EditCommand、事务边界、undo/redo、split/trim/move/ripple:ops/core 已有上游对齐实现;`split_clip_distributes_keyframes_at_cut` 通过。 +- Inspector、字幕/转写/搜索、Agent/MCP、Motion Studio opaque MP4:代码和对应测试入口已存在;仍需安装版场景证据才能升级为 verified。 +- Motion Studio transparent alpha:Rust/Web 纵切已接通并通过全量自动化;最新包已完成透明开关、发布进度、时间线落轨和保存重开屏幕验收;导出面板已新增透明 ProRes 4444 codec,真实 GPU/FFmpeg integration 已复核 `yuva444` alpha;透明导出屏幕仍待解锁后复验。 +- Agent/MCP linked move parity:`move_clips` 现在和上游约定一致,显式 `toFrame` 会把 frame delta 传播到 linked A/V partner,track-only move 不传播;新增两条 dispatcher 回归测试。最新安装版还验证了 Agent 面板标签生命周期、无通道时发送禁用和快捷键收起布局。 +- 上游收敛新切片:`create_folder.entries` 与 `move_to_folder.entries` 已接入单一 EditCommand/Undo 事务;普通媒体导入现在接受有效 `.json` / `.lottie`,用 Velato 校验并写入 Lottie metadata,坏文档在批量入口跳过,MCP 单文件入口返回明确错误。 +- OpenTake 扩展:MediaPanel 的 Text 标签已接入既有 `addTextClip()`/Inspector Text 工作流并保持单步 Undo;此处是当时的 Sticker/Effect 占位状态;当前 Sticker 已实现,Effect 预设已接链接选区的视频片段及单步撤销,见本轮候选说明。 + +## 首轮明确缺口 + +| ID | 优先级 | 当前状态 | 最小实施切片 | 验收 | +|---|---:|---|---|---| +| `OT-WINDOW-SMALL-SCREEN` | P0 | verified | Tauri 安全初始尺寸 + monitor-aware standard/compact 裁剪 + Home/Editor min-size 回归 | 1066×666、1280×720、1331×768 contract 测试;安装版紧凑/标准切换与 Home/编辑器截图 | +| `UP-MEDIA-IMPORT-AND-FOLDERS` | P0 | verified | 文件/文件夹导入、跳过 unsupported、relink 和预览均已通过安装版验收 | 新构建 app 中 MP4、PNG、双文件多选 Open 可用;导入数 1→3;文件夹导入出现目录并提示跳过 64;relink 恢复离线媒体 | +| `UP-MEDIA-LOTTIE-IMPORT` | P1 | partial | 对齐上游 `.json/.lottie` 导入;Velato JSON 校验、`.lottie` ZIP `animations/*.json` 提取、metadata 写入和坏文件 fail-soft 已通过自动化;安装版 JSON 卡片、素材预览、双击落轨和最近项目重开已通过,容器屏幕仍待 | `cargo test -p opentake-core importable_clip_type_covers_whitelist_and_rejects_others`;Tauri Lottie JSON/容器/MCP path 定向测试;安装版 JSON 屏幕对拍 | +| `UP-TIMELINE-UI` | P0 | partial | 已补 Shift+标尺范围标记、范围 start/end 边缘命中/拖动、标尺吸附、取消回滚、同帧清除、PointerEvent 回归,以及上游 playhead split/trim 的无选区 no-op 语义;继续补真实删除、撤销及拖拽对拍 | Timeline/编辑动作定向 7 files / 97 tests;Web 全量 151 files / 1419 tests;最新包 `acd7b105…c319` 已安装但本轮屏幕重跑因 Mac 锁定 blocked | +| `UP-PREVIEW-PLAYBACK` | P0 | partial | compositor temporal route、speed/reversed 原生帧映射、Lottie native compositor 路由、音频整轨解码、缺失媒体 fail-closed 和 WebKit >0 dB GainNode 路由已补齐;安装版与 preview/export 全面对拍仍继续 | Render 18 项、Playback resolver 15 项、Playback integration 8 项、Preview 目录 20 files / 196 tests、Web 152/1425 通过;Lottie JSON 时间轴画面/播放头/截帧已安装版验证,preview/export 同帧语义、实时音画同步、GainNode 听感、取消和 `.lottie` 容器屏幕仍待 | +| `UP-EXPORT` | P0 | partial | H.264/AAC、H.265、ProRes 422 和字幕命令已有实现;普通输出失败清理、external cancel 全链路、输出父目录/文件 identity 校验已补齐;本轮修复 ExportDialog 在进度订阅 pending 时丢失取消意图的竞态 | 最新包已补 H.265/AAC、ProRes 422 HQ/PCM、透明 ProRes SavePanel 和字幕 SRT/VTT SavePanel 屏幕证据;Shell export 39/39、取消竞态自动化通过;真实导出中途取消、实时音画同步和首中尾对拍仍待 | +| `UP-PREVIEW-TABS` | P1 | verified | `previewTabIds + previewTabHistory + activeTabId`,含 legacy 归一和删除清理 | 安装版同时显示 Timeline/两个素材 tab;关闭第二个回退第一个;91 个定向测试通过 | +| `UP-MEDIA-VIEW-MODES` | P1 | verified | folder/flat/grouped 三态投影、网格/列表密度、文件夹导航和音频导入入口已通过测试及安装版验收 | 搜索、选择、拖拽和预览沿用同一 MediaItem ID 链路,继续纳入后续模块化 QA | +| `OT-MOTION-ALPHA` | P1 | partial | Motion Studio 透明发布开关、导出面板 ProRes 4444/yuva444、manifest straight-alpha provenance | 透明 motion 覆盖底图、预览/导出一致,并完成安装版时间线导入对拍;代码/自动化已通过,透明导出屏幕和 opaque→transparent 编辑仍待 | +| `UP-ACCOUNT-CLOUD-BOUNDARY` | P0 | blocked | 先定义 BYOK/provider 替代还是追同构云契约,再补 capability gate | 未授权时不广告/不发送;有凭据时生成→媒体→落轨闭环 | +| `UP-AGENT-FOLDER-BATCH` | P1 | verified | `create_folder.entries` / `move_to_folder.entries` 批量参数与上游一致,分别返回 folder records/汇总并保持单步 Undo | `cargo test -p opentake-agent folder_batch`;Agent lib 417/417 | +| `UP-SETTINGS-TAXONOMY` | P2 | partial | 明确 models/agent/storage/general/account 语义映射 | 设置入口和上游语义一一可解释 | + +## 真实 UI 证据摘要 + +## OT-MOTION-ALPHA 当前纵切 + +- Rust:`DocumentMotionAddRequest.transparent` 已贯通到模板渲染、`MotionRenderRequest.with_transparent(true)`、ProRes 4444 `.mov` 编码和项目媒体落盘;title-card/lower-third 模板在透明模式下不填充不透明底色;编辑已有透明 Motion 时从 manifest provenance 保留 `.mov`/alpha 格式。 +- Manifest:`GenerationInput.transparent` 持久化为 `true`,`MediaManifestEntry::carries_straight_alpha()` 对透明 Motion 返回 `true`;既有 RVM matting provenance 规则保持不变。 +- Web/Agent:Motion Studio Inspector 新增“透明背景(ProRes 4444)”开关;预览请求保持原 schema,只有发布请求带 `transparent` 字段,切换项目时重置为关闭;Agent `add_motion_graphic` 和 `publish_motion_document` 的新增 clip 路径也接受透明发布,已有透明 clip 的文档编辑保留 alpha。 +- 自动化证据:`src-tauri/tests/motion_command.rs` 的透明发布集成测试验证 `.mov`、ProRes、64×36 尺寸、完全透明像素和半透明动画像素;Rust workspace 串行全量通过(Tauri 720 tests、Motion Chromium/integration、导出/播放等);Web 全量 `151 files / 1415 tests`、Web build 通过。 +- 文件级证据:`/private/tmp/opentake-audio-desktop-qa-L8Nvwh/audio-preview-export-qa.opentake/media/motion-0e4cacc9-161a-4704-a790-7e233397c8c4.mov` 被 `ffprobe` 识别为 `prores` profile `4444`、tag `ap4h`、pixel format `yuva444p12le`、90 帧/3.000s;`ffmpeg -vf alphaextract` 成功,抽样 alpha 平面为非全黑值;对应 `media.json` 的 `generationInput.transparent` 为 `true`。 +- 安装包:当前 `/Applications/OpenTake.app` 二进制 SHA-256 `2a5f8d72b8bac29c1f92d85c418a4987e8748eabe1fabd3b157f038db720034e`,包含 WebKit >0 dB GainNode、Lottie native timeline route 和 ExportDialog cancel-race 修复;本轮已完成 JSON Lottie 卡片/预览/落轨/最近项目重开/时间轴画面/播放头屏幕验收。透明 ProRes 4444 SavePanel、字幕 SRT/VTT SavePanel 已打开并取消;GainNode 听感、可交互取消屏幕和 `.lottie` 容器屏幕仍保持 partial。 + +最新安装版(2026-08-22 12:22):`/Applications/OpenTake.app` SHA-256 `eaad6fb35b43e39884b8b756090548eace9559e366c1bd3eedcc37b6fb599e57`,包含 Text/Effect 面板接线和 linked V1/A1 Effect 筛选修复;Text/Effect 自动化通过,Effect 入口按钮已实机启用,但点击后截图/Undo 尚未闭环,Sticker 仍为占位。 + +## 上游 linked A/V parity 当前切片 + +- Agent/MCP 的 `move_clips` 入口已补齐上游 `partnerMoves` 语义:只提供 `toFrame` 时按 lead 的时间差移动同组伙伴,并保留伙伴所在轨道;只提供 `toTrack` 时不改伙伴时间;调用方显式列出伙伴时不再生成重复 move。 +- 证据:`cargo test -p opentake-agent --lib -- --test-threads=1` → 415 passed;新增测试覆盖 frame delta propagation 和 track-only no propagation。Web Timeline 拖拽原有 linked expansion 保持不变。 + +## 最新安装版模块入口证据 + +- Inspector:选中 V1 后视频页展开变换/裁剪/翻转/淡入淡出/运动追踪/防抖/抠像/速度/调色;水平翻转切换后由 Undo 恢复。音频页显示音量、响度归一化、降噪和人声/伴奏分离;AI 编辑页生成本地启发式建议,应用后显示“可撤销编辑命令”,再撤销恢复。 +- 字幕:字幕样式、位置和翻译同意项可见;点击生成字幕时在无模型环境明确提示下载约 141 MB 的 multilingual 转写模型;无同意时翻译按钮保持禁用。 +- Agent:面板可打开,新建/关闭对话标签可用;未配置通道时发送保持禁用;`⌘⌥A` 可收起并恢复编辑器布局。未发送外部消息。 + +- 约 `1331×768`:标准窗口配置偏大,最近项目区域宽度控制弱。 +- 紧凑档约 `1066×666`:可见、可操作,当前切换行为通过。 +- 新建/打开项目可弹原生 sheet 并取消;示例项目可打开;播放头会推进;选中文本片段能联动 Inspector;Agent/Motion/导出/帮助入口可达。 +- 首轮导入图片时选中后 Open 仍禁用,导入失败阻塞素材预览闭环;时间线分割在帧 0/片段外无变化,但在有效选区和帧 15 前置条件下已实测新增片段并启用撤销。 +- 本轮修复后,文件导入 MP4/PNG/双文件多选均能点 Open,导入后素材预览可见;文件夹导入成功出现目录并提示跳过 unsupported;relink 成功恢复离线媒体。 +- Preview tabs 已在安装版打开两个素材并验证关闭回退;当前 Preview/Media slice 的定向测试为 4 files / 91 tests passed。 +- Preview temporal parity 已补 route、native surface 和 source-frame rewind reset;安装版 QA 工程设置 `speed=1.5` + 曝光 compositor 属性后没有 unsupported surface,时间线可见、播放头可到尾帧、暂停后抓帧按钮可用。另修复 Lottie 被 route 误判为 unsupported 的缺口,安装版 JSON Lottie 时间轴在 02:40 显示画面并推进到 02:50,播放/截帧可用。 +- 时间轴范围切片已补 Shift+标尺 range mark、已有范围 start/end 边缘命中/拖动、clip-edge/playhead snap、pointercancel/lost-capture 回滚和同帧清除;最新安装包按上游顺序完成 I/O 标记范围→选 V1 锚点→Shift+Backspace,V1/A1 联动删除、Motion 保留、播放头/时间码同步到新 3 秒长度、Undo 恢复。同步刷新新增 playhead clamp;Timeline/编辑动作定向 7 files / 97 tests,Web 全量 151 files / 1419 tests、tsc、Web build 通过。Option trim 屏幕坐标拖动仍待。 +- 带音频 fixture `nested-timeline-compound-export-2026-07-31.mp4` 已通过 `ffmpeg -xerror` 音视频解码;安装版出现 V1+A1、A1 波形和音频 Inspector,并完成起点/中点/终点 seek 与暂停稳定性检查。fixture 为 7.700s H.264 1280×720/30fps/231帧 + AAC 48kHz 单声道;独立实时听感级音画同步仍未完成。 +- 带音频导出真实证据:在安装包 `03bdcef36def17646c64e6c40978e26b6b675a5d90874e3264d93a0e3351ce2e` 中生成 `/private/tmp/opentake-audio-export-qa-YPhWaR/qa-h264-aac-export.mp4`;ffprobe 为 H.264 1280×720/30fps/231 帧 + AAC 48kHz 单声道/362 包/7.700s,视频和音频均通过 `ffmpeg -xerror`。源/导出首中尾 SSIM:`0.999997 / 0.999997 / 1.000000`,帧文件在 `/private/tmp/opentake-audio-frame-compare.M2Jvfu/`。这证明一次真实 preview-source/export 对拍,但不是听感级实时同步证据;cleanup 版最新包 `ca08edf97441ce3b3c69b5683a3b7383997371952490c8897021772e521eed64` 的 SavePanel 复测仍保持 partial。 +- 导出文件证据:包 `893b6ed0…d0ba` 生成 H.265/AAC `/private/tmp/opentake-audio-desktop-qa-L8Nvwh/audio-preview-export-qa.mp4`(HEVC `hev1` + AAC,7.700s)和 ProRes 422 HQ/PCM `/private/tmp/opentake-audio-desktop-qa-L8Nvwh/audio-preview-export-qa.mov`(`apch`/`yuv422p10le` + PCM,7.700s),两者均通过完整 `ffmpeg -xerror`。新增 `OPENTAKE_RUN_FFMPEG_TESTS=1 cargo test -p opentake-tauri --test export_integration export_prores_4444_preserves_transparent_text_alpha` 验证 ProRes 4444 `yuva444p10le/12le` 和 alpha 0→非零。导出失败边界:`0bae80e` 增加 identity-safe 普通输出 guard、replacement race 测试、双 cancel source fail-closed;`a7d98d6` 再拒绝最终输出路径的 symlink/reparse identity,并新增替换为 symlink 的回归测试。当前 `export::tests` 72/72、`opentake-media` cancel 18/18、带音频 integration 6/6 通过。 +- 上游 playhead parity:`f05bac8` 修复无选区 `splitAtPlayhead` 不应发编辑请求,`478e1b4` 修复无选区 Q/W trim 不应发编辑请求;相关 Web 全量门禁 `151 files / 1412 tests` 通过。最新 app `de4cb52b…` 已构建并安装,但 Computer Use 因 Mac 锁定未能完成本轮屏幕 smoke。 +- 缺失媒体 fail-closed:`741ff07` 让普通 Preview 缺失图片返回 materialization error,Playback Image/Text 缺失通过 `RenderLoop::render_frame` 返回错误而不是发布黑帧,并统一 Playback/Export 错误文案;新增 render 18、resolver 15、playback integration 8 项测试,Tauri lib 719 项和顺序 workspace 全量均通过。 +- 曾出现 AX 更新但截图未重绘的黑屏瞬间;关闭/重开后页面正常,后续以窗口重绘时序风险记录,不把瞬态截图直接等同于产品黑屏。 + +## 定向验证 + +| 命令/场景 | 结果 | 说明 | +|---|---|---| +| `cargo test -p opentake-ops split_clip_distributes_keyframes_at_cut` | 通过 | 上游 split 关键帧边界对拍锚点 | +| `cargo test -p opentake-agent --test advertised_tool_acceptance` | 通过 | 工具广告面锚点 | +| `NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-localstorage.json pnpm exec vitest run src/store/settingsStore.test.ts src/components/home/HomeView.visual.test.ts src/components/shell/SplitPane.interaction.test.tsx` | 3 files / 22 tests 通过 | Node 26 需要显式 localStorage 文件 | +| `pnpm test`(无 Node localStorage 参数) | 15 files / 125 tests 初始化失败 | 环境假红:`localStorage` 未提供,不作产品失败结论 | + +## Task 2 新鲜验收证据 + +- 提交范围:`ff6177e8`(主实现)、`2235c638`(SplitPane 测试)、`32c8c3c`(review 修复)、`70e56c9`(Settings contract 修复)。 +- 受影响 Web 回归: + `NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-localstorage-final.json pnpm exec vitest run src/store/settingsStore.test.ts src/components/home/HomeView.visual.test.ts src/components/shell/SplitPane.interaction.test.tsx src/components/settings/SettingsView.visual.test.ts src/components/settings/SettingsView.interaction.test.tsx` + → 5 files / 50 tests passed。 +- Web build:`pnpm build` → exit 0;保留既有 dynamic-import / large-chunk warnings。 +- Rust 格式:`cargo fmt --all -- --check` → exit 0。 +- 上游 split 锚点:`cargo test -p opentake-ops split_clip_distributes_keyframes_at_cut` → 1 passed。 +- Release app:`web/node_modules/.bin/tauri build` 已完成 release 编译并生成 `/Users/trip/TRUE 开发/PRIMARY-CN/OpenTake-generation/target/release/bundle/macos/OpenTake.app`;DMG `bundle_dmg.sh` 无输出后手动停止,不能声明 DMG 完成。 +- 安装版二进制:最终 token 修复后的 `.app` 已复制到 `/Applications/OpenTake.app`,SHA-256 `02150854e418cd3c3dedad97d905f972ddad99e3a23d287ed1fcac42b15b40a0`。 +- Computer Use:新构建首次进入约 `1066×666` 紧凑档;Home/编辑器/预览/时间轴/Inspector/设置可见;切换标准档时窗口铺满约 `1331×768` 工作区、无白边,再切回紧凑档恢复约 `1066×666`。 + +## 当前未闭环的全量门禁 + +- 旧的并行 Web 门禁曾受 Node 26 共享 localStorage 影响;当前统一使用显式 localStorage 文件和单 worker,最新串行门禁已 151/151 files、1415/1415 tests 通过。 +- 文件导入定向回归:`mediaActions` + `MediaPanel` 为 2 files / 59 tests passed;Preview/媒体/Store 定向回归为 4 files / 91 tests passed;两条切片均已安装版验证。 +- 主线 fresh verification(2026-08-21 12:09 +08:00):Preview/Store/Media 4 files / 91 tests passed;MediaActions/MediaPanel 2 files / 59 tests passed;`pnpm build` exit 0;仅保留既有 Vite chunk warnings。 +- 全量 Web 串行门禁(2026-08-21 16:46 +08:00):`NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-localstorage-temporal-final.json pnpm -C web exec vitest run --maxWorkers=1` → 151 files / 1410 tests passed;使用新 localStorage 文件会触发既有 locale/异步测试状态假失败,已保留为运行约束。 +- Rust workspace 门禁(2026-08-21 20:19 +08:00):`cargo test --workspace --jobs 1 -- --test-threads=1` 通过;包含缺失媒体 fail-closed、export cleanup/external cancel、symlink identity、H.264/AAC integration、full-track PCM、RenderPlan temporal、Motion Chromium 和 native playback tests。`cargo fmt --all -- --check` 同样通过。全 workspace clippy 仍被既有 `chunks_exact`/`chunks_exact_mut` lint 阻塞,当前输出涉及 `opentake-media`、`opentake-motion` 等旧模块;本次导出改动触发的两个 clippy 建议已在 `6b31449` 收口。此前未限并发的 workspace 运行曾出现一次 motion sandbox 180s 超时,单独串行重跑 4 次均通过,保留为并发 GPU/Chromium 资源争用风险,不作为代码绿证据。 + +## Media View Modes 新鲜验收证据 + +- 实现提交:`745a190`(folder/flat/grouped UI)与 `5b1f45d`(空字符串根节点、悬挂 folderId fail-soft)。 +- 聚焦测试:`NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-localstorage-media-modes-final.json pnpm exec vitest run src/lib/mediaViewModes.test.ts src/lib/folderTree.test.ts src/components/media/MediaPanel.test.tsx` → 3 files / 64 tests passed。 +- 安装版:重新构建并复制 `/Applications/OpenTake.app`,当前二进制 SHA-256 为 `fb46fbcaa96ddd10d8e760160b47674c6f233e55effd88eca6d3d8b552092a51`。 +- Computer Use:菜单 `媒体组织方式` 可访问地提供“文件夹 / 平铺 / 分组”;带真实文件夹导入后,文件夹模式显示目录卡片,平铺模式显示 37 个媒体且隐藏目录卡片,分组模式显示“全部 / editor-core-after-fix-assets”分组;分组网格/列表均可用;进入目录后面包屑、返回上级及 17 个目录内项目可见;音频“导入”有同一模式入口,“我的/收藏”保持任务型空态。 +- Preview token 修复:`tokenUsage.test.ts` 曾捕获未声明的 `--space-2xs`,已改为现有 `--space-xxs`;token、Preview、Store 48 tests 和 Web build 通过。 +- 最终原生包:`web/node_modules/.bin/tauri build --bundles app` exit 0,release `.app` 完成生成;最终包 Computer Use 再次验证两个媒体 Preview tabs、关闭回退和紧凑窗口。 +- `cargo tauri build` 的历史文档入口不可用(没有全局 cargo-tauri);本轮使用项目本地 Tauri CLI。DMG 尾脚本仍需单独排查。 + +## Preview Temporal Remap 新鲜验收证据 + +- 代码提交:`a833764`(compositor temporal 路由)、`1013355`(多视频 temporal fail-closed)、`dafd5eb`(native Preview surface)、`3e124dc`(source frame 回退时 reset stream)。 +- Web:路由 16/16,Preview/engine 44/44,串行全量 151 files / 1410 tests,`pnpm build` 均通过。 +- Rust:`opentake-media` 432 tests + integration 通过;`opentake-render plan` 46 tests;native playback transport 8 tests;engine 11、transport 7 tests;workspace 全量通过。 +- 安装版:当前 `/Applications/OpenTake.app` SHA-256 `7ad988f3d00c50af4758c0c036753d690101c2b4c66db17302b29b308bda9799`,对应代码提交 `6b31449`;temporal 与带音频导出证据仍见旧 QA 包,最新包的 SavePanel/取消/实时 A/V 同步和屏幕 smoke 尚未重新跑完,因此不把旧包导出证据静默升级到当前包。 +- 最新 range slice 安装包:`549a6e50a6de52c9653bcb50a2be09b41c0566892f6783d5276c2847c64698c9`;仅用于本轮范围交互代码和 Web build 的 packaged smoke,不能替代范围拖动/Undo/Redo 的完整桌面证据。 +- 带音频桌面 QA 证据:`/private/tmp/opentake-audio-desktop-qa-L8Nvwh/01-imported-timeline-waveform.png`、`02-preview-start-paused.png`、`03-preview-middle.png`、`04-preview-end-paused.png`、`05-playback-paused-stable.png`;原生 H.264 导出已打开并在开始前取消,不能声称有安装版带音频导出文件。 + +- 视频导出 SavePanel:`35801f8` 移除 macOS 视频保存 filters,保留 codec→扩展名和 `withExt`;focused ExportDialog 11/11、H.264 临时导出 160 帧 / 2.666667s / H.264 通过。 + +## Related Documents + +- Parent index: [Capabilities README](./README.md) +- Machine manifest: [requirements.json](./requirements.json) +- Desktop matrix: [2026-08-21 functional matrix](../audit/2026-08-21/full-desktop-functional-matrix.md) +- Long-term plan: [Full UI and upstream convergence](../superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md) + +## Change Log + +- `2026-08-21T10:06:00+08:00` — 写入首轮 UI 真机巡检和上游 parity audit;状态均保守记录为 implemented/partial/blocked。 +- `2026-08-21T19:32:27+08:00` — 修复最终输出 symlink/reparse identity 校验并通过 RED→GREEN 回归;顺序 Rust workspace 全量通过;重新构建并安装对应提交 `a7d98d6` 的 app,记录 SHA-256 `12e1db…9455d9`。并发 workspace motion 超时保留为环境风险。 +- `2026-08-21T19:48:03+08:00` — 对齐上游无选区 split/trim no-op(`f05bac8`、`478e1b4`),Web 全量更新为 151 files / 1412 tests;最新安装包 SHA-256 `de4cb52b…25405`。Computer Use 屏幕 smoke 因 Mac 锁定 blocked,未升级桌面证据。 +- `2026-08-21T20:09:19+08:00` — 完成缺失媒体 Preview/Playback fail-closed(`741ff07`),补真实 RenderLoop 集成证据;顺序 workspace 全量通过,当前安装包 SHA-256 `40c21ce0…e7f08`。Mac 仍锁定,未升级屏幕证据。 +- `2026-08-21T20:19:15+08:00` — 复跑当前提交 `6b31449` 的顺序 workspace 全量、导出/播放集成和 app 构建;最新安装包 SHA-256 `7ad988f3…a9799`。全 workspace clippy 的既有 `chunks_exact` lint 仍单独记录,Mac 屏幕 smoke 仍未完成。 +- `2026-08-21T20:57:59+08:00` — 完成 `OT-MOTION-ALPHA` 首条 Rust/Web 纵切:透明模板→Chromium alpha→ProRes 4444 `.mov`→manifest provenance→Motion Studio 发布开关;Rust workspace 720 Tauri tests 与 Web 151/1413 全量通过,重新构建并安装包 SHA-256 `7634979e…607f00`。最新包屏幕验收仍因 Mac 锁屏阻塞,能力保持 partial。 +- `2026-08-21T21:15:16+08:00` — 补齐 Agent/MCP `move_clips` 的 linked A/V frame-delta parity,并修正文档中 `add_motion_graphic.transparent` 已支持 ProRes 4444 的描述;Agent lib 415/415 通过。屏幕验收阻塞边界不变。 +- `2026-08-21T21:21:26+08:00` — 将透明参数接入 Agent `publish_motion_document` 的新增 clip schema/host bridge;已有透明 Motion 的 document edit 仍需由旧 manifest provenance 保留 alpha,本轮随后补齐并用真实 FFmpeg integration 验证。 +- `2026-08-21T21:21:26+08:00` — 重新构建并安装包含 Agent Motion 透明参数的 app,当前二进制 SHA-256 `38c814f4…bc838`;Computer Use 仍因 Mac locked 无法执行最新包屏幕 smoke。 +- `2026-08-21T21:31:52+08:00` — alpha-preserving document edit 真实 FFmpeg integration 通过,重新构建并安装当前 app,二进制 SHA-256 `d5f8aa46…87ae7`;屏幕 smoke 仍因 Mac locked 阻塞。 +- `2026-08-21T22:15:18+08:00` — 最新包 `918eac84…9551b` 完成真实桌面 smoke:小屏/标准 Home、媒体预览 tab、播放到尾帧、分割/撤销、导出面板和 Motion 透明发布落轨;Option trim 坐标接口仍返回 `noWindowsAvailable`,透明导出/保存重开保持待验证。 +- `2026-08-21T22:27:41+08:00` — 最新包 H.264/AAC SavePanel 导出写入 `/private/tmp/opentake-audio-desktop-qa-L8Nvwh/opentake-latest-smoke.mp4`,ffprobe 和完整 `ffmpeg -xerror` 通过;透明 ProRes 专用导出仍待。 +- `2026-08-21T22:32:49+08:00` — 最新包真实屏幕验证 linked V1/A1 的 Shift+Backspace ripple delete 与 Undo:伙伴同时删除、其它 Motion clip 保留、撤销恢复;时间轴剩余 Option trim/range 屏幕对拍继续保持 partial。 +- `2026-08-21T22:37:23+08:00` — 复核透明 Motion 文件级产物:真实 QA `.mov` 为 ProRes 4444 `ap4h` / `yuva444p12le`,alphaextract 可读且非全黑;能力仍因透明专用导出 UI、opaque→transparent 编辑和其它导出矩阵保持 partial。 +- `2026-08-21T22:47:48+08:00` — 写回最新安装版 Inspector、字幕和 Agent 入口/失败边界证据;完整模型下载、转写/字幕导出、MCP/Agent 工具调用仍未升级为完成。 +- `2026-08-21T23:09:09+08:00` — 写回最新包范围删除/Undo 与 playhead clamp 屏幕证据;安装包 SHA-256 `893b6ed0…d0ba`,Web 全量 151/1415 通过;Option trim 和其它导出矩阵仍保持 partial。 +- `2026-08-21T23:19:12+08:00` — 写回最新包 H.265/AAC、ProRes 422 HQ/PCM SavePanel 与文件证据;透明 ProRes 4444 专用导出、字幕、取消和实时音画同步仍未升级为完成。 +- `2026-08-21T23:47:02+08:00` — 写回透明 ProRes 4444 导出实现和 GPU/FFmpeg alpha integration;最新安装包 `3ce3c1d0…c7eef`,Computer Use 因 Mac 锁屏未完成透明导出屏幕复验。 +- `2026-08-22T00:46:36+08:00` — 完成上游 agent 确认的两个 P1 切片:Agent 文件夹批量 entries 进入单步 Undo;媒体导入接受 Lottie JSON/`.lottie`,Velato 校验、`.lottie` ZIP animation 提取和 metadata 进入 Tauri 边界;Agent 417/417、ops 209/209、Tauri Lottie JSON/容器/MCP path 定向测试通过,安装版 Lottie 屏幕验收仍待解锁。 +- `2026-08-22T01:03:46+08:00` — Tauri lib 726/726、Web 151/1416、tsc/build 通过;workspace 全量在既有 Motion Chromium 4K budget smoke 超时 180s 后 poison gate,记录为环境/GPU/锁屏风险,未把该失败归因到本轮改动。 +- `2026-08-22T01:08:00+08:00` — 基于 `c1db732` 重建并安装最新 `.app`,二进制 SHA-256 `acd7b105…c319`;Mac 锁屏,仅记录构建/安装证据。 +- `2026-08-22T01:17:36+08:00` — 窄范围上游对拍确认 Rust ripple/range/insert/trim、linked partner 和原子拒绝语义一致;前端 Timeline 逐函数对拍与安装版屏幕验收仍未完成,能力保持 partial。 +- `2026-08-22T01:20:00+08:00` — 前端 Timeline 定向门禁 6 files / 55 tests 通过,覆盖 TimelineContainer、范围交互、sync-lock 轨道头、overlay、range/clip context menu;仅作为代码基线,不升级屏幕验收状态。 +- `2026-08-22T01:28:03+08:00` — `editActions.test.ts` 新增 ripple action routing 覆盖,选中片段/标记范围/选中 gap/out-of-band gap 拒绝 42/42 通过,并确认前端范围 wire 使用 `{start,end}`;Timeline 屏幕验收仍待解锁。 +- `2026-08-22T01:32:55+08:00` — Tauri playback resolver 新增 `.lottie` 容器 preview/export 独立 materializer 像素一致性测试,真实 GPU 路径通过;安装版 Lottie 预览/落轨屏幕验收仍待解锁。 +- `2026-08-22T01:38:58+08:00` — 统一 Timeline/编辑动作与 Web 全量测试数字:7 files / 97 定向测试、151 files / 1419 全量测试;屏幕验收边界不变。 +- `2026-08-22T01:49:23+08:00` — WebKit 预览新增可复用 GainNode 路由,修复 >0 dB 增益被截断;预览 58/58、Web 152/1423、tsc/build 通过;最新安装包 `d454bccc…b4b6` 已安装,Mac 锁屏,保留听感/屏幕验收为 partial。 +- `2026-08-22T11:19:00+08:00` — 修复 `resolveTimelinePlaybackRoute` 把 Lottie 直接标为 unsupported 的前端缺口,改为使用已有 Rust `TextureSource::Lottie` native compositor;route/Preview 20 files / 196 tests、Web 全量 152/1424、tsc/build 和安装版 QA 屏幕验证通过,包 SHA-256 `6bcbfa0f…7dec14`。 +- `2026-08-22T11:33:00+08:00` — 修复 ExportDialog 在 progress listener 尚未返回时丢失取消意图的竞态;Shell export 3 files / 39 tests、Web 全量 152/1425、tsc/build 和新安装包 `2a5f8d72…0034e` 通过;导出中途取消仍需屏幕解锁后验证。 +- `2026-08-22T11:58:52+08:00` — MediaPanel Text 标签接入已有 `addTextClip()` 和 Inspector Text 流程;MediaTabBar/MediaPanel 54/54、Web 全量 152/1427、tsc/build 和新安装包 `61dbb3e4…703e0` 通过;Sticker/Effect 继续保留为明确未实现占位。 +- `2026-08-22T12:05:54+08:00` — Effect 标签接入 grayscale/sepia/invert 预设,追加到选中片段效果链并复用 `setEffects()`;MediaTabBar/MediaPanel 56/56、Web 全量 152/1429、tsc/build 和新安装包 `8031c640…3fd44` 通过;Sticker 继续保留为明确未实现占位。 +- `2026-08-22T12:22:40+08:00` — 修复 Effect 面板对 linked V1/A1 选择的错误禁用,只筛选视觉片段;新包 `eaad6fb3…9e57` 已安装。实机按钮启用,但点击后黑屏/Undo 证据未闭环,保持 partial。 diff --git a/docs/capabilities/README.md b/docs/capabilities/README.md new file mode 100644 index 00000000..b63b709b --- /dev/null +++ b/docs/capabilities/README.md @@ -0,0 +1,52 @@ +--- +id: capabilities.readme +title: OpenTake Capability Evidence +summary: 说明 capability manifest 与人工 ledger 的状态含义和证据门槛。 +kind: engineering +status: draft +content_stage: partial-implementation +scope: + - all-modules +triggers: + - capability ledger + - 功能清单 + - 完成度 +read_when: + - 新增、补齐或验收用户可见能力 +skip_when: + - 只改内部实现且没有公共行为变化 +priority: must +freshness_class: project +last_verified: 2026-08-21T10:06:00+08:00 +owners: + - OpenTake-generation +source_of_truth: + - ./requirements.json + - ./CAPABILITY-LEDGER.md +related: + prerequisites: [] + next: + - ../audit/2026-08-21/full-desktop-functional-matrix.md +supersedes: [] +tags: + - capabilities + - evidence +--- + +# Capability Evidence + +`requirements.json` 保存稳定 ID 和机器可读字段;`CAPABILITY-LEDGER.md` 保存当前人工审计结论和证据索引。状态含义: + +- `implemented`:代码链路存在,但本轮尚未完成匹配范围的自动化/桌面证据。 +- `verified`:代码、相关测试和匹配范围的最新运行证据都存在。 +- `partial`:部分层或部分场景可用,仍有明确缺口。 +- `missing`:当前没有可用实现。 +- `blocked`:实现路径或验收依赖外部服务、凭据、硬件或未解决环境问题。 + +AX 节点存在不等于 verified;浏览器/fallback 测试也不等于安装版桌面证据。 + +## Related Documents + +- [Capability ledger](./CAPABILITY-LEDGER.md) +- [Desktop functional matrix](../audit/2026-08-21/full-desktop-functional-matrix.md) +- [Long-term plan](../superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md) diff --git a/docs/capabilities/requirements.json b/docs/capabilities/requirements.json new file mode 100644 index 00000000..8430c48d --- /dev/null +++ b/docs/capabilities/requirements.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "catalog_status": "beta6-candidate", + "last_verified": "2026-09-06", + "scope": "Beta 6 候选代码与实际证据同步;既有 verified 条目保留其历史验收日期,不能自动作为当前安装包或全平台证明。", + "capabilities": [ + {"id":"UP-MODELS-TIMELINE","area":"models","title":"Timeline/Track/Clip/Keyframe/Transform/TextStyle","status":"implemented","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Models/","opentake":"crates/opentake-domain/src/","tests":["cargo test -p opentake-domain"]}, + {"id":"UP-EDITOR-COMMAND-TRANSACTION","area":"editor","title":"Single EditCommand entry and undo/redo","status":"implemented","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Editor/EditorViewModel.swift","opentake":"crates/opentake-ops/src/command.rs;crates/opentake-core/src/session.rs","tests":["cargo test -p opentake-ops","cargo test -p opentake-core"]}, + {"id":"UP-EDITOR-SPLIT-TRIM-RIPPLE","area":"editor","title":"Split/trim/move/ripple/caption mutations","status":"implemented","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Editor/EditorViewModel+ClipMutations.swift","opentake":"crates/opentake-ops/src/ops/","tests":["cargo test -p opentake-ops split_clip_distributes_keyframes_at_cut"]}, + {"id":"UP-TIMELINE-UI","area":"timeline","title":"Timeline geometry, hit testing, tools, snap and drag","status":"partial","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Editor/Timeline/","opentake":"web/src/components/timeline/;web/src/components/toolbar/","tests":["web/src/components/timeline/TimelineContainer.test.ts","web/src/components/timeline/","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-PREVIEW-PLAYBACK","area":"preview","title":"Timeline composite, playback, pause, seek and audio clock","status":"partial","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Preview/","opentake":"src-tauri/src/render.rs;src-tauri/src/playback/;web/src/components/preview/","tests":["web/src/components/preview/playbackRoute.test.ts","web/src/components/preview/Preview.test.tsx","web/src/components/preview/audioGain.test.ts","src-tauri/tests/playback_transport_integration.rs","crates/opentake-render/src/plan/tests.rs","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-PREVIEW-TABS","area":"preview","title":"Multiple preview tabs with switch/close","status":"verified","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Preview/PreviewTab.swift","opentake":"web/src/components/preview/Preview.tsx;web/src/store/uiStore.ts","tests":["web/src/store/uiStore.test.ts","web/src/components/preview/Preview.interaction.test.tsx","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-MEDIA-IMPORT-AND-FOLDERS","area":"media","title":"Import, manifest, recursive folders, missing and relink","status":"verified","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Editor/EditorViewModel+MediaLibrary.swift","opentake":"src-tauri/src/media.rs;web/src/store/mediaActions.ts;web/src/store/mediaStore.ts;web/src/components/media/MediaPanel.tsx","tests":["web/src/store/mediaActions.test.ts","web/src/components/media/MediaPanel.test.tsx","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-MEDIA-LOTTIE-IMPORT","area":"media","title":"Lottie JSON and .lottie container import","status":"partial","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Models/ClipType.swift;Agent/Tools/ToolExecutor+Import.swift","opentake":"crates/opentake-core/src/session.rs;src-tauri/src/media.rs;src-tauri/src/mcp.rs;src-tauri/src/render.rs","tests":["cargo test -p opentake-core importable_clip_type_covers_whitelist_and_rejects_others","cargo test -p opentake-tauri media::tests::import_one_registers_valid_lottie_json_with_composition_metadata","cargo test -p opentake-tauri media::tests::import_one_registers_lottie_container_animation_json","cargo test -p opentake-tauri mcp::tests::import_from_path_lottie_registers_valid_animation_metadata"]}, + {"id":"UP-MEDIA-VIEW-MODES","area":"media","title":"Folder/flat/grouped media panel views","status":"verified","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Editor/MediaTab.swift","opentake":"web/src/components/media/MediaPanel.tsx;web/src/lib/mediaViewModes.ts","tests":["web/src/lib/mediaViewModes.test.ts","web/src/components/media/MediaPanel.test.tsx","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-INSPECTOR","area":"inspector","title":"Clip/text/source/project inspector and keyframes","status":"implemented","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Editor/InspectorView.swift","opentake":"web/src/components/inspector/","tests":["web/src/components/inspector/","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-TRANSCRIPTION-SEARCH-CAPTIONS","area":"intelligence","title":"Transcription, search, captions and subtitle export","status":"implemented","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Transcription/;Search/","opentake":"src-tauri/src/transcribe.rs;src-tauri/src/search.rs;web/src/components/media/CaptionsTab.tsx;web/src/components/shell/TitleBar.tsx;crates/opentake-domain/src/subtitle_export.rs","tests":["cargo test -p opentake-domain subtitle_export","cargo test -p opentake-tauri subtitle_export_tests -- --nocapture","NODE_OPTIONS=--localstorage-file=/tmp/opentake-vitest-subtitles-20260822.json pnpm -C web exec vitest run src/components/media/CaptionsTab.test.tsx src/components/shell/TitleBar.visual.test.ts --pool=forks --maxWorkers=1"]}, + {"id":"UP-EXPORT","area":"export","title":"Video/range/clip export and interchange formats","status":"partial","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Export/","opentake":"src-tauri/src/export.rs;crates/opentake-project/src/;web/src/components/shell/ExportDialog.tsx","tests":["src-tauri/src/export.rs","src-tauri/tests/export_integration.rs","web/src/components/shell/ExportDialog.interaction.test.tsx","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"UP-AGENT-MCP","area":"agent","title":"Upstream tools, MCP transport, dispatcher and project scope","status":"implemented","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Agent/","opentake":"crates/opentake-agent/src/;src-tauri/src/mcp.rs;src-tauri/src/external_mcp.rs","tests":["cargo test -p opentake-agent --test advertised_tool_acceptance","cargo test -p opentake-agent --lib mcp::dispatch::tests::move_clips_dispatch","cargo test -p opentake-agent folder_batch"]}, + {"id":"UP-AGENT-FOLDER-BATCH","area":"agent","title":"Batch create_folder and move_to_folder entries","status":"implemented","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Agent/Tools/ToolExecutor+Folders.swift","opentake":"crates/opentake-agent/src/mcp/dispatch.rs;crates/opentake-ops/src/command.rs","tests":["cargo test -p opentake-agent folder_batch"]}, + {"id":"OT-MOTION-STUDIO","area":"motion","title":"Motion Studio authoring, preview, publish and timeline import","status":"implemented","priority":"P1","source":"OpenTake requirement; no direct Swift equivalent","opentake":"src-tauri/src/motion.rs;web/src/components/motion/;crates/opentake-motion/src/","tests":["cargo test -p opentake-motion","web/src/components/motion/MotionStudio.interaction.test.tsx"]}, + {"id":"OT-MOTION-ALPHA","area":"motion","title":"Transparent motion output and compositing","status":"partial","priority":"P1","source":"OpenTake requirement; upstream Lottie alpha behavior","opentake":"src-tauri/src/motion.rs;crates/opentake-domain/src/media.rs;web/src/components/motion/;web/src/store/motionStudioStore.ts","tests":["cargo test --workspace --jobs 1 -- --test-threads=1","web/src/store/motionStudioStore.test.ts","web/src/components/motion/MotionStudio.interaction.test.tsx","src-tauri/tests/motion_command.rs::transparent_motion_publishes_prores_alpha_and_marks_manifest"]}, + {"id":"UP-ACCOUNT-CLOUD-BOUNDARY","area":"account-generation","title":"Palmier account/subscription/cloud generation contract","status":"blocked","priority":"P0","source":"palmier-pro-upstream/Sources/PalmierPro/Account/;Generation/","opentake":"crates/opentake-gen/src/;src-tauri/src/advanced.rs;src-tauri/src/secret.rs","tests":[]}, + {"id":"UP-SETTINGS-TAXONOMY","area":"settings","title":"Account/general/models/agent/storage settings taxonomy","status":"partial","priority":"P2","source":"palmier-pro-upstream/Sources/PalmierPro/Settings/","opentake":"web/src/components/settings/","tests":["web/src/components/settings/SettingsView.interaction.test.tsx"]}, + {"id":"OT-WINDOW-SMALL-SCREEN","area":"shell","title":"Window size and responsive shell on small screens","status":"verified","priority":"P0","source":"User goal 2026-08-21","opentake":"src-tauri/tauri.conf.json;web/src/store/settingsStore.ts;web/src/components/shell/;web/src/components/home/","tests":["web/src/store/settingsStore.test.ts","docs/audit/2026-08-21/full-desktop-functional-matrix.md"]}, + {"id":"OT-MEDIA-STICKERS","area":"media","title":"Project image/Lottie sticker import, preview and timeline insertion","status":"implemented","priority":"P1","source":"OpenTake requirement","opentake":"web/src/components/media/MediaPanel.tsx","tests":["web/src/components/media/StickerPanel.test.tsx","docs/audit/2026-09-06/sticker-panel.md"]}, + {"id":"UP-SEMANTIC-SEARCH-MODEL","area":"intelligence","title":"Verified SigLIP2 assets, offline install, text/image inference and recoverable search","status":"partial","priority":"P1","source":"palmier-pro-upstream/Sources/PalmierPro/Search/","opentake":"crates/opentake-media/src/search/;src-tauri/src/search.rs;web/src/components/media/MediaSearch.tsx","tests":["docs/audit/2026-09-06/semantic-search-model.md","docs/audit/2026-09-06/search-model-recovery.md","cargo test -p opentake-tauri --lib search::tests"]}, + {"id":"OT-DESKTOP-FUNCTIONAL-QA","area":"qa","title":"Installed-app modular functional verification","status":"partial","priority":"P0","source":"User goal 2026-08-21","opentake":"docs/audit/2026-08-21/full-desktop-functional-matrix.md","tests":["Computer Use @oai/sky"]} + ] +} diff --git a/docs/documentation-sync-2026-09-06.md b/docs/documentation-sync-2026-09-06.md new file mode 100644 index 00000000..72ab8324 --- /dev/null +++ b/docs/documentation-sync-2026-09-06.md @@ -0,0 +1,494 @@ +# 全项目 Markdown 同步报告 — 2026-09-06 + +> 状态:generated · 阶段:implementation-backed · 文档审计日期:2026-09-06。 +> 本报告记录文档同步与静态源码核对,不代替候选功能、GUI、安装包或远端发布验收。 + +工作树:`OpenTake-generation/`。起点为 `release/v1.0.0-beta.5` / `33ee8e2`,origin 为 `appergb/OpenTake`。工作期间主代理切到 `release/v1.0.0-beta.6` 并更新版本清单;Beta 1–5 已发布,Beta 6 仍是未发布候选。文档子任务未执行发布;主线已将候选提交至 [PR #249](https://github.com/appergb/OpenTake/pull/249),tag/公开发布状态以当日验证记录为准。 + +当前入口:[活动计划](plans/active/2026-09-06-public-beta.md) · [Beta 6 候选](releases/1.0.0-beta.6.md) · [当日验证](audit/2026-09-06/public-beta-validation.md) · [文档总目录](INDEX.md) · [开发规范](project/conventions.md)。 + +## 覆盖方式与边界 + +`scripts/check_docs.py` 通过 Git 枚举 tracked + 非忽略 untracked Markdown(含 `.MD` 与隐藏的 `.superpowers` 历史报告),逐文件扫描可点击的 Markdown 内联链接、引用定义和 HTML href/src 的本地文件目标,检查自动入口命名。代码块/行内代码中的历史命令和路径不当作链接。完整机器清单可运行 `python3 scripts/check_docs.py --json` 获取。 + +覆盖包括维护文档、设计来源、日期化历史、运行时 Markdown 资源和第三方归属记录。排除 `marketing/`、`output/`、`.playwright-cli/`、构建/依赖目录;其中视频切片、媒体输出由其他所有者维护。主代理指定的计划、Beta 6 发布说明、能力账本及 `docs/audit/2026-09-06/` 只读进入清单。 + +“逐文件覆盖”是路径/链接与状态分类审计,不代表全部历史正文重新逐句验真。正文同步重点为项目入口、README 三语、CHANGELOG、CONTRIBUTING、架构、各模块 OVERVIEW/INDEX、已变化子系统。历史测试日志、上游映射和旧设计保持原时点含义,不批量改成 canonical/final-verified。外部 URL、Markdown 标题 fragment、代码块中的机器绝对路径不由本脚本验证。 + +## 主要同步结果 + +- 根 `AGENTS.md` 压缩为项目摘要、技术栈、意图、风险和路由;有效开发规则迁入 `docs/project/conventions.md`,补 `docs/project/index.md` 与 `intent.md`。当前活动工作树、origin 和候选版本已纠正。 +- `CLAUDE.md` 明确 historical,索引不再把它当作当前交接。`docs/port-map/agent.md` 改名 `agent-port-map.md`,有效上游映射保留、所有 Markdown 引用更新;当前仓库只有根 `AGENTS.md` 自动入口。 +- 三语 README 与 CHANGELOG 区分已发布 Beta 1–5、Beta 6 候选和源码新增。平台表不再把跨平台目标等同于已验证 Windows/Linux 发行;语义搜索改为模型安装及 macOS 真实 Rust 链路已验证,保留首次安装要求与 Windows/新包 UI 边界;外部 MCP 配对不再写“未来才开放”。 +- 模块路由补缺失的 `opentake-process-tree`:Cargo 有 10 个库 crate + Tauri 壳,共 11 个成员。OVERVIEW/INDEX 区分源码和候选验收,早期规格保留设计状态。 +- 播放架构更新 temporal compositor、Lottie 与能力路由;导出文档更新 H.264/H.265/ProRes 422/4444、alpha、进度、取消和失败清理。媒体 Text/Effect 和 Agent 面板不再写未接线;预览 tab、媒体视图有当前路由。 +- 修复旧审计/实现计划中从模块目录复制后失效的 `dispatch-tools.md`、`renderer.md`、`probe-ff.md`、SPEC 和 architecture 相对链接,保留日期和原始证据正文。 + +## 74 个提交的源码核对 + +执行 `git rev-list --count aae0ae6..33ee8e2` 得到 74;`git log` 逐条核对该范围(包含文档与测试提交,不能称 74 项新功能,也不将该范围直接等同完整 Beta 5 tag diff)。 + +| 领域 | 提交依据 | 定向源码与结论 | +|---|---|---| +| 多预览 tab | `bd06d62`、`4c8b761` | `web/src/store/uiStore.ts`、Preview 组件;多 tab、旧状态归一存在 | +| 媒体视图/导入 | `745a190`、`5b1f45d`、`0bd7c6a` | MediaPanel 与导入动作;folder/flat/grouped、悬空 folder 归一、原生导入修复 | +| temporal compositor | `a833764`、`1013355`、`dafd5eb`、`3e124dc` | `web/src/components/preview/playbackRoute.ts` 与 native transport;合成时间线倒放/速度路由和发布流重置 | +| 透明 Motion | `e106bc3`、`ba85202`、`5e3b5b7`、`6ce80fe` | `src-tauri/src/motion.rs`、Motion UI;透明发布、编辑 alpha 保留与 Inspector 开关 | +| ProRes 4444 | `cca9327` | `src-tauri/src/export.rs` 与 media encode;透明 `.mov` 编码路径 | +| 时间线与链接行为 | `a7a372e`、`478e1b4`、`f05bac8`、`3b8ba9b`、`76e700b` | ops / editActions / timeline 手势;范围选择、选区驱动修剪/分割、链接移动、Option trim | +| 导出与播放失败 | `0bae80e`、`a7d98d6`、`eb1cc1a`、`741ff07` | 导出清理/取消/身份与缺失素材拒绝路径 | +| Lottie | `c1db732`、`2ed8a61` | Lottie 素材和原生播放接入;不能继续列为路由必然 unsupported | +| 文本/特效面板 | `17bcb8c`、`d87b74f`、`33ee8e2` | TextTab 调 addTextClip;EffectTab 向单个视觉选区追加预设,保留既有 effects,链接音轨不增加视觉计数 | + +## 实际代码与旧文档冲突 + +| 旧说法 | 当前依据与处理 | +|---|---| +| domain 零依赖 | Cargo.toml 实际依赖 serde;改为无 I/O 叶子 | +| Swift/Rust round 中点向偶 | 两者默认就近中点远离零;纠正规范,secondsToFrame 截断语义另列 | +| web 有 lint script | `web/package.json` 只有 dev/build/preview/test;CONTRIBUTING 改用真实脚本,build 包含 TypeScript 检查 | +| 全工作区 9 crate / 11 模块 | Cargo 实际 11 成员(10 库 + shell),加 web 为 12 个文档模块 | +| Beta 2 当前入口、Beta 5 未发布候选 | 当前 Beta 6 未发布候选;旧 Beta 日期/发布文件留存为历史 | +| 透明 Motion/ProRes 4444 未做 | 本轮之前的 74 提交中已有实现;更新当前总览与导出说明,旧设计注明时点 | +| Text/Effect/Agent 面板占位 | HEAD 已有接线;源码、浏览器测试、原生 GUI 验收分别记账 | +| MCP OAuth 元数据为空等于无需认证 | 生产桌面有 Bearer 与工程 gate;空 authorization_servers 不表示无认证 | +| 语义搜索模型仍为占位/修复中 | 原空 hash/bytes=0 已修复;约 1.5 GB 固定 revision 资产校验、macOS 真实 Rust 离线安装/图文 embedding/排名均通过。使用前需安装模型;Windows ort-tract 真实图及新包 UI 待验收 | +| Sticker 仍为占位 | 核对起点是禁用占位;Banach 已完成图片/Lottie 展示、导入、预览、拖拽/落轨和工程身份隔离,4 files / 114 tests 与 build 通过;新包原生 GUI 待主线验收 | + +未遇到必须由文档切片决定的新增产品意图冲突。Sticker 已由实现切片完成接线并通过定向 Web 检查,原生 GUI 仍待验收;语义搜索模型实现与 macOS 真实 Rust 链路已验证,Windows ort-tract 真实图及新包 UI 仍待。文档分别记录实现和验收状态。 + +Sticker 的实现与定向验证见[专项审计](audit/2026-09-06/sticker-panel.md),该记录由实现切片维护。 + +语义模型状态补记:实现切片的[最终专项审计](audit/2026-09-06/semantic-search-model.md)记录固定 revision 的约 1.5 GB 资产逐项校验通过;正式 Cargo media search **84 passed / 1 ignored**、Tauri search **14 passed**,引用实际产品模块的真实模型 Rust harness **72 passed / 0 ignored**。这些是不同且有重叠的验证集合,不能求和;真实 Rust 链路已覆盖离线安装、图文 embedding 和排名。Windows ort-tract 真实图及新包 UI 仍待主线验收,不据此宣称全平台、整仓或发布包通过。 + +## 验证记录与遗留 + +文档切片执行:`python3 scripts/check_docs.py`、脚本针对链接/代码块/分类的内存检查、Markdown 范围 `git diff --check`,并审阅本切片最终 diff。无新增依赖,无 Rust/TS/package/lock 修改;未重跑与此文档切片无关的构建/GUI/付费 provider。 + +主代理提供的较早一轮状态(非本切片执行,未包含随后新增的 Sticker 用例):Web 152 files / 1429 tests 通过,使用每文件 happy-dom Storage 隔离,命令 `pnpm -C web exec vitest run --maxWorkers=1`,无需 NODE_OPTIONS 或残留 localStorage 文件;workspace Clippy exit 0;脚本 209/209、sidecar 7/7 通过。Carver 跨 crate 中间态已由正式 Tauri search 14 项验证解决;主线整仓统一测试/审查仍在执行,不记整仓最终通过。依赖安全、运行验证和发布状态以当日审计后续更新为准。 + +用户预先删除的 5 张截图保持删除,未恢复:`09-dual-track-playback-frame-239.png`、`11-dual-track-seek-pause-frame-115.png`、`14-gui-export-completed.png`、`17-relaunch-editor-thumbnails-composite.png`、`18-relaunch-source-preview.png`(均在 `docs/audit/2026-08-07/editor-core-after-fix-assets/`)。已有工程 JSON、视频、日志、诊断素材保持原状。历史正文中的已删除证据文件名保留,不当作本轮可用证据。 + +后续由主代理在源码切片完成后统一复验并更新候选/当日审计,审阅文档 diff 后再决定发布。本报告不会把这些待执行门槛写成已经通过。 + +## 全部 Markdown 覆盖清单 + + + +本次快照覆盖 **402** 份 Markdown、**1891** 个本地文件链接;普通断链 **0**,待生成链接 **0**。本切片已同步/新增 **71** 份 Markdown(含本报告);历史保留类别 **188** 份,其中可能仅修复迁移链接。分类数量与变更数量为不同维度,不可直接相加。协作过程中新增文件可能改变下次审计计数。 + +| 分类 | 数量 | +|---|---:| +| 主代理维护 | 7 | +| 历史保留 | 188 | +| 生成的同步报告 | 1 | +| 维护文档 | 120 | +| 设计与来源参考 | 83 | +| 运行资源或归属记录 | 3 | + +逐文件状态:`同步` = 本轮文档 diff 或新增(不含主线切片文件);`保留` = 内容未更改但已完成链接/分类扫描;`主代理` = 主线及实现切片只读覆盖。历史/设计文件的状态解释由分类给出,不会把路径审计提升成完整功能验证。 + +| 路径 | 分类 | 本轮处理 | 本地文件链接数 | +|---|---|---|---:| +| `.superpowers/sdd/2026-08-13-beta5-agent-conversation/task-1-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-agent-conversation/task-2-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-agent-conversation/task-3-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-agent-conversation/task-4-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-external-mcp/task-6-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-interface-polish/task-2-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-interface-polish/task-3-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-interface-polish/task-4-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-interface-polish/task-5-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-interface-polish/task-6-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-1-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-2-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-3-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-4-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-5-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-6-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-13-beta5-motion-studio/task-7-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-2-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/2026-08-21-preview-temporal-remap-parity/task-3-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/task-11-red-replay.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/task-11-report.md` | 历史保留 | 保留 | 1 | +| `.superpowers/sdd/task-5-agent-chat-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/task-5-export-freeze-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/task-5-media-library-report.md` | 历史保留 | 保留 | 0 | +| `.superpowers/sdd/task-5-render-report.md` | 历史保留 | 保留 | 0 | +| `AGENTS.md` | 维护文档 | 同步 | 8 | +| `CHANGELOG.md` | 维护文档 | 同步 | 4 | +| `CLAUDE.md` | 历史保留 | 同步 | 2 | +| `CONTRIBUTING.md` | 维护文档 | 同步 | 5 | +| `DECISIONS.md` | 维护文档 | 保留 | 0 | +| `README.ja.md` | 维护文档 | 同步 | 24 | +| `README.md` | 维护文档 | 同步 | 25 | +| `README.zh-CN.md` | 维护文档 | 同步 | 25 | +| `THIRD_PARTY_NOTICES.md` | 运行资源或归属记录 | 保留 | 0 | +| `crates/opentake-agent/src/plugin/builtin/audio-first/instructions.md` | 运行资源或归属记录 | 保留 | 0 | +| `docs/INDEX.md` | 维护文档 | 同步 | 54 | +| `docs/architecture/ADVANCED-FEATURES.md` | 维护文档 | 同步 | 2 | +| `docs/architecture/ARCHITECTURE.md` | 维护文档 | 同步 | 4 | +| `docs/architecture/BUGS.md` | 历史保留 | 同步 | 6 | +| `docs/architecture/CAPCUT-GAP.md` | 历史保留 | 同步 | 2 | +| `docs/architecture/EDITING-ENGINE-PLAN.md` | 维护文档 | 同步 | 4 | +| `docs/architecture/FULL_PROJECT_SCAN_REPORT.md` | 历史保留 | 同步 | 13 | +| `docs/architecture/HANDOFF-2026-07.md` | 历史保留 | 同步 | 9 | +| `docs/architecture/INDEX.md` | 维护文档 | 同步 | 22 | +| `docs/architecture/MODULE-PORT-MAP.md` | 维护文档 | 同步 | 2 | +| `docs/architecture/PLAYBACK-ENGINE.md` | 维护文档 | 同步 | 4 | +| `docs/architecture/PORT-1TO1-GAP.md` | 历史保留 | 同步 | 2 | +| `docs/architecture/ROADMAP.md` | 历史保留 | 同步 | 7 | +| `docs/architecture/STEM-SEPARATION.md` | 维护文档 | 同步 | 2 | +| `docs/architecture/UPDATER.md` | 维护文档 | 同步 | 2 | +| `docs/architecture/editing-automation/EDITING-AUTOMATION-DOS.md` | 维护文档 | 保留 | 13 | +| `docs/architecture/editing-automation/EDITING-AUTOMATION/acceptance-tests.md` | 维护文档 | 保留 | 9 | +| `docs/architecture/editing-automation/EDITING-AUTOMATION/agent-editing-suggestions.md` | 维护文档 | 保留 | 8 | +| `docs/architecture/editing-automation/EDITING-AUTOMATION/auto-crop-smart-reframe.md` | 维护文档 | 保留 | 8 | +| `docs/architecture/editing-automation/EDITING-AUTOMATION/beat-sync-auto-cut.md` | 维护文档 | 保留 | 8 | +| `docs/architecture/editing-automation/EDITING-AUTOMATION/workflow-plugin-recipes.md` | 维护文档 | 保留 | 7 | +| `docs/architecture/editing-automation/README.md` | 维护文档 | 保留 | 19 | +| `docs/audit/2026-07-14/beta-1-convergence-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/completion-report.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/document-reconciliation.md` | 历史保留 | 同步 | 30 | +| `docs/audit/2026-07-14/implementation-plan-index.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/accessibility-polish-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/accessibility-polish-implementation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/agent-settings-generation-design.md` | 历史保留 | 同步 | 5 | +| `docs/audit/2026-07-14/implementation-plans/agent-settings-generation-implementation.md` | 历史保留 | 同步 | 5 | +| `docs/audit/2026-07-14/implementation-plans/command-contracts-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/command-contracts-implementation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/data-safety-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/data-safety-implementation.md` | 历史保留 | 保留 | 6 | +| `docs/audit/2026-07-14/implementation-plans/documentation-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/documentation-implementation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/home-shell-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/home-shell-implementation.md` | 历史保留 | 保留 | 5 | +| `docs/audit/2026-07-14/implementation-plans/inspector-text-keyframes-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/inspector-text-keyframes-implementation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/media-library-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/media-library-implementation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/media-render-playback-export-design.md` | 历史保留 | 同步 | 25 | +| `docs/audit/2026-07-14/implementation-plans/media-render-playback-export-implementation.md` | 历史保留 | 同步 | 31 | +| `docs/audit/2026-07-14/implementation-plans/preview-timeline-design.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/implementation-plans/preview-timeline-implementation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/interface-traces.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/agent-lottie-inspect-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/ai-matting-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/avatar-voice-clone-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/beta-1-sequential-validation-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/bounded-audio-streaming-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/caption-translation-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/cli-sidecar-boundary-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/color-match-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-cache-identity-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-cross-cutting-security-partial-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-generation-seed-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-legacy-default-matrix-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-manifest-corruption-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-mcp-redaction-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-mcp-tool-import-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-mcp-transport-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-project-open-composite-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-shared-core-command-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/data-safety-windows-safe-fs-native-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/denoise-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/ffmpeg-license-replacement-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/five-panel-layout-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/generation-finalization-2026-07-29.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/generic-effects-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/hdr-proxy-account-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/headless-chromium-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-autosave-metadata-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-component-mapping-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-new-project-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-open-project-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-project-lifecycle-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-sample-project-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-secondary-controls-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/home-upstream-composite-real-device-2026-07-31.md` | 历史保留 | 保留 | 5 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/hsl-secondary-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/interchange-export-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/lgg-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/linked-audio-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/loudness-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/lut-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/mask-rendering-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/media-render-packaged-ffmpeg-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/motion-tracking-agent-backend-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/nested-timeline-compound-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/object-removal-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/optical-flow-24-to-60-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/playback-route-lifecycle-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/recent-project-card-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/schema-safe-persistence-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/script-to-video-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/stabilization-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/stem-separation-agent-vertical-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/stems-real-device-2026-08-01.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/subtitle-export-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/talking-head-cleanup-2026-07-29.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/titlebar-controls-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/transition-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/view-menu-contract-real-device-2026-07-31.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/runtime-artifacts/automated/view-menu-real-device-2026-07-30.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/task8-core-slice-validation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/task8-ledger-slice-validation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/task8-ui-slice-validation.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-07-14/upstream-downstream.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-02/beta-functional-verification.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-after-fix-assets/README.md` | 历史保留 | 保留 | 1 | +| `docs/audit/2026-08-07/editor-core-after-fix-assets/installation-ledger.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-after-fix-assets/playback-av-real-device.md` | 历史保留 | 保留 | 3 | +| `docs/audit/2026-08-07/editor-core-remediation-assets/p0-asset-scope-import.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-remediation-assets/proxy-regression.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-remediation-assets/resource-scheduler.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-remediation-assets/safe-asset-authority.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-remediation-assets/ui-semantics.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-remediation-matrix.md` | 历史保留 | 保留 | 3 | +| `docs/audit/2026-08-07/editor-core-validation-assets/README.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-validation-assets/competitor-official-sources.md` | 历史保留 | 保留 | 23 | +| `docs/audit/2026-08-07/editor-core-validation-assets/fresh-gui-ledger.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-validation-assets/resource-ui-static.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-validation-evidence-b/README.md` | 历史保留 | 保留 | 1 | +| `docs/audit/2026-08-07/editor-core-validation-evidence-b/command-ledger.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-07/editor-core-validation.md` | 历史保留 | 保留 | 35 | +| `docs/audit/2026-08-08/release-module-tests/competitor-gap.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/compositor-render.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/import-export.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/motion-canvas-supply-chain.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/playback-fallback-fix.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/playback.md` | 历史保留 | 保留 | 6 | +| `docs/audit/2026-08-08/release-module-tests/project-media-scope-hardening.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/release-workflow.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/resource-loading.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/ui-blockers-fix.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/ui.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/windows-ci-hardening-fix.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/windows-ci-retest.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-08/release-module-tests/windows-ci.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-10/final-module-validation.md` | 历史保留 | 保留 | 1 | +| `docs/audit/2026-08-13/beta5-external-mcp.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-13/beta5-motion-studio.md` | 历史保留 | 保留 | 2 | +| `docs/audit/2026-08-13/beta5-release-candidate.md` | 历史保留 | 保留 | 0 | +| `docs/audit/2026-08-21/full-desktop-functional-matrix.md` | 历史保留 | 保留 | 3 | +| `docs/audit/2026-09-06/public-beta-validation.md` | 主代理维护 | 主代理 | 2 | +| `docs/audit/2026-09-06/semantic-search-model.md` | 主代理维护 | 主代理 | 1 | +| `docs/audit/2026-09-06/sticker-panel.md` | 主代理维护 | 主代理 | 0 | +| `docs/capabilities/CAPABILITY-LEDGER.md` | 主代理维护 | 主代理 | 6 | +| `docs/capabilities/README.md` | 维护文档 | 保留 | 3 | +| `docs/documentation-sync-2026-09-06.md` | 生成的同步报告 | 同步 | 6 | +| `docs/knowledge/2026-09-06-beta-dependencies.md` | 维护文档 | 同步 | 0 | +| `docs/knowledge/2026-09-06-semantic-search-model.md` | 主代理维护 | 主代理 | 1 | +| `docs/modules/INDEX.md` | 维护文档 | 同步 | 34 | +| `docs/modules/opentake-agent/AGENT-CONTEXT-SIGNAL.md` | 维护文档 | 保留 | 5 | +| `docs/modules/opentake-agent/INDEX.md` | 维护文档 | 同步 | 31 | +| `docs/modules/opentake-agent/OVERVIEW.md` | 维护文档 | 同步 | 15 | +| `docs/modules/opentake-agent/SPEC.md` | 设计与来源参考 | 同步 | 2 | +| `docs/modules/opentake-agent/WORKFLOW-PLUGIN-SYSTEM.md` | 维护文档 | 保留 | 4 | +| `docs/modules/opentake-agent/context-signal.md` | 维护文档 | 保留 | 12 | +| `docs/modules/opentake-agent/core-handle-convert.md` | 维护文档 | 保留 | 15 | +| `docs/modules/opentake-agent/dispatch-tools.md` | 维护文档 | 保留 | 14 | +| `docs/modules/opentake-agent/mcp-server.md` | 维护文档 | 同步 | 16 | +| `docs/modules/opentake-agent/plugin-system.md` | 维护文档 | 保留 | 14 | +| `docs/modules/opentake-agent/prompt.md` | 维护文档 | 保留 | 12 | +| `docs/modules/opentake-core/INDEX.md` | 维护文档 | 同步 | 22 | +| `docs/modules/opentake-core/OVERVIEW.md` | 维护文档 | 同步 | 27 | +| `docs/modules/opentake-core/SPEC.md` | 设计与来源参考 | 同步 | 5 | +| `docs/modules/opentake-core/core-router.md` | 维护文档 | 保留 | 17 | +| `docs/modules/opentake-core/deps-di.md` | 维护文档 | 保留 | 13 | +| `docs/modules/opentake-core/dto.md` | 维护文档 | 保留 | 19 | +| `docs/modules/opentake-core/events-bus.md` | 维护文档 | 保留 | 17 | +| `docs/modules/opentake-core/session.md` | 维护文档 | 保留 | 17 | +| `docs/modules/opentake-domain/INDEX.md` | 维护文档 | 同步 | 33 | +| `docs/modules/opentake-domain/OVERVIEW.md` | 维护文档 | 同步 | 11 | +| `docs/modules/opentake-domain/keyframe-transform.md` | 维护文档 | 保留 | 11 | +| `docs/modules/opentake-domain/media-signal.md` | 维护文档 | 保留 | 7 | +| `docs/modules/opentake-domain/split-subtitle.md` | 维护文档 | 保留 | 13 | +| `docs/modules/opentake-domain/text-grade.md` | 维护文档 | 保留 | 10 | +| `docs/modules/opentake-domain/timeline-model.md` | 维护文档 | 保留 | 15 | +| `docs/modules/opentake-gen/INDEX.md` | 维护文档 | 同步 | 57 | +| `docs/modules/opentake-gen/OVERVIEW.md` | 维护文档 | 同步 | 28 | +| `docs/modules/opentake-gen/SPEC.md` | 设计与来源参考 | 同步 | 2 | +| `docs/modules/opentake-gen/catalog.md` | 维护文档 | 保留 | 14 | +| `docs/modules/opentake-gen/client-transport.md` | 维护文档 | 保留 | 18 | +| `docs/modules/opentake-gen/keys-byok.md` | 维护文档 | 保留 | 13 | +| `docs/modules/opentake-gen/params.md` | 维护文档 | 保留 | 15 | +| `docs/modules/opentake-gen/providers.md` | 维护文档 | 保留 | 21 | +| `docs/modules/opentake-media/INDEX.md` | 维护文档 | 同步 | 28 | +| `docs/modules/opentake-media/OVERVIEW.md` | 维护文档 | 同步 | 21 | +| `docs/modules/opentake-media/SPEC.md` | 设计与来源参考 | 同步 | 2 | +| `docs/modules/opentake-media/analysis.md` | 维护文档 | 保留 | 13 | +| `docs/modules/opentake-media/decode.md` | 维护文档 | 保留 | 20 | +| `docs/modules/opentake-media/encode.md` | 维护文档 | 保留 | 14 | +| `docs/modules/opentake-media/library-index.md` | 维护文档 | 保留 | 14 | +| `docs/modules/opentake-media/probe-ff.md` | 维护文档 | 保留 | 11 | +| `docs/modules/opentake-media/semantic-search.md` | 维护文档 | 同步 | 19 | +| `docs/modules/opentake-media/thumbnail.md` | 维护文档 | 保留 | 13 | +| `docs/modules/opentake-media/transcribe.md` | 维护文档 | 保留 | 13 | +| `docs/modules/opentake-media/waveform.md` | 维护文档 | 保留 | 14 | +| `docs/modules/opentake-motion/INDEX.md` | 维护文档 | 同步 | 21 | +| `docs/modules/opentake-motion/MOTION-GRAPHICS-PLUGIN.md` | 维护文档 | 同步 | 1 | +| `docs/modules/opentake-motion/OVERVIEW.md` | 维护文档 | 同步 | 16 | +| `docs/modules/opentake-motion/cache.md` | 维护文档 | 保留 | 10 | +| `docs/modules/opentake-motion/integration.md` | 维护文档 | 保留 | 11 | +| `docs/modules/opentake-motion/manifest-source.md` | 维护文档 | 保留 | 12 | +| `docs/modules/opentake-motion/renderer.md` | 维护文档 | 保留 | 10 | +| `docs/modules/opentake-motion/sandbox.md` | 维护文档 | 保留 | 10 | +| `docs/modules/opentake-ops/INDEX.md` | 维护文档 | 同步 | 17 | +| `docs/modules/opentake-ops/OVERVIEW.md` | 维护文档 | 同步 | 15 | +| `docs/modules/opentake-ops/command-apply.md` | 维护文档 | 保留 | 8 | +| `docs/modules/opentake-ops/engines.md` | 维护文档 | 保留 | 5 | +| `docs/modules/opentake-ops/intent-id.md` | 维护文档 | 保留 | 7 | +| `docs/modules/opentake-ops/ops-algorithms.md` | 维护文档 | 保留 | 8 | +| `docs/modules/opentake-process-tree/INDEX.md` | 维护文档 | 同步 | 5 | +| `docs/modules/opentake-process-tree/OVERVIEW.md` | 维护文档 | 同步 | 5 | +| `docs/modules/opentake-project/INDEX.md` | 维护文档 | 同步 | 17 | +| `docs/modules/opentake-project/OVERVIEW.md` | 维护文档 | 同步 | 18 | +| `docs/modules/opentake-project/bundle-archive.md` | 维护文档 | 保留 | 9 | +| `docs/modules/opentake-project/fcpxml-export.md` | 维护文档 | 保留 | 4 | +| `docs/modules/opentake-project/gen-log.md` | 维护文档 | 保留 | 6 | +| `docs/modules/opentake-project/layout.md` | 维护文档 | 保留 | 10 | +| `docs/modules/opentake-render/INDEX.md` | 维护文档 | 同步 | 20 | +| `docs/modules/opentake-render/OVERVIEW.md` | 维护文档 | 同步 | 16 | +| `docs/modules/opentake-render/SPEC.md` | 设计与来源参考 | 同步 | 2 | +| `docs/modules/opentake-render/gpu-compositor.md` | 维护文档 | 保留 | 12 | +| `docs/modules/opentake-render/render-plan.md` | 维护文档 | 保留 | 10 | +| `docs/modules/opentake-render/source-size.md` | 维护文档 | 保留 | 8 | +| `docs/modules/opentake-render/text-rasterizer.md` | 维护文档 | 保留 | 7 | +| `docs/modules/src-tauri/INDEX.md` | 维护文档 | 同步 | 27 | +| `docs/modules/src-tauri/OVERVIEW.md` | 维护文档 | 同步 | 20 | +| `docs/modules/src-tauri/commands-ipc.md` | 维护文档 | 保留 | 15 | +| `docs/modules/src-tauri/export.md` | 维护文档 | 同步 | 9 | +| `docs/modules/src-tauri/library-media.md` | 维护文档 | 保留 | 16 | +| `docs/modules/src-tauri/mcp.md` | 维护文档 | 保留 | 11 | +| `docs/modules/src-tauri/render.md` | 维护文档 | 保留 | 13 | +| `docs/modules/src-tauri/secret.md` | 维护文档 | 保留 | 10 | +| `docs/modules/src-tauri/setup-lib.md` | 维护文档 | 保留 | 24 | +| `docs/modules/web/INDEX.md` | 维护文档 | 同步 | 28 | +| `docs/modules/web/OVERVIEW.md` | 维护文档 | 同步 | 9 | +| `docs/modules/web/SPEC.md` | 设计与来源参考 | 同步 | 2 | +| `docs/modules/web/hooks-i18n-theme.md` | 维护文档 | 保留 | 9 | +| `docs/modules/web/ipc-api.md` | 维护文档 | 保留 | 11 | +| `docs/modules/web/panels-ui.md` | 维护文档 | 同步 | 13 | +| `docs/modules/web/preview-ui.md` | 维护文档 | 同步 | 13 | +| `docs/modules/web/state-stores.md` | 维护文档 | 保留 | 11 | +| `docs/modules/web/timeline-ui.md` | 维护文档 | 保留 | 11 | +| `docs/plans/active/2026-09-06-public-beta.md` | 主代理维护 | 主代理 | 0 | +| `docs/port-map/account.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/agent-port-map.md` | 设计与来源参考 | 同步 | 1 | +| `docs/port-map/app.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/editor.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/export.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/generation.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/help.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/inspector.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/mediapanel.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/models.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/preview.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/project.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/search.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/settings.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/telemetry.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/timeline.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/toolbar.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/transcription.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/ui.md` | 设计与来源参考 | 保留 | 0 | +| `docs/port-map/utilities.md` | 设计与来源参考 | 保留 | 0 | +| `docs/project/conventions.md` | 维护文档 | 同步 | 2 | +| `docs/project/index.md` | 维护文档 | 同步 | 5 | +| `docs/project/intent.md` | 维护文档 | 同步 | 2 | +| `docs/releases/1.0.0-beta.1.md` | 历史保留 | 保留 | 0 | +| `docs/releases/1.0.0-beta.2.md` | 历史保留 | 保留 | 0 | +| `docs/releases/1.0.0-beta.3.md` | 历史保留 | 保留 | 0 | +| `docs/releases/1.0.0-beta.4.md` | 历史保留 | 保留 | 1 | +| `docs/releases/1.0.0-beta.5.md` | 历史保留 | 保留 | 2 | +| `docs/releases/1.0.0-beta.6.md` | 主代理维护 | 主代理 | 4 | +| `docs/specs/INDEX.md` | 设计与来源参考 | 保留 | 46 | +| `docs/specs/agent/0-evidence-index.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/1-mcp-server.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/10-implementation.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/2-tools.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/3-short-id.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/4-execution-shell.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/5-chat.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/6-context-signal.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/7-system-prompt.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/8-core-dispatch.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/agent/9-telemetry.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/0-design-baseline.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/1-editor-state.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/2-command-routing.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/3-event-bus.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/4-frontend-sync.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/5-assembly.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/6-tauri-commands.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/7-security.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/core/8-implementation.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/0-principles.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/1-design-tokens.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/10-state.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/11-tauri.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/12-data-models.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/13-implementation.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/2-layout.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/3-components.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/4-toolbar.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/5-timeline.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/6-inspector.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/7-media-panel.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/8-preview.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/frontend/9-interactions.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/0-principles.md` | 设计与来源参考 | 保留 | 1 | +| `docs/specs/media/1-structure.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/10-acceptance.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/11-implementation.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/2-ffmpeg.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/3-thumbnails.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/4-waveform.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/5-search.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/6-transcribe.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/7-ort-worker.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/8-coordinator.md` | 设计与来源参考 | 保留 | 0 | +| `docs/specs/media/9-domain-contract.md` | 设计与来源参考 | 保留 | 0 | +| `docs/superpowers/archive/2026-07-08-branch-integration-register.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/archive/2026-07-08-editing-automation-dos-session.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/archive/2026-07-08-verification-report.md` | 历史保留 | 保留 | 1 | +| `docs/superpowers/archive/2026-07-10-playback-cache-installed-app-qa.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/archive/2026-07-10-wave1a-baseline-disposition.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/archive/2026-07-13-cloud-integration-release-report.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-08-opentake-recovery-integration.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-10-opentake-wave-1a-integration-baseline.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-11-opentake-wave-1b-schema-compatibility.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-12-opentake-wave-1b-c1a-fail-closed-export.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-12-opentake-wave-1b-c1b-safe-filesystem.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-14-opentake-completion-audit.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-07-18-long-media-playback-handoff.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-10-opentake-beta4-release.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-13-beta5-agent-conversation.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-13-beta5-external-mcp.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-13-beta5-interface-polish.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-13-beta5-motion-studio.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-13-beta5-release.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-21-file-import-dialog.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-21-media-view-modes.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-21-opentake-full-ui-and-upstream-convergence.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-21-preview-tabs-parity.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/2026-08-21-preview-temporal-remap-parity.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/c1b/2026-07-12-c1b-common-unix-normative.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/plans/c1b/2026-07-12-c1b-windows-ci-normative.md` | 历史保留 | 保留 | 0 | +| `docs/superpowers/specs/2026-07-08-opentake-recovery-integration-design.md` | 设计与来源参考 | 保留 | 0 | +| `docs/superpowers/specs/2026-07-10-opentake-full-convergence-design.md` | 设计与来源参考 | 保留 | 0 | +| `docs/superpowers/specs/2026-07-12-opentake-wave-1b-c-filesystem-security-design.md` | 设计与来源参考 | 保留 | 0 | +| `docs/superpowers/specs/2026-07-14-opentake-completion-audit-design.md` | 设计与来源参考 | 保留 | 0 | +| `docs/superpowers/specs/2026-08-13-opentake-beta5-design.md` | 设计与来源参考 | 保留 | 0 | +| `docs/upstream-analysis/01-架构与数据流.md` | 设计与来源参考 | 保留 | 0 | +| `docs/upstream-analysis/02-苹果框架可移植性.md` | 设计与来源参考 | 保留 | 0 | +| `docs/upstream-analysis/03-闭源云边界.md` | 设计与来源参考 | 保留 | 0 | +| `docs/upstream-analysis/04-MCP与Agent工具.md` | 设计与来源参考 | 保留 | 0 | +| `docs/upstream-analysis/README.md` | 设计与来源参考 | 保留 | 0 | +| `docs/需求与问题汇总.md` | 维护文档 | 保留 | 0 | +| `plugins/motion-canvas-studio/README.md` | 维护文档 | 保留 | 0 | +| `plugins/motion-canvas-studio/THIRD_PARTY_NOTICES.md` | 运行资源或归属记录 | 保留 | 0 | +| `src-tauri/README.md` | 维护文档 | 同步 | 3 | +| `src-tauri/resources/ffmpeg/SOURCE.md` | 维护文档 | 保留 | 0 | +| `update-summary/2026-06-27-ui-refresh.md` | 历史保留 | 保留 | 0 | diff --git a/docs/knowledge/2026-09-06-beta-dependencies.md b/docs/knowledge/2026-09-06-beta-dependencies.md new file mode 100644 index 00000000..cb97c412 --- /dev/null +++ b/docs/knowledge/2026-09-06-beta-dependencies.md @@ -0,0 +1,19 @@ +--- +status: canonical +content_stage: implementation-backed +retrieved: 2026-09-06 +freshness_class: rapid +valid_until: 2026-09-07 +confidence: high +--- + +# Beta 候选依赖修复来源 + +发布前需实时审计,本记录不代替发布安全门禁。 + +| 依赖 | 项目原版本 | 目标版本 | 来源与结论 | +|---|---|---|---| +| Web/PostCSS → nanoid | 3.3.16 | 3.3.18 | [维护者发布](https://github.com/ai/nanoid/releases/tag/3.3.18)及[GitHub公告](https://github.com/advisories/GHSA-2v37-7h3g-55p8);审计要求3.x分支至少3.3.18,避免零长度自定义生成器的无限循环。锁文件仅更新此传递依赖。 | +| Motion Canvas → speech-rule-engine → @xmldom/xmldom | 0.9.10 | 0.9.12 | [维护者安全公告](https://github.com/xmldom/xmldom/security/advisories/GHSA-6gmq-8vp8-gcm6)说明无效 EntityReference 名称可注入序列化XML。speech-rule-engine 4.1.4 精确依赖旧版,使用 npm override 保持其4.x及Motion Canvas3.17.2,锁定修复版本。 | + +本轮实际 `pnpm -C web audit --audit-level moderate` 与 Motion Canvas `npm audit --audit-level=moderate` 均为0项;Motion测试、许可证和runner构建通过。此结论限定本次解析出的依赖图;应用自身未直接使用公告中的危险API,升级不代表已验证每条第三方执行路径。 diff --git a/docs/knowledge/2026-09-06-ffmpeg-pcm-boundary.md b/docs/knowledge/2026-09-06-ffmpeg-pcm-boundary.md new file mode 100644 index 00000000..c4d7d028 --- /dev/null +++ b/docs/knowledge/2026-09-06-ffmpeg-pcm-boundary.md @@ -0,0 +1,20 @@ +--- +status: canonical +content_stage: implementation-backed +retrieved: 2026-09-06 +freshness_class: normal +valid_until: 2026-09-16 +confidence: high +--- + +# FFmpeg PCM 尾部补齐与采样边界 + +GitHub CI 34041435244 的 Ubuntu FFmpeg `7:6.1.1-3ubuntu5` 在一秒、16kHz AAC 夹具上输出 16,384 个 f32 采样(65,536 bytes),超过按容器时长推得的一秒预算及单帧余量 64,004 bytes,触发媒体 facade 集成失败。macOS 当前 FFmpeg 9.0.1 没有在该夹具复现相同多余尾部。远端格式与 clippy 步骤通过,失败发生在实际媒体测试。 + +[FFmpeg 官方 atrim 文档](https://ffmpeg.org/ffmpeg-filters.html#atrim)说明 `end_sample` 是第一个应丢弃的采样编号,按实际采样计数而不是时间戳。应用需先转换到请求采样率,再以和 stdout 内存预算相同的目标帧数裁剪,避免对 48kHz 源直接按 16kHz 数量裁剪而截短内容。 + +最终契约:显式区间在重采样后按区间目标帧数裁剪,保留一帧舍入余量;`range=None` 保留原FFmpeg完整解码参数,不按容器时长隐式裁剪。无区间的stdout预算允许在探测时长之外最多一秒输出采样作为编解码器padding余量(16kHz/mono/f32额外64,000bytes(62.5KiB)),该余量通过checked乘加计算,超限仍拒绝并回收进程。不会静默截断过大的完整音轨。采样率/声道非零验证归一到所有时长预算调用都会经过的入口。 + +回归使用真实 FFmpeg,把两秒 48kHz WAV 转为 16kHz 且限于一秒预算:修复前 128,000 bytes,期望 64,000;修复后准确 64,000。13 项 PCM 单测与一项 facade 集成通过。同一轮 Windows full-product gate 也在相同 AAC 夹具复现相同字节数超限,证据为 job 101508803694。Linux/Windows 的原始失败场景须由新候选远端 CI 复验,不能用本地成功代替。 + +2026-09-07第二轮CI校正:`64dce59`的统一裁剪虽然修复facade,却被既有`extract_pcm_without_explicit_range_matches_full_track_decode`用例拒绝:Linux和Windows都应保留16,384采样,而统一裁剪只有16,000。该测试保留原样;代码按上述完整轨契约修正。1秒是明确的缓冲容差策略,不是对所有编解码器最大padding的测量或保证。当前本地media facade与FFmpeg集成15项通过/1项原有ignored;新候选远端复验待执行。 diff --git a/docs/knowledge/2026-09-06-semantic-search-model.md b/docs/knowledge/2026-09-06-semantic-search-model.md new file mode 100644 index 00000000..beb15fb1 --- /dev/null +++ b/docs/knowledge/2026-09-06-semantic-search-model.md @@ -0,0 +1,60 @@ +--- +status: canonical +stage: implementation-backed +retrieved_at: 2026-09-06T06:34:20Z +freshness: normal +valid_until: 2026-09-16 +confidence: high +--- + +# SigLIP2 公开 ONNX 模型资产 + +本记录是 2026-09-06 搜索模型缺口修复的外部知识来源。固定模型事实长期有效;远端可达性按 10 天重新验证。不是对上游最新版本的声明。 + +## 来源与版本 + +- [Google 模型卡](https://huggingface.co/google/siglip2-base-patch16-256):原始模型 `google/siglip2-base-patch16-256`,Apache-2.0,支持图文检索。 +- [ONNX Community 固定发布](https://huggingface.co/onnx-community/siglip2-base-patch16-256-ONNX/tree/d1114256522a37ffa257a0a58017348ab0058db2):由 Hugging Face 的 Xenova 发布,模型卡明确来源为上述 Google 模型;不是 OpenTake 自行托管或同名替代模型。 +- [固定 revision 文件 API](https://huggingface.co/api/models/onnx-community/siglip2-base-patch16-256-ONNX/revision/d1114256522a37ffa257a0a58017348ab0058db2?blobs=true):提供 LFS SHA-256 和准确长度。必须使用 `lfs.sha256`,不能将 Xet 存储 hash 或 Git blobId 当文件 SHA-256。 +- [固定预处理配置](https://huggingface.co/onnx-community/siglip2-base-patch16-256-ONNX/blob/d1114256522a37ffa257a0a58017348ab0058db2/preprocessor_config.json):256×256、双线性缩放、RGB mean/std 均为 0.5。 +- [固定 tokenizer 配置](https://huggingface.co/onnx-community/siglip2-base-patch16-256-ONNX/blob/d1114256522a37ffa257a0a58017348ab0058db2/tokenizer_config.json):Gemma tokenizer,无 BOS、有 EOS,右填 `` id 0,长度 64。 +- [Transformers 4.50.3 GemmaTokenizerFast](https://github.com/huggingface/transformers/blob/v4.50.3/src/transformers/models/gemma/tokenization_gemma_fast.py):特殊 token 后处理;截断应在加入 EOS 前完成。所下载 tokenizer.json 包含完整词表、后处理和空格正规化,不需要 tokenizer.model 或联网加载配置。 + +项目使用 `ort = 2.0.0-rc.11`、`tokenizers = 0.21.4`(锁文件实际解析版本),ONNX 配置记载导出工具 `transformers 4.50.0.dev0`;这几者不代表上游最新版本。本次没有改依赖。 + +## 选用资产 + +基础地址:`https://huggingface.co/onnx-community/siglip2-base-patch16-256-ONNX/resolve/d1114256522a37ffa257a0a58017348ab0058db2`。 + +| 相对路径 | 准确字节数 | 文件 SHA-256 | +|---|---:|---| +| `onnx/vision_model.onnx` | 371992072 | `f5cb16728a704703f05516ded628397e11dbca4de2eb5db04b0c0bcee988aa7a` | +| `onnx/text_model.onnx` | 1129469657 | `d3de4a6bbbfcb429b6615ac496790353cf4a4fc0f19fbbe7179e523ae60daaef` | +| `tokenizer.json` | 34363039 | `cb9140fae3ac5122c972d37adf83e1248471a38147ad76f8215c8872c6fd8322` | + +合计 **1,535,824,768 字节**(约 1.43 GiB)。以上值已与实际匿名公开下载重新计算的 SHA-256、文件长度逐一比对。只下载 FP32 两个独立编码器,不下载整个 11.4 GB 模型仓库、不使用量化变体、不生成修改后的 ONNX。 + +## 实际推理契约 + +| 图 | 输入 | 使用的输出 | +|---|---|---| +| Vision | `pixel_values`, float32 `(1,3,256,256)` | `pooler_output`, float32 `(1,768)` | +| Text | `input_ids`, int64 `(1,64)` | `pooler_output`, float32 `(1,768)` | + +图内还暴露 `last_hidden_state`,不能拿它当最终检索向量。两个池化输出没有 L2 归一化;零图像与示例文本测得范数约 14.08 和 24.39,必须图外归一化后再交给现有余弦点积排名。短文本右填 0,长文本截断保留 EOS。沿用 Rust 的黑底 alpha 合成、双线性正方形缩放。完整真实图像/文本和 Rust 验证见[审计记录](../audit/2026-09-06/semantic-search-model.md)。 + +旧文档曾要求等待 OpenTake 自托管资产,并假定图内归一化;这属于旧占位实现。本次选用公开 ONNX 分离编码器,因此明确更正接口契约;配置中的安装/索引版本升至 2,旧 v1 索引需重新生成。没有改模型名称、维度、分辨率或上下文长度。 + +## 有效性边界 + +已验证 macOS ARM64 CPU 原生 ONNX Runtime 与 Python ONNX Runtime。Windows 使用项目现有 ort-tract 后端,其对这两个实际图的兼容性需要 Windows 运行证据;不能由 macOS 验证推断已通过。公开 Hugging Face 链路在受限网络可能不可达,离线安装可使用相同字节与校验值。 + +## Windows 固定输入适配补充(2026-09-06) + +同一公开模型现通过 macOS 宿主上的**产品 Windows 搜索分支**真实 tract 推理,原生 Windows CI 仍待验证;完整红绿证据与数值见审计最后一节。 + +项目 Windows 搜索使用已有锁定 `tract-onnx 0.22.3` 的直接接口,其余 ort-tract 使用者保持原样。必要性来自已下载的发布源代码:`ort-tract 0.2.0+0.22/api.rs` 未实现 ORT 线程配置和 `AddFreeDimensionOverride*`;`ort-sys 2.0.0-rc.11/src/stub.rs` 对这些接口返回 Unimplemented。原生 ORT 可接受的动态图,不能由此推断 ort-tract 包装层可接受。 + +固定输入后还需重新推导中间动态维度:Tract 对导出 `value_info` 内嵌表达式的除法解析实测产生错误的 batch=0(`batch_size*(height/16)*(width/4096)`),与实际 Reshape 结果冲突。处理只发生在内存图的 shape fact:保留类型、rank、静态维度,绑定固定输入,再执行完整推导及 float32 `[1,768]` 输出检查。算子、权重、原始 ONNX 文件、manifest SHA-256 和模型版本均不改变。 + +这不是更换模型或升级上游依赖。`Cargo.lock` 仅新增 opentake-media 到既有 tract-onnx 包的直接依赖边;macOS/Linux 仍使用 native ORT。当前结论置信度:宿主同一产品分支运行高;Windows OS/MSVC 原生资格待主线 CI。有效期与本文件元数据一致。 diff --git a/docs/knowledge/2026-09-06-tauri-search-scheduling.md b/docs/knowledge/2026-09-06-tauri-search-scheduling.md new file mode 100644 index 00000000..bc63577b --- /dev/null +++ b/docs/knowledge/2026-09-06-tauri-search-scheduling.md @@ -0,0 +1,22 @@ +--- +status: canonical +content_stage: implementation-backed +retrieved: 2026-09-06 +freshness_class: normal +valid_until: 2026-09-16 +confidence: high +--- + +# Tauri 搜索任务调度 + +项目使用 Tauri 2(Cargo.lock 解析到 2.11.3)。[官方命令文档](https://v2.tauri.app/develop/calling-rust/#async-commands)说明:默认同步命令在主线程执行;async命令在异步任务执行,适合避免重任务卡住界面。 + +本次真实SigLIP2模型接通后,模型字节校验、初始化和推理是实际重任务。原`search_query`同步完成这些工作,`search_index_start`虽然提交后台worker,但同步等待结果,都会占住调用线程。 + +本轮将两个命令改为async,并通过`spawn_blocking`等待CPU任务。视觉查询复用现有有界`OrtWorker`,优先级为Interactive,索引为Background;两者共享已校验模型的registry,避免并发加载多份大型模型。没有新增调度依赖或另一套模型缓存。`visualError`是向后兼容的可选结果字段,文件名与字幕关键词结果仍保留。 + +此文记录官方执行规则和代码改动;真实安装包的响应性以当前候选验收为准。 + +排队复核补充:现有 worker 不抢占已经开始的整批索引。视觉查询采用 250 ms 的排队等待上限;仍未启动时持有与 worker 状态转换相同的锁取消该查询,返回 SEARCH_VISUAL_BUSY,已开始/完成的任务保留正常结果。队列满也返回相同忙状态。独立源码复核与 channel 阻塞索引的回归用例通过,避免用优先级名称误称存在抢占。 + +2026-09-07原生GUI进一步确认同一规则影响export_video:进程sample显示主AppKit线程同步运行完整GPU/FFmpeg导出,导致进度和取消无法处理。导出改用async+spawn_blocking并让后台任务持有ExportGuard;转写也沿既有有界推理worker后台执行。同步命令不会自动在worker执行,相关旧注释已纠正。 diff --git a/docs/knowledge/2026-09-07-motion-4k-screencast.md b/docs/knowledge/2026-09-07-motion-4k-screencast.md new file mode 100644 index 00000000..902050ca --- /dev/null +++ b/docs/knowledge/2026-09-07-motion-4k-screencast.md @@ -0,0 +1,41 @@ +--- +status: canonical +stage: implementation-backed +retrieved_at: 2026-09-07 +retrieval_timezone: Asia/Shanghai +freshness: normal +valid_until: 2026-09-17 +confidence: high +--- + +# Chrome 152 screencast 背压与 Motion 4K 提交顺序 + +项目实测浏览器是 `/Applications/Google Chrome.app`,实际进程版本 **152.0.7977.76**,macOS ARM64 / Apple M4。本文记录对应版本行为,不把 CDP tip-of-tree 文档当作项目固定浏览器版本。 + +## 官方来源 + +- [Chrome 152.0.7977.76 的 page_handler.cc](https://chromium.googlesource.com/chromium/src/+/refs/tags/152.0.7977.76/content/browser/devtools/protocol/page_handler.cc):获取源码并核对 `kMaxScreencastFramesInFlight`、`ShouldCaptureNextScreencastFrame`、`OnFrameFromVideoConsumer`、`ScreencastFrameAck`。 +- [CDP Page.startScreencast / screencastFrameAck](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-startScreencast):screencast 与按其会话 ID 确认帧的接口。 +- [CDP Emulation.setVirtualTimePolicy](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setVirtualTimePolicy):虚拟时间策略及预算;预算耗尽事件不能替代已收到目标像素帧的确认。 + +## 对应版本的关键事实 + +`page_handler.cc` 在第 100 行把最大 in-flight 常量定义为 2,但发送条件用 `<=`,因此实际可以有 **三个**尚未 ACK 的帧。`OnFrameFromVideoConsumer` 在窗口满时直接返回,不编码/发送该帧。`ScreencastFrameAck` 只在会话匹配时减少计数,没有重发被丢弃静态画面的逻辑。PNG 编码异步执行,启动帧和多个绘制更新可能在客户端开始消费前占满窗口。 + +项目修复前先 startScreencast,再更新作者 marker、transition 色,最后才开始消费/ACK 帧。4K 复现收到三个旧/seed 帧并逐一正常 ACK,但 transition 帧没有到达,最终命中原有 180 秒 deadline。这个观测与上述官方实现相互印证。不是颜色 guard 的舍入误判,也不是 ACK 命令卡住。 + +修复给已存在的 seed 增加提交握手:startScreencast + 作者 marker 更新后,先等待符合 seed 外部 guard **和当前作者 marker** 的帧并 ACK,再发布 transition。保留后续 transition、desired、三次作者像素一致性与黑白 alpha 恢复;没有延长 timeout 或加入重试。 + +## 边界与新鲜度 + +这是产品对 Chromium 背压处理的顺序缺陷,4K 和宿主时序使其暴露;不能简单归类为“当前 Mac 环境坏了”。握手是协议层顺序保证,真实 Windows/Linux 运行仍按项目平台门槛验收。旧 Windows 4K gate 成功不代表原顺序没有竞争窗口。 + +来源代码与首次 trace 保存在 `/tmp/opentake-motion-4k-20260906/`,完整复现、修复和测试结果见[专项审计](../audit/2026-09-07/motion-4k-screencast.md)。若浏览器升级,应重新核对发送窗口/ACK 行为及 opaque+transparent 实测,不沿用过期可达性或性能结论。 + +## 第三轮 CI 的补充结论 + +aa40672 / CI34047983006 的 Windows trace 证明:seed 本身也可能在首个发送窗口被三张旧白色帧挤掉。因此上文仅在修改seed后等待seed的第一版顺序不足,最新候选先启动并消费/ACK旧启动帧,再依次提交seed、作者marker、transition、desired;只把满足最终guard/marker与像素一致性的帧作为输出。重复attach/detach不是本次首个capture失败的必要条件,未据猜测重写该生命周期。 + +Linux mock 的 `Timeout(60s)` 是标签与手工构造的1秒deadline不一致:生产 `check_abort_state` 判断deadline、展示timeout字段。对应mock的原始TCP没有NoDelay,而锁定 [tungstenite 0.29.0 client.rs](https://docs.rs/crate/tungstenite/0.29.0/source/src/client.rs) 的连接路径会启用TCP_NODELAY。本轮保持1秒deadline,mock两端启用NoDelay并对齐标签;同样六次采样本机0.03秒通过。该修正消除mock额外的Nagle/delayed-ACK累积,并非延长超时或减少验算。 + +本轮Windows/Linux新补丁尚待原生资格结果,宿主macOS的97+7通过只能作为候选证据。第三轮实际trace与最新状态详见审计最后一节。 diff --git a/docs/knowledge/2026-09-07-windows-export-handles.md b/docs/knowledge/2026-09-07-windows-export-handles.md new file mode 100644 index 00000000..20be18aa --- /dev/null +++ b/docs/knowledge/2026-09-07-windows-export-handles.md @@ -0,0 +1,18 @@ +--- +status: canonical +content_stage: implementation-backed +retrieved: 2026-09-07 +freshness_class: normal +valid_until: 2026-09-17 +confidence: high +--- + +# Windows 普通导出的清理权限与文件验证 + +CI `34052877839` 的 Windows 全产品任务在 Tauri 单测阶段为 689 passed / 1 failed;唯一失败是普通输出清理,`SetFileInformationByHandle(FileDispositionInfo)` 返回 Access denied。普通输出打开函数没有申请 DELETE 权限,而保留到工程的输出打开函数已有该权限。 + +[Microsoft 文档](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle)明确要求设置删除 disposition 的句柄在 CreateFile 时申请 DELETE;文件删除在相关句柄关闭后完成。当前修复为普通输出申请 GENERIC_READ / GENERIC_WRITE / DELETE,同时继续拒绝 FILE_SHARE_DELETE,不给外部句柄新增替换文件名的能力。 + +输出验证改用已有的 `probe_file`,经继承的普通文件句柄和 `fd:` 协议读取同一份输出,避免路径重开与 Windows CRT 共享模式冲突。[FFmpeg 6.1.1 file 协议源码](https://raw.githubusercontent.com/FFmpeg/FFmpeg/n6.1.1/libavformat/file.c)包含 fd 协议;项目锁定 Windows FFmpeg/ffprobe 6.1.1,macOS ARM64 为 7.0。没有升级 sidecar 或扩大文件共享权限。 + +普通和 reserved 输出都保留验证所需的 File;`enabled` 继续决定谁负责删除,reserved 输出仍由外层 `ProjectMediaOutput` 管理。回归使用实际生产 opener,断言清理先把仍被 encoder clone 持有的文件截为零,再在最后 clone 关闭后确认文件不存在;另有真实 WAV 的保留句柄 probe/cleanup 用例。Windows 原生验证结果写入当日总验收,不能用 macOS 通过代替。 diff --git a/docs/modules/INDEX.md b/docs/modules/INDEX.md index 0cb78dbf..0bf1dc45 100644 --- a/docs/modules/INDEX.md +++ b/docs/modules/INDEX.md @@ -31,6 +31,10 @@ web React/TS 前端(只读镜像) - **[opentake-domain](opentake-domain/INDEX.md)** — Timeline/Track/Clip/Keyframe/Transform/Text/Grade 纯值语义;序列化模型。叶子 crate,禁止 I/O。 [总览](opentake-domain/OVERVIEW.md) +### 平台基础 +- **[opentake-process-tree](opentake-process-tree/INDEX.md)** — Unix 进程组 / Windows Job Object 生命周期与取消支持。 + [总览](opentake-process-tree/OVERVIEW.md) + ### 引擎层 - **[opentake-ops](opentake-ops/INDEX.md)** — Overwrite/Ripple/Snap 纯引擎、`EditCommand` 枚举、`apply()` 事务、撤销/重做栈、各 ops 算法(trim/move/split/ripple/link…)。 [总览](opentake-ops/OVERVIEW.md) @@ -38,13 +42,13 @@ web React/TS 前端(只读镜像) ### 能力层 - **[opentake-project](opentake-project/INDEX.md)** — 工程持久化、bundle/archive、布局、FCPXML(XMEML) 导出、生成日志。 [总览](opentake-project/OVERVIEW.md) -- **[opentake-render](opentake-render/INDEX.md)** — RenderPlan(纯函数 Timeline→每帧属性)、wgpu 合成器、文本栅格化;预览与导出像素一致。 +- **[opentake-render](opentake-render/INDEX.md)** — RenderPlan(纯函数 Timeline→每帧属性)、wgpu 合成器、文本栅格化;预览/导出共享语义,像素一致性按场景验收。 [总览](opentake-render/OVERVIEW.md) · [规格 SPEC](opentake-render/SPEC.md) - **[opentake-media](opentake-media/INDEX.md)** — FFmpeg 编解码、缩略图/雪碧图、波形、转写(whisper)、语义搜索(SigLIP2+ort)、节拍/静音/自动裁剪分析。 [总览](opentake-media/OVERVIEW.md) · [规格 SPEC](opentake-media/SPEC.md) - **[opentake-motion](opentake-motion/INDEX.md)** — Lottie / web 动态图形渲染、沙箱、缓存、与渲染管线集成。 [总览](opentake-motion/OVERVIEW.md) · [Motion Graphics 插件设计](opentake-motion/MOTION-GRAPHICS-PLUGIN.md) -- **[opentake-agent](opentake-agent/INDEX.md)** — MCP/Agent(当前最多 44 个可发布工具:38 基础 + 4 生成 + 2 动效,能力门控后按能力发布;KNOWN wire 名共 54)、工具派发、Context Signal、工作流插件、内置 Agent 聊天(Codex/ChatGPT 登录)。 +- **[opentake-agent](opentake-agent/INDEX.md)** — MCP/Agent(工具按基础集合、生成/Motion 桥与凭据动态发布;完整集合以源码和运行期发现为准)、工具派发、Context Signal、工作流插件、内置 Agent 聊天(Codex/ChatGPT 登录)。 [总览](opentake-agent/OVERVIEW.md) · [规格 SPEC](opentake-agent/SPEC.md) · [Context Signal](opentake-agent/AGENT-CONTEXT-SIGNAL.md) · [工作流插件](opentake-agent/WORKFLOW-PLUGIN-SYSTEM.md) - **[opentake-gen](opentake-gen/INDEX.md)** — 生成式 AI 客户端(fal.ai/Replicate/OpenAI/ElevenLabs)、模型目录、生成参数、BYOK 密钥(无后端)。 [总览](opentake-gen/OVERVIEW.md) · [规格 SPEC](opentake-gen/SPEC.md) diff --git a/docs/modules/opentake-agent/INDEX.md b/docs/modules/opentake-agent/INDEX.md index 8938d44c..4268f3db 100644 --- a/docs/modules/opentake-agent/INDEX.md +++ b/docs/modules/opentake-agent/INDEX.md @@ -1,8 +1,11 @@ # opentake-agent — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > -> `opentake-agent` = 工具层(= 上游 `ToolExecutor`,**44 个可发布工具:38 基础 + 4 生成 + 2 动效,能力门控后按能力发布;KNOWN wire 名共 54**)+ MCP server(rmcp Streamable-HTTP,`127.0.0.1:19789/mcp`)+ Context Signal + 工作流插件 + 内置 Agent 提示。能力层:依赖 `domain / ops / core / media / gen`,被 `src-tauri` 集成启动。 +> `opentake-agent` = 工具层(= 上游 `ToolExecutor`,**44 个可发布工具:基础 + 生成 + 动效,能力门控后按能力发布;KNOWN wire 名共 54**)+ MCP server(rmcp Streamable-HTTP,`127.0.0.1:19789/mcp`)+ Context Signal + 工作流插件 + 内置 Agent 提示。能力层:依赖 `domain / ops / core / media / gen`,被 `src-tauri` 集成启动。 --- diff --git a/docs/modules/opentake-agent/OVERVIEW.md b/docs/modules/opentake-agent/OVERVIEW.md index 415f1cf2..c2884680 100644 --- a/docs/modules/opentake-agent/OVERVIEW.md +++ b/docs/modules/opentake-agent/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-agent — 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-agent/src/tools/`、`mcp/`、`src-tauri/src/external_mcp.rs`。 +> 工具集合按实际主机能力和凭据动态发布,勿混用上游 31、兼容 wire 名和实际可见工具数。Beta 5 已引入外部 MCP 认证配对;透明 Motion 文档工具、链接移动修复在后续源码中存在。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块目录](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) --- @@ -30,7 +35,7 @@ src-tauri 桌面壳:build_registry + server::serve 起 MCP(src-tauri **做什么** -- 定义 **44 个可发布工具**(38 基础 + 4 生成 + 2 动效,能力门控后按能力发布;另有 9 个高级 AI 工具仅 schema 已知不主动发布,KNOWN wire 名共 54)的名称 / 描述 / JSON Schema / 类型化参数(= 上游工具单一事实源)。 +- 定义 **44 个可发布工具**(基础 + 生成 + 动效,能力门控后按能力发布;另有 9 个高级 AI 工具仅 schema 已知不主动发布,KNOWN wire 名共 54)的名称 / 描述 / JSON Schema / 类型化参数(= 上游工具单一事实源)。 - 一条统一派发管线包裹**每个**工具:解析名 → 快照 → 展开短 id → 解码(精确路径错误)→ 跑 body → 附 Context Signal → 缩短 id。 - 把编辑类工具归一到 `opentake-ops::EditCommand`,经 `CoreHandle` 应用到权威 `AppCore`。 - 提供 rmcp transport 组装能力;Beta 2 由官方 Codex 每轮创建带 Bearer 的临时回环 server。 @@ -112,13 +117,12 @@ MCP 客户端 → /mcp (loopback 守卫) → McpServer::call_tool ### 已实现 -- **44 个可发布工具的名称 / 描述 / Schema / 类型化参数**(`tools/`:38 基础 + 4 生成 + 2 动效,能力门控后按能力发布;未就绪能力按主机会话 fail-closed 隐藏)。 +- **可发布工具的名称 / 描述 / Schema / 类型化参数**(`tools/`:基础 + 生成 + 动效,能力门控后按能力发布;未就绪能力按主机会话 fail-closed 隐藏)。 - **统一派发管线**全链路(解析 → 快照 → 短 id 展开 → 解码 → body → 信号 → 短 id 缩短)。 - 编辑类工具接线到 `EditCommand`:`add_clips` / `insert_clips` / `move_clips` / `remove_clips` / `remove_tracks` / `split_clip` / `set_keyframes` / `ripple_delete_ranges` / `add_texts` / `set_clip_properties` / `create_folder` / `move_to_folder` / `rename_media` / `rename_folder` / `delete_media` / `delete_folder` / `undo`,以及 A-tier 效果 `set_color_grade` / `chroma_key` / `set_mask` / `apply_effect`。 - 读类工具:`get_timeline`(紧凑编码)/ `get_media` / `list_folders` / `list_models`(读 `opentake-gen` 静态目录,纯本地)。 - 分析驱动工具:`detect_beats` / `auto_cut_to_beats` / `tighten_silences` / `remove_filler_words`(PCM 或词级转写分析,**返回预览/建议,不直接落地**——`applied:false`,由模型审阅后再调编辑工具落地)。 -- **MCP server**:rmcp Streamable-HTTP + loopback/Origin 守卫 + Bearer gate;Beta 2 由 - `src-tauri/src/codex.rs` 按官方 Codex 轮次临时启动,固定外部 listener 不启动。 +- **MCP server**:rmcp Streamable-HTTP + loopback/Origin 守卫 + Bearer gate;官方 Codex 轮次由 `src-tauri/src/codex.rs` 临时启动;外部客户端由 `src-tauri/src/external_mcp.rs` 管理显式配对与认证 listener。 - **Context Signal**:视频类型自动判定 / 轨道角色检测 + 逐轨建议 / 剪辑阶段推断 + 阶段指引 / 内置规则告警 + 插件规则。 - **工作流插件**:JSON 模型 + 注册表(扫描/校验/激活)+ 内置 `audio-first` + 三个工作流工具(`list_workflows` / `activate_workflow` / `deactivate_workflow`)。 - **系统提示**:分段 base + 插件围栏注入。 @@ -133,7 +137,7 @@ MCP 客户端 → /mcp (loopback 守卫) → McpServer::call_tool - `create_folder` / `move_to_folder` 的批量 `entries` 形式未接线(仅单条形式)。 - **应用内聊天客户端**(`AgentService` 等价的 SSE 工具循环、BYOK 直连)已随 Beta 1 交付内置 Agent 聊天面板(`chat/` 模块 + `ChatLoop` / `ChatTurnGate`),并接入官方 Codex CLI / ChatGPT 登录(逐轮临时 loopback MCP,Beta 2)。剩余:ChatGPT 凭据由官方 CLI 独占,应用内不持久化。 -## 工具总数:**最多 44 个可发布工具(当前 38 基础 + 4 生成 + 2 动效,能力门控后按能力发布);KNOWN wire 名共 54(含 9 个高级 AI 工具,仅 schema 已知、不主动发布)** +## 工具总数:**最多 44 个可发布工具(当前 基础 + 生成 + 动效,能力门控后按能力发布);KNOWN wire 名共 54(含 9 个高级 AI 工具,仅 schema 已知、不主动发布)** 源:`crates/opentake-agent/src/tools/names.rs` 的 `ALL`(38)/ `GENERATION`(4)/ `MOTION`(2)/ `KNOWN`(54)/ `UPSTREAM`(31)常量。 diff --git a/docs/modules/opentake-agent/SPEC.md b/docs/modules/opentake-agent/SPEC.md index 4ff0ed6a..8096a8eb 100644 --- a/docs/modules/opentake-agent/SPEC.md +++ b/docs/modules/opentake-agent/SPEC.md @@ -1,5 +1,8 @@ # opentake-agent 实现就绪规格(Issue #9) +> 状态:draft · 阶段:partial-implementation · 设计与早期实现来源保留;原文日期、行号和“待做”属于设计时点。2026-09-06 当前实现见本模块 [OVERVIEW.md](OVERVIEW.md),当前验收见[公开 Beta 记录](../../audit/2026-09-06/public-beta-validation.md)。 + + > **Beta 2 安全裁决(2026-08-03):**本文保留上游固定端口方案作为设计来源与测试合同, > 但其“默认启动、未认证 `127.0.0.1:19789`、外部客户端直连”不再是产品运行合同。 > 当前产品仅由官方 Codex / ChatGPT 每轮创建随机 loopback 端口、256-bit Bearer、工程绑定的 @@ -9,7 +12,7 @@ > 短 ID 系统 + 统一执行壳 + 面向 LLM 的精确路径错误 + 应用内 chat + > **Agent Context Signal 注入** + **Workflow Plugin 系统**。 > -> 设计来源(已逐行核读):上游 `palmier-pro-upstream/Sources/PalmierPro/Agent/`(29 文件),以及 OpenTake `docs/AGENT-CONTEXT-SIGNAL.md`、`docs/WORKFLOW-PLUGIN-SYSTEM.md`、`docs/ARCHITECTURE.md §7/§9`、`docs/MODULE-PORT-MAP.md`「Agent」、`docs/_analysis/04-MCP与Agent工具.md`、`docs/ROADMAP.md` Phase 7/S/W。 +> 设计来源(已逐行核读):上游 `palmier-pro-upstream/Sources/PalmierPro/Agent/`(29 文件),以及 OpenTake `docs/AGENT-CONTEXT-SIGNAL.md`、`docs/WORKFLOW-PLUGIN-SYSTEM.md`、`docs/architecture/ARCHITECTURE.md §7/§9`、`docs/architecture/MODULE-PORT-MAP.md`「Agent」、`docs/_analysis/04-MCP与Agent工具.md`、`docs/architecture/ROADMAP.md` Phase 7/S/W。 > > 核心架构原则(上游验证,OpenTake 照搬):**编辑能力只有一处真实定义**(`opentake-core` 的 `EditCommand` 路由 → `opentake-ops`),**MCP server 与应用内 chat 是它的两个对等前端**,不写两套。Agent 层「非常薄」——31 个工具是 `opentake-core` 命令的薄包装;真正的编辑算法在 `opentake-ops`/`opentake-domain`(不在本 crate)。 > @@ -34,10 +37,10 @@ | 后端代理客户端(计费通道,OpenTake 替换) | `…/Agent/Clients/PalmierClient.swift` | `endpoint v1/agent/stream :35`;错误信封 `:80-101` | | get_timeline 编码(轨道/clip 结构 + 压缩规则,Context Signal 检测依据) | `…/Agent/Tools/ToolExecutor+Timeline.swift` | `getTimeline :17`;`trackDefaults :60`(`muted/hidden/syncLocked`);`clipDefaults :62`;`compactTrack :73`;`compactClip :112` | | add_clips 行为(覆写、自动建轨、linked audio、全有或全无) | `…/Agent/Tools/ToolExecutor+Clips.swift` | `AddClipsInput :5`;`addClips :13`;`Mixed trackIndex :171-174`;`insertTrack :194/:199`;`clearRegion+placeClip :225-226` | -| OpenTake 目标 crate 边界 + §7 MCP 设计 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/ARCHITECTURE.md` | crate 布局 `:64-87`;`EditCommand/EditResult :105-116`;§7 `:148-154`;§9 目录 `:165-177` | +| OpenTake 目标 crate 边界 + §7 MCP 设计 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/architecture/ARCHITECTURE.md` | crate 布局 `:64-87`;`EditCommand/EditResult :105-116`;§7 `:148-154`;§9 目录 `:165-177` | | Context Signal 全设计 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/AGENT-CONTEXT-SIGNAL.md` | 发射时机表 `:37-47`;数据结构 `:50-83`;插件叠加 `:88-98`;类型检测 `:104-140`;轨道角色 `:148-173`;规则 `:177-203` | | Workflow Plugin 全设计 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/WORKFLOW-PLUGIN-SYSTEM.md` | 目录 `:18-24`;plugin.json schema `:28-96`;激活 `:100-104`;影响 Agent `:108-118`;与 Core 关系 `:136-141` | -| Phase 7 / S / W 验证标准 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/ROADMAP.md` | Phase 7 `:52-59`;Phase S `:99-110`;Phase W `:113-119` | +| Phase 7 / S / W 验证标准 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/architecture/ROADMAP.md` | Phase 7 `:52-59`;Phase S `:99-110`;Phase W `:113-119` | --- diff --git a/docs/modules/opentake-agent/mcp-server.md b/docs/modules/opentake-agent/mcp-server.md index cb55d53e..7d2b84ec 100644 --- a/docs/modules/opentake-agent/mcp-server.md +++ b/docs/modules/opentake-agent/mcp-server.md @@ -1,5 +1,8 @@ # mcp-server — rmcp MCP server 网络面 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 实际桌面认证与候选验证见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。 + > 上级:[模块目录](INDEX.md) · [总览](OVERVIEW.md) · [docs 总目录](../../INDEX.md) > > 源码:[`../../../crates/opentake-agent/src/mcp/server.rs`](../../../crates/opentake-agent/src/mcp/server.rs) @@ -17,7 +20,7 @@ `McpServer` 实现 rmcp 的 `ServerHandler`,持有一个 `Arc`(自带会话级 agent-undo 栈)+ 构造时快照的系统提示 `instructions`。 - **`get_info`** — 广告 `instructions`(base 提示 + 激活插件,构造时由 [`assemble_system_prompt`](prompt.md) 生成)与 tools 能力;`server_info.name = "opentake"`,版本取 `CARGO_PKG_VERSION`。 -- **`list_tools`** — 从最多 39 个基础工具(`ToolName::ALL`)按当前主机的媒体桥能力过滤,在授权可用时追加 4 个生成工具,并在桌面 Motion 渲染桥可用时追加 add/edit 两工具;描述/Schema 来自 [`tools::descriptions`](dispatch-tools.md)。 +- **`list_tools`** — 从基础工具集合(`ToolName::ALL`)按当前主机的媒体桥能力过滤,在授权可用时追加 4 个生成工具,并在桌面 Motion 渲染桥可用时追加 add/edit 两工具;描述/Schema 来自 [`tools::descriptions`](dispatch-tools.md)。 - **`call_tool`** — 把工具调用交给 `Dispatcher::dispatch`。因为所有已接线工具是同步的,用 `tokio::task::spawn_blocking` 在阻塞线程池跑,避免堵住 async 运行时;结果经 [`convert::to_call_tool_result`](core-handle-convert.md) 转成 rmcp `CallToolResult`。 - **`call`** — 与 `call_tool` 等价的同步入口,单独拆出以便**不构造传输 `RequestContext`** 就能单测一次工具派发。 @@ -26,19 +29,19 @@ `build_router` 组装 axum 路由: - `nest_service("/mcp", StreamableHttpService::new(...))` —— 每次会话用 `McpServer::new(handle, registry)` 新建(`LocalSessionManager` 管理会话)。 -- `GET /.well-known/oauth-protected-resource` —— 返回 `{ resource: "opentake", authorization_servers: [] }`,让探测客户端得到明确的"无需鉴权"回答(服务仅回环,故不挂任何授权服务器)。 +- `GET /.well-known/oauth-protected-resource` —— 返回 `{ resource: "opentake", authorization_servers: [] }`,该发现信息不列出 OAuth authorization server;不能由空数组推导为桌面端无需 Bearer 认证。 - 整条路由外层 `from_fn(localhost_guard)`。 -`serve(addr, handle, registry)` 绑定回环 `TcpListener` 并 `axum::serve` 到进程退出。`DEFAULT_ADDR = "127.0.0.1:19789"`(端口沿用上游)。 +`build_router*` 是库层装配面;桌面生产路径使用 gated/authorized transport。官方 Codex 逐轮临时启动认证 listener,Beta 5 外部 MCP 使用显式配对和可撤销凭据。`DEFAULT_ADDR = "127.0.0.1:19789"` 是默认常量,不能等同于默认启动的未认证服务。生命周期见 `src-tauri/src/codex.rs` 与 `src-tauri/src/external_mcp.rs`。 ### 回环 Origin/Host 守卫(DNS-rebinding 防御) `localhost_guard` 中间件检查请求头: -- `Host` 与 `Origin` **若存在**必须指向回环;**缺省即放行**(原生 MCP 客户端常不带 `Origin`)。 +- Host/Origin 的缺省、重复值、authority 与端口按 `localhost_guard`、`host_is_local` 和 `origin_is_local` 的实际规则校验;认证由外层 Bearer authorizer 与工程/请求 gate 完成。 - 存在但非回环 → `403 "non-local Origin/Host rejected"`。 -`host_is_local` 解析规则:剥协议(`http://host:port` 形式)→ 剥路径/查询 → 剥端口(IPv6 括号形式 `[::1]:port` 单独处理)→ 匹配 `localhost` / `127.0.0.1` / `::1`。这是防 DNS-rebinding 把本地回环服务暴露给 LAN/网页的关键(对应上游 `NWParameters.requiredLocalEndpoint` 锁回环)。 +`host_is_local` 解析规则:剥协议(`http://host:port` 形式)→ 剥路径/查询 → 剥端口(IPv6 括号形式 `[::1]:port` 单独处理)→ 匹配 `localhost` / `127.0.0.1` / `::1`。这是回环请求校验的一部分,不能替代 Bearer、工程身份和请求边界检查(对应上游 `NWParameters.requiredLocalEndpoint` 锁回环)。 ## 数据流 diff --git a/docs/modules/opentake-core/INDEX.md b/docs/modules/opentake-core/INDEX.md index 5add89c6..928207fe 100644 --- a/docs/modules/opentake-core/INDEX.md +++ b/docs/modules/opentake-core/INDEX.md @@ -1,5 +1,8 @@ # opentake-core — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `opentake-core` = **装配层(命令路由层)**:把 `opentake-{domain,ops,project}` + 注入式能力句柄装配成一个权威可观测的会话 `EditorSession`,对 UI / Agent / MCP 三客户端暴露唯一编辑入口 `AppCore::apply`,经版本号 + 事件广播驱动前端只读镜像。依赖只向下:依赖 `domain` / `ops` / `project`,被 `src-tauri` 调用(`opentake-agent` 也作为客户端持其句柄)。 diff --git a/docs/modules/opentake-core/OVERVIEW.md b/docs/modules/opentake-core/OVERVIEW.md index 7de65571..6f1f79ea 100644 --- a/docs/modules/opentake-core/OVERVIEW.md +++ b/docs/modules/opentake-core/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-core 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-core/src/`。 +> 继续持有工程/会话、命令编排和能力桥;本轮 Motion 媒体目录保留及链接行为修复沿用工程身份与事务边界。下文较早 API 列表是结构参考,完整注册/变体以源码为准。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) · 本模块目录:[INDEX.md](INDEX.md) ## 一句话定位 diff --git a/docs/modules/opentake-core/SPEC.md b/docs/modules/opentake-core/SPEC.md index 4b679f70..002236a0 100644 --- a/docs/modules/opentake-core/SPEC.md +++ b/docs/modules/opentake-core/SPEC.md @@ -1,5 +1,8 @@ # opentake-core 历史合订规格(Issue #11) +> 状态:draft · 阶段:partial-implementation · 设计与早期实现来源保留;原文日期、行号和“待做”属于设计时点。2026-09-06 当前实现见本模块 [OVERVIEW.md](OVERVIEW.md),当前验收见[公开 Beta 记录](../../audit/2026-09-06/public-beta-validation.md)。 + + > **状态:历史快照,不是当前实现的权威来源。** 本文件保留 Issue #11 时的原始合订设计,部分类型名、字段草案和 Undo/Redo 形态已与代码不同。当前可执行规格以 [`docs/specs/core/`](../../specs/core/) 的拆分文件为准;共享编辑状态与命令路由请直接阅读 [`1-editor-state.md`](../../specs/core/1-editor-state.md) 和 [`2-command-routing.md`](../../specs/core/2-command-routing.md)。审计与验收不得用本历史快照覆盖拆分规格或当前代码证据。 > **范围**:`crates/opentake-core/` —— EditorState 组装、命令路由(= 上游单一能力层)、事件总线、Tauri 边界契约。 @@ -17,7 +20,7 @@ > - `Utilities/Constants.swift:105-115`(工程目录包文件名常量) > - `Export/ExportService.swift` + `Export/ExportView.swift:13-26`(导出表面) > - `Agent/Tools/ToolExecutor+Clips.swift:129-209`(写工具样板:decode→validate→withUndoGroup→mutate) -> 以及 `docs/ARCHITECTURE.md` §2/§5/§7 与 `docs/ROADMAP.md` Phase 6/7。 +> 以及 `docs/architecture/ARCHITECTURE.md` §2/§5/§7 与 `docs/architecture/ROADMAP.md` Phase 6/7。 --- @@ -371,7 +374,7 @@ impl EditorCore { ## 5. 与 ops / project / render / agent 的装配关系 -`opentake-core` 是**装配中枢**(ARCHITECTURE §3:「`opentake-core/` # 组装:EditorState…、command 路由、事件总线」)。依赖法则(ARCHITECTURE §3 末「依赖法则」):`domain` 零依赖叶子;`ops` 只依赖 `domain`;`command` 是唯一编辑入口;UI/Agent/MCP 是三个对等客户端。 +`opentake-core` 是**装配中枢**(ARCHITECTURE §3:「`opentake-core/` # 组装:EditorState…、command 路由、事件总线」)。依赖法则(ARCHITECTURE §3 末「依赖法则」):`domain` 无 I/O 叶子;`ops` 只依赖 `domain`;`command` 是唯一编辑入口;UI/Agent/MCP 是三个对等客户端。 ### 5.1 依赖方向(谁依赖谁) diff --git a/docs/modules/opentake-domain/INDEX.md b/docs/modules/opentake-domain/INDEX.md index 35c0caca..b4d1ec5f 100644 --- a/docs/modules/opentake-domain/INDEX.md +++ b/docs/modules/opentake-domain/INDEX.md @@ -1,5 +1,8 @@ # opentake-domain — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > 值语义叶子层(依赖只有 `serde`,禁 `std::fs` / 网络)。被 `opentake-ops` 及其上所有层依赖。 diff --git a/docs/modules/opentake-domain/OVERVIEW.md b/docs/modules/opentake-domain/OVERVIEW.md index 878ec1d6..36c7ee54 100644 --- a/docs/modules/opentake-domain/OVERVIEW.md +++ b/docs/modules/opentake-domain/OVERVIEW.md @@ -1,5 +1,11 @@ # opentake-domain — 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-domain/Cargo.toml`、`src/clip.rs`、`src/timeline.rs`。 +> 领域 crate 无 I/O、仅依赖 serde(开发依赖另计);它不是零依赖 crate。帧/关键帧与兼容模型是值层契约,不承担 GUI 与发布验收。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + + > 模块目录:[INDEX.md](INDEX.md) · 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) --- diff --git a/docs/modules/opentake-gen/INDEX.md b/docs/modules/opentake-gen/INDEX.md index 05a9a624..66ca4c7a 100644 --- a/docs/modules/opentake-gen/INDEX.md +++ b/docs/modules/opentake-gen/INDEX.md @@ -1,5 +1,8 @@ # opentake-gen — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > 生成式 AI 客户端(**BYOK 无后端**:自带 key 直连 fal.ai/Replicate/OpenAI/ElevenLabs + 内置静态模型目录;可选托管 proxy)。能力层叶子 crate,仅依赖 `opentake-domain`,被 `opentake-agent` / `src-tauri` 调用。 diff --git a/docs/modules/opentake-gen/OVERVIEW.md b/docs/modules/opentake-gen/OVERVIEW.md index df3ba682..b03e5245 100644 --- a/docs/modules/opentake-gen/OVERVIEW.md +++ b/docs/modules/opentake-gen/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-gen — 模块总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-gen/src/`、`src-tauri/src/generation.rs`。 +> provider adapter、能力/凭据检查与生成作业实现和真实付费 provider 验收分开记录;无需因本轮文档同步重跑付费任务。源代码中的 provider 接入不能写成全部账号开箱即用。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[opentake-gen 目录](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > 模块/子系统级总览(不逐函数)。完整规格见 [SPEC.md](SPEC.md)(只读,本总览只链接、不复述)。 @@ -94,7 +99,7 @@ GenerationInput(持久化) + uploaded URLs - `HttpTransport` + `ReqwestTransport` / `MockTransport`(全套测试零 socket)。 - **已接线**:`list_models` 工具已从存根接到内置静态目录(agent `mcp/gen_catalog.rs`,ROADMAP #111);BYOK 钥匙串 save/load/delete Tauri 命令(聊天 LLM key)。 -**计划中 / 后续版本(Beta 1/2 已交付之外):** +**集成边界与后续核对:** - **`generate_*` / `upscale_media` 已接线**:agent `dispatch.rs` 的四个生成工具经 `GenerationBridge`(异步 job + BYOK/托管授权 + 成本确认 + 进度/取消/重试/恢复 + 产物安全下载落库)接入;存在兼容凭据时动态进入 MCP/Chat 发现面,无凭据 fail-closed 隐藏(ROADMAP Phase 9 / Phase 7 进度注记)。 - **托管 proxy `opentake-gen-proxy` 未实现**(Phase 9 自建后端:`/v1/models`、`/v1/generations`、`/v1/uploads/sign`、SSE stream、对象存储预签名、可选积分计费;SPEC §3)。客户端侧已就绪,等服务端;Beta 阶段以 BYOK 直连为准。 diff --git a/docs/modules/opentake-gen/SPEC.md b/docs/modules/opentake-gen/SPEC.md index 91e83442..ba3de284 100644 --- a/docs/modules/opentake-gen/SPEC.md +++ b/docs/modules/opentake-gen/SPEC.md @@ -1,5 +1,8 @@ # gen-SPEC — opentake-gen 实现就绪规格(Issue #10) +> 状态:draft · 阶段:partial-implementation · 设计与早期实现来源保留;原文日期、行号和“待做”属于设计时点。2026-09-06 当前实现见本模块 [OVERVIEW.md](OVERVIEW.md),当前验收见[公开 Beta 记录](../../audit/2026-09-06/public-beta-validation.md)。 + + > 状态:实现就绪(implementation-ready)。本规格基于上游 Swift 真实源码逐字段复刻,每个契约点给出 `文件:行号` 证据。 > 范围:`crates/opentake-gen`(BYOK 生成客户端 + 静态 catalog + provider adapters)与可选的 `opentake-gen-proxy`(axum 托管模式后端)。 > 约束遵循 OpenTake `AGENTS.md`:内部错误用 `anyhow::Error`,边界层转 `Err(String)`;所有 serde 模型加 `#[serde(default)]` + `Option` 以读旧数据不破坏;注释最小化。 @@ -57,7 +60,7 @@ bytes = "1" opentake-domain = { workspace = true } # GenerationInput 等共享类型(见 §5) ``` -> 说明:`opentake-domain` 是零依赖叶子 crate(`crates/opentake-domain/src/lib.rs:1-8`),不允许网络/FS。因此 **`GenerationInput` 放 domain,`GenerationParams`/`GenClient`/adapters 放 opentake-gen**,方向是 gen → domain。详见 §5.1。 +> 说明:`opentake-domain` 是无 I/O 叶子 crate(`crates/opentake-domain/src/lib.rs:1-8`),不允许网络/FS。因此 **`GenerationInput` 放 domain,`GenerationParams`/`GenClient`/adapters 放 opentake-gen**,方向是 gen → domain。详见 §5.1。 ### 1.1 顶层客户端 @@ -739,7 +742,7 @@ pub struct UpscaleCaps { ### 5.1 GenerationInput 落在 opentake-domain(逐字段复刻 `MediaManifest.swift:36-63`) -`GenerationInput` 是**持久化到工程文件**的领域类型(上游存于 `MediaManifestEntry.generationInput` `MediaManifest.swift:26`),故放零依赖 `opentake-domain`,按 `AGENTS.md:62` 全字段 `#[serde(default)]` + `Option`: +`GenerationInput` 是**持久化到工程文件**的领域类型(上游存于 `MediaManifestEntry.generationInput` `MediaManifest.swift:26`),故放无 I/O 的 `opentake-domain`,按 `AGENTS.md:62` 全字段 `#[serde(default)]` + `Option`: ```rust // crates/opentake-domain/src/generation.rs(新增;domain 当前无此文件) @@ -888,6 +891,6 @@ pub struct GenerationInput { - `…/OpenTake/Cargo.toml` — workspace 含 `opentake-gen`/`opentake-domain`/`opentake-agent`(`members`);`serde`/`serde_json` 在 `[workspace.dependencies]` - `…/OpenTake/crates/opentake-gen/{Cargo.toml,src/lib.rs}` — 当前为 Phase 0 空脚手架(`[dependencies]` 为空 / lib 仅 `crate_compiles` 测试) -- `…/OpenTake/crates/opentake-domain/{Cargo.toml,src/lib.rs}` — 零依赖叶子 crate(仅依赖 serde;声明“Zero IO”) +- `…/OpenTake/crates/opentake-domain/{Cargo.toml,src/lib.rs}` — 无 I/O 叶子 crate(仅依赖 serde;声明“Zero IO”) - `…/OpenTake/crates/opentake-agent/src/lib.rs` — 工具层 + MCP(rmcp) + chat 客户端(peer clients) - `…/OpenTake/AGENTS.md` — Rust 风格铁律(`anyhow`→边界 `Err(String)`;domain 零网络/FS;serde `default`+`Option`;≥80% 覆盖) diff --git a/docs/modules/opentake-media/INDEX.md b/docs/modules/opentake-media/INDEX.md index 83648d28..4035ff92 100644 --- a/docs/modules/opentake-media/INDEX.md +++ b/docs/modules/opentake-media/INDEX.md @@ -1,5 +1,8 @@ # opentake-media — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `opentake-media` = 媒体读取与离线分析层:ffmpeg sidecar 探测/解码/编码、缩略图/雪碧图、波形、转写(whisper)、SigLIP2 语义搜索、节拍/静音/自动裁剪分析、全局素材库。依赖只向下:仅依赖 `opentake-domain`,被 `opentake-core` / `src-tauri` / `opentake-agent` / `opentake-render` 调用。运行期需 **FFmpeg ≥ 6.0 在 PATH**。 diff --git a/docs/modules/opentake-media/OVERVIEW.md b/docs/modules/opentake-media/OVERVIEW.md index 98543566..1f4be234 100644 --- a/docs/modules/opentake-media/OVERVIEW.md +++ b/docs/modules/opentake-media/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-media — 模块总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-media/src/search/config.rs`、`encode/`、`ffmpeg/`。 +> 包含 ProRes 4444、完整轨道 PCM 抽取和媒体索引。语义搜索模型安装修复已完成:约 1.5 GB 固定 revision 资产校验、macOS 真实 Rust 离线安装/图文 embedding/排名已验证。使用前需安装模型;Windows ort-tract 真实图及新包 UI 待验收,见[专项审计](../../audit/2026-09-06/semantic-search-model.md)。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块目录 INDEX.md](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) --- @@ -101,15 +106,15 @@ opentake-core / src-tauri / opentake-agent / opentake-render 调用方 - 探测 / 解码帧 / 抽 PCM / ffmpeg sidecar 封装(含旋转校正、零声道音轨防幻影链接)。 - 视频缩略图序列 + JPEG 雪碧图磁盘缓存(与上游 key/meta 互读)、图片单缩略图。 - 波形(ffmpeg PCM → RMS → 归一化 + `.waveform` 缓存)。 -- 编码器 + 预设表(H.264/H.265/ProRes)+ 线性音频混音(逐 clip 偏移 + 增益 + 硬限幅,第二趟 ffmpeg mux AAC / `-shortest`,mux 失败回退视频-only)。 +- 编码器 + 预设表(H.264/H.265/ProRes)+ 线性音频混音(逐 clip 偏移 + 增益 + 硬限幅,第二趟 ffmpeg mux AAC / `-shortest`,mux 失败按错误/清理路径返回,不静默交付无音轨结果)。 - 转写数据模型 + locale 匹配 + 双层缓存 + 转写内关键词搜索(纯逻辑全测);whisper 后端在 `whisper-backend` feature 后。 - 语义搜索全链路纯函数(预处理 / tokenize / 视觉去重抽帧 / 索引累积 / `PALMEMB1` 存储 / 排名 / 模型下载校验);ort 后端在 `ort-backend` feature 后;默认 build 用 mock 离线可测。 - 节拍检测、静音检测、自动裁剪(黑边)。 - 全局素材库(内容寻址去重 + 原子 manifest,#104;Tauri 命令层在 src-tauri,#106)。 - 后台索引调度内核(`work_needed` / `visual_share` / `ExportPause`)。 -**计划中 / 后续版本(Beta 1/2 已交付之外):** -- **whisper / ort 后端真实接线**:whisper-rs 本地转写(word/segment 时间戳)已交付;SigLIP2 + ort 语义搜索的推理/索引/排名链路已接入产品路径(Captions / Moments / `search_media`)。⚠️ **SigLIP2 ONNX 模型资产待外部托管**:`search/config.rs` 的 `Manifest` sha256/bytes 仍为空占位(`MODEL_DOWNLOAD_BASE_URL` 指向 `huggingface.co/opentake/siglip2-base-patch16-256-onnx`),在 ONNX 资产正式托管并填实校验值前,`download_search_model`(Tauri 命令)/ `model_download::install` 无法通过 SHA-256 校验完成真实下载——语义搜索**不是开箱即用**,模型首次下载需联网。 +**集成边界与后续核对:** +- **whisper / ort 后端真实接线**:whisper-rs 本地转写(word/segment 时间戳)已交付;SigLIP2 + ort 语义搜索的推理/索引/排名链路已接入产品路径(Captions / Moments / `search_media`)。**模型安装与真实 Rust 链路已验证**:固定 revision 资产逐文件校验通过;Cargo media search 84 passed / 1 ignored、Tauri search 14 passed,引用实际产品模块的真实模型 Rust harness 72 passed。各组覆盖有重叠,不合计为独立测试数。Windows ort-tract 真实图及新包 UI 尚待验收,证据见[专项审计](../../audit/2026-09-06/semantic-search-model.md)。 - **自动裁剪升级**:当前 `autocrop` 仅做黑边/透明区扫描,**未集成人脸/显著性 ML**(SPEC 的 `smart_reframe` 完整语义为后续版本)。 - **编码导出**:H.264/H.265/ProRes + 进度/取消已随 Beta 1 交付(`src-tauri/export.rs`,预览与导出共享 RenderPlan);音频重采样曲线 / pan / 立体声 / 动态处理为后续。 - **进阶 AI 推理(ADVANCED-FEATURES B 层,复用 `ort_worker`)**:Beta 1 已交付 RVM 抠像、防抖、补帧、智能擦除、参考色彩匹配与可视化运动追踪;超分 / 真实显著性自动裁剪为后续。 diff --git a/docs/modules/opentake-media/SPEC.md b/docs/modules/opentake-media/SPEC.md index 36b6f8b7..ea9a1db5 100644 --- a/docs/modules/opentake-media/SPEC.md +++ b/docs/modules/opentake-media/SPEC.md @@ -1,5 +1,8 @@ # opentake-media 实现就绪规格 (Issue #8) +> 状态:draft · 阶段:partial-implementation · 设计与早期实现来源保留;原文日期、行号和“待做”属于设计时点。2026-09-06 当前实现见本模块 [OVERVIEW.md](OVERVIEW.md),当前验收见[公开 Beta 记录](../../audit/2026-09-06/public-beta-validation.md)。 + + > 范围:`crates/opentake-media`。把上游基于 AVFoundation / DSWaveformImage / macOS 26 Speech / CoreML 的媒体读取层,移植为跨平台 Rust:**ffmpeg-next 解码/编码/缩略图/抽 PCM、Symphonia 波形、whisper-rs 转写、candle/ort + SigLIP2 语义搜索、ort 通用推理 worker**。本 crate 是媒体**读取与离线分析**层,不含 wgpu 帧合成器(那在 `opentake-render`,见 §9 边界)。 > > 状态:设计规格。对应 ROADMAP **Phase 2**(缩略图/波形,易)与 **Phase 8**(转写/语义搜索/进阶 AI worker)。本 crate 在 workspace 已有空壳 `crates/opentake-media/{Cargo.toml,src/lib.rs}`(`crate_compiles` 占位测试)。 @@ -8,8 +11,8 @@ > - 解码/编码/缩略图/PCM:`palmier-pro-upstream/Sources/PalmierPro/Preview/{ImageVideoGenerator,AlphaVideoNormalizer,TimelineRenderer}.swift`、`Transcription/Transcription.swift`(`extractAudioTrack`)、`Timeline/MediaVisualCache.swift`(缩略图 sprite + 波形 + 磁盘缓存)。 > - 转写:`Transcription/{Transcription,TranscriptCache,TranscriptSearch}.swift`。 > - 语义搜索:`Search/{SearchIndexConfig,SearchIndexCoordinator}.swift`、`Search/Models/{VisualEmbedder,VisualModelLoader,ModelDownloader,TextTokenizer}.swift`、`Search/Indexing/{FrameSampler,VisualIndexer,EmbeddingStore}.swift`、`Search/Query/VisualSearch.swift`。 -> - 横切分析:`docs/_analysis/02-苹果框架可移植性.md`、`docs/_analysis/01-架构与数据流.md`、`docs/MODULE-PORT-MAP.md`(行级算法笔记 L833–883、L923–940、L1211)。 -> - 架构/相位:`docs/ARCHITECTURE.md` §1/§6/§7、`docs/ROADMAP.md` Phase 2/8、`docs/ADVANCED-FEATURES.md`(ort worker 复用方)。 +> - 横切分析:`docs/_analysis/02-苹果框架可移植性.md`、`docs/_analysis/01-架构与数据流.md`、`docs/architecture/MODULE-PORT-MAP.md`(行级算法笔记 L833–883、L923–940、L1211)。 +> - 架构/相位:`docs/architecture/ARCHITECTURE.md` §1/§6/§7、`docs/architecture/ROADMAP.md` Phase 2/8、`docs/architecture/ADVANCED-FEATURES.md`(ort worker 复用方)。 > - 领域契约:`crates/opentake-domain/src/{media,clip_type,timeline,clip}.rs`(本 crate 消费方,不可改)。 --- @@ -21,11 +24,11 @@ 1. **时间单位分层**:本 crate 一律用**秒(`f64`)**与**源采样位置**作 IO 边界量;帧↔秒换算(`Int(s*fps)` 截断)留在 `opentake-domain`/调用层,本 crate**不做** fps 折算。证据:上游 `Transcription`/`MediaVisualCache`/`FrameSampler` 全用 `seconds`,`secondsToFrame` 在 `MediaTab`(上层)。 2. **零硬编码常量**:所有阈值(promoteDiff=12、coverageFloor=8.0、imageSize=256、dim=768、relativeCutoff=0.85、cosineFloor=0.05、波形 count 公式 150/帧 与 20000 上限、缩略图 maximumSize=120×68、sprite 列数=50 …)以 `pub const` / `Options` 结构体集中声明,值**逐字照搬**上游。 3. **缓存键与磁盘格式逐字节复刻**:嵌入/转写使用 `SHA256("path|mtime_unix_f64|size")`,缩略图/波形使用 `SHA256("path|size|mtime_unix_f64")`,均保留前 16 字节为 32 hex;同时复刻 `PALMEMB1` 二进制布局、`.waveform`/`.thumbs.jpg`+`.thumbs.json` sidecar、转写 JSON。理由:让 OpenTake 与上游/旧工程的缓存目录**可互读**(同机迁移),并保证幂等判定一致。 -4. **错误用 `thiserror` 定义本 crate 错误,内部传播用 `anyhow`,边界返回 `Result`**;`opentake-domain` 零依赖,本 crate 是第一层允许 IO 的 crate。 +4. **错误用 `thiserror` 定义本 crate 错误,内部传播用 `anyhow`,边界返回 `Result`**;`opentake-domain` 无 I/O(依赖 serde),本 crate 是第一层允许 IO 的 crate。 5. **不可变 / 纯函数优先**:排名(`VisualSearch`)、波形降采样、采样判定、转写过滤等都是无副作用纯函数,可全单测;有状态的只有索引调度器(§7.7)与模型加载器(§5.6)。 6. **后端推理可插拔**:`Embedder` / `Transcriber` / `OrtWorker` 定义为 trait,默认实现走 ort 或 candle;测试注入 mock(协议化 DI)。 7. **导出期让路**:任何后台任务(索引/缩略图/波形)在导出活跃时暂停。证据:上游 `ExportService.isExporting.didSet → SearchIndexCoordinator.exportDidBegin/End`(`MODULE-PORT-MAP` L457)、`SearchIndexCoordinator.waitWhileExportActive`(`SearchIndexCoordinator.swift:49`)。 -8. **L2 归一化对齐风险**:上游裸点积 `cblas_sgemv` 是否等价余弦,取决于导出模型是否在图内 L2 归一化(`MODULE-PORT-MAP` L860)。本 crate **必须复用上游同一份权重转 ONNX**,并在 `Embedder::encode` 后做一次**条件 L2 归一化开关**(`Spec.normalized: bool`),默认 false 以匹配上游(模型内已归一化)——除非验证证明需要外部归一化。 +8. **L2 归一化契约**:以真实固定模型输出为准。当前 ONNX Community 图输出未归一化,`Spec.normalized=false`,应用执行 L2 归一化后再按余弦阈值排名;已通过 macOS/Windows 真实模型验证。旧“false 表示模型已归一化”的假设已废止。更换模型/I/O 必须重测并升级缓存版本,见 [语义搜索规范](semantic-search.md)。 --- @@ -759,7 +762,7 @@ pub fn best_supported_locale(supported: &[&str]) -> Option; // 系统首 ## 7. ort 推理 worker 通用接口(供进阶 AI 特性复用) -上游无此抽象(CoreML 直接在 `VisualEmbedder`)。`docs/ROADMAP.md` Phase 8 与 `docs/ADVANCED-FEATURES.md` B/C/D 层要求「统一 ort worker」承载:超分(Real-ESRGAN/SeedVR)、AI 抠像(RVM/BiRefNet)、运动追踪(CoTracker)、人声分离(Demucs)等。SigLIP2 的 `OrtEmbedder`(§5.7)是它的第一个使用者。 +上游无此抽象(CoreML 直接在 `VisualEmbedder`)。`docs/architecture/ROADMAP.md` Phase 8 与 `docs/architecture/ADVANCED-FEATURES.md` B/C/D 层要求「统一 ort worker」承载:超分(Real-ESRGAN/SeedVR)、AI 抠像(RVM/BiRefNet)、运动追踪(CoTracker)、人声分离(Demucs)等。SigLIP2 的 `OrtEmbedder`(§5.7)是它的第一个使用者。 ### 7.1 通用模型抽象 @@ -858,14 +861,14 @@ impl IndexCoordinator { - `encode::{VideoEncoder, ExportPreset}`(导出后端把合成 RGBA 帧序列 + 混音编码成容器)。 - `MediaProbe`(渲染尺寸/源 fps 决策)。 -**职责切分**(`docs/ARCHITECTURE.md` §1/§6): +**职责切分**(`docs/architecture/ARCHITECTURE.md` §1/§6): - `opentake-media` = **读取/编码 + 离线分析**(解码到 RGBA、抽 PCM、缩略图、波形、转写、语义索引/搜索、ort worker)。 - `opentake-render` = **合成 + 调度**(RenderPlan 纯函数、wgpu 逐帧合成、媒体物化为纹理、预览/导出后端、A/V 同步)。`renderSize` 偶数化、BT.709 instruction、关键帧 ramp **全在 render**。 - 二者通过 **`RgbaFrame` / `PcmBuffer`** 这两个朴素值类型交换帧/样本,无 wgpu/ffmpeg 类型泄漏到边界。 ### 8.3 媒体物化(图片/Lottie → 纹理)的归属 -上游用 `ImageVideoGenerator`(图片烧静止视频)、`LottieVideoGenerator`(Lottie 烧 ProRes)、`AlphaVideoNormalizer`(直 alpha 预乘)绕开 AVPlayer 限制。`docs/_analysis/02` 表 L74/L75/L81 与 `docs/ARCHITECTURE.md` §6 L130:**自建 wgpu 合成器后,这三类 hack 整类消失**——图片/Lottie 在合成前**物化为纹理**(content-hash 缓存),由 `opentake-render` 负责。 +上游用 `ImageVideoGenerator`(图片烧静止视频)、`LottieVideoGenerator`(Lottie 烧 ProRes)、`AlphaVideoNormalizer`(直 alpha 预乘)绕开 AVPlayer 限制。`docs/_analysis/02` 表 L74/L75/L81 与 `docs/architecture/ARCHITECTURE.md` §6 L130:**自建 wgpu 合成器后,这三类 hack 整类消失**——图片/Lottie 在合成前**物化为纹理**(content-hash 缓存),由 `opentake-render` 负责。 - 本 crate **提供**:图片解码 → `RgbaFrame`(§3.2 / `image` crate);(可选)Lottie 解码用 `rlottie` FFI 或 `velato`(`docs/_analysis/02` 表 L81),渲成 `RgbaFrame` 序列。**建议** Lottie 放 render 的物化层或独立 `opentake-motion` fallback。Motion Canvas v1 输出 mp4 后走普通视频 import,不归本 crate。 - 本 crate **不提供**:静止视频烧制、ProRes 烧制、alpha 预乘转码(整类删除)。 @@ -966,4 +969,4 @@ impl MediaEngine { - 转写:`Transcription/Transcription.swift:5-39`(模型/`offsetting:26-38`)、`:72-90`(locale)、`:284-322`(`decodeResults` segment/word);`Transcription/TranscriptCache.swift:12-88`(缓存/filter/key);`Transcription/TranscriptSearch.swift:12-36`(terms/matches)。 - 语义搜索:`Search/SearchIndexConfig.swift:4-45`(阈值/manifest);`Search/Models/VisualEmbedder.swift:7-87`(Spec/encode/预处理 squash 黑底 `:81-85`/输出断言 `:53-61`);`Search/Models/TextTokenizer.swift:16-23`(截断 64 右填 0);`Search/Models/ModelDownloader.swift:46-172`(安装/校验/解压);`Search/Models/VisualModelLoader.swift:86-110`(load + warm-up);`Search/Indexing/FrameSampler.swift:40-117`(采样/LumaGrid `:94-117`);`Search/Indexing/VisualIndexer.swift:15-86`(累积/幂等);`Search/Indexing/EmbeddingStore.swift:30-115`(PALMEMB1);`Search/Query/VisualSearch.swift:16-56`(sgemv 排名);`Search/SearchIndexCoordinator.swift:37-257`(调度/导出暂停/查询)。 - 领域契约:`OpenTake/crates/opentake-domain/src/media.rs:226-440`(MediaResolver/MediaAsset)、`clip_type.rs:781-832`(ClipType)、`timeline.rs:931-1031`、`clip.rs`。 -- 横切:`OpenTake/docs/_analysis/02-苹果框架可移植性.md`(能力→栈映射表 L66-83、攻坚清单 L98-136);`OpenTake/docs/MODULE-PORT-MAP.md`(L833-883 搜索、L923-940 存储、L1211 转写);`OpenTake/docs/ARCHITECTURE.md` §1/§6/§7;`OpenTake/docs/ROADMAP.md` Phase 2/8。 +- 横切:`OpenTake/docs/_analysis/02-苹果框架可移植性.md`(能力→栈映射表 L66-83、攻坚清单 L98-136);`OpenTake/docs/architecture/MODULE-PORT-MAP.md`(L833-883 搜索、L923-940 存储、L1211 转写);`OpenTake/docs/architecture/ARCHITECTURE.md` §1/§6/§7;`OpenTake/docs/architecture/ROADMAP.md` Phase 2/8。 diff --git a/docs/modules/opentake-media/semantic-search.md b/docs/modules/opentake-media/semantic-search.md index 4a4e730b..40c507f1 100644 --- a/docs/modules/opentake-media/semantic-search.md +++ b/docs/modules/opentake-media/semantic-search.md @@ -14,11 +14,11 @@ --- -## 常量 `config.rs`(逐字照搬上游 `SearchIndexConfig`) +## 常量 `config.rs`(检索阈值对齐上游,模型缓存版本按实际 ONNX 契约升级) ```rust pub const MODEL_NAME: &str = "siglip2-base-patch16-256"; -pub const MODEL_VERSION: i32 = 1; +pub const MODEL_VERSION: i32 = 2; pub const EMBEDDING_DIM: usize = 768; pub const IMAGE_SIZE: u32 = 256; pub const CONTEXT_LENGTH: usize = 64; @@ -54,15 +54,15 @@ pub const PAD_TOKEN: i64 = 0; pub struct SiglipTokenizer { /* HF tokenizers + context_length */ } pub fn pad_or_truncate(ids: &[u32], len: usize) -> Vec; // 截断到 len,右填 0 ``` -- HF `tokenizers` crate(与上游 swift-transformers 同源);**手动**截断到 64 + 右填 0,**关闭** tokenizer 自动 padding/truncation——SigLIP 训练无 attention mask,必须与 Python 参考严格一致。 +- HF `tokenizers` 在特殊 token 后处理前按 context length 截断,保留长查询末尾 EOS,再右填 0 到 64;模型只接收 input_ids,不传 attention mask。通用 `pad_or_truncate` 仍保留,真实 tokenizer 路径先完成上述约束。 ### ort 后端 `ort_embedder.rs`(feature `ort-backend`) ```rust pub struct OrtEmbedder { image: Mutex, text: Mutex, tokenizer: SiglipTokenizer, spec, io: IoNames } -pub struct IoNames { pub image_input, image_output, text_input, text_output: String } // 默认 "image"/"embedding"/"tokens"/"embedding" +pub struct IoNames { pub image_input, image_output, text_input, text_output: String } // 默认 "pixel_values"/"pooler_output"/"input_ids"/"pooler_output" ``` -- 图像输入 NCHW f32、文本输入 `(1,64)` int64;输出断言 `len == dim`,否则 `BadModelOutput`。 -- **L2 归一化开关**:`spec.normalized` 默认 `false`(上游模型图内已归一化),裸点积即等价余弦;当前 `finalize` 仅做长度校验(标定路径保留为后续,[SPEC.md](SPEC.md) §0.8 风险)。务必复用上游同一份导出权重转 ONNX。 +- 图像输入 NCHW f32、文本输入 `(1,64)` int64;输出校验维度、有限值及非零范数。Windows 使用同一锁定 Tract 引擎绑定固定输入并重推导动态维度,保留 dtype/rank/静态尺寸及 `[1,768]` 输出约束;macOS/Linux 使用原生 ORT。 +- **L2 归一化**:`spec.normalized` 表示图输出是否已经归一化。当前固定 ONNX 图输出未归一化,因此该值为 `false`,`finalize` 在校验后执行 L2 归一化;随后点积才可按余弦阈值排名。更换权重或 I/O 契约须重新实测并更新缓存版本。 --- @@ -161,22 +161,24 @@ pub fn rank(query: &[f32], indexes, limit, relative_cutoff, min_score) -> Vec PathBuf; // /-v/ -pub fn installed(models_dir, m) -> Option; +pub fn installed(models_dir, m) -> Option; // 快速检查回执及长度 +pub fn verify_installed(models_dir, m) -> Result; // 加载前重验实际 SHA-256 +pub fn install_from_directory(models_dir, m, source_dir) -> Result; pub fn verify_sha256(path, expected) -> Result<()>; // 1MiB 流式 pub async fn install(models_dir, m, base_url, on_progress) -> Result<...>; // feature model-download ``` -- 幂等下载 image/text encoder + tokenizer → 逐个 **SHA-256 流式校验**(1MiB 块)→ tokenizer.zip 解压单顶层目录 → 原子 rename 到最终位置 → 写 spec.json。`installed` 按三件 + `tokenizer/tokenizer.json` 存在性判定。 +- 按固定 revision、长度和 SHA-256 下载两个 encoder 与 tokenizer,支持安全相对子目录。当前直接使用 tokenizer.json,保留旧 ZIP 兼容。先在临时目录完整校验并写 spec/manifest 回执,再发布安装目录,失败时清理或恢复旧目录。`installed` 核对当前回执和准确长度,加载前 `verify_installed` 重新流式验 hash;离线安装走相同校验。 - 相比上游去掉了 `MLModel.compileModel`(ONNX 无需编译)。 -- ⚠️ **占位待填**:`Manifest` 的 sha256/bytes 当前为空字符串/0,待实际 ONNX 资产托管后填实([ROADMAP.md](../../architecture/ROADMAP.md) Phase 8)。 +- **模型安装修复已完成并验证(2026-09-06)**:原空 sha256/bytes 占位已替换为固定 revision 资产清单,约 1.5 GB 文件已逐一校验。macOS 真实 Rust 路径已验证离线安装、图文 embedding、归一化、PALMEMB1 f16 往返和排名;Windows/MSVC 与 macOS 原生资格流程已通过真实安装、图文推理和五条检索断言;当前新包 UI 验收另见总记录。详见[模型专项审计](../../audit/2026-09-06/semantic-search-model.md),完整 revision/校验值由审计链接的知识记录统一维护。 --- ## feature 与完成状态 ```toml -ort-backend = ["ort", "ndarray"] # 默认 build 不含;启用后真实 SigLIP2 推理 +ort-backend = ["dep:ort", "dep:ort-tract", "dep:tract-onnx"] # 依赖按平台激活;默认不启用 model-download = ["reqwest", "zip", ...] # 启用后下载 ``` -全链路纯函数 + mock **已实现并全测**;真实 ort 推理 + 模型托管属 Phase 8 计划中。改任何烧印常量(promoteDiff/coverageFloor/dim/imageSize…)须两侧同步。 +模型安装与真实后端纵向链路已实现并验证:正式 Cargo media search 为 84 passed / 1 ignored,Tauri search 为 14 passed;引用实际产品模块并运行真实模型的 Rust harness 为 72 passed / 0 ignored,包含离线安装、图文 embedding 与排名。后者不是 Python 推理替代,也不是已执行正式 Cargo opt-in 命令;复现方式与证据见[专项审计](../../audit/2026-09-06/semantic-search-model.md)。三组测试覆盖有重叠,不相加为独立测试数。后续正式平台流程已在 `64dce59` 上于 macOS 与 Windows 各执行同一真实模型 Cargo 用例(1 passed/0 ignored),见 [平台资格结果](https://github.com/appergb/OpenTake/actions/runs/34043674623)。新包 UI 与最终发布状态按总验收记录维护。改任何烧印常量(promoteDiff/coverageFloor/dim/imageSize…)须两侧同步。 ## 测试 预处理(黑→-1/白→+1/squash/alpha 合成)、pad_or_truncate、候选时间(stride/回退/零 duration)、luma_grid(黑/白/Rec.601)、ShotDetector 状态机(首帧/去重/镜头切/覆盖下限/grid 总更新)、accumulate_rows(首镜头归零/链接/幂等)、PALMEMB1 往返(f16 量化/版本校验/多字节拒绝)、排名(点积排序/best-per-shot/limit-then-floor/空索引)、install_dir/installed/SHA256、ort_worker 张量互转;端到端 `index_then_rank_finds_brightest_match`(mock 流)。 diff --git a/docs/modules/opentake-motion/INDEX.md b/docs/modules/opentake-motion/INDEX.md index 618b2ef5..caa46ef1 100644 --- a/docs/modules/opentake-motion/INDEX.md +++ b/docs/modules/opentake-motion/INDEX.md @@ -1,10 +1,13 @@ # opentake-motion — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `opentake-motion` = **原生 web 动态图形 fallback 渲染原语层**:把内联 HTML/CSS/JS(或模板 + 参数)确定性逐帧栅格化为磁盘 RGBA PNG 帧序列、内容寻址缓存、安全沙箱,并适配成 `opentake-render` 的 clip source。 > **Beta v1 已接入桌面**:`plugins/motion-canvas-studio/` 锁定 Motion Canvas 3.17.2,`title-card` 通过官方逐帧 renderer 产帧并编码 `mp4`;本 crate 的离线 Chromium 宿主也承载本地 HTML/CSS fallback。两条路径经 Tauri/Core/Agent/UI 共用的原子导入落轨流程进入普通视频预览与导出。**不是 Lottie 渲染器**(Lottie 在 [opentake-render](../opentake-render/INDEX.md))。 -> 透明 alpha、任意 TSX 与 frame-sequence source 仍是后续能力;当前 Beta 对它们显式报不支持。 +> 透明 alpha 发布已进入候选源码;任意 TSX 与通用 frame-sequence source 仍属后续范围,不能混为同一未实现项。 --- diff --git a/docs/modules/opentake-motion/MOTION-GRAPHICS-PLUGIN.md b/docs/modules/opentake-motion/MOTION-GRAPHICS-PLUGIN.md index 9238dd35..8b4fb5a5 100644 --- a/docs/modules/opentake-motion/MOTION-GRAPHICS-PLUGIN.md +++ b/docs/modules/opentake-motion/MOTION-GRAPHICS-PLUGIN.md @@ -1,6 +1,6 @@ # Motion Canvas 动效 / AI Video 插件设计与 Beta v1 实施记录 -> 状态:**Beta v1 已实现并通过代码/原生集成验收(2026-08-01)**。固定 `title-card` 使用 Motion Canvas 3.17.2;HTML/CSS 路径保留为受限 fallback,透明/frame-sequence 属后续版本。 +> 状态:**Beta v1 已实现并通过代码/原生集成验收(2026-08-01)**。固定 `title-card` 使用 Motion Canvas 3.17.2;HTML/CSS 路径保留为受限 fallback,这是 2026-08-01 的历史范围。2026-09-06 候选已有透明 Motion/ProRes 4444 路径,通用 frame-sequence 仍为后续目标;当前状态见[模块总览](OVERVIEW.md)。 > Issue trail:#34 motion dispatch / motion graphics;已认领实现切片。 > 决策:先 fork / vendor Motion Canvas(MIT)做独立外部模块或内置插件,让它产出可导入的视频文件;OpenTake 负责一站式导入、落轨、预览和导出。 diff --git a/docs/modules/opentake-motion/OVERVIEW.md b/docs/modules/opentake-motion/OVERVIEW.md index fbf8d6dd..838ef961 100644 --- a/docs/modules/opentake-motion/OVERVIEW.md +++ b/docs/modules/opentake-motion/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-motion — 模块总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-motion/src/`、`src-tauri/src/motion.rs`。 +> 受限 HTML/CSS Motion Studio 与固定 Motion Canvas runner 已接入。透明发布与编辑保留 alpha 已有源码;ProRes 4444 编码/输出由 media/Tauri 层承担。任意 TSX 工程宿主仍不在当前开放范围。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块目录 INDEX.md](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > **Beta v1 已接入桌面时间线。** `plugins/motion-canvas-studio/` 提供锁定的 Motion Canvas 3.17.2 `title-card` runner;本 crate 提供离线 Chromium 帧宿主、缓存与 HTML/CSS fallback。Tauri/Core/Agent/Motion Panel 共用同一原子 Render → Import → Place/Replace 流程。完整设计与验收见 [Motion Graphics 插件设计](MOTION-GRAPHICS-PLUGIN.md)。 @@ -46,7 +51,7 @@ opentake-core / src-tauri / opentake-agent 已接 v1 调用方(渲染、验 - **Motion Canvas 模板是 v1 主模板路径**。本 crate 的 Chromium 宿主负责逐帧执行其官方 renderer;HTML/CSS/JS 仍是兼容 fallback。二者产 `mp4` 后复用普通视频导入/预览/导出链路。 - **默认 build 不启动浏览器**。真实 CDP 后端必须显式启用 `chromium` feature;默认路径仍 fail-closed 返回 `RendererUnavailable`。 - **不持 UI 状态 / 不直接做时间线落轨**。导入 + 落轨的单事务(Render → Import Media → Place Clip)仍属 `src-tauri`/`opentake-core`,本 crate 负责安全、确定性的帧生产。 -- **不定义 `ClipType::Motion`**。透明动效与新 clip 类型 / frame sequence source 是后续目标。 +- **不定义独立 `ClipType::Motion`**。透明发布已使用媒体导入/编码路径;直接 frame sequence source 仍是独立后续目标。 - **不做帧↔秒折算的真理**。本 crate 内部 `t = frame / fps` 仅用于渲染时间网格;时间线帧↔秒的真理在 domain / 调用层(移植铁律,见 §6)。 --- @@ -128,13 +133,13 @@ MotionSource (Code 内联文档 | Template id + params) - 集成桥:`MotionClipSource` 实现 `SourceMetrics` + `FrameProvider`,解码器注入、过末端钳位、缺帧返回 `None`。 **Beta v1 已实现(集成验收):** -- `plugins/motion-canvas-studio/`:Motion Canvas 3.17.2 lockfile、MIT LICENSE/notice、typed job、`title-card.tsx`、确定性离线 runner 与可复现 bundle;npm audit 0,锁定依赖许可证门禁通过。 +- `plugins/motion-canvas-studio/`:Motion Canvas 3.17.2 lockfile、MIT LICENSE/notice、typed job、`title-card.tsx`、确定性离线 runner 与可复现 bundle;当时的 npm audit 与许可证门禁记录保留;当前依赖审计见当日候选记录。 - Tauri/Core:受控临时目录、离线 Chromium、进度/取消、FFmpeg `output.mp4`、`motion-result.json` 精确校验、项目 media capability 发布、单事务注册/落轨或替换、一步撤销与保存重开。 - 产品入口:独立 Motion Panel 与动态发布的 Agent add/edit 工具共享生产桥;普通视频 preview/export 无需识别 Motion Canvas。 - 自动化:两次固定模板渲染像素/哈希一致;失败、取消、遍历、符号链接、畸形/篡改元数据无 manifest/timeline 变更;`composite_frame` 与 `export_video` 均验证包含生成片段。 **计划中 / 待做(明确未实现):** -- `MotionClipSource` 直接接入 `TextureSource::FrameSequence`、PNG sequence、透明 alpha overlay/ProRes4444——属 v2。 +- `MotionClipSource` 直接接入通用 frame sequence/PNG sequence 仍待规划;透明 alpha 发布与 ProRes 4444 已有候选源码,不再列作未实现。 - 用户提供的任意 TS/TSX 编译和通用模板工程宿主——Beta 仅开放固定 `title-card`;其他输入显式拒绝或走受限 HTML fallback。 - 独立结构化 `motion_metadata`/长期保留 job 文件;Beta 使用 `generation_input` 保存可编辑来源,并在命令结果返回经验证的 output metadata。 @@ -142,7 +147,7 @@ MotionSource (Code 内联文档 | Template id + params) ## 6. 移植铁律(本模块必须遵守) -来自 [AGENTS.md](../../../AGENTS.md) 移植铁律、[MOTION-GRAPHICS-PLUGIN.md](MOTION-GRAPHICS-PLUGIN.md) 与代码现况: +来自[开发规范](../../project/conventions.md)的移植规则、[MOTION-GRAPHICS-PLUGIN.md](MOTION-GRAPHICS-PLUGIN.md) 与代码现况: 1. **一切以整数帧为单位**:渲染时间网格 `t = i / fps`(`i ∈ 0..duration_frames`);`RenderedClip` / 缓存 / source 全以帧索引寻址。时间线帧↔秒的真理留在 domain / 调用层(`secondsToFrame` 用截断 `Int(s*fps)`,不四舍五入)。 2. **确定性可复现(预览 == 导出)**:渲染器必须对同一 `MotionRenderRequest` 产出**字节一致**的帧。这是内容寻址缓存与"预览/导出像素一致"的硬前提;任何引入非确定性(墙钟、随机、未冻结的页面时钟)的实现都违规。 diff --git a/docs/modules/opentake-ops/INDEX.md b/docs/modules/opentake-ops/INDEX.md index 163c2d67..a5cc8aeb 100644 --- a/docs/modules/opentake-ops/INDEX.md +++ b/docs/modules/opentake-ops/INDEX.md @@ -1,5 +1,8 @@ # opentake-ops — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `opentake-ops` = 纯编辑引擎 + 唯一编辑入口 `EditCommand` + `apply()` 事务 + 整树快照撤销/重做栈。依赖只向下:仅依赖 `opentake-domain`,被 `opentake-core` 调用。 diff --git a/docs/modules/opentake-ops/OVERVIEW.md b/docs/modules/opentake-ops/OVERVIEW.md index 5dc05dca..c1c5f2a0 100644 --- a/docs/modules/opentake-ops/OVERVIEW.md +++ b/docs/modules/opentake-ops/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-ops 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-ops/src/ops/`、`web/src/store/editActions.ts`。 +> 编辑仍经过 Rust 命令与撤销事务;本轮选区驱动 split/trim、链接移动、Option trim 和范围选择的接线进展见同步报告。旧 GAP 文档不是当前未实现列表。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) · 本模块目录:[INDEX.md](INDEX.md) ## 一句话定位 @@ -95,7 +100,7 @@ UI 手势 / Agent / MCP 工具 - 命令层属性类操作:`SetClipProperties` / 关键帧族(`SetKeyframes` / `StampKeyframe` / `RemoveKeyframe` / `MoveKeyframe` / `SetKeyframeInterpolation`)/ `SetColorGrade` / `SetChromaKey` / `SetMasks` / `SetEffects` / `SetTrackProps` / `SwapMedia`。 - `intent.rs` 高层意图预检:自动建轨放置、卡点放置、修剪到播放头、单区间波纹删除、smart-reframe。 -**计划中(仅 ROADMAP / GAP 规划,本 crate 代码尚未落地):** +**设计与后续核对(旧 ROADMAP / GAP 仅作来源,不能直接判定未实现):** - 与上游 1:1 的若干接线层 / 模型扩展缺口主要在**前端与 domain**,不在本 crate(见 [EDITING-ENGINE-PLAN.md](../../architecture/EDITING-ENGINE-PLAN.md) §3):如 fade knee 拖拽态、隐藏轨 hitTest 过滤、`Clip.isSoloed` 字段(需前后端 DTO 扩展)、轨间插入阈值 `insertThreshold`、Snap 容差按 DPI 缩放。 - 曲线变速(speed 升级为关键帧轨)、复合片段嵌套等属 ROADMAP 后期能力,本 crate 当前无对应命令。 diff --git a/docs/modules/opentake-process-tree/INDEX.md b/docs/modules/opentake-process-tree/INDEX.md new file mode 100644 index 00000000..399f613b --- /dev/null +++ b/docs/modules/opentake-process-tree/INDEX.md @@ -0,0 +1,8 @@ +# opentake-process-tree — 目录 + +> 状态:draft · 阶段:implementation-backed · 同步日期:2026-09-06 + +- [总览](OVERVIEW.md):职责、平台差异与验证边界。 +- [源码](../../../crates/opentake-process-tree/src/lib.rs):启动配置、绑定、终止与等待。 +- [依赖清单](../../../crates/opentake-process-tree/Cargo.toml)。 +- [开发规范](../../project/conventions.md) · [模块总目录](../INDEX.md)。 diff --git a/docs/modules/opentake-process-tree/OVERVIEW.md b/docs/modules/opentake-process-tree/OVERVIEW.md new file mode 100644 index 00000000..12cb0fca --- /dev/null +++ b/docs/modules/opentake-process-tree/OVERVIEW.md @@ -0,0 +1,9 @@ +# opentake-process-tree — 总览 + +> 状态:draft · 阶段:implementation-backed · 同步日期:2026-09-06 + +进程树生命周期支持 crate,位于 [Cargo workspace](../../../Cargo.toml);实现见 [lib.rs](../../../crates/opentake-process-tree/src/lib.rs)。`configure_command` 配置平台进程启动,`ProcessTree::attach` 绑定子进程,`terminate` / `terminate_and_wait` / `wait_for_exit` 提供取消与退出等待。 + +Unix 使用进程组,Windows 使用 Job Object 相关实现。它为调用方清理媒体或工具进程提供基础能力,不持有 Timeline,也不负责 UI。源码存在不代表 Windows 原生进程树与安装器已在此候选验收;证据见[当日验证](../../audit/2026-09-06/public-beta-validation.md)。 + +[模块目录](INDEX.md) · [模块总目录](../INDEX.md) diff --git a/docs/modules/opentake-project/INDEX.md b/docs/modules/opentake-project/INDEX.md index 0a97eb74..2652dfe9 100644 --- a/docs/modules/opentake-project/INDEX.md +++ b/docs/modules/opentake-project/INDEX.md @@ -1,5 +1,8 @@ # opentake-project — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `opentake-project` = 工程持久化层:`.opentake` 目录包读写 + 自包含归档 + XMEML(FCP7 XML) 时间线导出 + 生成日志。依赖只向下:仅依赖 `opentake-domain`,被 `opentake-core` / `src-tauri` 调用。 diff --git a/docs/modules/opentake-project/OVERVIEW.md b/docs/modules/opentake-project/OVERVIEW.md index 501607e0..bcf21acc 100644 --- a/docs/modules/opentake-project/OVERVIEW.md +++ b/docs/modules/opentake-project/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-project 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-project/src/`。 +> 工程保存、自包含归档、布局和 XMEML 交换格式为本模块范围;视频编码位于 media/Tauri。日期化测试和旧交接不自动代表 Beta 6 候选已验收。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) · 本模块目录:[INDEX.md](INDEX.md) ## 一句话定位 diff --git a/docs/modules/opentake-render/INDEX.md b/docs/modules/opentake-render/INDEX.md index d695db3e..4e5cf1e5 100644 --- a/docs/modules/opentake-render/INDEX.md +++ b/docs/modules/opentake-render/INDEX.md @@ -1,5 +1,8 @@ # opentake-render — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `opentake-render` = RenderPlan(纯函数 `Timeline → 每帧属性`)+ wgpu 帧合成器 + 文本栅格化。**预览与导出共用同一条 RenderPlan + 同一个合成器,保证像素一致**。依赖只向下:仅依赖 `opentake-domain`,被 `opentake-core` / `src-tauri` 的预览与导出后端调用。 diff --git a/docs/modules/opentake-render/OVERVIEW.md b/docs/modules/opentake-render/OVERVIEW.md index 1718408d..56977f05 100644 --- a/docs/modules/opentake-render/OVERVIEW.md +++ b/docs/modules/opentake-render/OVERVIEW.md @@ -1,5 +1,10 @@ # opentake-render 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`crates/opentake-render/src/plan/`、`gpu/`、`src-tauri/src/export.rs`。 +> RenderPlan、源帧时间映射、透明合成与导出协作已经实现;temporal 播放调度由桌面壳/预览路由负责。共享合成语义不等于所有预览/导出组合已通过像素验收。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) · 本模块目录:[INDEX.md](INDEX.md) · 完整规格:[SPEC.md](SPEC.md) ## 一句话定位 @@ -98,7 +103,7 @@ Compositor::render_to_rgba(device, queue, size, &frame_plan, resolver) -> Decode **计划中(仅 SPEC / ROADMAP / GAP 规划,本 crate 代码尚未落地或仅占位):** - **线性光混合**:当前合成在 **sRGB 非线性域**直接混合以最贴近 AVFoundation(`RT_FORMAT = Rgba8Unorm`,`color.rs` 的 sRGB↔linear 已备但合成 over 未切线性);线性光(RGBA16F)为质量增强项,仅在像素 diff 通过后切换(SPEC §3.7)。 - **转场 transitions**(相邻 clip 重叠区 pass):ADVANCED-FEATURES A 层 p0;Beta 1 已交付 cross-dissolve 全链(含时间线标记 / Inspector 选型),扩库为后续版本项。 -- **透明 alpha / PNG sequence / 任意 TSX 动效**:Motion Canvas 透明输出与 frame-sequence 明确属于后续版本(v1 走普通视频导入/预览/导出链路)。 +- **通用 PNG sequence / 任意 TSX 动效**:仍属后续范围;透明 Motion 与 ProRes 4444 已通过普通媒体/编码路径进入候选源码。 > **2026-08-03 更新:**「运行期预览接线」「真实播放引擎」「图片/Lottie 物化」三项 > 已随 Beta 1 落地(`composite_frame`/播放引擎在 `src-tauri/playback` 与 `web`,像素仍走本 crate diff --git a/docs/modules/opentake-render/SPEC.md b/docs/modules/opentake-render/SPEC.md index 87d3b658..4ff06a85 100644 --- a/docs/modules/opentake-render/SPEC.md +++ b/docs/modules/opentake-render/SPEC.md @@ -1,5 +1,8 @@ # opentake-render 实现就绪规格(Issue #7:wgpu 帧合成器 + RenderPlan) +> 状态:draft · 阶段:partial-implementation · 设计与早期实现来源保留;原文日期、行号和“待做”属于设计时点。2026-09-06 当前实现见本模块 [OVERVIEW.md](OVERVIEW.md),当前验收见[公开 Beta 记录](../../audit/2026-09-06/public-beta-validation.md)。 + + > 状态:实现就绪(implementation-ready)。本文是 `crates/opentake-render/` 的逐项施工图。 > 范围:① 纯函数 `Timeline → RenderPlan`(Rust 数据结构 + 算法,逐条对应上游公式);② wgpu render graph;③ 预览/导出共享 RenderPlan;④ 图片/文字/Lottie 物化为纹理;⑤ 与 `opentake-domain` / `opentake-media` 的接口契约;⑥ PoC 验收(与上游 `inspect_timeline` 像素 diff)+ 分步实施清单。 > 定位:这是**全项目命门**(ARCHITECTURE.md §1、ROADMAP Phase 3)。上游所有像素级合成都委托给 AVFoundation 黑盒(`AVVideoComposition` + layer instructions + ramps),无 Metal/CoreImage/手写 shader;OpenTake 必须自建 wgpu 合成器把这块从零补回。 @@ -19,7 +22,7 @@ | 文字渲染(预览+导出) | `…/Preview/TextLayerController.swift`(224 行) | `applyStyle` L152、`isGeometryFlipped` L13、`applyOpacityAnimation` discrete L191、`buildForExport` L75、`visibleTextClips` L122、`referenceCanvasHeight=1080` L150 | | 文字样式/测量 | `…/Models/TextStyle.swift`、`…/Models/TextLayout.swift` | `attributes` L138、`naturalSize` L9、shadowPadding=12 L6 | | 领域模型(已 1:1 移植到 Rust) | `…/Models/Timeline.swift`、`…/Models/Keyframe.swift`、`…/Models/ClipType.swift` | `Transform` L364、`Crop` L501、`affineTransform` 输入语义见下 | -| 架构/路线 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/ARCHITECTURE.md` §1/§6 | `…/docs/_analysis/02-苹果框架可移植性.md`、`…/docs/ROADMAP.md` Phase 3 L25-34 | +| 架构/路线 | `/Users/lvbaiqing/TRUE 开发/PRIMARY-CN/OpenTake/docs/architecture/ARCHITECTURE.md` §1/§6 | `…/docs/_analysis/02-苹果框架可移植性.md`、`…/docs/architecture/ROADMAP.md` Phase 3 L25-34 | **已就位的 Rust 依赖(本 crate 不得重写,只能调用):** `crates/opentake-domain/` 已逐行移植并单测覆盖: - `Clip::transform_at / size_at / top_left_at / rotation_at`(`clip.rs` L214-246) @@ -83,7 +86,7 @@ return placed ∘ translate(-cx,-cy) ∘ rotate(rotation*π/180) ∘ translate(c ### 1.5 多轨混合(对应 instruction.layerInstructions 顺序 L405-449) - `layerInstructions` 顺序 = `trackMappings.filter(\.isVideo)` 顺序 = **轨道枚举顺序 + 黑底最后追加**(L194/L206)。AVFoundation 按 layer instruction 数组顺序混合,**后者在上**。 -- 因此 OpenTake 混合顺序:**黑底(最底)→ track[0] → track[1] → … → track[n-1](最顶)**。注意上游黑底 mapping 是最后 append 到 trackMappings 的(L209-215),但它的 opacity 在自身区间是 1、其余轨道盖在它上面——等价于"黑底铺底,视频轨从下标 0 到 n-1 依次叠加"。OpenTake 直接按此顺序 alpha-over 合成(见 §3.6)。 +- 因此 OpenTake 混合顺序:**黑底(最底)→ track[0] → track[1] → … → `track[n-1]`(最顶)**。注意上游黑底 mapping 是最后 append 到 trackMappings 的(L209-215),但它的 opacity 在自身区间是 1、其余轨道盖在它上面——等价于"黑底铺底,视频轨从下标 0 到 n-1 依次叠加"。OpenTake 直接按此顺序 alpha-over 合成(见 §3.6)。 - `track.hidden` ⇒ 整轨不渲染(L419);`track.muted` ⇒ 音频不出声(audio 路径 L391,本 crate 视频侧只需关心 hidden)。 ### 1.6 色彩空间(对应 vcConfig L456-458 + ImageVideoGenerator L169-173) diff --git a/docs/modules/src-tauri/INDEX.md b/docs/modules/src-tauri/INDEX.md index 1fb35f2c..318d0f02 100644 --- a/docs/modules/src-tauri/INDEX.md +++ b/docs/modules/src-tauri/INDEX.md @@ -1,5 +1,8 @@ # src-tauri — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > **Tauri 2 桌面壳 + 命令边界层**:装配所有 crate 成原生进程,持有权威 `AppCore`,对前端暴露薄 `#[tauri::command]` 接口,并把 core 事件桥回 WebView。它是 workspace member,但不在 `crates/` 下。 diff --git a/docs/modules/src-tauri/OVERVIEW.md b/docs/modules/src-tauri/OVERVIEW.md index fb1a36d1..435bce48 100644 --- a/docs/modules/src-tauri/OVERVIEW.md +++ b/docs/modules/src-tauri/OVERVIEW.md @@ -1,5 +1,10 @@ # src-tauri — 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`src-tauri/src/export.rs`、`motion.rs`、`external_mcp.rs`、`lib.rs`。 +> 导出支持 H.264、H.265、ProRes 422/4444,进度/取消与失败清理有实现。透明 Motion、Lottie 原生播放、外部 MCP 配对已接入;本次安装包与 GUI 验收另见当日审计。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[本模块目录](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) ## 一句话定位 @@ -116,15 +121,15 @@ pub enum EditRequest { … } | 子系统 | 状态 | 说明 | |---|---|---| -| 命令边界 / `EditRequest` 映射 | ✅ 已实现 | 30 个命令注册;`EditRequest` 覆盖前端 v1 全部编辑变体,带回归测试 | +| 命令边界 / `EditRequest` 映射 | ✅ 已实现 | 命令集中注册;`EditRequest` 覆盖前端 v1 全部编辑变体,带回归测试 | | 事件桥 | ✅ 已实现 | 4 类 CoreEvent 全部转发 | | 启动 / 窗口 / FFmpeg 解析 | ✅ 已实现 | 关窗隐藏 + 关窗前 flush 存盘;`RunEvent::Reopen` **仅 macOS** | | 单帧预览合成 `composite_frame` | ✅ 已实现 | 视频 + 图片 + 文本 + Lottie 层;预览与导出共享 RenderPlan 像素路径 | -| 整片导出 `export_video` | ✅ 已实现 | **H.264 / .mp4、H.265 / .mp4、ProRes 422 / .mov** + 线性音频混音;带进度回调 / 取消(`cancel_export`) | +| 整片导出 `export_video` | ✅ 已实现 | **H.264 / .mp4、H.265 / .mp4、ProRes 422 / 4444 / .mov** + 线性音频混音;带进度回调 / 取消(`cancel_export`) | | 媒体导入 / relink / 波形 / 缩略图 | ✅ 已实现 | 导入白名单 + 首帧海报缩略图 + 波形 + 缺失素材重链接 | | 全局素材库(7 命令) | ✅ 已实现 | copy-on-favorite,跨工程 | | 密钥(BYOK) | ✅ 已实现 | keyring 存储 + 白名单账户(含生成侧 provider) | -| MCP / Agent 通道 | ✅ 已实现 | 官方 Codex/ChatGPT 逐轮临时 loopback MCP(256-bit Bearer、工程绑定);固定端口外部 MCP 在 Beta 2 默认关闭 | +| MCP / Agent 通道 | ✅ 已实现 | 官方 Codex/ChatGPT 逐轮临时 loopback MCP(256-bit Bearer、工程绑定);外部 MCP 从 Beta 5 起采用显式认证配对,旧未认证入口仍关闭 | | 跨平台窗口重显 | 🟡 部分 | `RunEvent::Reopen` 仅 macOS;其它平台靠托盘 / OS 重现是后续项 | | FFmpeg 随包分发 | ✅ 已实现 | sidecar 随包打包(macOS/Windows);Windows CI 校验 exact-SHA | diff --git a/docs/modules/src-tauri/export.md b/docs/modules/src-tauri/export.md index 3b8e667d..dda90f99 100644 --- a/docs/modules/src-tauri/export.md +++ b/docs/modules/src-tauri/export.md @@ -1,87 +1,27 @@ -# export — 整条时间线视频导出 +# export — 时间线视频导出 -> 上级:[本模块目录](INDEX.md) · [总览](OVERVIEW.md) · [模块文档树](../INDEX.md) -> -> 源码:[`../../../src-tauri/src/export.rs`](../../../src-tauri/src/export.rs) +> 状态:draft · 阶段:implementation-backed · 同步日期:2026-09-06 +> [模块总览](OVERVIEW.md) · [模块目录](INDEX.md) · [源码 export.rs](../../../src-tauri/src/export.rs) -## 定位 +`export_video` / `run_export` 将时间线采样成 RenderPlan,通过合成器生成 RGBA,再由 media 编码层写入目标容器。格式和扩展名在 `resolve_preset` 等入口校验。 -`export_video` 命令:把当前时间线的**每一帧**在 GPU 上合成(wgpu 合成器 `opentake-render`),把 RGBA 帧喂给系统 ffmpeg 编码器(`opentake_media::VideoEncoder`),产出磁盘上真实的 `.mp4`。它是单帧预览路径 [render.md](render.md) 的「整片」对应物。 - -## 完成状态(首版切片,SPEC §2.4 / §8.2) - -| 维度 | 状态 | +| 路径 | 当前源码行为 | |---|---| -| 视频编码 | 🟡 **仅 H.264 / .mp4**。编码器本身已支持 H.265 / ProRes preset,但本命令未接线 | -| 音频 | ✅ **线性混音**:每个含音频 clip 的源窗解码成 mono f32 @ 混音采样率,按帧推导的样本偏移落位,乘 `volume_at` 包络,求和、硬限幅,由编码器 mux 入(AAC) | -| 分辨率 | ✅ 全量导出分辨率(`export_render_size`),非预览降采样上限 | -| 进度 / 取消 | ❌ 未实现——编排器在 GPU 锁下逐帧跑到完 | -| 文本层 | ✅ 支持(`CosmicTextRasterizer`) | -| Lottie 层 | ❌ resolver 返回 `None`(跳过) | - -H.265 / ProRes 在 `resolve_preset` 里**显式报错**(`"H.265 export is not wired yet (TODO)"` / ProRes 同理),而非默默失败。 - -## IPC 入参(ExportRequest) - -```rust -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExportRequest { - pub out_path: String, // 必须 .mp4(H.264 路径) - #[serde(default)] pub codec: ExportCodec, // 默认 H264 - #[serde(default)] pub quality: ExportQuality, // 默认 1080p -} -``` - -- `ExportCodec`(`rename_all = "lowercase"`):`h264`(默认)/ `h265`(留位)/ `prores`(留位)。 -- `ExportQuality`:`720p` / `1080p`(默认)/ `4k`——每档同时映射到 render-crate 分辨率与 encode-crate 分辨率两个选择器。 -- `#[serde(default)]` 让 bare 载荷 `{ "outPath": "…" }` 即导出 H.264 / 1080p。 - -返回 `ExportSummary { outPath, width, height, fps, frameCount }`(camelCase)。 - -## 编排流程(run_export) - -`export_video` 命令只做「快照 live 会话 → 委派」,真正逻辑在 `run_export()`(与 Tauri / `AppCore` 解耦,便于 ffmpeg-gated 集成测试 `tests/export_integration.rs` 直接用手搭的 timeline + manifest 驱动): - -``` -resolve_preset(codec, quality, out) // 校验扩展名匹配容器;拒未接线 codec -project_text(timeline) // 文本 clip → {content, style, box} 按 clip id -project_media(manifest, project_dir) // manifest → (sizes, media 路径);解析 Project 相对路径 -export_render_size((w,h), quality) // 全量导出尺寸(偶数化) -build_render_plan(timeline, size, metrics) -RenderDevice::try_new() // 本地一次性 GPU 上下文(不复用预览的缓存上下文) -VideoEncoder::new(out, w, h, fps, preset) -for f in 0..total_frames { - plan.frame(timeline, f) → MediaResolver(每帧新建,cache cap=64) - compositor.render_to_rgba(...) → encoder.push_frame(RgbaFrame) -} -mix_timeline_audio(timeline, media) → encoder.push_audio(pcm) // 无音频则视频-only -encoder.finish() -``` - -要点: -- **GPU 上下文本地一次性**:导出是一次性批处理,不复用预览缓存的上下文,避免与预览锁竞争。 -- 空时间线仍产出合法(可能零帧)文件;越界帧合成为不透明黑(正确的 clear color,非错误)。 -- GPU 获取 / 解码 / 编码失败均转 `Err(String)`(Tauri 边界约定)。 - -## 纹理解析(MediaResolver,导出版) - -`TextureResolver` 实现:video 按源帧 key、image 一次 key、text 栅格化其 box、Lottie 返回 `None`。与预览版相比,导出版 `FrameRequest.tolerance_secs = 0.0`(精确落帧,质量优先;预览用 0.1s 宽容差换 scrub 速度)。这份 resolver / metrics / 投影逻辑是预览路径逻辑的**自包含拷贝**(有意留在本模块,不动 `render.rs`);待两条路径稳定后再把共享投影上提为 `pub(crate)` 辅助。 +| H.264 / H.265 | `.mp4`,带时间线音频混音 | +| ProRes 422 | `.mov`,不承载透明输出 | +| ProRes 4444 | `.mov`,承载 alpha;输入直 alpha 与合成预乘边界显式处理 | +| 文本与 Lottie | 经渲染/源解析器进入帧合成,不再是早期跳过 Lottie 的切片 | +| 进度与取消 | `ExportControl` 绑定导出代次;`cancel_export` 请求取消,逐帧与收尾继续校验 | +| 失败与输出身份 | 失败/取消清理不完整结果,拒绝不安全或冲突的输出身份;不会静默交付失败的无音轨输出 | -## 音频混音(mix_timeline_audio) +`ExportRequest`、`ExportCodec`、`ExportQuality` 与前端[导出面板](../../../web/src/components/shell/ExportDialog.tsx)共同定义选项。参数字段以类型定义为准,文档不复制易过时的完整 DTO。 -- 解码规格 `AUDIO_DECODE_SPEC`:mono / f32 / `MIX_SAMPLE_RATE`——在混音率上解码,使混音成为样本对齐的纯加法(本切片不做逐 clip 重采样)。 -- 仅 `Audio` / `Video` 类型 clip 贡献声音(text/image/lottie 无声);**muted 轨被跳过**。 -- 每个 clip 经 `project_clip_audio`:解码可见源窗 → 落到帧推导的起始样本 → 按 `volume_at` 逐样本建增益包络(全 unity 则塌缩为空包络)。 -- clip 指向无音轨的视频 → `MediaError::NoTrack` 被吞为「贡献静音」,**不是导出失败**;其它解码错误才上抛。 -- 全部 clip 无音频 → 返回 `None` → 保持视频-only 输出。 +导出尺寸采用目标导出配置,区别于低分辨率预览。时间重映射、音频窗口、透明输出和缺失素材均有专门代码路径;这些路径的存在不代表所有组合和平台已经实机验收。 -## 与 `export_fcpxml` 的区别 +`aae0ae6..33ee8e2` 中的候选新增包括 ProRes 4444、失败清理、取消传播和输出身份检查。[本次同步报告](../../documentation-sync-2026-09-06.md)记录源码依据,[当前验证](../../audit/2026-09-06/public-beta-validation.md)记录实际运行结果。早期 H.264-only、无进度/取消的描述已被当前实现替代。 -`export_video`(本文件)产出**像素级渲染的视频文件**;`export_fcpxml`([commands-ipc.md](commands-ipc.md))产出 **XMEML 工程交换 XML**(给 Premiere/DaVinci/FCP)。两者无关。 +关联:[媒体编码](../opentake-media/encode.md) · [RenderPlan](../opentake-render/render-plan.md) · [单帧渲染](render.md)。 ---- +## 2026-09-07 调度修复 -> 相关:[render.md](render.md)(共享逻辑的单帧版)· [commands-ipc.md](commands-ipc.md)(`export_fcpxml` 对比)· 跨模块 [opentake-render](../opentake-render/INDEX.md)(合成器 / RenderPlan)· [opentake-media](../opentake-media/INDEX.md)(VideoEncoder / PCM) -> -> 导航:[本模块目录](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) +`export_video` 通过async命令调用专用blocking task。调用时先获取单导出lease和项目snapshot,worker持有共享ExportControl及owned ExportGuard,直到真正结束;取消、进度事件和WebView主循环可以并行响应。GUI首包已暴露原同步命令占住主线程的问题,本次调度改动的原生进度/取消验收见[当前验证记录](../../audit/2026-09-06/public-beta-validation.md)。 diff --git a/docs/modules/web/INDEX.md b/docs/modules/web/INDEX.md index f349b2cb..8e6c521f 100644 --- a/docs/modules/web/INDEX.md +++ b/docs/modules/web/INDEX.md @@ -1,5 +1,8 @@ # web — 模块目录 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 当前模块状态见 [OVERVIEW.md](OVERVIEW.md),候选验收见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。规格文档保留设计语义,不作为全功能完成证明。 + > 上级:[模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > `web/` = OpenTake 的 **React/TypeScript + Vite + Zustand** 前端(包管理器 pnpm,测试 vitest)。它是架构最上层的**纯消费者**:只持后端 `Timeline` 只读镜像 + 版本号,**不做撤销、不持领域逻辑**;编辑经 `edit_apply` 发往 Rust,由 `timeline_changed` 事件回流刷新。像素↔帧换算放前端,帧↔秒换算放 Rust。非 Tauri 下 `isTauri=false`,命令落内存 fallback。 @@ -10,13 +13,15 @@ - **[OVERVIEW.md](OVERVIEW.md)** — 一句话定位与架构位置、职责边界(做/不做)、关键概念与数据流(手势→editActions→`editApply`→`edit_apply`→`timeline_changed`→`get_timeline`;camelCase 契约;单表面单时钟预览;非 Tauri 降级)、完成状态、代码风格。 +当前新增:[Sticker 专项验证](../../audit/2026-09-06/sticker-panel.md)(图片/Lottie 既有媒体路径已接线,新包 GUI 待验收)。 + ## 子系统文档 - **[state-stores.md](state-stores.md)** — `store/`:Zustand 各 store + actions + 镜像同步。`projectStore`(只读镜像 + 版本 + canUndo/canRedo,**无撤销栈**)、`uiStore`/`settingsStore`/`clipboardStore`/`recentStore`(纯 UI 态)、`mediaStore`/`libraryStore`(后端镜像)、`editActions`(手势→`EditRequest`,删除健壮化、媒体落轨串行化、复制/剪切/粘贴)、`mediaActions`/`projectActions`(对话框驱动)、`sync.ts`(镜像更新唯一入口 + `forceRefresh`)。 - **[ipc-api.md](ipc-api.md)** — `lib/` 对接面:`api.ts`(IPC + `isTauri`,`editApply`/`getTimeline`/`getWaveform` try/catch/`compositeFrame`/`secret_*` 事件)、`types.ts`(领域镜像 + `EditRequest` 全变体 + **camelCase 对齐铁律**)、`asset.ts`(`convertFileSrc` 资产协议)、`libraryApi.ts`(全局库通道)、`dialog.ts`(对话框懒加载)、`fallback.ts`(浏览器内存 demo 子集)。 - **[timeline-ui.md](timeline-ui.md)** — `components/timeline/` + `lib/geometry.ts`/`snap.ts`/`ruler.ts`/`zones.ts`/`clip.ts`:像素↔帧(前端、截断)、Canvas 绘制(`timelineCanvas`/`clipRenderer`/`rulerCanvas`)、吸附/多探针、命中测试、刮擦/缩放/平移/移动/修剪/切割与触控板手势、轨道头、右键菜单、媒体交换。 - **[preview-ui.md](preview-ui.md)** — `components/preview/`:单表面 + 单时钟模型,`previewEngine`(rAF 三态 PLAY/SCRUB/PAUSE)、`timelinePlayback`(纯逻辑)、`TimelinePlaybackLayer`(被动 DOM 注册)、`previewLayerStyles`(样式采样)、`Preview`(单素材/合成两模式 + 运输控制)。 -- **[panels-ui.md](panels-ui.md)** — `components/` 其余:inspector(检查器 + 关键帧面板 + 可拖拽数值 + 文本)、media(媒体面板 + 全局库页 + 星标)、toolbar、home(启动器)、settings(含 BYOK keychain)、agent(占位)、shell(五面板布局 + 分割条 + 标题栏)、ui(lucide `Icon` / `HoverButton` / `Dropdown` / `PanelShell`)。 +- **[panels-ui.md](panels-ui.md)** — `components/` 其余:inspector(检查器 + 关键帧面板 + 可拖拽数值 + 文本)、media(媒体面板 + 全局库页 + 星标)、toolbar、home(启动器)、settings(含 BYOK keychain)、agent(有序对话与工具结果)、shell(五面板布局 + 分割条 + 标题栏)、ui(lucide `Icon` / `HoverButton` / `Dropdown` / `PanelShell`)。 - **[hooks-i18n-theme.md](hooks-i18n-theme.md)** — `hooks/`(`useAutosave` 防抖保存、`useKeyboardShortcuts` 快捷键)+ `i18n/`(zh-CN 默认 / en)+ `lib/theme.ts`(`AppTheme` 数值常量单一源)+ `styles/`(`tokens.css` CSS 变量、`global.css` 全局基础)。 ## 规格 @@ -84,7 +89,7 @@ web/src/ │ ├── toolbar/ 顶部工具栏 │ ├── home/ 启动器 │ ├── settings/ 设置(含 BYOK) -│ ├── agent/ Agent 面板(占位) +│ ├── agent/ Agent 面板(有序对话与工具结果) │ ├── shell/ 五面板布局 + 分割条 + 标题栏 + 视图菜单 │ └── ui/ 通用原始件(lucide Icon / HoverButton / Dropdown / PanelShell) ├── hooks/ useAutosave / useKeyboardShortcuts diff --git a/docs/modules/web/OVERVIEW.md b/docs/modules/web/OVERVIEW.md index ccd0e0fe..f82f6f74 100644 --- a/docs/modules/web/OVERVIEW.md +++ b/docs/modules/web/OVERVIEW.md @@ -1,5 +1,10 @@ # web — 总览 +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> 本次定向来源:`web/src/components/media/MediaPanel.tsx`、`MediaTabBar.tsx`、`components/preview/playbackRoute.ts`、`store/uiStore.ts`。 +> Text/Effect/Sticker 已接线;Sticker 使用项目图片/Lottie 与既有导入、预览、落轨流程。Transition/Captions/Smart Pack 有对应面板。多预览 tab、媒体三种视图和 temporal Rust compositor 已接入。当前测试结果统一引用当日审计,旧测试数仅为历史。 +> 当前验收见[公开 Beta 审计](../../audit/2026-09-06/public-beta-validation.md);下文历史里程碑和测试记录保留其原时点边界。 + > 上级:[本模块目录](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) ## 一句话定位 @@ -64,10 +69,10 @@ UI 手势 / 快捷键 / 拖放 - 检查器(Video/Audio/Text、现场采样、关键帧面板与钻石键、可拖拽数值、Source 检查器、截帧入库)。 - 媒体面板(导入/拖放/双击/星标/萃取音频/Relink/搜索/文件夹)、全局库页、工具栏、主页启动器、设置(BYOK keychain、Codex/ChatGPT 官方登录、外观、导入目录)。 - 复制/剪切/粘贴、删除健壮化、自动保存、双语(zh-CN/en)、AppTheme 全令牌、可访问性(focus trap、24px 命中、reduced-motion、760×480 布局)。 -- 全量回归:129 个测试文件 / 1072 项测试通过(Vitest),`pnpm build` 通过。 +- 历史 Beta 1/2 回归记录为 129 文件 / 1072 测试;本候选结果见当日审计,不沿用旧计数。 **计划中 / 后续版本:** -- 媒体面板部分标签(Text/Sticker/Effect/Transition/Captions/Smart Wrap)在剪映式顶栏中为置灰占位,接相应功能面板后逐个点亮。 +- Sticker 已实现项目图片/Lottie 展示、导入、预览、拖拽/落轨及工程身份隔离;[定向测试](../../audit/2026-09-06/sticker-panel.md)通过,当前新包 GUI 验收仍待执行。 - 高负载流式预览引擎进一步优化、库→时间线拖拽接线(#37 尾巴)。 - 与上游逐项 1:1 截图/行为/几何对拍验收(见 [SPEC.md](SPEC.md))。 diff --git a/docs/modules/web/SPEC.md b/docs/modules/web/SPEC.md index 75d25e6c..9a75381b 100644 --- a/docs/modules/web/SPEC.md +++ b/docs/modules/web/SPEC.md @@ -1,5 +1,8 @@ # 前端 React UI —— 1:1 复刻上游 实现就绪规格 (Issue #12) +> 状态:draft · 阶段:partial-implementation · 设计与早期实现来源保留;原文日期、行号和“待做”属于设计时点。2026-09-06 当前实现见本模块 [OVERVIEW.md](OVERVIEW.md),当前验收见[公开 Beta 记录](../../audit/2026-09-06/public-beta-validation.md)。 + + > **状态**:v1 硬要求规格。**已有功能的 UI 与交互必须 1:1 复刻上游 palmier-pro。** > **范围**:`web/`(React + TypeScript + Vite + Zustand)的全部可视层与交互层。**不含** Rust core / Tauri command 的内部实现(只定义对接契约)。 > **证据基准**:全部数值/行为均引自 `palmier-pro-upstream/Sources/PalmierPro/`,并标注 `文件:行号`。凡本规格与上游源码冲突,**以上游源码为准**。 @@ -1360,4 +1363,4 @@ Tauri envelope;Rust `every_frontend_edit_request_deserializes_to_intended_comm | Preview | `Preview/PreviewContainerView.swift` + `PreviewTab.swift` + overlays | | 数据模型 | `Models/Timeline.swift` / `Keyframe.swift` / `ClipType.swift` | | 小组件 | `UI/HoverHighlight.swift` / `CapsuleButton.swift` | -| 目标架构(Tauri/React/Zustand/命令-事件) | `OpenTake/docs/ARCHITECTURE.md` | +| 目标架构(Tauri/React/Zustand/命令-事件) | `OpenTake/docs/architecture/ARCHITECTURE.md` | diff --git a/docs/modules/web/panels-ui.md b/docs/modules/web/panels-ui.md index 190e60d5..a2b1b169 100644 --- a/docs/modules/web/panels-ui.md +++ b/docs/modules/web/panels-ui.md @@ -22,15 +22,21 @@ ## inspector(检查器) -- `Inspector.tsx`:属性检查器主体(SPEC §6)。四态:多选摘要 / 单选 clip 检查(Video·Audio·Text 标签)/ 无选时显工程元数据 / 媒体资产检查(占位)。**现场采样**:每次 render 从 `activeFrame` 取 clip 的动画值,故数值字段总显示播放头处当前值;已有关键帧轨的属性显示为只读「(animated)」并把编辑转到关键帧面板。所有编辑经 `editActions.setClipProperties()`。 +- `Inspector.tsx`:属性检查器主体(SPEC §6)。四态:多选摘要 / 单选 clip 检查(Video·Audio·Text 标签)/ 无选时显工程元数据 / 媒体资产 Source 检查器。**现场采样**:每次 render 从 `activeFrame` 取 clip 的动画值,故数值字段总显示播放头处当前值;已有关键帧轨的属性显示为只读「(animated)」并把编辑转到关键帧面板。所有编辑经 `editActions.setClipProperties()`。 - `KeyframesPanel.tsx` + `KeyframesLaneRow.tsx`:关键帧面板(SPEC §6.4)。单选 clip 下每个可动画属性一行(视频 position/scale/rotation/opacity/crop;音频 volume),顶部刻度尺 + 面板级红色播放头叠加,行内可拖拽菱形标记(→ stamp/move/remove/insertation 关键帧动作)。 - `ScrubbableNumberField.tsx`:可拖拽数值控件(SPEC §6.6)。水平拖拽改值(Shift×10 / Cmd×0.1),单击切文本输入(Enter/失焦提交、ESC 取消),动画属性时只读;`onCommit` 触发命令。 -- `TextTab.tsx`:编辑 `Clip.textContent`(草稿本地态,失焦提交;字号/颜色/对齐等样式延后)。 +- `TextTab.tsx`:编辑 `Clip.textContent`及 TextStyle(字体、字号、颜色、对齐等),经命令提交。 - `SwapMediaSection.tsx`:检查器内的「替换素材」入口(→ `swapMedia`)。 +## 2026-09-06 媒体与预览增量 + +`MediaPanel.tsx` 的 TextTab 调用 `addTextClip`;EffectTab 仅对单个选中视觉片段追加预设并保留既有 effects,链接音轨不作为额外视觉选中项。Sticker 已从 `33ee8e2` 的禁用占位接为可用主标签,筛选当前项目(含子目录)的 image/Lottie,复用 MediaCard 与导入/落轨事务;空态、处理中、错误和工程身份隔离有对应处理。转场、字幕与 Smart Pack 均有独立内容组件。媒体视图支持 folder/flat/grouped,预览区支持多个素材 tab 与旧存储状态归一。 + +本段记录源码接线;[当日审计](../../audit/2026-09-06/public-beta-validation.md)记录 GUI 与自动化结果。 + ## media(媒体面板与全局库) -- `MediaPanel.tsx`:媒体库容器。顶部主标签(Material/Audio/Text/Sticker/Effect/Transition/Captions/Smart Wrap,仅 Material·Audio 可用、余者置灰占位,仿剪映);二级标签 Import/Mine(Mine=星标收藏,localStorage)。过滤管线不可变(audio 标签仅显纯音频,Mine 仅显收藏)。卡片 HTML5-draggable(`MEDIA_DND_TYPE`),单击预览、双击 `addMediaToTimeline`、星标切换、视频可「萃取音频」(`extractAudio` + 保存对话框);离线素材红覆盖 + Relink。订阅 `uiStore.mediaTab/mediaSubTab`,消费 `mediaStore`。 +- `MediaPanel.tsx`:媒体库容器。顶部主标签(Material/Audio/Music/Text/Sticker/Effect/Transition/Captions/Smart Pack;全部已有对应内容入口);二级标签 Import/Mine(Mine=星标收藏,localStorage)。过滤管线不可变(audio 标签仅显纯音频,Mine 仅显收藏)。卡片 HTML5-draggable(`MEDIA_DND_TYPE`),单击预览、双击 `addMediaToTimeline`、星标切换、视频可「萃取音频」(`extractAudio` + 保存对话框);离线素材红覆盖 + Relink。订阅 `uiStore.mediaTab/mediaSubTab`,消费 `mediaStore`。 - `LibraryView.tsx`:全局库整页视图(`view === "library"`,跨项目永久库),消费 `libraryStore`,支持分类/搜索/排序与「导入到项目」。 - `MediaTabBar.tsx`:主/次标签按钮组。 - `favorites.ts`:星标收藏 store(localStorage)。 @@ -43,7 +49,7 @@ - `home/HomeView.tsx`:启动器。最近项目列表 / 空态 + 新建/打开/设置入口。消费 `recentStore`,调 `newProjectAndEnter`/`openProjectViaDialog`/`openProjectPath`。 - `settings/SettingsView.tsx`:模态设置。标签式:General(语言) / Appearance(主题) / Import(默认目录) / AI(BYOK 密钥) / About(版本·许可)。**BYOK 明文密钥存 OS keychain**(`secret_*`),不进 localStorage。消费 `settingsStore` 与 i18n。 -- `agent/AgentPanel.tsx`:内置 Agent 聊天面板(当前占位,SPEC §2.1,后续接通)。 +- `agent/AgentPanel.tsx`:内置 Agent 聊天面板(有序文本/tool use/tool result、历史恢复与 provider 对话接线)。 ## ui(通用原始件) @@ -57,7 +63,7 @@ ## 完成状态 - **已实现**:三种布局 + 最大化、检查器(Video/Audio/Text + 现场采样 + 关键帧面板 + 可拖拽数值)、媒体面板(导入/拖放/双击/星标/萃取音频/Relink)、全局库页、工具栏、主页启动器、设置(含 BYOK keychain)、lucide 图标体系。 -- **计划中/占位**:Agent 面板真实对话;媒体面板的 Text/Sticker/Effect/Transition/Captions/Smart Wrap 标签为置灰占位;检查器文本样式(字号/颜色/对齐)与媒体资产检查待补。 +- **待原生验收**:Sticker 主标签及图片/Lottie 导入、预览、拖拽/落轨已经实现,[专项记录](../../audit/2026-09-06/sticker-panel.md)包含定向测试与构建;其他主标签、文本样式与 Source 检查器亦已有实现,新包原生交互按候选验收。 ## 相关文档 diff --git a/docs/modules/web/preview-ui.md b/docs/modules/web/preview-ui.md index 224df2ae..26a3a86e 100644 --- a/docs/modules/web/preview-ui.md +++ b/docs/modules/web/preview-ui.md @@ -1,5 +1,8 @@ # preview-ui — 预览与播放(components/preview) +> 状态:draft · 阶段:implementation-backed · 源码同步:2026-09-06。 +> Preview 已支持多个素材 tab、激活历史、关闭/恢复与旧状态归一(`uiStore.ts`);temporal compositor 支持合成时间线倒放/变速,Lottie 走 Rust 原生路由。下文早期单素材描述表示显示模式,不是只能打开一个素材 tab。最新路由见[播放架构](../../architecture/PLAYBACK-ENGINE.md),测试/GUI 边界见[当日审计](../../audit/2026-09-06/public-beta-validation.md)。 + > 上级:[本模块目录](INDEX.md) · [模块文档树](../INDEX.md) · [docs 总目录](../../INDEX.md) > > 覆盖 `web/src/components/preview/`。预览面板按**单表面 + 单时钟**模型工作:播放/暂停/拖拽共用同一组 `