improved tools for ai
This commit is contained in:
BIN
.diagnostics/diag_20260415_163239.tar.gz
Normal file
BIN
.diagnostics/diag_20260415_163239.tar.gz
Normal file
Binary file not shown.
16
.diagnostics/diag_20260415_163239/backend_health_check.txt
Normal file
16
.diagnostics/diag_20260415_163239/backend_health_check.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# backend_health_check
|
||||||
|
# cmd: /home/dillon/_code/TalkEdit/.venv312/bin/python3.12 -c import importlib; importlib.import_module('backend.main'); print('backend import OK')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<string>", line 1, in <module>
|
||||||
|
File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
|
||||||
|
return _bootstrap._gcd_import(name[level:], package, level)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
|
||||||
|
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
|
||||||
|
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
|
||||||
|
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
|
||||||
|
File "<frozen importlib._bootstrap_external>", line 999, in exec_module
|
||||||
|
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
|
||||||
|
File "/home/dillon/_code/TalkEdit/backend/main.py", line 12, in <module>
|
||||||
|
from routers import transcribe, export, ai, captions, audio
|
||||||
|
ModuleNotFoundError: No module named 'routers'
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# backend_python_version
|
||||||
|
# cmd: /home/dillon/_code/TalkEdit/.venv312/bin/python3.12 --version
|
||||||
|
Python 3.12.13
|
||||||
3
.diagnostics/diag_20260415_163239/env_git_head.txt
Normal file
3
.diagnostics/diag_20260415_163239/env_git_head.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# env_git_head
|
||||||
|
# cmd: git -C /home/dillon/_code/TalkEdit rev-parse --short HEAD
|
||||||
|
4f90750
|
||||||
10
.diagnostics/diag_20260415_163239/env_git_status.txt
Normal file
10
.diagnostics/diag_20260415_163239/env_git_status.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# env_git_status
|
||||||
|
# cmd: git -C /home/dillon/_code/TalkEdit status --short
|
||||||
|
M frontend/src/App.tsx
|
||||||
|
M frontend/src/components/VolumePanel.tsx
|
||||||
|
M frontend/src/components/WaveformTimeline.tsx
|
||||||
|
M frontend/src/store/editorStore.ts
|
||||||
|
?? .diagnostics/
|
||||||
|
?? AI_dev.md
|
||||||
|
?? docs/
|
||||||
|
?? scripts/
|
||||||
3
.diagnostics/diag_20260415_163239/env_node_version.txt
Normal file
3
.diagnostics/diag_20260415_163239/env_node_version.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# env_node_version
|
||||||
|
# cmd: node --version
|
||||||
|
v22.18.0
|
||||||
3
.diagnostics/diag_20260415_163239/env_npm_version.txt
Normal file
3
.diagnostics/diag_20260415_163239/env_npm_version.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# env_npm_version
|
||||||
|
# cmd: npm --version
|
||||||
|
10.9.3
|
||||||
3
.diagnostics/diag_20260415_163239/env_uname.txt
Normal file
3
.diagnostics/diag_20260415_163239/env_uname.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# env_uname
|
||||||
|
# cmd: uname -a
|
||||||
|
Linux cachyos-x 6.19.10-1-cachyos #1 SMP PREEMPT_DYNAMIC Wed, 25 Mar 2026 23:30:07 +0000 x86_64 GNU/Linux
|
||||||
11
.diagnostics/diag_20260415_163239/frontend_build.txt
Normal file
11
.diagnostics/diag_20260415_163239/frontend_build.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frontend_build
|
||||||
|
# cmd: bash -lc cd '/home/dillon/_code/TalkEdit/frontend' && npm run -s build
|
||||||
|
vite v6.4.1 building for production...
|
||||||
|
transforming...
|
||||||
|
✓ 1606 modules transformed.
|
||||||
|
rendering chunks...
|
||||||
|
computing gzip size...
|
||||||
|
dist/index.html 1.20 kB │ gzip: 0.57 kB
|
||||||
|
dist/assets/index-gyhcOzhr.css 19.31 kB │ gzip: 4.48 kB
|
||||||
|
dist/assets/index-B5NnH24A.js 354.13 kB │ gzip: 108.13 kB
|
||||||
|
✓ built in 2.43s
|
||||||
3
.diagnostics/diag_20260415_163239/frontend_lint.txt
Normal file
3
.diagnostics/diag_20260415_163239/frontend_lint.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frontend_lint
|
||||||
|
# cmd: bash -lc cd '/home/dillon/_code/TalkEdit/frontend' && npm run -s lint
|
||||||
|
sh: line 1: eslint: command not found
|
||||||
72
.diagnostics/diag_20260415_163239/list_recent_files.txt
Normal file
72
.diagnostics/diag_20260415_163239/list_recent_files.txt
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# list_recent_files
|
||||||
|
# cmd: find /home/dillon/_code/TalkEdit -maxdepth 2 -type f
|
||||||
|
/home/dillon/_code/TalkEdit/.git/description
|
||||||
|
/home/dillon/_code/TalkEdit/.git/packed-refs
|
||||||
|
/home/dillon/_code/TalkEdit/.git/COMMIT_EDITMSG
|
||||||
|
/home/dillon/_code/TalkEdit/.git/FETCH_HEAD
|
||||||
|
/home/dillon/_code/TalkEdit/.git/ORIG_HEAD
|
||||||
|
/home/dillon/_code/TalkEdit/.git/REBASE_HEAD
|
||||||
|
/home/dillon/_code/TalkEdit/.git/HEAD
|
||||||
|
/home/dillon/_code/TalkEdit/.git/config
|
||||||
|
/home/dillon/_code/TalkEdit/.git/index
|
||||||
|
/home/dillon/_code/TalkEdit/backend/requirements.txt
|
||||||
|
/home/dillon/_code/TalkEdit/backend/.python-version
|
||||||
|
/home/dillon/_code/TalkEdit/backend/dev_main.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/video_editor.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/audio_cleaner.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/diarization.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/ai_provider.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/caption_generator.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/background_removal.py
|
||||||
|
/home/dillon/_code/TalkEdit/backend/main.py
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/postcss.config.js
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/tailwind.config.js
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/tsconfig.json
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/vite.config.ts
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/frontend_dev.log
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/index.html
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/package-lock.json
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/package.json
|
||||||
|
/home/dillon/_code/TalkEdit/frontend/tsconfig.tsbuildinfo
|
||||||
|
/home/dillon/_code/TalkEdit/shared/project-schema.json
|
||||||
|
/home/dillon/_code/TalkEdit/node_modules/.package-lock.json
|
||||||
|
/home/dillon/_code/TalkEdit/src-tauri/.gitignore
|
||||||
|
/home/dillon/_code/TalkEdit/src-tauri/Cargo.toml
|
||||||
|
/home/dillon/_code/TalkEdit/src-tauri/build.rs
|
||||||
|
/home/dillon/_code/TalkEdit/src-tauri/tauri_dev.log
|
||||||
|
/home/dillon/_code/TalkEdit/src-tauri/Cargo.lock
|
||||||
|
/home/dillon/_code/TalkEdit/src-tauri/tauri.conf.json
|
||||||
|
/home/dillon/_code/TalkEdit/.dockerignore
|
||||||
|
/home/dillon/_code/TalkEdit/.gitattributes
|
||||||
|
/home/dillon/_code/TalkEdit/FIX-GITHUB-ACTIONS.md
|
||||||
|
/home/dillon/_code/TalkEdit/LICENSE
|
||||||
|
/home/dillon/_code/TalkEdit/M4A-SUPPORT.md
|
||||||
|
/home/dillon/_code/TalkEdit/package-lock.json
|
||||||
|
/home/dillon/_code/TalkEdit/TECH_FEATURES.md
|
||||||
|
/home/dillon/_code/TalkEdit/FFmpeg_COMPLIANCE.md
|
||||||
|
/home/dillon/_code/TalkEdit/transcribe.py
|
||||||
|
/home/dillon/_code/TalkEdit/test_api.py
|
||||||
|
/home/dillon/_code/TalkEdit/.vscode/settings.json
|
||||||
|
/home/dillon/_code/TalkEdit/.venv312/pyvenv.cfg
|
||||||
|
/home/dillon/_code/TalkEdit/webview.log
|
||||||
|
/home/dillon/_code/TalkEdit/.gitmodules
|
||||||
|
/home/dillon/_code/TalkEdit/split_audio.sh
|
||||||
|
/home/dillon/_code/TalkEdit/venv/.gitignore
|
||||||
|
/home/dillon/_code/TalkEdit/venv/pyvenv.cfg
|
||||||
|
/home/dillon/_code/TalkEdit/.gitignore
|
||||||
|
/home/dillon/_code/TalkEdit/FEATURES.md
|
||||||
|
/home/dillon/_code/TalkEdit/README.md
|
||||||
|
/home/dillon/_code/TalkEdit/close
|
||||||
|
/home/dillon/_code/TalkEdit/electron/main.js
|
||||||
|
/home/dillon/_code/TalkEdit/electron/preload.js
|
||||||
|
/home/dillon/_code/TalkEdit/electron/python-bridge.js
|
||||||
|
/home/dillon/_code/TalkEdit/idea summary.md
|
||||||
|
/home/dillon/_code/TalkEdit/open
|
||||||
|
/home/dillon/_code/TalkEdit/package.json
|
||||||
|
/home/dillon/_code/TalkEdit/plan.md
|
||||||
|
/home/dillon/_code/TalkEdit/.github/copilot-instructions.md
|
||||||
|
/home/dillon/_code/TalkEdit/AI_dev.md
|
||||||
|
/home/dillon/_code/TalkEdit/docs/spec-template.md
|
||||||
|
/home/dillon/_code/TalkEdit/docs/ai-policy.md
|
||||||
|
/home/dillon/_code/TalkEdit/scripts/validate-all.sh
|
||||||
|
/home/dillon/_code/TalkEdit/scripts/collect-diagnostics.sh
|
||||||
327
AI_dev_plan.md
Normal file
327
AI_dev_plan.md
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
# AI Dev Roadmap
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document defines how TalkEdit can evolve toward highly autonomous AI-driven implementation and debugging.
|
||||||
|
|
||||||
|
Goal: AI can execute most engineering work end-to-end with minimal human feedback while preserving safety, quality, and product intent.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Frontend: React + TypeScript + Vite
|
||||||
|
- Desktop host: Tauri
|
||||||
|
- Backend: FastAPI + Python services
|
||||||
|
- Media pipeline: FFmpeg, transcription, audio processing
|
||||||
|
|
||||||
|
## Autonomy Target
|
||||||
|
|
||||||
|
- Near-term target: 80-90% autonomous execution for well-scoped work.
|
||||||
|
- Mid-term target: 90-95% for low/medium-risk features with CI gates.
|
||||||
|
- 100% no-feedback autonomy is not realistic for ambiguous product decisions, legal/security tradeoffs, or high-risk migrations.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. Specs are executable and machine-readable.
|
||||||
|
2. Tests are the primary source of truth for completion.
|
||||||
|
3. Every failure is diagnosable from logs/artifacts.
|
||||||
|
4. AI has bounded permissions and policy guardrails.
|
||||||
|
5. AI updates docs and memory as part of done criteria.
|
||||||
|
|
||||||
|
## Execution Status (2026-04-15)
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
|
||||||
|
1. Added roadmap companion docs:
|
||||||
|
- `docs/spec-template.md`
|
||||||
|
- `docs/ai-policy.md`
|
||||||
|
- `docs/runbooks/error-codes.md`
|
||||||
|
2. Added operational scripts:
|
||||||
|
- `scripts/validate-all.sh`
|
||||||
|
- `scripts/collect-diagnostics.sh`
|
||||||
|
3. Ran Step 1 validation script (`./scripts/validate-all.sh`).
|
||||||
|
4. Ran Step 2 diagnostics script (`./scripts/collect-diagnostics.sh`).
|
||||||
|
5. Captured diagnostics archive:
|
||||||
|
- `.diagnostics/diag_20260415_163239.tar.gz`
|
||||||
|
6. Renamed roadmap file to `AI_dev_plan.md`.
|
||||||
|
|
||||||
|
### Current Blockers
|
||||||
|
|
||||||
|
1. Frontend lint baseline is not green yet.
|
||||||
|
2. Remaining lint issues are mostly pre-existing unused vars and hook dependency warnings across app components.
|
||||||
|
|
||||||
|
### Next Actions
|
||||||
|
|
||||||
|
1. Triage existing lint findings into:
|
||||||
|
- safe autofix
|
||||||
|
- manual low-risk cleanup
|
||||||
|
- intentional warnings to suppress with justification
|
||||||
|
2. Reach green `./scripts/validate-all.sh` in local dev.
|
||||||
|
3. Add CI workflow to enforce `validate-all` on pull requests.
|
||||||
|
|
||||||
|
## Roadmap Phases
|
||||||
|
|
||||||
|
## Phase 0: Foundation (1-2 weeks)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
1. Deterministic dev and test environment.
|
||||||
|
2. Baseline lint/type/test commands working in CI and local.
|
||||||
|
3. Standardized log format across frontend, backend, and Tauri host.
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Stabilize toolchain commands:
|
||||||
|
- frontend lint/typecheck/test
|
||||||
|
- backend lint/typecheck/test
|
||||||
|
- workspace e2e smoke command
|
||||||
|
2. Add a single script for local validation, for example `npm run validate:all`.
|
||||||
|
3. Introduce structured logging fields:
|
||||||
|
- timestamp
|
||||||
|
- request/job id
|
||||||
|
- subsystem (frontend/backend/host)
|
||||||
|
- error code
|
||||||
|
4. Add reproducible media fixtures for tests under a dedicated test-fixtures path.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Fresh clone can run validation with one command.
|
||||||
|
- CI produces deterministic pass/fail on clean branches.
|
||||||
|
- Failures include enough context to reproduce without manual guessing.
|
||||||
|
|
||||||
|
## Phase 1: Spec + Test Contracts (2-4 weeks)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
1. Feature spec template used for all new work.
|
||||||
|
2. API and schema contracts versioned and validated.
|
||||||
|
3. Regression harness for previous bugs.
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Create `docs/spec-template.md` with required sections:
|
||||||
|
- user story
|
||||||
|
- acceptance criteria
|
||||||
|
- non-goals
|
||||||
|
- edge cases
|
||||||
|
- rollback behavior
|
||||||
|
2. Add contract tests for backend routers:
|
||||||
|
- transcribe
|
||||||
|
- export
|
||||||
|
- captions
|
||||||
|
- audio
|
||||||
|
3. Add project schema validation tests for `shared/project-schema.json` and project load/save behavior.
|
||||||
|
4. For each resolved bug, add a regression test before closing issue.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- New feature PRs must include spec and tests.
|
||||||
|
- Breaking contract changes are detected automatically in CI.
|
||||||
|
|
||||||
|
## Phase 2: Observability and Self-Debugging (2-3 weeks)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
1. Unified diagnostics bundle command.
|
||||||
|
2. AI-readable failure artifacts from CI and local runs.
|
||||||
|
3. Error taxonomy and runbook mapping.
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Implement diagnostics command to collect:
|
||||||
|
- frontend logs
|
||||||
|
- backend logs
|
||||||
|
- Tauri logs
|
||||||
|
- failing test outputs
|
||||||
|
- environment metadata
|
||||||
|
2. Define error codes for common classes:
|
||||||
|
- media decode
|
||||||
|
- FFmpeg pipeline
|
||||||
|
- transcription model
|
||||||
|
- project load/save
|
||||||
|
- network/IPC bridge
|
||||||
|
3. Add runbook table mapping error codes to probable causes and first fixes.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Agent can identify likely root cause from artifacts without asking for manual logs.
|
||||||
|
- 80%+ of recurring failures map to known error classes.
|
||||||
|
|
||||||
|
## Phase 3: Controlled Autonomous Implementation (3-5 weeks)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
1. Policy file defining what AI can edit/run without approval.
|
||||||
|
2. Autonomous task loop for implement -> validate -> fix -> revalidate.
|
||||||
|
3. Automatic PR summary with risk and assumptions.
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Add policy file (for example `docs/ai-policy.md`):
|
||||||
|
- allowed directories for autonomous edits
|
||||||
|
- blocked files requiring approval
|
||||||
|
- blocked commands
|
||||||
|
2. Add task template for AI execution:
|
||||||
|
- parse feature spec
|
||||||
|
- locate impacted modules
|
||||||
|
- implement smallest changes
|
||||||
|
- run validation suite
|
||||||
|
- retry up to N fix cycles
|
||||||
|
- produce summary + residual risks
|
||||||
|
3. Require AI to update:
|
||||||
|
- copilot instructions
|
||||||
|
- changelog/roadmap note
|
||||||
|
- regression tests when bugfixing
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Low-risk feature tasks complete end-to-end without human intervention.
|
||||||
|
- CI gate pass rate for autonomous PRs remains above agreed threshold (for example 95%).
|
||||||
|
|
||||||
|
## Phase 4: High-Autonomy with Human Escalation (ongoing)
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
1. Explicit escalation triggers for ambiguity and risk.
|
||||||
|
2. Broader autonomous scope with mandatory gates.
|
||||||
|
3. Drift monitoring for quality, velocity, and regressions.
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
1. Define escalation triggers:
|
||||||
|
- user-visible behavior changes without clear spec
|
||||||
|
- API/schema breakage
|
||||||
|
- security-sensitive modifications
|
||||||
|
- destructive migrations
|
||||||
|
2. Add quality dashboards:
|
||||||
|
- flaky tests
|
||||||
|
- escaped defects
|
||||||
|
- mean time to recovery
|
||||||
|
- autonomous task success rate
|
||||||
|
3. Monthly calibration:
|
||||||
|
- adjust autonomy scope
|
||||||
|
- update policies
|
||||||
|
- prune stale runbooks and memories
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
|
||||||
|
- Autonomous throughput increases while defect rate stays stable or improves.
|
||||||
|
- Human review focuses on strategy and product decisions, not routine implementation/debugging.
|
||||||
|
|
||||||
|
## Required Engineering Systems
|
||||||
|
|
||||||
|
## 1. Spec System
|
||||||
|
|
||||||
|
Minimum implementation:
|
||||||
|
|
||||||
|
1. `docs/spec-template.md`
|
||||||
|
2. `docs/specs/` folder with one file per feature
|
||||||
|
3. CI check that new feature PRs include a spec reference
|
||||||
|
|
||||||
|
## 2. Test System
|
||||||
|
|
||||||
|
Minimum implementation:
|
||||||
|
|
||||||
|
1. Frontend unit tests for stores/components/hook logic.
|
||||||
|
2. Backend unit+integration tests for routers/services.
|
||||||
|
3. E2E smoke tests for core workflow:
|
||||||
|
- open media
|
||||||
|
- transcribe
|
||||||
|
- edit zones
|
||||||
|
- export
|
||||||
|
4. Regression tests required for every bugfix.
|
||||||
|
|
||||||
|
## 3. Environment System
|
||||||
|
|
||||||
|
Minimum implementation:
|
||||||
|
|
||||||
|
1. Locked dependencies and pinned runtimes.
|
||||||
|
2. Single bootstrap script.
|
||||||
|
3. Fixture media files for deterministic test runs.
|
||||||
|
|
||||||
|
## 4. Observability System
|
||||||
|
|
||||||
|
Minimum implementation:
|
||||||
|
|
||||||
|
1. Structured logs.
|
||||||
|
2. Standard error codes.
|
||||||
|
3. Diagnostics bundle command.
|
||||||
|
4. CI artifact retention for failed runs.
|
||||||
|
|
||||||
|
## 5. Governance System
|
||||||
|
|
||||||
|
Minimum implementation:
|
||||||
|
|
||||||
|
1. Protected branch + required checks.
|
||||||
|
2. Secret and dependency scanning.
|
||||||
|
3. Policy-based approval requirements for high-risk changes.
|
||||||
|
|
||||||
|
## Suggested Repository Additions
|
||||||
|
|
||||||
|
1. `AI_dev_plan.md` (this file)
|
||||||
|
2. `docs/spec-template.md`
|
||||||
|
3. `docs/ai-policy.md`
|
||||||
|
4. `docs/runbooks/error-codes.md`
|
||||||
|
5. `docs/runbooks/debug-playbooks.md`
|
||||||
|
6. `scripts/validate-all.sh`
|
||||||
|
7. `scripts/collect-diagnostics.sh`
|
||||||
|
|
||||||
|
## Definition of Done for Autonomous Tasks
|
||||||
|
|
||||||
|
A task is complete only if all items pass:
|
||||||
|
|
||||||
|
1. Feature spec acceptance criteria satisfied.
|
||||||
|
2. Relevant tests added/updated and passing.
|
||||||
|
3. No lint/type errors in changed scope.
|
||||||
|
4. Docs and instructions updated if behavior changed.
|
||||||
|
5. Risk summary and assumptions recorded.
|
||||||
|
|
||||||
|
## Escalation Rules (Must Ask Human)
|
||||||
|
|
||||||
|
AI must stop and ask when:
|
||||||
|
|
||||||
|
1. Requirement ambiguity changes user-visible behavior.
|
||||||
|
2. Multiple valid product decisions exist without clear preference.
|
||||||
|
3. Security/privacy/compliance implications are uncertain.
|
||||||
|
4. Data loss or destructive migration is possible.
|
||||||
|
5. CI remains failing after bounded auto-fix attempts.
|
||||||
|
|
||||||
|
## Metrics to Track
|
||||||
|
|
||||||
|
1. Autonomous task success rate.
|
||||||
|
2. Reopen rate of AI-completed tasks.
|
||||||
|
3. Regression rate per release.
|
||||||
|
4. Flaky test percentage.
|
||||||
|
5. Mean time to diagnose and resolve failures.
|
||||||
|
|
||||||
|
## 30-Day Execution Plan
|
||||||
|
|
||||||
|
Week 1:
|
||||||
|
|
||||||
|
1. Baseline scripts and deterministic environment.
|
||||||
|
2. Restore lint/test commands to green status.
|
||||||
|
3. Add structured logging and IDs.
|
||||||
|
|
||||||
|
Week 2:
|
||||||
|
|
||||||
|
1. Spec template and mandatory spec policy.
|
||||||
|
2. Contract tests for core backend routes.
|
||||||
|
3. First diagnostics bundle version.
|
||||||
|
|
||||||
|
Week 3:
|
||||||
|
|
||||||
|
1. AI policy and bounded autonomous edit/run loop.
|
||||||
|
2. Regression-test-first bugfix workflow.
|
||||||
|
3. CI artifact enrichment and runbook mapping.
|
||||||
|
|
||||||
|
Week 4:
|
||||||
|
|
||||||
|
1. Pilot autonomous feature tasks in low-risk areas.
|
||||||
|
2. Measure success/failure patterns.
|
||||||
|
3. Expand scope only if quality gates hold.
|
||||||
|
|
||||||
|
## Notes for TalkEdit
|
||||||
|
|
||||||
|
1. Keep router files thin and service logic isolated to improve AI edit precision.
|
||||||
|
2. Preserve compatibility in desktop bridge contracts to avoid frontend breakage.
|
||||||
|
3. Treat export/transcription pipeline changes as high-risk and always require regression tests.
|
||||||
|
4. Keep Linux WebKit startup and media URL consistency as explicit regression targets.
|
||||||
73
docs/ai-policy.md
Normal file
73
docs/ai-policy.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# AI Execution Policy
|
||||||
|
|
||||||
|
Purpose: define what autonomous AI can do in this repository without explicit human approval.
|
||||||
|
|
||||||
|
## Default Mode
|
||||||
|
|
||||||
|
- AI may implement and debug within approved scope.
|
||||||
|
- AI must run validation commands after code changes.
|
||||||
|
- AI must stop and escalate when blocked by policy or ambiguity.
|
||||||
|
|
||||||
|
## Allowed Autonomous Actions
|
||||||
|
|
||||||
|
1. Edit frontend, backend, shared schema, docs, and scripts.
|
||||||
|
2. Add/modify tests related to the task.
|
||||||
|
3. Run non-destructive validation commands.
|
||||||
|
4. Update project docs and Copilot instructions when behavior changes.
|
||||||
|
|
||||||
|
## Restricted Actions (Require Approval)
|
||||||
|
|
||||||
|
1. Security/privacy-sensitive logic changes.
|
||||||
|
2. Data migrations or destructive file operations.
|
||||||
|
3. Credential handling changes or secrets management changes.
|
||||||
|
4. Breaking API/schema changes.
|
||||||
|
5. Build/release signing, packaging, and deployment automation changes.
|
||||||
|
|
||||||
|
## Prohibited Actions
|
||||||
|
|
||||||
|
1. Destructive git commands (`git reset --hard`, force pushing protected branches).
|
||||||
|
2. Deleting user project/media data.
|
||||||
|
3. Bypassing required checks in CI.
|
||||||
|
|
||||||
|
## Required Validation Workflow
|
||||||
|
|
||||||
|
For each autonomous task:
|
||||||
|
|
||||||
|
1. Implement smallest safe change set.
|
||||||
|
2. Run lint/type/test/build checks for impacted scope.
|
||||||
|
3. Inspect errors and fix with bounded retries.
|
||||||
|
4. Re-run checks until green or escalated.
|
||||||
|
5. Produce concise summary with risks and assumptions.
|
||||||
|
|
||||||
|
## Escalation Triggers
|
||||||
|
|
||||||
|
AI must ask a human when:
|
||||||
|
|
||||||
|
1. Requirements are ambiguous and affect user-visible behavior.
|
||||||
|
2. Multiple product choices are plausible with no clear preference.
|
||||||
|
3. Potential legal, security, or compliance impact exists.
|
||||||
|
4. CI remains failing after 3 repair attempts in the same area.
|
||||||
|
5. A requested operation conflicts with this policy.
|
||||||
|
|
||||||
|
## Required Artifacts In AI PR/Change Summary
|
||||||
|
|
||||||
|
1. What changed.
|
||||||
|
2. Why it changed.
|
||||||
|
3. Validation commands and outcome.
|
||||||
|
4. Residual risks.
|
||||||
|
5. Follow-up tasks.
|
||||||
|
|
||||||
|
## Risk Levels
|
||||||
|
|
||||||
|
- Low: docs, styling, isolated refactors, non-critical bugfixes.
|
||||||
|
- Medium: feature additions with contract-stable behavior.
|
||||||
|
- High: API/schema/security/export pipeline/transcription pipeline changes.
|
||||||
|
|
||||||
|
High-risk changes require explicit human review before merge.
|
||||||
|
|
||||||
|
## TalkEdit-Specific Rules
|
||||||
|
|
||||||
|
1. Preserve compatibility for desktop bridge contracts unless explicitly approved.
|
||||||
|
2. Keep routers thin and business logic in backend services.
|
||||||
|
3. Export/transcription pipeline changes must include regression tests.
|
||||||
|
4. Linux WebKit startup behavior and media URL consistency are mandatory regression targets.
|
||||||
113
docs/runbooks/error-codes.md
Normal file
113
docs/runbooks/error-codes.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Error Codes Runbook
|
||||||
|
|
||||||
|
Purpose: provide consistent, AI-readable error categories for faster autonomous debugging.
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
Use codes in this format: `<SUBSYSTEM>-<CATEGORY>-<ID>`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `BE-EXPORT-001`
|
||||||
|
- `FE-WAVEFORM-002`
|
||||||
|
- `HOST-BRIDGE-003`
|
||||||
|
|
||||||
|
## Backend (FastAPI / Services)
|
||||||
|
|
||||||
|
### Export
|
||||||
|
|
||||||
|
- `BE-EXPORT-001`: Export request validation failed.
|
||||||
|
- Symptoms: HTTP 400, missing/invalid ranges.
|
||||||
|
- Likely causes: malformed payload, empty segments.
|
||||||
|
- First checks: request body shape, keep/mute/gain ranges.
|
||||||
|
|
||||||
|
- `BE-EXPORT-002`: FFmpeg command failed.
|
||||||
|
- Symptoms: HTTP 500, stderr contains filter/codec error.
|
||||||
|
- Likely causes: invalid filter chain, unsupported codec/container.
|
||||||
|
- First checks: generated FFmpeg args, source media codec, target format.
|
||||||
|
|
||||||
|
- `BE-EXPORT-003`: Caption burn-in/subtitle generation failed.
|
||||||
|
- Symptoms: burn-in export fails while plain export works.
|
||||||
|
- Likely causes: ASS generation issue, subtitle path/temp file cleanup race.
|
||||||
|
- First checks: ASS file generation, temp file lifecycle.
|
||||||
|
|
||||||
|
### Transcription
|
||||||
|
|
||||||
|
- `BE-TRANSCRIBE-001`: Model unavailable or download failure.
|
||||||
|
- Symptoms: transcription never starts or exits early.
|
||||||
|
- Likely causes: missing model, network/cache issue.
|
||||||
|
- First checks: model cache path, ensure-model logs.
|
||||||
|
|
||||||
|
- `BE-TRANSCRIBE-002`: Inference pipeline runtime failure.
|
||||||
|
- Symptoms: mid-run crash, partial output.
|
||||||
|
- Likely causes: CUDA/CPU mismatch, unsupported media, resource exhaustion.
|
||||||
|
- First checks: environment, GPU availability, media decoding logs.
|
||||||
|
|
||||||
|
### Audio / Waveform
|
||||||
|
|
||||||
|
- `BE-AUDIO-001`: Waveform endpoint failed.
|
||||||
|
- Symptoms: waveform panel shows unavailable/error.
|
||||||
|
- Likely causes: decode error, invalid file path, unsupported media input.
|
||||||
|
- First checks: `audio/waveform` response body, file existence, FFmpeg decode path.
|
||||||
|
|
||||||
|
## Frontend (React)
|
||||||
|
|
||||||
|
### Timeline / Zones
|
||||||
|
|
||||||
|
- `FE-TIMELINE-001`: Zone interaction state inconsistency.
|
||||||
|
- Symptoms: cannot drag/select/delete zones predictably.
|
||||||
|
- Likely causes: stale selection/editing state, hidden/selected mismatch.
|
||||||
|
- First checks: zone mode flags, selectedZone state transitions.
|
||||||
|
|
||||||
|
- `FE-TIMELINE-002`: Visibility filter mismatch.
|
||||||
|
- Symptoms: hidden zones still interactive or selected.
|
||||||
|
- Likely causes: hit-testing ignores visibility flags.
|
||||||
|
- First checks: hit-test filters and selected-zone reset logic.
|
||||||
|
|
||||||
|
### Media UI
|
||||||
|
|
||||||
|
- `FE-WAVEFORM-001`: Waveform fetch failed.
|
||||||
|
- Symptoms: warning banner with URL/error.
|
||||||
|
- Likely causes: backend unavailable, bad path encoding, CORS/proxy issue.
|
||||||
|
- First checks: backend health endpoint, waveform URL, network tab logs.
|
||||||
|
|
||||||
|
- `FE-PROJECT-001`: Project load mismatch.
|
||||||
|
- Symptoms: loaded media/transcript differs from saved data.
|
||||||
|
- Likely causes: schema drift, fallback URL mismatch.
|
||||||
|
- First checks: project schema fields, loadVideo/loadProject URL parity.
|
||||||
|
|
||||||
|
## Host / Bridge (Tauri)
|
||||||
|
|
||||||
|
- `HOST-BRIDGE-001`: Desktop API bridge unavailable.
|
||||||
|
- Symptoms: open/save/transcribe actions no-op or throw.
|
||||||
|
- Likely causes: bridge init error, host command mismatch.
|
||||||
|
- First checks: bridge initialization, command names, runtime environment.
|
||||||
|
|
||||||
|
- `HOST-WEBKIT-001`: Linux WebKit startup/render regression.
|
||||||
|
- Symptoms: noisy startup errors, UI load issues.
|
||||||
|
- Likely causes: CSP/font regressions, unsupported protocol calls.
|
||||||
|
- First checks: CSP config, remote font usage, bridge fallback behavior.
|
||||||
|
|
||||||
|
## Logging Guidance
|
||||||
|
|
||||||
|
When raising errors, include:
|
||||||
|
|
||||||
|
1. Error code.
|
||||||
|
2. Human message.
|
||||||
|
3. Correlation/request id.
|
||||||
|
4. Relevant paths/ids (sanitized).
|
||||||
|
5. Suggested first-check hints.
|
||||||
|
|
||||||
|
Example structured payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "BE-EXPORT-002",
|
||||||
|
"message": "FFmpeg export failed",
|
||||||
|
"requestId": "exp_20260415_001",
|
||||||
|
"context": {
|
||||||
|
"format": "mp4",
|
||||||
|
"mode": "reencode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
113
docs/spec-template.md
Normal file
113
docs/spec-template.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Feature Spec Template
|
||||||
|
|
||||||
|
Use this template for every net-new feature and major behavior change.
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
- Spec ID: SPEC-YYYYMMDD-<short-name>
|
||||||
|
- Owner:
|
||||||
|
- Date:
|
||||||
|
- Status: draft | approved | in-progress | done
|
||||||
|
- Related issue/PR:
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Describe the user problem in 2-5 sentences.
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a <user type>, I want <capability>, so that <outcome>.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. Given <state>, when <action>, then <result>.
|
||||||
|
2. Given <state>, when <action>, then <result>.
|
||||||
|
3. Failure handling is deterministic and user-visible.
|
||||||
|
|
||||||
|
## UX Notes
|
||||||
|
|
||||||
|
- Entry points (toolbar/panel/command):
|
||||||
|
- Empty/loading/error states:
|
||||||
|
- Keyboard shortcuts / accessibility expectations:
|
||||||
|
|
||||||
|
## API And Data Contracts
|
||||||
|
|
||||||
|
- Endpoints impacted:
|
||||||
|
- Request/response changes:
|
||||||
|
- Backward compatibility plan:
|
||||||
|
- Project schema impact (`shared/project-schema.json`):
|
||||||
|
|
||||||
|
## Architecture Impact
|
||||||
|
|
||||||
|
- Frontend files/components likely affected:
|
||||||
|
- Backend routers/services likely affected:
|
||||||
|
- Tauri/bridge changes required:
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
### E2E / Smoke Tests
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
### Regression Tests
|
||||||
|
|
||||||
|
List known regressions this spec must prevent.
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
- New logs/error codes:
|
||||||
|
- Metrics/traces needed:
|
||||||
|
- Diagnostics artifacts expected on failure:
|
||||||
|
|
||||||
|
## Rollout Plan
|
||||||
|
|
||||||
|
1. Development and internal validation.
|
||||||
|
2. Staged rollout or feature flag (if applicable).
|
||||||
|
3. Rollback path.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
## Definition Of Done
|
||||||
|
|
||||||
|
1. Acceptance criteria pass.
|
||||||
|
2. Tests added and green.
|
||||||
|
3. Docs/instructions updated.
|
||||||
|
4. Risks and assumptions recorded in PR summary.
|
||||||
26
frontend/eslint.config.js
Normal file
26
frontend/eslint.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist', 'node_modules'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
1412
frontend/package-lock.json
generated
1412
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,14 +22,20 @@
|
|||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.4",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.39.4",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"globals": "^17.5.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
|
"typescript-eslint": "^8.58.2",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
Scissors,
|
Scissors,
|
||||||
VolumeX,
|
VolumeX,
|
||||||
Volume2,
|
Volume2,
|
||||||
|
SlidersHorizontal,
|
||||||
FilePlus2,
|
FilePlus2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@ -57,6 +58,7 @@ export default function App() {
|
|||||||
selectedWordIndices,
|
selectedWordIndices,
|
||||||
addCutRange,
|
addCutRange,
|
||||||
addMuteRange,
|
addMuteRange,
|
||||||
|
addGainRange,
|
||||||
} = useEditorStore();
|
} = useEditorStore();
|
||||||
|
|
||||||
const [activePanel, setActivePanel] = useState<Panel>(null);
|
const [activePanel, setActivePanel] = useState<Panel>(null);
|
||||||
@ -64,6 +66,8 @@ export default function App() {
|
|||||||
const [whisperModel, setWhisperModel] = useState('base');
|
const [whisperModel, setWhisperModel] = useState('base');
|
||||||
const [cutMode, setCutMode] = useState(false);
|
const [cutMode, setCutMode] = useState(false);
|
||||||
const [muteMode, setMuteMode] = useState(false);
|
const [muteMode, setMuteMode] = useState(false);
|
||||||
|
const [gainMode, setGainMode] = useState(false);
|
||||||
|
const [gainModeDb, setGainModeDb] = useState(3);
|
||||||
const [showReprocessConfirm, setShowReprocessConfirm] = useState(false);
|
const [showReprocessConfirm, setShowReprocessConfirm] = useState(false);
|
||||||
const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false);
|
const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false);
|
||||||
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
|
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
|
||||||
@ -133,12 +137,13 @@ export default function App() {
|
|||||||
|
|
||||||
useKeyboardShortcuts();
|
useKeyboardShortcuts();
|
||||||
|
|
||||||
// Handle Escape key to exit cut/mute modes
|
// Handle Escape key to exit timeline zone modes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
setCutMode(false);
|
setCutMode(false);
|
||||||
setMuteMode(false);
|
setMuteMode(false);
|
||||||
|
setGainMode(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,6 +241,7 @@ export default function App() {
|
|||||||
setManualPath('');
|
setManualPath('');
|
||||||
setCutMode(false);
|
setCutMode(false);
|
||||||
setMuteMode(false);
|
setMuteMode(false);
|
||||||
|
setGainMode(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -348,6 +354,7 @@ export default function App() {
|
|||||||
// Toggle cut mode
|
// Toggle cut mode
|
||||||
setCutMode(!cutMode);
|
setCutMode(!cutMode);
|
||||||
setMuteMode(false); // Exit mute mode
|
setMuteMode(false); // Exit mute mode
|
||||||
|
setGainMode(false); // Exit gain mode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -362,6 +369,20 @@ export default function App() {
|
|||||||
// Toggle mute mode
|
// Toggle mute mode
|
||||||
setMuteMode(!muteMode);
|
setMuteMode(!muteMode);
|
||||||
setCutMode(false); // Exit cut mode
|
setCutMode(false); // Exit cut mode
|
||||||
|
setGainMode(false); // Exit gain mode
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGain = () => {
|
||||||
|
if (selectedWordIndices.length > 0) {
|
||||||
|
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||||
|
const startTime = words[sorted[0]].start;
|
||||||
|
const endTime = words[sorted[sorted.length - 1]].end;
|
||||||
|
addGainRange(startTime, endTime, gainModeDb);
|
||||||
|
} else {
|
||||||
|
setGainMode(!gainMode);
|
||||||
|
setCutMode(false);
|
||||||
|
setMuteMode(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -518,6 +539,12 @@ export default function App() {
|
|||||||
onClick={handleMute}
|
onClick={handleMute}
|
||||||
active={muteMode}
|
active={muteMode}
|
||||||
/>
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
icon={<SlidersHorizontal className="w-4 h-4" />}
|
||||||
|
label="Gain Zone"
|
||||||
|
onClick={handleGain}
|
||||||
|
active={gainMode}
|
||||||
|
/>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<Volume2 className="w-4 h-4" />}
|
icon={<Volume2 className="w-4 h-4" />}
|
||||||
label="Volume"
|
label="Volume"
|
||||||
@ -635,14 +662,21 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Waveform timeline */}
|
{/* Waveform timeline */}
|
||||||
<div className="h-32 border-t border-editor-border shrink-0">
|
<div className="h-32 border-t border-editor-border shrink-0">
|
||||||
<WaveformTimeline cutMode={cutMode} muteMode={muteMode} />
|
<WaveformTimeline cutMode={cutMode} muteMode={muteMode} gainMode={gainMode} gainModeDb={gainModeDb} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel (AI / Export / Settings) */}
|
{/* Right panel (AI / Export / Settings) */}
|
||||||
{activePanel && (
|
{activePanel && (
|
||||||
<div className="w-80 border-l border-editor-border overflow-y-auto shrink-0">
|
<div className="w-80 border-l border-editor-border overflow-y-auto shrink-0">
|
||||||
{activePanel === 'volume' && <VolumePanel />}
|
{activePanel === 'volume' && (
|
||||||
|
<VolumePanel
|
||||||
|
gainMode={gainMode}
|
||||||
|
onToggleGainMode={handleGain}
|
||||||
|
timelineGainDb={gainModeDb}
|
||||||
|
onTimelineGainDbChange={setGainModeDb}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{activePanel === 'silence' && <SilenceTrimmerPanel />}
|
{activePanel === 'silence' && <SilenceTrimmerPanel />}
|
||||||
{activePanel === 'ai' && <AIPanel />}
|
{activePanel === 'ai' && <AIPanel />}
|
||||||
{activePanel === 'export' && <ExportDialog />}
|
{activePanel === 'export' && <ExportDialog />}
|
||||||
|
|||||||
@ -2,7 +2,19 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useEditorStore } from '../store/editorStore';
|
import { useEditorStore } from '../store/editorStore';
|
||||||
import { Trash2, Volume2 } from 'lucide-react';
|
import { Trash2, Volume2 } from 'lucide-react';
|
||||||
|
|
||||||
export default function VolumePanel() {
|
interface VolumePanelProps {
|
||||||
|
gainMode: boolean;
|
||||||
|
onToggleGainMode: () => void;
|
||||||
|
timelineGainDb: number;
|
||||||
|
onTimelineGainDbChange: (gainDb: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VolumePanel({
|
||||||
|
gainMode,
|
||||||
|
onToggleGainMode,
|
||||||
|
timelineGainDb,
|
||||||
|
onTimelineGainDbChange,
|
||||||
|
}: VolumePanelProps) {
|
||||||
const {
|
const {
|
||||||
words,
|
words,
|
||||||
selectedWordIndices,
|
selectedWordIndices,
|
||||||
@ -65,6 +77,34 @@ export default function VolumePanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 pt-1 border-t border-editor-border">
|
||||||
|
<label className="text-xs text-editor-text-muted font-medium">Timeline Gain Zone (dB)</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={-24}
|
||||||
|
max={24}
|
||||||
|
step={0.5}
|
||||||
|
value={timelineGainDb}
|
||||||
|
onChange={(e) => onTimelineGainDbChange(Math.max(-24, Math.min(24, Number(e.target.value) || 0)))}
|
||||||
|
className="w-24 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={onToggleGainMode}
|
||||||
|
className={`px-3 py-1.5 text-xs rounded transition-colors ${
|
||||||
|
gainMode
|
||||||
|
? 'bg-editor-accent text-white hover:bg-editor-accent-hover'
|
||||||
|
: 'bg-editor-accent/20 text-editor-accent hover:bg-editor-accent/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{gainMode ? 'Exit Zone Mode' : 'Add Gain Zones'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-editor-text-muted">
|
||||||
|
In gain zone mode, drag on the timeline to create a zone with this dB value.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 pt-1 border-t border-editor-border">
|
<div className="space-y-2 pt-1 border-t border-editor-border">
|
||||||
<label className="text-xs text-editor-text-muted font-medium">Selection Gain (dB)</label>
|
<label className="text-xs text-editor-text-muted font-medium">Selection Gain (dB)</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@ -103,7 +103,17 @@ function pickInterval(pxPerSec: number): { major: number; minor: number } {
|
|||||||
return { major, minor };
|
return { major, minor };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boolean; muteMode: boolean }) {
|
export default function WaveformTimeline({
|
||||||
|
cutMode,
|
||||||
|
muteMode,
|
||||||
|
gainMode,
|
||||||
|
gainModeDb,
|
||||||
|
}: {
|
||||||
|
cutMode: boolean;
|
||||||
|
muteMode: boolean;
|
||||||
|
gainMode: boolean;
|
||||||
|
gainModeDb: number;
|
||||||
|
}) {
|
||||||
const waveCanvasRef = useRef<HTMLCanvasElement>(null);
|
const waveCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const headCanvasRef = useRef<HTMLCanvasElement>(null);
|
const headCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -116,13 +126,17 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
||||||
const cutRanges = useEditorStore((s) => s.cutRanges);
|
const cutRanges = useEditorStore((s) => s.cutRanges);
|
||||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||||
|
const gainRanges = useEditorStore((s) => s.gainRanges);
|
||||||
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
||||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||||
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
||||||
|
const addGainRange = useEditorStore((s) => s.addGainRange);
|
||||||
const updateCutRange = useEditorStore((s) => s.updateCutRange);
|
const updateCutRange = useEditorStore((s) => s.updateCutRange);
|
||||||
const updateMuteRange = useEditorStore((s) => s.updateMuteRange);
|
const updateMuteRange = useEditorStore((s) => s.updateMuteRange);
|
||||||
|
const updateGainRangeBounds = useEditorStore((s) => s.updateGainRangeBounds);
|
||||||
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
||||||
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
||||||
|
const removeGainRange = useEditorStore((s) => s.removeGainRange);
|
||||||
|
|
||||||
const waveformDataRef = useRef<WaveformData | null>(null);
|
const waveformDataRef = useRef<WaveformData | null>(null);
|
||||||
const zoomRef = useRef(1); // 1 = show all, >1 = zoomed in
|
const zoomRef = useRef(1); // 1 = show all, >1 = zoomed in
|
||||||
@ -135,10 +149,13 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
const selectionStartRef = useRef<number | null>(null);
|
const selectionStartRef = useRef<number | null>(null);
|
||||||
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
||||||
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
||||||
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute', id: string} | null>(null);
|
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string} | null>(null);
|
||||||
const [editingZone, setEditingZone] = useState<{type: 'cut' | 'mute', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
const [editingZone, setEditingZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
||||||
const [hoverCursor, setHoverCursor] = useState<string>('crosshair');
|
const [hoverCursor, setHoverCursor] = useState<string>('crosshair');
|
||||||
const editingZoneRef = useRef<{type: 'cut' | 'mute', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
const editingZoneRef = useRef<{type: 'cut' | 'mute' | 'gain', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
||||||
|
const [showCutZones, setShowCutZones] = useState(true);
|
||||||
|
const [showMuteZones, setShowMuteZones] = useState(true);
|
||||||
|
const [showGainZones, setShowGainZones] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!videoUrl || !videoPath) return;
|
if (!videoUrl || !videoPath) return;
|
||||||
@ -304,7 +321,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw cut ranges (red overlays)
|
// Draw cut ranges (red overlays)
|
||||||
for (const range of cutRanges) {
|
for (const range of showCutZones ? cutRanges : []) {
|
||||||
const x1 = (range.start - scroll) * pxPerSec;
|
const x1 = (range.start - scroll) * pxPerSec;
|
||||||
const x2 = (range.end - scroll) * pxPerSec;
|
const x2 = (range.end - scroll) * pxPerSec;
|
||||||
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
|
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
|
||||||
@ -329,7 +346,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw mute ranges (blue overlays)
|
// Draw mute ranges (blue overlays)
|
||||||
for (const range of muteRanges) {
|
for (const range of showMuteZones ? muteRanges : []) {
|
||||||
const x1 = (range.start - scroll) * pxPerSec;
|
const x1 = (range.start - scroll) * pxPerSec;
|
||||||
const x2 = (range.end - scroll) * pxPerSec;
|
const x2 = (range.end - scroll) * pxPerSec;
|
||||||
const isSelected = selectedZone?.type === 'mute' && selectedZone.id === range.id;
|
const isSelected = selectedZone?.type === 'mute' && selectedZone.id === range.id;
|
||||||
@ -353,15 +370,45 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection overlay (when in cut/mute mode)
|
// Draw gain ranges (amber overlays)
|
||||||
if ((cutMode || muteMode) && selectionStart !== null && selectionEnd !== null) {
|
for (const range of showGainZones ? gainRanges : []) {
|
||||||
|
const x1 = (range.start - scroll) * pxPerSec;
|
||||||
|
const x2 = (range.end - scroll) * pxPerSec;
|
||||||
|
const isSelected = selectedZone?.type === 'gain' && selectedZone.id === range.id;
|
||||||
|
|
||||||
|
ctx.fillStyle = isSelected ? 'rgba(245, 158, 11, 0.55)' : 'rgba(245, 158, 11, 0.35)';
|
||||||
|
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
ctx.strokeStyle = '#f59e0b';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||||
|
|
||||||
|
ctx.fillStyle = '#f59e0b';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x1, waveTop + waveH / 2, 4, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x2, waveTop + waveH / 2, 4, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw selection overlay (when in zone mode)
|
||||||
|
if ((cutMode || muteMode || gainMode) && selectionStart !== null && selectionEnd !== null) {
|
||||||
const x1 = (Math.min(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
const x1 = (Math.min(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
||||||
const x2 = (Math.max(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
const x2 = (Math.max(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
||||||
ctx.fillStyle = cutMode ? 'rgba(239, 68, 68, 0.5)' : 'rgba(59, 130, 246, 0.5)';
|
const fillColor = cutMode
|
||||||
|
? 'rgba(239, 68, 68, 0.5)'
|
||||||
|
: muteMode
|
||||||
|
? 'rgba(59, 130, 246, 0.5)'
|
||||||
|
: 'rgba(245, 158, 11, 0.5)';
|
||||||
|
const strokeColor = cutMode ? '#ef4444' : muteMode ? '#3b82f6' : '#f59e0b';
|
||||||
|
ctx.fillStyle = fillColor;
|
||||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||||
|
|
||||||
// Add border
|
// Add border
|
||||||
ctx.strokeStyle = cutMode ? '#ef4444' : '#3b82f6';
|
ctx.strokeStyle = strokeColor;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||||
}
|
}
|
||||||
@ -389,7 +436,21 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
ctx.lineTo(x, mid + max * amp);
|
ctx.lineTo(x, mid + max * amp);
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}, [deletedRanges, cutRanges, muteRanges, selectionStart, selectionEnd, cutMode, muteMode, selectedZone]);
|
}, [
|
||||||
|
deletedRanges,
|
||||||
|
cutRanges,
|
||||||
|
muteRanges,
|
||||||
|
gainRanges,
|
||||||
|
selectionStart,
|
||||||
|
selectionEnd,
|
||||||
|
cutMode,
|
||||||
|
muteMode,
|
||||||
|
gainMode,
|
||||||
|
selectedZone,
|
||||||
|
showCutZones,
|
||||||
|
showMuteZones,
|
||||||
|
showGainZones,
|
||||||
|
]);
|
||||||
|
|
||||||
// Keep the ref in sync with the latest drawStaticWaveform closure
|
// Keep the ref in sync with the latest drawStaticWaveform closure
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -537,7 +598,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
const handleSize = forHover ? 6 : 8; // Smaller hit area for hover, larger for click
|
const handleSize = forHover ? 6 : 8; // Smaller hit area for hover, larger for click
|
||||||
|
|
||||||
// Check cut ranges
|
// Check cut ranges
|
||||||
for (const range of cutRanges) {
|
for (const range of showCutZones ? cutRanges : []) {
|
||||||
const rangeX1 = (range.start - scroll) * pxPerSec;
|
const rangeX1 = (range.start - scroll) * pxPerSec;
|
||||||
const rangeX2 = (range.end - scroll) * pxPerSec;
|
const rangeX2 = (range.end - scroll) * pxPerSec;
|
||||||
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
|
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
|
||||||
@ -579,7 +640,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check mute ranges
|
// Check mute ranges
|
||||||
for (const range of muteRanges) {
|
for (const range of showMuteZones ? muteRanges : []) {
|
||||||
const rangeX1 = (range.start - scroll) * pxPerSec;
|
const rangeX1 = (range.start - scroll) * pxPerSec;
|
||||||
const rangeX2 = (range.end - scroll) * pxPerSec;
|
const rangeX2 = (range.end - scroll) * pxPerSec;
|
||||||
const isSelected = selectedZone?.type === 'mute' && selectedZone.id === range.id;
|
const isSelected = selectedZone?.type === 'mute' && selectedZone.id === range.id;
|
||||||
@ -620,8 +681,43 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check gain ranges
|
||||||
|
for (const range of showGainZones ? gainRanges : []) {
|
||||||
|
const rangeX1 = (range.start - scroll) * pxPerSec;
|
||||||
|
const rangeX2 = (range.end - scroll) * pxPerSec;
|
||||||
|
const isSelected = selectedZone?.type === 'gain' && selectedZone.id === range.id;
|
||||||
|
|
||||||
|
if (forHover && isSelected) {
|
||||||
|
if (Math.abs(x - rangeX1) <= handleSize) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'start' as const };
|
||||||
|
}
|
||||||
|
if (Math.abs(x - rangeX2) <= handleSize) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'end' as const };
|
||||||
|
}
|
||||||
|
} else if (!forHover) {
|
||||||
|
if (isSelected) {
|
||||||
|
if (Math.abs(x - rangeX1) <= handleSize) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'start' as const };
|
||||||
|
}
|
||||||
|
if (Math.abs(x - rangeX2) <= handleSize) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'end' as const };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Math.abs(x - rangeX1) <= handleSize && Math.abs(y - (waveTop + waveH / 2)) <= handleSize) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'start' as const };
|
||||||
|
}
|
||||||
|
if (Math.abs(x - rangeX2) <= handleSize && Math.abs(y - (waveTop + waveH / 2)) <= handleSize) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'end' as const };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x >= rangeX1 && x <= rangeX2) {
|
||||||
|
return { type: 'gain' as const, id: range.id, edge: 'move' as const };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [cutRanges, muteRanges, selectedZone]);
|
}, [cutRanges, muteRanges, gainRanges, selectedZone, showCutZones, showMuteZones, showGainZones]);
|
||||||
|
|
||||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (isDragging) return; // Don't change cursor while dragging
|
if (isDragging) return; // Don't change cursor while dragging
|
||||||
@ -654,9 +750,11 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
|
|
||||||
const startTime = clientXToTime(e.clientX);
|
const startTime = clientXToTime(e.clientX);
|
||||||
const originalRange = zoneHit.type === 'cut'
|
const originalRange = zoneHit.type === 'cut'
|
||||||
? cutRanges.find(r => r.id === zoneHit.id)
|
? cutRanges.find(r => r.id === zoneHit.id)
|
||||||
: muteRanges.find(r => r.id === zoneHit.id);
|
: zoneHit.type === 'mute'
|
||||||
|
? muteRanges.find(r => r.id === zoneHit.id)
|
||||||
|
: gainRanges.find(r => r.id === zoneHit.id);
|
||||||
|
|
||||||
if (!originalRange) return;
|
if (!originalRange) return;
|
||||||
|
|
||||||
@ -686,8 +784,10 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
if (newStart < newEnd) {
|
if (newStart < newEnd) {
|
||||||
if (editingZoneRef.current.type === 'cut') {
|
if (editingZoneRef.current.type === 'cut') {
|
||||||
updateCutRange(editingZoneRef.current.id, newStart, newEnd);
|
updateCutRange(editingZoneRef.current.id, newStart, newEnd);
|
||||||
} else {
|
} else if (editingZoneRef.current.type === 'mute') {
|
||||||
updateMuteRange(editingZoneRef.current.id, newStart, newEnd);
|
updateMuteRange(editingZoneRef.current.id, newStart, newEnd);
|
||||||
|
} else {
|
||||||
|
updateGainRangeBounds(editingZoneRef.current.id, newStart, newEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -710,7 +810,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
setSelectedZone(null);
|
setSelectedZone(null);
|
||||||
setEditingZone(null);
|
setEditingZone(null);
|
||||||
|
|
||||||
if (cutMode || muteMode) {
|
if (cutMode || muteMode || gainMode) {
|
||||||
// Range selection mode
|
// Range selection mode
|
||||||
const startTime = clientXToTime(e.clientX);
|
const startTime = clientXToTime(e.clientX);
|
||||||
selectionStartRef.current = startTime;
|
selectionStartRef.current = startTime;
|
||||||
@ -737,6 +837,8 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
addCutRange(start, end);
|
addCutRange(start, end);
|
||||||
} else if (muteMode) {
|
} else if (muteMode) {
|
||||||
addMuteRange(start, end);
|
addMuteRange(start, end);
|
||||||
|
} else if (gainMode) {
|
||||||
|
addGainRange(start, end, gainModeDb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -771,7 +873,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
window.addEventListener('mouseup', onUp);
|
window.addEventListener('mouseup', onUp);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cutMode, muteMode, clientXToTime, seekToClientX, addCutRange, addMuteRange, selectionEnd, getZoneAtPosition],
|
[cutMode, muteMode, gainMode, gainModeDb, clientXToTime, seekToClientX, addCutRange, addMuteRange, addGainRange, selectionEnd, getZoneAtPosition, cutRanges, muteRanges, gainRanges, duration, updateCutRange, updateMuteRange, updateGainRangeBounds],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle keyboard shortcuts for zone editing
|
// Handle keyboard shortcuts for zone editing
|
||||||
@ -793,8 +895,10 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
if (selectedZone.type === 'cut') {
|
if (selectedZone.type === 'cut') {
|
||||||
removeCutRange(selectedZone.id);
|
removeCutRange(selectedZone.id);
|
||||||
} else {
|
} else if (selectedZone.type === 'mute') {
|
||||||
removeMuteRange(selectedZone.id);
|
removeMuteRange(selectedZone.id);
|
||||||
|
} else {
|
||||||
|
removeGainRange(selectedZone.id);
|
||||||
}
|
}
|
||||||
setSelectedZone(null);
|
setSelectedZone(null);
|
||||||
setEditingZone(null);
|
setEditingZone(null);
|
||||||
@ -806,7 +910,14 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
// Capture phase ensures zone delete runs before app-level bubble shortcuts.
|
// Capture phase ensures zone delete runs before app-level bubble shortcuts.
|
||||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||||
}, [selectedZone, removeCutRange, removeMuteRange]);
|
}, [selectedZone, removeCutRange, removeMuteRange, removeGainRange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedZone) return;
|
||||||
|
if (selectedZone.type === 'cut' && !showCutZones) setSelectedZone(null);
|
||||||
|
if (selectedZone.type === 'mute' && !showMuteZones) setSelectedZone(null);
|
||||||
|
if (selectedZone.type === 'gain' && !showGainZones) setSelectedZone(null);
|
||||||
|
}, [selectedZone, showCutZones, showMuteZones, showGainZones]);
|
||||||
|
|
||||||
if (!videoUrl) {
|
if (!videoUrl) {
|
||||||
return (
|
return (
|
||||||
@ -818,13 +929,41 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="w-full h-full flex flex-col">
|
<div ref={containerRef} className="w-full h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between px-3 py-1 shrink-0">
|
<div className="flex items-center justify-between px-3 py-1 shrink-0 gap-3">
|
||||||
<span className="text-[10px] text-editor-text-muted font-medium uppercase tracking-wider">
|
<div className="flex items-center gap-2">
|
||||||
Timeline
|
<span className="text-[10px] text-editor-text-muted font-medium uppercase tracking-wider">
|
||||||
</span>
|
Timeline
|
||||||
<span className="text-[10px] text-editor-text-muted">
|
</span>
|
||||||
Scroll · Ctrl+Scroll to zoom
|
{cutMode && <span className="text-[10px] text-red-400">Cut mode</span>}
|
||||||
</span>
|
{muteMode && <span className="text-[10px] text-blue-400">Mute mode</span>}
|
||||||
|
{gainMode && <span className="text-[10px] text-amber-400">Gain mode ({gainModeDb.toFixed(1)} dB)</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCutZones((v) => !v)}
|
||||||
|
className={`px-1.5 py-0.5 rounded text-[10px] border ${showCutZones ? 'border-red-500/60 text-red-300 bg-red-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||||
|
title="Toggle cut zones"
|
||||||
|
>
|
||||||
|
Cut
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowMuteZones((v) => !v)}
|
||||||
|
className={`px-1.5 py-0.5 rounded text-[10px] border ${showMuteZones ? 'border-blue-500/60 text-blue-300 bg-blue-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||||
|
title="Toggle mute zones"
|
||||||
|
>
|
||||||
|
Mute
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowGainZones((v) => !v)}
|
||||||
|
className={`px-1.5 py-0.5 rounded text-[10px] border ${showGainZones ? 'border-amber-500/60 text-amber-300 bg-amber-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||||
|
title="Toggle gain zones"
|
||||||
|
>
|
||||||
|
Gain
|
||||||
|
</button>
|
||||||
|
<span className="text-[10px] text-editor-text-muted">
|
||||||
|
Scroll · Ctrl+Scroll to zoom
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{audioError ? (
|
{audioError ? (
|
||||||
<div className="flex-1 flex items-start justify-center gap-2 text-editor-text-muted text-xs p-3 overflow-auto">
|
<div className="flex-1 flex items-start justify-center gap-2 text-editor-text-muted text-xs p-3 overflow-auto">
|
||||||
|
|||||||
@ -65,6 +65,7 @@ interface EditorActions {
|
|||||||
addGainRange: (start: number, end: number, gainDb: number) => void;
|
addGainRange: (start: number, end: number, gainDb: number) => void;
|
||||||
updateCutRange: (id: string, start: number, end: number) => void;
|
updateCutRange: (id: string, start: number, end: number) => void;
|
||||||
updateMuteRange: (id: string, start: number, end: number) => void;
|
updateMuteRange: (id: string, start: number, end: number) => void;
|
||||||
|
updateGainRangeBounds: (id: string, start: number, end: number) => void;
|
||||||
updateGainRange: (id: string, gainDb: number) => void;
|
updateGainRange: (id: string, gainDb: number) => void;
|
||||||
removeCutRange: (id: string) => void;
|
removeCutRange: (id: string) => void;
|
||||||
removeMuteRange: (id: string) => void;
|
removeMuteRange: (id: string) => void;
|
||||||
@ -299,6 +300,15 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateGainRangeBounds: (id, start, end) => {
|
||||||
|
const { gainRanges } = get();
|
||||||
|
set({
|
||||||
|
gainRanges: gainRanges.map((r) =>
|
||||||
|
r.id === id ? { ...r, start, end } : r
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
updateGainRange: (id, gainDb) => {
|
updateGainRange: (id, gainDb) => {
|
||||||
const { gainRanges } = get();
|
const { gainRanges } = get();
|
||||||
set({
|
set({
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/SettingsPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/WaveformTimeline.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/tauri-bridge.ts","./src/store/aiStore.ts","./src/store/editorStore.ts","./src/types/project.ts"],"errors":true,"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/VolumePanel.tsx","./src/components/WaveformTimeline.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/tauri-bridge.ts","./src/store/aiStore.ts","./src/store/editorStore.ts","./src/types/project.ts"],"version":"5.9.3"}
|
||||||
72
scripts/collect-diagnostics.sh
Executable file
72
scripts/collect-diagnostics.sh
Executable file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT_BASE="$ROOT_DIR/.diagnostics"
|
||||||
|
TS="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
OUT_DIR="$OUT_BASE/diag_$TS"
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[collect-diagnostics] %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_cmd() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
{
|
||||||
|
echo "# $name"
|
||||||
|
echo "# cmd: $*"
|
||||||
|
"$@"
|
||||||
|
} >"$OUT_DIR/$name.txt" 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
log "output: $OUT_DIR"
|
||||||
|
|
||||||
|
capture_cmd "env_uname" uname -a
|
||||||
|
capture_cmd "env_node_version" node --version
|
||||||
|
capture_cmd "env_npm_version" npm --version
|
||||||
|
capture_cmd "env_git_status" git -C "$ROOT_DIR" status --short
|
||||||
|
capture_cmd "env_git_head" git -C "$ROOT_DIR" rev-parse --short HEAD
|
||||||
|
|
||||||
|
if [[ -f "$ROOT_DIR/webview.log" ]]; then
|
||||||
|
cp "$ROOT_DIR/webview.log" "$OUT_DIR/webview.log" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$ROOT_DIR/backend.log" ]]; then
|
||||||
|
cp "$ROOT_DIR/backend.log" "$OUT_DIR/backend.log" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$ROOT_DIR/frontend/package.json" ]]; then
|
||||||
|
capture_cmd "frontend_lint" bash -lc "cd '$ROOT_DIR/frontend' && npm run -s lint"
|
||||||
|
capture_cmd "frontend_build" bash -lc "cd '$ROOT_DIR/frontend' && npm run -s build"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PY=""
|
||||||
|
for p in \
|
||||||
|
"$ROOT_DIR/.venv312/bin/python3.12" \
|
||||||
|
"$ROOT_DIR/.venv312/bin/python" \
|
||||||
|
"$ROOT_DIR/.venv/bin/python3" \
|
||||||
|
"$ROOT_DIR/.venv/bin/python" \
|
||||||
|
"$ROOT_DIR/venv/bin/python3" \
|
||||||
|
"$ROOT_DIR/venv/bin/python"; do
|
||||||
|
if [[ -x "$p" ]]; then
|
||||||
|
PY="$p"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$PY" ]]; then
|
||||||
|
capture_cmd "backend_python_version" "$PY" --version
|
||||||
|
capture_cmd "backend_health_check" "$PY" -c "import importlib; importlib.import_module('backend.main'); print('backend import OK')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
capture_cmd "list_recent_files" find "$ROOT_DIR" -maxdepth 2 -type f | head -n 200
|
||||||
|
|
||||||
|
if command -v tar >/dev/null 2>&1; then
|
||||||
|
tar -czf "$OUT_DIR.tar.gz" -C "$OUT_BASE" "diag_$TS"
|
||||||
|
log "archive: $OUT_DIR.tar.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "done"
|
||||||
93
scripts/validate-all.sh
Executable file
93
scripts/validate-all.sh
Executable file
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[validate-all] %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_if_present() {
|
||||||
|
local cmd="$1"
|
||||||
|
if command -v ${cmd%% *} >/dev/null 2>&1; then
|
||||||
|
eval "$cmd"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log "root: $ROOT_DIR"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
log "Step 1/5: frontend dependency check"
|
||||||
|
if [[ ! -d "frontend/node_modules" ]]; then
|
||||||
|
log "frontend/node_modules missing; install with: cd frontend && npm install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Step 2/5: frontend lint"
|
||||||
|
if [[ -f "frontend/package.json" ]]; then
|
||||||
|
(
|
||||||
|
cd frontend
|
||||||
|
if npm run -s lint; then
|
||||||
|
log "frontend lint: OK"
|
||||||
|
else
|
||||||
|
log "frontend lint failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
else
|
||||||
|
log "frontend/package.json not found; skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Step 3/5: frontend build"
|
||||||
|
if [[ -f "frontend/package.json" ]]; then
|
||||||
|
(
|
||||||
|
cd frontend
|
||||||
|
if npm run -s build; then
|
||||||
|
log "frontend build: OK"
|
||||||
|
else
|
||||||
|
log "frontend build failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Step 4/5: backend syntax check"
|
||||||
|
PY=""
|
||||||
|
for p in \
|
||||||
|
"$ROOT_DIR/.venv312/bin/python3.12" \
|
||||||
|
"$ROOT_DIR/.venv312/bin/python" \
|
||||||
|
"$ROOT_DIR/.venv/bin/python3" \
|
||||||
|
"$ROOT_DIR/.venv/bin/python" \
|
||||||
|
"$ROOT_DIR/venv/bin/python3" \
|
||||||
|
"$ROOT_DIR/venv/bin/python"; do
|
||||||
|
if [[ -x "$p" ]]; then
|
||||||
|
PY="$p"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$PY" ]]; then
|
||||||
|
log "using python: $PY"
|
||||||
|
"$PY" -m py_compile "$ROOT_DIR/backend/main.py" "$ROOT_DIR/backend/routers/export.py"
|
||||||
|
log "backend syntax check: OK"
|
||||||
|
else
|
||||||
|
log "no project python found (.venv312/.venv/venv); skipping backend syntax check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Step 5/5: backend health import smoke"
|
||||||
|
if [[ -n "$PY" ]]; then
|
||||||
|
"$PY" - <<'PYCODE'
|
||||||
|
import importlib
|
||||||
|
mods = [
|
||||||
|
"backend.main",
|
||||||
|
"backend.routers.export",
|
||||||
|
"backend.services.video_editor",
|
||||||
|
]
|
||||||
|
for m in mods:
|
||||||
|
importlib.import_module(m)
|
||||||
|
print("backend import smoke: OK")
|
||||||
|
PYCODE
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Validation complete"
|
||||||
Reference in New Issue
Block a user