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:
Oleksandr Bezdieniezhnykh
2026-03-25 04:40:03 +02:00
parent e7ea5a8ded
commit 9e7dc290db
367 changed files with 8840 additions and 16583 deletions
+179
View File
@@ -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
```
+49
View File
@@ -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
+71
View File
@@ -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
+64
View File
@@ -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
+38
View File
@@ -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
+53
View File
@@ -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
+62
View File
@@ -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
+122
View File
@@ -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
+22
View File
@@ -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
+9
View File
@@ -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
+281
View File
@@ -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
```
+393
View File
@@ -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.
+127
View File
@@ -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"|`)
+172
View File
@@ -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.
+470
View File
@@ -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)
+311
View File
@@ -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('&lt;script&gt;');
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.
+879
View File
@@ -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
View File
@@ -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
-33
View File
@@ -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"
}
]
}
-41
View File
@@ -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"
}
]
}
-661
View File
@@ -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>
-728
View File
@@ -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;
}
}
-495
View File
@@ -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();
}
}
-55
View File
@@ -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;
}
-10
View File
@@ -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)
)]
-36
View File
@@ -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>
-22
View File
@@ -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();
}
-49
View File
@@ -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>
-342
View File
@@ -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;
}
}
}
-163
View File
@@ -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;
}
-9
View File
@@ -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);
}
}
-24
View File
@@ -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нтер] для збереження і перегляду далі" }
};
}
-55
View File
@@ -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>
-19
View File
@@ -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();
}
}
-16
View File
@@ -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": "Назва класу"
}
}
-33
View File
@@ -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>
-273
View File
@@ -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;
}
}
-551
View File
@@ -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;
}
}
-165
View File
@@ -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;
}
}
}
-50
View File
@@ -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);
}
}
}
-35
View File
@@ -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" }
};
}
-35
View File
@@ -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();
// }
// }
-49
View File
@@ -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));
}
-24
View File
@@ -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;
}
-11
View File
@@ -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!;
}
-70
View File
@@ -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);
}
}
-8
View File
@@ -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; }
}
-7
View File
@@ -1,7 +0,0 @@
namespace Azaion.Common.DTO.Config;
public class MapConfig
{
public string Service { get; set; } = null!;
public string ApiKey { get; set; } = null!;
}
-14
View File
@@ -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; }
}
-10
View File
@@ -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; }
}
-32
View File
@@ -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);
}
-63
View File
@@ -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
}
-17
View File
@@ -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";
}
-15
View File
@@ -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!;
}
-12
View File
@@ -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; }
}
-20
View File
@@ -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; }
}
-9
View File
@@ -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);
}
-12
View File
@@ -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!;
}
-220
View File
@@ -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(',', '.');
}
-10
View File
@@ -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; }
}
-6
View File
@@ -1,6 +0,0 @@
namespace Azaion.Common.DTO;
public class LoginResponse
{
public string Token { get; set; } = null!;
}
-20
View File
@@ -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; }
}
-7
View File
@@ -1,7 +0,0 @@
namespace Azaion.Common.DTO.Queue;
public enum SourceEnum
{
AI,
Manual
}
-58
View File
@@ -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