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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"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",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
Scissors,
|
||||
VolumeX,
|
||||
Volume2,
|
||||
SlidersHorizontal,
|
||||
FilePlus2,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
@ -57,6 +58,7 @@ export default function App() {
|
||||
selectedWordIndices,
|
||||
addCutRange,
|
||||
addMuteRange,
|
||||
addGainRange,
|
||||
} = useEditorStore();
|
||||
|
||||
const [activePanel, setActivePanel] = useState<Panel>(null);
|
||||
@ -64,6 +66,8 @@ export default function App() {
|
||||
const [whisperModel, setWhisperModel] = useState('base');
|
||||
const [cutMode, setCutMode] = useState(false);
|
||||
const [muteMode, setMuteMode] = useState(false);
|
||||
const [gainMode, setGainMode] = useState(false);
|
||||
const [gainModeDb, setGainModeDb] = useState(3);
|
||||
const [showReprocessConfirm, setShowReprocessConfirm] = useState(false);
|
||||
const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false);
|
||||
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
|
||||
@ -133,12 +137,13 @@ export default function App() {
|
||||
|
||||
useKeyboardShortcuts();
|
||||
|
||||
// Handle Escape key to exit cut/mute modes
|
||||
// Handle Escape key to exit timeline zone modes
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setCutMode(false);
|
||||
setMuteMode(false);
|
||||
setGainMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -236,6 +241,7 @@ export default function App() {
|
||||
setManualPath('');
|
||||
setCutMode(false);
|
||||
setMuteMode(false);
|
||||
setGainMode(false);
|
||||
});
|
||||
};
|
||||
|
||||
@ -348,6 +354,7 @@ export default function App() {
|
||||
// Toggle cut mode
|
||||
setCutMode(!cutMode);
|
||||
setMuteMode(false); // Exit mute mode
|
||||
setGainMode(false); // Exit gain mode
|
||||
}
|
||||
};
|
||||
|
||||
@ -362,6 +369,20 @@ export default function App() {
|
||||
// Toggle mute mode
|
||||
setMuteMode(!muteMode);
|
||||
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}
|
||||
active={muteMode}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<SlidersHorizontal className="w-4 h-4" />}
|
||||
label="Gain Zone"
|
||||
onClick={handleGain}
|
||||
active={gainMode}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={<Volume2 className="w-4 h-4" />}
|
||||
label="Volume"
|
||||
@ -635,14 +662,21 @@ export default function App() {
|
||||
|
||||
{/* Waveform timeline */}
|
||||
<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>
|
||||
|
||||
{/* Right panel (AI / Export / Settings) */}
|
||||
{activePanel && (
|
||||
<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 === 'ai' && <AIPanel />}
|
||||
{activePanel === 'export' && <ExportDialog />}
|
||||
|
||||
@ -2,7 +2,19 @@ import { useMemo, useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
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 {
|
||||
words,
|
||||
selectedWordIndices,
|
||||
@ -65,6 +77,34 @@ export default function VolumePanel() {
|
||||
</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">
|
||||
<label className="text-xs text-editor-text-muted font-medium">Selection Gain (dB)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@ -103,7 +103,17 @@ function pickInterval(pxPerSec: number): { major: number; minor: number } {
|
||||
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 headCanvasRef = useRef<HTMLCanvasElement>(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 cutRanges = useEditorStore((s) => s.cutRanges);
|
||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||
const gainRanges = useEditorStore((s) => s.gainRanges);
|
||||
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
||||
const addGainRange = useEditorStore((s) => s.addGainRange);
|
||||
const updateCutRange = useEditorStore((s) => s.updateCutRange);
|
||||
const updateMuteRange = useEditorStore((s) => s.updateMuteRange);
|
||||
const updateGainRangeBounds = useEditorStore((s) => s.updateGainRangeBounds);
|
||||
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
||||
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
||||
const removeGainRange = useEditorStore((s) => s.removeGainRange);
|
||||
|
||||
const waveformDataRef = useRef<WaveformData | null>(null);
|
||||
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 [selectionStart, setSelectionStart] = useState<number | null>(null);
|
||||
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
||||
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute', id: string} | null>(null);
|
||||
const [editingZone, setEditingZone] = useState<{type: 'cut' | 'mute', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
||||
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string} | 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 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(() => {
|
||||
if (!videoUrl || !videoPath) return;
|
||||
@ -304,7 +321,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
}
|
||||
|
||||
// Draw cut ranges (red overlays)
|
||||
for (const range of cutRanges) {
|
||||
for (const range of showCutZones ? cutRanges : []) {
|
||||
const x1 = (range.start - scroll) * pxPerSec;
|
||||
const x2 = (range.end - scroll) * pxPerSec;
|
||||
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)
|
||||
for (const range of muteRanges) {
|
||||
for (const range of showMuteZones ? muteRanges : []) {
|
||||
const x1 = (range.start - scroll) * pxPerSec;
|
||||
const x2 = (range.end - scroll) * pxPerSec;
|
||||
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)
|
||||
if ((cutMode || muteMode) && selectionStart !== null && selectionEnd !== null) {
|
||||
// Draw gain ranges (amber overlays)
|
||||
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 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);
|
||||
|
||||
// Add border
|
||||
ctx.strokeStyle = cutMode ? '#ef4444' : '#3b82f6';
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = 2;
|
||||
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.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
|
||||
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
|
||||
|
||||
// Check cut ranges
|
||||
for (const range of cutRanges) {
|
||||
for (const range of showCutZones ? cutRanges : []) {
|
||||
const rangeX1 = (range.start - scroll) * pxPerSec;
|
||||
const rangeX2 = (range.end - scroll) * pxPerSec;
|
||||
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
|
||||
@ -579,7 +640,7 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
}
|
||||
|
||||
// Check mute ranges
|
||||
for (const range of muteRanges) {
|
||||
for (const range of showMuteZones ? muteRanges : []) {
|
||||
const rangeX1 = (range.start - scroll) * pxPerSec;
|
||||
const rangeX2 = (range.end - scroll) * pxPerSec;
|
||||
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;
|
||||
}, [cutRanges, muteRanges, selectedZone]);
|
||||
}, [cutRanges, muteRanges, gainRanges, selectedZone, showCutZones, showMuteZones, showGainZones]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (isDragging) return; // Don't change cursor while dragging
|
||||
@ -656,7 +752,9 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
const startTime = clientXToTime(e.clientX);
|
||||
const originalRange = zoneHit.type === 'cut'
|
||||
? 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;
|
||||
|
||||
@ -686,8 +784,10 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
if (newStart < newEnd) {
|
||||
if (editingZoneRef.current.type === 'cut') {
|
||||
updateCutRange(editingZoneRef.current.id, newStart, newEnd);
|
||||
} else {
|
||||
} else if (editingZoneRef.current.type === 'mute') {
|
||||
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);
|
||||
setEditingZone(null);
|
||||
|
||||
if (cutMode || muteMode) {
|
||||
if (cutMode || muteMode || gainMode) {
|
||||
// Range selection mode
|
||||
const startTime = clientXToTime(e.clientX);
|
||||
selectionStartRef.current = startTime;
|
||||
@ -737,6 +837,8 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
addCutRange(start, end);
|
||||
} else if (muteMode) {
|
||||
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);
|
||||
}
|
||||
},
|
||||
[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
|
||||
@ -793,8 +895,10 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
e.stopImmediatePropagation();
|
||||
if (selectedZone.type === 'cut') {
|
||||
removeCutRange(selectedZone.id);
|
||||
} else {
|
||||
} else if (selectedZone.type === 'mute') {
|
||||
removeMuteRange(selectedZone.id);
|
||||
} else {
|
||||
removeGainRange(selectedZone.id);
|
||||
}
|
||||
setSelectedZone(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.
|
||||
window.addEventListener('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) {
|
||||
return (
|
||||
@ -818,13 +929,41 @@ export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boole
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-full flex flex-col">
|
||||
<div className="flex items-center justify-between px-3 py-1 shrink-0">
|
||||
<span className="text-[10px] text-editor-text-muted font-medium uppercase tracking-wider">
|
||||
Timeline
|
||||
</span>
|
||||
<span className="text-[10px] text-editor-text-muted">
|
||||
Scroll · Ctrl+Scroll to zoom
|
||||
</span>
|
||||
<div className="flex items-center justify-between px-3 py-1 shrink-0 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-editor-text-muted font-medium uppercase tracking-wider">
|
||||
Timeline
|
||||
</span>
|
||||
{cutMode && <span className="text-[10px] text-red-400">Cut mode</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>
|
||||
{audioError ? (
|
||||
<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;
|
||||
updateCutRange: (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;
|
||||
removeCutRange: (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) => {
|
||||
const { gainRanges } = get();
|
||||
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