cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a Vision Agent — screen capture, frame pipeline, annotation,
|
|
3
|
+
visual context, and before/after verification.
|
|
4
|
+
|
|
5
|
+
Usage (server):
|
|
6
|
+
from calc_terminal.vision.session import VisionSessionManager
|
|
7
|
+
from calc_terminal.vision.context import VisionContextEngine
|
|
8
|
+
from calc_terminal.vision.verify import BeforeAfterVerifier
|
|
9
|
+
|
|
10
|
+
Usage (frontend JS calls /api/vision/* endpoints on the server).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.8.a"
|
|
14
|
+
|
|
15
|
+
from .session import VisionSessionManager, VisionSession, SessionState
|
|
16
|
+
from .capture import CaptureEngine, CaptureMode, CaptureConfig
|
|
17
|
+
from .frame_pipeline import FramePipeline, FrameMeta, ChangeLevel
|
|
18
|
+
from .annotations import AnnotationEngine, Annotation, AnnotationTool
|
|
19
|
+
from .context import VisionContextEngine, VisualContext
|
|
20
|
+
from .verify import BeforeAfterVerifier, VerificationResult
|
|
21
|
+
from .safety import PrivacyController, SafetyConfig
|
|
22
|
+
from .provider import VisionProviderRouter
|
|
23
|
+
from .cursor import CursorTracker, PointerEvent, DwellRegion
|
|
24
|
+
from .priority import FramePriorityEngine, PriorityLevel
|
|
25
|
+
from . import events as vision_events
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"VisionSessionManager", "VisionSession", "SessionState",
|
|
29
|
+
"CaptureEngine", "CaptureMode", "CaptureConfig",
|
|
30
|
+
"FramePipeline", "FrameMeta", "ChangeLevel",
|
|
31
|
+
"AnnotationEngine", "Annotation", "AnnotationTool",
|
|
32
|
+
"VisionContextEngine", "VisualContext",
|
|
33
|
+
"BeforeAfterVerifier", "VerificationResult",
|
|
34
|
+
"PrivacyController", "SafetyConfig",
|
|
35
|
+
"VisionProviderRouter",
|
|
36
|
+
"CursorTracker", "PointerEvent", "DwellRegion",
|
|
37
|
+
"FramePriorityEngine", "PriorityLevel",
|
|
38
|
+
"vision_events",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Legacy image-pipeline compatibility (calc_terminal/vision.py)
|
|
43
|
+
# The file calc_terminal/vision.py (image validation/normalization +
|
|
44
|
+
# vision-capable model routing) and the package calc_terminal/vision/
|
|
45
|
+
# (screen-capture agent) share the import name. Python prefers the
|
|
46
|
+
# package directory, so `import calc_terminal.vision` would hide the file.
|
|
47
|
+
# Re-expose the file's public symbols here so `from calc_terminal import
|
|
48
|
+
# vision` and `vision.pil_available()` keep working without a rename.
|
|
49
|
+
try:
|
|
50
|
+
import importlib.util as _ilu
|
|
51
|
+
import pathlib as _pl
|
|
52
|
+
_vision_file = _pl.Path(__file__).with_name("vision.py")
|
|
53
|
+
# When this package is loaded as `calc_terminal.vision`, the file is
|
|
54
|
+
# `calc_terminal/vision.py`'s sibling (one directory up).
|
|
55
|
+
if not _vision_file.exists():
|
|
56
|
+
_vision_file = _pl.Path(__file__).parent.parent / "vision.py"
|
|
57
|
+
# fallback: the classic location is calc_terminal/vision.py next
|
|
58
|
+
# to the package directory.
|
|
59
|
+
if not _vision_file.exists():
|
|
60
|
+
_vision_file = _pl.Path(__file__).parent / "vision.py"
|
|
61
|
+
if _vision_file.exists():
|
|
62
|
+
_spec = _ilu.spec_from_file_location(
|
|
63
|
+
"calc_terminal._vision_image", str(_vision_file))
|
|
64
|
+
_mod = _ilu.module_from_spec(_spec)
|
|
65
|
+
_spec.loader.exec_module(_mod) # type: ignore[union-attr]
|
|
66
|
+
# Re-export the image pipeline API
|
|
67
|
+
for _name in (
|
|
68
|
+
"MAX_DIMENSION", "MAX_BYTES", "JPEG_QUALITY",
|
|
69
|
+
"PROVIDER_NATIVE", "ALL_IMAGE_EXTS",
|
|
70
|
+
"pil_available", "ocr_available", "ImageError",
|
|
71
|
+
"validate", "classify_image", "inspect",
|
|
72
|
+
"normalize", "encode_for_model",
|
|
73
|
+
"analysis_prompt", "BASE_ANALYSIS_PROMPT",
|
|
74
|
+
"run_local_ocr", "find_vision_config", "analyze",
|
|
75
|
+
):
|
|
76
|
+
if hasattr(_mod, _name):
|
|
77
|
+
globals()[_name] = getattr(_mod, _name)
|
|
78
|
+
# Backward-compat: `vision.vision` self-reference (some probes do
|
|
79
|
+
# `from calc_terminal import vision; vision.vision`?)
|
|
80
|
+
__all__.extend([n for n in (
|
|
81
|
+
"pil_available", "ocr_available", "ImageError",
|
|
82
|
+
"validate", "classify_image", "inspect",
|
|
83
|
+
"normalize", "encode_for_model",
|
|
84
|
+
"analysis_prompt", "BASE_ANALYSIS_PROMPT",
|
|
85
|
+
"run_local_ocr", "find_vision_config", "analyze",
|
|
86
|
+
"MAX_DIMENSION", "MAX_BYTES",
|
|
87
|
+
) if n not in __all__])
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — AI Analysis Modes (spec 19).
|
|
3
|
+
|
|
4
|
+
Three internal levels:
|
|
5
|
+
QUICK — low latency, current frame + current annotation + recent interaction
|
|
6
|
+
DEEP — selected frame(s) + annotations + cursor + project + chat
|
|
7
|
+
DEBUG — full pipeline: vision → identify UI region → locate code → propose fix → apply → verify
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AnalysisLevel(str, Enum):
|
|
19
|
+
QUICK = "quick"
|
|
20
|
+
DEEP = "deep"
|
|
21
|
+
DEBUG = "debug"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AnalysisRequest:
|
|
26
|
+
level: AnalysisLevel
|
|
27
|
+
session_id: str
|
|
28
|
+
prompt: str = ""
|
|
29
|
+
include_code: bool = False
|
|
30
|
+
include_before_after: bool = False
|
|
31
|
+
error_message: str = ""
|
|
32
|
+
user_intent: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AnalysisResult:
|
|
37
|
+
level: AnalysisLevel
|
|
38
|
+
analysis: str
|
|
39
|
+
frame_index: Optional[int] = None
|
|
40
|
+
confidence: float = 0.0
|
|
41
|
+
findings: List[str] = field(default_factory=list)
|
|
42
|
+
suggested_fix: Optional[str] = None
|
|
43
|
+
related_files: List[str] = field(default_factory=list)
|
|
44
|
+
duration_ms: float = 0.0
|
|
45
|
+
provider: str = ""
|
|
46
|
+
model: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_system_prompt(level: AnalysisLevel) -> str:
|
|
50
|
+
if level == AnalysisLevel.QUICK:
|
|
51
|
+
return (
|
|
52
|
+
"You are CAT Vision — QUICK mode. Low latency.\n"
|
|
53
|
+
"Analyze the current frame + current annotation + recent cursor context.\n"
|
|
54
|
+
"Be concise, point to the immediate issue, suggest one next step."
|
|
55
|
+
)
|
|
56
|
+
if level == AnalysisLevel.DEEP:
|
|
57
|
+
return (
|
|
58
|
+
"You are CAT Vision — DEEP analysis. Thorough reasoning.\n"
|
|
59
|
+
"Analyze selected frame(s), annotations, cursor context, project/code context, and chat history.\n"
|
|
60
|
+
"Explain what you see, root cause, and a specific fix with file/function references when possible."
|
|
61
|
+
)
|
|
62
|
+
# DEBUG
|
|
63
|
+
return (
|
|
64
|
+
"You are CAT Vision — DEBUG mode. Full engineering workflow.\n"
|
|
65
|
+
"Pipeline: Vision → Identify UI region → Determine likely behavior → Inspect project\n"
|
|
66
|
+
"→ Locate related code → Reproduce where possible → Propose fix → Wait for permission → Verify.\n"
|
|
67
|
+
"Correlate the visual region with project code. If exact mapping is unavailable, say so — never hallucinate file locations.\n"
|
|
68
|
+
"Propose a concrete code change and flag the permission gate."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_user_prompt(req: AnalysisRequest, ctx: Dict[str, Any]) -> str:
|
|
73
|
+
parts: List[str] = []
|
|
74
|
+
if req.level == AnalysisLevel.QUICK:
|
|
75
|
+
parts.append(req.prompt or "Quick analyze: what is visible and what should the user check?")
|
|
76
|
+
if ctx.get("annotation_summary"):
|
|
77
|
+
parts.append(f"Annotation: {ctx['annotation_summary']}")
|
|
78
|
+
if ctx.get("cursor_summary"):
|
|
79
|
+
parts.append(f"Cursor: {ctx['cursor_summary']}")
|
|
80
|
+
elif req.level == AnalysisLevel.DEEP:
|
|
81
|
+
parts.append(req.prompt or "Deep analyze this screen. What is wrong and how to fix it?")
|
|
82
|
+
if req.error_message:
|
|
83
|
+
parts.append(f"Error reported: {req.error_message}")
|
|
84
|
+
if req.user_intent:
|
|
85
|
+
parts.append(f"User intent: {req.user_intent}")
|
|
86
|
+
if ctx.get("annotation_summary"):
|
|
87
|
+
parts.append(f"Annotations: {ctx['annotation_summary']}")
|
|
88
|
+
if ctx.get("cursor_summary"):
|
|
89
|
+
parts.append(f"Cursor context: {ctx['cursor_summary']}")
|
|
90
|
+
if ctx.get("project_hint"):
|
|
91
|
+
parts.append(f"Project: {ctx['project_hint']}")
|
|
92
|
+
else: # DEBUG
|
|
93
|
+
parts.append("VISUAL DEBUG SESSION — full pipeline.")
|
|
94
|
+
if req.prompt:
|
|
95
|
+
parts.append(f"User: {req.prompt}")
|
|
96
|
+
if req.error_message:
|
|
97
|
+
parts.append(f"Error: {req.error_message}")
|
|
98
|
+
if req.user_intent:
|
|
99
|
+
parts.append(f"Intent: {req.user_intent}")
|
|
100
|
+
parts.append(
|
|
101
|
+
"Provide:\n"
|
|
102
|
+
"1. What you see (UI elements, text, state)\n"
|
|
103
|
+
"2. Errors/warnings/anomalies\n"
|
|
104
|
+
"3. Root cause hypothesis with code reference if locatable\n"
|
|
105
|
+
"4. Specific fix suggestion with code\n"
|
|
106
|
+
"5. Observation: say when exact source mapping is unavailable\n"
|
|
107
|
+
"6. Do NOT apply fixes yourself — propose and wait for permission"
|
|
108
|
+
)
|
|
109
|
+
return "\n\n".join(parts)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run_analysis(req: AnalysisRequest, visual_ctx, config: Optional[Dict] = None) -> AnalysisResult:
|
|
113
|
+
"""Run one analysis level against a VisualContext (vision/context.py)."""
|
|
114
|
+
t0 = time.time()
|
|
115
|
+
system_prompt = build_system_prompt(req.level)
|
|
116
|
+
# build minimal ctx hints from visual_ctx
|
|
117
|
+
ctx_hints: Dict[str, Any] = {}
|
|
118
|
+
try:
|
|
119
|
+
if visual_ctx.annotations:
|
|
120
|
+
ctx_hints["annotation_summary"] = "; ".join(
|
|
121
|
+
f"{a.get('tool','?')}" for a in visual_ctx.annotations[:5]
|
|
122
|
+
)
|
|
123
|
+
if visual_ctx.pointer_telemetry:
|
|
124
|
+
pt = visual_ctx.pointer_telemetry
|
|
125
|
+
ctx_hints["cursor_summary"] = f"active={pt.get('active_element','')} moving={pt.get('cursor_moving',False)}"
|
|
126
|
+
if visual_ctx.project_context:
|
|
127
|
+
ctx_hints["project_hint"] = visual_ctx.project_context.get("type", "")
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
user_prompt = build_user_prompt(req, ctx_hints)
|
|
131
|
+
# enrich visual_ctx with our prompts before calling provider
|
|
132
|
+
visual_ctx.analysis_prompt = user_prompt
|
|
133
|
+
# route
|
|
134
|
+
try:
|
|
135
|
+
from .provider import VisionProviderRouter
|
|
136
|
+
from .. import aicore
|
|
137
|
+
from ..attachments import Attachment
|
|
138
|
+
import base64
|
|
139
|
+
router = VisionProviderRouter(config)
|
|
140
|
+
cfg, caps = router.find_vision_capable(config) or (None, None)
|
|
141
|
+
if cfg is None:
|
|
142
|
+
return AnalysisResult(level=req.level, analysis="No vision-capable model configured. Run /model to choose one.", duration_ms=(time.time()-t0)*1000)
|
|
143
|
+
# Build attachment from frame_b64
|
|
144
|
+
att = Attachment.__new__(Attachment)
|
|
145
|
+
att.id = f"att-vision-{req.level.value}-{int(time.time()*1000)%1000000}"
|
|
146
|
+
att.name = "vision_frame"
|
|
147
|
+
att.path = ""
|
|
148
|
+
att.extension = ".png"
|
|
149
|
+
att.mime_type = "image/png"
|
|
150
|
+
att.size = len(visual_ctx.frame_b64 or "")
|
|
151
|
+
att.kind = "image"
|
|
152
|
+
att.content = "[vision frame]"
|
|
153
|
+
att.metadata = {"inline_b64": visual_ctx.frame_b64 or ""}
|
|
154
|
+
att.extraction_status = "ready"
|
|
155
|
+
att.error = None
|
|
156
|
+
att.created_at = time.time()
|
|
157
|
+
att.source = "vision_analysis"
|
|
158
|
+
# Attachments: frame + optional before frame
|
|
159
|
+
attachments = [att]
|
|
160
|
+
if req.include_before_after and visual_ctx.before_frame_b64:
|
|
161
|
+
att2 = Attachment.__new__(Attachment)
|
|
162
|
+
att2.id = "att-vision-before"
|
|
163
|
+
att2.name = "before_frame"
|
|
164
|
+
att2.path = ""
|
|
165
|
+
att2.extension = ".png"
|
|
166
|
+
att2.mime_type = "image/png"
|
|
167
|
+
att2.size = len(visual_ctx.before_frame_b64)
|
|
168
|
+
att2.kind = "image"
|
|
169
|
+
att2.content = "[before frame]"
|
|
170
|
+
att2.metadata = {"inline_b64": visual_ctx.before_frame_b64}
|
|
171
|
+
att2.extraction_status = "ready"
|
|
172
|
+
att2.error = None
|
|
173
|
+
att2.created_at = time.time()
|
|
174
|
+
att2.source = "vision_verify"
|
|
175
|
+
attachments.append(att2)
|
|
176
|
+
analysis_text = aicore.query_ai(
|
|
177
|
+
user_prompt,
|
|
178
|
+
system_prompt=system_prompt,
|
|
179
|
+
history=None,
|
|
180
|
+
config=cfg,
|
|
181
|
+
attachments=attachments,
|
|
182
|
+
size_class="large" if req.level != AnalysisLevel.QUICK else "normal",
|
|
183
|
+
)
|
|
184
|
+
return AnalysisResult(
|
|
185
|
+
level=req.level,
|
|
186
|
+
analysis=analysis_text or "",
|
|
187
|
+
frame_index=None,
|
|
188
|
+
confidence=0.7,
|
|
189
|
+
duration_ms=(time.time()-t0)*1000,
|
|
190
|
+
provider=cfg.get("provider",""),
|
|
191
|
+
model=cfg.get("model",""),
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return AnalysisResult(level=req.level, analysis=f"Analysis failed: {e}", duration_ms=(time.time()-t0)*1000)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.8.a — Annotation Engine.
|
|
3
|
+
|
|
4
|
+
Handles pen, arrow, rect, circle, text, and highlight annotations.
|
|
5
|
+
Manages undo/redo stack, serialization, and frame annotation binding.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnnotationTool(str, Enum):
|
|
18
|
+
PEN = "pen"
|
|
19
|
+
ARROW = "arrow"
|
|
20
|
+
RECT = "rect"
|
|
21
|
+
CIRCLE = "circle"
|
|
22
|
+
TEXT = "text"
|
|
23
|
+
HIGHLIGHT = "highlight"
|
|
24
|
+
CROSSHAIR = "crosshair"
|
|
25
|
+
ERASER = "eraser"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Color:
|
|
30
|
+
r: int = 255
|
|
31
|
+
g: int = 0
|
|
32
|
+
b: int = 0
|
|
33
|
+
a: float = 1.0
|
|
34
|
+
|
|
35
|
+
def to_css(self) -> str:
|
|
36
|
+
return f"rgba({self.r},{self.g},{self.b},{self.a})"
|
|
37
|
+
|
|
38
|
+
def to_hex(self) -> str:
|
|
39
|
+
return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
42
|
+
return {"r": self.r, "g": self.g, "b": self.b, "a": self.a}
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_hex(cls, hex_str: str) -> "Color":
|
|
46
|
+
h = hex_str.lstrip("#")
|
|
47
|
+
if len(h) == 3:
|
|
48
|
+
h = "".join(c * 2 for c in h)
|
|
49
|
+
if len(h) == 6:
|
|
50
|
+
return cls(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
|
51
|
+
elif len(h) == 8:
|
|
52
|
+
return cls(
|
|
53
|
+
int(h[0:2], 16), int(h[2:4], 16),
|
|
54
|
+
int(h[4:6], 16), int(h[6:8], 16) / 255.0
|
|
55
|
+
)
|
|
56
|
+
return cls()
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, d: Dict) -> "Color":
|
|
60
|
+
return cls(d.get("r", 255), d.get("g", 0), d.get("b", 0), d.get("a", 1.0))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Annotation:
|
|
65
|
+
id: str
|
|
66
|
+
tool: AnnotationTool
|
|
67
|
+
points: List[Tuple[float, float]]
|
|
68
|
+
color: Color
|
|
69
|
+
line_width: float
|
|
70
|
+
text: str = ""
|
|
71
|
+
text_position: Optional[Tuple[float, float]] = None
|
|
72
|
+
opacity: float = 1.0
|
|
73
|
+
created_at: float = 0.0
|
|
74
|
+
frame_index: int = -1
|
|
75
|
+
bounding_box: Optional[Dict[str, float]] = None
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
78
|
+
return {
|
|
79
|
+
"id": self.id,
|
|
80
|
+
"tool": self.tool.value,
|
|
81
|
+
"points": [[p[0], p[1]] for p in self.points],
|
|
82
|
+
"color": self.color.to_dict(),
|
|
83
|
+
"line_width": self.line_width,
|
|
84
|
+
"text": self.text,
|
|
85
|
+
"text_position": list(self.text_position) if self.text_position else None,
|
|
86
|
+
"opacity": self.opacity,
|
|
87
|
+
"created_at": self.created_at,
|
|
88
|
+
"frame_index": self.frame_index,
|
|
89
|
+
"bounding_box": self.bounding_box,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_dict(cls, d: Dict) -> "Annotation":
|
|
94
|
+
return cls(
|
|
95
|
+
id=d.get("id", str(uuid.uuid4())),
|
|
96
|
+
tool=AnnotationTool(d.get("tool", "pen")),
|
|
97
|
+
points=[(p[0], p[1]) for p in d.get("points", [])],
|
|
98
|
+
color=Color.from_dict(d.get("color", {})),
|
|
99
|
+
line_width=d.get("line_width", 3),
|
|
100
|
+
text=d.get("text", ""),
|
|
101
|
+
text_position=tuple(d["text_position"]) if d.get("text_position") else None,
|
|
102
|
+
opacity=d.get("opacity", 1.0),
|
|
103
|
+
created_at=d.get("created_at", 0.0),
|
|
104
|
+
frame_index=d.get("frame_index", -1),
|
|
105
|
+
bounding_box=d.get("bounding_box"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AnnotationEngine:
|
|
110
|
+
"""Manages annotation creation, undo/redo, and serialization."""
|
|
111
|
+
|
|
112
|
+
def __init__(self):
|
|
113
|
+
self.annotations: List[Annotation] = []
|
|
114
|
+
self._undo_stack: List[Annotation] = []
|
|
115
|
+
self._redo_stack: List[Annotation] = []
|
|
116
|
+
self._max_undo = 50
|
|
117
|
+
self._default_color = Color(255, 0, 0)
|
|
118
|
+
self._default_width = 3.0
|
|
119
|
+
|
|
120
|
+
def set_default_color(self, color: Color):
|
|
121
|
+
self._default_color = color
|
|
122
|
+
|
|
123
|
+
def set_default_width(self, width: float):
|
|
124
|
+
self._default_width = width
|
|
125
|
+
|
|
126
|
+
def add_annotation(self, tool: AnnotationTool,
|
|
127
|
+
points: List[Tuple[float, float]],
|
|
128
|
+
color: Optional[Color] = None,
|
|
129
|
+
line_width: Optional[float] = None,
|
|
130
|
+
text: str = "",
|
|
131
|
+
frame_index: int = -1,
|
|
132
|
+
annotation_id: Optional[str] = None) -> Annotation:
|
|
133
|
+
ann = Annotation(
|
|
134
|
+
id=annotation_id or str(uuid.uuid4()),
|
|
135
|
+
tool=tool,
|
|
136
|
+
points=points,
|
|
137
|
+
color=color or self._default_color,
|
|
138
|
+
line_width=line_width or self._default_width,
|
|
139
|
+
text=text,
|
|
140
|
+
text_position=points[0] if points else None,
|
|
141
|
+
created_at=time.time(),
|
|
142
|
+
frame_index=frame_index,
|
|
143
|
+
bounding_box=self._compute_bbox(points),
|
|
144
|
+
)
|
|
145
|
+
self.annotations.append(ann)
|
|
146
|
+
self._undo_stack.append(ann)
|
|
147
|
+
if len(self._undo_stack) > self._max_undo:
|
|
148
|
+
self._undo_stack.pop(0)
|
|
149
|
+
self._redo_stack.clear()
|
|
150
|
+
return ann
|
|
151
|
+
|
|
152
|
+
def undo(self) -> Optional[Annotation]:
|
|
153
|
+
if not self._undo_stack:
|
|
154
|
+
return None
|
|
155
|
+
ann = self._undo_stack.pop()
|
|
156
|
+
self.annotations = [a for a in self.annotations if a.id != ann.id]
|
|
157
|
+
self._redo_stack.append(ann)
|
|
158
|
+
return ann
|
|
159
|
+
|
|
160
|
+
def redo(self) -> Optional[Annotation]:
|
|
161
|
+
if not self._redo_stack:
|
|
162
|
+
return None
|
|
163
|
+
ann = self._redo_stack.pop()
|
|
164
|
+
self.annotations.append(ann)
|
|
165
|
+
self._undo_stack.append(ann)
|
|
166
|
+
return ann
|
|
167
|
+
|
|
168
|
+
def clear(self):
|
|
169
|
+
self.annotations.clear()
|
|
170
|
+
self._undo_stack.clear()
|
|
171
|
+
self._redo_stack.clear()
|
|
172
|
+
|
|
173
|
+
def clear_frame(self, frame_index: int):
|
|
174
|
+
self.annotations = [
|
|
175
|
+
a for a in self.annotations if a.frame_index != frame_index
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
def update_annotation(self, annotation_id: str, **kwargs) -> Optional[Annotation]:
|
|
179
|
+
for ann in self.annotations:
|
|
180
|
+
if ann.id == annotation_id:
|
|
181
|
+
for key, val in kwargs.items():
|
|
182
|
+
if key == "color" and isinstance(val, dict):
|
|
183
|
+
ann.color = Color.from_dict(val)
|
|
184
|
+
elif key == "points" and isinstance(val, list):
|
|
185
|
+
ann.points = [(p[0], p[1]) for p in val]
|
|
186
|
+
ann.bounding_box = self._compute_bbox(ann.points)
|
|
187
|
+
elif hasattr(ann, key):
|
|
188
|
+
setattr(ann, key, val)
|
|
189
|
+
return ann
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def delete_annotation(self, annotation_id: str) -> bool:
|
|
193
|
+
before = len(self.annotations)
|
|
194
|
+
self.annotations = [a for a in self.annotations if a.id != annotation_id]
|
|
195
|
+
return len(self.annotations) < before
|
|
196
|
+
|
|
197
|
+
def get_annotations_for_frame(self, frame_index: int) -> List[Dict]:
|
|
198
|
+
return [a.to_dict() for a in self.annotations if a.frame_index == frame_index]
|
|
199
|
+
|
|
200
|
+
def get_pointer_annotation(self, x: float, y: float,
|
|
201
|
+
radius: float = 15.0) -> Optional[Annotation]:
|
|
202
|
+
"""Find annotation near a pointer position."""
|
|
203
|
+
for ann in reversed(self.annotations):
|
|
204
|
+
if ann.tool == AnnotationTool.CROSSHAIR:
|
|
205
|
+
continue
|
|
206
|
+
if ann.bounding_box:
|
|
207
|
+
bb = ann.bounding_box
|
|
208
|
+
if (bb["x"] - radius <= x <= bb["x"] + bb["width"] + radius and
|
|
209
|
+
bb["y"] - radius <= y <= bb["y"] + bb["height"] + radius):
|
|
210
|
+
return ann
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def to_list(self) -> List[Dict]:
|
|
214
|
+
return [a.to_dict() for a in self.annotations]
|
|
215
|
+
|
|
216
|
+
def from_list(self, data: List[Dict]):
|
|
217
|
+
self.clear()
|
|
218
|
+
for d in data:
|
|
219
|
+
self.annotations.append(Annotation.from_dict(d))
|
|
220
|
+
self._undo_stack = list(self.annotations)
|
|
221
|
+
self._redo_stack.clear()
|
|
222
|
+
|
|
223
|
+
def _compute_bbox(self, points: List[Tuple[float, float]]) -> Optional[Dict[str, float]]:
|
|
224
|
+
if not points:
|
|
225
|
+
return None
|
|
226
|
+
xs = [p[0] for p in points]
|
|
227
|
+
ys = [p[1] for p in points]
|
|
228
|
+
return {
|
|
229
|
+
"x": min(xs), "y": min(ys),
|
|
230
|
+
"width": max(xs) - min(xs),
|
|
231
|
+
"height": max(ys) - min(ys),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def export_svg(self, width: int = 1920, height: int = 1080) -> str:
|
|
235
|
+
lines = [
|
|
236
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">',
|
|
237
|
+
]
|
|
238
|
+
for ann in self.annotations:
|
|
239
|
+
color_hex = ann.color.to_hex()
|
|
240
|
+
opacity = ann.opacity
|
|
241
|
+
if ann.tool == AnnotationTool.PEN and len(ann.points) >= 2:
|
|
242
|
+
path_d = "M " + " L ".join(
|
|
243
|
+
f"{p[0]:.1f},{p[1]:.1f}" for p in ann.points
|
|
244
|
+
)
|
|
245
|
+
lines.append(
|
|
246
|
+
f' <path d="{path_d}" stroke="{color_hex}" '
|
|
247
|
+
f'stroke-width="{ann.line_width}" fill="none" '
|
|
248
|
+
f'opacity="{opacity}" stroke-linecap="round"/>'
|
|
249
|
+
)
|
|
250
|
+
elif ann.tool == AnnotationTool.ARROW and len(ann.points) >= 2:
|
|
251
|
+
x1, y1 = ann.points[0]
|
|
252
|
+
x2, y2 = ann.points[-1]
|
|
253
|
+
lines.append(
|
|
254
|
+
f' <line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" '
|
|
255
|
+
f'stroke="{color_hex}" stroke-width="{ann.line_width}" '
|
|
256
|
+
f'opacity="{opacity}" marker-end="url(#arrowhead)"/>'
|
|
257
|
+
)
|
|
258
|
+
elif ann.tool == AnnotationTool.RECT and len(ann.points) >= 2:
|
|
259
|
+
x1, y1 = ann.points[0]
|
|
260
|
+
x2, y2 = ann.points[-1]
|
|
261
|
+
lines.append(
|
|
262
|
+
f' <rect x="{min(x1,x2):.1f}" y="{min(y1,y2):.1f}" '
|
|
263
|
+
f'width="{abs(x2-x1):.1f}" height="{abs(y2-y1):.1f}" '
|
|
264
|
+
f'stroke="{color_hex}" stroke-width="{ann.line_width}" '
|
|
265
|
+
f'fill="none" opacity="{opacity}"/>'
|
|
266
|
+
)
|
|
267
|
+
elif ann.tool == AnnotationTool.CIRCLE and len(ann.points) >= 2:
|
|
268
|
+
x1, y1 = ann.points[0]
|
|
269
|
+
x2, y2 = ann.points[-1]
|
|
270
|
+
cx = (x1 + x2) / 2
|
|
271
|
+
cy = (y1 + y2) / 2
|
|
272
|
+
rx = abs(x2 - x1) / 2
|
|
273
|
+
ry = abs(y2 - y1) / 2
|
|
274
|
+
lines.append(
|
|
275
|
+
f' <ellipse cx="{cx:.1f}" cy="{cy:.1f}" rx="{rx:.1f}" ry="{ry:.1f}" '
|
|
276
|
+
f'stroke="{color_hex}" stroke-width="{ann.line_width}" '
|
|
277
|
+
f'fill="none" opacity="{opacity}"/>'
|
|
278
|
+
)
|
|
279
|
+
elif ann.tool == AnnotationTool.TEXT and ann.text_position:
|
|
280
|
+
tx, ty = ann.text_position
|
|
281
|
+
escaped = ann.text.replace("&", "&").replace("<", "<")
|
|
282
|
+
lines.append(
|
|
283
|
+
f' <text x="{tx:.1f}" y="{ty:.1f}" fill="{color_hex}" '
|
|
284
|
+
f'font-size="{ann.line_width * 5:.0f}" opacity="{opacity}">'
|
|
285
|
+
f'{escaped}</text>'
|
|
286
|
+
)
|
|
287
|
+
elif ann.tool == AnnotationTool.HIGHLIGHT and len(ann.points) >= 2:
|
|
288
|
+
x1, y1 = ann.points[0]
|
|
289
|
+
x2, y2 = ann.points[-1]
|
|
290
|
+
highlight_color = Color(ann.color.r, ann.color.g, ann.color.b, 0.3)
|
|
291
|
+
lines.append(
|
|
292
|
+
f' <rect x="{min(x1,x2):.1f}" y="{min(y1,y2):.1f}" '
|
|
293
|
+
f'width="{abs(x2-x1):.1f}" height="{abs(y2-y1):.1f}" '
|
|
294
|
+
f'fill="{highlight_color.to_hex()}" opacity="{opacity}"/>'
|
|
295
|
+
)
|
|
296
|
+
lines.append("</svg>")
|
|
297
|
+
return "\n".join(lines)
|