mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 03:46:31 +00:00
Refactor annotation tool from WPF desktop app to .NET API
Replace the WPF desktop application (Azaion.Suite, Azaion.Annotator, Azaion.Common, Azaion.Inference, Azaion.Loader, Azaion.LoaderUI, Azaion.Dataset, Azaion.Test) with a standalone .NET Web API in src/. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
## Developer TODO (Project Mode)
|
||||
|
||||
### BUILD (green-field or new features)
|
||||
|
||||
```
|
||||
1. Create _docs/00_problem/ — describe what you're building
|
||||
- problem.md (required)
|
||||
- restrictions.md (required)
|
||||
- acceptance_criteria.md (required)
|
||||
- security_approach.md (optional)
|
||||
|
||||
2. /research — produces solution drafts in _docs/01_solution/
|
||||
Run multiple times: Mode A → draft, Mode B → assess & revise
|
||||
Finalize as solution.md
|
||||
|
||||
3. /plan — architecture, components, risks, tests → _docs/02_plans/
|
||||
|
||||
4. /decompose — feature specs, implementation order → _docs/02_tasks/
|
||||
|
||||
5. /implement-initial — scaffold project from initial_structure.md (once)
|
||||
|
||||
6. /implement-wave — implement next wave of features (repeat per wave)
|
||||
|
||||
7. /implement-code-review — review implemented code (after each wave or at the end)
|
||||
|
||||
8. /implement-black-box-tests — E2E tests via Docker consumer app (after all waves)
|
||||
|
||||
9. commit & push
|
||||
```
|
||||
|
||||
### SHIP (deploy and operate)
|
||||
|
||||
```
|
||||
10. /implement-cicd — validate/enhance CI/CD pipeline
|
||||
11. /deploy — deployment strategy per environment
|
||||
12. /observability — monitoring, logging, alerting plan
|
||||
```
|
||||
|
||||
### EVOLVE (maintenance and improvement)
|
||||
|
||||
```
|
||||
13. /refactor — structured refactoring (skill, 6-phase workflow)
|
||||
```
|
||||
|
||||
## Implementation Flow
|
||||
|
||||
### `/implement-initial`
|
||||
|
||||
Reads `_docs/02_tasks/<topic>/initial_structure.md` and scaffolds the project skeleton: folder structure, shared models, interfaces, stubs, .gitignore, .env.example, CI/CD config, DB migrations setup, test structure.
|
||||
|
||||
Run once after decompose.
|
||||
|
||||
### `/implement-wave`
|
||||
|
||||
Reads `SUMMARY.md` and `cross_dependencies.md` from `_docs/02_tasks/<topic>/`.
|
||||
|
||||
1. Detects which features are already implemented
|
||||
2. Identifies the next wave (phase) of independent features
|
||||
3. Presents the wave for confirmation (blocks until user confirms)
|
||||
4. Launches parallel `implementer` subagents (max 4 concurrent; same-component features run sequentially)
|
||||
5. Runs tests, reports results
|
||||
6. Suggests commit
|
||||
|
||||
Repeat `/implement-wave` until all phases are done.
|
||||
|
||||
### `/implement-code-review`
|
||||
|
||||
Reviews implemented code against specs. Reports issues by type (Bug/Security/Performance/Style/Debt) with priorities and suggested fixes.
|
||||
|
||||
### `/implement-black-box-tests`
|
||||
|
||||
Reads `_docs/02_plans/<topic>/e2e_test_infrastructure.md` (produced by plan skill). Builds a separate Docker-based consumer app that exercises the system as a black box — no internal imports, no direct DB access. Runs E2E scenarios, produces a CSV test report.
|
||||
|
||||
Run after all waves are done.
|
||||
|
||||
### `/implement-cicd`
|
||||
|
||||
Reviews existing CI/CD pipeline configuration, validates all stages work, optimizes performance (parallelization, caching), ensures quality gates are enforced (coverage, linting, security scanning).
|
||||
|
||||
Run after `/implement-initial` or after all waves.
|
||||
|
||||
### `/deploy`
|
||||
|
||||
Defines deployment strategy per environment: deployment procedures, rollback procedures, health checks, deployment checklist. Outputs `_docs/02_components/deployment_strategy.md`.
|
||||
|
||||
Run before first production release.
|
||||
|
||||
### `/observability`
|
||||
|
||||
Plans logging strategy, metrics collection, distributed tracing, alerting rules, and dashboards. Outputs `_docs/02_components/observability_plan.md`.
|
||||
|
||||
Run before first production release.
|
||||
|
||||
### Commit
|
||||
|
||||
After each wave or review — standard `git add && git commit`. The wave command suggests a commit message.
|
||||
|
||||
## Available Skills
|
||||
|
||||
| Skill | Triggers | Purpose |
|
||||
|-------|----------|---------|
|
||||
| **research** | "research", "investigate", "assess solution" | 8-step research → solution drafts |
|
||||
| **plan** | "plan", "decompose solution" | Architecture, components, risks, tests, epics |
|
||||
| **decompose** | "decompose", "task decomposition" | Feature specs + implementation order |
|
||||
| **refactor** | "refactor", "refactoring", "improve code" | 6-phase structured refactoring workflow |
|
||||
| **security** | "security audit", "OWASP" | OWASP-based security testing |
|
||||
|
||||
## Project Folder Structure
|
||||
|
||||
```
|
||||
_docs/
|
||||
├── 00_problem/
|
||||
│ ├── problem.md
|
||||
│ ├── restrictions.md
|
||||
│ ├── acceptance_criteria.md
|
||||
│ └── security_approach.md
|
||||
├── 01_solution/
|
||||
│ ├── solution_draft01.md
|
||||
│ ├── solution_draft02.md
|
||||
│ ├── solution.md
|
||||
│ ├── tech_stack.md
|
||||
│ └── security_analysis.md
|
||||
├── 01_research/
|
||||
│ └── <topic>/
|
||||
├── 02_plans/
|
||||
│ └── <topic>/
|
||||
│ ├── architecture.md
|
||||
│ ├── system-flows.md
|
||||
│ ├── components/
|
||||
│ └── FINAL_report.md
|
||||
├── 02_tasks/
|
||||
│ └── <topic>/
|
||||
│ ├── initial_structure.md
|
||||
│ ├── cross_dependencies.md
|
||||
│ ├── SUMMARY.md
|
||||
│ └── [##]_[component]/
|
||||
│ └── [##].[##]_feature_[name].md
|
||||
└── 04_refactoring/
|
||||
├── baseline_metrics.md
|
||||
├── discovery/
|
||||
├── analysis/
|
||||
├── test_specs/
|
||||
├── coupling_analysis.md
|
||||
├── execution_log.md
|
||||
├── hardening/
|
||||
└── FINAL_report.md
|
||||
```
|
||||
|
||||
## Implementation Tools
|
||||
|
||||
| Tool | Type | Purpose |
|
||||
|------|------|---------|
|
||||
| `implementer` | Subagent | Implements a single feature from its spec. Launched by implement-wave. |
|
||||
| `/implement-initial` | Command | Scaffolds project skeleton from `initial_structure.md`. Run once. |
|
||||
| `/implement-wave` | Command | Detects next wave, launches parallel implementers. Repeatable. |
|
||||
| `/implement-code-review` | Command | Reviews code against specs. |
|
||||
| `/implement-black-box-tests` | Command | E2E tests via Docker consumer app. After all waves. |
|
||||
| `/implement-cicd` | Command | Validate and enhance CI/CD pipeline. |
|
||||
| `/deploy` | Command | Plan deployment strategy per environment. |
|
||||
| `/observability` | Command | Plan logging, metrics, tracing, alerting. |
|
||||
|
||||
## Standalone Mode (Reference)
|
||||
|
||||
Any skill can run in standalone mode by passing an explicit file:
|
||||
|
||||
```
|
||||
/research @my_problem.md
|
||||
/plan @my_design.md
|
||||
/decompose @some_spec.md
|
||||
/refactor @some_component.md
|
||||
```
|
||||
|
||||
Output goes to `_standalone/<topic>/` (git-ignored) instead of `_docs/`. Standalone mode relaxes guardrails — only the provided file is required; restrictions and acceptance criteria are optional.
|
||||
|
||||
Single component decompose is also supported:
|
||||
|
||||
```
|
||||
/decompose @_docs/02_plans/<topic>/components/03_parser/description.md
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: implementer
|
||||
description: |
|
||||
Implements a single feature from its spec file. Use when implementing features from _docs/02_tasks/.
|
||||
Reads the feature spec, analyzes the codebase, implements the feature with tests, and verifies acceptance criteria.
|
||||
---
|
||||
|
||||
You are a professional software developer implementing a single feature.
|
||||
|
||||
## Input
|
||||
|
||||
You receive a path to a feature spec file (e.g., `_docs/02_tasks/<topic>/[##]_[name]/[##].[##]_feature_[name].md`).
|
||||
|
||||
## Context
|
||||
|
||||
Read these files for project context:
|
||||
- `_docs/00_problem/problem.md`
|
||||
- `_docs/00_problem/restrictions.md`
|
||||
- `_docs/00_problem/acceptance_criteria.md`
|
||||
- `_docs/01_solution/solution.md`
|
||||
|
||||
## Process
|
||||
|
||||
1. Read the feature spec thoroughly — understand acceptance criteria, scope, constraints
|
||||
2. Analyze the existing codebase: conventions, patterns, related code, shared interfaces
|
||||
3. Research best implementation approaches for the tech stack if needed
|
||||
4. If the feature has a dependency on an unimplemented component, create a temporary mock
|
||||
5. Implement the feature following existing code conventions
|
||||
6. Implement error handling per the project's defined strategy
|
||||
7. Implement unit tests (use //Arrange //Act //Assert comments)
|
||||
8. Implement integration tests — analyze existing tests, add to them or create new
|
||||
9. Run all tests, fix any failures
|
||||
10. Verify the implementation satisfies every acceptance criterion from the spec
|
||||
|
||||
## After completion
|
||||
|
||||
Report:
|
||||
- What was implemented
|
||||
- Which acceptance criteria are satisfied
|
||||
- Test results (passed/failed)
|
||||
- Any mocks created for unimplemented dependencies
|
||||
- Any concerns or deviations from the spec
|
||||
|
||||
## Principles
|
||||
|
||||
- Follow SOLID, KISS, DRY
|
||||
- Dumb code, smart data
|
||||
- No unnecessary comments or logs (only exceptions)
|
||||
- Ask if requirements are ambiguous — do not assume
|
||||
@@ -0,0 +1,71 @@
|
||||
# Deployment Strategy Planning
|
||||
|
||||
## Initial data:
|
||||
- Problem description: `@_docs/00_problem/problem_description.md`
|
||||
- Restrictions: `@_docs/00_problem/restrictions.md`
|
||||
- Full Solution Description: `@_docs/01_solution/solution.md`
|
||||
- Components: `@_docs/02_components`
|
||||
- Environment Strategy: `@_docs/00_templates/environment_strategy.md`
|
||||
|
||||
## Role
|
||||
You are a DevOps/Platform engineer
|
||||
|
||||
## Task
|
||||
- Define deployment strategy for each environment
|
||||
- Plan deployment procedures and automation
|
||||
- Define rollback procedures
|
||||
- Establish deployment verification steps
|
||||
- Document manual intervention points
|
||||
|
||||
## Output
|
||||
|
||||
### Deployment Architecture
|
||||
- Infrastructure diagram (where components run)
|
||||
- Network topology
|
||||
- Load balancing strategy
|
||||
- Container/VM configuration
|
||||
|
||||
### Deployment Procedures
|
||||
|
||||
#### Staging Deployment
|
||||
- Trigger conditions
|
||||
- Pre-deployment checks
|
||||
- Deployment steps
|
||||
- Post-deployment verification
|
||||
- Smoke tests to run
|
||||
|
||||
#### Production Deployment
|
||||
- Approval workflow
|
||||
- Deployment window
|
||||
- Pre-deployment checks
|
||||
- Deployment steps (blue-green, rolling, canary)
|
||||
- Post-deployment verification
|
||||
- Smoke tests to run
|
||||
|
||||
### Rollback Procedures
|
||||
- Rollback trigger criteria
|
||||
- Rollback steps per environment
|
||||
- Data rollback considerations
|
||||
- Communication plan during rollback
|
||||
|
||||
### Health Checks
|
||||
- Liveness probe configuration
|
||||
- Readiness probe configuration
|
||||
- Custom health endpoints
|
||||
|
||||
### Deployment Checklist
|
||||
- [ ] All tests pass in CI
|
||||
- [ ] Security scan clean
|
||||
- [ ] Database migrations reviewed
|
||||
- [ ] Feature flags configured
|
||||
- [ ] Monitoring alerts configured
|
||||
- [ ] Rollback plan documented
|
||||
- [ ] Stakeholders notified
|
||||
|
||||
Store output to `_docs/02_components/deployment_strategy.md`
|
||||
|
||||
## Notes
|
||||
- Prefer automated deployments over manual
|
||||
- Zero-downtime deployments for production
|
||||
- Always have a rollback plan
|
||||
- Ask questions about infrastructure constraints
|
||||
@@ -0,0 +1,45 @@
|
||||
# Implement E2E Black-Box Tests
|
||||
|
||||
Build a separate Docker-based consumer application that exercises the main system as a black box, validating end-to-end use cases.
|
||||
|
||||
## Input
|
||||
- E2E test infrastructure spec: `_docs/02_plans/<topic>/e2e_test_infrastructure.md` (produced by plan skill Step 4b)
|
||||
|
||||
## Context
|
||||
- Problem description: `@_docs/00_problem/problem.md`
|
||||
- Acceptance criteria: `@_docs/00_problem/acceptance_criteria.md`
|
||||
- Solution: `@_docs/01_solution/solution.md`
|
||||
- Architecture: `@_docs/02_plans/<topic>/architecture.md`
|
||||
|
||||
## Role
|
||||
You are a professional QA engineer and developer
|
||||
|
||||
## Task
|
||||
- Read the E2E test infrastructure spec thoroughly
|
||||
- Build the Docker test environment:
|
||||
- Create docker-compose.yml with all services (system under test, test DB, consumer app, dependency mocks)
|
||||
- Configure networks and volumes per spec
|
||||
- Implement the consumer application:
|
||||
- Separate project/folder that communicates with the main system only through its public interfaces
|
||||
- No internal imports from the main system, no direct DB access
|
||||
- Use the tech stack and entry point defined in the spec
|
||||
- Implement each E2E test scenario from the spec:
|
||||
- Check existing E2E tests; update if a similar test already exists
|
||||
- Prepare seed data and fixtures per the test data management section
|
||||
- Implement teardown/cleanup procedures
|
||||
- Run the full E2E suite via `docker compose up`
|
||||
- If tests fail:
|
||||
- Fix issues iteratively until all pass
|
||||
- If a failure is caused by missing external data, API access, or environment config, ask the user
|
||||
- Ensure the E2E suite integrates into the CI pipeline per the spec
|
||||
- Produce a CSV test report (test ID, name, execution time, result, error message) at the output path defined in the spec
|
||||
|
||||
## Safety Rules
|
||||
- The consumer app must treat the main system as a true black box
|
||||
- Never import internal modules or access the main system's database directly
|
||||
- Docker environment must be self-contained — no host dependencies beyond Docker itself
|
||||
- If external services need mocking, implement mock/stub services as Docker containers
|
||||
|
||||
## Notes
|
||||
- Ask questions if the spec is ambiguous or incomplete
|
||||
- If `e2e_test_infrastructure.md` is missing, stop and inform the user to run the plan skill first
|
||||
@@ -0,0 +1,64 @@
|
||||
# CI/CD Pipeline Validation & Enhancement
|
||||
|
||||
## Initial data:
|
||||
- Problem description: `@_docs/00_problem/problem_description.md`
|
||||
- Restrictions: `@_docs/00_problem/restrictions.md`
|
||||
- Full Solution Description: `@_docs/01_solution/solution.md`
|
||||
- Components: `@_docs/02_components`
|
||||
- Environment Strategy: `@_docs/00_templates/environment_strategy.md`
|
||||
|
||||
## Role
|
||||
You are a DevOps engineer
|
||||
|
||||
## Task
|
||||
- Review existing CI/CD pipeline configuration
|
||||
- Validate all stages are working correctly
|
||||
- Optimize pipeline performance (parallelization, caching)
|
||||
- Ensure test coverage gates are enforced
|
||||
- Verify security scanning is properly configured
|
||||
- Add missing quality gates
|
||||
|
||||
## Checklist
|
||||
|
||||
### Pipeline Health
|
||||
- [ ] All stages execute successfully
|
||||
- [ ] Build time is acceptable (<10 min for most projects)
|
||||
- [ ] Caching is properly configured (dependencies, build artifacts)
|
||||
- [ ] Parallel execution where possible
|
||||
|
||||
### Quality Gates
|
||||
- [ ] Code coverage threshold enforced (minimum 75%)
|
||||
- [ ] Linting errors block merge
|
||||
- [ ] Security vulnerabilities block merge (critical/high)
|
||||
- [ ] All tests must pass
|
||||
|
||||
### Environment Deployments
|
||||
- [ ] Staging deployment works on merge to stage branch
|
||||
- [ ] Environment variables properly configured per environment
|
||||
- [ ] Secrets are securely managed (not in code)
|
||||
- [ ] Rollback procedure documented
|
||||
|
||||
### Monitoring
|
||||
- [ ] Build notifications configured (Slack, email, etc.)
|
||||
- [ ] Failed build alerts
|
||||
- [ ] Deployment success/failure notifications
|
||||
|
||||
## Output
|
||||
|
||||
### Pipeline Status Report
|
||||
- Current pipeline configuration summary
|
||||
- Issues found and fixes applied
|
||||
- Performance metrics (build times)
|
||||
|
||||
### Recommended Improvements
|
||||
- Short-term improvements
|
||||
- Long-term optimizations
|
||||
|
||||
### Quality Gate Configuration
|
||||
- Thresholds configured
|
||||
- Enforcement rules
|
||||
|
||||
## Notes
|
||||
- Do not break existing functionality
|
||||
- Test changes in separate branch first
|
||||
- Document any manual steps required
|
||||
@@ -0,0 +1,38 @@
|
||||
# Code Review
|
||||
|
||||
## Initial data:
|
||||
- Problem description: `@_docs/00_problem/problem_description.md`.
|
||||
- Acceptance criteria: `@_docs/00_problem/acceptance_criteria.md`.
|
||||
- Security approach: `@_docs/00_problem/security_approach.md`.
|
||||
- Full Solution Description: `@_docs/01_solution/solution.md`
|
||||
- Components: `@_docs/02_components`
|
||||
|
||||
## Role
|
||||
You are a senior software engineer performing code review
|
||||
|
||||
## Task
|
||||
- Review implemented code against component specifications
|
||||
- Check code quality: readability, maintainability, SOLID principles
|
||||
- Check error handling consistency
|
||||
- Check logging implementation
|
||||
- Check security requirements are met
|
||||
- Check test coverage is adequate
|
||||
- Identify code smells and technical debt
|
||||
|
||||
## Output
|
||||
### Issues Found
|
||||
For each issue:
|
||||
- File/Location
|
||||
- Issue type (Bug/Security/Performance/Style/Debt)
|
||||
- Description
|
||||
- Suggested fix
|
||||
- Priority (High/Medium/Low)
|
||||
|
||||
### Summary
|
||||
- Total issues by type
|
||||
- Blocking issues that must be fixed
|
||||
- Recommended improvements
|
||||
|
||||
## Notes
|
||||
- Can also use Cursor's built-in review feature
|
||||
- Focus on critical issues first
|
||||
@@ -0,0 +1,53 @@
|
||||
# Implement Initial Structure
|
||||
|
||||
## Input
|
||||
- Structure plan: `_docs/02_tasks/<topic>/initial_structure.md` (produced by decompose skill)
|
||||
|
||||
## Context
|
||||
- Problem description: `@_docs/00_problem/problem.md`
|
||||
- Restrictions: `@_docs/00_problem/restrictions.md`
|
||||
- Solution: `@_docs/01_solution/solution.md`
|
||||
|
||||
## Role
|
||||
You are a professional software architect
|
||||
|
||||
## Task
|
||||
- Read carefully the structure plan in `initial_structure.md`
|
||||
- Execute the plan — create the project skeleton:
|
||||
- DTOs and shared models
|
||||
- Component interfaces
|
||||
- Empty implementations (stubs)
|
||||
- Helpers — empty implementations or interfaces
|
||||
- Add .gitignore appropriate for the project's language/framework
|
||||
- Add .env.example with required environment variables
|
||||
- Configure CI/CD pipeline per the structure plan stages
|
||||
- Apply environment strategy (dev, staging, production) per the structure plan
|
||||
- Add database migration setup if applicable
|
||||
- Add README.md, describe the project based on the solution
|
||||
- Create test folder structure per the structure plan
|
||||
- Configure branch protection rules recommendations
|
||||
|
||||
## Example
|
||||
The structure should roughly look like this (varies by tech stack):
|
||||
- .gitignore
|
||||
- .env.example
|
||||
- .github/workflows/ (or .gitlab-ci.yml or azure-pipelines.yml)
|
||||
- api/
|
||||
- components/
|
||||
- component1_folder/
|
||||
- component2_folder/
|
||||
- db/
|
||||
- migrations/
|
||||
- helpers/
|
||||
- models/
|
||||
- tests/
|
||||
- unit/
|
||||
- integration/
|
||||
- test_data/
|
||||
|
||||
Semantically coherent components may have their own project or subfolder. Common interfaces can be in a shared layer or per-component — follow language conventions.
|
||||
|
||||
## Notes
|
||||
- Follow SOLID, KISS, DRY
|
||||
- Follow conventions of the project's programming language
|
||||
- Ask as many questions as needed
|
||||
@@ -0,0 +1,62 @@
|
||||
# Implement Next Wave
|
||||
|
||||
Identify the next batch of independent features and implement them in parallel using the implementer subagent.
|
||||
|
||||
## Prerequisites
|
||||
- Project scaffolded (`/implement-initial` completed)
|
||||
- `_docs/02_tasks/<topic>/SUMMARY.md` exists
|
||||
- `_docs/02_tasks/<topic>/cross_dependencies.md` exists
|
||||
|
||||
## Wave Sizing
|
||||
- One wave = one phase from SUMMARY.md (features whose dependencies are all satisfied)
|
||||
- Max 4 subagents run concurrently; features in the same component run sequentially
|
||||
- If a phase has more than 8 features or more than 20 complexity points, suggest splitting into smaller waves and let the user cherry-pick which features to include
|
||||
|
||||
## Task
|
||||
|
||||
1. **Read the implementation plan**
|
||||
- Read `SUMMARY.md` for the phased implementation order
|
||||
- Read `cross_dependencies.md` for the dependency graph
|
||||
|
||||
2. **Detect current progress**
|
||||
- Analyze the codebase to determine which features are already implemented
|
||||
- Match implemented code against feature specs in `_docs/02_tasks/<topic>/`
|
||||
- Identify the next incomplete wave/phase from the implementation order
|
||||
|
||||
3. **Present the wave**
|
||||
- List all features in this wave with their complexity points
|
||||
- Show which component each feature belongs to
|
||||
- Confirm total features and estimated complexity
|
||||
- If the phase exceeds 8 features or 20 complexity points, recommend splitting and let user select a subset
|
||||
- **BLOCKING**: Do NOT proceed until user confirms
|
||||
|
||||
4. **Launch parallel implementation**
|
||||
- For each feature in the wave, launch an `implementer` subagent in background
|
||||
- Each subagent receives the path to its feature spec file
|
||||
- Features within different components can run in parallel
|
||||
- Features within the same component should run sequentially to avoid file conflicts
|
||||
|
||||
5. **Monitor and report**
|
||||
- Wait for all subagents to complete
|
||||
- Collect results from each: what was implemented, test results, any issues
|
||||
- Run the full test suite
|
||||
- Report summary:
|
||||
- Features completed successfully
|
||||
- Features that failed or need manual attention
|
||||
- Test results (passed/failed/skipped)
|
||||
- Any mocks created for future-wave dependencies
|
||||
|
||||
6. **Post-wave actions**
|
||||
- Suggest: `git add . && git commit` with a wave-level commit message
|
||||
- If all features passed: "Ready for next wave. Run `/implement-wave` again."
|
||||
- If some failed: "Fix the failing features before proceeding to the next wave."
|
||||
|
||||
## Safety Rules
|
||||
- Never launch features whose dependencies are not yet implemented
|
||||
- Features within the same component run sequentially, not in parallel
|
||||
- If a subagent fails, do NOT retry automatically — report and let user decide
|
||||
- Always run tests after the wave completes, before suggesting commit
|
||||
|
||||
## Notes
|
||||
- Ask questions if the implementation order is ambiguous
|
||||
- If SUMMARY.md or cross_dependencies.md is missing, stop and inform the user to run the decompose skill first
|
||||
@@ -0,0 +1,122 @@
|
||||
# Observability Planning
|
||||
|
||||
## Initial data:
|
||||
- Problem description: `@_docs/00_problem/problem_description.md`
|
||||
- Full Solution Description: `@_docs/01_solution/solution.md`
|
||||
- Components: `@_docs/02_components`
|
||||
- Deployment Strategy: `@_docs/02_components/deployment_strategy.md`
|
||||
|
||||
## Role
|
||||
You are a Site Reliability Engineer (SRE)
|
||||
|
||||
## Task
|
||||
- Define logging strategy across all components
|
||||
- Plan metrics collection and dashboards
|
||||
- Design distributed tracing (if applicable)
|
||||
- Establish alerting rules
|
||||
- Document incident response procedures
|
||||
|
||||
## Output
|
||||
|
||||
### Logging Strategy
|
||||
|
||||
#### Log Levels
|
||||
| Level | Usage | Example |
|
||||
|-------|-------|---------|
|
||||
| ERROR | Exceptions, failures requiring attention | Database connection failed |
|
||||
| WARN | Potential issues, degraded performance | Retry attempt 2/3 |
|
||||
| INFO | Significant business events | User registered, Order placed |
|
||||
| DEBUG | Detailed diagnostic information | Request payload, Query params |
|
||||
|
||||
#### Log Format
|
||||
```json
|
||||
{
|
||||
"timestamp": "ISO8601",
|
||||
"level": "INFO",
|
||||
"service": "service-name",
|
||||
"correlation_id": "uuid",
|
||||
"message": "Event description",
|
||||
"context": {}
|
||||
}
|
||||
```
|
||||
|
||||
#### Log Storage
|
||||
- Development: Console/file
|
||||
- Staging: Centralized (ELK, CloudWatch, etc.)
|
||||
- Production: Centralized with retention policy
|
||||
|
||||
### Metrics
|
||||
|
||||
#### System Metrics
|
||||
- CPU usage
|
||||
- Memory usage
|
||||
- Disk I/O
|
||||
- Network I/O
|
||||
|
||||
#### Application Metrics
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| request_count | Counter | Total requests |
|
||||
| request_duration | Histogram | Response time |
|
||||
| error_count | Counter | Failed requests |
|
||||
| active_connections | Gauge | Current connections |
|
||||
|
||||
#### Business Metrics
|
||||
- [Define based on acceptance criteria]
|
||||
|
||||
### Distributed Tracing
|
||||
|
||||
#### Trace Context
|
||||
- Correlation ID propagation
|
||||
- Span naming conventions
|
||||
- Sampling strategy
|
||||
|
||||
#### Integration Points
|
||||
- HTTP headers
|
||||
- Message queue metadata
|
||||
- Database query tagging
|
||||
|
||||
### Alerting
|
||||
|
||||
#### Alert Categories
|
||||
| Severity | Response Time | Examples |
|
||||
|----------|---------------|----------|
|
||||
| Critical | 5 min | Service down, Data loss |
|
||||
| High | 30 min | High error rate, Performance degradation |
|
||||
| Medium | 4 hours | Elevated latency, Disk usage high |
|
||||
| Low | Next business day | Non-critical warnings |
|
||||
|
||||
#### Alert Rules
|
||||
```yaml
|
||||
alerts:
|
||||
- name: high_error_rate
|
||||
condition: error_rate > 5%
|
||||
duration: 5m
|
||||
severity: high
|
||||
|
||||
- name: service_down
|
||||
condition: health_check_failed
|
||||
duration: 1m
|
||||
severity: critical
|
||||
```
|
||||
|
||||
### Dashboards
|
||||
|
||||
#### Operations Dashboard
|
||||
- Service health status
|
||||
- Request rate and error rate
|
||||
- Response time percentiles
|
||||
- Resource utilization
|
||||
|
||||
#### Business Dashboard
|
||||
- Key business metrics
|
||||
- User activity
|
||||
- Transaction volumes
|
||||
|
||||
Store output to `_docs/02_components/observability_plan.md`
|
||||
|
||||
## Notes
|
||||
- Follow the principle: "If it's not monitored, it's not in production"
|
||||
- Balance verbosity with cost
|
||||
- Ensure PII is not logged
|
||||
- Plan for log rotation and retention
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Coding rules
|
||||
alwaysApply: true
|
||||
---
|
||||
# Coding preferences
|
||||
- Always prefer simple solution
|
||||
- Generate concise code
|
||||
- Do not put comments in the code
|
||||
- Do not put logs unless it is an exception, or was asked specifically
|
||||
- Do not put code annotations unless it was asked specifically
|
||||
- Write code that takes into account the different environments: development, production
|
||||
- You are careful to make changes that are requested or you are confident the changes are well understood and related to the change being requested
|
||||
- Mocking data is needed only for tests, never mock data for dev or prod env
|
||||
- When you add new libraries or dependencies make sure you are using the same version of it as other parts of the code
|
||||
|
||||
- Focus on the areas of code relevant to the task
|
||||
- Do not touch code that is unrelated to the task
|
||||
- Always think about what other methods and areas of code might be affected by the code changes
|
||||
- When you think you are done with changes, run tests and make sure they are not broken
|
||||
- Do not rename any databases or tables or table columns without confirmation. Avoid such renaming if possible.
|
||||
- Do not create diagrams unless I ask explicitly
|
||||
- Make sure we don't commit binaries, create and keep .gitignore up to date and delete binaries after you are done with the task
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: Techstack
|
||||
alwaysApply: true
|
||||
---
|
||||
# Tech Stack
|
||||
- Using Postgres database
|
||||
- Depending on task, for backend prefer .Net or Python. Could be RUST for more specific things.
|
||||
- For Frontend, use React with Tailwind css (or even plain css, if it is a simple project)
|
||||
- document api with OpenAPI
|
||||
@@ -0,0 +1,281 @@
|
||||
---
|
||||
name: decompose
|
||||
description: |
|
||||
Decompose planned components into atomic implementable features with bootstrap structure plan.
|
||||
4-step workflow: bootstrap structure plan, feature decomposition, cross-component verification, and Jira task creation.
|
||||
Supports project mode (_docs/ structure), single component mode, and standalone mode (@file.md).
|
||||
Trigger phrases:
|
||||
- "decompose", "decompose features", "feature decomposition"
|
||||
- "task decomposition", "break down components"
|
||||
- "prepare for implementation"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Feature Decomposition
|
||||
|
||||
Decompose planned components into atomic, implementable feature specs with a bootstrap structure plan through a systematic workflow.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Atomic features**: each feature does one thing; if it exceeds 5 complexity points, split it
|
||||
- **Behavioral specs, not implementation plans**: describe what the system should do, not how to build it
|
||||
- **Save immediately**: write artifacts to disk after each component; never accumulate unsaved work
|
||||
- **Ask, don't assume**: when requirements are ambiguous, ask the user before proceeding
|
||||
- **Plan, don't code**: this workflow produces documents and Jira tasks, never implementation code
|
||||
|
||||
## Context Resolution
|
||||
|
||||
Determine the operating mode based on invocation before any other logic runs.
|
||||
|
||||
**Full project mode** (no explicit input file provided):
|
||||
- PLANS_DIR: `_docs/02_plans/`
|
||||
- TASKS_DIR: `_docs/02_tasks/`
|
||||
- Reads from: `_docs/00_problem/`, `_docs/01_solution/`, PLANS_DIR
|
||||
- Runs Step 1 (bootstrap) + Step 2 (all components) + Step 3 (cross-verification) + Step 4 (Jira)
|
||||
|
||||
**Single component mode** (provided file is within `_docs/02_plans/` and inside a `components/` subdirectory):
|
||||
- PLANS_DIR: `_docs/02_plans/`
|
||||
- TASKS_DIR: `_docs/02_tasks/`
|
||||
- Derive `<topic>`, component number, and component name from the file path
|
||||
- Ask user for the parent Epic ID
|
||||
- Runs Step 2 (that component only) + Step 4 (Jira)
|
||||
- Overwrites existing feature files in that component's TASKS_DIR subdirectory
|
||||
|
||||
**Standalone mode** (explicit input file provided, not within `_docs/02_plans/`):
|
||||
- INPUT_FILE: the provided file (treated as a component spec)
|
||||
- Derive `<topic>` from the input filename (without extension)
|
||||
- TASKS_DIR: `_standalone/<topic>/tasks/`
|
||||
- Guardrails relaxed: only INPUT_FILE must exist and be non-empty
|
||||
- Ask user for the parent Epic ID
|
||||
- Runs Step 2 (that component only) + Step 4 (Jira)
|
||||
|
||||
Announce the detected mode and resolved paths to the user before proceeding.
|
||||
|
||||
## Input Specification
|
||||
|
||||
### Required Files
|
||||
|
||||
**Full project mode:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `_docs/00_problem/problem.md` | Problem description and context |
|
||||
| `_docs/00_problem/restrictions.md` | Constraints and limitations (if available) |
|
||||
| `_docs/00_problem/acceptance_criteria.md` | Measurable acceptance criteria (if available) |
|
||||
| `_docs/01_solution/solution.md` | Finalized solution |
|
||||
| `PLANS_DIR/<topic>/architecture.md` | Architecture from plan skill |
|
||||
| `PLANS_DIR/<topic>/system-flows.md` | System flows from plan skill |
|
||||
| `PLANS_DIR/<topic>/components/[##]_[name]/description.md` | Component specs from plan skill |
|
||||
|
||||
**Single component mode:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| The provided component `description.md` | Component spec to decompose |
|
||||
| Corresponding `tests.md` in the same directory (if available) | Test specs for context |
|
||||
|
||||
**Standalone mode:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| INPUT_FILE (the provided file) | Component spec to decompose |
|
||||
|
||||
### Prerequisite Checks (BLOCKING)
|
||||
|
||||
**Full project mode:**
|
||||
1. At least one `<topic>/` directory exists under PLANS_DIR with `architecture.md` and `components/` — **STOP if missing**
|
||||
2. If multiple topics exist, ask user which one to decompose
|
||||
3. Create TASKS_DIR if it does not exist
|
||||
4. If `TASKS_DIR/<topic>/` already exists, ask user: **resume from last checkpoint or start fresh?**
|
||||
|
||||
**Single component mode:**
|
||||
1. The provided component file exists and is non-empty — **STOP if missing**
|
||||
2. Create the component's subdirectory under TASKS_DIR if it does not exist
|
||||
|
||||
**Standalone mode:**
|
||||
1. INPUT_FILE exists and is non-empty — **STOP if missing**
|
||||
2. Create TASKS_DIR if it does not exist
|
||||
|
||||
## Artifact Management
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
TASKS_DIR/<topic>/
|
||||
├── initial_structure.md (Step 1, full mode only)
|
||||
├── cross_dependencies.md (Step 3, full mode only)
|
||||
├── SUMMARY.md (final)
|
||||
├── [##]_[component_name]/
|
||||
│ ├── [##].[##]_feature_[feature_name].md
|
||||
│ ├── [##].[##]_feature_[feature_name].md
|
||||
│ └── ...
|
||||
├── [##]_[component_name]/
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Save Timing
|
||||
|
||||
| Step | Save immediately after | Filename |
|
||||
|------|------------------------|----------|
|
||||
| Step 1 | Bootstrap structure plan complete | `initial_structure.md` |
|
||||
| Step 2 | Each component decomposed | `[##]_[name]/[##].[##]_feature_[feature_name].md` |
|
||||
| Step 3 | Cross-component verification complete | `cross_dependencies.md` |
|
||||
| Step 4 | Jira tasks created | Jira via MCP |
|
||||
| Final | All steps complete | `SUMMARY.md` |
|
||||
|
||||
### Resumability
|
||||
|
||||
If `TASKS_DIR/<topic>/` already contains artifacts:
|
||||
|
||||
1. List existing files and match them to the save timing table
|
||||
2. Identify the last completed component based on which feature files exist
|
||||
3. Resume from the next incomplete component
|
||||
4. Inform the user which components are being skipped
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
At the start of execution, create a TodoWrite with all applicable steps. Update status as each step/component completes.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Bootstrap Structure Plan (full project mode only)
|
||||
|
||||
**Role**: Professional software architect
|
||||
**Goal**: Produce `initial_structure.md` describing the project skeleton for implementation
|
||||
**Constraints**: This is a plan document, not code. The `implement-initial` command executes it.
|
||||
|
||||
1. Read architecture.md, all component specs, and system-flows.md from PLANS_DIR
|
||||
2. Read problem, solution, and restrictions from `_docs/00_problem/` and `_docs/01_solution/`
|
||||
3. Research best implementation patterns for the identified tech stack
|
||||
4. Document the structure plan using `templates/initial-structure.md`
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] All components have corresponding folders in the layout
|
||||
- [ ] All inter-component interfaces have DTOs defined
|
||||
- [ ] CI/CD stages cover build, lint, test, security, deploy
|
||||
- [ ] Environment strategy covers dev, staging, production
|
||||
- [ ] Test structure includes unit and integration test locations
|
||||
|
||||
**Save action**: Write `initial_structure.md`
|
||||
|
||||
**BLOCKING**: Present structure plan summary to user. Do NOT proceed until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Feature Decomposition (all modes)
|
||||
|
||||
**Role**: Professional software architect
|
||||
**Goal**: Decompose each component into atomic, implementable feature specs
|
||||
**Constraints**: Behavioral specs only — describe what, not how. No implementation code.
|
||||
|
||||
For each component (or the single provided component):
|
||||
|
||||
1. Read the component's `description.md` and `tests.md` (if available)
|
||||
2. Decompose into atomic features; create only 1 feature if the component is simple or atomic
|
||||
3. Split into multiple features only when it is necessary and would be easier to implement
|
||||
4. Do not create features of other components — only features of the current component
|
||||
5. Each feature should be atomic, containing 0 APIs or a list of semantically connected APIs
|
||||
6. Write each feature spec using `templates/feature-spec.md`
|
||||
7. Estimate complexity per feature (1, 2, 3, 5 points); no feature should exceed 5 points — split if it does
|
||||
8. Note feature dependencies (within component and cross-component)
|
||||
|
||||
**Self-verification** (per component):
|
||||
- [ ] Every feature is atomic (single concern)
|
||||
- [ ] No feature exceeds 5 complexity points
|
||||
- [ ] Feature dependencies are noted
|
||||
- [ ] Features cover all interfaces defined in the component spec
|
||||
- [ ] No features duplicate work from other components
|
||||
|
||||
**Save action**: Write each `[##]_[name]/[##].[##]_feature_[feature_name].md`
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Cross-Component Verification (full project mode only)
|
||||
|
||||
**Role**: Professional software architect and analyst
|
||||
**Goal**: Verify feature consistency across all components
|
||||
**Constraints**: Review step — fix gaps found, do not add new features
|
||||
|
||||
1. Verify feature dependencies across all components are consistent
|
||||
2. Check no gaps: every interface in architecture.md has features covering it
|
||||
3. Check no overlaps: features don't duplicate work across components
|
||||
4. Produce dependency matrix showing cross-component feature dependencies
|
||||
5. Determine recommended implementation order based on dependencies
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Every architecture interface is covered by at least one feature
|
||||
- [ ] No circular feature dependencies across components
|
||||
- [ ] Cross-component dependencies are explicitly noted in affected feature specs
|
||||
|
||||
**Save action**: Write `cross_dependencies.md`
|
||||
|
||||
**BLOCKING**: Present cross-component summary to user. Do NOT proceed until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Jira Tasks (all modes)
|
||||
|
||||
**Role**: Professional product manager
|
||||
**Goal**: Create Jira tasks from feature specs under the appropriate parent epics
|
||||
**Constraints**: Be concise — fewer words with the same meaning is better
|
||||
|
||||
1. For each feature spec, create a Jira task following the parsing rules and field mapping from `gen_jira_task_and_branch.md` (skip branch creation and file renaming — those happen during implementation)
|
||||
2. In full mode: search Jira for epics matching component names/labels to find parent epic IDs
|
||||
3. In single component mode: use the Epic ID obtained during context resolution
|
||||
4. In standalone mode: use the Epic ID obtained during context resolution
|
||||
5. Do NOT create git branches or rename files — that happens during implementation
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Every feature has a corresponding Jira task
|
||||
- [ ] Every task is linked to the correct parent epic
|
||||
- [ ] Task descriptions match feature spec content
|
||||
|
||||
**Save action**: Jira tasks created via MCP
|
||||
|
||||
---
|
||||
|
||||
## Summary Report
|
||||
|
||||
After all steps complete, write `SUMMARY.md` using `templates/summary.md` as structure.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- **Coding during decomposition**: this workflow produces specs, never code
|
||||
- **Over-splitting**: don't create many features if the component is simple — 1 feature is fine
|
||||
- **Features exceeding 5 points**: split them; no feature should be too complex for a single task
|
||||
- **Cross-component features**: each feature belongs to exactly one component
|
||||
- **Skipping BLOCKING gates**: never proceed past a BLOCKING marker without user confirmation
|
||||
- **Creating git branches**: branch creation is an implementation concern, not a decomposition one
|
||||
|
||||
## Escalation Rules
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Ambiguous component boundaries | ASK user |
|
||||
| Feature complexity exceeds 5 points after splitting | ASK user |
|
||||
| Missing component specs in PLANS_DIR | ASK user |
|
||||
| Cross-component dependency conflict | ASK user |
|
||||
| Jira epic not found for a component | ASK user for Epic ID |
|
||||
| Component naming | PROCEED, confirm at next BLOCKING gate |
|
||||
|
||||
## Methodology Quick Reference
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Feature Decomposition (4-Step Method) │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ CONTEXT: Resolve mode (full / single component / standalone) │
|
||||
│ 1. Bootstrap Structure → initial_structure.md (full only) │
|
||||
│ [BLOCKING: user confirms structure] │
|
||||
│ 2. Feature Decompose → [##]_[name]/[##].[##]_feature_* │
|
||||
│ 3. Cross-Verification → cross_dependencies.md (full only) │
|
||||
│ [BLOCKING: user confirms dependencies] │
|
||||
│ 4. Jira Tasks → Jira via MCP │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ Summary → SUMMARY.md │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ Principles: Atomic features · Behavioral specs · Save now │
|
||||
│ Ask don't assume · Plan don't code │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# Feature Specification Template
|
||||
|
||||
Create a focused behavioral specification that describes **what** the system should do, not **how** it should be built.
|
||||
Save as `TASKS_DIR/<topic>/[##]_[component_name]/[##].[##]_feature_[feature_name].md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# [Feature Name]
|
||||
|
||||
**Status**: Draft | **Date**: [YYYY-MM-DD] | **Feature**: [Brief Feature Description]
|
||||
**Complexity**: [1|2|3|5] points
|
||||
**Dependencies**: [List dependent features or "None"]
|
||||
**Component**: [##]_[component_name]
|
||||
|
||||
## Problem
|
||||
|
||||
Clear, concise statement of the problem users are facing.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Measurable or observable goal 1
|
||||
- Measurable or observable goal 2
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- What's in scope for this feature
|
||||
|
||||
### Excluded
|
||||
- Explicitly what's NOT in scope
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: [Title]**
|
||||
Given [precondition]
|
||||
When [action]
|
||||
Then [expected result]
|
||||
|
||||
**AC-2: [Title]**
|
||||
Given [precondition]
|
||||
When [action]
|
||||
Then [expected result]
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- [requirement if relevant]
|
||||
|
||||
**Compatibility**
|
||||
- [requirement if relevant]
|
||||
|
||||
**Reliability**
|
||||
- [requirement if relevant]
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|-------------|-----------------|
|
||||
| AC-1 | [test subject] | [expected result] |
|
||||
|
||||
## Integration Tests
|
||||
|
||||
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|
||||
|--------|------------------------|-------------|-------------------|----------------|
|
||||
| AC-1 | [setup] | [test subject] | [expected behavior] | [NFR if any] |
|
||||
|
||||
## Constraints
|
||||
|
||||
- [Architectural pattern constraint if critical]
|
||||
- [Technical limitation]
|
||||
- [Integration requirement]
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: [Title]**
|
||||
- *Risk*: [Description]
|
||||
- *Mitigation*: [Approach]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complexity Points Guide
|
||||
|
||||
- 1 point: Trivial, self-contained, no dependencies
|
||||
- 2 points: Non-trivial, low complexity, minimal coordination
|
||||
- 3 points: Multi-step, moderate complexity, potential alignment needed
|
||||
- 5 points: Difficult, interconnected logic, medium-high risk
|
||||
- 8 points: Too complex — split into smaller features
|
||||
|
||||
## Output Guidelines
|
||||
|
||||
**DO:**
|
||||
- Focus on behavior and user experience
|
||||
- Use clear, simple language
|
||||
- Keep acceptance criteria testable (Gherkin format)
|
||||
- Include realistic scope boundaries
|
||||
- Write from the user's perspective
|
||||
- Include complexity estimation
|
||||
- Note dependencies on other features
|
||||
|
||||
**DON'T:**
|
||||
- Include implementation details (file paths, classes, methods)
|
||||
- Prescribe technical solutions or libraries
|
||||
- Add architectural diagrams or code examples
|
||||
- Specify exact API endpoints or data structures
|
||||
- Include step-by-step implementation instructions
|
||||
- Add "how to build" guidance
|
||||
@@ -0,0 +1,113 @@
|
||||
# Initial Structure Plan Template
|
||||
|
||||
Use this template for the bootstrap structure plan. Save as `TASKS_DIR/<topic>/initial_structure.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# Initial Project Structure Plan
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Tech Stack**: [language, framework, database, etc.]
|
||||
**Source**: architecture.md, component specs from _docs/02_plans/<topic>/
|
||||
|
||||
## Project Folder Layout
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── [folder structure based on tech stack and components]
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Layout Rationale
|
||||
|
||||
[Brief explanation of why this structure was chosen — language conventions, framework patterns, etc.]
|
||||
|
||||
## DTOs and Interfaces
|
||||
|
||||
### Shared DTOs
|
||||
|
||||
| DTO Name | Used By Components | Fields Summary |
|
||||
|----------|-------------------|---------------|
|
||||
| [name] | [component list] | [key fields] |
|
||||
|
||||
### Component Interfaces
|
||||
|
||||
| Component | Interface | Methods | Exposed To |
|
||||
|-----------|-----------|---------|-----------|
|
||||
| [##]_[name] | [InterfaceName] | [method list] | [consumers] |
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
| Stage | Purpose | Trigger |
|
||||
|-------|---------|---------|
|
||||
| Build | Compile/bundle the application | Every push |
|
||||
| Lint / Static Analysis | Code quality and style checks | Every push |
|
||||
| Unit Tests | Run unit test suite | Every push |
|
||||
| Integration Tests | Run integration test suite | Every push |
|
||||
| Security Scan | SAST / dependency check | Every push |
|
||||
| Deploy to Staging | Deploy to staging environment | Merge to staging branch |
|
||||
|
||||
### Pipeline Configuration Notes
|
||||
|
||||
[Framework-specific notes: CI tool, runners, caching, parallelism, etc.]
|
||||
|
||||
## Environment Strategy
|
||||
|
||||
| Environment | Purpose | Configuration Notes |
|
||||
|-------------|---------|-------------------|
|
||||
| Development | Local development | [local DB, mock services, debug flags] |
|
||||
| Staging | Pre-production testing | [staging DB, staging services, production-like config] |
|
||||
| Production | Live system | [production DB, real services, optimized config] |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Dev | Staging | Production | Description |
|
||||
|----------|-----|---------|------------|-------------|
|
||||
| [VAR_NAME] | [value/source] | [value/source] | [value/source] | [purpose] |
|
||||
|
||||
## Database Migration Approach
|
||||
|
||||
**Migration tool**: [tool name]
|
||||
**Strategy**: [migration strategy — e.g., versioned scripts, ORM migrations]
|
||||
|
||||
### Initial Schema
|
||||
|
||||
[Key tables/collections that need to be created, referencing component data access patterns]
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/
|
||||
│ ├── [component_1]/
|
||||
│ ├── [component_2]/
|
||||
│ └── ...
|
||||
├── integration/
|
||||
│ ├── test_data/
|
||||
│ └── [test files]
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Test Configuration Notes
|
||||
|
||||
[Test runner, fixtures, test data management, isolation strategy]
|
||||
|
||||
## Implementation Order
|
||||
|
||||
| Order | Component | Reason |
|
||||
|-------|-----------|--------|
|
||||
| 1 | [##]_[name] | [why first — foundational, no dependencies] |
|
||||
| 2 | [##]_[name] | [depends on #1] |
|
||||
| ... | ... | ... |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidance Notes
|
||||
|
||||
- This is a PLAN document, not code. The `3.05_implement_initial_structure` command executes it.
|
||||
- Focus on structure and organization decisions, not implementation details.
|
||||
- Reference component specs for interface and DTO details — don't repeat everything.
|
||||
- The folder layout should follow conventions of the identified tech stack.
|
||||
- Environment strategy should account for secrets management and configuration.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Decomposition Summary Template
|
||||
|
||||
Use this template after all steps complete. Save as `TASKS_DIR/<topic>/SUMMARY.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# Decomposition Summary
|
||||
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Topic**: [topic name]
|
||||
**Total Components**: [N]
|
||||
**Total Features**: [N]
|
||||
**Total Complexity Points**: [N]
|
||||
|
||||
## Component Breakdown
|
||||
|
||||
| # | Component | Features | Total Points | Jira Epic |
|
||||
|---|-----------|----------|-------------|-----------|
|
||||
| 01 | [name] | [count] | [sum] | [EPIC-ID] |
|
||||
| 02 | [name] | [count] | [sum] | [EPIC-ID] |
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
## Feature List
|
||||
|
||||
| Component | Feature | Complexity | Jira Task | Dependencies |
|
||||
|-----------|---------|-----------|-----------|-------------|
|
||||
| [##]_[name] | [##].[##]_feature_[name] | [points] | [TASK-ID] | [deps or "None"] |
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Recommended sequence based on dependency analysis:
|
||||
|
||||
| Phase | Components / Features | Rationale |
|
||||
|-------|----------------------|-----------|
|
||||
| 1 | [list] | [foundational, no dependencies] |
|
||||
| 2 | [list] | [depends on phase 1] |
|
||||
| 3 | [list] | [depends on phase 1-2] |
|
||||
| ... | ... | ... |
|
||||
|
||||
### Parallelization Opportunities
|
||||
|
||||
[Features/components that can be implemented concurrently within each phase]
|
||||
|
||||
## Cross-Component Dependencies
|
||||
|
||||
| From (Feature) | To (Feature) | Dependency Type |
|
||||
|----------------|-------------|-----------------|
|
||||
| [comp.feature] | [comp.feature] | [data / API / event] |
|
||||
| ... | ... | ... |
|
||||
|
||||
## Artifacts Produced
|
||||
|
||||
- `initial_structure.md` — project skeleton plan
|
||||
- `cross_dependencies.md` — dependency matrix
|
||||
- `[##]_[name]/[##].[##]_feature_*.md` — feature specs per component
|
||||
- Jira tasks created under respective epics
|
||||
```
|
||||
@@ -0,0 +1,393 @@
|
||||
---
|
||||
name: plan
|
||||
description: |
|
||||
Decompose a solution into architecture, system flows, components, tests, and Jira epics.
|
||||
Systematic 5-step planning workflow with BLOCKING gates, self-verification, and structured artifact management.
|
||||
Supports project mode (_docs/ + _docs/02_plans/ structure) and standalone mode (@file.md).
|
||||
Trigger phrases:
|
||||
- "plan", "decompose solution", "architecture planning"
|
||||
- "break down the solution", "create planning documents"
|
||||
- "component decomposition", "solution analysis"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Solution Planning
|
||||
|
||||
Decompose a problem and solution into architecture, system flows, components, tests, and Jira epics through a systematic 5-step workflow.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Single Responsibility**: each component does one thing well; do not spread related logic across components
|
||||
- **Dumb code, smart data**: keep logic simple, push complexity into data structures and configuration
|
||||
- **Save immediately**: write artifacts to disk after each step; never accumulate unsaved work
|
||||
- **Ask, don't assume**: when requirements are ambiguous, ask the user before proceeding
|
||||
- **Plan, don't code**: this workflow produces documents and specs, never implementation code
|
||||
|
||||
## Context Resolution
|
||||
|
||||
Determine the operating mode based on invocation before any other logic runs.
|
||||
|
||||
**Project mode** (no explicit input file provided):
|
||||
- PROBLEM_FILE: `_docs/00_problem/problem.md`
|
||||
- SOLUTION_FILE: `_docs/01_solution/solution.md`
|
||||
- PLANS_DIR: `_docs/02_plans/`
|
||||
- All existing guardrails apply as-is.
|
||||
|
||||
**Standalone mode** (explicit input file provided, e.g. `/plan @some_doc.md`):
|
||||
- INPUT_FILE: the provided file (treated as combined problem + solution context)
|
||||
- Derive `<topic>` from the input filename (without extension)
|
||||
- PLANS_DIR: `_standalone/<topic>/plans/`
|
||||
- Guardrails relaxed: only INPUT_FILE must exist and be non-empty
|
||||
- `acceptance_criteria.md` and `restrictions.md` are optional — warn if absent
|
||||
|
||||
Announce the detected mode and resolved paths to the user before proceeding.
|
||||
|
||||
## Input Specification
|
||||
|
||||
### Required Files
|
||||
|
||||
**Project mode:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| PROBLEM_FILE (`_docs/00_problem/problem.md`) | Problem description and context |
|
||||
| `_docs/00_problem/input_data/` | Reference data examples (if available) |
|
||||
| `_docs/00_problem/restrictions.md` | Constraints and limitations (if available) |
|
||||
| `_docs/00_problem/acceptance_criteria.md` | Measurable acceptance criteria (if available) |
|
||||
| SOLUTION_FILE (`_docs/01_solution/solution.md`) | Solution draft to decompose |
|
||||
|
||||
**Standalone mode:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| INPUT_FILE (the provided file) | Combined problem + solution context |
|
||||
|
||||
### Prerequisite Checks (BLOCKING)
|
||||
|
||||
**Project mode:**
|
||||
1. PROBLEM_FILE exists and is non-empty — **STOP if missing**
|
||||
2. SOLUTION_FILE exists and is non-empty — **STOP if missing**
|
||||
3. Create PLANS_DIR if it does not exist
|
||||
4. If `PLANS_DIR/<topic>/` already exists, ask user: **resume from last checkpoint or start fresh?**
|
||||
|
||||
**Standalone mode:**
|
||||
1. INPUT_FILE exists and is non-empty — **STOP if missing**
|
||||
2. Warn if no `restrictions.md` or `acceptance_criteria.md` provided alongside INPUT_FILE
|
||||
3. Create PLANS_DIR if it does not exist
|
||||
4. If `PLANS_DIR/<topic>/` already exists, ask user: **resume from last checkpoint or start fresh?**
|
||||
|
||||
## Artifact Management
|
||||
|
||||
### Directory Structure
|
||||
|
||||
At the start of planning, create a topic-named working directory under PLANS_DIR:
|
||||
|
||||
```
|
||||
PLANS_DIR/<topic>/
|
||||
├── architecture.md
|
||||
├── system-flows.md
|
||||
├── risk_mitigations.md
|
||||
├── risk_mitigations_02.md (iterative, ## as sequence)
|
||||
├── components/
|
||||
│ ├── 01_[name]/
|
||||
│ │ ├── description.md
|
||||
│ │ └── tests.md
|
||||
│ ├── 02_[name]/
|
||||
│ │ ├── description.md
|
||||
│ │ └── tests.md
|
||||
│ └── ...
|
||||
├── common-helpers/
|
||||
│ ├── 01_helper_[name]/
|
||||
│ ├── 02_helper_[name]/
|
||||
│ └── ...
|
||||
├── e2e_test_infrastructure.md
|
||||
├── diagrams/
|
||||
│ ├── components.drawio
|
||||
│ └── flows/
|
||||
│ ├── flow_[name].md (Mermaid)
|
||||
│ └── ...
|
||||
└── FINAL_report.md
|
||||
```
|
||||
|
||||
### Save Timing
|
||||
|
||||
| Step | Save immediately after | Filename |
|
||||
|------|------------------------|----------|
|
||||
| Step 1 | Architecture analysis complete | `architecture.md` |
|
||||
| Step 1 | System flows documented | `system-flows.md` |
|
||||
| Step 2 | Each component analyzed | `components/[##]_[name]/description.md` |
|
||||
| Step 2 | Common helpers generated | `common-helpers/[##]_helper_[name].md` |
|
||||
| Step 2 | Diagrams generated | `diagrams/` |
|
||||
| Step 3 | Risk assessment complete | `risk_mitigations.md` |
|
||||
| Step 4 | Tests written per component | `components/[##]_[name]/tests.md` |
|
||||
| Step 4b | E2E test infrastructure spec | `e2e_test_infrastructure.md` |
|
||||
| Step 5 | Epics created in Jira | Jira via MCP |
|
||||
| Final | All steps complete | `FINAL_report.md` |
|
||||
|
||||
### Save Principles
|
||||
|
||||
1. **Save immediately**: write to disk as soon as a step completes; do not wait until the end
|
||||
2. **Incremental updates**: same file can be updated multiple times; append or replace
|
||||
3. **Preserve process**: keep all intermediate files even after integration into final report
|
||||
4. **Enable recovery**: if interrupted, resume from the last saved artifact (see Resumability)
|
||||
|
||||
### Resumability
|
||||
|
||||
If `PLANS_DIR/<topic>/` already contains artifacts:
|
||||
|
||||
1. List existing files and match them to the save timing table above
|
||||
2. Identify the last completed step based on which artifacts exist
|
||||
3. Resume from the next incomplete step
|
||||
4. Inform the user which steps are being skipped
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
At the start of execution, create a TodoWrite with all steps (1 through 5, including 4b). Update status as each step completes.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Solution Analysis
|
||||
|
||||
**Role**: Professional software architect
|
||||
**Goal**: Produce `architecture.md` and `system-flows.md` from the solution draft
|
||||
**Constraints**: No code, no component-level detail yet; focus on system-level view
|
||||
|
||||
1. Read all input files thoroughly
|
||||
2. Research unknown or questionable topics via internet; ask user about ambiguities
|
||||
3. Document architecture using `templates/architecture.md` as structure
|
||||
4. Document system flows using `templates/system-flows.md` as structure
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Architecture covers all capabilities mentioned in solution.md
|
||||
- [ ] System flows cover all main user/system interactions
|
||||
- [ ] No contradictions with problem.md or restrictions.md
|
||||
- [ ] Technology choices are justified
|
||||
|
||||
**Save action**: Write `architecture.md` and `system-flows.md`
|
||||
|
||||
**BLOCKING**: Present architecture summary to user. Do NOT proceed until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Component Decomposition
|
||||
|
||||
**Role**: Professional software architect
|
||||
**Goal**: Decompose the architecture into components with detailed specs
|
||||
**Constraints**: No code; only names, interfaces, inputs/outputs. Follow SRP strictly.
|
||||
|
||||
1. Identify components from the architecture; think about separation, reusability, and communication patterns
|
||||
2. If additional components are needed (data preparation, shared helpers), create them
|
||||
3. For each component, write a spec using `templates/component-spec.md` as structure
|
||||
4. Generate diagrams:
|
||||
- draw.io component diagram showing relations (minimize line intersections, group semantically coherent components, place external users near their components)
|
||||
- Mermaid flowchart per main control flow
|
||||
5. Components can share and reuse common logic, same for multiple components. Hence for such occurences common-helpers folder is specified.
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Each component has a single, clear responsibility
|
||||
- [ ] No functionality is spread across multiple components
|
||||
- [ ] All inter-component interfaces are defined (who calls whom, with what)
|
||||
- [ ] Component dependency graph has no circular dependencies
|
||||
- [ ] All components from architecture.md are accounted for
|
||||
|
||||
**Save action**: Write:
|
||||
- each component `components/[##]_[name]/description.md`
|
||||
- comomon helper `common-helpers/[##]_helper_[name].md`
|
||||
- diagrams `diagrams/`
|
||||
|
||||
**BLOCKING**: Present component list with one-line summaries to user. Do NOT proceed until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Architecture Review & Risk Assessment
|
||||
|
||||
**Role**: Professional software architect and analyst
|
||||
**Goal**: Validate all artifacts for consistency, then identify and mitigate risks
|
||||
**Constraints**: This is a review step — fix problems found, do not add new features
|
||||
|
||||
#### 3a. Evaluator Pass (re-read ALL artifacts)
|
||||
|
||||
Review checklist:
|
||||
- [ ] All components follow Single Responsibility Principle
|
||||
- [ ] All components follow dumb code / smart data principle
|
||||
- [ ] Inter-component interfaces are consistent (caller's output matches callee's input)
|
||||
- [ ] No circular dependencies in the dependency graph
|
||||
- [ ] No missing interactions between components
|
||||
- [ ] No over-engineering — is there a simpler decomposition?
|
||||
- [ ] Security considerations addressed in component design
|
||||
- [ ] Performance bottlenecks identified
|
||||
- [ ] API contracts are consistent across components
|
||||
|
||||
Fix any issues found before proceeding to risk identification.
|
||||
|
||||
#### 3b. Risk Identification
|
||||
|
||||
1. Identify technical and project risks
|
||||
2. Assess probability and impact using `templates/risk-register.md`
|
||||
3. Define mitigation strategies
|
||||
4. Apply mitigations to architecture, flows, and component documents where applicable
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Every High/Critical risk has a concrete mitigation strategy
|
||||
- [ ] Mitigations are reflected in the relevant component or architecture docs
|
||||
- [ ] No new risks introduced by the mitigations themselves
|
||||
|
||||
**Save action**: Write `risk_mitigations.md`
|
||||
|
||||
**BLOCKING**: Present risk summary to user. Ask whether assessment is sufficient.
|
||||
|
||||
**Iterative**: If user requests another round, repeat Step 3 and write `risk_mitigations_##.md` (## as sequence number). Continue until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Test Specifications
|
||||
|
||||
**Role**: Professional Quality Assurance Engineer
|
||||
**Goal**: Write test specs for each component achieving minimum 75% acceptance criteria coverage
|
||||
**Constraints**: Test specs only — no test code. Each test must trace to an acceptance criterion.
|
||||
|
||||
1. For each component, write tests using `templates/test-spec.md` as structure
|
||||
2. Cover all 4 types: integration, performance, security, acceptance
|
||||
3. Include test data management (setup, teardown, isolation)
|
||||
4. Verify traceability: every acceptance criterion from `acceptance_criteria.md` must be covered by at least one test
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Every acceptance criterion has at least one test covering it
|
||||
- [ ] Test inputs are realistic and well-defined
|
||||
- [ ] Expected results are specific and measurable
|
||||
- [ ] No component is left without tests
|
||||
|
||||
**Save action**: Write each `components/[##]_[name]/tests.md`
|
||||
|
||||
---
|
||||
|
||||
### Step 4b: E2E Black-Box Test Infrastructure
|
||||
|
||||
**Role**: Professional Quality Assurance Engineer
|
||||
**Goal**: Specify a separate consumer application and Docker environment for black-box end-to-end testing of the main system
|
||||
**Constraints**: Spec only — no test code. Consumer must treat the main system as a black box (no internal imports, no direct DB access).
|
||||
|
||||
1. Define Docker environment: services (system under test, test DB, consumer app, dependencies), networks, volumes
|
||||
2. Specify consumer application: tech stack, entry point, communication interfaces with the main system
|
||||
3. Define E2E test scenarios from acceptance criteria — focus on critical end-to-end use cases that cross component boundaries
|
||||
4. Specify test data management: seed data, isolation strategy, external dependency mocks
|
||||
5. Define CI/CD integration: when to run, gate behavior, timeout
|
||||
6. Define reporting format (CSV: test ID, name, execution time, result, error message)
|
||||
|
||||
Use `templates/e2e-test-infrastructure.md` as structure.
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Critical acceptance criteria are covered by at least one E2E scenario
|
||||
- [ ] Consumer app has no direct access to system internals
|
||||
- [ ] Docker environment is self-contained (`docker compose up` sufficient)
|
||||
- [ ] External dependencies have mock/stub services defined
|
||||
|
||||
**Save action**: Write `e2e_test_infrastructure.md`
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Jira Epics
|
||||
|
||||
**Role**: Professional product manager
|
||||
**Goal**: Create Jira epics from components, ordered by dependency
|
||||
**Constraints**: Be concise — fewer words with the same meaning is better
|
||||
|
||||
1. Generate Jira Epics from components using Jira MCP, structured per `templates/epic-spec.md`
|
||||
2. Order epics by dependency (which must be done first)
|
||||
3. Include effort estimation per epic (T-shirt size or story points range)
|
||||
4. Ensure each epic has clear acceptance criteria cross-referenced with component specs
|
||||
5. Generate updated draw.io diagram showing component-to-epic mapping
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Every component maps to exactly one epic
|
||||
- [ ] Dependency order is respected (no epic depends on a later one)
|
||||
- [ ] Acceptance criteria are measurable
|
||||
- [ ] Effort estimates are realistic
|
||||
|
||||
**Save action**: Epics created in Jira via MCP
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist (before FINAL_report.md)
|
||||
|
||||
Before writing the final report, verify ALL of the following:
|
||||
|
||||
### Architecture
|
||||
- [ ] Covers all capabilities from solution.md
|
||||
- [ ] Technology choices are justified
|
||||
- [ ] Deployment model is defined
|
||||
|
||||
### Components
|
||||
- [ ] Every component follows SRP
|
||||
- [ ] No circular dependencies
|
||||
- [ ] All inter-component interfaces are defined and consistent
|
||||
- [ ] No orphan components (unused by any flow)
|
||||
|
||||
### Risks
|
||||
- [ ] All High/Critical risks have mitigations
|
||||
- [ ] Mitigations are reflected in component/architecture docs
|
||||
- [ ] User has confirmed risk assessment is sufficient
|
||||
|
||||
### Tests
|
||||
- [ ] Every acceptance criterion is covered by at least one test
|
||||
- [ ] All 4 test types are represented per component (where applicable)
|
||||
- [ ] Test data management is defined
|
||||
|
||||
### E2E Test Infrastructure
|
||||
- [ ] Critical use cases covered by E2E scenarios
|
||||
- [ ] Docker environment is self-contained
|
||||
- [ ] Consumer app treats main system as black box
|
||||
- [ ] CI/CD integration and reporting defined
|
||||
|
||||
### Epics
|
||||
- [ ] Every component maps to an epic
|
||||
- [ ] Dependency order is correct
|
||||
- [ ] Acceptance criteria are measurable
|
||||
|
||||
**Save action**: Write `FINAL_report.md` using `templates/final-report.md` as structure
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- **Coding during planning**: this workflow produces documents, never code
|
||||
- **Multi-responsibility components**: if a component does two things, split it
|
||||
- **Skipping BLOCKING gates**: never proceed past a BLOCKING marker without user confirmation
|
||||
- **Diagrams without data**: generate diagrams only after the underlying structure is documented
|
||||
- **Copy-pasting problem.md**: the architecture doc should analyze and transform, not repeat the input
|
||||
- **Vague interfaces**: "component A talks to component B" is not enough; define the method, input, output
|
||||
- **Ignoring restrictions.md**: every constraint must be traceable in the architecture or risk register
|
||||
|
||||
## Escalation Rules
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Ambiguous requirements | ASK user |
|
||||
| Missing acceptance criteria | ASK user |
|
||||
| Technology choice with multiple valid options | ASK user |
|
||||
| Component naming | PROCEED, confirm at next BLOCKING gate |
|
||||
| File structure within templates | PROCEED |
|
||||
| Contradictions between input files | ASK user |
|
||||
| Risk mitigation requires architecture change | ASK user |
|
||||
|
||||
## Methodology Quick Reference
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Solution Planning (5-Step Method) │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ CONTEXT: Resolve mode (project vs standalone) + set paths │
|
||||
│ 1. Solution Analysis → architecture.md, system-flows.md │
|
||||
│ [BLOCKING: user confirms architecture] │
|
||||
│ 2. Component Decompose → components/[##]_[name]/description │
|
||||
│ [BLOCKING: user confirms decomposition] │
|
||||
│ 3. Review & Risk Assess → risk_mitigations.md │
|
||||
│ [BLOCKING: user confirms risks, iterative] │
|
||||
│ 4. Test Specifications → components/[##]_[name]/tests.md │
|
||||
│ 4b.E2E Test Infra → e2e_test_infrastructure.md │
|
||||
│ 5. Jira Epics → Jira via MCP │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ Quality Checklist → FINAL_report.md │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ Principles: SRP · Dumb code/smart data · Save immediately │
|
||||
│ Ask don't assume · Plan don't code │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -0,0 +1,128 @@
|
||||
# Architecture Document Template
|
||||
|
||||
Use this template for the architecture document. Save as `_docs/02_plans/<topic>/architecture.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# [System Name] — Architecture
|
||||
|
||||
## 1. System Context
|
||||
|
||||
**Problem being solved**: [One paragraph summarizing the problem from problem.md]
|
||||
|
||||
**System boundaries**: [What is inside the system vs. external]
|
||||
|
||||
**External systems**:
|
||||
|
||||
| System | Integration Type | Direction | Purpose |
|
||||
|--------|-----------------|-----------|---------|
|
||||
| [name] | REST / Queue / DB / File | Inbound / Outbound / Both | [why] |
|
||||
|
||||
## 2. Technology Stack
|
||||
|
||||
| Layer | Technology | Version | Rationale |
|
||||
|-------|-----------|---------|-----------|
|
||||
| Language | | | |
|
||||
| Framework | | | |
|
||||
| Database | | | |
|
||||
| Cache | | | |
|
||||
| Message Queue | | | |
|
||||
| Hosting | | | |
|
||||
| CI/CD | | | |
|
||||
|
||||
**Key constraints from restrictions.md**:
|
||||
- [Constraint 1 and how it affects technology choices]
|
||||
- [Constraint 2]
|
||||
|
||||
## 3. Deployment Model
|
||||
|
||||
**Environments**: Development, Staging, Production
|
||||
|
||||
**Infrastructure**:
|
||||
- [Cloud provider / On-prem / Hybrid]
|
||||
- [Container orchestration if applicable]
|
||||
- [Scaling strategy: horizontal / vertical / auto]
|
||||
|
||||
**Environment-specific configuration**:
|
||||
|
||||
| Config | Development | Production |
|
||||
|--------|-------------|------------|
|
||||
| Database | [local/docker] | [managed service] |
|
||||
| Secrets | [.env file] | [secret manager] |
|
||||
| Logging | [console] | [centralized] |
|
||||
|
||||
## 4. Data Model Overview
|
||||
|
||||
> High-level data model covering the entire system. Detailed per-component models go in component specs.
|
||||
|
||||
**Core entities**:
|
||||
|
||||
| Entity | Description | Owned By Component |
|
||||
|--------|-------------|--------------------|
|
||||
| [entity] | [what it represents] | [component ##] |
|
||||
|
||||
**Key relationships**:
|
||||
- [Entity A] → [Entity B]: [relationship description]
|
||||
|
||||
**Data flow summary**:
|
||||
- [Source] → [Transform] → [Destination]: [what data and why]
|
||||
|
||||
## 5. Integration Points
|
||||
|
||||
### Internal Communication
|
||||
|
||||
| From | To | Protocol | Pattern | Notes |
|
||||
|------|----|----------|---------|-------|
|
||||
| [component] | [component] | Sync REST / Async Queue / Direct call | Request-Response / Event / Command | |
|
||||
|
||||
### External Integrations
|
||||
|
||||
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|
||||
|----------------|----------|------|-------------|--------------|
|
||||
| [system] | [REST/gRPC/etc] | [API key/OAuth/etc] | [limits] | [retry/circuit breaker/fallback] |
|
||||
|
||||
## 6. Non-Functional Requirements
|
||||
|
||||
| Requirement | Target | Measurement | Priority |
|
||||
|------------|--------|-------------|----------|
|
||||
| Availability | [e.g., 99.9%] | [how measured] | High/Medium/Low |
|
||||
| Latency (p95) | [e.g., <200ms] | [endpoint/operation] | |
|
||||
| Throughput | [e.g., 1000 req/s] | [peak/sustained] | |
|
||||
| Data retention | [e.g., 90 days] | [which data] | |
|
||||
| Recovery (RPO/RTO) | [e.g., RPO 1hr, RTO 4hr] | | |
|
||||
| Scalability | [e.g., 10x current load] | [timeline] | |
|
||||
|
||||
## 7. Security Architecture
|
||||
|
||||
**Authentication**: [mechanism — JWT / session / API key]
|
||||
|
||||
**Authorization**: [RBAC / ABAC / per-resource]
|
||||
|
||||
**Data protection**:
|
||||
- At rest: [encryption method]
|
||||
- In transit: [TLS version]
|
||||
- Secrets management: [tool/approach]
|
||||
|
||||
**Audit logging**: [what is logged, where, retention]
|
||||
|
||||
## 8. Key Architectural Decisions
|
||||
|
||||
Record significant decisions that shaped the architecture.
|
||||
|
||||
### ADR-001: [Decision Title]
|
||||
|
||||
**Context**: [Why this decision was needed]
|
||||
|
||||
**Decision**: [What was decided]
|
||||
|
||||
**Alternatives considered**:
|
||||
1. [Alternative 1] — rejected because [reason]
|
||||
2. [Alternative 2] — rejected because [reason]
|
||||
|
||||
**Consequences**: [Trade-offs accepted]
|
||||
|
||||
### ADR-002: [Decision Title]
|
||||
|
||||
...
|
||||
```
|
||||
@@ -0,0 +1,156 @@
|
||||
# Component Specification Template
|
||||
|
||||
Use this template for each component. Save as `components/[##]_[name]/description.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# [Component Name]
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: [One sentence: what this component does and its role in the system]
|
||||
|
||||
**Architectural Pattern**: [e.g., Repository, Event-driven, Pipeline, Facade, etc.]
|
||||
|
||||
**Upstream dependencies**: [Components that this component calls or consumes from]
|
||||
|
||||
**Downstream consumers**: [Components that call or consume from this component]
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
For each interface this component exposes internally:
|
||||
|
||||
### Interface: [InterfaceName]
|
||||
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `method_name` | `InputDTO` | `OutputDTO` | Yes/No | `ErrorType1`, `ErrorType2` |
|
||||
|
||||
**Input DTOs**:
|
||||
```
|
||||
[DTO name]:
|
||||
field_1: type (required/optional) — description
|
||||
field_2: type (required/optional) — description
|
||||
```
|
||||
|
||||
**Output DTOs**:
|
||||
```
|
||||
[DTO name]:
|
||||
field_1: type — description
|
||||
field_2: type — description
|
||||
```
|
||||
|
||||
## 3. External API Specification
|
||||
|
||||
> Include this section only if the component exposes an external HTTP/gRPC API.
|
||||
> Skip if the component is internal-only.
|
||||
|
||||
| Endpoint | Method | Auth | Rate Limit | Description |
|
||||
|----------|--------|------|------------|-------------|
|
||||
| `/api/v1/...` | GET/POST/PUT/DELETE | Required/Public | X req/min | Brief description |
|
||||
|
||||
**Request/Response schemas**: define per endpoint using OpenAPI-style notation.
|
||||
|
||||
**Example request/response**:
|
||||
```json
|
||||
// Request
|
||||
{ }
|
||||
|
||||
// Response
|
||||
{ }
|
||||
```
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
### Queries
|
||||
|
||||
| Query | Frequency | Hot Path | Index Needed |
|
||||
|-------|-----------|----------|--------------|
|
||||
| [describe query] | High/Medium/Low | Yes/No | Yes/No |
|
||||
|
||||
### Caching Strategy
|
||||
|
||||
| Data | Cache Type | TTL | Invalidation |
|
||||
|------|-----------|-----|-------------|
|
||||
| [data item] | In-memory / Redis / None | [duration] | [trigger] |
|
||||
|
||||
### Storage Estimates
|
||||
|
||||
| Table/Collection | Est. Row Count (1yr) | Row Size | Total Size | Growth Rate |
|
||||
|-----------------|---------------------|----------|------------|-------------|
|
||||
| [table_name] | | | | /month |
|
||||
|
||||
### Data Management
|
||||
|
||||
**Seed data**: [Required seed data and how to load it]
|
||||
|
||||
**Rollback**: [Rollback procedure for this component's data changes]
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**Algorithmic Complexity**: [Big O for critical methods — only if non-trivial]
|
||||
|
||||
**State Management**: [Local state / Global state / Stateless — explain how state is handled]
|
||||
|
||||
**Key Dependencies**: [External libraries and their purpose]
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| [name] | [version] | [why needed] |
|
||||
|
||||
**Error Handling Strategy**:
|
||||
- [How errors are caught, propagated, and reported]
|
||||
- [Retry policy if applicable]
|
||||
- [Circuit breaker if applicable]
|
||||
|
||||
## 6. Extensions and Helpers
|
||||
|
||||
> List any shared utilities this component needs that should live in a `helpers/` folder.
|
||||
|
||||
| Helper | Purpose | Used By |
|
||||
|--------|---------|---------|
|
||||
| [helper_name] | [what it does] | [list of components] |
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
**Known limitations**:
|
||||
- [Limitation 1]
|
||||
|
||||
**Potential race conditions**:
|
||||
- [Race condition scenario, if any]
|
||||
|
||||
**Performance bottlenecks**:
|
||||
- [Bottleneck description and mitigation approach]
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: [list of component numbers/names]
|
||||
|
||||
**Can be implemented in parallel with**: [list of component numbers/names]
|
||||
|
||||
**Blocks**: [list of components that depend on this one]
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| ERROR | Unrecoverable failures | `Failed to process order {id}: {error}` |
|
||||
| WARN | Recoverable issues | `Retry attempt {n} for {operation}` |
|
||||
| INFO | Key business events | `Order {id} created by user {uid}` |
|
||||
| DEBUG | Development diagnostics | `Query returned {n} rows in {ms}ms` |
|
||||
|
||||
**Log format**: [structured JSON / plaintext — match system standard]
|
||||
|
||||
**Log storage**: [stdout / file / centralized logging service]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidance Notes
|
||||
|
||||
- **Section 3 (External API)**: skip entirely for internal-only components. Include for any component that exposes HTTP endpoints, WebSocket connections, or gRPC services.
|
||||
- **Section 4 (Storage Estimates)**: critical for components that manage persistent data. Skip for stateless components.
|
||||
- **Section 5 (Algorithmic Complexity)**: only document if the algorithm is non-trivial (O(n^2) or worse, recursive, etc.). Simple CRUD operations don't need this.
|
||||
- **Section 6 (Helpers)**: if the helper is used by only one component, keep it inside that component. Only extract to `helpers/` if shared by 2+ components.
|
||||
- **Section 8 (Dependency Graph)**: this is essential for determining implementation order. Be precise about what "depends on" means — data dependency, API dependency, or shared infrastructure.
|
||||
@@ -0,0 +1,141 @@
|
||||
# E2E Black-Box Test Infrastructure Template
|
||||
|
||||
Describes a separate consumer application that tests the main system as a black box.
|
||||
Save as `PLANS_DIR/<topic>/e2e_test_infrastructure.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# E2E Test Infrastructure
|
||||
|
||||
## Overview
|
||||
|
||||
**System under test**: [main system name and entry points — API URLs, message queues, etc.]
|
||||
**Consumer app purpose**: Standalone application that exercises the main system through its public interfaces, validating end-to-end use cases without access to internals.
|
||||
|
||||
## Docker Environment
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Image / Build | Purpose | Ports |
|
||||
|---------|--------------|---------|-------|
|
||||
| system-under-test | [main app image or build context] | The main system being tested | [ports] |
|
||||
| test-db | [postgres/mysql/etc.] | Database for the main system | [ports] |
|
||||
| e2e-consumer | [build context for consumer app] | Black-box test runner | — |
|
||||
| [dependency] | [image] | [purpose — cache, queue, etc.] | [ports] |
|
||||
|
||||
### Networks
|
||||
|
||||
| Network | Services | Purpose |
|
||||
|---------|----------|---------|
|
||||
| e2e-net | all | Isolated test network |
|
||||
|
||||
### Volumes
|
||||
|
||||
| Volume | Mounted to | Purpose |
|
||||
|--------|-----------|---------|
|
||||
| [name] | [service:path] | [test data, DB persistence, etc.] |
|
||||
|
||||
### docker-compose structure
|
||||
|
||||
```yaml
|
||||
# Outline only — not runnable code
|
||||
services:
|
||||
system-under-test:
|
||||
# main system
|
||||
test-db:
|
||||
# database
|
||||
e2e-consumer:
|
||||
# consumer test app
|
||||
depends_on:
|
||||
- system-under-test
|
||||
```
|
||||
|
||||
## Consumer Application
|
||||
|
||||
**Tech stack**: [language, framework, test runner]
|
||||
**Entry point**: [how it starts — e.g., pytest, jest, custom runner]
|
||||
|
||||
### Communication with system under test
|
||||
|
||||
| Interface | Protocol | Endpoint / Topic | Authentication |
|
||||
|-----------|----------|-----------------|----------------|
|
||||
| [API name] | [HTTP/gRPC/AMQP/etc.] | [URL or topic] | [method] |
|
||||
|
||||
### What the consumer does NOT have access to
|
||||
|
||||
- No direct database access to the main system
|
||||
- No internal module imports
|
||||
- No shared memory or file system with the main system
|
||||
|
||||
## E2E Test Scenarios
|
||||
|
||||
### Acceptance Criteria Traceability
|
||||
|
||||
| AC ID | Acceptance Criterion | E2E Test IDs | Coverage |
|
||||
|-------|---------------------|-------------|----------|
|
||||
| AC-01 | [criterion] | E2E-01 | Covered |
|
||||
| AC-02 | [criterion] | E2E-02, E2E-03 | Covered |
|
||||
| AC-03 | [criterion] | — | NOT COVERED — [reason] |
|
||||
|
||||
### E2E-01: [Scenario Name]
|
||||
|
||||
**Summary**: [One sentence: what end-to-end use case this validates]
|
||||
|
||||
**Traces to**: AC-01
|
||||
|
||||
**Preconditions**:
|
||||
- [System state required before test]
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | [call / send] | [response / event] |
|
||||
| 2 | [call / send] | [response / event] |
|
||||
|
||||
**Max execution time**: [e.g., 10s]
|
||||
|
||||
---
|
||||
|
||||
### E2E-02: [Scenario Name]
|
||||
|
||||
(repeat structure)
|
||||
|
||||
---
|
||||
|
||||
## Test Data Management
|
||||
|
||||
**Seed data**:
|
||||
|
||||
| Data Set | Description | How Loaded | Cleanup |
|
||||
|----------|-------------|-----------|---------|
|
||||
| [name] | [what it contains] | [SQL script / API call / fixture file] | [how removed after test] |
|
||||
|
||||
**Isolation strategy**: [e.g., each test run gets a fresh DB via container restart, or transactions are rolled back, or namespaced data]
|
||||
|
||||
**External dependencies**: [any external APIs that need mocking or sandbox environments]
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
**When to run**: [e.g., on PR merge to dev, nightly, before production deploy]
|
||||
**Pipeline stage**: [where in the CI pipeline this fits]
|
||||
**Gate behavior**: [block merge / warning only / manual approval]
|
||||
**Timeout**: [max total suite duration before considered failed]
|
||||
|
||||
## Reporting
|
||||
|
||||
**Format**: CSV
|
||||
**Columns**: Test ID, Test Name, Execution Time (ms), Result (PASS/FAIL/SKIP), Error Message (if FAIL)
|
||||
**Output path**: [where the CSV is written — e.g., ./e2e-results/report.csv]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidance Notes
|
||||
|
||||
- Every E2E test MUST trace to at least one acceptance criterion. If it doesn't, question whether it's needed.
|
||||
- The consumer app must treat the main system as a true black box — no internal imports, no direct DB queries against the main system's database.
|
||||
- Keep the number of E2E tests focused on critical use cases. Exhaustive testing belongs in per-component tests (Step 4).
|
||||
- Docker environment should be self-contained — `docker compose up` must be sufficient to run the full suite.
|
||||
- If the main system requires external services (payment gateways, third-party APIs), define mock/stub services in the Docker environment.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Jira Epic Template
|
||||
|
||||
Use this template for each Jira epic. Create epics via Jira MCP.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
## Epic: [Component Name] — [Outcome]
|
||||
|
||||
**Example**: Data Ingestion — Near-real-time pipeline
|
||||
|
||||
### Epic Summary
|
||||
|
||||
[1-2 sentences: what we are building + why it matters]
|
||||
|
||||
### Problem / Context
|
||||
|
||||
[Current state, pain points, constraints, business opportunities.
|
||||
Link to architecture.md and relevant component spec.]
|
||||
|
||||
### Scope
|
||||
|
||||
**In Scope**:
|
||||
- [Capability 1 — describe what, not how]
|
||||
- [Capability 2]
|
||||
- [Capability 3]
|
||||
|
||||
**Out of Scope**:
|
||||
- [Explicit exclusion 1 — prevents scope creep]
|
||||
- [Explicit exclusion 2]
|
||||
|
||||
### Assumptions
|
||||
|
||||
- [System design assumption]
|
||||
- [Data structure assumption]
|
||||
- [Infrastructure assumption]
|
||||
|
||||
### Dependencies
|
||||
|
||||
**Epic dependencies** (must be completed first):
|
||||
- [Epic name / ID]
|
||||
|
||||
**External dependencies**:
|
||||
- [Services, hardware, environments, certificates, data sources]
|
||||
|
||||
### Effort Estimation
|
||||
|
||||
**T-shirt size**: S / M / L / XL
|
||||
**Story points range**: [min]-[max]
|
||||
|
||||
### Users / Consumers
|
||||
|
||||
| Type | Who | Key Use Cases |
|
||||
|------|-----|--------------|
|
||||
| Internal | [team/role] | [use case] |
|
||||
| External | [user type] | [use case] |
|
||||
| System | [service name] | [integration point] |
|
||||
|
||||
### Requirements
|
||||
|
||||
**Functional**:
|
||||
- [API expectations, events, data handling]
|
||||
- [Idempotency, retry behavior]
|
||||
|
||||
**Non-functional**:
|
||||
- [Availability, latency, throughput targets]
|
||||
- [Scalability, processing limits, data retention]
|
||||
|
||||
**Security / Compliance**:
|
||||
- [Authentication, encryption, secrets management]
|
||||
- [Logging, audit trail]
|
||||
- [SOC2 / ISO / GDPR if applicable]
|
||||
|
||||
### Design & Architecture
|
||||
|
||||
- Architecture doc: `_docs/02_plans/<topic>/architecture.md`
|
||||
- Component spec: `_docs/02_plans/<topic>/components/[##]_[name]/description.md`
|
||||
- System flows: `_docs/02_plans/<topic>/system-flows.md`
|
||||
|
||||
### Definition of Done
|
||||
|
||||
- [ ] All in-scope capabilities implemented
|
||||
- [ ] Automated tests pass (unit + integration + e2e)
|
||||
- [ ] Minimum coverage threshold met (75%)
|
||||
- [ ] Runbooks written (if applicable)
|
||||
- [ ] Documentation updated
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
| # | Criterion | Measurable Condition |
|
||||
|---|-----------|---------------------|
|
||||
| 1 | [criterion] | [how to verify] |
|
||||
| 2 | [criterion] | [how to verify] |
|
||||
|
||||
### Risks & Mitigations
|
||||
|
||||
| # | Risk | Mitigation | Owner |
|
||||
|---|------|------------|-------|
|
||||
| 1 | [top risk] | [mitigation] | [owner] |
|
||||
| 2 | | | |
|
||||
| 3 | | | |
|
||||
|
||||
### Labels
|
||||
|
||||
- `component:[name]`
|
||||
- `env:prod` / `env:stg`
|
||||
- `type:platform` / `type:data` / `type:integration`
|
||||
|
||||
### Child Issues
|
||||
|
||||
| Type | Title | Points |
|
||||
|------|-------|--------|
|
||||
| Spike | [research/investigation task] | [1-3] |
|
||||
| Task | [implementation task] | [1-5] |
|
||||
| Task | [implementation task] | [1-5] |
|
||||
| Enabler | [infrastructure/setup task] | [1-3] |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidance Notes
|
||||
|
||||
- Be concise. Fewer words with the same meaning = better epic.
|
||||
- Capabilities in scope are "what", not "how" — avoid describing implementation details.
|
||||
- Dependency order matters: epics that must be done first should be listed earlier in the backlog.
|
||||
- Every epic maps to exactly one component. If a component is too large for one epic, split the component first.
|
||||
- Complexity points for child issues follow the project standard: 1, 2, 3, 5, 8. Do not create issues above 5 points — split them.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Final Planning Report Template
|
||||
|
||||
Use this template after completing all 5 steps and the quality checklist. Save as `_docs/02_plans/<topic>/FINAL_report.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# [System Name] — Planning Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
[2-3 sentences: what was planned, the core architectural approach, and the key outcome (number of components, epics, estimated effort)]
|
||||
|
||||
## Problem Statement
|
||||
|
||||
[Brief restatement from problem.md — transformed, not copy-pasted]
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
[Key architectural decisions and technology stack summary. Reference `architecture.md` for full details.]
|
||||
|
||||
**Technology stack**: [language, framework, database, hosting — one line]
|
||||
|
||||
**Deployment**: [environment strategy — one line]
|
||||
|
||||
## Component Summary
|
||||
|
||||
| # | Component | Purpose | Dependencies | Epic |
|
||||
|---|-----------|---------|-------------|------|
|
||||
| 01 | [name] | [one-line purpose] | — | [Jira ID] |
|
||||
| 02 | [name] | [one-line purpose] | 01 | [Jira ID] |
|
||||
| ... | | | | |
|
||||
|
||||
**Implementation order** (based on dependency graph):
|
||||
1. [Phase 1: components that can start immediately]
|
||||
2. [Phase 2: components that depend on Phase 1]
|
||||
3. [Phase 3: ...]
|
||||
|
||||
## System Flows
|
||||
|
||||
| Flow | Description | Key Components |
|
||||
|------|-------------|---------------|
|
||||
| [name] | [one-line summary] | [component list] |
|
||||
|
||||
[Reference `system-flows.md` for full diagrams and details.]
|
||||
|
||||
## Risk Summary
|
||||
|
||||
| Level | Count | Key Risks |
|
||||
|-------|-------|-----------|
|
||||
| Critical | [N] | [brief list] |
|
||||
| High | [N] | [brief list] |
|
||||
| Medium | [N] | — |
|
||||
| Low | [N] | — |
|
||||
|
||||
**Iterations completed**: [N]
|
||||
**All Critical/High risks mitigated**: Yes / No — [details if No]
|
||||
|
||||
[Reference `risk_mitigations.md` for full register.]
|
||||
|
||||
## Test Coverage
|
||||
|
||||
| Component | Integration | Performance | Security | Acceptance | AC Coverage |
|
||||
|-----------|-------------|-------------|----------|------------|-------------|
|
||||
| [name] | [N tests] | [N tests] | [N tests] | [N tests] | [X/Y ACs] |
|
||||
| ... | | | | | |
|
||||
|
||||
**Overall acceptance criteria coverage**: [X / Y total ACs covered] ([percentage]%)
|
||||
|
||||
## Epic Roadmap
|
||||
|
||||
| Order | Epic | Component | Effort | Dependencies |
|
||||
|-------|------|-----------|--------|-------------|
|
||||
| 1 | [Jira ID]: [name] | [component] | [S/M/L/XL] | — |
|
||||
| 2 | [Jira ID]: [name] | [component] | [S/M/L/XL] | Epic 1 |
|
||||
| ... | | | | |
|
||||
|
||||
**Total estimated effort**: [sum or range]
|
||||
|
||||
## Key Decisions Made
|
||||
|
||||
| # | Decision | Rationale | Alternatives Rejected |
|
||||
|---|----------|-----------|----------------------|
|
||||
| 1 | [decision] | [why] | [what was rejected] |
|
||||
| 2 | | | |
|
||||
|
||||
## Open Questions
|
||||
|
||||
| # | Question | Impact | Assigned To |
|
||||
|---|----------|--------|-------------|
|
||||
| 1 | [unresolved question] | [what it blocks or affects] | [who should answer] |
|
||||
|
||||
## Artifact Index
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `architecture.md` | System architecture |
|
||||
| `system-flows.md` | System flows and diagrams |
|
||||
| `components/01_[name]/description.md` | Component spec |
|
||||
| `components/01_[name]/tests.md` | Test spec |
|
||||
| `risk_mitigations.md` | Risk register |
|
||||
| `diagrams/components.drawio` | Component diagram |
|
||||
| `diagrams/flows/flow_[name].md` | Flow diagrams |
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# Risk Register Template
|
||||
|
||||
Use this template for risk assessment. Save as `_docs/02_plans/<topic>/risk_mitigations.md`.
|
||||
Subsequent iterations: `risk_mitigations_02.md`, `risk_mitigations_03.md`, etc.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# Risk Assessment — [Topic] — Iteration [##]
|
||||
|
||||
## Risk Scoring Matrix
|
||||
|
||||
| | Low Impact | Medium Impact | High Impact |
|
||||
|--|------------|---------------|-------------|
|
||||
| **High Probability** | Medium | High | Critical |
|
||||
| **Medium Probability** | Low | Medium | High |
|
||||
| **Low Probability** | Low | Low | Medium |
|
||||
|
||||
## Acceptance Criteria by Risk Level
|
||||
|
||||
| Level | Action Required |
|
||||
|-------|----------------|
|
||||
| Low | Accepted, monitored quarterly |
|
||||
| Medium | Mitigation plan required before implementation |
|
||||
| High | Mitigation + contingency plan required, reviewed weekly |
|
||||
| Critical | Must be resolved before proceeding to next planning step |
|
||||
|
||||
## Risk Register
|
||||
|
||||
| ID | Risk | Category | Probability | Impact | Score | Mitigation | Owner | Status |
|
||||
|----|------|----------|-------------|--------|-------|------------|-------|--------|
|
||||
| R01 | [risk description] | [category] | High/Med/Low | High/Med/Low | Critical/High/Med/Low | [mitigation strategy] | [owner] | Open/Mitigated/Accepted |
|
||||
| R02 | | | | | | | | |
|
||||
|
||||
## Risk Categories
|
||||
|
||||
### Technical Risks
|
||||
- Technology choices may not meet requirements
|
||||
- Integration complexity underestimated
|
||||
- Performance targets unachievable
|
||||
- Security vulnerabilities in design
|
||||
- Data model cannot support future requirements
|
||||
|
||||
### Schedule Risks
|
||||
- Dependencies delayed
|
||||
- Scope creep from ambiguous requirements
|
||||
- Underestimated complexity
|
||||
|
||||
### Resource Risks
|
||||
- Key person dependency
|
||||
- Team lacks experience with chosen technology
|
||||
- Infrastructure not available in time
|
||||
|
||||
### External Risks
|
||||
- Third-party API changes or deprecation
|
||||
- Vendor reliability or pricing changes
|
||||
- Regulatory or compliance changes
|
||||
- Data source availability
|
||||
|
||||
## Detailed Risk Analysis
|
||||
|
||||
### R01: [Risk Title]
|
||||
|
||||
**Description**: [Detailed description of the risk]
|
||||
|
||||
**Trigger conditions**: [What would cause this risk to materialize]
|
||||
|
||||
**Affected components**: [List of components impacted]
|
||||
|
||||
**Mitigation strategy**:
|
||||
1. [Action 1]
|
||||
2. [Action 2]
|
||||
|
||||
**Contingency plan**: [What to do if mitigation fails]
|
||||
|
||||
**Residual risk after mitigation**: [Low/Medium/High]
|
||||
|
||||
**Documents updated**: [List architecture/component docs that were updated to reflect this mitigation]
|
||||
|
||||
---
|
||||
|
||||
### R02: [Risk Title]
|
||||
|
||||
(repeat structure above)
|
||||
|
||||
## Architecture/Component Changes Applied
|
||||
|
||||
| Risk ID | Document Modified | Change Description |
|
||||
|---------|------------------|--------------------|
|
||||
| R01 | `architecture.md` §3 | [what changed] |
|
||||
| R01 | `components/02_[name]/description.md` §5 | [what changed] |
|
||||
|
||||
## Summary
|
||||
|
||||
**Total risks identified**: [N]
|
||||
**Critical**: [N] | **High**: [N] | **Medium**: [N] | **Low**: [N]
|
||||
**Risks mitigated this iteration**: [N]
|
||||
**Risks requiring user decision**: [list]
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# System Flows Template
|
||||
|
||||
Use this template for the system flows document. Save as `_docs/02_plans/<topic>/system-flows.md`.
|
||||
Individual flow diagrams go in `_docs/02_plans/<topic>/diagrams/flows/flow_[name].md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# [System Name] — System Flows
|
||||
|
||||
## Flow Inventory
|
||||
|
||||
| # | Flow Name | Trigger | Primary Components | Criticality |
|
||||
|---|-----------|---------|-------------------|-------------|
|
||||
| F1 | [name] | [user action / scheduled / event] | [component list] | High/Medium/Low |
|
||||
| F2 | [name] | | | |
|
||||
| ... | | | | |
|
||||
|
||||
## Flow Dependencies
|
||||
|
||||
| Flow | Depends On | Shares Data With |
|
||||
|------|-----------|-----------------|
|
||||
| F1 | — | F2 (via [entity]) |
|
||||
| F2 | F1 must complete first | F3 |
|
||||
|
||||
---
|
||||
|
||||
## Flow F1: [Flow Name]
|
||||
|
||||
### Description
|
||||
|
||||
[1-2 sentences: what this flow does, who triggers it, what the outcome is]
|
||||
|
||||
### Preconditions
|
||||
|
||||
- [Condition 1]
|
||||
- [Condition 2]
|
||||
|
||||
### Sequence Diagram
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant ComponentA
|
||||
participant ComponentB
|
||||
participant Database
|
||||
|
||||
User->>ComponentA: [action]
|
||||
ComponentA->>ComponentB: [call with params]
|
||||
ComponentB->>Database: [query/write]
|
||||
Database-->>ComponentB: [result]
|
||||
ComponentB-->>ComponentA: [response]
|
||||
ComponentA-->>User: [result]
|
||||
```
|
||||
|
||||
### Flowchart
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Trigger]) --> Step1[Step description]
|
||||
Step1 --> Decision{Condition?}
|
||||
Decision -->|Yes| Step2[Step description]
|
||||
Decision -->|No| Step3[Step description]
|
||||
Step2 --> EndNode([Result])
|
||||
Step3 --> EndNode
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
| Step | From | To | Data | Format |
|
||||
|------|------|----|------|--------|
|
||||
| 1 | [source] | [destination] | [what data] | [DTO/event/etc] |
|
||||
| 2 | | | | |
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
| Error | Where | Detection | Recovery |
|
||||
|-------|-------|-----------|----------|
|
||||
| [error type] | [which step] | [how detected] | [what happens] |
|
||||
|
||||
### Performance Expectations
|
||||
|
||||
| Metric | Target | Notes |
|
||||
|--------|--------|-------|
|
||||
| End-to-end latency | [target] | [conditions] |
|
||||
| Throughput | [target] | [peak/sustained] |
|
||||
|
||||
---
|
||||
|
||||
## Flow F2: [Flow Name]
|
||||
|
||||
(repeat structure above)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mermaid Diagram Conventions
|
||||
|
||||
Follow these conventions for consistency across all flow diagrams:
|
||||
|
||||
- **Participants**: use component names matching `components/[##]_[name]`
|
||||
- **Node IDs**: camelCase, no spaces (e.g., `validateInput`, `saveOrder`)
|
||||
- **Decision nodes**: use `{Question?}` format
|
||||
- **Start/End**: use `([label])` stadium shape
|
||||
- **External systems**: use `[[label]]` subroutine shape
|
||||
- **Subgraphs**: group by component or bounded context
|
||||
- **No styling**: do not add colors or CSS classes — let the renderer theme handle it
|
||||
- **Edge labels**: wrap special characters in quotes (e.g., `-->|"O(n) check"|`)
|
||||
@@ -0,0 +1,172 @@
|
||||
# Test Specification Template
|
||||
|
||||
Use this template for each component's test spec. Save as `components/[##]_[name]/tests.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
# Test Specification — [Component Name]
|
||||
|
||||
## Acceptance Criteria Traceability
|
||||
|
||||
| AC ID | Acceptance Criterion | Test IDs | Coverage |
|
||||
|-------|---------------------|----------|----------|
|
||||
| AC-01 | [criterion from acceptance_criteria.md] | IT-01, AT-01 | Covered |
|
||||
| AC-02 | [criterion] | PT-01 | Covered |
|
||||
| AC-03 | [criterion] | — | NOT COVERED — [reason] |
|
||||
|
||||
---
|
||||
|
||||
## Integration Tests
|
||||
|
||||
### IT-01: [Test Name]
|
||||
|
||||
**Summary**: [One sentence: what this test verifies]
|
||||
|
||||
**Traces to**: AC-01, AC-03
|
||||
|
||||
**Description**: [Detailed test scenario]
|
||||
|
||||
**Input data**:
|
||||
```
|
||||
[specific input data for this test]
|
||||
```
|
||||
|
||||
**Expected result**:
|
||||
```
|
||||
[specific expected output or state]
|
||||
```
|
||||
|
||||
**Max execution time**: [e.g., 5s]
|
||||
|
||||
**Dependencies**: [other components/services that must be running]
|
||||
|
||||
---
|
||||
|
||||
### IT-02: [Test Name]
|
||||
|
||||
(repeat structure)
|
||||
|
||||
---
|
||||
|
||||
## Performance Tests
|
||||
|
||||
### PT-01: [Test Name]
|
||||
|
||||
**Summary**: [One sentence: what performance aspect is tested]
|
||||
|
||||
**Traces to**: AC-02
|
||||
|
||||
**Load scenario**:
|
||||
- Concurrent users: [N]
|
||||
- Request rate: [N req/s]
|
||||
- Duration: [N minutes]
|
||||
- Ramp-up: [strategy]
|
||||
|
||||
**Expected results**:
|
||||
|
||||
| Metric | Target | Failure Threshold |
|
||||
|--------|--------|-------------------|
|
||||
| Latency (p50) | [target] | [max] |
|
||||
| Latency (p95) | [target] | [max] |
|
||||
| Latency (p99) | [target] | [max] |
|
||||
| Throughput | [target req/s] | [min req/s] |
|
||||
| Error rate | [target %] | [max %] |
|
||||
|
||||
**Resource limits**:
|
||||
- CPU: [max %]
|
||||
- Memory: [max MB/GB]
|
||||
- Database connections: [max pool size]
|
||||
|
||||
---
|
||||
|
||||
### PT-02: [Test Name]
|
||||
|
||||
(repeat structure)
|
||||
|
||||
---
|
||||
|
||||
## Security Tests
|
||||
|
||||
### ST-01: [Test Name]
|
||||
|
||||
**Summary**: [One sentence: what security aspect is tested]
|
||||
|
||||
**Traces to**: AC-04
|
||||
|
||||
**Attack vector**: [e.g., SQL injection on search endpoint, privilege escalation via direct ID access]
|
||||
|
||||
**Test procedure**:
|
||||
1. [Step 1]
|
||||
2. [Step 2]
|
||||
|
||||
**Expected behavior**: [what the system should do — reject, sanitize, log, etc.]
|
||||
|
||||
**Pass criteria**: [specific measurable condition]
|
||||
|
||||
**Fail criteria**: [what constitutes a failure]
|
||||
|
||||
---
|
||||
|
||||
### ST-02: [Test Name]
|
||||
|
||||
(repeat structure)
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
### AT-01: [Test Name]
|
||||
|
||||
**Summary**: [One sentence: what user-facing behavior is verified]
|
||||
|
||||
**Traces to**: AC-01
|
||||
|
||||
**Preconditions**:
|
||||
- [Precondition 1]
|
||||
- [Precondition 2]
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Action | Expected Result |
|
||||
|------|--------|-----------------|
|
||||
| 1 | [user action] | [expected outcome] |
|
||||
| 2 | [user action] | [expected outcome] |
|
||||
| 3 | [user action] | [expected outcome] |
|
||||
|
||||
---
|
||||
|
||||
### AT-02: [Test Name]
|
||||
|
||||
(repeat structure)
|
||||
|
||||
---
|
||||
|
||||
## Test Data Management
|
||||
|
||||
**Required test data**:
|
||||
|
||||
| Data Set | Description | Source | Size |
|
||||
|----------|-------------|--------|------|
|
||||
| [name] | [what it contains] | [generated / fixture / copy of prod subset] | [approx size] |
|
||||
|
||||
**Setup procedure**:
|
||||
1. [How to prepare the test environment]
|
||||
2. [How to load test data]
|
||||
|
||||
**Teardown procedure**:
|
||||
1. [How to clean up after tests]
|
||||
2. [How to restore initial state]
|
||||
|
||||
**Data isolation strategy**: [How tests are isolated from each other — separate DB, transactions, namespacing]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Guidance Notes
|
||||
|
||||
- Every test MUST trace back to at least one acceptance criterion (AC-XX). If a test doesn't trace to any, question whether it's needed.
|
||||
- If an acceptance criterion has no test covering it, mark it as NOT COVERED and explain why (e.g., "requires manual verification", "deferred to phase 2").
|
||||
- Performance test targets should come from the NFR section in `architecture.md`.
|
||||
- Security tests should cover at minimum: authentication bypass, authorization escalation, injection attacks relevant to this component.
|
||||
- Not every component needs all 4 test types. A stateless utility component may only need integration tests.
|
||||
@@ -0,0 +1,470 @@
|
||||
---
|
||||
name: refactor
|
||||
description: |
|
||||
Structured refactoring workflow (6-phase method) with three execution modes:
|
||||
- Full Refactoring: all 6 phases — baseline, discovery, analysis, safety net, execution, hardening
|
||||
- Targeted Refactoring: skip discovery if docs exist, focus on a specific component/area
|
||||
- Quick Assessment: phases 0-2 only, outputs a refactoring plan without execution
|
||||
Supports project mode (_docs/ structure) and standalone mode (@file.md).
|
||||
Trigger phrases:
|
||||
- "refactor", "refactoring", "improve code"
|
||||
- "analyze coupling", "decoupling", "technical debt"
|
||||
- "refactoring assessment", "code quality improvement"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Structured Refactoring (6-Phase Method)
|
||||
|
||||
Transform existing codebases through a systematic refactoring workflow: capture baseline, document current state, research improvements, build safety net, execute changes, and harden.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Preserve behavior first**: never refactor without a passing test suite
|
||||
- **Measure before and after**: every change must be justified by metrics
|
||||
- **Small incremental changes**: commit frequently, never break tests
|
||||
- **Save immediately**: write artifacts to disk after each phase; never accumulate unsaved work
|
||||
- **Ask, don't assume**: when scope or priorities are unclear, STOP and ask the user
|
||||
|
||||
## Context Resolution
|
||||
|
||||
Determine the operating mode based on invocation before any other logic runs.
|
||||
|
||||
**Project mode** (no explicit input file provided):
|
||||
- PROBLEM_DIR: `_docs/00_problem/`
|
||||
- SOLUTION_DIR: `_docs/01_solution/`
|
||||
- COMPONENTS_DIR: `_docs/02_components/`
|
||||
- TESTS_DIR: `_docs/02_tests/`
|
||||
- REFACTOR_DIR: `_docs/04_refactoring/`
|
||||
- All existing guardrails apply.
|
||||
|
||||
**Standalone mode** (explicit input file provided, e.g. `/refactor @some_component.md`):
|
||||
- INPUT_FILE: the provided file (treated as component/area description)
|
||||
- Derive `<topic>` from the input filename (without extension)
|
||||
- REFACTOR_DIR: `_standalone/<topic>/refactoring/`
|
||||
- Guardrails relaxed: only INPUT_FILE must exist and be non-empty
|
||||
- `acceptance_criteria.md` is optional — warn if absent
|
||||
|
||||
Announce the detected mode and resolved paths to the user before proceeding.
|
||||
|
||||
## Mode Detection
|
||||
|
||||
After context resolution, determine the execution mode:
|
||||
|
||||
1. **User explicitly says** "quick assessment" or "just assess" → **Quick Assessment**
|
||||
2. **User explicitly says** "refactor [component/file/area]" with a specific target → **Targeted Refactoring**
|
||||
3. **Default** → **Full Refactoring**
|
||||
|
||||
| Mode | Phases Executed | When to Use |
|
||||
|------|----------------|-------------|
|
||||
| **Full Refactoring** | 0 → 1 → 2 → 3 → 4 → 5 | Complete refactoring of a system or major area |
|
||||
| **Targeted Refactoring** | 0 → (skip 1 if docs exist) → 2 → 3 → 4 → 5 | Refactor a specific component; docs already exist |
|
||||
| **Quick Assessment** | 0 → 1 → 2 | Produce a refactoring roadmap without executing changes |
|
||||
|
||||
Inform the user which mode was detected and confirm before proceeding.
|
||||
|
||||
## Prerequisite Checks (BLOCKING)
|
||||
|
||||
**Project mode:**
|
||||
1. PROBLEM_DIR exists with `problem.md` (or `problem_description.md`) — **STOP if missing**, ask user to create it
|
||||
2. If `acceptance_criteria.md` is missing: **warn** and ask whether to proceed
|
||||
3. Create REFACTOR_DIR if it does not exist
|
||||
4. If REFACTOR_DIR already contains artifacts, ask user: **resume from last checkpoint or start fresh?**
|
||||
|
||||
**Standalone mode:**
|
||||
1. INPUT_FILE exists and is non-empty — **STOP if missing**
|
||||
2. Warn if no `acceptance_criteria.md` provided
|
||||
3. Create REFACTOR_DIR if it does not exist
|
||||
|
||||
## Artifact Management
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
REFACTOR_DIR/
|
||||
├── baseline_metrics.md (Phase 0)
|
||||
├── discovery/
|
||||
│ ├── components/
|
||||
│ │ └── [##]_[name].md (Phase 1)
|
||||
│ ├── solution.md (Phase 1)
|
||||
│ └── system_flows.md (Phase 1)
|
||||
├── analysis/
|
||||
│ ├── research_findings.md (Phase 2)
|
||||
│ └── refactoring_roadmap.md (Phase 2)
|
||||
├── test_specs/
|
||||
│ └── [##]_[test_name].md (Phase 3)
|
||||
├── coupling_analysis.md (Phase 4)
|
||||
├── execution_log.md (Phase 4)
|
||||
├── hardening/
|
||||
│ ├── technical_debt.md (Phase 5)
|
||||
│ ├── performance.md (Phase 5)
|
||||
│ └── security.md (Phase 5)
|
||||
└── FINAL_report.md (after all phases)
|
||||
```
|
||||
|
||||
### Save Timing
|
||||
|
||||
| Phase | Save immediately after | Filename |
|
||||
|-------|------------------------|----------|
|
||||
| Phase 0 | Baseline captured | `baseline_metrics.md` |
|
||||
| Phase 1 | Each component documented | `discovery/components/[##]_[name].md` |
|
||||
| Phase 1 | Solution synthesized | `discovery/solution.md`, `discovery/system_flows.md` |
|
||||
| Phase 2 | Research complete | `analysis/research_findings.md` |
|
||||
| Phase 2 | Roadmap produced | `analysis/refactoring_roadmap.md` |
|
||||
| Phase 3 | Test specs written | `test_specs/[##]_[test_name].md` |
|
||||
| Phase 4 | Coupling analyzed | `coupling_analysis.md` |
|
||||
| Phase 4 | Execution complete | `execution_log.md` |
|
||||
| Phase 5 | Each hardening track | `hardening/<track>.md` |
|
||||
| Final | All phases done | `FINAL_report.md` |
|
||||
|
||||
### Resumability
|
||||
|
||||
If REFACTOR_DIR already contains artifacts:
|
||||
|
||||
1. List existing files and match to the save timing table
|
||||
2. Identify the last completed phase based on which artifacts exist
|
||||
3. Resume from the next incomplete phase
|
||||
4. Inform the user which phases are being skipped
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
At the start of execution, create a TodoWrite with all applicable phases. Update status as each phase completes.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 0: Context & Baseline
|
||||
|
||||
**Role**: Software engineer preparing for refactoring
|
||||
**Goal**: Collect refactoring goals and capture baseline metrics
|
||||
**Constraints**: Measurement only — no code changes
|
||||
|
||||
#### 0a. Collect Goals
|
||||
|
||||
If PROBLEM_DIR files do not yet exist, help the user create them:
|
||||
|
||||
1. `problem.md` — what the system currently does, what changes are needed, pain points
|
||||
2. `acceptance_criteria.md` — success criteria for the refactoring
|
||||
3. `security_approach.md` — security requirements (if applicable)
|
||||
|
||||
Store in PROBLEM_DIR.
|
||||
|
||||
#### 0b. Capture Baseline
|
||||
|
||||
1. Read problem description and acceptance criteria
|
||||
2. Measure current system metrics using project-appropriate tools:
|
||||
|
||||
| Metric Category | What to Capture |
|
||||
|----------------|-----------------|
|
||||
| **Coverage** | Overall, unit, integration, critical paths |
|
||||
| **Complexity** | Cyclomatic complexity (avg + top 5 functions), LOC, tech debt ratio |
|
||||
| **Code Smells** | Total, critical, major |
|
||||
| **Performance** | Response times (P50/P95/P99), CPU/memory, throughput |
|
||||
| **Dependencies** | Total count, outdated, security vulnerabilities |
|
||||
| **Build** | Build time, test execution time, deployment time |
|
||||
|
||||
3. Create functionality inventory: all features/endpoints with status and coverage
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] All metric categories measured (or noted as N/A with reason)
|
||||
- [ ] Functionality inventory is complete
|
||||
- [ ] Measurements are reproducible
|
||||
|
||||
**Save action**: Write `REFACTOR_DIR/baseline_metrics.md`
|
||||
|
||||
**BLOCKING**: Present baseline summary to user. Do NOT proceed until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Discovery
|
||||
|
||||
**Role**: Principal software architect
|
||||
**Goal**: Generate documentation from existing code and form solution description
|
||||
**Constraints**: Document what exists, not what should be. No code changes.
|
||||
|
||||
**Skip condition** (Targeted mode): If `COMPONENTS_DIR` and `SOLUTION_DIR` already contain documentation for the target area, skip to Phase 2. Ask user to confirm skip.
|
||||
|
||||
#### 1a. Document Components
|
||||
|
||||
For each component in the codebase:
|
||||
|
||||
1. Analyze project structure, directories, files
|
||||
2. Go file by file, analyze each method
|
||||
3. Analyze connections between components
|
||||
|
||||
Write per component to `REFACTOR_DIR/discovery/components/[##]_[name].md`:
|
||||
- Purpose and architectural patterns
|
||||
- Mermaid diagrams for logic flows
|
||||
- API reference table (name, description, input, output)
|
||||
- Implementation details: algorithmic complexity, state management, dependencies
|
||||
- Caveats, edge cases, known limitations
|
||||
|
||||
#### 1b. Synthesize Solution & Flows
|
||||
|
||||
1. Review all generated component documentation
|
||||
2. Synthesize into a cohesive solution description
|
||||
3. Create flow diagrams showing component interactions
|
||||
|
||||
Write:
|
||||
- `REFACTOR_DIR/discovery/solution.md` — product description, component overview, interaction diagram
|
||||
- `REFACTOR_DIR/discovery/system_flows.md` — Mermaid flowcharts per major use case
|
||||
|
||||
Also copy to project standard locations if in project mode:
|
||||
- `SOLUTION_DIR/solution.md`
|
||||
- `COMPONENTS_DIR/system_flows.md`
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Every component in the codebase is documented
|
||||
- [ ] Solution description covers all components
|
||||
- [ ] Flow diagrams cover all major use cases
|
||||
- [ ] Mermaid diagrams are syntactically correct
|
||||
|
||||
**Save action**: Write discovery artifacts
|
||||
|
||||
**BLOCKING**: Present discovery summary to user. Do NOT proceed until user confirms documentation accuracy.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Analysis
|
||||
|
||||
**Role**: Researcher and software architect
|
||||
**Goal**: Research improvements and produce a refactoring roadmap
|
||||
**Constraints**: Analysis only — no code changes
|
||||
|
||||
#### 2a. Deep Research
|
||||
|
||||
1. Analyze current implementation patterns
|
||||
2. Research modern approaches for similar systems
|
||||
3. Identify what could be done differently
|
||||
4. Suggest improvements based on state-of-the-art practices
|
||||
|
||||
Write `REFACTOR_DIR/analysis/research_findings.md`:
|
||||
- Current state analysis: patterns used, strengths, weaknesses
|
||||
- Alternative approaches per component: current vs alternative, pros/cons, migration effort
|
||||
- Prioritized recommendations: quick wins + strategic improvements
|
||||
|
||||
#### 2b. Solution Assessment
|
||||
|
||||
1. Assess current implementation against acceptance criteria
|
||||
2. Identify weak points in codebase, map to specific code areas
|
||||
3. Perform gap analysis: acceptance criteria vs current state
|
||||
4. Prioritize changes by impact and effort
|
||||
|
||||
Write `REFACTOR_DIR/analysis/refactoring_roadmap.md`:
|
||||
- Weak points assessment: location, description, impact, proposed solution
|
||||
- Gap analysis: what's missing, what needs improvement
|
||||
- Phased roadmap: Phase 1 (critical fixes), Phase 2 (major improvements), Phase 3 (enhancements)
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] All acceptance criteria are addressed in gap analysis
|
||||
- [ ] Recommendations are grounded in actual code, not abstract
|
||||
- [ ] Roadmap phases are prioritized by impact
|
||||
- [ ] Quick wins are identified separately
|
||||
|
||||
**Save action**: Write analysis artifacts
|
||||
|
||||
**BLOCKING**: Present refactoring roadmap to user. Do NOT proceed until user confirms.
|
||||
|
||||
**Quick Assessment mode stops here.** Present final summary and write `FINAL_report.md` with phases 0-2 content.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Safety Net
|
||||
|
||||
**Role**: QA engineer and developer
|
||||
**Goal**: Design and implement tests that capture current behavior before refactoring
|
||||
**Constraints**: Tests must all pass on the current codebase before proceeding
|
||||
|
||||
#### 3a. Design Test Specs
|
||||
|
||||
Coverage requirements (must meet before refactoring):
|
||||
- Minimum overall coverage: 75%
|
||||
- Critical path coverage: 90%
|
||||
- All public APIs must have integration tests
|
||||
- All error handling paths must be tested
|
||||
|
||||
For each critical area, write test specs to `REFACTOR_DIR/test_specs/[##]_[test_name].md`:
|
||||
- Integration tests: summary, current behavior, input data, expected result, max expected time
|
||||
- Acceptance tests: summary, preconditions, steps with expected results
|
||||
- Coverage analysis: current %, target %, uncovered critical paths
|
||||
|
||||
#### 3b. Implement Tests
|
||||
|
||||
1. Set up test environment and infrastructure if not exists
|
||||
2. Implement each test from specs
|
||||
3. Run tests, verify all pass on current codebase
|
||||
4. Document any discovered issues
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] Coverage requirements met (75% overall, 90% critical paths)
|
||||
- [ ] All tests pass on current codebase
|
||||
- [ ] All public APIs have integration tests
|
||||
- [ ] Test data fixtures are configured
|
||||
|
||||
**Save action**: Write test specs; implemented tests go into the project's test folder
|
||||
|
||||
**GATE (BLOCKING)**: ALL tests must pass before proceeding to Phase 4. If tests fail, fix the tests (not the code) or ask user for guidance. Do NOT proceed to Phase 4 with failing tests.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Execution
|
||||
|
||||
**Role**: Software architect and developer
|
||||
**Goal**: Analyze coupling and execute decoupling changes
|
||||
**Constraints**: Small incremental changes; tests must stay green after every change
|
||||
|
||||
#### 4a. Analyze Coupling
|
||||
|
||||
1. Analyze coupling between components/modules
|
||||
2. Map dependencies (direct and transitive)
|
||||
3. Identify circular dependencies
|
||||
4. Form decoupling strategy
|
||||
|
||||
Write `REFACTOR_DIR/coupling_analysis.md`:
|
||||
- Dependency graph (Mermaid)
|
||||
- Coupling metrics per component
|
||||
- Problem areas: components involved, coupling type, severity, impact
|
||||
- Decoupling strategy: priority order, proposed interfaces/abstractions, effort estimates
|
||||
|
||||
**BLOCKING**: Present coupling analysis to user. Do NOT proceed until user confirms strategy.
|
||||
|
||||
#### 4b. Execute Decoupling
|
||||
|
||||
For each change in the decoupling strategy:
|
||||
|
||||
1. Implement the change
|
||||
2. Run integration tests
|
||||
3. Fix any failures
|
||||
4. Commit with descriptive message
|
||||
|
||||
Address code smells encountered: long methods, large classes, duplicate code, dead code, magic numbers.
|
||||
|
||||
Write `REFACTOR_DIR/execution_log.md`:
|
||||
- Change description, files affected, test status per change
|
||||
- Before/after metrics comparison against baseline
|
||||
|
||||
**Self-verification**:
|
||||
- [ ] All tests still pass after execution
|
||||
- [ ] No circular dependencies remain (or reduced per plan)
|
||||
- [ ] Code smells addressed
|
||||
- [ ] Metrics improved compared to baseline
|
||||
|
||||
**Save action**: Write execution artifacts
|
||||
|
||||
**BLOCKING**: Present execution summary to user. Do NOT proceed until user confirms.
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Hardening (Optional, Parallel Tracks)
|
||||
|
||||
**Role**: Varies per track
|
||||
**Goal**: Address technical debt, performance, and security
|
||||
**Constraints**: Each track is optional; user picks which to run
|
||||
|
||||
Present the three tracks and let user choose which to execute:
|
||||
|
||||
#### Track A: Technical Debt
|
||||
|
||||
**Role**: Technical debt analyst
|
||||
|
||||
1. Identify and categorize debt items: design, code, test, documentation
|
||||
2. Assess each: location, description, impact, effort, interest (cost of not fixing)
|
||||
3. Prioritize: quick wins → strategic debt → tolerable debt
|
||||
4. Create actionable plan with prevention measures
|
||||
|
||||
Write `REFACTOR_DIR/hardening/technical_debt.md`
|
||||
|
||||
#### Track B: Performance Optimization
|
||||
|
||||
**Role**: Performance engineer
|
||||
|
||||
1. Profile current performance, identify bottlenecks
|
||||
2. For each bottleneck: location, symptom, root cause, impact
|
||||
3. Propose optimizations with expected improvement and risk
|
||||
4. Implement one at a time, benchmark after each change
|
||||
5. Verify tests still pass
|
||||
|
||||
Write `REFACTOR_DIR/hardening/performance.md` with before/after benchmarks
|
||||
|
||||
#### Track C: Security Review
|
||||
|
||||
**Role**: Security engineer
|
||||
|
||||
1. Review code against OWASP Top 10
|
||||
2. Verify security requirements from `security_approach.md` are met
|
||||
3. Check: authentication, authorization, input validation, output encoding, encryption, logging
|
||||
|
||||
Write `REFACTOR_DIR/hardening/security.md`:
|
||||
- Vulnerability assessment: location, type, severity, exploit scenario, fix
|
||||
- Security controls review
|
||||
- Compliance check against `security_approach.md`
|
||||
- Recommendations: critical fixes, improvements, hardening
|
||||
|
||||
**Self-verification** (per track):
|
||||
- [ ] All findings are grounded in actual code
|
||||
- [ ] Recommendations are actionable with effort estimates
|
||||
- [ ] All tests still pass after any changes
|
||||
|
||||
**Save action**: Write hardening artifacts
|
||||
|
||||
---
|
||||
|
||||
## Final Report
|
||||
|
||||
After all executed phases complete, write `REFACTOR_DIR/FINAL_report.md`:
|
||||
|
||||
- Refactoring mode used and phases executed
|
||||
- Baseline metrics vs final metrics comparison
|
||||
- Changes made summary
|
||||
- Remaining items (deferred to future)
|
||||
- Lessons learned
|
||||
|
||||
## Escalation Rules
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Unclear refactoring scope | **ASK user** |
|
||||
| Ambiguous acceptance criteria | **ASK user** |
|
||||
| Tests failing before refactoring | **ASK user** — fix tests or fix code? |
|
||||
| Coupling change risks breaking external contracts | **ASK user** |
|
||||
| Performance optimization vs readability trade-off | **ASK user** |
|
||||
| Missing baseline metrics (no test suite, no CI) | **WARN user**, suggest building safety net first |
|
||||
| Security vulnerability found during refactoring | **WARN user** immediately, don't defer |
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
When the user wants to:
|
||||
- Improve existing code structure or quality
|
||||
- Reduce technical debt or coupling
|
||||
- Prepare codebase for new features
|
||||
- Assess code health before major changes
|
||||
|
||||
**Keywords**: "refactor", "refactoring", "improve code", "reduce coupling", "technical debt", "code quality", "decoupling"
|
||||
|
||||
## Methodology Quick Reference
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Structured Refactoring (6-Phase Method) │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ CONTEXT: Resolve mode (project vs standalone) + set paths │
|
||||
│ MODE: Full / Targeted / Quick Assessment │
|
||||
│ │
|
||||
│ 0. Context & Baseline → baseline_metrics.md │
|
||||
│ [BLOCKING: user confirms baseline] │
|
||||
│ 1. Discovery → discovery/ (components, solution) │
|
||||
│ [BLOCKING: user confirms documentation] │
|
||||
│ 2. Analysis → analysis/ (research, roadmap) │
|
||||
│ [BLOCKING: user confirms roadmap] │
|
||||
│ ── Quick Assessment stops here ── │
|
||||
│ 3. Safety Net → test_specs/ + implemented tests │
|
||||
│ [GATE: all tests must pass] │
|
||||
│ 4. Execution → coupling_analysis, execution_log │
|
||||
│ [BLOCKING: user confirms changes] │
|
||||
│ 5. Hardening → hardening/ (debt, perf, security) │
|
||||
│ [optional, user picks tracks] │
|
||||
│ ───────────────────────────────────────────────── │
|
||||
│ FINAL_report.md │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ Principles: Preserve behavior · Measure before/after │
|
||||
│ Small changes · Save immediately · Ask don't assume│
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
# Solution Draft
|
||||
|
||||
## Product Solution Description
|
||||
[Short description of the proposed solution. Brief component interaction diagram.]
|
||||
|
||||
## Existing/Competitor Solutions Analysis
|
||||
[Analysis of existing solutions for similar problems, if any.]
|
||||
|
||||
## Architecture
|
||||
|
||||
[Architecture solution that meets restrictions and acceptance criteria.]
|
||||
|
||||
### Component: [Component Name]
|
||||
|
||||
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|
||||
|----------|-------|-----------|-------------|-------------|----------|------|-----|
|
||||
| [Option 1] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [cost] | [fit assessment] |
|
||||
| [Option 2] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [cost] | [fit assessment] |
|
||||
|
||||
[Repeat per component]
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Integration / Functional Tests
|
||||
- [Test 1]
|
||||
- [Test 2]
|
||||
|
||||
### Non-Functional Tests
|
||||
- [Performance test 1]
|
||||
- [Security test 1]
|
||||
|
||||
## References
|
||||
[All cited source links]
|
||||
|
||||
## Related Artifacts
|
||||
- Tech stack evaluation: `_docs/01_solution/tech_stack.md` (if Phase 3 was executed)
|
||||
- Security analysis: `_docs/01_solution/security_analysis.md` (if Phase 4 was executed)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Solution Draft
|
||||
|
||||
## Assessment Findings
|
||||
|
||||
| Old Component Solution | Weak Point (functional/security/performance) | New Solution |
|
||||
|------------------------|----------------------------------------------|-------------|
|
||||
| [old] | [weak point] | [new] |
|
||||
|
||||
## Product Solution Description
|
||||
[Short description. Brief component interaction diagram. Written as if from scratch — no "updated" markers.]
|
||||
|
||||
## Architecture
|
||||
|
||||
[Architecture solution that meets restrictions and acceptance criteria.]
|
||||
|
||||
### Component: [Component Name]
|
||||
|
||||
| Solution | Tools | Advantages | Limitations | Requirements | Security | Performance | Fit |
|
||||
|----------|-------|-----------|-------------|-------------|----------|------------|-----|
|
||||
| [Option 1] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [perf] | [fit assessment] |
|
||||
| [Option 2] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [perf] | [fit assessment] |
|
||||
|
||||
[Repeat per component]
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Integration / Functional Tests
|
||||
- [Test 1]
|
||||
- [Test 2]
|
||||
|
||||
### Non-Functional Tests
|
||||
- [Performance test 1]
|
||||
- [Security test 1]
|
||||
|
||||
## References
|
||||
[All cited source links]
|
||||
|
||||
## Related Artifacts
|
||||
- Tech stack evaluation: `_docs/01_solution/tech_stack.md` (if Phase 3 was executed)
|
||||
- Security analysis: `_docs/01_solution/security_analysis.md` (if Phase 4 was executed)
|
||||
@@ -0,0 +1,311 @@
|
||||
---
|
||||
name: security-testing
|
||||
description: "Test for security vulnerabilities using OWASP principles. Use when conducting security audits, testing auth, or implementing security practices."
|
||||
category: specialized-testing
|
||||
priority: critical
|
||||
tokenEstimate: 1200
|
||||
agents: [qe-security-scanner, qe-api-contract-validator, qe-quality-analyzer]
|
||||
implementation_status: optimized
|
||||
optimization_version: 1.0
|
||||
last_optimized: 2025-12-02
|
||||
dependencies: []
|
||||
quick_reference_card: true
|
||||
tags: [security, owasp, sast, dast, vulnerabilities, auth, injection]
|
||||
trust_tier: 3
|
||||
validation:
|
||||
schema_path: schemas/output.json
|
||||
validator_path: scripts/validate-config.json
|
||||
eval_path: evals/security-testing.yaml
|
||||
---
|
||||
|
||||
# Security Testing
|
||||
|
||||
<default_to_action>
|
||||
When testing security or conducting audits:
|
||||
1. TEST OWASP Top 10 vulnerabilities systematically
|
||||
2. VALIDATE authentication and authorization on every endpoint
|
||||
3. SCAN dependencies for known vulnerabilities (npm audit)
|
||||
4. CHECK for injection attacks (SQL, XSS, command)
|
||||
5. VERIFY secrets aren't exposed in code/logs
|
||||
|
||||
**Quick Security Checks:**
|
||||
- Access control → Test horizontal/vertical privilege escalation
|
||||
- Crypto → Verify password hashing, HTTPS, no sensitive data exposed
|
||||
- Injection → Test SQL injection, XSS, command injection
|
||||
- Auth → Test weak passwords, session fixation, MFA enforcement
|
||||
- Config → Check error messages don't leak info
|
||||
|
||||
**Critical Success Factors:**
|
||||
- Think like an attacker, build like a defender
|
||||
- Security is built in, not added at the end
|
||||
- Test continuously in CI/CD, not just before release
|
||||
</default_to_action>
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
### When to Use
|
||||
- Security audits and penetration testing
|
||||
- Testing authentication/authorization
|
||||
- Validating input sanitization
|
||||
- Reviewing security configuration
|
||||
|
||||
### OWASP Top 10 (2021)
|
||||
| # | Vulnerability | Key Test |
|
||||
|---|---------------|----------|
|
||||
| 1 | Broken Access Control | User A accessing User B's data |
|
||||
| 2 | Cryptographic Failures | Plaintext passwords, HTTP |
|
||||
| 3 | Injection | SQL/XSS/command injection |
|
||||
| 4 | Insecure Design | Rate limiting, session timeout |
|
||||
| 5 | Security Misconfiguration | Verbose errors, exposed /admin |
|
||||
| 6 | Vulnerable Components | npm audit, outdated packages |
|
||||
| 7 | Auth Failures | Weak passwords, no MFA |
|
||||
| 8 | Integrity Failures | Unsigned updates, malware |
|
||||
| 9 | Logging Failures | No audit trail for breaches |
|
||||
| 10 | SSRF | Server fetching internal URLs |
|
||||
|
||||
### Tools
|
||||
| Type | Tool | Purpose |
|
||||
|------|------|---------|
|
||||
| SAST | SonarQube, Semgrep | Static code analysis |
|
||||
| DAST | OWASP ZAP, Burp | Dynamic scanning |
|
||||
| Deps | npm audit, Snyk | Dependency vulnerabilities |
|
||||
| Secrets | git-secrets, TruffleHog | Secret scanning |
|
||||
|
||||
### Agent Coordination
|
||||
- `qe-security-scanner`: Multi-layer SAST/DAST scanning
|
||||
- `qe-api-contract-validator`: API security testing
|
||||
- `qe-quality-analyzer`: Security code review
|
||||
|
||||
---
|
||||
|
||||
## Key Vulnerability Tests
|
||||
|
||||
### 1. Broken Access Control
|
||||
```javascript
|
||||
// Horizontal escalation - User A accessing User B's data
|
||||
test('user cannot access another user\'s order', async () => {
|
||||
const userAToken = await login('userA');
|
||||
const userBOrder = await createOrder('userB');
|
||||
|
||||
const response = await api.get(`/orders/${userBOrder.id}`, {
|
||||
headers: { Authorization: `Bearer ${userAToken}` }
|
||||
});
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
// Vertical escalation - Regular user accessing admin
|
||||
test('regular user cannot access admin', async () => {
|
||||
const userToken = await login('regularUser');
|
||||
expect((await api.get('/admin/users', {
|
||||
headers: { Authorization: `Bearer ${userToken}` }
|
||||
})).status).toBe(403);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Injection Attacks
|
||||
```javascript
|
||||
// SQL Injection
|
||||
test('prevents SQL injection', async () => {
|
||||
const malicious = "' OR '1'='1";
|
||||
const response = await api.get(`/products?search=${malicious}`);
|
||||
expect(response.body.length).toBeLessThan(100); // Not all products
|
||||
});
|
||||
|
||||
// XSS
|
||||
test('sanitizes HTML output', async () => {
|
||||
const xss = '<script>alert("XSS")</script>';
|
||||
await api.post('/comments', { text: xss });
|
||||
|
||||
const html = (await api.get('/comments')).body;
|
||||
expect(html).toContain('<script>');
|
||||
expect(html).not.toContain('<script>');
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Cryptographic Failures
|
||||
```javascript
|
||||
test('passwords are hashed', async () => {
|
||||
await db.users.create({ email: 'test@example.com', password: 'MyPassword123' });
|
||||
const user = await db.users.findByEmail('test@example.com');
|
||||
|
||||
expect(user.password).not.toBe('MyPassword123');
|
||||
expect(user.password).toMatch(/^\$2[aby]\$\d{2}\$/); // bcrypt
|
||||
});
|
||||
|
||||
test('no sensitive data in API response', async () => {
|
||||
const response = await api.get('/users/me');
|
||||
expect(response.body).not.toHaveProperty('password');
|
||||
expect(response.body).not.toHaveProperty('ssn');
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Security Misconfiguration
|
||||
```javascript
|
||||
test('errors don\'t leak sensitive info', async () => {
|
||||
const response = await api.post('/login', { email: 'nonexistent@test.com', password: 'wrong' });
|
||||
expect(response.body.error).toBe('Invalid credentials'); // Generic message
|
||||
});
|
||||
|
||||
test('sensitive endpoints not exposed', async () => {
|
||||
const endpoints = ['/debug', '/.env', '/.git', '/admin'];
|
||||
for (let ep of endpoints) {
|
||||
expect((await fetch(`https://example.com${ep}`)).status).not.toBe(200);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Rate Limiting
|
||||
```javascript
|
||||
test('rate limiting prevents brute force', async () => {
|
||||
const responses = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
responses.push(await api.post('/login', { email: 'test@example.com', password: 'wrong' }));
|
||||
}
|
||||
expect(responses.filter(r => r.status === 429).length).toBeGreaterThan(0);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist
|
||||
|
||||
### Authentication
|
||||
- [ ] Strong password requirements (12+ chars)
|
||||
- [ ] Password hashing (bcrypt, scrypt, Argon2)
|
||||
- [ ] MFA for sensitive operations
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] Session ID changes after login
|
||||
- [ ] Session timeout
|
||||
|
||||
### Authorization
|
||||
- [ ] Check authorization on every request
|
||||
- [ ] Least privilege principle
|
||||
- [ ] No horizontal escalation
|
||||
- [ ] No vertical escalation
|
||||
|
||||
### Data Protection
|
||||
- [ ] HTTPS everywhere
|
||||
- [ ] Encrypted at rest
|
||||
- [ ] Secrets not in code/logs
|
||||
- [ ] PII compliance (GDPR)
|
||||
|
||||
### Input Validation
|
||||
- [ ] Server-side validation
|
||||
- [ ] Parameterized queries (no SQL injection)
|
||||
- [ ] Output encoding (no XSS)
|
||||
- [ ] Rate limiting
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
security-checks:
|
||||
steps:
|
||||
- name: Dependency audit
|
||||
run: npm audit --audit-level=high
|
||||
|
||||
- name: SAST scan
|
||||
run: npm run sast
|
||||
|
||||
- name: Secret scan
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
|
||||
- name: DAST scan
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: docker run owasp/zap2docker-stable zap-baseline.py -t https://staging.example.com
|
||||
```
|
||||
|
||||
**Pre-commit hooks:**
|
||||
```bash
|
||||
#!/bin/sh
|
||||
git-secrets --scan
|
||||
npm run lint:security
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent-Assisted Security Testing
|
||||
|
||||
```typescript
|
||||
// Comprehensive multi-layer scan
|
||||
await Task("Security Scan", {
|
||||
target: 'src/',
|
||||
layers: { sast: true, dast: true, dependencies: true, secrets: true },
|
||||
severity: ['critical', 'high', 'medium']
|
||||
}, "qe-security-scanner");
|
||||
|
||||
// OWASP Top 10 testing
|
||||
await Task("OWASP Scan", {
|
||||
categories: ['broken-access-control', 'injection', 'cryptographic-failures'],
|
||||
depth: 'comprehensive'
|
||||
}, "qe-security-scanner");
|
||||
|
||||
// Validate fix
|
||||
await Task("Validate Fix", {
|
||||
vulnerability: 'CVE-2024-12345',
|
||||
expectedResolution: 'upgrade package to v2.0.0',
|
||||
retestAfterFix: true
|
||||
}, "qe-security-scanner");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent Coordination Hints
|
||||
|
||||
### Memory Namespace
|
||||
```
|
||||
aqe/security/
|
||||
├── scans/* - Scan results
|
||||
├── vulnerabilities/* - Found vulnerabilities
|
||||
├── fixes/* - Remediation tracking
|
||||
└── compliance/* - Compliance status
|
||||
```
|
||||
|
||||
### Fleet Coordination
|
||||
```typescript
|
||||
const securityFleet = await FleetManager.coordinate({
|
||||
strategy: 'security-testing',
|
||||
agents: [
|
||||
'qe-security-scanner',
|
||||
'qe-api-contract-validator',
|
||||
'qe-quality-analyzer',
|
||||
'qe-deployment-readiness'
|
||||
],
|
||||
topology: 'parallel'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Security by Obscurity
|
||||
Hiding admin at `/super-secret-admin` → **Use proper auth**
|
||||
|
||||
### ❌ Client-Side Validation Only
|
||||
JavaScript validation can be bypassed → **Always validate server-side**
|
||||
|
||||
### ❌ Trusting User Input
|
||||
Assuming input is safe → **Sanitize, validate, escape all input**
|
||||
|
||||
### ❌ Hardcoded Secrets
|
||||
API keys in code → **Environment variables, secret management**
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
- [agentic-quality-engineering](../agentic-quality-engineering/) - Security with agents
|
||||
- [api-testing-patterns](../api-testing-patterns/) - API security testing
|
||||
- [compliance-testing](../compliance-testing/) - GDPR, HIPAA, SOC2
|
||||
|
||||
---
|
||||
|
||||
## Remember
|
||||
|
||||
**Think like an attacker:** What would you try to break? Test that.
|
||||
**Build like a defender:** Assume input is malicious until proven otherwise.
|
||||
**Test continuously:** Security testing is ongoing, not one-time.
|
||||
|
||||
**With Agents:** Agents automate vulnerability scanning, track remediation, and validate fixes. Use agents to maintain security posture at scale.
|
||||
@@ -0,0 +1,789 @@
|
||||
# =============================================================================
|
||||
# AQE Skill Evaluation Test Suite: Security Testing v1.0.0
|
||||
# =============================================================================
|
||||
#
|
||||
# Comprehensive evaluation suite for the security-testing skill per ADR-056.
|
||||
# Tests OWASP Top 10 2021 detection, severity classification, remediation
|
||||
# quality, and cross-model consistency.
|
||||
#
|
||||
# Schema: .claude/skills/.validation/schemas/skill-eval.schema.json
|
||||
# Validator: .claude/skills/security-testing/scripts/validate-config.json
|
||||
#
|
||||
# Coverage:
|
||||
# - OWASP A01:2021 - Broken Access Control
|
||||
# - OWASP A02:2021 - Cryptographic Failures
|
||||
# - OWASP A03:2021 - Injection (SQL, XSS, Command)
|
||||
# - OWASP A07:2021 - Identification and Authentication Failures
|
||||
# - Negative tests (no false positives on secure code)
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
skill: security-testing
|
||||
version: 1.0.0
|
||||
description: >
|
||||
Comprehensive evaluation suite for the security-testing skill.
|
||||
Tests OWASP Top 10 2021 detection capabilities, CWE classification accuracy,
|
||||
CVSS scoring, severity classification, and remediation quality.
|
||||
Supports multi-model testing and integrates with ReasoningBank for
|
||||
continuous improvement.
|
||||
|
||||
# =============================================================================
|
||||
# Multi-Model Configuration
|
||||
# =============================================================================
|
||||
|
||||
models_to_test:
|
||||
- claude-3.5-sonnet # Primary model (high accuracy expected)
|
||||
- claude-3-haiku # Fast model (minimum quality threshold)
|
||||
- gpt-4o # Cross-vendor validation
|
||||
|
||||
# =============================================================================
|
||||
# MCP Integration Configuration
|
||||
# =============================================================================
|
||||
|
||||
mcp_integration:
|
||||
enabled: true
|
||||
namespace: skill-validation
|
||||
|
||||
# Query existing security patterns before running evals
|
||||
query_patterns: true
|
||||
|
||||
# Track each test outcome for learning feedback loop
|
||||
track_outcomes: true
|
||||
|
||||
# Store successful patterns after evals complete
|
||||
store_patterns: true
|
||||
|
||||
# Share learning with fleet coordinator agents
|
||||
share_learning: true
|
||||
|
||||
# Update quality gate with validation metrics
|
||||
update_quality_gate: true
|
||||
|
||||
# Target agents for learning distribution
|
||||
target_agents:
|
||||
- qe-learning-coordinator
|
||||
- qe-queen-coordinator
|
||||
- qe-security-scanner
|
||||
- qe-security-auditor
|
||||
|
||||
# =============================================================================
|
||||
# ReasoningBank Learning Configuration
|
||||
# =============================================================================
|
||||
|
||||
learning:
|
||||
store_success_patterns: true
|
||||
store_failure_patterns: true
|
||||
pattern_ttl_days: 90
|
||||
min_confidence_to_store: 0.7
|
||||
cross_model_comparison: true
|
||||
|
||||
# =============================================================================
|
||||
# Result Format Configuration
|
||||
# =============================================================================
|
||||
|
||||
result_format:
|
||||
json_output: true
|
||||
markdown_report: true
|
||||
include_raw_output: false
|
||||
include_timing: true
|
||||
include_token_usage: true
|
||||
|
||||
# =============================================================================
|
||||
# Environment Setup
|
||||
# =============================================================================
|
||||
|
||||
setup:
|
||||
required_tools:
|
||||
- jq # JSON parsing (required)
|
||||
- npm # Dependency audit (optional but recommended)
|
||||
|
||||
environment_variables:
|
||||
SECURITY_SCAN_DEPTH: "deep"
|
||||
OWASP_ENABLED: "true"
|
||||
SEVERITY_THRESHOLD: "medium"
|
||||
|
||||
fixtures:
|
||||
- name: vulnerable_express_app
|
||||
path: fixtures/vulnerable-express-app.js
|
||||
content: |
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
// SQL Injection vulnerability
|
||||
app.get('/user', (req, res) => {
|
||||
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
|
||||
db.query(query);
|
||||
});
|
||||
|
||||
// XSS vulnerability
|
||||
app.get('/profile', (req, res) => {
|
||||
res.send(`<h1>Hello ${req.query.name}</h1>`);
|
||||
});
|
||||
|
||||
// Path Traversal vulnerability
|
||||
app.get('/file', (req, res) => {
|
||||
const path = './uploads/' + req.query.filename;
|
||||
res.sendFile(path);
|
||||
});
|
||||
|
||||
# =============================================================================
|
||||
# TEST CASES
|
||||
# =============================================================================
|
||||
|
||||
test_cases:
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: SQL Injection (OWASP A03:2021)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc001_sql_injection_string_concat
|
||||
description: "Detect SQL injection via string concatenation in Node.js"
|
||||
category: injection
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
const express = require('express');
|
||||
const mysql = require('mysql');
|
||||
const app = express();
|
||||
|
||||
app.get('/api/users', (req, res) => {
|
||||
const userId = req.params.id;
|
||||
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
||||
db.query(query, (err, results) => {
|
||||
res.json(results);
|
||||
});
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
environment: production
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "SQL injection"
|
||||
- "parameterized"
|
||||
must_not_contain:
|
||||
- "no vulnerabilities"
|
||||
- "secure"
|
||||
must_match_regex:
|
||||
- "CWE-89|CWE-564"
|
||||
- "A03:20[21][0-9]"
|
||||
severity_classification: critical
|
||||
finding_count:
|
||||
min: 1
|
||||
max: 3
|
||||
recommendation_count:
|
||||
min: 1
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.8
|
||||
reasoning_quality_min: 0.7
|
||||
grading_rubric:
|
||||
completeness: 0.3
|
||||
accuracy: 0.5
|
||||
actionability: 0.2
|
||||
|
||||
timeout_ms: 30000
|
||||
|
||||
- id: tc002_sql_injection_parameterized_safe
|
||||
description: "Verify parameterized queries are NOT flagged as vulnerable"
|
||||
category: injection
|
||||
priority: high
|
||||
|
||||
input:
|
||||
code: |
|
||||
app.get('/api/users', (req, res) => {
|
||||
const userId = parseInt(req.params.id, 10);
|
||||
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, results) => {
|
||||
res.json(results);
|
||||
});
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "parameterized"
|
||||
- "secure"
|
||||
must_not_contain:
|
||||
- "SQL injection"
|
||||
- "critical"
|
||||
- "vulnerable"
|
||||
severity_classification: info
|
||||
finding_count:
|
||||
max: 1
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
allow_partial: true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Cross-Site Scripting (OWASP A03:2021)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc003_xss_reflected_html_output
|
||||
description: "Detect reflected XSS in unescaped HTML output"
|
||||
category: injection
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
app.get('/profile', (req, res) => {
|
||||
const name = req.query.name;
|
||||
res.send(`
|
||||
<html>
|
||||
<body>
|
||||
<h1>Welcome, ${name}!</h1>
|
||||
<p>Your profile has been loaded.</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "XSS"
|
||||
- "cross-site scripting"
|
||||
- "sanitize"
|
||||
- "escape"
|
||||
must_match_regex:
|
||||
- "CWE-79"
|
||||
severity_classification: high
|
||||
finding_count:
|
||||
min: 1
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.8
|
||||
reasoning_quality_min: 0.75
|
||||
|
||||
- id: tc004_xss_dom_based_innerhtml
|
||||
description: "Detect DOM-based XSS via innerHTML assignment"
|
||||
category: injection
|
||||
priority: high
|
||||
|
||||
input:
|
||||
code: |
|
||||
// Client-side JavaScript
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const message = params.get('msg');
|
||||
document.getElementById('output').innerHTML = message;
|
||||
context:
|
||||
language: javascript
|
||||
framework: vanilla
|
||||
environment: production
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "DOM"
|
||||
- "XSS"
|
||||
- "innerHTML"
|
||||
- "textContent"
|
||||
must_match_regex:
|
||||
- "CWE-79"
|
||||
severity_classification: high
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Authentication Failures (OWASP A07:2021)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc005_hardcoded_credentials
|
||||
description: "Detect hardcoded credentials and API keys"
|
||||
category: authentication
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
const ADMIN_PASSWORD = 'admin123';
|
||||
const API_KEY = 'sk-1234567890abcdef';
|
||||
const DATABASE_URL = 'postgres://admin:password123@localhost/db';
|
||||
|
||||
app.post('/login', (req, res) => {
|
||||
if (req.body.password === ADMIN_PASSWORD) {
|
||||
req.session.isAdmin = true;
|
||||
res.send('Login successful');
|
||||
}
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "hardcoded"
|
||||
- "credentials"
|
||||
- "secret"
|
||||
- "environment variable"
|
||||
must_match_regex:
|
||||
- "CWE-798|CWE-259"
|
||||
severity_classification: critical
|
||||
finding_count:
|
||||
min: 2
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.8
|
||||
reasoning_quality_min: 0.8
|
||||
|
||||
- id: tc006_weak_password_hashing
|
||||
description: "Detect weak password hashing algorithms (MD5, SHA1)"
|
||||
category: authentication
|
||||
priority: high
|
||||
|
||||
input:
|
||||
code: |
|
||||
const crypto = require('crypto');
|
||||
|
||||
function hashPassword(password) {
|
||||
return crypto.createHash('md5').update(password).digest('hex');
|
||||
}
|
||||
|
||||
function verifyPassword(password, hash) {
|
||||
return hashPassword(password) === hash;
|
||||
}
|
||||
context:
|
||||
language: javascript
|
||||
framework: nodejs
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "MD5"
|
||||
- "weak"
|
||||
- "bcrypt"
|
||||
- "argon2"
|
||||
must_match_regex:
|
||||
- "CWE-327|CWE-328|CWE-916"
|
||||
severity_classification: high
|
||||
finding_count:
|
||||
min: 1
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.8
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Broken Access Control (OWASP A01:2021)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc007_idor_missing_authorization
|
||||
description: "Detect IDOR vulnerability with missing authorization check"
|
||||
category: authorization
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
app.get('/api/users/:id/profile', (req, res) => {
|
||||
// No authorization check - any user can access any profile
|
||||
const userId = req.params.id;
|
||||
db.query('SELECT * FROM profiles WHERE user_id = ?', [userId])
|
||||
.then(profile => res.json(profile));
|
||||
});
|
||||
|
||||
app.delete('/api/users/:id', (req, res) => {
|
||||
// No check if requesting user owns this account
|
||||
db.query('DELETE FROM users WHERE id = ?', [req.params.id]);
|
||||
res.send('User deleted');
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "authorization"
|
||||
- "access control"
|
||||
- "IDOR"
|
||||
- "ownership"
|
||||
must_match_regex:
|
||||
- "CWE-639|CWE-284|CWE-862"
|
||||
- "A01:2021"
|
||||
severity_classification: critical
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Cryptographic Failures (OWASP A02:2021)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc008_weak_encryption_des
|
||||
description: "Detect use of weak encryption algorithms (DES, RC4)"
|
||||
category: cryptography
|
||||
priority: high
|
||||
|
||||
input:
|
||||
code: |
|
||||
const crypto = require('crypto');
|
||||
|
||||
function encryptData(data, key) {
|
||||
const cipher = crypto.createCipher('des', key);
|
||||
return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
|
||||
}
|
||||
|
||||
function decryptData(data, key) {
|
||||
const decipher = crypto.createDecipher('des', key);
|
||||
return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
|
||||
}
|
||||
context:
|
||||
language: javascript
|
||||
framework: nodejs
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "DES"
|
||||
- "weak"
|
||||
- "deprecated"
|
||||
- "AES"
|
||||
must_match_regex:
|
||||
- "CWE-327|CWE-328"
|
||||
- "A02:2021"
|
||||
severity_classification: high
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
- id: tc009_plaintext_password_storage
|
||||
description: "Detect plaintext password storage"
|
||||
category: cryptography
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
class User {
|
||||
constructor(email, password) {
|
||||
this.email = email;
|
||||
this.password = password; // Stored in plaintext!
|
||||
}
|
||||
|
||||
save() {
|
||||
db.query('INSERT INTO users (email, password) VALUES (?, ?)',
|
||||
[this.email, this.password]);
|
||||
}
|
||||
}
|
||||
context:
|
||||
language: javascript
|
||||
framework: nodejs
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "plaintext"
|
||||
- "password"
|
||||
- "hash"
|
||||
- "bcrypt"
|
||||
must_match_regex:
|
||||
- "CWE-256|CWE-312"
|
||||
- "A02:2021"
|
||||
severity_classification: critical
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.8
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Path Traversal (Related to A01:2021)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc010_path_traversal_file_access
|
||||
description: "Detect path traversal vulnerability in file access"
|
||||
category: injection
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
const fs = require('fs');
|
||||
|
||||
app.get('/download', (req, res) => {
|
||||
const filename = req.query.file;
|
||||
const filepath = './uploads/' + filename;
|
||||
res.sendFile(filepath);
|
||||
});
|
||||
|
||||
app.get('/read', (req, res) => {
|
||||
const content = fs.readFileSync('./data/' + req.params.name);
|
||||
res.send(content);
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "path traversal"
|
||||
- "directory traversal"
|
||||
- "../"
|
||||
- "sanitize"
|
||||
must_match_regex:
|
||||
- "CWE-22|CWE-23"
|
||||
severity_classification: critical
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Negative Tests (No False Positives)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc011_secure_code_no_false_positives
|
||||
description: "Verify secure code is NOT flagged as vulnerable"
|
||||
category: negative
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const bcrypt = require('bcrypt');
|
||||
const validator = require('validator');
|
||||
|
||||
const app = express();
|
||||
app.use(helmet());
|
||||
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
|
||||
|
||||
app.post('/api/users', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Input validation
|
||||
if (!validator.isEmail(email)) {
|
||||
return res.status(400).json({ error: 'Invalid email' });
|
||||
}
|
||||
|
||||
// Secure password hashing
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// Parameterized query
|
||||
await db.query(
|
||||
'INSERT INTO users (email, password) VALUES ($1, $2)',
|
||||
[email, hashedPassword]
|
||||
);
|
||||
|
||||
res.status(201).json({ message: 'User created' });
|
||||
});
|
||||
context:
|
||||
language: javascript
|
||||
framework: express
|
||||
environment: production
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "secure"
|
||||
- "best practice"
|
||||
must_not_contain:
|
||||
- "SQL injection"
|
||||
- "XSS"
|
||||
- "critical vulnerability"
|
||||
- "high severity"
|
||||
finding_count:
|
||||
max: 2 # Allow informational findings only
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.6
|
||||
allow_partial: true
|
||||
|
||||
- id: tc012_secure_auth_implementation
|
||||
description: "Verify secure authentication is recognized as safe"
|
||||
category: negative
|
||||
priority: high
|
||||
|
||||
input:
|
||||
code: |
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
async function login(email, password) {
|
||||
const user = await User.findByEmail(email);
|
||||
if (!user) {
|
||||
return { error: 'Invalid credentials' };
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!match) {
|
||||
return { error: 'Invalid credentials' };
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '1h' }
|
||||
);
|
||||
|
||||
return { token };
|
||||
}
|
||||
context:
|
||||
language: javascript
|
||||
framework: nodejs
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "bcrypt"
|
||||
- "jwt"
|
||||
- "secure"
|
||||
must_not_contain:
|
||||
- "vulnerable"
|
||||
- "critical"
|
||||
- "hardcoded"
|
||||
severity_classification: info
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
allow_partial: true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CATEGORY: Python Security (Multi-language Support)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- id: tc013_python_sql_injection
|
||||
description: "Detect SQL injection in Python Flask application"
|
||||
category: injection
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
from flask import Flask, request
|
||||
import sqlite3
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/user')
|
||||
def get_user():
|
||||
user_id = request.args.get('id')
|
||||
conn = sqlite3.connect('users.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
||||
return str(cursor.fetchone())
|
||||
context:
|
||||
language: python
|
||||
framework: flask
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "SQL injection"
|
||||
- "parameterized"
|
||||
- "f-string"
|
||||
must_match_regex:
|
||||
- "CWE-89"
|
||||
severity_classification: critical
|
||||
finding_count:
|
||||
min: 1
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
- id: tc014_python_ssti_jinja
|
||||
description: "Detect Server-Side Template Injection in Jinja2"
|
||||
category: injection
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
from flask import Flask, request, render_template_string
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/render')
|
||||
def render():
|
||||
template = request.args.get('template')
|
||||
return render_template_string(template)
|
||||
context:
|
||||
language: python
|
||||
framework: flask
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "SSTI"
|
||||
- "template injection"
|
||||
- "render_template_string"
|
||||
- "Jinja2"
|
||||
must_match_regex:
|
||||
- "CWE-94|CWE-1336"
|
||||
severity_classification: critical
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
- id: tc015_python_pickle_deserialization
|
||||
description: "Detect insecure deserialization with pickle"
|
||||
category: injection
|
||||
priority: critical
|
||||
|
||||
input:
|
||||
code: |
|
||||
import pickle
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/load')
|
||||
def load_data():
|
||||
data = request.get_data()
|
||||
obj = pickle.loads(data)
|
||||
return str(obj)
|
||||
context:
|
||||
language: python
|
||||
framework: flask
|
||||
|
||||
expected_output:
|
||||
must_contain:
|
||||
- "pickle"
|
||||
- "deserialization"
|
||||
- "untrusted"
|
||||
- "RCE"
|
||||
must_match_regex:
|
||||
- "CWE-502"
|
||||
- "A08:2021"
|
||||
severity_classification: critical
|
||||
|
||||
validation:
|
||||
schema_check: true
|
||||
keyword_match_threshold: 0.7
|
||||
|
||||
# =============================================================================
|
||||
# SUCCESS CRITERIA
|
||||
# =============================================================================
|
||||
|
||||
success_criteria:
|
||||
# Overall pass rate (90% of tests must pass)
|
||||
pass_rate: 0.9
|
||||
|
||||
# Critical tests must ALL pass (100%)
|
||||
critical_pass_rate: 1.0
|
||||
|
||||
# Average reasoning quality score
|
||||
avg_reasoning_quality: 0.75
|
||||
|
||||
# Maximum suite execution time (5 minutes)
|
||||
max_execution_time_ms: 300000
|
||||
|
||||
# Maximum variance between model results (15%)
|
||||
cross_model_variance: 0.15
|
||||
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
# =============================================================================
|
||||
|
||||
metadata:
|
||||
author: "qe-security-auditor"
|
||||
created: "2026-02-02"
|
||||
last_updated: "2026-02-02"
|
||||
coverage_target: >
|
||||
OWASP Top 10 2021: A01 (Broken Access Control), A02 (Cryptographic Failures),
|
||||
A03 (Injection - SQL, XSS, SSTI, Command), A07 (Authentication Failures),
|
||||
A08 (Software Integrity - Deserialization). Covers JavaScript/Node.js
|
||||
Express apps and Python Flask apps. 15 test cases with 90% pass rate
|
||||
requirement and 100% critical pass rate.
|
||||
@@ -0,0 +1,879 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://agentic-qe.dev/schemas/security-testing-output.json",
|
||||
"title": "AQE Security Testing Skill Output Schema",
|
||||
"description": "Schema for security-testing skill output validation. Extends the base skill-output template with OWASP Top 10 categories, CWE identifiers, and CVSS scoring.",
|
||||
"type": "object",
|
||||
"required": ["skillName", "version", "timestamp", "status", "trustTier", "output"],
|
||||
"properties": {
|
||||
"skillName": {
|
||||
"type": "string",
|
||||
"const": "security-testing",
|
||||
"description": "Must be 'security-testing'"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?$",
|
||||
"description": "Semantic version of the skill"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp of output generation"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["success", "partial", "failed", "skipped"],
|
||||
"description": "Overall execution status"
|
||||
},
|
||||
"trustTier": {
|
||||
"type": "integer",
|
||||
"const": 3,
|
||||
"description": "Trust tier 3 indicates full validation with eval suite"
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": ["summary", "findings", "owaspCategories"],
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"minLength": 50,
|
||||
"maxLength": 2000,
|
||||
"description": "Human-readable summary of security findings"
|
||||
},
|
||||
"score": {
|
||||
"$ref": "#/$defs/securityScore",
|
||||
"description": "Overall security score"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/securityFinding"
|
||||
},
|
||||
"maxItems": 500,
|
||||
"description": "List of security vulnerabilities discovered"
|
||||
},
|
||||
"recommendations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/securityRecommendation"
|
||||
},
|
||||
"maxItems": 100,
|
||||
"description": "Prioritized remediation recommendations with code examples"
|
||||
},
|
||||
"metrics": {
|
||||
"$ref": "#/$defs/securityMetrics",
|
||||
"description": "Security scan metrics and statistics"
|
||||
},
|
||||
"owaspCategories": {
|
||||
"$ref": "#/$defs/owaspCategoryBreakdown",
|
||||
"description": "OWASP Top 10 2021 category breakdown"
|
||||
},
|
||||
"artifacts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/artifact"
|
||||
},
|
||||
"maxItems": 50,
|
||||
"description": "Generated security reports and scan artifacts"
|
||||
},
|
||||
"timeline": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/timelineEvent"
|
||||
},
|
||||
"description": "Scan execution timeline"
|
||||
},
|
||||
"scanConfiguration": {
|
||||
"$ref": "#/$defs/scanConfiguration",
|
||||
"description": "Configuration used for the security scan"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/$defs/metadata"
|
||||
},
|
||||
"validation": {
|
||||
"$ref": "#/$defs/validationResult"
|
||||
},
|
||||
"learning": {
|
||||
"$ref": "#/$defs/learningData"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"securityScore": {
|
||||
"type": "object",
|
||||
"required": ["value", "max"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "Security score (0=critical issues, 100=no issues)"
|
||||
},
|
||||
"max": {
|
||||
"type": "number",
|
||||
"const": 100,
|
||||
"description": "Maximum score is always 100"
|
||||
},
|
||||
"grade": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-F][+-]?$",
|
||||
"description": "Letter grade: A (90-100), B (80-89), C (70-79), D (60-69), F (<60)"
|
||||
},
|
||||
"trend": {
|
||||
"type": "string",
|
||||
"enum": ["improving", "stable", "declining", "unknown"],
|
||||
"description": "Trend compared to previous scans"
|
||||
},
|
||||
"riskLevel": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "minimal"],
|
||||
"description": "Overall risk level assessment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityFinding": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "severity", "owasp"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^SEC-\\d{3,6}$",
|
||||
"description": "Unique finding identifier (e.g., SEC-001)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 10,
|
||||
"maxLength": 200,
|
||||
"description": "Finding title describing the vulnerability"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000,
|
||||
"description": "Detailed description of the vulnerability"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "info"],
|
||||
"description": "Severity: critical (CVSS 9.0-10.0), high (7.0-8.9), medium (4.0-6.9), low (0.1-3.9), info (0)"
|
||||
},
|
||||
"owasp": {
|
||||
"type": "string",
|
||||
"pattern": "^A(0[1-9]|10):20(21|25)$",
|
||||
"description": "OWASP Top 10 category (e.g., A01:2021, A03:2025)"
|
||||
},
|
||||
"owaspCategory": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"A01:2021-Broken-Access-Control",
|
||||
"A02:2021-Cryptographic-Failures",
|
||||
"A03:2021-Injection",
|
||||
"A04:2021-Insecure-Design",
|
||||
"A05:2021-Security-Misconfiguration",
|
||||
"A06:2021-Vulnerable-Components",
|
||||
"A07:2021-Identification-Authentication-Failures",
|
||||
"A08:2021-Software-Data-Integrity-Failures",
|
||||
"A09:2021-Security-Logging-Monitoring-Failures",
|
||||
"A10:2021-Server-Side-Request-Forgery"
|
||||
],
|
||||
"description": "Full OWASP category name"
|
||||
},
|
||||
"cwe": {
|
||||
"type": "string",
|
||||
"pattern": "^CWE-\\d{1,4}$",
|
||||
"description": "CWE identifier (e.g., CWE-79 for XSS, CWE-89 for SQLi)"
|
||||
},
|
||||
"cvss": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"description": "CVSS v3.1 base score"
|
||||
},
|
||||
"vector": {
|
||||
"type": "string",
|
||||
"pattern": "^CVSS:3\\.1/AV:[NALP]/AC:[LH]/PR:[NLH]/UI:[NR]/S:[UC]/C:[NLH]/I:[NLH]/A:[NLH]$",
|
||||
"description": "CVSS v3.1 vector string"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["None", "Low", "Medium", "High", "Critical"],
|
||||
"description": "CVSS severity rating"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/$defs/location",
|
||||
"description": "Location of the vulnerability"
|
||||
},
|
||||
"evidence": {
|
||||
"type": "string",
|
||||
"maxLength": 5000,
|
||||
"description": "Evidence: code snippet, request/response, or PoC"
|
||||
},
|
||||
"remediation": {
|
||||
"type": "string",
|
||||
"maxLength": 2000,
|
||||
"description": "Specific fix instructions for this finding"
|
||||
},
|
||||
"references": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["title", "url"],
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"url": { "type": "string", "format": "uri" }
|
||||
}
|
||||
},
|
||||
"maxItems": 10,
|
||||
"description": "External references (OWASP, CWE, CVE, etc.)"
|
||||
},
|
||||
"falsePositive": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Potential false positive flag"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence in finding accuracy (0.0-1.0)"
|
||||
},
|
||||
"exploitability": {
|
||||
"type": "string",
|
||||
"enum": ["trivial", "easy", "moderate", "difficult", "theoretical"],
|
||||
"description": "How easy is it to exploit this vulnerability"
|
||||
},
|
||||
"affectedVersions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Affected package/library versions for dependency vulnerabilities"
|
||||
},
|
||||
"cve": {
|
||||
"type": "string",
|
||||
"pattern": "^CVE-\\d{4}-\\d{4,}$",
|
||||
"description": "CVE identifier if applicable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityRecommendation": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "priority", "owaspCategories"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^REC-\\d{3,6}$",
|
||||
"description": "Unique recommendation identifier"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 10,
|
||||
"maxLength": 200,
|
||||
"description": "Recommendation title"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000,
|
||||
"description": "Detailed recommendation description"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low"],
|
||||
"description": "Remediation priority"
|
||||
},
|
||||
"effort": {
|
||||
"type": "string",
|
||||
"enum": ["trivial", "low", "medium", "high", "major"],
|
||||
"description": "Estimated effort: trivial(<1hr), low(1-4hr), medium(1-3d), high(1-2wk), major(>2wk)"
|
||||
},
|
||||
"impact": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"description": "Security impact if implemented (1-10)"
|
||||
},
|
||||
"relatedFindings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^SEC-\\d{3,6}$"
|
||||
},
|
||||
"description": "IDs of findings this addresses"
|
||||
},
|
||||
"owaspCategories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^A(0[1-9]|10):20(21|25)$"
|
||||
},
|
||||
"description": "OWASP categories this recommendation addresses"
|
||||
},
|
||||
"codeExample": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"before": {
|
||||
"type": "string",
|
||||
"maxLength": 2000,
|
||||
"description": "Vulnerable code example"
|
||||
},
|
||||
"after": {
|
||||
"type": "string",
|
||||
"maxLength": 2000,
|
||||
"description": "Secure code example"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Programming language"
|
||||
}
|
||||
},
|
||||
"description": "Before/after code examples for remediation"
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["title", "url"],
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"url": { "type": "string", "format": "uri" }
|
||||
}
|
||||
},
|
||||
"maxItems": 10,
|
||||
"description": "External resources and documentation"
|
||||
},
|
||||
"automatable": {
|
||||
"type": "boolean",
|
||||
"description": "Can this fix be automated?"
|
||||
},
|
||||
"fixCommand": {
|
||||
"type": "string",
|
||||
"description": "CLI command to apply fix if automatable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"owaspCategoryBreakdown": {
|
||||
"type": "object",
|
||||
"description": "OWASP Top 10 2021 category scores and findings",
|
||||
"properties": {
|
||||
"A01:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A01:2021 - Broken Access Control"
|
||||
},
|
||||
"A02:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A02:2021 - Cryptographic Failures"
|
||||
},
|
||||
"A03:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A03:2021 - Injection"
|
||||
},
|
||||
"A04:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A04:2021 - Insecure Design"
|
||||
},
|
||||
"A05:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A05:2021 - Security Misconfiguration"
|
||||
},
|
||||
"A06:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A06:2021 - Vulnerable and Outdated Components"
|
||||
},
|
||||
"A07:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A07:2021 - Identification and Authentication Failures"
|
||||
},
|
||||
"A08:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A08:2021 - Software and Data Integrity Failures"
|
||||
},
|
||||
"A09:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A09:2021 - Security Logging and Monitoring Failures"
|
||||
},
|
||||
"A10:2021": {
|
||||
"$ref": "#/$defs/owaspCategoryScore",
|
||||
"description": "A10:2021 - Server-Side Request Forgery (SSRF)"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"owaspCategoryScore": {
|
||||
"type": "object",
|
||||
"required": ["tested", "score"],
|
||||
"properties": {
|
||||
"tested": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this category was tested"
|
||||
},
|
||||
"score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "Category score (100 = no issues, 0 = critical)"
|
||||
},
|
||||
"grade": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-F][+-]?$",
|
||||
"description": "Letter grade for this category"
|
||||
},
|
||||
"findingCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of findings in this category"
|
||||
},
|
||||
"criticalCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of critical findings"
|
||||
},
|
||||
"highCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of high severity findings"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pass", "fail", "warn", "skip"],
|
||||
"description": "Category status"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Category description and context"
|
||||
},
|
||||
"cwes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^CWE-\\d{1,4}$"
|
||||
},
|
||||
"description": "CWEs found in this category"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityMetrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalFindings": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Total vulnerabilities found"
|
||||
},
|
||||
"criticalCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Critical severity findings"
|
||||
},
|
||||
"highCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "High severity findings"
|
||||
},
|
||||
"mediumCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Medium severity findings"
|
||||
},
|
||||
"lowCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Low severity findings"
|
||||
},
|
||||
"infoCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Informational findings"
|
||||
},
|
||||
"filesScanned": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of files analyzed"
|
||||
},
|
||||
"linesOfCode": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Lines of code scanned"
|
||||
},
|
||||
"dependenciesChecked": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of dependencies checked"
|
||||
},
|
||||
"owaspCategoriesTested": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"description": "OWASP Top 10 categories tested"
|
||||
},
|
||||
"owaspCategoriesPassed": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"description": "OWASP Top 10 categories with no findings"
|
||||
},
|
||||
"uniqueCwes": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Unique CWE identifiers found"
|
||||
},
|
||||
"falsePositiveRate": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Estimated false positive rate"
|
||||
},
|
||||
"scanDurationMs": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Total scan duration in milliseconds"
|
||||
},
|
||||
"coverage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sast": {
|
||||
"type": "boolean",
|
||||
"description": "Static analysis performed"
|
||||
},
|
||||
"dast": {
|
||||
"type": "boolean",
|
||||
"description": "Dynamic analysis performed"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "boolean",
|
||||
"description": "Dependency scan performed"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "boolean",
|
||||
"description": "Secret scanning performed"
|
||||
},
|
||||
"configuration": {
|
||||
"type": "boolean",
|
||||
"description": "Configuration review performed"
|
||||
}
|
||||
},
|
||||
"description": "Scan coverage indicators"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanConfiguration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Scan target (file path, URL, or package)"
|
||||
},
|
||||
"targetType": {
|
||||
"type": "string",
|
||||
"enum": ["source", "url", "package", "container", "infrastructure"],
|
||||
"description": "Type of target being scanned"
|
||||
},
|
||||
"scanTypes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["sast", "dast", "dependency", "secret", "configuration", "container", "iac"]
|
||||
},
|
||||
"description": "Types of scans performed"
|
||||
},
|
||||
"severity": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "info"]
|
||||
},
|
||||
"description": "Severity levels included in scan"
|
||||
},
|
||||
"owaspCategories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^A(0[1-9]|10):20(21|25)$"
|
||||
},
|
||||
"description": "OWASP categories tested"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Security tools used"
|
||||
},
|
||||
"excludePatterns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "File patterns excluded from scan"
|
||||
},
|
||||
"rulesets": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Security rulesets applied"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"maxLength": 500,
|
||||
"description": "File path relative to project root"
|
||||
},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Line number"
|
||||
},
|
||||
"column": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Column number"
|
||||
},
|
||||
"endLine": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "End line for multi-line findings"
|
||||
},
|
||||
"endColumn": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "End column"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "URL for web-based findings"
|
||||
},
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"description": "API endpoint path"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
||||
"description": "HTTP method for API findings"
|
||||
},
|
||||
"parameter": {
|
||||
"type": "string",
|
||||
"description": "Vulnerable parameter name"
|
||||
},
|
||||
"component": {
|
||||
"type": "string",
|
||||
"description": "Affected component or module"
|
||||
}
|
||||
}
|
||||
},
|
||||
"artifact": {
|
||||
"type": "object",
|
||||
"required": ["type", "path"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["report", "sarif", "data", "log", "evidence"],
|
||||
"description": "Artifact type"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"maxLength": 500,
|
||||
"description": "Path to artifact"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["json", "sarif", "html", "md", "txt", "xml", "csv"],
|
||||
"description": "Artifact format"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500,
|
||||
"description": "Artifact description"
|
||||
},
|
||||
"sizeBytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "File size in bytes"
|
||||
},
|
||||
"checksum": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[a-f0-9]{64}$",
|
||||
"description": "SHA-256 checksum"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelineEvent": {
|
||||
"type": "object",
|
||||
"required": ["timestamp", "event"],
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Event timestamp"
|
||||
},
|
||||
"event": {
|
||||
"type": "string",
|
||||
"maxLength": 200,
|
||||
"description": "Event description"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["start", "checkpoint", "warning", "error", "complete"],
|
||||
"description": "Event type"
|
||||
},
|
||||
"durationMs": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Duration since previous event"
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"enum": ["initialization", "sast", "dast", "dependency", "secret", "reporting"],
|
||||
"description": "Scan phase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"executionTimeMs": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 3600000,
|
||||
"description": "Execution time in milliseconds"
|
||||
},
|
||||
"toolsUsed": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["semgrep", "npm-audit", "trivy", "owasp-zap", "bandit", "gosec", "eslint-security", "snyk", "gitleaks", "trufflehog", "bearer"]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"description": "Security tools used"
|
||||
},
|
||||
"agentId": {
|
||||
"type": "string",
|
||||
"pattern": "^qe-[a-z][a-z0-9-]*$",
|
||||
"description": "Agent ID (e.g., qe-security-scanner)"
|
||||
},
|
||||
"modelUsed": {
|
||||
"type": "string",
|
||||
"description": "LLM model used for analysis"
|
||||
},
|
||||
"inputHash": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"description": "SHA-256 hash of input"
|
||||
},
|
||||
"targetUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Target URL if applicable"
|
||||
},
|
||||
"targetPath": {
|
||||
"type": "string",
|
||||
"description": "Target path if applicable"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"enum": ["development", "staging", "production", "ci"],
|
||||
"description": "Execution environment"
|
||||
},
|
||||
"retryCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"description": "Number of retries"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validationResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schemaValid": {
|
||||
"type": "boolean",
|
||||
"description": "Passes JSON schema validation"
|
||||
},
|
||||
"contentValid": {
|
||||
"type": "boolean",
|
||||
"description": "Passes content validation"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence score"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"maxItems": 20,
|
||||
"description": "Validation warnings"
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"maxItems": 20,
|
||||
"description": "Validation errors"
|
||||
},
|
||||
"validatorVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "Validator version"
|
||||
}
|
||||
}
|
||||
},
|
||||
"learningData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"patternsDetected": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 200
|
||||
},
|
||||
"maxItems": 20,
|
||||
"description": "Security patterns detected (e.g., sql-injection-string-concat)"
|
||||
},
|
||||
"reward": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Reward signal for learning (0.0-1.0)"
|
||||
},
|
||||
"feedbackLoop": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"previousRunId": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Previous run ID for comparison"
|
||||
},
|
||||
"improvement": {
|
||||
"type": "number",
|
||||
"minimum": -1,
|
||||
"maximum": 1,
|
||||
"description": "Improvement over previous run"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newVulnerabilityPatterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": { "type": "string" },
|
||||
"cwe": { "type": "string" },
|
||||
"confidence": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"description": "New vulnerability patterns learned"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"skillName": "security-testing",
|
||||
"skillVersion": "1.0.0",
|
||||
"requiredTools": [
|
||||
"jq"
|
||||
],
|
||||
"optionalTools": [
|
||||
"npm",
|
||||
"semgrep",
|
||||
"trivy",
|
||||
"ajv",
|
||||
"jsonschema",
|
||||
"python3"
|
||||
],
|
||||
"schemaPath": "schemas/output.json",
|
||||
"requiredFields": [
|
||||
"skillName",
|
||||
"status",
|
||||
"output",
|
||||
"output.summary",
|
||||
"output.findings",
|
||||
"output.owaspCategories"
|
||||
],
|
||||
"requiredNonEmptyFields": [
|
||||
"output.summary"
|
||||
],
|
||||
"mustContainTerms": [
|
||||
"OWASP",
|
||||
"security",
|
||||
"vulnerability"
|
||||
],
|
||||
"mustNotContainTerms": [
|
||||
"TODO",
|
||||
"placeholder",
|
||||
"FIXME"
|
||||
],
|
||||
"enumValidations": {
|
||||
".status": [
|
||||
"success",
|
||||
"partial",
|
||||
"failed",
|
||||
"skipped"
|
||||
]
|
||||
}
|
||||
}
|
||||
+18
-5
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
bin
|
||||
obj
|
||||
@@ -9,17 +10,29 @@ obj
|
||||
*.user
|
||||
log*.txt
|
||||
secured-config
|
||||
build
|
||||
venv
|
||||
*.c
|
||||
*.pyd
|
||||
cython_debug*
|
||||
dist-dlls
|
||||
dist-azaion
|
||||
Azaion*.exe
|
||||
Azaion*.bin
|
||||
|
||||
azaion\.*\.big
|
||||
_internal
|
||||
dist
|
||||
*.jpg
|
||||
|
||||
.env
|
||||
docker-compose.override.yml
|
||||
|
||||
# AI Training credentials
|
||||
ai-training/config.yaml
|
||||
ai-training/cdn.yaml
|
||||
ai-training/offset.yaml
|
||||
ai-training/_other/
|
||||
|
||||
# UI
|
||||
ui/node_modules
|
||||
ui/dist
|
||||
|
||||
*.enc
|
||||
key-fragment*.bin
|
||||
images.tar
|
||||
|
||||
Vendored
-33
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Azaion.Suite (with credentials)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
|
||||
"args": ["credsManual", "-e", "test-admin@azaion.com", "-p", "Az@1on1000TestT-addminn11"],
|
||||
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Launch Azaion.Suite",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Attach to Azaion.Suite",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processName": "Azaion.Suite"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-41
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Azaion.Suite.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/Azaion.Suite.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/Azaion.Suite.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,661 +0,0 @@
|
||||
<Window x:Class="Azaion.Annotator.Annotator"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF"
|
||||
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
||||
xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
||||
xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
|
||||
mc:Ignorable="d"
|
||||
xmlns:local="clr-namespace:Azaion.Annotator"
|
||||
Title="Azaion Annotator" Height="800" Width="1100"
|
||||
WindowState="Maximized"
|
||||
>
|
||||
|
||||
<Window.Resources>
|
||||
<Style x:Key="DataGridCellStyle1" TargetType="{x:Type DataGridCell}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type DataGridCell}">
|
||||
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
|
||||
<ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="SteelBlue"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static DataGrid.FocusBorderBrushKey}}"/>
|
||||
</Trigger>
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="true"/>
|
||||
<Condition Property="Selector.IsSelectionActive" Value="false"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="Background" Value="SteelBlue"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
|
||||
</MultiTrigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<local:GradientStyleSelector x:Key="GradientStyleSelector"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Name="GlobalGrid"
|
||||
ShowGridLines="False"
|
||||
Background="Black">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" Name="DetectionSection" />
|
||||
<RowDefinition Height="0" Name="GpsSplitterRow" />
|
||||
<RowDefinition Height="0" Name="GpsSectionRow"/>
|
||||
<RowDefinition Height="28" Name="ProgressBarSection"/>
|
||||
<RowDefinition Height="32" Name="ButtonsSection"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
Name="MainGrid"
|
||||
ShowGridLines="False"
|
||||
Background="Black"
|
||||
HorizontalAlignment="Stretch">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="80"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Menu Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Background="Black">
|
||||
<MenuItem Header="Файл" Foreground="#FFBDBCBC" Margin="0,3,0,0">
|
||||
<MenuItem x:Name="OpenFolderItem"
|
||||
Foreground="Black"
|
||||
IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
|
||||
<MenuItem x:Name="OpenHelpWindow"
|
||||
Foreground="Black"
|
||||
IsEnabled="True" Header="Як анотувати" Click="OpenHelpWindowClick"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="30"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="1"
|
||||
x:Name="TbFolder"></TextBox>
|
||||
<Button
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="1"
|
||||
Click="OpenFolderButtonClick">
|
||||
. . .
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="1"
|
||||
Foreground="LightGray"
|
||||
Content="Фільтр: "/>
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="1"
|
||||
x:Name="TbFilter"
|
||||
TextChanged="TbFilter_OnTextChanged">
|
||||
</TextBox>
|
||||
</Grid>
|
||||
|
||||
<ListView Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Name="LvFiles"
|
||||
Background="Black"
|
||||
SelectedItem="{Binding Path=SelectedVideo}"
|
||||
Foreground="#FFDDDDDD"
|
||||
>
|
||||
<ListView.Resources>
|
||||
<Style TargetType="{x:Type ListViewItem}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasAnnotations}" Value="true">
|
||||
<Setter Property="Background" Value="#FF505050"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value=" DimGray" />
|
||||
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="DimGray"></Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
<EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter>
|
||||
</Style>
|
||||
</ListView.Resources>
|
||||
<ListView.ContextMenu>
|
||||
<ContextMenu Name="LvFilesContextMenu">
|
||||
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
|
||||
<MenuItem Header="Видалити..." Click="DeleteMedia" Background="WhiteSmoke" />
|
||||
</ContextMenu>
|
||||
</ListView.ContextMenu>
|
||||
<ListView.View>
|
||||
<GridView>
|
||||
<GridViewColumn Width="Auto"
|
||||
Header="Файл"
|
||||
DisplayMemberBinding="{Binding Path=Name}"/>
|
||||
<GridViewColumn Width="Auto"
|
||||
Header="Тривалість"
|
||||
DisplayMemberBinding="{Binding Path=DurationStr}"/>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
|
||||
<controls1:CameraConfigControl
|
||||
x:Name="CameraConfigControl"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4"
|
||||
Camera="{Binding Camera, RelativeSource={RelativeSource AncestorType=Window}, Mode=OneWay}"
|
||||
>
|
||||
|
||||
</controls1:CameraConfigControl>
|
||||
|
||||
<controls1:DetectionClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="5">
|
||||
</controls1:DetectionClasses>
|
||||
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
ResizeDirection="Columns"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="5"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
DragCompleted="Thumb_OnDragCompleted"/>
|
||||
|
||||
<wpf:VideoView
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="5"
|
||||
x:Name="VideoView">
|
||||
<controls1:CanvasEditor x:Name="Editor"
|
||||
Background="#01000000"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</wpf:VideoView>
|
||||
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
ResizeDirection="Columns"
|
||||
Grid.Column="3"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="5"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
DragCompleted="Thumb_OnDragCompleted"
|
||||
/>
|
||||
|
||||
<DataGrid x:Name="DgAnnotations"
|
||||
Grid.Column="4"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="5"
|
||||
Background="Black"
|
||||
Foreground="White"
|
||||
RowHeaderWidth="0"
|
||||
Padding="2 0 0 0"
|
||||
AutoGenerateColumns="False"
|
||||
SelectionMode="Extended"
|
||||
CellStyle="{DynamicResource DataGridCellStyle1}"
|
||||
IsReadOnly="True"
|
||||
CanUserResizeRows="False"
|
||||
CanUserResizeColumns="False"
|
||||
RowStyleSelector="{StaticResource GradientStyleSelector}"
|
||||
local:GradientStyleSelector.ClassProvider="{Binding ClassProvider, RelativeSource={RelativeSource AncestorType=local:Annotator}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Width="60"
|
||||
Header="Кадр"
|
||||
CanUserSort="False"
|
||||
Binding="{Binding Path=TimeStr}">
|
||||
<DataGridTextColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#252525"></Setter>
|
||||
</Style>
|
||||
</DataGridTextColumn.HeaderStyle>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Header="Клас"
|
||||
Binding="{Binding Path=ClassName}"
|
||||
CanUserSort="False">
|
||||
<DataGridTextColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#252525"></Setter>
|
||||
</Style>
|
||||
</DataGridTextColumn.HeaderStyle>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
<GridSplitter
|
||||
Name="GpsSplitter"
|
||||
Background="DarkGray"
|
||||
ResizeDirection="Rows"
|
||||
Grid.Row="1"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<controls:MapMatcher
|
||||
x:Name="MapMatcherComponent"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
/>
|
||||
|
||||
<controls2:UpdatableProgressBar x:Name="VideoSlider"
|
||||
Grid.Column="0"
|
||||
Grid.Row="3"
|
||||
Background="#252525"
|
||||
Foreground="LightBlue">
|
||||
</controls2:UpdatableProgressBar>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid
|
||||
Name="Buttons"
|
||||
Grid.Row="4"
|
||||
Background="Black"
|
||||
>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" /> <!-- 0 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 1 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 2 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 3 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 4 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 5 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 6 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 7 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 8 -->
|
||||
<ColumnDefinition Width="56" /> <!-- 9 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 10 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 11 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 12 -->
|
||||
<ColumnDefinition Width="28" /> <!-- 13 -->
|
||||
<ColumnDefinition Width="*" /> <!-- 14-->
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
|
||||
Click="PlayClick">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="m295.84 146.049-256-144c-4.96-2.784-11.008-2.72-15.904.128-4.928
|
||||
2.88-7.936 8.128-7.936 13.824v288c0 5.696 3.008 10.944 7.936 13.824 2.496 1.44 5.28 2.176 8.064 2.176 2.688
|
||||
0 5.408-.672 7.84-2.048l256-144c5.024-2.848 8.16-8.16 8.16-13.952s-3.136-11.104-8.16-13.952z" />
|
||||
</Button>
|
||||
<Button Grid.Column="1" Padding="2" Width="25" Height="25" ToolTip="Пауза/Відновити. Клавіша: [Пробіл]" Background="Black" BorderBrush="Black"
|
||||
Click="PauseClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="F1 M320,320z M0,0z M112,0L16,0C7.168,0,0,7.168,0,16L0,304C0,312.832,7.168,320,16,320L112,320C120.832,320,128,312.832,128,304L128,16C128,7.168,120.832,0,112,0z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="F1 M320,320z M0,0z M304,0L208,0C199.168,0,192,7.168,192,16L192,304C192,312.832,199.168,320,208,320L304,320C312.832,320,320,312.832,320,304L320,16C320,7.168,312.832,0,304,0z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="2" Padding="2" Width="25" Height="25" ToolTip="Зупинити перегляд" Background="Black" BorderBrush="Black"
|
||||
Click="StopClick">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="m288 0h-256c-17.632 0-32 14.368-32 32v256c0 17.632 14.368 32 32 32h256c17.632
|
||||
0 32-14.368 32-32v-256c0-17.632-14.368-32-32-32z" />
|
||||
</Button>
|
||||
<Button Grid.Column="3" Padding="2" Width="25" Height="25" ToolTip="На 1 кадр назад. +[Ctrl] на 5 секунд назад. Клавіша: [Вліво]" Background="Black" BorderBrush="Black"
|
||||
Click="PreviousFrameClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m23.026 4.99579v22.00155c.00075.77029-.83285 1.25227-1.49993.86724l-19.05188-11.00078c-.66693-.38492-.66693-1.34761
|
||||
0-1.73254l19.05188-11.00078c.62227-.35929 1.49993.0539 1.49993.86531z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m29.026 4h-2c-.554 0-1 .446-1 1v22c0 .554.446 1 1 1h2c.554 0 1-.446 1-1v-22c0-.554-.446-1-1-1z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="4" Padding="2" Width="25" Height="25" ToolTip="На 1 кадр вперед. +[Ctrl] на 5 секунд вперед. Клавіша: [Вправо]" Background="Black" BorderBrush="Black"
|
||||
Click="NextFrameClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m8.974 4.99579v22.00155c-.00075.77029.83285 1.25227 1.49993.86724l19.05188-11.00078c.66693-.38492.66693-1.34761
|
||||
0-1.73254l-19.05188-11.00078c-.62227-.35929-1.49993.0539-1.49993.86531z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m2.974 4h2c.554 0 1 .446 1 1v22c0 .554-.446 1-1 1h-2c-.554 0-1-.446-1-1v-22c0-.554.446-1 1-1z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="5" Padding="2" Width="25" Height="25" ToolTip="Зберегти анотації та продовжити. Клавіша: [Ентер]" Background="Black" BorderBrush="Black"
|
||||
Click="SaveAnnotationsClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m30.71 7.29-6-6a1 1 0 0 0 -.71-.29h-2v8a2 2 0 0 1 -2 2h-8a2 2 0 0
|
||||
1 -2-2v-8h-6a3 3 0 0 0 -3 3v24a3 3 0 0 0 3 3h2v-9a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v9h2a3 3 0 0 0 3-3v-20a1 1 0 0 0 -.29-.71z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m12 1h8v8h-8z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m23 21h-14a1 1 0 0 0 -1 1v9h16v-9a1 1 0 0 0 -1-1z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="6" Padding="2" Width="25" Height="25" ToolTip="Видалити обрані анотації. Клавіша: [Del]" Background="Black" BorderBrush="Black"
|
||||
Click="RemoveSelectedClick">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="M395.439,368.206h18.158v45.395h-45.395v-18.158h27.236V368.206z M109.956,413.601h64.569v-18.158h-64.569V413.601z
|
||||
M239.082,413.601h64.558v-18.158h-64.558V413.601z M18.161,368.206H0.003v45.395h45.395v-18.158H18.161V368.206z M18.161,239.079
|
||||
H0.003v64.562h18.158V239.079z M18.161,109.958H0.003v64.563h18.158V109.958z M0.003,45.395h18.158V18.158h27.237V0H0.003V45.395z
|
||||
M174.519,0h-64.563v18.158h64.563V0z M303.64,0h-64.558v18.158h64.558V0z M368.203,0v18.158h27.236v27.237h18.158V0H368.203z
|
||||
M395.439,303.642h18.158v-64.562h-18.158V303.642z M395.439,174.521h18.158v-64.563h-18.158V174.521z M325.45,93.187
|
||||
c-11.467-11.464-30.051-11.464-41.518,0l-77.135,77.129l-77.129-77.129c-11.476-11.464-30.056-11.464-41.521,0
|
||||
c-11.476,11.47-11.476,30.062,0,41.532l77.118,77.123l-77.124,77.124c-11.476,11.479-11.476,30.062,0,41.529
|
||||
c5.73,5.733,13.243,8.605,20.762,8.605c7.516,0,15.028-2.872,20.765-8.605l77.129-77.124l77.129,77.124
|
||||
c5.728,5.733,13.246,8.605,20.765,8.605c7.513,0,15.025-2.872,20.759-8.605c11.479-11.467,11.479-30.062,0-41.529l-77.124-77.124
|
||||
l77.124-77.123C336.923,123.243,336.923,104.656,325.45,93.187z" />
|
||||
</Button>
|
||||
<Button Grid.Column="7" Padding="2" Width="25" Height="25" ToolTip="Видалити всі аннотації. Клавіша: [X]" Background="Black" BorderBrush="Black"
|
||||
Click="RemoveAllClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m66.1455 13.1562c2.2083-4.26338 7.4546-5.92939 11.718-3.72109 4.2702 2.21179
|
||||
5.9335 7.47029 3.7121 11.73549l-8.9288 17.1434c-.3573.6862-.8001 1.3124-1.312 1.8677 2.44 3.6128 3.1963 8.2582 1.6501
|
||||
12.6558-.3523 1.002-.7242 2.0466-1.1108 3.1145-.1645.4546-.6923.659-1.1208.4351l-28.8106-15.0558c-.4666-.2438-.5746-.8639-.2219-1.2547.7171-.7943
|
||||
1.4152-1.5917 2.0855-2.3761 3.1513-3.6881 7.8213-5.7743 12.5381-5.6197.0534-.1099.1097-.2193.1689-.3283z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m37.7187 44.9911c-.3028-.1582-.6723-.1062-.9226.1263-1.7734 1.6478-3.5427
|
||||
3.0861-5.1934 4.1101-5.5739 3.4578-10.1819 4.704-13.0435 5.1463-1.6736.2587-3.032 1.3362-3.6937 2.7335-.6912 1.4595-.6391
|
||||
3.3721.7041 4.8522 1.48 1.6309 3.6724 3.7893 6.8345 6.3861.1854.1523.4298.2121.665.1649 2.2119-.4446 4.5148-.8643
|
||||
6.5245-1.9149.5849-.3058 1.4606-.8505 2.5588-1.7923 1.0935-.9379 2.7579-.8372 3.7175.2247.9595 1.062.8509 2.6831-.2426
|
||||
3.621-1.3886 1.1908-2.596 1.965-3.5534 2.4655-.7833.4094-1.603.7495-2.4399 1.0396-.6358.2203-.7846 1.0771-.2325 1.4619
|
||||
1.5928 1.1099 3.3299 2.2689 5.223 3.4729.9682.6158 1.9229 1.1946 2.8588 1.7383.2671.1552.6002.141.8515-.0387 1.351-.9664
|
||||
2.5145-1.9362 3.463-2.8261 2.1458-2.013 3.9974-4.231 5.4947-6.7819.7286-1.2414 2.3312-1.6783 3.5794-.9757s1.6693 2.2785.9406
|
||||
3.52c-1.7525 2.9859-3.9213 5.6002-6.4356 7.9591-.4351.4082-.9081.8302-1.4172 1.2601-.4505.3805-.3701 1.1048.1642 1.3543 3.184
|
||||
1.4867 5.8634 2.4904 7.7071 3.1131 2.6745.9033 5.5327-.1298 7.0673-2.4281 1.9401-2.9057 5.3476-8.3855 8.2732-15.0533.7591-1.7301
|
||||
1.5313-3.6163 2.2883-5.5494.1485-.3793-.0133-.8092-.3743-.9978z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m22.9737 37.9072c2.0802 0 3.7666-1.6864 3.7666-3.7667 0-2.0802-1.6864-3.7666-3.7666-3.7666-2.0803
|
||||
0-3.7667 1.6864-3.7667 3.7666 0 2.0803 1.6864 3.7667 3.7667 3.7667z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m12.7198 49.4854c2.0802 0 3.7666-1.6864 3.7666-3.7667 0-2.0802-1.6864-3.7666-3.7666-3.7666-2.0803
|
||||
0-3.76667 1.6864-3.76668 3.7666 0 2.0803 1.68638 3.7667 3.76668 3.7667z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="TurnOffVolumeBtn"
|
||||
Visibility="Visible"
|
||||
Grid.Column="8" Padding="2" Width="25"
|
||||
Height="25"
|
||||
ToolTip="Виключити звук. Клавіша: [M]" Background="Black" BorderBrush="Black"
|
||||
Click="TurnOffVolume">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="m9.383 3.07602c.18269.07574.33881.20395.44863.36842.10983.16447.16837.35781.16837
|
||||
.55558v11.99998c-.00004.1978-.05871.3911-.1686.5555-.10988.1644-.26605.2925-.44875.3682s-.38373.0955-.57768.0569-.37212-.1338-.51197-.2736l-3.707
|
||||
-3.707h-2.586c-.26522 0-.51957-.1053-.70711-.2929-.18753-.1875-.29289-.4419-.29289-.7071v-3.99998c0-.26522.10536-.51957.29289-.70711.18754-.18754
|
||||
.44189-.29289.70711-.29289h2.586l3.707-3.707c.13985-.13994.31805-.23524.51208-.27387.19402-.03863.39515-.01884.57792.05687zm5.274-.147c.1875-.18747
|
||||
.4418-.29279.707-.29279s.5195.10532.707.29279c.9298.92765 1.6672 2.02985 2.1699 3.24331.5026 1.21345.7606 2.51425.7591 3.82767.0015 1.3135-.2565
|
||||
2.6143-.7591 3.8277-.5027 1.2135-1.2401 2.3157-2.1699 3.2433-.1886.1822-.4412.283-.7034.2807s-.513-.1075-.6984-.2929-.2906-.4362-.2929-.6984
|
||||
.0985-.5148.2807-.7034c.7441-.7419 1.3342-1.6237 1.7363-2.5945.4022-.9709.6083-2.0117.6067-3.0625 0-2.20998-.894-4.20798-2.343-5.65698-.1875
|
||||
-.18753-.2928-.44184-.2928-.707 0-.26517.1053-.51948.2928-.707zm-2.829 2.828c.0929-.09298.2032-.16674.3246-.21706.1214-.05033.2515-.07623.3829
|
||||
-.07623s.2615.0259.3829.07623c.1214.05032.2317.12408.3246.21706.5579.55666 1.0003 1.21806 1.3018 1.94621.3015.72814.4562 1.50868.4552 2.29677.001
|
||||
.7881-.1537 1.5686-.4553 2.2968-.3015.7281-.7439 1.3895-1.3017 1.9462-.1876.1877-.4421.2931-.7075.2931s-.5199-.1054-.7075-.2931c-.1876-.1876
|
||||
-.2931-.4421-.2931-.7075 0-.2653.1055-.5198.2931-.7075.3722-.3708.6673-.8116.8685-1.2969.2011-.4854.3043-1.0057.3035-1.5311.0008-.52537-.1023
|
||||
-1.04572-.3035-1.53107-.2011-.48536-.4963-.92612-.8685-1.29691-.093-.09288-.1667-.20316-.2171-.32456-.0503-.1214-.0762-.25153-.0762-.38294
|
||||
0-.13142.0259-.26155.0762-.38294.0504-.1214.1241-.23169.2171-.32456z" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="TurnOnVolumeBtn"
|
||||
Visibility="Collapsed"
|
||||
Grid.Column="8" Padding="2" Width="25"
|
||||
Height="25"
|
||||
ToolTip="Включити звук. Клавіша: [M]" Background="Black" BorderBrush="Black"
|
||||
Click="TurnOnVolume">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m9.38268 3.07615c.37368.15478.61732.51942.61732.92388v11.99997c0
|
||||
.4045-.24364.7691-.61732.9239-.37367.1548-.80379.0692-1.08979-.2168l-3.7071-3.7071h-2.58579c-.55228
|
||||
0-1-.4477-1-1v-3.99997c0-.55229.44772-1 1-1h2.58579l3.7071-3.70711c.286-.286.71612-.37155 1.08979-.21677z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m12.2929 7.29289c.3905-.39052 1.0237-.39052 1.4142 0l1.2929
|
||||
1.2929 1.2929-1.2929c.3905-.39052 1.0237-.39052 1.4142 0 .3905.39053.3905 1.02369 0 1.41422l-1.2929 1.29289
|
||||
1.2929 1.2929c.3905.3905.3905 1.0237 0 1.4142s-1.0237.3905-1.4142 0l-1.2929-1.2929-1.2929
|
||||
1.2929c-.3905.3905-1.0237.3905-1.4142 0s-.3905-1.0237 0-1.4142l1.2929-1.2929-1.2929-1.29289c-.3905-.39053-.3905-1.02369
|
||||
0-1.41422z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
|
||||
<controls2:UpdatableProgressBar
|
||||
x:Name="Volume"
|
||||
Grid.Column="9"
|
||||
Width="70" Height="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="#252525" BorderBrush="#252525" Foreground="LightBlue"
|
||||
Maximum="100" Minimum="0">
|
||||
</controls2:UpdatableProgressBar>
|
||||
|
||||
<Button
|
||||
x:Name="AIDetectBtn"
|
||||
IsEnabled="False"
|
||||
Grid.Column="10"
|
||||
Padding="2" Width="25"
|
||||
Height="25"
|
||||
ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
|
||||
Click="AIDetectBtn_OnClick">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
|
||||
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
|
||||
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
|
||||
24.168-16.433 39.53-16.433zm59.98 204.329h-39.825l30.577-117.964h58.32l30.577 117.964h-39.825l-3.051-18.686h-33.725l-3.048 18.686zm15.645-81.726l-5.801
|
||||
33.032h19.945l-5.61-33.032h-8.534zm74.007 81.726V171.634h37.749v117.964h-37.749zm161.348-35.797v30.763c0 3.165 2.587 5.751 5.752 5.751h45.199c3.165 0
|
||||
5.752-2.586 5.752-5.751v-30.763c0-3.165-2.587-5.752-5.752-5.752h-45.199c-3.165 0-5.752 2.587-5.752 5.752zm0-70.639v30.762c0 3.163 2.587 5.752 5.752
|
||||
5.752h45.199c3.165 0 5.752-2.589 5.752-5.752v-30.762c0-3.168-2.587-5.752-5.752-5.752h-45.199c-3.165 0-5.752 2.584-5.752 5.752zm0 141.278v30.763c0 3.165
|
||||
2.587 5.752 5.752 5.752h45.199c3.165 0 5.752-2.587 5.752-5.752V324.44c0-3.165-2.587-5.751-5.752-5.751h-45.199c-3.165 0-5.752 2.586-5.752 5.751zm0-211.92v30.763c0
|
||||
3.164 2.587 5.751 5.752 5.751h45.199c3.165 0 5.752-2.587 5.752-5.751V112.52c0-3.165-2.587-5.752-5.752-5.752h-45.199c-3.165 0-5.752 2.587-5.752 5.752zM56.703
|
||||
253.801v30.763c0 3.165-2.587 5.751-5.752 5.751H5.752c-3.165 0-5.752-2.586-5.752-5.751v-30.763c0-3.165 2.587-5.752 5.752-5.752h45.199c3.165 0 5.752 2.587
|
||||
5.752 5.752zm0-70.639v30.762c0 3.163-2.587 5.752-5.752 5.752H5.752c-3.165 0-5.752-2.589-5.752-5.752v-30.762c0-3.168 2.587-5.752 5.752-5.752h45.199c3.165
|
||||
0 5.752 2.584 5.752 5.752zm0 141.278v30.763c0 3.165-2.587 5.752-5.752 5.752H5.752c-3.165 0-5.752-2.587-5.752-5.752V324.44c0-3.165 2.587-5.751
|
||||
5.752-5.751h45.199c3.165 0 5.752 2.586 5.752 5.751zm0-211.92v30.763c0 3.164-2.587 5.751-5.752 5.751H5.752c-3.165 0-5.752-2.587-5.752-5.751V112.52c0-3.165
|
||||
2.587-5.752 5.752-5.752h45.199c3.165 0 5.752 2.587 5.752 5.752zM346.579 415.7h30.763c3.162 0 5.751 2.587 5.751 5.752v45.199c0 3.165-2.589 5.752-5.751
|
||||
5.752h-30.763c-3.167 0-5.752-2.587-5.752-5.752v-45.199c0-3.165 2.585-5.752 5.752-5.752zm-70.642 0H306.7c3.165 0 5.751 2.587 5.751 5.752v45.199c0 3.165-2.586
|
||||
5.752-5.751 5.752h-30.763c-3.165 0-5.752-2.587-5.752-5.752v-45.199c0-3.165 2.587-5.752 5.752-5.752zm-70.639 0h30.762c3.165 0 5.752 2.587 5.752 5.752v45.199c0
|
||||
3.165-2.587 5.752-5.752 5.752h-30.762c-3.165 0-5.752-2.587-5.752-5.752v-45.199c0-3.165 2.587-5.752 5.752-5.752zm-70.64 0h30.763c3.165 0 5.752 2.587 5.752
|
||||
5.752v45.199c0 3.165-2.587 5.752-5.752 5.752h-30.763c-3.165 0-5.751-2.587-5.751-5.752v-45.199c0-3.165 2.586-5.752 5.751-5.752zM346.579 0h30.763c3.162 0 5.751
|
||||
2.587 5.751 5.752v45.199c0 3.165-2.589 5.752-5.751 5.752h-30.763c-3.167 0-5.752-2.587-5.752-5.752V5.752c0-3.165 2.585-5.752 5.752-5.752zm-70.642 0H306.7c3.165
|
||||
0 5.751 2.587 5.751 5.752v45.199c0 3.165-2.586 5.752-5.751 5.752h-30.763c-3.165 0-5.752-2.587-5.752-5.752V5.752c0-3.165 2.587-5.752 5.752-5.752zm-70.639
|
||||
0h30.762c3.165 0 5.752 2.587 5.752 5.752v45.199c0 3.165-2.587 5.752-5.752 5.752h-30.762c-3.165 0-5.752-2.587-5.752-5.752V5.752c0-3.165 2.587-5.752
|
||||
5.752-5.752zm-70.64 0h30.763c3.165 0 5.752 2.587 5.752 5.752v45.199c0 3.165-2.587 5.752-5.752 5.752h-30.763c-3.165 0-5.751-2.587-5.751-5.752V5.752c0-3.165
|
||||
2.586-5.752 5.751-5.752zm233.027 111.097H144.317a30.11 30.11 0 00-21.35 8.844l-.049.049a30.117 30.117 0 00-8.844 21.348v189.728c0 8.292 3.414 15.847 8.9
|
||||
21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053
|
||||
8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" />
|
||||
</Button>
|
||||
<Button Grid.Column="11" Padding="2" Width="25" Height="25" ToolTip="Показати GPS. Клавіша: [M]" Background="Black" BorderBrush="Black"
|
||||
Click="SwitchGpsPanel">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V520 H580 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="M307.1,311.97c-12.55-14-22.75-31.86-32.9-47.68c-10.23-15.94-19.78-32.43-27.3-49.83c-7.03-16.28-12.48-33.08-9.25-50.97
|
||||
c2.87-15.93,11.75-31.29,23.84-42.03c22.3-19.8,57.81-22.55,82.67-5.98c29.17,19.45,39.48,55.06,27.59,87.55
|
||||
c-6.8,18.59-16.41,36.14-27.02,52.8C332.76,274.63,320.84,294.45,307.1,311.97z M307.01,143.45c-38.65-0.46-39.68,59.79-0.95,60.47
|
||||
c16.47,0.29,30.83-13.34,31-29.9C337.22,157.75,323.24,143.65,307.01,143.45z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="M367.34,310.68c10.09,2.5,23.61,4.83,31.46,12.19c11.05,10.35-5.42,18.17-14.21,21.43c-24.55,9.11-53.52,10.41-79.44,10.11
|
||||
c-25.7-0.3-54.62-1.23-78.68-11.19c-7.68-3.18-21.53-10.2-12.52-19.47c8.26-8.49,23.33-11.42,34.5-12.94
|
||||
c-5.15,1.98-16.18,5.12-17.07,11.49c-1,7.13,9.78,10.81,15.02,12.59c18.28,6.22,38.72,7.58,57.89,7.73
|
||||
c18.91,0.15,38.85-0.72,57.13-5.92c5.72-1.63,18.65-4.74,20.7-11.49c2.28-7.47-9.8-11.66-15.04-13.71
|
||||
C367.18,311.22,367.26,310.95,367.34,310.68z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="12"
|
||||
Padding="2"
|
||||
Width="25"
|
||||
Height="25"
|
||||
ToolTip="Показати обєкти по аудіоаналізу. Клавіша: [R]" Background="Black" BorderBrush="Black"
|
||||
Click="SoundDetections">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<GeometryDrawing Geometry="m19.05,171.43v152.38">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Geometry="m95.24,95.24v342.86">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Geometry="m171.43,209.52v76.19">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Geometry="m247.62,133.33v259.58">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
|
||||
<GeometryDrawing Geometry="m323.81,19.05v457.14">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Geometry="m401.43,86.69v342.86">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
<GeometryDrawing Geometry="m473.43,209.02v76.19">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="13"
|
||||
Padding="2"
|
||||
Width="25"
|
||||
Height="25"
|
||||
ToolTip="Аналіз стану БПЛА. Клавіша: [K]" Background="Black" BorderBrush="Black"
|
||||
Click="RunDroneMaintenance">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="
|
||||
M128,7.10542736e-15 C198.692448,7.10542736e-15 256,57.307552 256,128 C256,140.931179 254.082471,153.414494 250.516246,165.181113 L384,298.666667
|
||||
C407.564149,322.230816 407.564149,360.435851 384,384 C360.435851,407.564149 322.230816,407.564149 298.666667,384 L165.181113,250.516246
|
||||
C153.414494,254.082471 140.931179,256 128,256 C57.307552,256 7.10542736e-15,198.692448 7.10542736e-15,128 C7.10542736e-15,114.357909
|
||||
2.13416363,101.214278 6.08683609,88.884763 L66.6347809,149.333333 L126.649,129.346 L129.329,126.666 L149.333333,66.7080586 L88.7145729,6.14152881
|
||||
C101.0933,2.15385405 114.29512,7.10542736e-15 128,7.10542736e-15 Z" />
|
||||
</Button>
|
||||
<StatusBar Grid.Column="14"
|
||||
Background="#252525"
|
||||
Foreground="White">
|
||||
<StatusBar.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
</Grid>
|
||||
</ItemsPanelTemplate>
|
||||
</StatusBar.ItemsPanel>
|
||||
<StatusBarItem Grid.Column="0">
|
||||
<TextBlock Margin="3 0 0 0" x:Name="StatusClock" FontSize="16" Text="00:00 / 00:00"></TextBlock>
|
||||
</StatusBarItem>
|
||||
<Separator Grid.Column="1" />
|
||||
<StatusBarItem Grid.Column="2">
|
||||
<TextBlock Margin="3 0 0 0" x:Name="StatusHelp" FontSize="12" ></TextBlock>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="3">
|
||||
<TextBlock x:Name="Status"></TextBlock>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
<!-- /Buttons -->
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,728 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Events;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.Common.Services;
|
||||
using Azaion.Common.Services.Inference;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
using Size = System.Windows.Size;
|
||||
using IntervalTree;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class Annotator
|
||||
{
|
||||
private readonly AppConfig? _appConfig;
|
||||
private readonly LibVLC _libVlc;
|
||||
private readonly MediaPlayer _mediaPlayer;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly FormState _formState;
|
||||
|
||||
private readonly IConfigUpdater _configUpdater;
|
||||
private readonly HelpWindow _helpWindow;
|
||||
private readonly ILogger<Annotator> _logger;
|
||||
private readonly IDbFactory _dbFactory;
|
||||
private readonly IInferenceService _inferenceService;
|
||||
|
||||
private bool _suspendLayout;
|
||||
private bool _gpsPanelVisible;
|
||||
|
||||
private readonly CancellationTokenSource _mainCancellationSource = new();
|
||||
public CancellationTokenSource DetCancelSource = new();
|
||||
private bool _isInferenceNow;
|
||||
|
||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
|
||||
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
|
||||
|
||||
public ObservableCollection<MediaFile> AllMediaFiles { get; set; } = new();
|
||||
private ObservableCollection<MediaFile> FilteredMediaFiles { get; set; } = new();
|
||||
public Dictionary<string, MediaFile> MediaFilesDict = new();
|
||||
|
||||
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
|
||||
public string MainTitle { get; set; }
|
||||
|
||||
public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig();
|
||||
private static readonly Guid ReloadTaskId = Guid.NewGuid();
|
||||
|
||||
private readonly IAnnotationPathResolver _pathResolver;
|
||||
private readonly IDetectionClassProvider _classProvider;
|
||||
|
||||
public IDetectionClassProvider ClassProvider => _classProvider;
|
||||
|
||||
public Annotator(
|
||||
IConfigUpdater configUpdater,
|
||||
IOptions<AppConfig> appConfig,
|
||||
LibVLC libVlc,
|
||||
MediaPlayer mediaPlayer,
|
||||
IMediator mediator,
|
||||
FormState formState,
|
||||
HelpWindow helpWindow,
|
||||
ILogger<Annotator> logger,
|
||||
IDbFactory dbFactory,
|
||||
IInferenceService inferenceService,
|
||||
IInferenceClient inferenceClient,
|
||||
IGpsMatcherService gpsMatcherService,
|
||||
IAnnotationPathResolver pathResolver,
|
||||
IDetectionClassProvider classProvider)
|
||||
{
|
||||
_pathResolver = pathResolver;
|
||||
_classProvider = classProvider;
|
||||
// Initialize configuration and services BEFORE InitializeComponent so bindings can see real values
|
||||
_appConfig = appConfig.Value;
|
||||
_configUpdater = configUpdater;
|
||||
_libVlc = libVlc;
|
||||
_mediaPlayer = mediaPlayer;
|
||||
_mediator = mediator;
|
||||
_formState = formState;
|
||||
_helpWindow = helpWindow;
|
||||
_logger = logger;
|
||||
_dbFactory = dbFactory;
|
||||
_inferenceService = inferenceService;
|
||||
|
||||
// Ensure bindings (e.g., Camera) resolve immediately
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
MainTitle = $"Azaion Annotator {Constants.GetLocalVersion()}";
|
||||
Title = MainTitle;
|
||||
|
||||
Loaded += OnLoaded;
|
||||
Closed += OnFormClosed;
|
||||
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
|
||||
TbFolder.TextChanged += async (_, _) =>
|
||||
{
|
||||
if (!Path.Exists(TbFolder.Text))
|
||||
return;
|
||||
try
|
||||
{
|
||||
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
|
||||
await ReloadFiles();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, e.Message);
|
||||
}
|
||||
};
|
||||
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
||||
MapMatcherComponent.Init(_appConfig, gpsMatcherService);
|
||||
|
||||
// When camera settings change, persist config
|
||||
CameraConfigControl.CameraChanged += (_, _) =>
|
||||
{
|
||||
if (_appConfig != null)
|
||||
_configUpdater.Save(_appConfig);
|
||||
};
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Core.Initialize();
|
||||
InitControls();
|
||||
|
||||
_suspendLayout = true;
|
||||
|
||||
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.LeftPanelWidth ?? Constants.DEFAULT_LEFT_PANEL_WIDTH);
|
||||
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.RightPanelWidth ?? Constants.DEFAULT_RIGHT_PANEL_WIDTH);
|
||||
|
||||
_suspendLayout = false;
|
||||
TbFolder.Text = _appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR;
|
||||
|
||||
LvClasses.Init(_appConfig?.AnnotationConfig.DetectionClasses ?? Constants.DefaultAnnotationClasses);
|
||||
}
|
||||
|
||||
public void BlinkHelp(string helpText, int times = 2)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
for (int i = 0; i < times; i++)
|
||||
{
|
||||
Dispatcher.Invoke(() => StatusHelp.Text = helpText);
|
||||
await Task.Delay(200);
|
||||
Dispatcher.Invoke(() => StatusHelp.Text = "");
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() => StatusHelp.Text = helpText);
|
||||
});
|
||||
}
|
||||
|
||||
private void InitControls()
|
||||
{
|
||||
VideoView.MediaPlayer = _mediaPlayer;
|
||||
|
||||
//On start playing media
|
||||
_mediaPlayer.Playing += (_, _) =>
|
||||
{
|
||||
uint vw = 0, vh = 0;
|
||||
_mediaPlayer.Size(0, ref vw, ref vh);
|
||||
_formState.CurrentMediaSize = new Size(vw, vh);
|
||||
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
|
||||
};
|
||||
|
||||
LvFiles.MouseDoubleClick += async (_, _) =>
|
||||
{
|
||||
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
|
||||
};
|
||||
|
||||
LvClasses.DetectionClassChanged += (_, args) =>
|
||||
{
|
||||
var selectedClass = args.DetectionClass;
|
||||
Editor.CurrentAnnClass = selectedClass;
|
||||
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
||||
};
|
||||
|
||||
_mediaPlayer.PositionChanged += (_, _) =>
|
||||
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
|
||||
|
||||
VideoSlider.ValueChanged += (_, newValue) =>
|
||||
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
|
||||
|
||||
VideoSlider.KeyDown += (sender, args) =>
|
||||
_mediator.Publish(new KeyEvent(sender, args, WindowEnum.Annotator));
|
||||
|
||||
Volume.ValueChanged += (_, newValue) =>
|
||||
_mediator.Publish(new VolumeChangedEvent((int)newValue));
|
||||
|
||||
DgAnnotations.MouseDoubleClick += (sender, args) =>
|
||||
{
|
||||
if (ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) is DataGridRow dgRow)
|
||||
OpenAnnotationResult((Annotation)dgRow.Item);
|
||||
|
||||
};
|
||||
|
||||
DgAnnotations.KeyUp += async (_, args) =>
|
||||
{
|
||||
switch (args.Key)
|
||||
{
|
||||
case Key.Down: //cursor is already moved by system behaviour
|
||||
OpenAnnotationResult((Annotation)DgAnnotations.SelectedItem);
|
||||
break;
|
||||
case Key.Delete:
|
||||
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.OK)
|
||||
return;
|
||||
|
||||
var res = DgAnnotations.SelectedItems.Cast<Annotation>().ToList();
|
||||
var annotationNames = res.Select(x => x.Name).ToList();
|
||||
|
||||
await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
|
||||
break;
|
||||
}
|
||||
};
|
||||
DgAnnotations.ItemsSource = _formState.AnnotationResults;
|
||||
}
|
||||
|
||||
private void OpenAnnotationResult(Annotation ann)
|
||||
{
|
||||
_mediaPlayer.SetPause(true);
|
||||
if (!ann.IsSplit)
|
||||
Editor.RemoveAllAnns();
|
||||
|
||||
_mediaPlayer.Time = (long)ann.Time.TotalMilliseconds;
|
||||
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
|
||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||
Editor.ClearExpiredAnnotations(ann.Time);
|
||||
});
|
||||
|
||||
ShowAnnotation(ann, showImage: true, openResult: true);
|
||||
}
|
||||
private void SaveUserSettings()
|
||||
{
|
||||
if (_suspendLayout || _appConfig is null)
|
||||
return;
|
||||
|
||||
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||
|
||||
_configUpdater.Save(_appConfig);
|
||||
}
|
||||
|
||||
public void ShowTimeAnnotations(TimeSpan time, bool showImage = false)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
|
||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||
Editor.ClearExpiredAnnotations(time);
|
||||
});
|
||||
var annotations = TimedAnnotations.Query(time).ToList();
|
||||
if (!annotations.Any())
|
||||
return;
|
||||
foreach (var ann in annotations)
|
||||
ShowAnnotation(ann, showImage);
|
||||
}
|
||||
|
||||
private void ShowAnnotation(Annotation annotation, bool showImage = false, bool openResult = false)
|
||||
{
|
||||
Dispatcher.Invoke(async () =>
|
||||
{
|
||||
var imagePath = _pathResolver.GetImagePath(annotation);
|
||||
if (showImage && !annotation.IsSplit && File.Exists(imagePath))
|
||||
{
|
||||
Editor.SetBackground(await imagePath.OpenImage());
|
||||
_formState.BackgroundTime = annotation.Time;
|
||||
}
|
||||
|
||||
if (annotation.SplitTile != null && openResult)
|
||||
{
|
||||
var canvasTileLocation = new CanvasLabel(new YoloLabel(annotation.SplitTile, _formState.CurrentMediaSize),
|
||||
RenderSize);
|
||||
Editor.ZoomTo(new Point(canvasTileLocation.CenterX, canvasTileLocation.CenterY));
|
||||
}
|
||||
else
|
||||
Editor.CreateDetections(annotation, _appConfig?.AnnotationConfig.DetectionClasses ?? [], _formState.CurrentMediaSize);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ReloadAnnotations()
|
||||
{
|
||||
await Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
_formState.AnnotationResults.Clear();
|
||||
TimedAnnotations.Clear();
|
||||
Editor.RemoveAllAnns();
|
||||
|
||||
var mediaHash = _formState.CurrentMedia?.Hash;
|
||||
var mediaName = _formState.CurrentMedia?.Name;
|
||||
|
||||
var annotations = await _dbFactory.Run(async db =>
|
||||
await db.Annotations.LoadWith(x => x.Detections)
|
||||
.Where(x =>
|
||||
(!string.IsNullOrEmpty(mediaHash) && x.MediaHash == mediaHash) ||
|
||||
(x.MediaHash == null && x.OriginalMediaName == mediaName))
|
||||
.OrderBy(x => x.Time)
|
||||
.ToListAsync(token: _mainCancellationSource.Token));
|
||||
|
||||
TimedAnnotations.Clear();
|
||||
_formState.AnnotationResults.Clear();
|
||||
foreach (var ann in annotations)
|
||||
{
|
||||
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
|
||||
_formState.AnnotationResults.Add(ann);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Add manually
|
||||
public void AddAnnotation(Annotation annotation)
|
||||
{
|
||||
var time = annotation.Time;
|
||||
var previousAnnotations = TimedAnnotations.Query(time);
|
||||
TimedAnnotations.Remove(previousAnnotations);
|
||||
TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
|
||||
|
||||
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
|
||||
if (existingResult != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_formState.AnnotationResults.Remove(existingResult);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var dict = _formState.AnnotationResults
|
||||
.Select((x, i) => new { x.Time, Index = i })
|
||||
.ToDictionary(x => x.Time, x => x.Index);
|
||||
|
||||
var index = dict.Where(x => x.Key < time)
|
||||
.OrderBy(x => time - x.Key)
|
||||
.Select(x => x.Value + 1)
|
||||
.FirstOrDefault();
|
||||
_formState.AnnotationResults.Insert(index, annotation);
|
||||
}
|
||||
|
||||
public void ReloadFilesThrottled()
|
||||
{
|
||||
ThrottleExt.Throttle(async () =>
|
||||
{
|
||||
await ReloadFiles();
|
||||
}, ReloadTaskId, TimeSpan.FromSeconds(4));
|
||||
}
|
||||
|
||||
public async Task ReloadFiles()
|
||||
{
|
||||
var dir = new DirectoryInfo(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR);
|
||||
if (!dir.Exists)
|
||||
return;
|
||||
|
||||
var folderFiles = dir.GetFiles(Constants.DefaultVideoFormats.Concat(Constants.DefaultImageFormats).ToArray())
|
||||
.Select(x => x.FullName)
|
||||
.Select(x => new MediaFile(x))
|
||||
.GroupBy(x => x.Hash)
|
||||
.ToDictionary(x => x.Key, v => v.First());
|
||||
|
||||
//sync with db
|
||||
var dbFiles = await _dbFactory.Run(async db =>
|
||||
await db.MediaFiles
|
||||
.Where(x => folderFiles.ContainsKey(x.Hash))
|
||||
.ToDictionaryAsync(x => x.Hash));
|
||||
var newFiles = folderFiles
|
||||
.Where(x => !dbFiles.ContainsKey(x.Key))
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
if (newFiles.Count > 0)
|
||||
await _dbFactory.RunWrite(async db => await db.BulkCopyAsync(newFiles));
|
||||
|
||||
var allFiles = dbFiles.Select(x => x.Value)
|
||||
.Concat(newFiles)
|
||||
.ToList();
|
||||
|
||||
await SyncAnnotations(allFiles);
|
||||
|
||||
AllMediaFiles = new ObservableCollection<MediaFile>(allFiles);
|
||||
MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name)
|
||||
.ToDictionary(gr => gr.Key, gr => gr.First());
|
||||
var selectedIndex = LvFiles.SelectedIndex;
|
||||
LvFiles.ItemsSource = AllMediaFiles;
|
||||
LvFiles.SelectedIndex = selectedIndex;
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
private async Task SyncAnnotations(List<MediaFile> allFiles)
|
||||
{
|
||||
var hashes = allFiles.Select(x => x.Hash).ToList();
|
||||
var filenames = allFiles.Select(x => x.Name).ToList();
|
||||
var nameHashMap = allFiles.ToDictionary(x => x.Name.ToFName(), x => x.Hash);
|
||||
await _dbFactory.RunWrite(async db =>
|
||||
{
|
||||
var hashedAnnotations = await db.Annotations
|
||||
.Where(a => hashes.Contains(a.MediaHash))
|
||||
.ToDictionaryAsync(x => x.Name);
|
||||
|
||||
var fileNameAnnotations = await db.Annotations
|
||||
.Where(a => filenames.Contains(a.OriginalMediaName))
|
||||
.ToDictionaryAsync(x => x.Name);
|
||||
|
||||
var toUpdate = fileNameAnnotations
|
||||
.Where(a => !hashedAnnotations.ContainsKey(a.Key))
|
||||
.Select(a => new { a.Key, MediaHash = nameHashMap.GetValueOrDefault(a.Value.OriginalMediaName) ?? "" })
|
||||
.ToList();
|
||||
|
||||
if (toUpdate.Count > 0)
|
||||
{
|
||||
var caseBuilder = new StringBuilder("UPDATE Annotations SET MediaHash = CASE Name ");
|
||||
var parameters = new List<DataParameter>();
|
||||
|
||||
for (int i = 0; i < toUpdate.Count; i++)
|
||||
{
|
||||
caseBuilder.Append($"WHEN @name{i} THEN @hash{i} ");
|
||||
parameters.Add(new DataParameter($"@name{i}", toUpdate[i].Key, DataType.NVarChar));
|
||||
parameters.Add(new DataParameter($"@hash{i}", toUpdate[i].MediaHash, DataType.NVarChar));
|
||||
}
|
||||
|
||||
caseBuilder.Append("END WHERE Name IN (");
|
||||
caseBuilder.Append(string.Join(", ", Enumerable.Range(0, toUpdate.Count).Select(i => $"@name{i}")));
|
||||
caseBuilder.Append(")");
|
||||
|
||||
await db.ExecuteAsync(caseBuilder.ToString(), parameters.ToArray());
|
||||
}
|
||||
var annotationMediaHashes = hashedAnnotations
|
||||
.GroupBy(x => x.Value.MediaHash)
|
||||
.Select(x => x.Key)
|
||||
.ToList();
|
||||
|
||||
var annotationMediaNames = fileNameAnnotations
|
||||
.GroupBy(x => x.Value.OriginalMediaName)
|
||||
.Select(x => x.Key)
|
||||
.ToList();
|
||||
|
||||
await db.MediaFiles
|
||||
.Where(m => annotationMediaHashes.Contains(m.Hash) ||
|
||||
annotationMediaNames.Contains(m.Name))
|
||||
.Set(m => m.Status, MediaStatus.Confirmed)
|
||||
.UpdateAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnFormClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_mainCancellationSource.Cancel();
|
||||
_inferenceService.StopInference();
|
||||
DetCancelSource.Cancel();
|
||||
|
||||
_mediaPlayer.Stop();
|
||||
_mediaPlayer.Dispose();
|
||||
_libVlc.Dispose();
|
||||
}
|
||||
|
||||
private void OpenContainingFolder(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFile;
|
||||
if (mediaFileInfo == null)
|
||||
return;
|
||||
|
||||
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.MediaUrl +"\"");
|
||||
}
|
||||
|
||||
public void SeekTo(long timeMilliseconds, bool setPause = true)
|
||||
{
|
||||
_mediaPlayer.SetPause(setPause);
|
||||
_mediaPlayer.Time = timeMilliseconds;
|
||||
VideoSlider.Value = _mediaPlayer.Position * 100;
|
||||
|
||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||
}
|
||||
|
||||
private void OpenFolderItemClick(object sender, RoutedEventArgs e) => OpenFolder();
|
||||
private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder();
|
||||
|
||||
private void OpenFolder()
|
||||
{
|
||||
var dlg = new CommonOpenFileDialog
|
||||
{
|
||||
Title = "Open Video folder",
|
||||
IsFolderPicker = true,
|
||||
InitialDirectory = Path.GetDirectoryName(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR)
|
||||
};
|
||||
var dialogResult = dlg.ShowDialog();
|
||||
|
||||
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
|
||||
return;
|
||||
|
||||
if (_appConfig is not null)
|
||||
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
|
||||
|
||||
TbFolder.Text = dlg.FileName;
|
||||
}
|
||||
|
||||
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
FilteredMediaFiles = new ObservableCollection<MediaFile>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
|
||||
MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.Name);
|
||||
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||
}
|
||||
|
||||
private void PlayClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
|
||||
}
|
||||
|
||||
private void PauseClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Pause));
|
||||
private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Stop));
|
||||
|
||||
private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.PreviousFrame));
|
||||
private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.NextFrame));
|
||||
|
||||
private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.SaveAnnotations));
|
||||
|
||||
private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveSelectedAnns));
|
||||
private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveAllAnns));
|
||||
private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOffVolume));
|
||||
private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOnVolume));
|
||||
|
||||
private void OpenHelpWindowClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_helpWindow.Show();
|
||||
_helpWindow.Activate();
|
||||
}
|
||||
|
||||
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => SaveUserSettings();
|
||||
|
||||
private void LvFilesContextOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var listItem = sender as ListViewItem;
|
||||
LvFilesContextMenu.DataContext = listItem!.DataContext;
|
||||
}
|
||||
|
||||
private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await AutoDetect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AutoDetect()
|
||||
{
|
||||
if (_isInferenceNow)
|
||||
return;
|
||||
|
||||
if (LvFiles.Items.IsEmpty)
|
||||
return;
|
||||
if (LvFiles.SelectedIndex == -1)
|
||||
LvFiles.SelectedIndex = 0;
|
||||
|
||||
Dispatcher.Invoke(() => Editor.SetBackground(null));
|
||||
|
||||
_isInferenceNow = true;
|
||||
AIDetectBtn.IsEnabled = false;
|
||||
|
||||
DetCancelSource = new CancellationTokenSource();
|
||||
|
||||
var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles)
|
||||
.Skip(LvFiles.SelectedIndex)
|
||||
.Select(x => x.MediaUrl)
|
||||
.ToList();
|
||||
if (files.Count == 0)
|
||||
return;
|
||||
|
||||
await _inferenceService.RunInference(files, _appConfig?.CameraConfig ?? Constants.DefaultCameraConfig, DetCancelSource.Token);
|
||||
|
||||
LvFiles.Items.Refresh();
|
||||
_isInferenceNow = false;
|
||||
StatusHelp.Text = "Розпізнавання завершено";
|
||||
AIDetectBtn.IsEnabled = true;
|
||||
}
|
||||
|
||||
private void SwitchGpsPanel(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_gpsPanelVisible = !_gpsPanelVisible;
|
||||
|
||||
if (_gpsPanelVisible)
|
||||
{
|
||||
GpsSplitterRow.Height = new GridLength(4);
|
||||
GpsSplitter.Visibility = Visibility.Visible;
|
||||
|
||||
GpsSectionRow.Height = new GridLength(1, GridUnitType.Star);
|
||||
MapMatcherComponent.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
GpsSplitterRow.Height = new GridLength(0);
|
||||
GpsSplitter.Visibility = Visibility.Collapsed;
|
||||
|
||||
GpsSectionRow.Height = new GridLength(0);
|
||||
MapMatcherComponent.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
#region Denys Wishes
|
||||
|
||||
private void SoundDetections(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MessageBox.Show("Функція Аудіоаналіз знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private void RunDroneMaintenance(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MessageBox.Show("Функція Аналіз стану БПЛА знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void DeleteMedia(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFile;
|
||||
if (mediaFileInfo == null)
|
||||
return;
|
||||
DeleteMedia(mediaFileInfo);
|
||||
}
|
||||
|
||||
public void DeleteMedia(MediaFile mediaFile)
|
||||
{
|
||||
var obj = mediaFile.MediaType == MediaTypes.Image
|
||||
? "цю картинку "
|
||||
: "це відео ";
|
||||
var result = MessageBox.Show($"Видалити {obj}?",
|
||||
"Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
File.Delete(mediaFile.MediaUrl);
|
||||
AllMediaFiles.Remove(mediaFile);
|
||||
}
|
||||
}
|
||||
|
||||
public class GradientStyleSelector : StyleSelector
|
||||
{
|
||||
public static readonly DependencyProperty ClassProviderProperty = DependencyProperty.RegisterAttached(
|
||||
"ClassProvider",
|
||||
typeof(IDetectionClassProvider),
|
||||
typeof(GradientStyleSelector),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public static void SetClassProvider(DependencyObject element, IDetectionClassProvider value)
|
||||
{
|
||||
element.SetValue(ClassProviderProperty, value);
|
||||
}
|
||||
|
||||
public static IDetectionClassProvider GetClassProvider(DependencyObject element)
|
||||
{
|
||||
return (IDetectionClassProvider)element.GetValue(ClassProviderProperty);
|
||||
}
|
||||
|
||||
public override Style? SelectStyle(object item, DependencyObject container)
|
||||
{
|
||||
if (container is not DataGridRow row || row.DataContext is not Annotation result)
|
||||
return null;
|
||||
|
||||
var dataGrid = FindParent<DataGrid>(row);
|
||||
var classProvider = dataGrid != null ? GetClassProvider(dataGrid) : null;
|
||||
|
||||
var style = new Style(typeof(DataGridRow));
|
||||
var brush = new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new Point(0, 0),
|
||||
EndPoint = new Point(1, 0)
|
||||
};
|
||||
|
||||
var gradients = new List<GradientStop>();
|
||||
var colors = classProvider?.GetColors(result) ?? [];
|
||||
if (colors.Count == 0)
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString("#40DDDDDD");
|
||||
gradients = [new GradientStop(color, 0.99)];
|
||||
}
|
||||
else
|
||||
{
|
||||
var increment = 1.0 / colors.Count;
|
||||
var currentStop = increment;
|
||||
foreach (var c in colors)
|
||||
{
|
||||
var resultColor = c.Color.ToConfidenceColor(c.Confidence);
|
||||
brush.GradientStops.Add(new GradientStop(resultColor, currentStop));
|
||||
currentStop += increment;
|
||||
}
|
||||
}
|
||||
foreach (var gradientStop in gradients)
|
||||
brush.GradientStops.Add(gradientStop);
|
||||
|
||||
style.Setters.Add(new Setter(Control.BackgroundProperty, brush));
|
||||
return style;
|
||||
}
|
||||
|
||||
private static T? FindParent<T>(DependencyObject child) where T : DependencyObject
|
||||
{
|
||||
var parent = VisualTreeHelper.GetParent(child);
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is T typedParent)
|
||||
return typedParent;
|
||||
parent = VisualTreeHelper.GetParent(parent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Azaion.Annotator.Controls;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Events;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.Common.Services;
|
||||
using Azaion.Common.Services.Inference;
|
||||
using GMap.NET;
|
||||
using GMap.NET.WindowsPresentation;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public class AnnotatorEventHandler(
|
||||
LibVLC libVlc,
|
||||
MediaPlayer mediaPlayer,
|
||||
Annotator mainWindow,
|
||||
FormState formState,
|
||||
IAnnotationService annotationService,
|
||||
ILogger<AnnotatorEventHandler> logger,
|
||||
IOptions<DirectoriesConfig> dirConfig,
|
||||
IOptions<AnnotationConfig> annotationConfig,
|
||||
IInferenceService inferenceService,
|
||||
IDbFactory dbFactory,
|
||||
IAzaionApi api,
|
||||
FailsafeAnnotationsProducer producer,
|
||||
IAnnotationPathResolver pathResolver,
|
||||
IFileSystem fileSystem,
|
||||
IUICommandDispatcher uiDispatcher)
|
||||
:
|
||||
INotificationHandler<KeyEvent>,
|
||||
INotificationHandler<AnnClassSelectedEvent>,
|
||||
INotificationHandler<AnnotatorControlEvent>,
|
||||
INotificationHandler<VolumeChangedEvent>,
|
||||
INotificationHandler<AnnotationsDeletedEvent>,
|
||||
INotificationHandler<AnnotationAddedEvent>,
|
||||
INotificationHandler<SetStatusTextEvent>,
|
||||
INotificationHandler<GPSMatcherResultProcessedEvent>,
|
||||
INotificationHandler<AIAvailabilityStatusEvent>
|
||||
{
|
||||
private const int STEP = 20;
|
||||
private const int LARGE_STEP = 5000;
|
||||
private readonly string _tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
|
||||
|
||||
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
||||
{
|
||||
{ Key.Space, PlaybackControlEnum.Pause },
|
||||
{ Key.Left, PlaybackControlEnum.PreviousFrame },
|
||||
{ Key.Right, PlaybackControlEnum.NextFrame },
|
||||
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
|
||||
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
|
||||
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
|
||||
{ Key.PageUp, PlaybackControlEnum.Previous },
|
||||
{ Key.PageDown, PlaybackControlEnum.Next },
|
||||
};
|
||||
|
||||
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct)
|
||||
{
|
||||
SelectClass(notification.DetectionClass);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void SelectClass(DetectionClass detClass)
|
||||
{
|
||||
mainWindow.Editor.CurrentAnnClass = detClass;
|
||||
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
|
||||
ann.DetectionClass = detClass;
|
||||
mainWindow.LvClasses.SelectNum(detClass.Id);
|
||||
}
|
||||
|
||||
public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default)
|
||||
{
|
||||
if (keyEvent.WindowEnum != WindowEnum.Annotator)
|
||||
return;
|
||||
|
||||
var key = keyEvent.Args.Key;
|
||||
var keyNumber = (int?)null;
|
||||
|
||||
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9)
|
||||
keyNumber = key - Key.D1;
|
||||
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
|
||||
keyNumber = key - Key.NumPad1;
|
||||
if (keyNumber.HasValue)
|
||||
SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
|
||||
|
||||
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
||||
await ControlPlayback(value, ct);
|
||||
|
||||
if (key == Key.R)
|
||||
await mainWindow.AutoDetect();
|
||||
|
||||
#region Volume
|
||||
switch (key)
|
||||
{
|
||||
case Key.VolumeMute when mediaPlayer.Volume == 0:
|
||||
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct);
|
||||
break;
|
||||
case Key.VolumeMute:
|
||||
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct);
|
||||
break;
|
||||
case Key.Up:
|
||||
case Key.VolumeUp:
|
||||
var vUp = Math.Min(100, mediaPlayer.Volume + 5);
|
||||
ChangeVolume(vUp);
|
||||
mainWindow.Volume.Value = vUp;
|
||||
break;
|
||||
case Key.Down:
|
||||
case Key.VolumeDown:
|
||||
var vDown = Math.Max(0, mediaPlayer.Volume - 5);
|
||||
ChangeVolume(vDown);
|
||||
mainWindow.Volume.Value = vDown;
|
||||
break;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default)
|
||||
{
|
||||
await ControlPlayback(notification.PlaybackControl, ct);
|
||||
mainWindow.VideoView.Focus();
|
||||
}
|
||||
|
||||
private async Task ControlPlayback(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
|
||||
var step = isCtrlPressed ? LARGE_STEP : STEP;
|
||||
|
||||
switch (controlEnum)
|
||||
{
|
||||
case PlaybackControlEnum.Play:
|
||||
await Play(cancellationToken);
|
||||
break;
|
||||
case PlaybackControlEnum.Pause:
|
||||
if (mediaPlayer.IsPlaying)
|
||||
{
|
||||
mediaPlayer.Pause();
|
||||
mediaPlayer.TakeSnapshot(0, _tempImgPath, 0, 0);
|
||||
mainWindow.Editor.SetBackground(await _tempImgPath.OpenImage());
|
||||
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaPlayer.Play();
|
||||
if (formState.BackgroundTime.HasValue)
|
||||
{
|
||||
mainWindow.Editor.SetBackground(null);
|
||||
formState.BackgroundTime = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PlaybackControlEnum.Stop:
|
||||
inferenceService.StopInference();
|
||||
await mainWindow.DetCancelSource.CancelAsync();
|
||||
mediaPlayer.Stop();
|
||||
break;
|
||||
case PlaybackControlEnum.PreviousFrame:
|
||||
mainWindow.SeekTo(mediaPlayer.Time - step);
|
||||
break;
|
||||
case PlaybackControlEnum.NextFrame:
|
||||
mainWindow.SeekTo(mediaPlayer.Time + step);
|
||||
break;
|
||||
case PlaybackControlEnum.SaveAnnotations:
|
||||
await SaveAnnotation(cancellationToken);
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||
var focusedElement = FocusManager.GetFocusedElement(mainWindow);
|
||||
if (focusedElement is ListViewItem item)
|
||||
{
|
||||
if (item.DataContext is not MediaFile mediaFileInfo)
|
||||
return;
|
||||
mainWindow.DeleteMedia(mediaFileInfo);
|
||||
}
|
||||
else
|
||||
mainWindow.Editor.RemoveSelectedAnns();
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveAllAnns:
|
||||
mainWindow.Editor.RemoveAllAnns();
|
||||
break;
|
||||
case PlaybackControlEnum.TurnOnVolume:
|
||||
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
|
||||
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
|
||||
mediaPlayer.Volume = formState.CurrentVolume;
|
||||
break;
|
||||
case PlaybackControlEnum.TurnOffVolume:
|
||||
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
|
||||
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
|
||||
formState.CurrentVolume = mediaPlayer.Volume;
|
||||
mediaPlayer.Volume = 0;
|
||||
break;
|
||||
case PlaybackControlEnum.Previous:
|
||||
await NextMedia(isPrevious: true, ct: cancellationToken);
|
||||
break;
|
||||
case PlaybackControlEnum.Next:
|
||||
await NextMedia(ct: cancellationToken);
|
||||
break;
|
||||
case PlaybackControlEnum.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NextMedia(bool isPrevious = false, CancellationToken ct = default)
|
||||
{
|
||||
var increment = isPrevious ? -1 : 1;
|
||||
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
|
||||
if (mainWindow.LvFiles.SelectedIndex + increment == check)
|
||||
return;
|
||||
|
||||
mainWindow.LvFiles.SelectedIndex += increment;
|
||||
await Play(ct);
|
||||
}
|
||||
|
||||
public async Task Handle(VolumeChangedEvent notification, CancellationToken ct)
|
||||
{
|
||||
ChangeVolume(notification.Volume);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ChangeVolume(int volume)
|
||||
{
|
||||
formState.CurrentVolume = volume;
|
||||
mediaPlayer.Volume = volume;
|
||||
}
|
||||
|
||||
private async Task Play(CancellationToken ct = default)
|
||||
{
|
||||
if (mainWindow.LvFiles.SelectedItem == null)
|
||||
return;
|
||||
var mediaInfo = (MediaFile)mainWindow.LvFiles.SelectedItem;
|
||||
|
||||
if (formState.CurrentMedia == mediaInfo)
|
||||
return; //already loaded
|
||||
|
||||
formState.CurrentMedia = mediaInfo;
|
||||
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
|
||||
|
||||
await mainWindow.ReloadAnnotations();
|
||||
|
||||
if (mediaInfo.MediaType == MediaTypes.Video)
|
||||
{
|
||||
mainWindow.Editor.SetBackground(null);
|
||||
//need to wait a bit for correct VLC playback event handling
|
||||
await Task.Delay(100, ct);
|
||||
mediaPlayer.Stop();
|
||||
mediaPlayer.Play(new Media(libVlc, mediaInfo.MediaUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
formState.BackgroundTime = TimeSpan.Zero;
|
||||
var image = await mediaInfo.MediaUrl.OpenImage();
|
||||
formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight);
|
||||
mainWindow.Editor.SetBackground(image);
|
||||
mediaPlayer.Stop();
|
||||
mainWindow.ShowTimeAnnotations(TimeSpan.Zero, showImage: true);
|
||||
}
|
||||
}
|
||||
|
||||
//SAVE: MANUAL
|
||||
private async Task SaveAnnotation(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (formState.CurrentMedia == null)
|
||||
return;
|
||||
|
||||
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||
var timeName = formState.CurrentMedia.Name.ToTimeName(time);
|
||||
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
||||
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{timeName}{Constants.JPG_EXT}");
|
||||
|
||||
var annotations = await SaveAnnotationInner(imgPath, cancellationToken);
|
||||
if (isVideo)
|
||||
{
|
||||
foreach (var annotation in annotations)
|
||||
mainWindow.AddAnnotation(annotation);
|
||||
mediaPlayer.Play();
|
||||
|
||||
// next item. Probably not needed
|
||||
// var annGrid = mainWindow.DgAnnotations;
|
||||
// annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
|
||||
// mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
|
||||
|
||||
mainWindow.Editor.SetBackground(null);
|
||||
formState.BackgroundTime = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
await NextMedia(ct: cancellationToken);
|
||||
}
|
||||
|
||||
mainWindow.LvFiles.Items.Refresh();
|
||||
mainWindow.Editor.RemoveAllAnns();
|
||||
}
|
||||
|
||||
private async Task<List<Annotation>> SaveAnnotationInner(string imgPath, CancellationToken ct = default)
|
||||
{
|
||||
var canvasDetections = mainWindow.Editor.CurrentDetections.Select(x => x.ToCanvasLabel()).ToList();
|
||||
|
||||
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
|
||||
var mediaSize = new Size(source.PixelWidth, source.PixelHeight);
|
||||
var annotationsResult = new List<Annotation>();
|
||||
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||
|
||||
if (!fileSystem.FileExists(imgPath))
|
||||
{
|
||||
if (mediaSize.FitSizeForAI())
|
||||
await source.SaveImage(imgPath, ct);
|
||||
else
|
||||
{
|
||||
//Tiling
|
||||
|
||||
//1. Convert from RenderSize to CurrentMediaSize
|
||||
var detectionCoords = canvasDetections.Select(x => new CanvasLabel(
|
||||
new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence))
|
||||
.ToList();
|
||||
|
||||
//2. Split to frames
|
||||
var results = TileProcessor.Split(formState.CurrentMediaSize, detectionCoords, ct);
|
||||
|
||||
//3. Save each frame as a separate annotation
|
||||
foreach (var res in results)
|
||||
{
|
||||
var annotationName = $"{formState.CurrentMedia?.Name ?? ""}{Constants.SPLIT_SUFFIX}{res.Tile.Width}_{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
|
||||
|
||||
var tempAnnotation = new Annotation { Name = annotationName, ImageExtension = Constants.JPG_EXT };
|
||||
var tileImgPath = pathResolver.GetImagePath(tempAnnotation);
|
||||
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.Left, (int)res.Tile.Top, (int)res.Tile.Width, (int)res.Tile.Height));
|
||||
await bitmap.SaveImage(tileImgPath, ct);
|
||||
|
||||
var frameSize = new Size(res.Tile.Width, res.Tile.Height);
|
||||
var detections = res.Detections
|
||||
.Select(det => det.ReframeToSmall(res.Tile))
|
||||
.Select(x => new Detection(annotationName, new YoloLabel(x, frameSize)))
|
||||
.ToList();
|
||||
|
||||
annotationsResult.Add(await annotationService.SaveAnnotation(formState.CurrentMediaHash, formState.CurrentMediaName, annotationName, time, detections, token: ct));
|
||||
}
|
||||
return annotationsResult;
|
||||
}
|
||||
}
|
||||
|
||||
var annName = (formState.CurrentMedia?.Name ?? "").ToTimeName(time);
|
||||
var currentDetections = canvasDetections.Select(x =>
|
||||
new Detection(annName, new YoloLabel(x, mainWindow.Editor.RenderSize, mediaSize)))
|
||||
.ToList();
|
||||
var annotation = await annotationService.SaveAnnotation(formState.CurrentMediaHash, formState.CurrentMediaName, annName, time, currentDetections, token: ct);
|
||||
return [annotation];
|
||||
}
|
||||
|
||||
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
uiDispatcher.Execute(() =>
|
||||
{
|
||||
var namesSet = notification.AnnotationNames.ToHashSet();
|
||||
|
||||
var remainAnnotations = formState.AnnotationResults
|
||||
.Where(x => !namesSet.Contains(x.Name)).ToList();
|
||||
formState.AnnotationResults.Clear();
|
||||
foreach (var ann in remainAnnotations)
|
||||
formState.AnnotationResults.Add(ann);
|
||||
|
||||
var timedAnnotationsToRemove = mainWindow.TimedAnnotations
|
||||
.Where(x => namesSet.Contains(x.Value.Name))
|
||||
.Select(x => x.Value).ToList();
|
||||
mainWindow.TimedAnnotations.Remove(timedAnnotationsToRemove);
|
||||
|
||||
mainWindow.ReloadFilesThrottled();
|
||||
});
|
||||
|
||||
await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
|
||||
|
||||
foreach (var name in notification.AnnotationNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempAnnotation = new Annotation { Name = name, ImageExtension = Constants.JPG_EXT };
|
||||
fileSystem.DeleteFile(pathResolver.GetImagePath(tempAnnotation));
|
||||
fileSystem.DeleteFile(pathResolver.GetLabelPath(tempAnnotation));
|
||||
fileSystem.DeleteFile(pathResolver.GetThumbPath(tempAnnotation));
|
||||
fileSystem.DeleteFile(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
//Only validators can send Delete to the queue
|
||||
var currentUser = await api.GetCurrentUserAsync();
|
||||
if (!notification.FromQueue && currentUser.Role.IsValidator())
|
||||
await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
|
||||
{
|
||||
uiDispatcher.Execute(() =>
|
||||
{
|
||||
var mediaInfo = (MediaFile)mainWindow.LvFiles.SelectedItem;
|
||||
if ((mediaInfo?.Name ?? "") == e.Annotation.OriginalMediaName)
|
||||
mainWindow.AddAnnotation(e.Annotation);
|
||||
|
||||
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
|
||||
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
|
||||
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
|
||||
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
|
||||
$"conf: {det.Confidence*100:F0}%"));
|
||||
|
||||
mainWindow.LvFiles.Items.Refresh();
|
||||
|
||||
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
|
||||
if (media != null)
|
||||
mainWindow.ReloadFilesThrottled();
|
||||
|
||||
mainWindow.LvFiles.Items.Refresh();
|
||||
mainWindow.StatusHelp.Text = log;
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
|
||||
{
|
||||
uiDispatcher.Execute(() =>
|
||||
{
|
||||
mainWindow.StatusHelp.Text = e.Text;
|
||||
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
|
||||
{
|
||||
uiDispatcher.Execute(() =>
|
||||
{
|
||||
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
|
||||
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
|
||||
if (e.ProcessedGeoPoint != e.GeoPoint)
|
||||
AddMarker(e.ProcessedGeoPoint, $"{e.Image}: corrected", Brushes.DarkViolet);
|
||||
ann.Lat = e.GeoPoint.Lat;
|
||||
ann.Lon = e.GeoPoint.Lon;
|
||||
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void AddMarker(GeoPoint point, string text, SolidColorBrush color)
|
||||
{
|
||||
var map = mainWindow.MapMatcherComponent;
|
||||
var pointLatLon = new PointLatLng(point.Lat, point.Lon);
|
||||
var marker = new GMapMarker(pointLatLon);
|
||||
marker.Shape = new CircleVisual(marker, size: 14, text: text, background: color);
|
||||
map.SatelliteMap.Markers.Add(marker);
|
||||
map.SatelliteMap.Position = pointLatLon;
|
||||
map.SatelliteMap.ZoomAndCenterMarkers(null);
|
||||
}
|
||||
|
||||
public async Task Handle(AIAvailabilityStatusEvent e, CancellationToken cancellationToken)
|
||||
{
|
||||
uiDispatcher.Execute(() =>
|
||||
{
|
||||
logger.LogInformation(e.ToString());
|
||||
mainWindow.AIDetectBtn.IsEnabled = e.Status == AIAvailabilityEnum.Enabled;
|
||||
mainWindow.StatusHelp.Text = e.ToString();
|
||||
});
|
||||
if (e.Status is AIAvailabilityEnum.Enabled or AIAvailabilityEnum.Error)
|
||||
await inferenceService.CheckAIAvailabilityTokenSource.CancelAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using System.Windows;
|
||||
using Azaion.Common.DTO;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public class AnnotatorModule : IAzaionModule
|
||||
{
|
||||
public string Name => "Анотатор";
|
||||
|
||||
public string SvgIcon =>
|
||||
@"<?xml version=""1.0"" encoding=""utf-8""?>
|
||||
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version=""1.1"" id=""Layer_1"" xmlns=""http://www.w3.org/2000/svg"" xmlns:xlink=""http://www.w3.org/1999/xlink"" x=""0px"" y=""0px""
|
||||
viewBox=""0 0 800 800"" style=""enable-background:new 0 0 800 800;"" xml:space=""preserve"">
|
||||
<style type=""text/css"">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{opacity:0.75;fill:#006932;stroke:#000000;stroke-width:20;stroke-miterlimit:10;}
|
||||
.st2{opacity:0.75;fill:#6C0A0B;stroke:#000000;stroke-width:20;stroke-miterlimit:10;}
|
||||
.st3{fill:#FFFFFF;stroke:#434444;stroke-width:8;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id=""SVGRepo_bgCarrier"">
|
||||
</g>
|
||||
<g id=""SVGRepo_tracerCarrier"">
|
||||
</g>
|
||||
<g id=""SVGRepo_iconCarrier"">
|
||||
<g transform=""translate(1)"">
|
||||
<g>
|
||||
<polygon class=""st0"" points=""465.7,93.3 545.7,93.3 545.7,13.3 465.7,13.3""/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<rect x=""43.3"" y=""53.3"" class=""st1"" width=""609.7"" height=""301""/>
|
||||
<rect x=""443.2"" y=""400"" class=""st2"" width=""285.8"" height=""363""/>
|
||||
<g>
|
||||
<rect x=""19"" y=""325"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""17.5"" y=""166"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""17.5"" y=""27"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""325.5"" y=""329"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""624.5"" y=""325"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""626.5"" y=""168"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""626.5"" y=""27"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""323.5"" y=""27"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""419.8"" y=""377.3"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""698.7"" y=""378.3"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""418.5"" y=""733.2"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""698.7"" y=""736.5"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""415.8"" y=""555.7"" class=""st3"" width=""53"" height=""53""/>
|
||||
<rect x=""701.2"" y=""551.7"" class=""st3"" width=""53"" height=""53""/>
|
||||
</g>
|
||||
</svg>";
|
||||
|
||||
public Type MainWindowType => typeof(Annotator);
|
||||
|
||||
public WindowEnum WindowEnum => WindowEnum.Annotator;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@@ -1,36 +0,0 @@
|
||||
<Window x:Class="Azaion.Annotator.AutodetectDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:extensions="clr-namespace:Azaion.Annotator.Extensions"
|
||||
mc:Ignorable="d"
|
||||
WindowStyle="SingleBorderWindow"
|
||||
ResizeMode="NoResize"
|
||||
Title="Розпізнавання"
|
||||
Height="247" Width="400"
|
||||
Background="LightGray">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="70"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="0"
|
||||
extensions:ScrollViewerExtensions.AlwaysScrollToEnd="True">
|
||||
<TextBlock
|
||||
Name="TextBlockLog"
|
||||
Padding="10 10 5 5"/>
|
||||
</ScrollViewer>
|
||||
<Button Grid.Row="1"
|
||||
Width="50" Height="50" ToolTip="Зупинити розпізнавання. [Esc]"
|
||||
Background="LightGray" BorderBrush="LightGray"
|
||||
Click="ButtonBase_OnClick">
|
||||
<Path Stretch="Fill" Fill="Gray" Data="M12,2 C17.5228,2 22,6.47715 22,12 C22,17.5228 17.5228,22 12,22 C6.47715,22 2,17.5228 2,12 C2,6.47715 6.47715,2 12,2 Z
|
||||
M9.87874,8.46443 C9.48821,8.07391 8.85505,8.07391 8.46452,8.46443 C8.10404,8.82491923 8.07631077,9.39214645 8.38133231,9.78443366 L8.46452,9.87864 L10.5858,11.9999
|
||||
L8.46443,14.1213 C8.07391,14.5118 8.07391,15.145 8.46443,15.5355 C8.82491923,15.8959615 9.39214645,15.9236893 9.78443366,15.6186834 L9.87864,15.5355 L12,13.4141
|
||||
L14.1214,15.5355 C14.5119,15.926 15.1451,15.926 15.5356,15.5355 C15.8960615,15.1750385 15.9237893,14.6077793 15.6187834,14.2155027 L15.5356,14.1213 L13.4142,11.9999
|
||||
L15.5355,9.87862 C15.926,9.4881 15.926,8.85493 15.5355,8.46441 C15.1750385,8.10392077 14.6077793,8.07619083 14.2155027,8.38122018 L14.1213,8.46441
|
||||
L12,10.5857 L9.87874,8.46443Z" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class AutodetectDialog : Window
|
||||
{
|
||||
public AutodetectDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
KeyUp += (sender, args) =>
|
||||
{
|
||||
if (args.Key == Key.Escape)
|
||||
Close();
|
||||
};
|
||||
}
|
||||
|
||||
public void Log(string message) =>
|
||||
TextBlockLog.Text = TextBlockLog.Text + Environment.NewLine + message;
|
||||
|
||||
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionDate>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd"))</VersionDate>
|
||||
<VersionSeconds>$([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes)))</VersionSeconds>
|
||||
|
||||
<AssemblyVersion>$(VersionDate).$(VersionSeconds)</AssemblyVersion>
|
||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
|
||||
<Copyright>Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved.</Copyright>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
|
||||
<PackageReference Include="libc.translation" Version="7.1.1" />
|
||||
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
|
||||
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="RangeTree" Version="3.0.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
||||
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
|
||||
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="AutodetectDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<XamlRuntime>Wpf</XamlRuntime>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,342 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
using GMap.NET.WindowsPresentation;
|
||||
|
||||
namespace Azaion.Annotator.Controls
|
||||
{
|
||||
public class CircleVisual : FrameworkElement
|
||||
{
|
||||
public readonly GMapMarker Marker;
|
||||
|
||||
public CircleVisual(GMapMarker m, int size, string text, Brush background)
|
||||
{
|
||||
ShadowEffect = new DropShadowEffect();
|
||||
Marker = m;
|
||||
Marker.ZIndex = 100;
|
||||
|
||||
SizeChanged += CircleVisual_SizeChanged;
|
||||
MouseEnter += CircleVisual_MouseEnter;
|
||||
MouseLeave += CircleVisual_MouseLeave;
|
||||
Loaded += OnLoaded;
|
||||
|
||||
Text = text;
|
||||
|
||||
StrokeArrow.EndLineCap = PenLineCap.Triangle;
|
||||
StrokeArrow.LineJoin = PenLineJoin.Round;
|
||||
|
||||
RenderTransform = _scale;
|
||||
|
||||
Width = Height = size;
|
||||
FontSize = Width / 1.55;
|
||||
|
||||
Background = background;
|
||||
Angle = null;
|
||||
}
|
||||
|
||||
void CircleVisual_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
Marker.Offset = new Point(-e.NewSize.Width / 2, -e.NewSize.Height / 2);
|
||||
_scale.CenterX = -Marker.Offset.X;
|
||||
_scale.CenterY = -Marker.Offset.Y;
|
||||
}
|
||||
|
||||
void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateVisual(true);
|
||||
}
|
||||
|
||||
readonly ScaleTransform _scale = new ScaleTransform(1, 1);
|
||||
|
||||
void CircleVisual_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
|
||||
Marker.ZIndex -= 10000;
|
||||
Cursor = Cursors.Arrow;
|
||||
|
||||
Effect = null;
|
||||
|
||||
_scale.ScaleY = 1;
|
||||
_scale.ScaleX = 1;
|
||||
}
|
||||
|
||||
void CircleVisual_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
Marker.ZIndex += 10000;
|
||||
Cursor = Cursors.Hand;
|
||||
|
||||
Effect = ShadowEffect;
|
||||
|
||||
_scale.ScaleY = 1.5;
|
||||
_scale.ScaleX = 1.5;
|
||||
}
|
||||
|
||||
public DropShadowEffect ShadowEffect;
|
||||
|
||||
static readonly Typeface Font = new Typeface(new FontFamily("Arial"),
|
||||
FontStyles.Normal,
|
||||
FontWeights.Bold,
|
||||
FontStretches.Normal);
|
||||
|
||||
FormattedText _fText = null!;
|
||||
|
||||
private Brush _background = Brushes.Blue;
|
||||
|
||||
public Brush Background
|
||||
{
|
||||
get
|
||||
{
|
||||
return _background;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_background != value)
|
||||
{
|
||||
_background = value;
|
||||
IsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Brush _foreground = Brushes.White;
|
||||
|
||||
public Brush Foreground
|
||||
{
|
||||
get
|
||||
{
|
||||
return _foreground;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_foreground != value)
|
||||
{
|
||||
_foreground = value;
|
||||
IsChanged = true;
|
||||
|
||||
ForceUpdateText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pen _stroke = new Pen(Brushes.Blue, 2.0);
|
||||
|
||||
public Pen Stroke
|
||||
{
|
||||
get
|
||||
{
|
||||
return _stroke;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_stroke != value)
|
||||
{
|
||||
_stroke = value;
|
||||
IsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pen _strokeArrow = new Pen(Brushes.Blue, 2.0);
|
||||
|
||||
public Pen StrokeArrow
|
||||
{
|
||||
get
|
||||
{
|
||||
return _strokeArrow;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_strokeArrow != value)
|
||||
{
|
||||
_strokeArrow = value;
|
||||
IsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double FontSize = 16;
|
||||
|
||||
private double? _angle = 0;
|
||||
|
||||
public double? Angle
|
||||
{
|
||||
get => _angle;
|
||||
set
|
||||
{
|
||||
if (!_angle.HasValue || !value.HasValue ||
|
||||
Angle.HasValue && Math.Abs(_angle.Value - value.Value) > 11)
|
||||
{
|
||||
_angle = value;
|
||||
IsChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsChanged = true;
|
||||
|
||||
void ForceUpdateText()
|
||||
{
|
||||
|
||||
_fText = new FormattedText(_text,
|
||||
CultureInfo.InvariantCulture,
|
||||
FlowDirection.LeftToRight,
|
||||
Font,
|
||||
FontSize,
|
||||
Foreground, 1.0);
|
||||
IsChanged = true;
|
||||
}
|
||||
|
||||
string _text = null!;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
return _text;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_text != value)
|
||||
{
|
||||
_text = value;
|
||||
ForceUpdateText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Visual _child = null!;
|
||||
|
||||
public virtual Visual? Child
|
||||
{
|
||||
get => _child;
|
||||
set
|
||||
{
|
||||
if (_child == value)
|
||||
return;
|
||||
|
||||
if (_child != null)
|
||||
{
|
||||
RemoveLogicalChild(_child);
|
||||
RemoveVisualChild(_child);
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
AddVisualChild(value);
|
||||
AddLogicalChild(value);
|
||||
}
|
||||
|
||||
// cache the new child
|
||||
_child = value!;
|
||||
|
||||
InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
public bool UpdateVisual(bool forceUpdate)
|
||||
{
|
||||
if (forceUpdate || IsChanged)
|
||||
{
|
||||
Child = Create();
|
||||
IsChanged = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int _countCreate;
|
||||
|
||||
private DrawingVisual Create()
|
||||
{
|
||||
_countCreate++;
|
||||
|
||||
var square = new DrawingVisualFx();
|
||||
|
||||
using var dc = square.RenderOpen();
|
||||
dc.DrawEllipse(null,
|
||||
Stroke,
|
||||
new Point(Width / 2, Height / 2),
|
||||
Width / 2 + Stroke.Thickness / 2,
|
||||
Height / 2 + Stroke.Thickness / 2);
|
||||
|
||||
if (Angle.HasValue)
|
||||
{
|
||||
dc.PushTransform(new RotateTransform(Angle.Value, Width / 2, Height / 2));
|
||||
{
|
||||
var polySeg = new PolyLineSegment(new[]
|
||||
{
|
||||
new Point(Width * 0.2, Height * 0.3), new Point(Width * 0.8, Height * 0.3)
|
||||
},
|
||||
true);
|
||||
var pathFig = new PathFigure(new Point(Width * 0.5, -Height * 0.22),
|
||||
new PathSegment[] {polySeg},
|
||||
true);
|
||||
var pathGeo = new PathGeometry(new[] {pathFig});
|
||||
dc.DrawGeometry(Brushes.AliceBlue, StrokeArrow, pathGeo);
|
||||
}
|
||||
dc.Pop();
|
||||
}
|
||||
|
||||
dc.DrawEllipse(Background, null, new Point(Width / 2, Height / 2), Width / 2, Height / 2);
|
||||
dc.DrawText(_fText, new Point(Width / 2 - _fText.Width / 2, Height / 2 - _fText.Height / 2));
|
||||
|
||||
return square;
|
||||
}
|
||||
|
||||
#region Necessary Overrides -- Needed by WPF to maintain bookkeeping of our hosted visuals
|
||||
|
||||
protected override int VisualChildrenCount
|
||||
{
|
||||
get
|
||||
{
|
||||
return Child == null ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Visual? GetVisualChild(int index)
|
||||
{
|
||||
return Child;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class DrawingVisualFx : DrawingVisual
|
||||
{
|
||||
public static readonly DependencyProperty EffectProperty = DependencyProperty.Register("Effect",
|
||||
typeof(Effect),
|
||||
typeof(DrawingVisualFx),
|
||||
new FrameworkPropertyMetadata(null,
|
||||
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||
OnEffectChanged));
|
||||
|
||||
public new Effect Effect
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Effect)GetValue(EffectProperty);
|
||||
}
|
||||
set
|
||||
{
|
||||
SetValue(EffectProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnEffectChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var drawingVisualFx = o as DrawingVisualFx;
|
||||
if (drawingVisualFx != null)
|
||||
{
|
||||
drawingVisualFx.SetMyProtectedVisualEffect((Effect)e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetMyProtectedVisualEffect(Effect effect)
|
||||
{
|
||||
VisualEffect = effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
<UserControl x:Class="Azaion.Annotator.Controls.MapMatcher"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Azaion.Annotator.Controls"
|
||||
xmlns:windowsPresentation="clr-namespace:GMap.NET.WindowsPresentation;assembly=GMap.NET.WindowsPresentation"
|
||||
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="1200">
|
||||
<Grid
|
||||
Name="MatcherGrid"
|
||||
ShowGridLines="False"
|
||||
Background="Black"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250" /> <!-- 0 list view -->
|
||||
<ColumnDefinition Width="4"/> <!-- 1 splitter -->
|
||||
<ColumnDefinition Width="*" /> <!-- 2 ExplorerEditor -->
|
||||
<ColumnDefinition Width="4"/> <!-- 3 splitter -->
|
||||
<ColumnDefinition Width="*" /> <!-- 4 Maps Control -->
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
<ColumnDefinition Width="32"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="1"
|
||||
x:Name="TbGpsMapFolder"></TextBox>
|
||||
<Button
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="1"
|
||||
Click="OpenGpsTilesFolderClick">
|
||||
. . .
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- <TextBlock -->
|
||||
<!-- Grid.Column="0" -->
|
||||
<!-- Text="Lat" -->
|
||||
<!-- Background="Gray"/> -->
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Margin="1"
|
||||
Click="TestGps">
|
||||
Test
|
||||
</Button>
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
x:Name="TbLat"
|
||||
Text="48.2748909"></TextBox>
|
||||
</Grid>
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Text="Lon"
|
||||
Background="Gray"/>
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
x:Name="TbLon"
|
||||
Text="37.3834877"></TextBox>
|
||||
</Grid>
|
||||
<ListView Grid.Row="3"
|
||||
Name="GpsFiles"
|
||||
Background="Black"
|
||||
SelectedItem="{Binding Path=SelectedVideo}"
|
||||
Foreground="#FFDDDDDD">
|
||||
<ListView.Resources>
|
||||
<Style TargetType="{x:Type ListViewItem}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasAnnotations}" Value="true">
|
||||
<Setter Property="Background" Value="#FF505050"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value=" DimGray" />
|
||||
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Foreground" Value="DimGray"></Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
<EventSetter Event="ContextMenuOpening" Handler="GpsFilesContextOpening"></EventSetter>
|
||||
</Style>
|
||||
</ListView.Resources>
|
||||
<ListView.ContextMenu>
|
||||
<ContextMenu Name="GpsFilesContextMenu">
|
||||
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
|
||||
</ContextMenu>
|
||||
</ListView.ContextMenu>
|
||||
<ListView.View>
|
||||
<GridView>
|
||||
<GridViewColumn Width="Auto"
|
||||
Header="Файл"
|
||||
DisplayMemberBinding="{Binding Path=Name}"/>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
</Grid>
|
||||
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
ResizeDirection="Columns"
|
||||
Grid.Column="1"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"/>
|
||||
|
||||
<Border Grid.Column="2" ClipToBounds="True">
|
||||
<controls:CanvasEditor
|
||||
x:Name="GpsImageEditor"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
ResizeDirection="Columns"
|
||||
Grid.Column="3"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"/>
|
||||
|
||||
<windowsPresentation:GMapControl
|
||||
Grid.Column="4"
|
||||
x:Name="SatelliteMap"
|
||||
Zoom="20" MaxZoom="24" MinZoom="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
MinWidth="400" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,119 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.Common.Services;
|
||||
using GMap.NET;
|
||||
using GMap.NET.MapProviders;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
|
||||
namespace Azaion.Annotator.Controls;
|
||||
|
||||
public partial class MapMatcher : UserControl
|
||||
{
|
||||
private AppConfig _appConfig = null!;
|
||||
List<MediaFile> _allMediaFiles = new();
|
||||
public Dictionary<int, Annotation> Annotations = new();
|
||||
private string _currentDir = null!;
|
||||
private IGpsMatcherService _gpsMatcherService = null!;
|
||||
|
||||
public MapMatcher()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Init(AppConfig appConfig, IGpsMatcherService gpsMatcherService)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
_gpsMatcherService = gpsMatcherService;
|
||||
GoogleMapProvider.Instance.ApiKey = appConfig.MapConfig.ApiKey;
|
||||
SatelliteMap.MapProvider = GMapProviders.GoogleSatelliteMap;
|
||||
SatelliteMap.Position = new PointLatLng(48.295985271707664, 37.14477539062501);
|
||||
SatelliteMap.MultiTouchEnabled = true;
|
||||
|
||||
GpsFiles.MouseDoubleClick += async (sender, args) => { await OpenGpsLocation(GpsFiles.SelectedIndex); };
|
||||
}
|
||||
|
||||
private async Task OpenGpsLocation(int gpsFilesIndex)
|
||||
{
|
||||
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
|
||||
var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
|
||||
if (ann == null)
|
||||
return;
|
||||
|
||||
GpsImageEditor.Background = new ImageBrush
|
||||
{
|
||||
ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage()
|
||||
};
|
||||
if (ann.Lat != 0 && ann.Lon != 0)
|
||||
SatelliteMap.Position = new PointLatLng(ann.Lat, ann.Lon);
|
||||
}
|
||||
|
||||
private void GpsFilesContextOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var listItem = sender as ListViewItem;
|
||||
GpsFilesContextMenu.DataContext = listItem!.DataContext;
|
||||
}
|
||||
|
||||
private void OpenContainingFolder(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFile;
|
||||
if (mediaFileInfo == null)
|
||||
return;
|
||||
|
||||
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.MediaUrl +"\"");
|
||||
}
|
||||
|
||||
private async void OpenGpsTilesFolderClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new CommonOpenFileDialog
|
||||
{
|
||||
Title = "Open Video folder",
|
||||
IsFolderPicker = true,
|
||||
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
|
||||
};
|
||||
var dialogResult = dlg.ShowDialog();
|
||||
|
||||
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
|
||||
return;
|
||||
|
||||
TbGpsMapFolder.Text = dlg.FileName;
|
||||
_currentDir = dlg.FileName;
|
||||
var dir = new DirectoryInfo(dlg.FileName);
|
||||
var mediaFiles = dir.GetFiles(Constants.DefaultImageFormats.ToArray())
|
||||
.Select(x => new MediaFile
|
||||
{
|
||||
Name = x.Name,
|
||||
MediaUrl = x.FullName,
|
||||
MediaType = MediaTypes.Image
|
||||
}).ToList();
|
||||
|
||||
_allMediaFiles = mediaFiles;
|
||||
GpsFiles.ItemsSource = new ObservableCollection<MediaFile>(_allMediaFiles);
|
||||
|
||||
Annotations = mediaFiles.Select((x, i) => (i, new Annotation
|
||||
{
|
||||
Name = x.Name,
|
||||
OriginalMediaName = x.Name
|
||||
})).ToDictionary(x => x.i, x => x.Item2);
|
||||
|
||||
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
|
||||
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLatLon);
|
||||
}
|
||||
|
||||
private async void TestGps(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
|
||||
return;
|
||||
|
||||
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
|
||||
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLatLon);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Azaion.Common.DTO;
|
||||
using MediatR;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
|
||||
public class AnnClassSelectedEvent(DetectionClass detectionClass) : INotification
|
||||
{
|
||||
public DetectionClass DetectionClass { get; } = detectionClass;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
|
||||
public class VolumeChangedEvent(int volume) : INotification
|
||||
{
|
||||
public int Volume { get; set; } = volume;
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
|
||||
public static class CanvasExtensions
|
||||
{
|
||||
public static readonly DependencyProperty PercentPositionProperty =
|
||||
DependencyProperty.RegisterAttached("PercentPosition", typeof(Point), typeof(CanvasExtensions),
|
||||
new PropertyMetadata(new Point(0, 0), OnPercentPositionChanged));
|
||||
|
||||
public static readonly DependencyProperty PercentSizeProperty =
|
||||
DependencyProperty.RegisterAttached("PercentSize", typeof(Point), typeof(CanvasExtensions),
|
||||
new PropertyMetadata(new Point(0, 0), OnPercentSizeChanged));
|
||||
|
||||
private static void OnPercentSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static void SetPercentPosition(DependencyObject obj, Point value) => obj.SetValue(PercentPositionProperty, value);
|
||||
|
||||
private static void OnPercentPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (!(d is FrameworkElement element)) return;
|
||||
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
|
||||
|
||||
canvas.SizeChanged += (s, arg) =>
|
||||
{
|
||||
var percentPosition = (Point)element.GetValue(PercentPositionProperty);
|
||||
var xPosition = percentPosition.X * canvas.ActualWidth - element.ActualWidth / 2;
|
||||
var yPosition = percentPosition.Y * canvas.ActualHeight - element.ActualHeight / 2;
|
||||
Canvas.SetLeft(element, xPosition);
|
||||
Canvas.SetTop(element, yPosition);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// using System.Windows;
|
||||
// using System.Windows.Controls;
|
||||
// using System.Windows.Controls.Primitives;
|
||||
// using System.Windows.Media;
|
||||
//
|
||||
// namespace Azaion.Annotator.Extensions;
|
||||
//
|
||||
// public static class DataGridExtensions
|
||||
// {
|
||||
// public static DataGridCell? GetCell(this DataGrid grid, int rowIndex, int columnIndex = 0)
|
||||
// {
|
||||
// var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
|
||||
// if (row == null)
|
||||
// return null;
|
||||
//
|
||||
// var presenter = FindVisualChild<DataGridCellsPresenter>(row);
|
||||
// if (presenter == null)
|
||||
// return null;
|
||||
//
|
||||
// var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||
// if (cell != null) return cell;
|
||||
//
|
||||
// // now try to bring into view and retrieve the cell
|
||||
// grid.ScrollIntoView(row, grid.Columns[columnIndex]);
|
||||
// cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||
//
|
||||
// return cell;
|
||||
// }
|
||||
//
|
||||
// private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObj) where T : DependencyObject
|
||||
// {
|
||||
// if (dependencyObj == null)
|
||||
// yield break;
|
||||
//
|
||||
// for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObj); i++)
|
||||
// {
|
||||
// var child = VisualTreeHelper.GetChild(dependencyObj, i);
|
||||
// if (child is T dependencyObject)
|
||||
// {
|
||||
// yield return dependencyObject;
|
||||
// }
|
||||
//
|
||||
// foreach (T childOfChild in FindVisualChildren<T>(child))
|
||||
// {
|
||||
// yield return childOfChild;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public static TChildItem? FindVisualChild<TChildItem>(DependencyObject? obj) where TChildItem : DependencyObject =>
|
||||
// FindVisualChildren<TChildItem>(obj).FirstOrDefault();
|
||||
// }
|
||||
@@ -1,54 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
|
||||
public class ScrollViewerExtensions
|
||||
{
|
||||
public static readonly DependencyProperty AlwaysScrollToEndProperty =
|
||||
DependencyProperty.RegisterAttached("AlwaysScrollToEnd", typeof(bool), typeof(ScrollViewerExtensions), new PropertyMetadata(false, AlwaysScrollToEndChanged));
|
||||
|
||||
private static bool _autoScroll;
|
||||
|
||||
private static void AlwaysScrollToEndChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is not ScrollViewer scroll)
|
||||
throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances.");
|
||||
|
||||
var alwaysScrollToEnd = e.NewValue != null && (bool)e.NewValue;
|
||||
if (alwaysScrollToEnd)
|
||||
{
|
||||
scroll.ScrollToEnd();
|
||||
scroll.ScrollChanged += ScrollChanged;
|
||||
}
|
||||
else
|
||||
scroll.ScrollChanged -= ScrollChanged;
|
||||
}
|
||||
|
||||
public static bool GetAlwaysScrollToEnd(ScrollViewer scroll)
|
||||
{
|
||||
if (scroll == null)
|
||||
throw new ArgumentNullException("scroll");
|
||||
return (bool)scroll.GetValue(AlwaysScrollToEndProperty);
|
||||
}
|
||||
|
||||
public static void SetAlwaysScrollToEnd(ScrollViewer scroll, bool alwaysScrollToEnd)
|
||||
{
|
||||
if (scroll == null)
|
||||
throw new ArgumentNullException("scroll");
|
||||
scroll.SetValue(AlwaysScrollToEndProperty, alwaysScrollToEnd);
|
||||
}
|
||||
|
||||
private static void ScrollChanged(object sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
var scroll = sender as ScrollViewer;
|
||||
if (scroll == null)
|
||||
throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances.");
|
||||
|
||||
if (e.ExtentHeightChange == 0)
|
||||
_autoScroll = scroll.VerticalOffset == scroll.ScrollableHeight;
|
||||
|
||||
if (_autoScroll && e.ExtentHeightChange != 0)
|
||||
scroll.ScrollToVerticalOffset(scroll.ExtentHeight);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public enum HelpTextEnum
|
||||
{
|
||||
None = 0,
|
||||
Initial = 1,
|
||||
PlayVideo = 2,
|
||||
PauseForAnnotations = 3,
|
||||
AnnotationHelp = 4
|
||||
}
|
||||
|
||||
public class HelpTexts
|
||||
{
|
||||
public static Dictionary<HelpTextEnum, string> HelpTextsDict = new()
|
||||
{
|
||||
{ HelpTextEnum.None, "" },
|
||||
{ HelpTextEnum.Initial, "Натисніть Файл - Відкрити папку... та виберіть папку з вашими відео для анотації" },
|
||||
{ HelpTextEnum.PlayVideo, "В списку відео виберіть потрібне та [подвійний клік] чи [Eнтер] на ньому - запустіть його на перегляд" },
|
||||
{ HelpTextEnum.PauseForAnnotations, "В потрібному місці відео де є один з об'єктів для анотації зупиніть його [Пробіл] або кн. на панелі" },
|
||||
{ HelpTextEnum.AnnotationHelp, "Клавішами [1] - [9] або мишкою оберіть потрібний клас та виділіть, тобто зробіть анотації всіх необхідних об'єктів. " +
|
||||
"Непотрібні анотації можна виділити (через [Ctrl] декілька) та [Del] видалити. [Eнтер] для збереження і перегляду далі" }
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<Window x:Class="Azaion.Annotator.HelpWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Azaion.Annotator"
|
||||
mc:Ignorable="d"
|
||||
Title="Як анотувати: прочитайте будь ласка, це важливо" Height="700" Width="800"
|
||||
ResizeMode="NoResize"
|
||||
Topmost="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" TextWrapping="Wrap" FontSize="18" >
|
||||
Анотація - це виділений на кадрі відео об'єкт з якимось класом (Броньована техніка, вантажівка, тощо)
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="1" TextWrapping="Wrap" FontSize="18" >
|
||||
1. Анотації мусять містити об'єкти найкращої чіткості та якості. Сильно розмазані чи задимлені об'єкти не підходять
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="2" TextWrapping="Wrap" FontSize="18" >
|
||||
2. Чим більше ракурсів одного і того самого об'єкту - тим краще. Наприклад, якщо на відео об'єкт малий,
|
||||
а далі на нього наводиться камера, то треба анотувати як малий об'єкт, так і великий. Якщо об'єкт статичний і ракурс не змінюється,
|
||||
достатньо одної анотації, а якщо рухається, і видно об'єкт з різних боків - то треба пару, по 1 на ракурс
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="3" TextWrapping="Wrap" FontSize="18" >
|
||||
3. Анотація об'єктів з формою що суттєво відрізняється від прямокутника. Наприклад, якщо танк має довге дуло, саме дуло не треба виділяти,
|
||||
оскільки попадає в анотацію дуже багато зайвого. Те ж саме з окопами - якщо окопи займають візуально багато місця,
|
||||
і в квадрат буде попадати багато зайвого, то краще зробити пару малих анотацій саме окопів
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="4" TextWrapping="Wrap" FontSize="18" >
|
||||
4. Будь-які існуючі позначки на відео, OSD і інше не мусять бути в анотаціях. Анотація мусить мати лише конкретний об'єкт без ліній на ньому
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="5" TextWrapping="Wrap" FontSize="18" >
|
||||
5. До кожного відео мусить бути 2-3 пустих фоток без об'єктів і без анотацій, для гарнішого навчання. (Просто натиснути [Ентер] на парі різних кадрів без нічого)
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="6" TextWrapping="Wrap" FontSize="18" >
|
||||
6. Об'єкти одного класу мусять бути візуально схожими, тоді як об'єкти різних класів мусять візуально відрізнятися.
|
||||
Оскільки це не є каталог військової техніки, а програма для створення датасету для навчання нейронної мережі,
|
||||
то принципи обрання класів мусять підпорядковуватись візуальній схожості для кращого розпізнавання, а не чіткій військовій класифікації.
|
||||
Наприклад, артилерія - це переважно міномети, тобто візуально це труба з чимось на основі.
|
||||
Тоді будь яка самохідна артилерія на гусеницях, хоч вона являє собою артилерію, мусить бути анотована як "Броньована техніка", оскільки візуально
|
||||
вона значно більш схожа на танк ніж на міномет.
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Windows;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class HelpWindow : Window
|
||||
{
|
||||
public HelpWindow()
|
||||
{
|
||||
Closing += (sender, args) =>
|
||||
{
|
||||
args.Cancel = true;
|
||||
Visibility = Visibility.Hidden;
|
||||
};
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"File": "File",
|
||||
"Duration": "Duration",
|
||||
"Key": "Key",
|
||||
"ClassName": "Class Name",
|
||||
"HelpOpen": "Open file from the list",
|
||||
"HelpPause": "Press Space to pause video and "
|
||||
},
|
||||
"ua": {
|
||||
"File": "Файл",
|
||||
"Duration": "Тривалість",
|
||||
"Key": "Клавіша",
|
||||
"ClassName": "Назва класу"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
<PackageReference Include="LazyCache" Version="2.4.0" />
|
||||
<PackageReference Include="linq2db.SQLite" Version="5.4.1" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.5" />
|
||||
<PackageReference Include="NetMQ" Version="4.0.1.16" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Polly" Version="8.5.2" />
|
||||
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
|
||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,273 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using System.Windows;
|
||||
|
||||
namespace Azaion.Common;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const string CONFIG_PATH = "config.json";
|
||||
public const string DEFAULT_API_URL = "https://api.azaion.com";
|
||||
public const string AZAION_SUITE_EXE = "Azaion.Suite.exe";
|
||||
|
||||
public const int AI_TILE_SIZE_DEFAULT = 1280;
|
||||
|
||||
#region ExternalClientsConfig
|
||||
|
||||
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
|
||||
private const int DEFAULT_ZMQ_LOADER_PORT = 5025;
|
||||
private static readonly LoaderClientConfig DefaultLoaderClientConfig = new()
|
||||
{
|
||||
ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST,
|
||||
ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT,
|
||||
ApiUrl = DEFAULT_API_URL
|
||||
};
|
||||
|
||||
public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe";
|
||||
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
|
||||
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
|
||||
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
|
||||
|
||||
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
|
||||
private const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
|
||||
|
||||
private static readonly InferenceClientConfig DefaultInferenceClientConfig = new()
|
||||
{
|
||||
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
|
||||
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
|
||||
ApiUrl = DEFAULT_API_URL
|
||||
};
|
||||
|
||||
private const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
|
||||
private const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255;
|
||||
private const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256;
|
||||
private static readonly GpsDeniedClientConfig DefaultGpsDeniedClientConfig = new()
|
||||
{
|
||||
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
|
||||
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
|
||||
ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT
|
||||
};
|
||||
|
||||
#endregion ExternalClientsConfig
|
||||
|
||||
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
|
||||
|
||||
public const string JPG_EXT = ".jpg";
|
||||
public const string TXT_EXT = ".txt";
|
||||
#region DirectoriesConfig
|
||||
|
||||
public const string DEFAULT_VIDEO_DIR = "video";
|
||||
private const string DEFAULT_LABELS_DIR = "labels";
|
||||
private const string DEFAULT_IMAGES_DIR = "images";
|
||||
private const string DEFAULT_RESULTS_DIR = "results";
|
||||
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
|
||||
private const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
|
||||
private const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region AnnotatorConfig
|
||||
|
||||
public static readonly List<DetectionClass> DefaultAnnotationClasses =
|
||||
[
|
||||
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor(), MaxSizeM = 7 },
|
||||
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor(), MaxSizeM = 8 },
|
||||
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor(), MaxSizeM = 7 },
|
||||
new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor(), MaxSizeM = 14 },
|
||||
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor(), MaxSizeM = 9 },
|
||||
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor(), MaxSizeM = 10 },
|
||||
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor(), MaxSizeM = 2 },
|
||||
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor(), MaxSizeM = 5 },
|
||||
new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor(), MaxSizeM = 7 },
|
||||
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor(), MaxSizeM = 8 },
|
||||
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor(), MaxSizeM = 12 },
|
||||
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor(), MaxSizeM = 3 },
|
||||
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor(), MaxSizeM = 14 },
|
||||
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor(), MaxSizeM = 8 },
|
||||
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor(), MaxSizeM = 15 },
|
||||
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor(), MaxSizeM = 20 },
|
||||
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor(), MaxSizeM = 10 },
|
||||
new() { Id = 17, Name = "Ammo", ShortName = "БК", Color = "#33658a".ToColor(), MaxSizeM = 2 },
|
||||
new() { Id = 18, Name = "Protect.Struct", ShortName = "Зуби.драк", Color = "#969647".ToColor(), MaxSizeM = 2 }
|
||||
];
|
||||
|
||||
public static readonly List<string> DefaultVideoFormats = [".mp4", ".mov", ".avi", ".ts", ".mkv"];
|
||||
public static readonly List<string> DefaultImageFormats = [".jpg", ".jpeg", ".png", ".bmp"];
|
||||
|
||||
private static readonly AnnotationConfig DefaultAnnotationConfig = new()
|
||||
{
|
||||
DetectionClasses = DefaultAnnotationClasses,
|
||||
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
|
||||
};
|
||||
|
||||
#region UIConfig
|
||||
public const int DEFAULT_LEFT_PANEL_WIDTH = 200;
|
||||
public const int DEFAULT_RIGHT_PANEL_WIDTH = 200;
|
||||
#endregion UIConfig
|
||||
|
||||
#region CameraConfig
|
||||
|
||||
public const int DEFAULT_ALTITUDE = 400;
|
||||
public const decimal DEFAULT_CAMERA_FOCAL_LENGTH = 24m;
|
||||
public const decimal DEFAULT_CAMERA_SENSOR_WIDTH = 23.5m;
|
||||
|
||||
public static readonly CameraConfig DefaultCameraConfig = new()
|
||||
{
|
||||
Altitude = DEFAULT_ALTITUDE,
|
||||
CameraFocalLength = DEFAULT_CAMERA_FOCAL_LENGTH,
|
||||
CameraSensorWidth = DEFAULT_CAMERA_SENSOR_WIDTH
|
||||
};
|
||||
|
||||
#endregion CameraConfig
|
||||
|
||||
private const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
|
||||
|
||||
# endregion AnnotatorConfig
|
||||
|
||||
# region AIRecognitionConfig
|
||||
|
||||
private static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
|
||||
{
|
||||
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
|
||||
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
|
||||
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
|
||||
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
|
||||
BigImageTileOverlapPercent = DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT,
|
||||
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
|
||||
};
|
||||
|
||||
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
||||
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
||||
private const double TRACKING_PROBABILITY_INCREASE = 15;
|
||||
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
||||
private const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20;
|
||||
private const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
||||
|
||||
# endregion AIRecognitionConfig
|
||||
|
||||
# region GpsDeniedConfig
|
||||
|
||||
private static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new()
|
||||
{
|
||||
MinKeyPoints = 11
|
||||
};
|
||||
|
||||
# endregion
|
||||
|
||||
#region Thumbnails
|
||||
|
||||
private static readonly Size DefaultThumbnailSize = new(240, 135);
|
||||
|
||||
private static readonly ThumbnailConfig DefaultThumbnailConfig = new()
|
||||
{
|
||||
Size = DefaultThumbnailSize,
|
||||
Border = DEFAULT_THUMBNAIL_BORDER
|
||||
};
|
||||
|
||||
private const int DEFAULT_THUMBNAIL_BORDER = 10;
|
||||
|
||||
public const string THUMBNAIL_PREFIX = "_thumb";
|
||||
public const string RESULT_PREFIX = "_result";
|
||||
|
||||
#endregion
|
||||
|
||||
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
||||
|
||||
#region Database
|
||||
|
||||
public const string ANNOTATIONS_TABLENAME = "annotations";
|
||||
public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue";
|
||||
public const string ADMIN_EMAIL = "admin@azaion.com";
|
||||
public const string DETECTIONS_TABLENAME = "detections";
|
||||
public const string MEDIAFILE_TABLENAME = "mediafiles";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Captions
|
||||
|
||||
public const string REGULAR_MODE_CAPTION = "Норма";
|
||||
public const string WINTER_MODE_CAPTION = "Зима";
|
||||
public const string NIGHT_MODE_CAPTION = "Ніч";
|
||||
|
||||
#endregion
|
||||
|
||||
public const string SPLIT_SUFFIX = "!split!";
|
||||
|
||||
|
||||
private static readonly InitConfig DefaultInitConfig = new()
|
||||
{
|
||||
LoaderClientConfig = DefaultLoaderClientConfig,
|
||||
InferenceClientConfig = DefaultInferenceClientConfig,
|
||||
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
|
||||
DirectoriesConfig = new DirectoriesConfig
|
||||
{
|
||||
ApiResourcesDirectory = ""
|
||||
},
|
||||
CameraConfig = DefaultCameraConfig
|
||||
};
|
||||
|
||||
public static readonly AppConfig FailsafeAppConfig = new()
|
||||
{
|
||||
AnnotationConfig = DefaultAnnotationConfig,
|
||||
|
||||
UIConfig = new UIConfig
|
||||
{
|
||||
LeftPanelWidth = DEFAULT_LEFT_PANEL_WIDTH,
|
||||
RightPanelWidth = DEFAULT_RIGHT_PANEL_WIDTH,
|
||||
GenerateAnnotatedImage = false
|
||||
},
|
||||
|
||||
DirectoriesConfig = new DirectoriesConfig
|
||||
{
|
||||
VideosDirectory = DEFAULT_VIDEO_DIR,
|
||||
ImagesDirectory = DEFAULT_IMAGES_DIR,
|
||||
LabelsDirectory = DEFAULT_LABELS_DIR,
|
||||
ResultsDirectory = DEFAULT_RESULTS_DIR,
|
||||
ThumbnailsDirectory = DEFAULT_THUMBNAILS_DIR,
|
||||
GpsSatDirectory = DEFAULT_GPS_SAT_DIRECTORY,
|
||||
GpsRouteDirectory = DEFAULT_GPS_ROUTE_DIRECTORY
|
||||
},
|
||||
|
||||
ThumbnailConfig = DefaultThumbnailConfig,
|
||||
AIRecognitionConfig = DefaultAIRecognitionConfig,
|
||||
GpsDeniedConfig = DefaultGpsDeniedConfig,
|
||||
|
||||
LoaderClientConfig = DefaultLoaderClientConfig,
|
||||
InferenceClientConfig = DefaultInferenceClientConfig,
|
||||
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
|
||||
CameraConfig = DefaultCameraConfig
|
||||
};
|
||||
|
||||
public static InitConfig ReadInitConfig(ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(CONFIG_PATH))
|
||||
throw new FileNotFoundException(CONFIG_PATH);
|
||||
var configStr = File.ReadAllText(CONFIG_PATH);
|
||||
var config = JsonConvert.DeserializeObject<InitConfig>(configStr);
|
||||
|
||||
return config ?? DefaultInitConfig;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, e.Message);
|
||||
return DefaultInitConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public static Version GetLocalVersion()
|
||||
{
|
||||
var localFileInfo = FileVersionInfo.GetVersionInfo(AZAION_SUITE_EXE);
|
||||
if (string.IsNullOrWhiteSpace(localFileInfo.ProductVersion))
|
||||
throw new Exception($"Can't find {AZAION_SUITE_EXE} and its version");
|
||||
return new Version(localFileInfo.FileVersion!);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<UserControl x:Class="Azaion.Common.Controls.CameraConfigControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:cfg="clr-namespace:Azaion.Common.DTO.Config"
|
||||
xmlns:controls="clr-namespace:Azaion.Common.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="120" d:DesignWidth="360">
|
||||
<Grid Margin="4" Background="Black">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="65"/>
|
||||
<ColumnDefinition Width="70"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="70"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Altitude -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Foreground="LightGray"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" Text="Altitude, m:"/>
|
||||
<Slider x:Name="AltitudeSlider" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Minimum="0" Maximum="10000" TickFrequency="100"
|
||||
IsSnapToTickEnabled="False"
|
||||
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<controls:NumericUpDown x:Name="AltitudeNud"
|
||||
Grid.Row="0" Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
MinValue="50"
|
||||
MaxValue="5000"
|
||||
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Step="10">
|
||||
|
||||
</controls:NumericUpDown>
|
||||
|
||||
<!-- Focal length -->
|
||||
<TextBlock
|
||||
Foreground="LightGray"
|
||||
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,8,8,0" Text="Focal length, mm:"/>
|
||||
<controls:NumericUpDown x:Name="FocalNud"
|
||||
Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
|
||||
MinValue="0.1"
|
||||
MaxValue="100"
|
||||
Step="0.05"
|
||||
VerticalAlignment="Center"
|
||||
Value="{Binding Camera.CameraFocalLength, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
</controls:NumericUpDown>
|
||||
|
||||
<!-- Sensor width -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="LightGray"
|
||||
Margin="0,8,8,0" Text="Sensor width, mm:"/>
|
||||
<controls:NumericUpDown x:Name="SensorNud"
|
||||
Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Step="0.05"
|
||||
VerticalAlignment="Center"
|
||||
MinValue="0.1"
|
||||
MaxValue="100"
|
||||
Value="{Binding Camera.CameraSensorWidth, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
</controls:NumericUpDown>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Azaion.Common.DTO.Config;
|
||||
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public partial class CameraConfigControl
|
||||
{
|
||||
public static readonly DependencyProperty CameraProperty = DependencyProperty.Register(
|
||||
nameof(Camera), typeof(CameraConfig), typeof(CameraConfigControl),
|
||||
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
|
||||
|
||||
public CameraConfig Camera
|
||||
{
|
||||
get => (CameraConfig)GetValue(CameraProperty) ?? new CameraConfig();
|
||||
set => SetValue(CameraProperty, value);
|
||||
}
|
||||
|
||||
// Fires whenever any camera parameter value changes in UI
|
||||
public event EventHandler? CameraChanged;
|
||||
|
||||
public CameraConfigControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Hook up change notifications
|
||||
if (AltitudeSlider != null)
|
||||
AltitudeSlider.ValueChanged += (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
SubscribeNud(AltitudeNud);
|
||||
SubscribeNud(FocalNud);
|
||||
SubscribeNud(SensorNud);
|
||||
}
|
||||
|
||||
private void SubscribeNud(UserControl? nud)
|
||||
{
|
||||
if (nud is NumericUpDown num)
|
||||
{
|
||||
var dpd = DependencyPropertyDescriptor.FromProperty(NumericUpDown.ValueProperty, typeof(NumericUpDown));
|
||||
dpd?.AddValueChanged(num, (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
// Initializes the control with the provided CameraConfig instance and wires two-way binding via dependency property
|
||||
public void Init(CameraConfig cameraConfig)
|
||||
{
|
||||
Camera = cameraConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,551 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.Extensions;
|
||||
using Color = System.Windows.Media.Color;
|
||||
using Image = System.Windows.Controls.Image;
|
||||
using Point = System.Windows.Point;
|
||||
using Rectangle = System.Windows.Shapes.Rectangle;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public class CanvasEditor : Canvas
|
||||
{
|
||||
private Point _lastPos;
|
||||
private SelectionState SelectionState { get; set; } = SelectionState.None;
|
||||
|
||||
private readonly Rectangle _newAnnotationRect;
|
||||
private Point _newAnnotationStartPos;
|
||||
|
||||
private readonly Line _horizontalLine;
|
||||
private readonly Line _verticalLine;
|
||||
private readonly TextBlock _classNameHint;
|
||||
|
||||
private Rectangle _curRec = new();
|
||||
private DetectionControl? _curAnn;
|
||||
|
||||
private readonly MatrixTransform _matrixTransform = new();
|
||||
private Point _panStartPoint;
|
||||
private bool _isZoomedIn;
|
||||
|
||||
private const int MIN_SIZE = 12;
|
||||
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
||||
|
||||
public Image BackgroundImage { get; set; } = new() { Stretch = Stretch.Uniform };
|
||||
private RectangleF? _clampedRect;
|
||||
|
||||
public static readonly DependencyProperty GetTimeFuncProp =
|
||||
DependencyProperty.Register(
|
||||
nameof(GetTimeFunc),
|
||||
typeof(Func<TimeSpan>),
|
||||
typeof(CanvasEditor),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public Func<TimeSpan> GetTimeFunc
|
||||
{
|
||||
get => (Func<TimeSpan>)GetValue(GetTimeFuncProp);
|
||||
set => SetValue(GetTimeFuncProp, value);
|
||||
}
|
||||
|
||||
private DetectionClass _currentAnnClass = null!;
|
||||
public DetectionClass? CurrentAnnClass
|
||||
{
|
||||
get => _currentAnnClass;
|
||||
set
|
||||
{
|
||||
_verticalLine.Stroke = value!.ColorBrush;
|
||||
_verticalLine.Fill = value.ColorBrush;
|
||||
_horizontalLine.Stroke = value.ColorBrush;
|
||||
_horizontalLine.Fill = value.ColorBrush;
|
||||
_classNameHint.Text = value.ShortName;
|
||||
|
||||
_newAnnotationRect.Stroke = value.ColorBrush;
|
||||
_newAnnotationRect.Fill = value.ColorBrush;
|
||||
_currentAnnClass = value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<DetectionControl> CurrentDetections = new();
|
||||
|
||||
public CanvasEditor()
|
||||
{
|
||||
_horizontalLine = new Line
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
Stroke = new SolidColorBrush(Colors.Blue),
|
||||
Fill = new SolidColorBrush(Colors.Blue),
|
||||
StrokeDashArray = [5],
|
||||
StrokeThickness = 2
|
||||
};
|
||||
_verticalLine = new Line
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
Stroke = new SolidColorBrush(Colors.Blue),
|
||||
Fill = new SolidColorBrush(Colors.Blue),
|
||||
StrokeDashArray = [5],
|
||||
StrokeThickness = 2
|
||||
};
|
||||
_classNameHint = new TextBlock
|
||||
{
|
||||
Text = CurrentAnnClass?.ShortName ?? "",
|
||||
Foreground = new SolidColorBrush(Colors.Black),
|
||||
Cursor = Cursors.Arrow,
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeights.Bold
|
||||
};
|
||||
_newAnnotationRect = new Rectangle
|
||||
{
|
||||
Name = "selector",
|
||||
Height = 0,
|
||||
Width = 0,
|
||||
Stroke = new SolidColorBrush(Colors.Gray),
|
||||
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
|
||||
};
|
||||
|
||||
MouseDown += CanvasMouseDown;
|
||||
MouseMove += CanvasMouseMove;
|
||||
MouseUp += CanvasMouseUp;
|
||||
SizeChanged += CanvasResized;
|
||||
Cursor = Cursors.Cross;
|
||||
Children.Insert(0, BackgroundImage);
|
||||
Children.Add(_newAnnotationRect);
|
||||
Children.Add(_horizontalLine);
|
||||
Children.Add(_verticalLine);
|
||||
Children.Add(_classNameHint);
|
||||
|
||||
Loaded += Init;
|
||||
RenderTransform = _matrixTransform;
|
||||
MouseWheel += CanvasWheel;
|
||||
}
|
||||
|
||||
public void SetBackground(ImageSource? source)
|
||||
{
|
||||
SetZoom();
|
||||
BackgroundImage.Source = source;
|
||||
UpdateClampedRect();
|
||||
}
|
||||
|
||||
private void SetZoom(Matrix? matrix = null)
|
||||
{
|
||||
if (matrix == null)
|
||||
{
|
||||
_matrixTransform.Matrix = Matrix.Identity;
|
||||
_isZoomedIn = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_matrixTransform.Matrix = matrix.Value;
|
||||
_isZoomedIn = true;
|
||||
}
|
||||
// foreach (var detection in CurrentDetections)
|
||||
// detection.UpdateAdornerScale(scale: _matrixTransform.Matrix.M11);
|
||||
}
|
||||
|
||||
private void CanvasWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (Keyboard.Modifiers != ModifierKeys.Control)
|
||||
return;
|
||||
|
||||
var mousePos = e.GetPosition(this);
|
||||
var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
|
||||
|
||||
var matrix = _matrixTransform.Matrix;
|
||||
if (scale < 1 && matrix.M11 * scale < 1.0)
|
||||
SetZoom();
|
||||
else
|
||||
{
|
||||
matrix.ScaleAt(scale, scale, mousePos.X, mousePos.Y);
|
||||
SetZoom(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
private void Init(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_horizontalLine.X1 = 0;
|
||||
_horizontalLine.X2 = ActualWidth;
|
||||
_verticalLine.Y1 = 0;
|
||||
_verticalLine.Y2 = ActualHeight;
|
||||
}
|
||||
|
||||
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
ClearSelections();
|
||||
if (e.LeftButton != MouseButtonState.Pressed)
|
||||
return;
|
||||
if (Keyboard.Modifiers == ModifierKeys.Control && _isZoomedIn)
|
||||
{
|
||||
_panStartPoint = e.GetPosition(this);
|
||||
SelectionState = SelectionState.PanZoomMoving;
|
||||
}
|
||||
else
|
||||
NewAnnotationStart(sender, e);
|
||||
(sender as UIElement)?.CaptureMouse();
|
||||
}
|
||||
|
||||
private void CanvasMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var pos = GetClampedPosition(e);
|
||||
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
|
||||
_verticalLine.X1 = _verticalLine.X2 = pos.X;
|
||||
SetLeft(_classNameHint, pos.X + 10);
|
||||
SetTop(_classNameHint, pos.Y - 30);
|
||||
|
||||
switch (SelectionState)
|
||||
{
|
||||
case SelectionState.NewAnnCreating:
|
||||
NewAnnotationCreatingMove(pos);
|
||||
break;
|
||||
case SelectionState.AnnResizing:
|
||||
AnnotationResizeMove(pos);
|
||||
break;
|
||||
case SelectionState.AnnMoving:
|
||||
AnnotationPositionMove(pos);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case SelectionState.PanZoomMoving:
|
||||
PanZoomMove(pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Point GetClampedPosition(MouseEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(this);
|
||||
return !_clampedRect.HasValue
|
||||
? pos
|
||||
: new Point
|
||||
(
|
||||
Math.Clamp(pos.X, _clampedRect.Value.X, _clampedRect.Value.Right),
|
||||
Math.Clamp(pos.Y, _clampedRect.Value.Y, _clampedRect.Value.Bottom)
|
||||
);
|
||||
}
|
||||
|
||||
private void PanZoomMove(Point point)
|
||||
{
|
||||
var delta = point - _panStartPoint;
|
||||
|
||||
var matrix = _matrixTransform.Matrix;
|
||||
matrix.Translate(delta.X, delta.Y);
|
||||
|
||||
_matrixTransform.Matrix = matrix;
|
||||
}
|
||||
|
||||
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
(sender as UIElement)?.ReleaseMouseCapture();
|
||||
if (SelectionState == SelectionState.NewAnnCreating)
|
||||
{
|
||||
var endPos = GetClampedPosition(e);
|
||||
_newAnnotationRect.Width = 0;
|
||||
_newAnnotationRect.Height = 0;
|
||||
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
|
||||
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
|
||||
if (width >= MIN_SIZE && height >= MIN_SIZE)
|
||||
{
|
||||
var time = GetTimeFunc();
|
||||
var control = CreateDetectionControl(CurrentAnnClass!, time, new CanvasLabel
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
Left = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
||||
Top = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
|
||||
Confidence = 1
|
||||
});
|
||||
control.UpdateLayout();
|
||||
CheckLabelBoundaries(control);
|
||||
}
|
||||
}
|
||||
else if (SelectionState != SelectionState.PanZoomMoving && _curAnn != null)
|
||||
CheckLabelBoundaries(_curAnn);
|
||||
|
||||
SelectionState = SelectionState.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CheckLabelBoundaries(DetectionControl detectionControl)
|
||||
{
|
||||
var lb = detectionControl.DetectionLabelContainer;
|
||||
var origin = lb.TranslatePoint(new Point(0, 0), this);
|
||||
lb.Children[0].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var size = lb.Children[0].DesiredSize;
|
||||
var controlLabel = new RectangleF((float)origin.X, (float)origin.Y, (float)size.Width, (float)size.Height);
|
||||
|
||||
foreach (var c in CurrentDetections)
|
||||
{
|
||||
if (c == detectionControl)
|
||||
continue;
|
||||
var detRect = new RectangleF((float)GetLeft(c), (float)GetTop(c), (float)c.Width, (float)c.Height);
|
||||
detRect.Intersect(controlLabel);
|
||||
|
||||
|
||||
// var intersect = detections[i].ToRectangle();
|
||||
// intersect.Intersect(detections[j].ToRectangle());
|
||||
|
||||
// detectionControl.
|
||||
// var otherControls = allControls.Where(c => c != control);
|
||||
// control.UpdateLabelPosition(otherControls);
|
||||
}
|
||||
}
|
||||
|
||||
private void CanvasResized(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
_horizontalLine.X2 = e.NewSize.Width;
|
||||
_verticalLine.Y2 = e.NewSize.Height;
|
||||
BackgroundImage.Width = e.NewSize.Width;
|
||||
BackgroundImage.Height = e.NewSize.Height;
|
||||
UpdateClampedRect();
|
||||
}
|
||||
|
||||
private void UpdateClampedRect()
|
||||
{
|
||||
if (BackgroundImage.Source is not BitmapSource imageSource)
|
||||
{
|
||||
_clampedRect = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var imgWidth = imageSource.PixelWidth;
|
||||
var imgHeight = imageSource.PixelHeight;
|
||||
var canvasWidth = ActualWidth;
|
||||
var canvasHeight = ActualHeight;
|
||||
|
||||
var imgRatio = imgWidth / (double)imgHeight;
|
||||
var canvasRatio = canvasWidth / canvasHeight;
|
||||
|
||||
double renderedWidth;
|
||||
double renderedHeight;
|
||||
|
||||
if (imgRatio > canvasRatio)
|
||||
{
|
||||
renderedWidth = canvasWidth;
|
||||
renderedHeight = canvasWidth / imgRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
renderedHeight = canvasHeight;
|
||||
renderedWidth = canvasHeight * imgRatio;
|
||||
}
|
||||
var xOffset = (canvasWidth - renderedWidth) / 2;
|
||||
var yOffset = (canvasHeight - renderedHeight) / 2;
|
||||
|
||||
_clampedRect = new RectangleF((float)xOffset, (float)yOffset, (float)renderedWidth, (float)renderedHeight);
|
||||
}
|
||||
|
||||
#region Annotation Resizing & Moving
|
||||
|
||||
private void AnnotationResizeStart(object sender, MouseEventArgs e)
|
||||
{
|
||||
SelectionState = SelectionState.AnnResizing;
|
||||
_lastPos = e.GetPosition(this);
|
||||
_curRec = (Rectangle)sender;
|
||||
_curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
|
||||
(sender as UIElement)?.CaptureMouse();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void AnnotationResizeMove(Point point)
|
||||
{
|
||||
if (SelectionState != SelectionState.AnnResizing || _curAnn == null)
|
||||
return;
|
||||
|
||||
var x = GetLeft(_curAnn);
|
||||
var y = GetTop(_curAnn);
|
||||
var offsetX = point.X - _lastPos.X;
|
||||
var offsetY = point.Y - _lastPos.Y;
|
||||
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
|
||||
{
|
||||
case (HorizontalAlignment.Left, VerticalAlignment.Top):
|
||||
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
|
||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
|
||||
SetLeft(_curAnn, x + offsetX);
|
||||
SetTop(_curAnn, y + offsetY);
|
||||
break;
|
||||
case (HorizontalAlignment.Center, VerticalAlignment.Top):
|
||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
|
||||
SetTop(_curAnn, y + offsetY);
|
||||
break;
|
||||
case (HorizontalAlignment.Right, VerticalAlignment.Top):
|
||||
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
|
||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
|
||||
SetTop(_curAnn, y + offsetY);
|
||||
break;
|
||||
|
||||
case (HorizontalAlignment.Left, VerticalAlignment.Center):
|
||||
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
|
||||
SetLeft(_curAnn, x + offsetX);
|
||||
break;
|
||||
case (HorizontalAlignment.Right, VerticalAlignment.Center):
|
||||
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
|
||||
break;
|
||||
|
||||
case (HorizontalAlignment.Left, VerticalAlignment.Bottom):
|
||||
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
|
||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||
SetLeft(_curAnn, x + offsetX);
|
||||
break;
|
||||
case (HorizontalAlignment.Center, VerticalAlignment.Bottom):
|
||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||
break;
|
||||
case (HorizontalAlignment.Right, VerticalAlignment.Bottom):
|
||||
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
|
||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||
break;
|
||||
}
|
||||
_lastPos = point;
|
||||
}
|
||||
|
||||
private void AnnotationPositionStart(object sender, MouseEventArgs e)
|
||||
{
|
||||
_lastPos = e.GetPosition(this);
|
||||
_curAnn = (DetectionControl)sender;
|
||||
|
||||
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
ClearSelections();
|
||||
|
||||
_curAnn.IsSelected = true;
|
||||
|
||||
SelectionState = SelectionState.AnnMoving;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void AnnotationPositionMove(Point point)
|
||||
{
|
||||
if (SelectionState != SelectionState.AnnMoving || _curAnn == null)
|
||||
return;
|
||||
|
||||
var offsetX = point.X - _lastPos.X;
|
||||
var offsetY = point.Y - _lastPos.Y;
|
||||
|
||||
var nextLeft = GetLeft(_curAnn) + offsetX;
|
||||
var nextTop = GetTop(_curAnn) + offsetY;
|
||||
|
||||
if (_clampedRect.HasValue)
|
||||
{
|
||||
nextLeft = Math.Clamp(nextLeft, _clampedRect.Value.X, _clampedRect.Value.Right - _curAnn.Width);
|
||||
nextTop = Math.Clamp(nextTop, _clampedRect.Value.Y, _clampedRect.Value.Bottom - _curAnn.Height);
|
||||
}
|
||||
|
||||
SetLeft(_curAnn, nextLeft);
|
||||
SetTop(_curAnn, nextTop);
|
||||
_lastPos = point;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NewAnnotation
|
||||
|
||||
private void NewAnnotationStart(object _, MouseButtonEventArgs e)
|
||||
{
|
||||
_newAnnotationStartPos = e.GetPosition(this);
|
||||
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
|
||||
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
|
||||
_newAnnotationRect.MouseMove += (_, args) =>
|
||||
{
|
||||
var currentPos = args.GetPosition(this);
|
||||
NewAnnotationCreatingMove(currentPos);
|
||||
};
|
||||
|
||||
SelectionState = SelectionState.NewAnnCreating;
|
||||
}
|
||||
|
||||
private void NewAnnotationCreatingMove(Point point)
|
||||
{
|
||||
if (SelectionState != SelectionState.NewAnnCreating)
|
||||
return;
|
||||
|
||||
var diff = point - _newAnnotationStartPos;
|
||||
|
||||
_newAnnotationRect.Height = Math.Abs(diff.Y);
|
||||
_newAnnotationRect.Width = Math.Abs(diff.X);
|
||||
|
||||
if (diff.X < 0)
|
||||
SetLeft(_newAnnotationRect, point.X);
|
||||
if (diff.Y < 0)
|
||||
SetTop(_newAnnotationRect, point.Y);
|
||||
}
|
||||
|
||||
public void CreateDetections(Annotation annotation, List<DetectionClass> detectionClasses, Size mediaSize)
|
||||
{
|
||||
foreach (var detection in annotation.Detections)
|
||||
{
|
||||
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
|
||||
CanvasLabel canvasLabel;
|
||||
if (!annotation.IsSplit || mediaSize.FitSizeForAI())
|
||||
canvasLabel = new CanvasLabel(detection, RenderSize, mediaSize, detection.Confidence);
|
||||
else
|
||||
{
|
||||
canvasLabel = new CanvasLabel(detection, annotation.SplitTile!.Size, null, detection.Confidence)
|
||||
.ReframeFromSmall(annotation.SplitTile);
|
||||
|
||||
//From CurrentMediaSize to Render Size
|
||||
var yoloLabel = new YoloLabel(canvasLabel, mediaSize);
|
||||
canvasLabel = new CanvasLabel(yoloLabel, RenderSize, mediaSize, canvasLabel.Confidence);
|
||||
}
|
||||
|
||||
var control = CreateDetectionControl(detectionClass, annotation.Time, canvasLabel);
|
||||
control.UpdateLayout();
|
||||
CheckLabelBoundaries(control);
|
||||
}
|
||||
}
|
||||
|
||||
private DetectionControl CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
|
||||
{
|
||||
var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
|
||||
detectionControl.MouseDown += AnnotationPositionStart;
|
||||
SetLeft(detectionControl, canvasLabel.Left );
|
||||
SetTop(detectionControl, canvasLabel.Top);
|
||||
Children.Add(detectionControl);
|
||||
CurrentDetections.Add(detectionControl);
|
||||
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
|
||||
return detectionControl;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void RemoveAnnotations(IEnumerable<DetectionControl> listToRemove)
|
||||
{
|
||||
foreach (var ann in listToRemove)
|
||||
{
|
||||
Children.Remove(ann);
|
||||
CurrentDetections.Remove(ann);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAllAnns()
|
||||
{
|
||||
foreach (var ann in CurrentDetections)
|
||||
Children.Remove(ann);
|
||||
CurrentDetections.Clear();
|
||||
}
|
||||
|
||||
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentDetections.Where(x => x.IsSelected).ToList());
|
||||
|
||||
private void ClearSelections()
|
||||
{
|
||||
foreach (var ann in CurrentDetections)
|
||||
ann.IsSelected = false;
|
||||
}
|
||||
|
||||
public void ClearExpiredAnnotations(TimeSpan time)
|
||||
{
|
||||
var expiredAnns = CurrentDetections.Where(x =>
|
||||
Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
|
||||
.ToList();
|
||||
RemoveAnnotations(expiredAnns);
|
||||
}
|
||||
|
||||
public void ZoomTo(Point point)
|
||||
{
|
||||
SetZoom();
|
||||
var matrix = _matrixTransform.Matrix;
|
||||
matrix.ScaleAt(2, 2, point.X, point.Y);
|
||||
SetZoom(matrix);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
<UserControl x:Class="Azaion.Common.Controls.DetectionClasses"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="ButtonRadioButtonStyle" TargetType="RadioButton">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}"
|
||||
Background="{TemplateBinding Background}" BorderThickness="1"
|
||||
Padding="10,5" CornerRadius="2">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="Gray"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="DarkGray"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="White"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="Black">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- Your DataGrid with detection classes -->
|
||||
<DataGrid x:Name="DetectionDataGrid"
|
||||
Grid.Row="0"
|
||||
Background="Black"
|
||||
RowBackground="#252525"
|
||||
Foreground="White"
|
||||
RowHeaderWidth="0"
|
||||
Padding="2 0 0 0"
|
||||
AutoGenerateColumns="False"
|
||||
SelectionMode="Single"
|
||||
CellStyle="{DynamicResource DataGridCellStyle1}"
|
||||
IsReadOnly="True"
|
||||
CanUserResizeRows="False"
|
||||
CanUserResizeColumns="False"
|
||||
SelectionChanged="DetectionDataGrid_SelectionChanged"
|
||||
x:FieldModifier="public"
|
||||
PreviewKeyDown="OnKeyBanActivity"
|
||||
>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
|
||||
<DataGridTemplateColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#252525"/>
|
||||
</Style>
|
||||
</DataGridTemplateColumn.HeaderStyle>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Path=ColorBrush}">
|
||||
<TextBlock Text="{Binding Path=ClassNumber}"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Width="*" Header="Назва" Binding="{Binding Path=ShortName}" CanUserSort="False">
|
||||
<DataGridTextColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#252525"/>
|
||||
</Style>
|
||||
</DataGridTextColumn.HeaderStyle>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<!-- StackPanel with mode switcher RadioButtons -->
|
||||
<StackPanel x:Name="ModeSwitcherPanel"
|
||||
Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,2,0,2">
|
||||
<RadioButton x:Name="NormalModeRadioButton"
|
||||
Tag="0"
|
||||
GroupName="Mode"
|
||||
Checked="ModeRadioButton_Checked"
|
||||
IsChecked="True"
|
||||
Style="{StaticResource ButtonRadioButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Height="16" Width="16">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m256,105.5c-83.9,0-152.2,68.3-152.2,152.2 0,83.9 68.3,152.2 152.2,152.2 83.9,0 152.2-68.3
|
||||
152.2-152.2 0-84-68.3-152.2-152.2-152.2zm0,263.5c-61.4,0-111.4-50-111.4-111.4 0-61.4 50-111.4 111.4-111.4 61.4,0 111.4,50 111.4,111.4
|
||||
0,61.4-50,111.4-111.4,111.4z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m256,74.8c11.3,0 20.4-9.1 20.4-20.4v-23c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v23c2.84217e-14,11.3 9.1,20.4 20.4,20.4z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m256,437.2c-11.3,0-20.4,9.1-20.4,20.4v22.9c0,11.3 9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-22.9c0-11.2-9.1-20.4-20.4-20.4z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m480.6,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h23c11.3,0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m54.4,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h22.9c11.3,0 20.4-9.1 20.4-20.4 0.1-11.3-9.1-20.4-20.3-20.4z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="M400.4,82.8L384.1,99c-8,8-8,20.9,0,28.9s20.9,8,28.9,0l16.2-16.2c8-8,8-20.9,0-28.9S408.3,74.8,400.4,82.8z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m99,384.1l-16.2,16.2c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l16.2-16.2c8-8 8-20.9 0-28.9s-20.9-7.9-28.9,0z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m413,384.1c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m99,127.9c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<TextBlock Name="RegularModeButton"
|
||||
Padding="3"
|
||||
/>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="EveningModeRadioButton"
|
||||
Tag="20"
|
||||
GroupName="Mode"
|
||||
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
|
||||
Style="{StaticResource ButtonRadioButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Height="16" Width="16">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m444.8,256l50.2-50.2c8-8 8-20.9 0-28.9-8-8-20.9-8-28.9,0l-58.7,58.7h-85c-1.3-4.2-3-8.3-5-12.1l60.1-60.1h83c11.3,
|
||||
0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-71v-71c0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4v83l-60.1,60.1c-3.8-2-7.9-3.7-12.1-5v-85l58.7-58.7c8-8 8-20.9
|
||||
0-28.9-8-8-20.9-8-28.9,0l-50.3,50.1-50.2-50.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l58.7,58.7v85c-4.2,1.3-8.3,
|
||||
3-12.1,5l-60.1-60.1v-83c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v71h-71c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,
|
||||
20.4h83l60.1,60.1c-2,3.8-3.7,7.9-5,12.1h-85l-58.7-58.7c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l50.1,50.3-50.2,50.2c-8,8-8,20.9 0,28.9
|
||||
8,8 20.9,8 28.9,0l58.7-58.7h85c1.3,4.2 3,8.3 5,12.1l-60.1,60.1h-83c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h71v71c0,11.3
|
||||
9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-83l60.1-60.1c3.8,2 7.9,3.7 12.1,5v85l-58.7,58.7c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l50.2-50.2
|
||||
50.2,50.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-58.7-58.7v-85c4.2-1.3 8.3-3 12.1-5l60.1,60.1v83c0,11.3 9.1,20.4 20.4,20.4s20.4-9.1 20.4-20.4v-71h71c11.3,0
|
||||
20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-83l-60.1-60.1c2-3.8 3.7-7.9 5-12.1h85l58.7,58.7c8,8 20.9,8 28.9,0 8-8 8-20.9
|
||||
0-28.9l-50-50.2zm-217.3,0c0-15.7 12.8-28.5 28.5-28.5s28.5,12.8 28.5,28.5-12.8,28.5-28.5,28.5-28.5-12.8-28.5-28.5z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<TextBlock Name="WinterModeButton"
|
||||
Padding="3"
|
||||
/>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="NightModeRadioButton"
|
||||
Tag="40"
|
||||
GroupName="Mode"
|
||||
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
|
||||
Style="{StaticResource ButtonRadioButtonStyle}"
|
||||
>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Height="16" Width="16">
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m500,113.1c-2.4-7.5-8.9-13-16.8-14.1l-55.2-7.9-24.6-48.9c-3.5-7-10.7-11.4-18.5-11.4-7.8,0-15,4.4-18.5,11.4l-24.6,
|
||||
48.9-55.2,7.9c-7.8,1.1-14.3,6.6-16.8,14.1-2.4,7.5-0.3,15.8 5.4,21.3l39.7,37.9-9.4,53.4c-1.4,7.7 1.8,15.6 8.1,20.2 6.3,4.7 14.7,5.3 21.7,1.7l49.5-25.5
|
||||
49.5,25.5c3,1.5 6.2,2.3 9.5,2.3 4.3,0 8.6-1.4 12.2-4 6.3-4.6 9.5-12.5 8.1-20.2l-9.4-53.4 39.7-37.9c5.9-5.5 8-13.8 5.6-21.3zm-81.6,36.9c-5,4.8-7.3,
|
||||
11.7-6.1,18.5l4.1,23.3-22-11.3c-5.9-3-13-3-18.9,0l-22,11.3 4.1-23.3c1.2-6.8-1.1-13.7-6.1-18.5l-16.9-16.2 23.8-3.4c6.7-1 12.5-5.1 15.5-11.2l11-21.9
|
||||
11,21.9c3,6 8.8,10.2 15.5,11.2l23.8,3.4-16.8,16.2z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m442,361c-14.9,3.4-30.3,5.1-45.7,5.1-113.8,0-206.4-92.6-206.4-206.3 0-41.8 12.4-82 35.9-116.3
|
||||
4.8-7 4.8-16.3 0-23.4-4.8-7.1-13.4-10.5-21.8-8.6-54,12.2-103,42.7-138,86-35.4,43.8-55,99.2-55,155.7 0,66.2 25.8,128.4 72.6,175.2 46.8,46.8
|
||||
109.1,72.6 175.3,72.6 81.9,0 158.4-40.4 204.8-108.1 4.8-7 4.8-16.3 0-23.4-4.8-7-13.4-10.4-21.7-8.5zm-183.1,98.5c-113.8,0-206.4-92.6-206.4-206.3
|
||||
0-78.2 45.3-149.1 112.8-183.8-11.2,28.6-17,59.1-17, 90.4 0,66.2 25.8,128.4 72.6,175.2 46.7,46.7 108.8,72.5 174.9,72.6-37.3,33.1-85.8,51.9-136.9,51.9z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<TextBlock Name="NightModeButton"
|
||||
Padding="3"
|
||||
/>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,97 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.Extensions;
|
||||
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public class DetectionClassChangedEventArgs(DetectionClass detectionClass, int classNumber) : EventArgs
|
||||
{
|
||||
public DetectionClass DetectionClass { get; } = detectionClass;
|
||||
public int ClassNumber { get; } = classNumber;
|
||||
}
|
||||
|
||||
public partial class DetectionClasses
|
||||
{
|
||||
public event EventHandler<DetectionClassChangedEventArgs>? DetectionClassChanged;
|
||||
private const int CaptionedMinWidth = 230;
|
||||
ObservableCollection<DetectionClass> _detectionClasses = new();
|
||||
|
||||
public DetectionClasses()
|
||||
{
|
||||
InitializeComponent();
|
||||
SizeChanged += (sender, args) =>
|
||||
{
|
||||
if (args.NewSize.Width < CaptionedMinWidth)
|
||||
{
|
||||
RegularModeButton.Text = "";
|
||||
WinterModeButton.Text = "";
|
||||
NightModeButton.Text = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
RegularModeButton.Text = Constants.REGULAR_MODE_CAPTION;
|
||||
WinterModeButton.Text= Constants.WINTER_MODE_CAPTION;
|
||||
NightModeButton.Text= Constants.NIGHT_MODE_CAPTION;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void Init(List<DetectionClass> detectionClasses)
|
||||
{
|
||||
foreach (var dClass in detectionClasses)
|
||||
{
|
||||
var cl = (DetectionClass)dClass.Clone();
|
||||
cl.Color = cl.Color.ToConfidenceColor();
|
||||
_detectionClasses.Add(cl);
|
||||
}
|
||||
|
||||
DetectionDataGrid.ItemsSource = _detectionClasses;
|
||||
DetectionDataGrid.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
public int CurrentClassNumber { get; private set; } = 0;
|
||||
public DetectionClass? CurrentDetectionClass { get; set; }
|
||||
|
||||
private void DetectionDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
|
||||
RaiseDetectionClassChanged();
|
||||
|
||||
private void ModeRadioButton_Checked(object sender, RoutedEventArgs e) =>
|
||||
RaiseDetectionClassChanged();
|
||||
|
||||
private void RaiseDetectionClassChanged()
|
||||
{
|
||||
var detClass = (DetectionClass)DetectionDataGrid.SelectedItem;
|
||||
if (detClass == null)
|
||||
return;
|
||||
|
||||
var modeAmplifier = 0;
|
||||
foreach (var child in ModeSwitcherPanel.Children)
|
||||
if (child is RadioButton { IsChecked: true } rb)
|
||||
if (int.TryParse(rb.Tag?.ToString(), out int modeIndex))
|
||||
{
|
||||
detClass.PhotoMode = (PhotoMode)modeIndex;
|
||||
modeAmplifier += modeIndex;
|
||||
}
|
||||
|
||||
CurrentDetectionClass = detClass;
|
||||
CurrentClassNumber = detClass.Id + modeAmplifier;
|
||||
|
||||
DetectionClassChanged?.Invoke(this, new DetectionClassChangedEventArgs(detClass, CurrentClassNumber));
|
||||
}
|
||||
|
||||
|
||||
public void SelectNum(int keyNumber)
|
||||
{
|
||||
DetectionDataGrid.SelectedIndex = keyNumber;
|
||||
}
|
||||
|
||||
private void OnKeyBanActivity(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp))
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.Extensions;
|
||||
using Annotation = Azaion.Common.Database.Annotation;
|
||||
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public class DetectionControl : Border
|
||||
{
|
||||
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
|
||||
private const double RESIZE_RECT_SIZE = 10;
|
||||
|
||||
private readonly Grid _grid;
|
||||
private readonly DetectionLabelPanel _detectionLabelPanel;
|
||||
public readonly Canvas DetectionLabelContainer;
|
||||
|
||||
public TimeSpan Time { get; set; }
|
||||
private readonly List<Rectangle> _resizedRectangles = new();
|
||||
|
||||
private DetectionClass _detectionClass = null!;
|
||||
public DetectionClass DetectionClass
|
||||
{
|
||||
get => _detectionClass;
|
||||
set
|
||||
{
|
||||
var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
|
||||
BorderBrush = brush;
|
||||
BorderThickness = new Thickness(1);
|
||||
foreach (var rect in _resizedRectangles)
|
||||
rect.Stroke = brush;
|
||||
|
||||
_detectionLabelPanel.DetectionClass = value;
|
||||
_detectionClass = value;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Rectangle _selectionFrame;
|
||||
|
||||
private bool _isSelected;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set
|
||||
{
|
||||
_selectionFrame.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
|
||||
_isSelected = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateAdornerScale(double scale)
|
||||
{
|
||||
if (Math.Abs(scale) < 0.0001)
|
||||
return;
|
||||
|
||||
var inverseScale = 1.0 / scale;
|
||||
BorderThickness = new Thickness(4 * inverseScale);
|
||||
foreach (var rect in _resizedRectangles)
|
||||
{
|
||||
rect.Width = 2 * RESIZE_RECT_SIZE * inverseScale;
|
||||
rect.Height = 2 * RESIZE_RECT_SIZE * inverseScale;;
|
||||
rect.Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
public (HorizontalAlignment Horizontal, VerticalAlignment Vertical) DetectionLabelPosition
|
||||
{
|
||||
get => (DetectionLabelContainer.HorizontalAlignment, DetectionLabelContainer.VerticalAlignment);
|
||||
set
|
||||
{
|
||||
DetectionLabelContainer.HorizontalAlignment = value.Horizontal;
|
||||
DetectionLabelContainer.VerticalAlignment = value.Vertical;
|
||||
}
|
||||
}
|
||||
|
||||
public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action<object,
|
||||
MouseButtonEventArgs> resizeStart, CanvasLabel canvasLabel)
|
||||
{
|
||||
Width = canvasLabel.Width;
|
||||
Height = canvasLabel.Height;
|
||||
Time = time;
|
||||
_resizeStart = resizeStart;
|
||||
|
||||
DetectionLabelContainer = new Canvas
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
ClipToBounds = false,
|
||||
};
|
||||
_detectionLabelPanel = new DetectionLabelPanel
|
||||
{
|
||||
Confidence = canvasLabel.Confidence,
|
||||
DetectionClass = detectionClass
|
||||
};
|
||||
|
||||
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
|
||||
|
||||
_selectionFrame = new Rectangle
|
||||
{
|
||||
Margin = new Thickness(-3),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
Stroke = new SolidColorBrush(Colors.Black),
|
||||
StrokeThickness = 2,
|
||||
Visibility = Visibility.Collapsed
|
||||
};
|
||||
_resizedRectangles =
|
||||
[
|
||||
CreateResizeRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
|
||||
CreateResizeRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
|
||||
CreateResizeRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
|
||||
CreateResizeRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
|
||||
CreateResizeRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
|
||||
CreateResizeRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
|
||||
CreateResizeRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
|
||||
CreateResizeRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
|
||||
];
|
||||
_grid = new Grid
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
Children = { _selectionFrame }
|
||||
};
|
||||
_grid.Children.Add(DetectionLabelContainer);
|
||||
foreach (var rect in _resizedRectangles)
|
||||
_grid.Children.Add(rect);
|
||||
|
||||
Child = _grid;
|
||||
Cursor = Cursors.SizeAll;
|
||||
DetectionClass = detectionClass;
|
||||
}
|
||||
|
||||
//small corners
|
||||
private Rectangle CreateResizeRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
|
||||
{
|
||||
var rect = new Rectangle() // small rectangles at the corners and sides
|
||||
{
|
||||
ClipToBounds = false,
|
||||
Margin = new Thickness(-1.1 * RESIZE_RECT_SIZE),
|
||||
HorizontalAlignment = ha,
|
||||
VerticalAlignment = va,
|
||||
Width = RESIZE_RECT_SIZE,
|
||||
Height = RESIZE_RECT_SIZE,
|
||||
Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
|
||||
StrokeThickness = 0.8,
|
||||
Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
|
||||
Cursor = crs,
|
||||
Name = name,
|
||||
};
|
||||
rect.MouseDown += (sender, args) => _resizeStart(sender, args);
|
||||
rect.MouseUp += (sender, args) => { (sender as UIElement)?.ReleaseMouseCapture(); };
|
||||
return rect;
|
||||
}
|
||||
|
||||
public CanvasLabel ToCanvasLabel() =>
|
||||
new(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
||||
|
||||
public YoloLabel ToYoloLabel(Size canvasSize, Size? videoSize = null) =>
|
||||
new(ToCanvasLabel(), canvasSize, videoSize);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<UserControl x:Class="Azaion.Common.Controls.DetectionLabelPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- Friendly (Light Blue Square) -->
|
||||
<DrawingImage x:Key="Friendly">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightBlue" Geometry="M25,50 l150,0 0,100 -150,0 z">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="Black" Thickness="8"/>
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
|
||||
<!-- Hostile (Red Diamond) -->
|
||||
<DrawingImage x:Key="Hostile">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="Red" Geometry="M 100,28 L172,100 100,172 28,100 100,28 Z">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="Black" Thickness="8"/>
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
|
||||
<!-- Unknown (Yellow Quatrefoil) -->
|
||||
<DrawingImage x:Key="Unknown">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="Yellow" Geometry="M63,63 C63,20 137,20 137,63 C180,63 180,137 137,137 C137,180
|
||||
63,180 63,137 C20,137 20,63 63,63 Z">
|
||||
<GeometryDrawing.Pen>
|
||||
<Pen Brush="Black" Thickness="8"/>
|
||||
</GeometryDrawing.Pen>
|
||||
</GeometryDrawing>
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid x:Name="DetectionGrid">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2"></ColumnDefinition>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" x:Name="AffiliationImage">
|
||||
</Image>
|
||||
<Label Grid.Column="1" x:Name="DetectionClassName" FontSize="16"></Label>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Windows.Media;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.Extensions;
|
||||
|
||||
namespace Azaion.Common.Controls
|
||||
{
|
||||
public partial class DetectionLabelPanel
|
||||
{
|
||||
private AffiliationEnum _affiliation = AffiliationEnum.None;
|
||||
|
||||
public AffiliationEnum Affiliation
|
||||
{
|
||||
get => _affiliation;
|
||||
set
|
||||
{
|
||||
_affiliation = value;
|
||||
UpdateAffiliationImage();
|
||||
}
|
||||
}
|
||||
|
||||
private DetectionClass _detectionClass = new();
|
||||
public DetectionClass DetectionClass {
|
||||
get => _detectionClass;
|
||||
set
|
||||
{
|
||||
_detectionClass = value;
|
||||
SetClassName();
|
||||
}
|
||||
}
|
||||
|
||||
private double _confidence;
|
||||
public double Confidence
|
||||
{
|
||||
get => _confidence;
|
||||
set
|
||||
{
|
||||
_confidence = value;
|
||||
SetClassName();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetClassName()
|
||||
{
|
||||
DetectionClassName.Content = _confidence >= 0.995 ? _detectionClass.UIName : $"{_detectionClass.UIName}: {_confidence * 100:F0}%";
|
||||
DetectionGrid.Background = new SolidColorBrush(_detectionClass.Color.ToConfidenceColor(_confidence));
|
||||
}
|
||||
|
||||
public DetectionLabelPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private string _detectionLabelText(string detectionClassName) =>
|
||||
_confidence >= 0.98 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%";
|
||||
|
||||
private void UpdateAffiliationImage()
|
||||
{
|
||||
if (_affiliation == AffiliationEnum.None)
|
||||
{
|
||||
AffiliationImage.Source = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryFindResource(_affiliation.ToString()) is DrawingImage drawingImage)
|
||||
AffiliationImage.Source = drawingImage;
|
||||
else
|
||||
AffiliationImage.Source = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<UserControl x:Class="Azaion.Common.Controls.NumericUpDown"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Azaion.Common.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<Grid Background="DimGray">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="24" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="12" />
|
||||
<RowDefinition Height="12" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox Name="NudTextBox"
|
||||
Background="DimGray"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
TextAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{Binding Value, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}"
|
||||
LostFocus="NudTextBox_OnLostFocus"
|
||||
PreviewTextInput="NudTextBox_OnPreviewTextInput"
|
||||
DataObject.Pasting="NudTextBox_OnPasting"
|
||||
/>
|
||||
<RepeatButton
|
||||
Name="NudButtonUp"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
FontSize="10"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
Click="NudButtonUp_OnClick"
|
||||
>^</RepeatButton>
|
||||
<RepeatButton
|
||||
Name="NudButtonDown"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
FontSize="10"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
Click="NudButtonDown_OnClick"
|
||||
>˅</RepeatButton>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,126 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public partial class NumericUpDown : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register(
|
||||
nameof(MinValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(0m));
|
||||
|
||||
public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register(
|
||||
nameof(MaxValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(100m));
|
||||
|
||||
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
|
||||
nameof(Value), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(10m, OnValueChanged));
|
||||
|
||||
public static readonly DependencyProperty StepProperty = DependencyProperty.Register(
|
||||
nameof(Step), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(1m));
|
||||
|
||||
public decimal MinValue
|
||||
{
|
||||
get => (decimal)GetValue(MinValueProperty);
|
||||
set => SetValue(MinValueProperty, value);
|
||||
}
|
||||
|
||||
public decimal MaxValue
|
||||
{
|
||||
get => (decimal)GetValue(MaxValueProperty);
|
||||
set => SetValue(MaxValueProperty, value);
|
||||
}
|
||||
|
||||
public decimal Value
|
||||
{
|
||||
get => (decimal)GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public decimal Step
|
||||
{
|
||||
get => (decimal)GetValue(StepProperty);
|
||||
set => SetValue(StepProperty, value);
|
||||
}
|
||||
|
||||
public NumericUpDown()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not NumericUpDown control)
|
||||
return;
|
||||
|
||||
control.NudTextBox.Text = ((decimal)e.NewValue).ToString(CultureInfo.InvariantCulture);
|
||||
control.NudTextBox.SelectionStart = control.NudTextBox.Text.Length;
|
||||
}
|
||||
|
||||
private void NudButtonUp_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var step = Step <= 0 ? 1m : Step;
|
||||
var newVal = Math.Min(MaxValue, Value + step);
|
||||
Value = newVal;
|
||||
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||
NudTextBox.SelectionStart = NudTextBox.Text.Length;
|
||||
}
|
||||
|
||||
private void NudButtonDown_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var step = Step <= 0 ? 1m : Step;
|
||||
var newVal = Math.Max(MinValue, Value - step);
|
||||
Value = newVal;
|
||||
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||
NudTextBox.SelectionStart = NudTextBox.Text.Length;
|
||||
}
|
||||
|
||||
private void NudTextBox_OnLostFocus(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrEmpty(NudTextBox.Text) || !decimal.TryParse(NudTextBox.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
|
||||
{
|
||||
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||
return;
|
||||
}
|
||||
if (number > MaxValue )
|
||||
{
|
||||
Value = MaxValue;
|
||||
NudTextBox.Text = MaxValue.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (number < MinValue)
|
||||
{
|
||||
Value = MinValue;
|
||||
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = number;
|
||||
}
|
||||
|
||||
NudTextBox.SelectionStart = NudTextBox.Text.Length;
|
||||
}
|
||||
|
||||
private void NudTextBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||
{
|
||||
var regex = new Regex("[^0-9.]+");
|
||||
e.Handled = regex.IsMatch(e.Text);
|
||||
}
|
||||
|
||||
private void NudTextBox_OnPasting(object sender, DataObjectPastingEventArgs e)
|
||||
{
|
||||
if (e.DataObject.GetDataPresent(typeof(string)))
|
||||
{
|
||||
var text = (string)e.DataObject.GetData(typeof(string));
|
||||
var regex = new Regex("[^0-9.]+");
|
||||
if (regex.IsMatch(text))
|
||||
{
|
||||
e.CancelCommand();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
e.CancelCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Azaion.Annotator.Controls
|
||||
{
|
||||
public class UpdatableProgressBar : ProgressBar
|
||||
{
|
||||
public delegate void ValueChange(double oldValue, double newValue);
|
||||
|
||||
public new event ValueChange? ValueChanged;
|
||||
|
||||
public UpdatableProgressBar() : base()
|
||||
{
|
||||
MouseDown += OnMouseDown;
|
||||
MouseMove += OnMouseMove;
|
||||
}
|
||||
|
||||
private double SetProgressBarValue(double mousePos)
|
||||
{
|
||||
Value = Minimum;
|
||||
var pbValue = mousePos / ActualWidth * Maximum;
|
||||
ValueChanged?.Invoke(Value, pbValue);
|
||||
return pbValue;
|
||||
}
|
||||
|
||||
private void OnMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Value = SetProgressBarValue(e.GetPosition(this).X);
|
||||
}
|
||||
|
||||
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
Value = SetProgressBarValue(e.GetPosition(this).X);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using MediatR;
|
||||
using MessagePack;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public enum AIAvailabilityEnum
|
||||
{
|
||||
None = 0,
|
||||
Downloading = 10,
|
||||
Converting = 20,
|
||||
Uploading = 30,
|
||||
Enabled = 200,
|
||||
Warning = 300,
|
||||
Error = 500
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class AIAvailabilityStatusEvent : INotification
|
||||
{
|
||||
[Key("s")] public AIAvailabilityEnum Status { get; set; }
|
||||
[Key("m")] public string? ErrorMessage { get; set; }
|
||||
|
||||
public override string ToString() => $"{StatusMessageDict.GetValueOrDefault(Status, "Помилка")} {ErrorMessage}";
|
||||
|
||||
private static readonly Dictionary<AIAvailabilityEnum, string> StatusMessageDict = new()
|
||||
{
|
||||
{ AIAvailabilityEnum.Downloading, "Йде завантаження AI для Вашої відеокарти" },
|
||||
{ AIAvailabilityEnum.Converting, "Йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" },
|
||||
{ AIAvailabilityEnum.Uploading, "Йде зберігання AI" },
|
||||
{ AIAvailabilityEnum.Enabled, "AI готовий для розпізнавання" },
|
||||
{ AIAvailabilityEnum.Warning, "Неможливо запустити AI наразі, йде налаштування під Ваше залізо" },
|
||||
{ AIAvailabilityEnum.Error, "Помилка під час налаштування AI" }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System.Windows.Media;
|
||||
using Azaion.Common.Database;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
// public class AnnotationResult
|
||||
//{
|
||||
//public Annotation Annotation { get; set; }
|
||||
|
||||
|
||||
//public string ImagePath { get; set; }
|
||||
//public string TimeStr { get; set; }
|
||||
|
||||
//public List<(Color Color, double Confidence)> Colors { get; private set; }
|
||||
// public string ClassName { get; set; }
|
||||
|
||||
// public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
|
||||
// {
|
||||
|
||||
//Annotation = annotation;
|
||||
|
||||
//TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
|
||||
//ImagePath = annotation.ImagePath;
|
||||
|
||||
// var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||
// ClassName = detectionClasses.Count > 1
|
||||
// ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
|
||||
// : allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
|
||||
//
|
||||
// Colors = annotation.Detections
|
||||
// .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
|
||||
// .ToList();
|
||||
|
||||
// }
|
||||
// }
|
||||
@@ -1,49 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.Extensions;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class AnnotationThumbnail(Annotation annotation, bool isValidator, string imagePath, string thumbPath) : INotifyPropertyChanged
|
||||
{
|
||||
public Annotation Annotation { get; set; } = annotation;
|
||||
public bool IsValidator { get; set; } = isValidator;
|
||||
private readonly string _imagePath = imagePath;
|
||||
private readonly string _thumbPath = thumbPath;
|
||||
|
||||
private BitmapImage? _thumbnail;
|
||||
public BitmapImage? Thumbnail
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_thumbnail == null)
|
||||
{
|
||||
Task.Run(async () => Thumbnail = await _thumbPath.OpenImage());
|
||||
}
|
||||
return _thumbnail;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_thumbnail = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ImageName => Path.GetFileName(_imagePath);
|
||||
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
|
||||
public string CreatedEmail => Annotation.CreatedEmail;
|
||||
public bool IsSeed => IsValidator &&
|
||||
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
|
||||
!Annotation.CreatedRole.IsValidator();
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public void UpdateUI() => OnPropertyChanged(nameof(IsSeed));
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using CommandLine;
|
||||
using MessagePack;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
[MessagePackObject]
|
||||
[Verb("credsManual", HelpText = "Manual Credentials")]
|
||||
public class ApiCredentials
|
||||
{
|
||||
[Key(nameof(Email))]
|
||||
[Option('e', "email", Required = true, HelpText = "User Email")]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
[Key(nameof(Password))]
|
||||
[Option('p', "pass", Required = true, HelpText = "User Password")]
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
||||
|
||||
[Verb("credsEncrypted", isDefault: true, HelpText = "Encrypted Credentials")]
|
||||
public class ApiCredentialsEncrypted
|
||||
{
|
||||
[Option('c', "creds", Group = "auto", HelpText = "Encrypted Creds")]
|
||||
public string Creds { get; set; } = null!;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class BusinessExceptionDto
|
||||
{
|
||||
public int ErrorCode { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class ClusterDistribution
|
||||
{
|
||||
public string Label { get; set; } = "";
|
||||
public Color Color { get; set; }
|
||||
public int ClassCount { get; set; }
|
||||
public double BarWidth { get; set; }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
[MessagePackObject]
|
||||
public class AIRecognitionConfig
|
||||
{
|
||||
[Key("f_pr")] public int FramePeriodRecognition { get; set; }
|
||||
[Key("f_rs")] public double FrameRecognitionSeconds { get; set; }
|
||||
[Key("pt")] public double ProbabilityThreshold { get; set; }
|
||||
|
||||
[Key("t_dc")] public double TrackingDistanceConfidence { get; set; }
|
||||
[Key("t_pi")] public double TrackingProbabilityIncrease { get; set; }
|
||||
[Key("t_it")] public double TrackingIntersectionThreshold { get; set; }
|
||||
|
||||
[Key("d")] public byte[] Data { get; set; } = null!;
|
||||
[Key("p")] public List<string> Paths { get; set; } = null!;
|
||||
[Key("m_bs")] public int ModelBatchSize { get; set; } = 4;
|
||||
|
||||
[Key("ov_p")] public double BigImageTileOverlapPercent { get; set; }
|
||||
|
||||
[Key("cam_a")] public double Altitude { get; set; }
|
||||
[Key("cam_fl")] public double CameraFocalLength { get; set; }
|
||||
[Key("cam_sw")] public double CameraSensorWidth { get; set; }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class AnnotationConfig
|
||||
{
|
||||
public List<DetectionClass> DetectionClasses { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
private Dictionary<int, DetectionClass>? _detectionClassesDict;
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<int, DetectionClass> DetectionClassesDict
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_detectionClassesDict != null)
|
||||
return _detectionClassesDict;
|
||||
|
||||
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
|
||||
|
||||
_detectionClassesDict = DetectionClasses.SelectMany(cls => photoModes.Select(mode => new DetectionClass
|
||||
{
|
||||
Id = cls.Id,
|
||||
Name = cls.Name,
|
||||
Color = cls.Color,
|
||||
ShortName = cls.ShortName,
|
||||
PhotoMode = mode
|
||||
}))
|
||||
.ToDictionary(x => x.YoloId, x => x);
|
||||
return _detectionClassesDict;
|
||||
}
|
||||
}
|
||||
|
||||
public string AnnotationsDbFile { get; set; } = null!;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.Common.Services;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class AppConfig
|
||||
{
|
||||
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
|
||||
|
||||
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||
|
||||
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||
|
||||
public QueueConfig QueueConfig { get; set; } = null!;
|
||||
|
||||
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||
|
||||
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
||||
|
||||
public UIConfig UIConfig { get; set; } = null!;
|
||||
|
||||
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
||||
|
||||
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
||||
|
||||
public MapConfig MapConfig{ get; set; } = null!;
|
||||
|
||||
public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!;
|
||||
|
||||
public CameraConfig CameraConfig { get; set; } = null!;
|
||||
}
|
||||
|
||||
public interface IConfigUpdater
|
||||
{
|
||||
void CheckConfig();
|
||||
void Save(AppConfig config);
|
||||
}
|
||||
|
||||
public class ConfigUpdater : IConfigUpdater
|
||||
{
|
||||
private readonly IConfigurationStore _configStore;
|
||||
|
||||
public ConfigUpdater(IConfigurationStore configStore)
|
||||
{
|
||||
_configStore = configStore;
|
||||
}
|
||||
|
||||
public ConfigUpdater() : this(new FileConfigurationStore(Constants.CONFIG_PATH, new PhysicalFileSystem()))
|
||||
{
|
||||
}
|
||||
|
||||
public void CheckConfig()
|
||||
{
|
||||
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
|
||||
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
|
||||
|
||||
if (File.Exists(configFilePath))
|
||||
return;
|
||||
|
||||
Save(Constants.FailsafeAppConfig);
|
||||
}
|
||||
|
||||
public void Save(AppConfig config)
|
||||
{
|
||||
_ = _configStore.SaveAsync(config);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class CameraConfig
|
||||
{
|
||||
public decimal Altitude { get; set; }
|
||||
public decimal CameraFocalLength { get; set; }
|
||||
public decimal CameraSensorWidth { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class GpsDeniedConfig
|
||||
{
|
||||
public int MinKeyPoints { get; set; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class MapConfig
|
||||
{
|
||||
public string Service { get; set; } = null!;
|
||||
public string ApiKey { get; set; } = null!;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class QueueConfig
|
||||
{
|
||||
public string Host { get; set; } = null!;
|
||||
public int Port { get; set; }
|
||||
|
||||
public string ProducerUsername { get; set; } = null!;
|
||||
public string ProducerPassword { get; set; } = null!;
|
||||
|
||||
public string ConsumerUsername { get; set; } = null!;
|
||||
public string ConsumerPassword { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class ThumbnailConfig
|
||||
{
|
||||
public Size Size { get; set; }
|
||||
public int Border { get; set; }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class UIConfig
|
||||
{
|
||||
public double LeftPanelWidth { get; set; }
|
||||
public double RightPanelWidth { get; set; }
|
||||
public bool GenerateAnnotatedImage { get; set; }
|
||||
public bool SilentDetection { get; set; }
|
||||
public bool ShowDatasetWithDetectionsOnly { get; set; }
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class GeoPoint
|
||||
{
|
||||
const double PRECISION_TOLERANCE = 0.00005;
|
||||
public double Lat { get; }
|
||||
public double Lon { get; }
|
||||
|
||||
public GeoPoint() { }
|
||||
|
||||
public GeoPoint(double lat, double lon)
|
||||
{
|
||||
Lat = lat;
|
||||
Lon = lon;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Lat:F4}, {Lon:F4}";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not GeoPoint point) return false;
|
||||
return ReferenceEquals(this, obj) || Equals(point);
|
||||
}
|
||||
|
||||
private bool Equals(GeoPoint point) =>
|
||||
Math.Abs(Lat - point.Lat) < PRECISION_TOLERANCE && Math.Abs(Lon - point.Lon) < PRECISION_TOLERANCE;
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Lat, Lon);
|
||||
|
||||
public static bool operator ==(GeoPoint left, GeoPoint right) => Equals(left, right);
|
||||
public static bool operator !=(GeoPoint left, GeoPoint right) => !Equals(left, right);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Windows.Media;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class DetectionClass : ICloneable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
public string ShortName { get; set; } = null!;
|
||||
|
||||
public Color Color { get; set; }
|
||||
|
||||
public int MaxSizeM { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string UIName
|
||||
{
|
||||
get
|
||||
{
|
||||
var mode = PhotoMode switch
|
||||
{
|
||||
PhotoMode.Night => "(ніч)",
|
||||
PhotoMode.Winter => "(зим)",
|
||||
PhotoMode.Regular => "",
|
||||
_ => ""
|
||||
};
|
||||
return ShortName + mode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public PhotoMode PhotoMode { get; set; }
|
||||
|
||||
[JsonIgnore] //For UI
|
||||
public int ClassNumber => Id + 1;
|
||||
|
||||
[JsonIgnore]
|
||||
public int YoloId => Id == -1 ? Id : (int)PhotoMode + Id;
|
||||
|
||||
[JsonIgnore]
|
||||
public SolidColorBrush ColorBrush => new(Color);
|
||||
|
||||
public static DetectionClass FromYoloId(int yoloId, List<DetectionClass> detectionClasses)
|
||||
{
|
||||
var cls = yoloId % 20;
|
||||
var photoMode = (PhotoMode)(yoloId - cls);
|
||||
var detClass = detectionClasses[cls];
|
||||
detClass.PhotoMode = photoMode;
|
||||
return detClass;
|
||||
}
|
||||
|
||||
public object Clone() => MemberwiseClone();
|
||||
}
|
||||
|
||||
public enum PhotoMode
|
||||
{
|
||||
Regular = 0,
|
||||
Winter = 20,
|
||||
Night = 40
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class Direction
|
||||
{
|
||||
public double Distance { get; set; }
|
||||
public double Azimuth { get; set; }
|
||||
|
||||
public Direction() { }
|
||||
|
||||
public Direction(double distance, double azimuth)
|
||||
{
|
||||
Distance = distance;
|
||||
Azimuth = azimuth;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Distance:F2}, {Azimuth:F1} deg";
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class DirectoriesConfig
|
||||
{
|
||||
public string? ApiResourcesDirectory { get; set; } = null!;
|
||||
|
||||
public string VideosDirectory { get; set; } = null!;
|
||||
public string LabelsDirectory { get; set; } = null!;
|
||||
public string ImagesDirectory { get; set; } = null!;
|
||||
public string ResultsDirectory { get; set; } = null!;
|
||||
public string ThumbnailsDirectory { get; set; } = null!;
|
||||
|
||||
public string GpsSatDirectory { get; set; } = null!;
|
||||
public string GpsRouteDirectory { get; set; } = null!;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class DownloadTilesResult
|
||||
{
|
||||
public ConcurrentDictionary<(int x, int y), byte[]> Tiles { get; set; } = null!;
|
||||
public double LatMin { get; set; }
|
||||
public double LatMax { get; set; }
|
||||
public double LonMin { get; set; }
|
||||
public double LonMax { get; set; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public abstract class ExternalClientConfig
|
||||
{
|
||||
public string ZeroMqHost { get; set; } = "";
|
||||
public int ZeroMqPort { get; set; }
|
||||
}
|
||||
|
||||
public class LoaderClientConfig : ExternalClientConfig
|
||||
{
|
||||
public string ApiUrl { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class InferenceClientConfig : ExternalClientConfig
|
||||
{
|
||||
public string ApiUrl { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class GpsDeniedClientConfig : ExternalClientConfig
|
||||
{
|
||||
public int ZeroMqReceiverPort { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using Azaion.Common.Database;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class FormState
|
||||
{
|
||||
public MediaFile? CurrentMedia { get; set; }
|
||||
public string CurrentMediaHash => CurrentMedia?.Hash ?? "";
|
||||
public string CurrentMediaName => CurrentMedia?.Name ?? "";
|
||||
|
||||
public Size CurrentMediaSize { get; set; }
|
||||
public TimeSpan CurrentVideoLength { get; set; }
|
||||
|
||||
public TimeSpan? BackgroundTime { get; set; }
|
||||
public int CurrentVolume { get; set; } = 100;
|
||||
public ObservableCollection<Annotation> AnnotationResults { get; set; } = [];
|
||||
public WindowEnum ActiveWindow { get; set; }
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public interface IAzaionModule
|
||||
{
|
||||
string Name { get; }
|
||||
string SvgIcon { get; }
|
||||
Type MainWindowType { get; }
|
||||
WindowEnum WindowEnum { get; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static bool In<T>(this T obj, params T[] objects) =>
|
||||
objects.Contains(obj);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Azaion.Common.DTO.Config;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class InitConfig
|
||||
{
|
||||
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
|
||||
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||
public CameraConfig CameraConfig { get; set; } = null!;
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
[MessagePackObject]
|
||||
public abstract class Label
|
||||
{
|
||||
[JsonProperty(PropertyName = "cl")][Key("c")] public int ClassNumber { get; set; }
|
||||
|
||||
protected Label() { }
|
||||
|
||||
protected Label(int classNumber)
|
||||
{
|
||||
ClassNumber = classNumber;
|
||||
}
|
||||
}
|
||||
|
||||
public class CanvasLabel : Label
|
||||
{
|
||||
public double Left { get; set; }
|
||||
public double Top { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double Confidence { get; set; }
|
||||
|
||||
public double Bottom
|
||||
{
|
||||
get => Top + Height;
|
||||
set => Height = value - Top;
|
||||
}
|
||||
|
||||
public double Right
|
||||
{
|
||||
get => Left + Width;
|
||||
set => Width = value - Left;
|
||||
}
|
||||
|
||||
public double CenterX => Left + Width / 2.0;
|
||||
public double CenterY => Top + Height / 2.0;
|
||||
public Size Size => new(Width, Height);
|
||||
|
||||
public CanvasLabel() { }
|
||||
|
||||
public CanvasLabel(double left, double right, double top, double bottom)
|
||||
{
|
||||
Left = left;
|
||||
Top = top;
|
||||
Width = right - left;
|
||||
Height = bottom - top;
|
||||
Confidence = 1;
|
||||
ClassNumber = -1;
|
||||
}
|
||||
|
||||
public CanvasLabel(int classNumber, double left, double top, double width, double height, double confidence = 1) : base(classNumber)
|
||||
{
|
||||
Left = left;
|
||||
Top = top;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Confidence = confidence;
|
||||
}
|
||||
|
||||
public CanvasLabel(YoloLabel label, Size canvasSize, Size? mediaSize = null, double confidence = 1)
|
||||
{
|
||||
var cw = canvasSize.Width;
|
||||
var ch = canvasSize.Height;
|
||||
var canvasAr = cw / ch;
|
||||
var videoAr = mediaSize.HasValue
|
||||
? mediaSize.Value.Width / mediaSize.Value.Height
|
||||
: canvasAr;
|
||||
|
||||
ClassNumber = label.ClassNumber;
|
||||
|
||||
var left = label.CenterX - label.Width / 2;
|
||||
var top = label.CenterY - label.Height / 2;
|
||||
|
||||
if (videoAr > canvasAr) //100% width
|
||||
{
|
||||
var realHeight = cw / videoAr; //real video height in pixels on canvas
|
||||
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
|
||||
|
||||
Left = left * cw;
|
||||
Top = top * realHeight + blackStripHeight;
|
||||
Width = label.Width * cw;
|
||||
Height = label.Height * realHeight;
|
||||
}
|
||||
else //100% height
|
||||
{
|
||||
var realWidth = ch * videoAr; //real video width in pixels on canvas
|
||||
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
|
||||
|
||||
Left = left * realWidth + blackStripWidth;
|
||||
Top = top * ch;
|
||||
Width = label.Width * realWidth;
|
||||
Height = label.Height * ch;
|
||||
}
|
||||
Confidence = confidence;
|
||||
}
|
||||
|
||||
public CanvasLabel ReframeToSmall(CanvasLabel smallTile) =>
|
||||
new(ClassNumber, Left - smallTile.Left, Top - smallTile.Top, Width, Height, Confidence);
|
||||
|
||||
public CanvasLabel ReframeFromSmall(CanvasLabel smallTile) =>
|
||||
new(ClassNumber, Left + smallTile.Left, Top + smallTile.Top, Width, Height, Confidence);
|
||||
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class YoloLabel : Label
|
||||
{
|
||||
[JsonProperty(PropertyName = "x")][Key("x")] public double CenterX { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "y")][Key("y")] public double CenterY { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "w")][Key("w")] public double Width { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "h")][Key("h")] public double Height { get; set; }
|
||||
|
||||
public YoloLabel()
|
||||
{
|
||||
}
|
||||
|
||||
public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber)
|
||||
{
|
||||
CenterX = centerX;
|
||||
CenterY = centerY;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
public RectangleF ToRectangle() =>
|
||||
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
|
||||
|
||||
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? mediaSize = null)
|
||||
{
|
||||
var cw = canvasSize.Width;
|
||||
var ch = canvasSize.Height;
|
||||
var canvasAr = cw / ch;
|
||||
var videoAr = mediaSize.HasValue
|
||||
? mediaSize.Value.Width / mediaSize.Value.Height
|
||||
: canvasAr;
|
||||
|
||||
ClassNumber = canvasLabel.ClassNumber;
|
||||
|
||||
double left, top;
|
||||
if (videoAr > canvasAr) //100% width
|
||||
{
|
||||
left = canvasLabel.Left / cw;
|
||||
Width = canvasLabel.Width / cw;
|
||||
var realHeight = cw / videoAr; //real video height in pixels on canvas
|
||||
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
|
||||
top = (canvasLabel.Top - blackStripHeight) / realHeight;
|
||||
Height = canvasLabel.Height / realHeight;
|
||||
}
|
||||
else //100% height
|
||||
{
|
||||
top = canvasLabel.Top / ch;
|
||||
Height = canvasLabel.Height / ch;
|
||||
var realWidth = ch * videoAr; //real video width in pixels on canvas
|
||||
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
|
||||
left = (canvasLabel.Left - blackStripWidth) / realWidth;
|
||||
Width = canvasLabel.Width / realWidth;
|
||||
}
|
||||
|
||||
CenterX = left + Width / 2.0;
|
||||
CenterY = top + Height / 2.0;
|
||||
}
|
||||
|
||||
public static YoloLabel? Parse(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return null;
|
||||
|
||||
var strings = s.Replace(',', '.').Split(' ');
|
||||
if (strings.Length < 5)
|
||||
throw new Exception("Wrong labels format!");
|
||||
if (strings.Length > 5)
|
||||
strings = strings[..5];
|
||||
|
||||
|
||||
var res = new YoloLabel
|
||||
{
|
||||
ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture),
|
||||
CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture),
|
||||
CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture),
|
||||
Width = double.Parse(strings[3], CultureInfo.InvariantCulture),
|
||||
Height = double.Parse(strings[4], CultureInfo.InvariantCulture)
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var str = await File.ReadAllTextAsync(filename, cancellationToken);
|
||||
return Deserialize(str);
|
||||
}
|
||||
|
||||
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var labelsStr = Serialize(labels);
|
||||
await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
|
||||
}
|
||||
|
||||
public static string Serialize(IEnumerable<YoloLabel> labels) =>
|
||||
string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
|
||||
|
||||
public static List<YoloLabel> Deserialize(string str) =>
|
||||
str.Split('\n')
|
||||
.Select(Parse)
|
||||
.Where(ann => ann != null)
|
||||
.ToList()!;
|
||||
|
||||
|
||||
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class LabelInfo
|
||||
{
|
||||
[JsonProperty("c")] public List<int> Classes { get; set; } = null!;
|
||||
|
||||
[JsonProperty("d")] public DateTime ImageDateTime { get; set; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; } = null!;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public enum PlaybackControlEnum
|
||||
{
|
||||
None = 0,
|
||||
Play = 1,
|
||||
Pause = 2,
|
||||
Stop = 3,
|
||||
PreviousFrame = 4,
|
||||
NextFrame = 5,
|
||||
SaveAnnotations = 6,
|
||||
RemoveSelectedAnns = 7,
|
||||
RemoveAllAnns = 8,
|
||||
TurnOffVolume = 9,
|
||||
TurnOnVolume = 10,
|
||||
Previous = 11,
|
||||
Next = 12,
|
||||
Close = 13,
|
||||
ValidateAnnotations = 15
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Azaion.Common.Database;
|
||||
|
||||
namespace Azaion.Common.DTO.Queue;
|
||||
using MessagePack;
|
||||
|
||||
[MessagePackObject]
|
||||
public class AnnotationMessage
|
||||
{
|
||||
[Key(0)] public DateTime CreatedDate { get; set; }
|
||||
[Key(1)] public string Name { get; set; } = null!;
|
||||
[Key(11)] public string MediaHash { get; set; } = null!;
|
||||
[Key(2)] public string OriginalMediaName { get; set; } = null!;
|
||||
[Key(3)] public TimeSpan Time { get; set; }
|
||||
[Key(4)] public string ImageExtension { get; set; } = null!;
|
||||
[Key(5)] public string Detections { get; set; } = null!;
|
||||
[Key(6)] public byte[]? Image { get; set; } = null!;
|
||||
[Key(7)] public RoleEnum Role { get; set; }
|
||||
[Key(8)] public string Email { get; set; } = null!;
|
||||
[Key(9)] public SourceEnum Source { get; set; }
|
||||
[Key(10)] public AnnotationStatus Status { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class AnnotationBulkMessage
|
||||
{
|
||||
[Key(0)] public string[] AnnotationNames { get; set; } = null!;
|
||||
[Key(1)] public AnnotationStatus AnnotationStatus { get; set; }
|
||||
[Key(2)] public string Email { get; set; } = null!;
|
||||
[Key(3)] public DateTime CreatedDate { get; set; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Queue;
|
||||
|
||||
public enum SourceEnum
|
||||
{
|
||||
AI,
|
||||
Manual
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
[MessagePackObject]
|
||||
public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
|
||||
{
|
||||
[Key("CommandType")]
|
||||
public CommandType CommandType { get; set; } = commandType;
|
||||
|
||||
[Key("Data")]
|
||||
public byte[]? Data { get; set; } = data;
|
||||
|
||||
[Key("Message")]
|
||||
public string? Message { get; set; } = message;
|
||||
|
||||
public static RemoteCommand Create(CommandType commandType) =>
|
||||
new(commandType);
|
||||
|
||||
public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
|
||||
new(commandType, MessagePackSerializer.Serialize(data), message);
|
||||
|
||||
public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class LoadFileData(string filename, string? folder = null )
|
||||
{
|
||||
[Key(nameof(Folder))]
|
||||
public string? Folder { get; set; } = folder;
|
||||
|
||||
[Key(nameof(Filename))]
|
||||
public string Filename { get; set; } = filename;
|
||||
}
|
||||
|
||||
|
||||
public enum CommandType
|
||||
{
|
||||
None = 0,
|
||||
Ok = 3,
|
||||
Login = 10,
|
||||
CheckResource = 12,
|
||||
ListRequest = 15,
|
||||
ListFiles = 18,
|
||||
Load = 20,
|
||||
LoadBigSmall = 22,
|
||||
UploadBigSmall = 24,
|
||||
DataBytes = 25,
|
||||
Inference = 30,
|
||||
InferenceData = 35,
|
||||
InferenceStatus = 37,
|
||||
InferenceDone = 38,
|
||||
StopInference = 40,
|
||||
AIAvailabilityCheck = 80,
|
||||
AIAvailabilityResult = 85,
|
||||
Error = 90,
|
||||
Exit = 100,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user