scitex 2.14.0__py3-none-any.whl → 2.15.3__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.
- scitex/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +27 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +17 -210
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +160 -41
- scitex/cli/capture.py +133 -20
- scitex/cli/introspect.py +488 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +414 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +154 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +355 -0
- scitex/cli/stats.py +136 -11
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +49 -299
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/__init__.py +43 -34
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.3.dist-info/METADATA +667 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_editor.py
|
|
4
|
+
|
|
5
|
+
"""Core WebEditor class for Flask-based figure editing."""
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import webbrowser
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .._utils import check_port_available, kill_process_on_port
|
|
15
|
+
|
|
16
|
+
__all__ = ["WebEditor"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WebEditor:
|
|
20
|
+
"""Browser-based figure editor using Flask.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Displays existing PNG from plot bundle (no re-rendering)
|
|
24
|
+
- Hitmap-based element selection for precise clicking
|
|
25
|
+
- Property editors with sliders and color pickers
|
|
26
|
+
- Save to .manual.json
|
|
27
|
+
- SciTeX style defaults pre-filled
|
|
28
|
+
- Auto-finds available port if default is in use
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
json_path: Path,
|
|
34
|
+
metadata: Dict[str, Any],
|
|
35
|
+
csv_data: Optional[Any] = None,
|
|
36
|
+
png_path: Optional[Path] = None,
|
|
37
|
+
hitmap_path: Optional[Path] = None,
|
|
38
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
39
|
+
port: int = 5050,
|
|
40
|
+
panel_info: Optional[Dict[str, Any]] = None,
|
|
41
|
+
):
|
|
42
|
+
self.json_path = Path(json_path)
|
|
43
|
+
self.metadata = metadata
|
|
44
|
+
self.csv_data = csv_data
|
|
45
|
+
self.png_path = Path(png_path) if png_path else None
|
|
46
|
+
self.hitmap_path = Path(hitmap_path) if hitmap_path else None
|
|
47
|
+
self.manual_overrides = manual_overrides or {}
|
|
48
|
+
self._requested_port = port
|
|
49
|
+
self.port = port
|
|
50
|
+
self.panel_info = panel_info
|
|
51
|
+
|
|
52
|
+
# Extract hit_regions from metadata
|
|
53
|
+
self.hit_regions = metadata.get("hit_regions", {})
|
|
54
|
+
self.color_map = self.hit_regions.get("color_map", {})
|
|
55
|
+
|
|
56
|
+
# Get SciTeX defaults and merge with metadata
|
|
57
|
+
from ..._defaults import extract_defaults_from_metadata, get_scitex_defaults
|
|
58
|
+
|
|
59
|
+
self.scitex_defaults = get_scitex_defaults()
|
|
60
|
+
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
61
|
+
|
|
62
|
+
# Start with defaults, then overlay manual overrides
|
|
63
|
+
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
64
|
+
self.current_overrides.update(self.metadata_defaults)
|
|
65
|
+
self.current_overrides.update(self.manual_overrides)
|
|
66
|
+
|
|
67
|
+
# Track initial state to detect modifications
|
|
68
|
+
self._initial_overrides = copy.deepcopy(self.current_overrides)
|
|
69
|
+
self._user_modified = False
|
|
70
|
+
|
|
71
|
+
def run(self):
|
|
72
|
+
"""Launch the web editor."""
|
|
73
|
+
try:
|
|
74
|
+
from flask import Flask
|
|
75
|
+
except ImportError:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"Flask is required for web editor. Install: pip install flask"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Handle port conflicts
|
|
81
|
+
self._setup_port()
|
|
82
|
+
|
|
83
|
+
# Configure Flask
|
|
84
|
+
import os
|
|
85
|
+
|
|
86
|
+
static_folder = os.path.join(
|
|
87
|
+
os.path.dirname(os.path.dirname(__file__)), "static"
|
|
88
|
+
)
|
|
89
|
+
app = Flask(__name__, static_folder=static_folder, static_url_path="/static")
|
|
90
|
+
|
|
91
|
+
# Register all routes
|
|
92
|
+
self._register_routes(app)
|
|
93
|
+
|
|
94
|
+
# Open browser after short delay
|
|
95
|
+
def open_browser():
|
|
96
|
+
time.sleep(0.5)
|
|
97
|
+
webbrowser.open(f"http://127.0.0.1:{self.port}")
|
|
98
|
+
|
|
99
|
+
threading.Thread(target=open_browser, daemon=True).start()
|
|
100
|
+
|
|
101
|
+
print(f"Starting SciTeX Figure Editor at http://127.0.0.1:{self.port}")
|
|
102
|
+
print("Press Ctrl+C to stop")
|
|
103
|
+
|
|
104
|
+
app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
|
|
105
|
+
|
|
106
|
+
def _setup_port(self):
|
|
107
|
+
"""Handle port conflicts."""
|
|
108
|
+
max_retries = 3
|
|
109
|
+
for attempt in range(max_retries):
|
|
110
|
+
if check_port_available(self._requested_port):
|
|
111
|
+
self.port = self._requested_port
|
|
112
|
+
break
|
|
113
|
+
print(
|
|
114
|
+
f"Port {self._requested_port} in use. Freeing... "
|
|
115
|
+
f"(attempt {attempt + 1}/{max_retries})"
|
|
116
|
+
)
|
|
117
|
+
kill_process_on_port(self._requested_port)
|
|
118
|
+
time.sleep(1.0)
|
|
119
|
+
else:
|
|
120
|
+
print(f"Warning: Port {self._requested_port} may still be in use")
|
|
121
|
+
self.port = self._requested_port
|
|
122
|
+
|
|
123
|
+
def _register_routes(self, app):
|
|
124
|
+
"""Register all Flask routes."""
|
|
125
|
+
from ._routes_basic import (
|
|
126
|
+
create_colormap_route,
|
|
127
|
+
create_hitmap_route,
|
|
128
|
+
create_index_route,
|
|
129
|
+
create_preview_route,
|
|
130
|
+
create_shutdown_route,
|
|
131
|
+
create_stats_route,
|
|
132
|
+
create_update_route,
|
|
133
|
+
)
|
|
134
|
+
from ._routes_export import (
|
|
135
|
+
create_download_figz_route,
|
|
136
|
+
create_download_route,
|
|
137
|
+
create_export_route,
|
|
138
|
+
)
|
|
139
|
+
from ._routes_panels import (
|
|
140
|
+
create_panels_route,
|
|
141
|
+
create_switch_panel_route,
|
|
142
|
+
)
|
|
143
|
+
from ._routes_save import (
|
|
144
|
+
create_save_element_position_route,
|
|
145
|
+
create_save_layout_route,
|
|
146
|
+
create_save_route,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Basic routes
|
|
150
|
+
create_index_route(app, self)
|
|
151
|
+
create_preview_route(app, self)
|
|
152
|
+
create_hitmap_route(app, self)
|
|
153
|
+
create_colormap_route(app, self)
|
|
154
|
+
create_update_route(app, self)
|
|
155
|
+
create_stats_route(app, self)
|
|
156
|
+
create_shutdown_route(app, self)
|
|
157
|
+
|
|
158
|
+
# Panel routes
|
|
159
|
+
create_panels_route(app, self)
|
|
160
|
+
create_switch_panel_route(app, self)
|
|
161
|
+
|
|
162
|
+
# Save routes
|
|
163
|
+
create_save_route(app, self)
|
|
164
|
+
create_save_layout_route(app, self)
|
|
165
|
+
create_save_element_position_route(app, self)
|
|
166
|
+
|
|
167
|
+
# Export routes
|
|
168
|
+
create_export_route(app, self)
|
|
169
|
+
create_download_route(app, self)
|
|
170
|
+
create_download_figz_route(app, self)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# EOF
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_export_helpers.py
|
|
4
|
+
|
|
5
|
+
"""Export and compose helpers for figure bundles."""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import json as json_module
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .._core import WebEditor
|
|
14
|
+
|
|
15
|
+
__all__ = ["export_composed_figure", "compose_panels_to_figure"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def export_composed_figure(
|
|
19
|
+
editor: "WebEditor",
|
|
20
|
+
formats: List[str] = None,
|
|
21
|
+
dpi: int = 150,
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
"""Compose and export figure to bundle.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
editor : WebEditor
|
|
28
|
+
The editor instance with panel_info.
|
|
29
|
+
formats : list of str
|
|
30
|
+
Output formats (default: ["png", "svg"]).
|
|
31
|
+
dpi : int
|
|
32
|
+
Resolution for raster output.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
dict
|
|
37
|
+
Result with 'success' and 'exported' keys.
|
|
38
|
+
"""
|
|
39
|
+
if formats is None:
|
|
40
|
+
formats = ["png", "svg"]
|
|
41
|
+
|
|
42
|
+
import matplotlib
|
|
43
|
+
|
|
44
|
+
matplotlib.use("Agg")
|
|
45
|
+
|
|
46
|
+
import matplotlib.pyplot as plt
|
|
47
|
+
|
|
48
|
+
from scitex.io import ZipBundle
|
|
49
|
+
|
|
50
|
+
if not editor.panel_info:
|
|
51
|
+
return {"success": False, "error": "No panel info"}
|
|
52
|
+
|
|
53
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
54
|
+
figure_dir = editor.panel_info.get("figure_dir")
|
|
55
|
+
|
|
56
|
+
if not bundle_path and not figure_dir:
|
|
57
|
+
return {"success": False, "error": "No bundle path"}
|
|
58
|
+
|
|
59
|
+
figure_name = (
|
|
60
|
+
Path(bundle_path).stem
|
|
61
|
+
if bundle_path
|
|
62
|
+
else (Path(figure_dir).stem.replace(".figure", "") if figure_dir else "figure")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Read spec.json and layout.json
|
|
66
|
+
spec, layout_overrides = _read_spec_and_layout(
|
|
67
|
+
bundle_path, figure_dir, editor.panel_info
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Get figure dimensions
|
|
71
|
+
fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
|
|
72
|
+
fig_width_in = fig_width_mm / 25.4
|
|
73
|
+
fig_height_in = fig_height_mm / 25.4
|
|
74
|
+
|
|
75
|
+
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor="white")
|
|
76
|
+
|
|
77
|
+
# Compose panels
|
|
78
|
+
_compose_panels(
|
|
79
|
+
fig,
|
|
80
|
+
spec,
|
|
81
|
+
editor.panel_info,
|
|
82
|
+
layout_overrides,
|
|
83
|
+
fig_width_mm,
|
|
84
|
+
fig_height_mm,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
exported = {}
|
|
88
|
+
|
|
89
|
+
# Save to bundle
|
|
90
|
+
if bundle_path:
|
|
91
|
+
with ZipBundle(bundle_path, mode="a") as bundle:
|
|
92
|
+
for fmt in formats:
|
|
93
|
+
buf = io.BytesIO()
|
|
94
|
+
fig.savefig(
|
|
95
|
+
buf,
|
|
96
|
+
format=fmt,
|
|
97
|
+
dpi=dpi,
|
|
98
|
+
bbox_inches="tight",
|
|
99
|
+
facecolor="white",
|
|
100
|
+
pad_inches=0.02,
|
|
101
|
+
)
|
|
102
|
+
buf.seek(0)
|
|
103
|
+
export_path = f"exports/{figure_name}.{fmt}"
|
|
104
|
+
bundle.write_bytes(export_path, buf.read())
|
|
105
|
+
exported[fmt] = export_path
|
|
106
|
+
|
|
107
|
+
plt.close(fig)
|
|
108
|
+
return {"success": True, "exported": exported}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def compose_panels_to_figure(
|
|
112
|
+
editor: "WebEditor",
|
|
113
|
+
fmt: str = "png",
|
|
114
|
+
dpi: int = 150,
|
|
115
|
+
) -> io.BytesIO:
|
|
116
|
+
"""Compose panels into a figure and return as BytesIO.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
editor : WebEditor
|
|
121
|
+
The editor instance.
|
|
122
|
+
fmt : str
|
|
123
|
+
Output format.
|
|
124
|
+
dpi : int
|
|
125
|
+
Resolution.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
io.BytesIO
|
|
130
|
+
The composed figure as bytes.
|
|
131
|
+
"""
|
|
132
|
+
import matplotlib
|
|
133
|
+
|
|
134
|
+
matplotlib.use("Agg")
|
|
135
|
+
import matplotlib.pyplot as plt
|
|
136
|
+
|
|
137
|
+
bundle_path = editor.panel_info.get("bundle_path")
|
|
138
|
+
figure_dir = editor.panel_info.get("figure_dir")
|
|
139
|
+
|
|
140
|
+
spec, layout_overrides = _read_spec_and_layout(
|
|
141
|
+
bundle_path, figure_dir, editor.panel_info
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
fig_width_mm, fig_height_mm = _get_figure_dimensions(spec)
|
|
145
|
+
fig_width_in = fig_width_mm / 25.4
|
|
146
|
+
fig_height_in = fig_height_mm / 25.4
|
|
147
|
+
|
|
148
|
+
fig = plt.figure(
|
|
149
|
+
figsize=(fig_width_in, fig_height_in),
|
|
150
|
+
dpi=dpi,
|
|
151
|
+
facecolor="white",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
_compose_panels(
|
|
155
|
+
fig,
|
|
156
|
+
spec,
|
|
157
|
+
editor.panel_info,
|
|
158
|
+
layout_overrides,
|
|
159
|
+
fig_width_mm,
|
|
160
|
+
fig_height_mm,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
buf = io.BytesIO()
|
|
164
|
+
fig.savefig(
|
|
165
|
+
buf,
|
|
166
|
+
format=fmt if fmt != "jpg" else "jpeg",
|
|
167
|
+
dpi=dpi,
|
|
168
|
+
bbox_inches="tight",
|
|
169
|
+
facecolor="white",
|
|
170
|
+
pad_inches=0.02,
|
|
171
|
+
)
|
|
172
|
+
plt.close(fig)
|
|
173
|
+
buf.seek(0)
|
|
174
|
+
return buf
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _read_spec_and_layout(bundle_path, figure_dir, panel_info):
|
|
178
|
+
"""Read spec.json and layout.json from bundle or directory."""
|
|
179
|
+
from scitex.io import ZipBundle
|
|
180
|
+
|
|
181
|
+
spec = {}
|
|
182
|
+
layout_overrides = {}
|
|
183
|
+
|
|
184
|
+
if bundle_path:
|
|
185
|
+
try:
|
|
186
|
+
with ZipBundle(bundle_path, mode="r") as bundle:
|
|
187
|
+
spec = bundle.read_json("spec.json")
|
|
188
|
+
try:
|
|
189
|
+
layout_overrides = bundle.read_json("layout.json")
|
|
190
|
+
except:
|
|
191
|
+
pass
|
|
192
|
+
except:
|
|
193
|
+
pass
|
|
194
|
+
elif figure_dir:
|
|
195
|
+
spec_path = Path(figure_dir) / "spec.json"
|
|
196
|
+
if spec_path.exists():
|
|
197
|
+
with open(spec_path) as f:
|
|
198
|
+
spec = json_module.load(f)
|
|
199
|
+
layout_path = Path(figure_dir) / "layout.json"
|
|
200
|
+
if layout_path.exists():
|
|
201
|
+
with open(layout_path) as f:
|
|
202
|
+
layout_overrides = json_module.load(f)
|
|
203
|
+
|
|
204
|
+
# In-memory layout overrides take precedence
|
|
205
|
+
if panel_info and panel_info.get("layout"):
|
|
206
|
+
layout_overrides = panel_info.get("layout", {})
|
|
207
|
+
|
|
208
|
+
return spec, layout_overrides
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _get_figure_dimensions(spec):
|
|
212
|
+
"""Extract figure dimensions from spec."""
|
|
213
|
+
fig_width_mm = 180
|
|
214
|
+
fig_height_mm = 120
|
|
215
|
+
|
|
216
|
+
if "figure" in spec:
|
|
217
|
+
fig_info = spec.get("figure", {})
|
|
218
|
+
styles = fig_info.get("styles", {})
|
|
219
|
+
size = styles.get("size", {})
|
|
220
|
+
fig_width_mm = size.get("width_mm", 180)
|
|
221
|
+
fig_height_mm = size.get("height_mm", 120)
|
|
222
|
+
|
|
223
|
+
return fig_width_mm, fig_height_mm
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _compose_panels(
|
|
227
|
+
fig, spec, panel_info, layout_overrides, fig_width_mm, fig_height_mm
|
|
228
|
+
):
|
|
229
|
+
"""Compose panels onto the figure."""
|
|
230
|
+
import zipfile
|
|
231
|
+
|
|
232
|
+
import numpy as np
|
|
233
|
+
from PIL import Image
|
|
234
|
+
|
|
235
|
+
from scitex.io import ZipBundle
|
|
236
|
+
|
|
237
|
+
panels_spec = spec.get("panels", [])
|
|
238
|
+
panel_paths = panel_info.get("panel_paths", [])
|
|
239
|
+
panel_is_zip = panel_info.get("panel_is_zip", [])
|
|
240
|
+
|
|
241
|
+
exclude_patterns = ["hitmap", "overview", "thumb", "preview"]
|
|
242
|
+
|
|
243
|
+
for panel_spec in panels_spec:
|
|
244
|
+
panel_id = panel_spec.get("id", "")
|
|
245
|
+
pos = panel_spec.get("position", {})
|
|
246
|
+
size = panel_spec.get("size", {})
|
|
247
|
+
|
|
248
|
+
# Skip auxiliary panels
|
|
249
|
+
panel_id_lower = panel_id.lower()
|
|
250
|
+
if any(
|
|
251
|
+
skip in panel_id_lower for skip in ["overview", "thumb", "preview", "aux"]
|
|
252
|
+
):
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Find panel path
|
|
256
|
+
panel_path, panel_name, is_zip = _find_panel_path(
|
|
257
|
+
panel_id, panel_paths, panel_is_zip
|
|
258
|
+
)
|
|
259
|
+
if not panel_path:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Get layout override
|
|
263
|
+
override = layout_overrides.get(panel_name, {})
|
|
264
|
+
override_pos = override.get("position", {})
|
|
265
|
+
override_size = override.get("size", {})
|
|
266
|
+
|
|
267
|
+
x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
|
|
268
|
+
y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
|
|
269
|
+
w_mm = override_size.get("width_mm", size.get("width_mm", 60))
|
|
270
|
+
h_mm = override_size.get("height_mm", size.get("height_mm", 40))
|
|
271
|
+
|
|
272
|
+
x_frac = x_mm / fig_width_mm
|
|
273
|
+
y_frac = 1 - (y_mm + h_mm) / fig_height_mm
|
|
274
|
+
w_frac = w_mm / fig_width_mm
|
|
275
|
+
h_frac = h_mm / fig_height_mm
|
|
276
|
+
|
|
277
|
+
# Load and place panel image
|
|
278
|
+
try:
|
|
279
|
+
if is_zip:
|
|
280
|
+
with ZipBundle(panel_path, mode="r") as plot_bundle:
|
|
281
|
+
with zipfile.ZipFile(panel_path, "r") as zf:
|
|
282
|
+
png_files = [
|
|
283
|
+
n
|
|
284
|
+
for n in zf.namelist()
|
|
285
|
+
if n.endswith(".png")
|
|
286
|
+
and "exports/" in n
|
|
287
|
+
and not any(p in n.lower() for p in exclude_patterns)
|
|
288
|
+
]
|
|
289
|
+
if png_files:
|
|
290
|
+
preview_path = png_files[0]
|
|
291
|
+
if ".plot/" in preview_path:
|
|
292
|
+
preview_path = preview_path.split(".plot/")[-1]
|
|
293
|
+
img_data = plot_bundle.read_bytes(preview_path)
|
|
294
|
+
img = Image.open(io.BytesIO(img_data))
|
|
295
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
296
|
+
ax.imshow(np.array(img))
|
|
297
|
+
ax.axis("off")
|
|
298
|
+
else:
|
|
299
|
+
plot_dir = Path(panel_path)
|
|
300
|
+
exports_dir = plot_dir / "exports"
|
|
301
|
+
if exports_dir.exists():
|
|
302
|
+
for png_file in exports_dir.glob("*.png"):
|
|
303
|
+
if not any(
|
|
304
|
+
p in png_file.name.lower() for p in exclude_patterns
|
|
305
|
+
):
|
|
306
|
+
img = Image.open(png_file)
|
|
307
|
+
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
308
|
+
ax.imshow(np.array(img))
|
|
309
|
+
ax.axis("off")
|
|
310
|
+
break
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Could not load panel {panel_id}: {e}")
|
|
313
|
+
|
|
314
|
+
# Draw panel letter
|
|
315
|
+
if panel_id and len(panel_id) <= 2:
|
|
316
|
+
letter_x = x_frac + 0.01
|
|
317
|
+
letter_y = y_frac + h_frac - 0.02
|
|
318
|
+
fig.text(
|
|
319
|
+
letter_x,
|
|
320
|
+
letter_y,
|
|
321
|
+
panel_id,
|
|
322
|
+
fontsize=14,
|
|
323
|
+
fontweight="bold",
|
|
324
|
+
color="black",
|
|
325
|
+
ha="left",
|
|
326
|
+
va="top",
|
|
327
|
+
transform=fig.transFigure,
|
|
328
|
+
bbox=dict(
|
|
329
|
+
boxstyle="square,pad=0.1",
|
|
330
|
+
facecolor="white",
|
|
331
|
+
edgecolor="none",
|
|
332
|
+
alpha=0.8,
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _find_panel_path(panel_id, panel_paths, panel_is_zip):
|
|
338
|
+
"""Find panel path matching the panel ID."""
|
|
339
|
+
for idx, pp in enumerate(panel_paths):
|
|
340
|
+
pp_name = Path(pp).stem.replace(".plot", "")
|
|
341
|
+
if (
|
|
342
|
+
pp_name == panel_id
|
|
343
|
+
or pp_name.startswith(f"panel_{panel_id}_")
|
|
344
|
+
or pp_name == f"panel_{panel_id}"
|
|
345
|
+
or f"_{panel_id}_" in pp_name
|
|
346
|
+
):
|
|
347
|
+
panel_name = Path(pp).name
|
|
348
|
+
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
|
|
349
|
+
return pp, panel_name, is_zip
|
|
350
|
+
return None, None, False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# EOF
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-01-24 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_routes_basic.py
|
|
4
|
+
|
|
5
|
+
"""Basic Flask routes for the editor."""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .._core import WebEditor
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"create_index_route",
|
|
16
|
+
"create_preview_route",
|
|
17
|
+
"create_hitmap_route",
|
|
18
|
+
"create_colormap_route",
|
|
19
|
+
"create_update_route",
|
|
20
|
+
"create_stats_route",
|
|
21
|
+
"create_shutdown_route",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_index_route(app, editor: "WebEditor"):
|
|
26
|
+
"""Create the index route."""
|
|
27
|
+
from flask import render_template_string
|
|
28
|
+
|
|
29
|
+
from ..templates import build_html_template
|
|
30
|
+
|
|
31
|
+
@app.route("/")
|
|
32
|
+
def index():
|
|
33
|
+
html_template = build_html_template()
|
|
34
|
+
json_path_str = str(editor.json_path.resolve())
|
|
35
|
+
figure_path = ""
|
|
36
|
+
panel_path = ""
|
|
37
|
+
|
|
38
|
+
if ".figure/" in json_path_str:
|
|
39
|
+
parts = json_path_str.split(".figure/")
|
|
40
|
+
figure_path = parts[0] + ".figure"
|
|
41
|
+
panel_path = parts[1] if len(parts) > 1 else ""
|
|
42
|
+
elif ".plot/" in json_path_str:
|
|
43
|
+
parts = json_path_str.split(".plot/")
|
|
44
|
+
figure_path = parts[0] + ".plot"
|
|
45
|
+
panel_path = parts[1] if len(parts) > 1 else ""
|
|
46
|
+
else:
|
|
47
|
+
figure_path = json_path_str
|
|
48
|
+
|
|
49
|
+
return render_template_string(
|
|
50
|
+
html_template,
|
|
51
|
+
filename=figure_path,
|
|
52
|
+
panel_path=panel_path,
|
|
53
|
+
overrides=json.dumps(editor.current_overrides),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return index
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_preview_route(app, editor: "WebEditor"):
|
|
60
|
+
"""Create the preview route."""
|
|
61
|
+
from flask import jsonify, request
|
|
62
|
+
|
|
63
|
+
from .._renderer import render_preview_with_bboxes
|
|
64
|
+
|
|
65
|
+
@app.route("/preview")
|
|
66
|
+
def preview():
|
|
67
|
+
dark_mode = request.args.get("dark_mode", "false").lower() == "true"
|
|
68
|
+
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
69
|
+
editor.csv_data,
|
|
70
|
+
editor.current_overrides,
|
|
71
|
+
metadata=editor.metadata,
|
|
72
|
+
dark_mode=dark_mode,
|
|
73
|
+
)
|
|
74
|
+
return jsonify(
|
|
75
|
+
{
|
|
76
|
+
"image": img_data,
|
|
77
|
+
"bboxes": bboxes,
|
|
78
|
+
"img_size": img_size,
|
|
79
|
+
"has_hitmap": editor.hitmap_path is not None
|
|
80
|
+
and editor.hitmap_path.exists(),
|
|
81
|
+
"format": "png",
|
|
82
|
+
"panel_info": editor.panel_info,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return preview
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_hitmap_route(app, editor: "WebEditor"):
|
|
90
|
+
"""Create the hitmap route."""
|
|
91
|
+
from flask import jsonify
|
|
92
|
+
|
|
93
|
+
@app.route("/hitmap")
|
|
94
|
+
def hitmap():
|
|
95
|
+
if editor.hitmap_path and editor.hitmap_path.exists():
|
|
96
|
+
with open(editor.hitmap_path, "rb") as f:
|
|
97
|
+
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
98
|
+
return jsonify(
|
|
99
|
+
{
|
|
100
|
+
"image": img_data,
|
|
101
|
+
"color_map": editor.color_map,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
return jsonify({"error": "No hitmap available"}), 404
|
|
105
|
+
|
|
106
|
+
return hitmap
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_colormap_route(app, editor: "WebEditor"):
|
|
110
|
+
"""Create the color_map route."""
|
|
111
|
+
from flask import jsonify
|
|
112
|
+
|
|
113
|
+
@app.route("/color_map")
|
|
114
|
+
def color_map():
|
|
115
|
+
return jsonify(
|
|
116
|
+
{
|
|
117
|
+
"color_map": editor.color_map,
|
|
118
|
+
"hit_regions": editor.hit_regions,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return color_map
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_update_route(app, editor: "WebEditor"):
|
|
126
|
+
"""Create the update route."""
|
|
127
|
+
from flask import jsonify, request
|
|
128
|
+
|
|
129
|
+
from .._renderer import render_preview_with_bboxes
|
|
130
|
+
|
|
131
|
+
@app.route("/update", methods=["POST"])
|
|
132
|
+
def update():
|
|
133
|
+
data = request.json
|
|
134
|
+
editor.current_overrides.update(data.get("overrides", {}))
|
|
135
|
+
editor._user_modified = True
|
|
136
|
+
dark_mode = data.get("dark_mode", False)
|
|
137
|
+
|
|
138
|
+
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
139
|
+
editor.csv_data,
|
|
140
|
+
editor.current_overrides,
|
|
141
|
+
metadata=editor.metadata,
|
|
142
|
+
dark_mode=dark_mode,
|
|
143
|
+
)
|
|
144
|
+
return jsonify(
|
|
145
|
+
{
|
|
146
|
+
"image": img_data,
|
|
147
|
+
"bboxes": bboxes,
|
|
148
|
+
"img_size": img_size,
|
|
149
|
+
"status": "updated",
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return update
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_stats_route(app, editor: "WebEditor"):
|
|
157
|
+
"""Create the stats route."""
|
|
158
|
+
from flask import jsonify
|
|
159
|
+
|
|
160
|
+
@app.route("/stats")
|
|
161
|
+
def stats():
|
|
162
|
+
stats_data = editor.metadata.get("stats", [])
|
|
163
|
+
stats_summary = editor.metadata.get("stats_summary", None)
|
|
164
|
+
return jsonify(
|
|
165
|
+
{
|
|
166
|
+
"stats": stats_data,
|
|
167
|
+
"stats_summary": stats_summary,
|
|
168
|
+
"has_stats": len(stats_data) > 0,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return stats
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def create_shutdown_route(app, editor: "WebEditor"):
|
|
176
|
+
"""Create the shutdown route."""
|
|
177
|
+
from flask import jsonify, request
|
|
178
|
+
|
|
179
|
+
@app.route("/shutdown", methods=["POST"])
|
|
180
|
+
def shutdown():
|
|
181
|
+
func = request.environ.get("werkzeug.server.shutdown")
|
|
182
|
+
if func is None:
|
|
183
|
+
raise RuntimeError("Not running with Werkzeug Server")
|
|
184
|
+
func()
|
|
185
|
+
return jsonify({"status": "shutdown"})
|
|
186
|
+
|
|
187
|
+
return shutdown
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# EOF
|