Ashwin V. Mohanan commited on
Commit
b8077d0
·
1 Parent(s): 3216119

Add first rough version

Browse files
LICENSE CHANGED
The diff for this file is too large to render. See raw diff
 
app/__init__.py ADDED
File without changes
app/content/main_sub_title.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ <a href="https://dawsonia.readthedocs.io">
2
+ <img src="https://git.smhi.se/ai-for-obs/dawsonia/-/raw/main/docs/source/_static/smhi_logo_black_8mm.png" width="17%" align="right" margin-right="200" />
3
+ </a>
app/content/main_title.md ADDED
@@ -0,0 +1 @@
 
 
1
+ <h1><center> Dawsonia 🔍 App </center></h1>
app/gradio_config.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ theme = gr.themes.Default(
4
+ primary_hue="blue",
5
+ secondary_hue="blue",
6
+ neutral_hue="slate",
7
+ # font=[
8
+ # gr.themes.GoogleFont("Open Sans"),
9
+ # "ui-sans-serif",
10
+ # "system-ui",
11
+ # "sans-serif",
12
+ # ],
13
+ )
14
+
15
+ css = """
16
+ .svg-image {
17
+ height: auto;
18
+ width: 100%;
19
+ margin: auto;
20
+ }
21
+
22
+ .transcription {
23
+ font-size: large;
24
+ position: sticky;
25
+ top: 20px;
26
+ }
27
+
28
+ .transcription-column {
29
+ height: 100vh;
30
+ }
31
+
32
+ /* this is needed in order to make the transcription sticky */
33
+ .app {
34
+ overflow: visible;
35
+ }
36
+
37
+ /* style of textline svg elements */
38
+ .textline {
39
+ fill: transparent;
40
+ stroke: blue;
41
+ stroke-width: 10;
42
+ stroke-opacity: 0.2;
43
+ }
44
+
45
+ .highlighted polygon {
46
+ fill:blue;
47
+ fill-opacity: 0.2;
48
+ }
49
+
50
+ span.highlighted {
51
+ background-color: rgba(0%, 0%, 100%, 0.2);
52
+ font-size: large;
53
+ }
54
+
55
+ hr.region-divider {
56
+ margin-top: 0.5em;
57
+ margin-bottom: 0.5em;
58
+ }
59
+
60
+ .pipeline-panel {
61
+ background: none;
62
+ border: solid 1px;
63
+ border-color: var(--block-border-color);
64
+ }
65
+
66
+ .pipeline-help {
67
+ padding: 5px 0 0 0;
68
+ font-weight: var(--block-info-text-weight);
69
+ font-size: var(--block-info-text-size);
70
+ color: var(--block-info-text-color);
71
+ }
72
+
73
+ .pipeline-info {
74
+ padding: 0 0 0 2px;
75
+ font-weight: var(--block-info-text-weight);
76
+ font-size: var(--block-info-text-size);
77
+ color: var(--block-info-text-color);
78
+ }
79
+
80
+ .pipeline-help a {
81
+ color: var(--secondary-400);
82
+ }
83
+
84
+ .pipeline-help a:hover {
85
+ color: var(--secondary-500);
86
+ }
87
+
88
+ .pipeline-header {
89
+ padding: 2px 0px 0px 2px;
90
+ color: var(--body-text-color);
91
+ }
92
+
93
+ .pipeline-description {
94
+ margin: auto;
95
+ color: var(--body-text-color);
96
+ }
97
+
98
+ .button-group-viz {
99
+ margin: auto;
100
+ display: flex;
101
+ justify-content: center;
102
+ gap: 1rem;
103
+ text-align: center;
104
+ }
105
+
106
+ .modal-block {
107
+ width: 60%;
108
+ padding: 1rem;
109
+ }
110
+
111
+ @media (max-width: 1024px) { /* mobile and standing iPads */
112
+ .modal-block {
113
+ width: 100%;
114
+ }
115
+ }
116
+
117
+
118
+ """
app/main.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+
4
+ import gradio as gr
5
+ # from htrflow.models.huggingface.trocr import TrOCR
6
+
7
+ from app.gradio_config import css, theme
8
+
9
+ # from app.tabs.export import collection as collection_export_state
10
+ # from app.tabs.export import export
11
+ from app.tabs.submit import collection_submit_state, submit
12
+
13
+ # from app.tabs.visualizer import collection as collection_viz_state
14
+ # from app.tabs.visualizer import visualizer
15
+
16
+ # Suppress transformers logging
17
+ logging.getLogger("transformers").setLevel(logging.ERROR)
18
+
19
+
20
+ TEMPLATE_YAML_FOLDER = "app/assets/templates"
21
+ gr.set_static_paths(paths=[TEMPLATE_YAML_FOLDER])
22
+
23
+
24
+ def load_markdown(language, section, content_dir="app/content"):
25
+ """Load markdown content from files."""
26
+ if language is None:
27
+ file_path = os.path.join(content_dir, f"{section}.md")
28
+ else:
29
+ file_path = os.path.join(content_dir, language, f"{section}.md")
30
+
31
+ if os.path.exists(file_path):
32
+ with open(file_path, "r", encoding="utf-8") as f:
33
+ return f.read()
34
+ return f"## Content missing for {file_path} in {language}"
35
+
36
+
37
+ def activate_tab(collection):
38
+ return gr.update(interactive=collection is not None)
39
+
40
+
41
+ html_header = ""
42
+
43
+
44
+ with gr.Blocks(title="Dawsonia Demo", theme=theme, css=css, head=html_header) as demo:
45
+ with gr.Row():
46
+ with gr.Column(scale=1):
47
+ pass
48
+ with gr.Column(scale=2):
49
+ gr.Markdown(load_markdown(None, "main_title"))
50
+ with gr.Column(scale=1):
51
+ gr.Markdown(load_markdown(None, "main_sub_title"))
52
+
53
+ with gr.Tabs(elem_classes="top-navbar") as navbar:
54
+ with gr.Tab(label="Upload") as tab_submit:
55
+ submit.render()
56
+
57
+ # with gr.Tab(label="Result", interactive=False, id="result") as tab_visualizer:
58
+ # visualizer.render()
59
+ #
60
+ # with gr.Tab(label="Export", interactive=False) as tab_export:
61
+ # export.render()
62
+
63
+ # @demo.load()
64
+ # def inital_trocr_load():
65
+ # TrOCR("Riksarkivet/trocr-base-handwritten-hist-swe-2")
66
+
67
+ def sync_gradio_object_state(input_value, state_value):
68
+ """Synchronize the Collection."""
69
+ state_value = input_value
70
+ return state_value if state_value is not None else gr.skip()
71
+
72
+ # collection_submit_state.change(
73
+ # activate_tab, collection_submit_state, tab_visualizer
74
+ # )
75
+ # collection_submit_state.change(activate_tab, collection_submit_state, tab_export)
76
+ collection_submit_state.change(lambda: gr.Tabs(selected="result"), outputs=navbar)
77
+
78
+ # tab_visualizer.select(
79
+ # inputs=[collection_submit_state, collection_viz_state],
80
+ # outputs=[collection_viz_state],
81
+ # fn=sync_gradio_object_state,
82
+ # )
83
+ #
84
+ # tab_export.select(
85
+ # inputs=[collection_submit_state, collection_export_state],
86
+ # outputs=[collection_export_state],
87
+ # fn=sync_gradio_object_state,
88
+ # )
89
+
90
+ demo.queue()
91
+
92
+ if __name__ == "__main__":
93
+ demo.launch(
94
+ server_name="0.0.0.0", server_port=7860, enable_monitoring=True, show_api=False
95
+ )
app/tabs/__init__.py ADDED
File without changes
app/tabs/submit.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import time
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from pathlib import Path
8
+ from dawsonia import io
9
+ from dawsonia import digitize
10
+ from dawsonia.ml import ml
11
+ import pooch
12
+ import gradio as gr
13
+ import yaml
14
+ from gradio_modal import Modal
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Max number of images a user can upload at once
19
+ MAX_IMAGES = int(os.environ.get("MAX_IMAGES", 5))
20
+
21
+ # Setup the cache directory to point to the directory where the example images
22
+ # are located. The images must lay in the cache directory because otherwise they
23
+ # have to be reuploaded when drag-and-dropped to the input image widget.
24
+ GRADIO_CACHE = ".gradio_cache"
25
+ DATA_CACHE = os.path.join(GRADIO_CACHE, "data")
26
+ EXAMPLES_DIRECTORY = os.path.join(os.getcwd(), "examples")
27
+
28
+ # Example books
29
+ PIPELINES: dict[str, dict[str, str]] = {
30
+ "bjuröklubb": dict(
31
+ url="https://git.smhi.se/ai-for-obs/data/-/raw/688c04f13e8e946962792fe4b4e0ded98800b154/raw_zarr/BJUR%C3%96KLUBB/DAGBOK_Bjur%C3%B6klubb_Station_Jan-Dec_1928.zarr.zip",
32
+ known_hash="sha256:6d87b7f79836ae6373cfab11260fe28787d93fe16199fefede6697ccd750f71a",
33
+ )
34
+ }
35
+
36
+ if os.environ.get("GRADIO_CACHE_DIR", GRADIO_CACHE) != GRADIO_CACHE:
37
+ os.environ["GRADIO_CACHE_DIR"] = GRADIO_CACHE
38
+ logger.warning("Setting GRADIO_CACHE_DIR to '%s' (overriding a previous value).")
39
+
40
+
41
+ def run_dawsonia(
42
+ table_fmt_config_override, batch_image_gallery, book, progress=gr.Progress()
43
+ ):
44
+ if None in (batch_image_gallery, book) or len(batch_image_gallery) == 0:
45
+ raise ValueError("You need to select / upload the pages to digitize")
46
+ progress(0, desc="Dawsonia: starting")
47
+
48
+ model_path = Path("data/models/dawsonia/2024-07-02")
49
+ output_path = Path(GRADIO_CACHE, "output")
50
+
51
+ print("Dawsonia: digitizing", book)
52
+ table_fmt = book.table_format
53
+
54
+ output_path_book = output_path / book.station_name / book._name
55
+ output_path_book.mkdir(exist_ok=True, parents=True)
56
+ (output_path_book / "probablities").mkdir(exist_ok=True)
57
+
58
+ init_data: list[dict[str, NDArray]] = [
59
+ {
60
+ key: np.empty(len(table_fmt.rows), dtype="O")
61
+ for key in table_fmt.columns[table_idx]
62
+ }
63
+ for table_idx in table_fmt.preproc.idx_tables_size_verify
64
+ ]
65
+
66
+ for page_number in range(len(batch_image_gallery)):
67
+ output_path_page = output_path_book / str(page_number)
68
+ results = [
69
+ digitize.digitize_page_and_write_output(
70
+ book,
71
+ init_data,
72
+ page_number=page_number + 3,
73
+ date_str="2022-02-02",
74
+ model_path=model_path,
75
+ model_predict=ml.model_predict,
76
+ prob_thresh=0.5,
77
+ output_path_page=output_path_page,
78
+ output_text_fmt=True,
79
+ debug=True,
80
+ )
81
+ ]
82
+
83
+ collection = []
84
+ time.sleep(1)
85
+ gr.Info("Pages were succesfully digitized ✨")
86
+
87
+ yield collection, gr.skip()
88
+
89
+
90
+ def all_example_images() -> list[str]:
91
+ """
92
+ Get paths to all example images.
93
+ """
94
+ examples = [
95
+ os.path.join(EXAMPLES_DIRECTORY, f"{pipeline}.png") for pipeline in PIPELINES
96
+ ]
97
+ return examples
98
+
99
+
100
+ def get_selected_example_image(
101
+ first_page, last_page, event: gr.SelectData
102
+ ) -> tuple[str, io.Book] | None:
103
+ """
104
+ Get the name of the pipeline that corresponds to the selected image.
105
+ """
106
+ # for name, details in PIPELINES.items():
107
+ name, _ext = event.value["image"]["orig_name"].split(".")
108
+
109
+ if name in PIPELINES:
110
+ book_path = pooch.retrieve(**PIPELINES[name], path=DATA_CACHE)
111
+ first, last, book = io.read_book(book_path)
112
+ book._name = name
113
+ book.size_cell = [1.0, 1.0, 1.0, 1.0]
114
+ return [book.read_image(pg) for pg in range(first_page, last_page)], book
115
+
116
+
117
+ table_fmt_config_override_placeholder = (
118
+ """\
119
+ [default]
120
+ version = 0
121
+
122
+ # Default values, but wrote explicitly here. See PreprocConfig class
123
+ [default.preproc]
124
+ table_modif = true
125
+ corr_rotate = true
126
+ row_idx_unit = "HOURS"
127
+ idx_tables_size_verify = [0, 1]
128
+
129
+ [version.0]
130
+ columns = [
131
+ [
132
+ "term_på_baro",
133
+ "barom",
134
+ "torra_term",
135
+ "våta_term",
136
+ "moln_slag_lägre",
137
+ "moln_mängd_lägre",
138
+ "moln_slag_medel",
139
+ "moln_slag_högre"
140
+ ],
141
+ [
142
+ "moln_het_sol_dimma_nederbörd_total",
143
+ "vind_riktning",
144
+ "vind_beaufort",
145
+ "vind_m_sek",
146
+ "sikt",
147
+ "sjögang",
148
+ "maximi_term",
149
+ "minimi_term",
150
+ "nederbörd_mängd",
151
+ "nederbörd_slag"
152
+ ]
153
+ ]
154
+ name_idx = "tid"
155
+ rows = [2, 8, 14, 19, 21]
156
+ tables = [
157
+ [5, 8],
158
+ [5, 10],
159
+ [3, 1],
160
+ [4, 2],
161
+ [4, 5]
162
+ ]
163
+ """,
164
+ )
165
+
166
+ with gr.Blocks() as submit:
167
+ gr.Markdown("# Upload")
168
+ gr.Markdown(
169
+ "Select or upload the image you want to transcribe. You can upload up to five images at a time."
170
+ )
171
+
172
+ batch_book_state = gr.State()
173
+ collection_submit_state = gr.State()
174
+
175
+ with gr.Group():
176
+ with gr.Row(equal_height=True):
177
+ with gr.Column(scale=5):
178
+ batch_image_gallery = gr.Gallery(
179
+ file_types=["image"],
180
+ label="Image to digitize",
181
+ interactive=True,
182
+ object_fit="scale-down",
183
+ scale=10,
184
+ )
185
+
186
+ with gr.Column(scale=2):
187
+ first_page = gr.Number(3, label="First page of the book", precision=0)
188
+ last_page = gr.Number(4, label="Last page of the book", precision=0)
189
+ examples = gr.Gallery(
190
+ all_example_images(),
191
+ label="Examples",
192
+ interactive=False,
193
+ allow_preview=False,
194
+ object_fit="scale-down",
195
+ min_width=250,
196
+ )
197
+
198
+ with Modal(visible=False) as edit_table_fmt_modal:
199
+ with gr.Column():
200
+ gr.Markdown(
201
+ "## Table format configuration\n"
202
+ "Write a custom table format, overriding the default one. "
203
+ "Close [x] the popup when you are done."
204
+ )
205
+ table_fmt_config_override = gr.Code("", language="python")
206
+ gr.HTML(
207
+ (
208
+ "<a href='https://dawsonia.readthedocs.io/en/latest/user_guide/misc.html#table-formats' target='_blank'>"
209
+ "Read the docs for the table-formats spec"
210
+ "</a>. "
211
+ ),
212
+ padding=False,
213
+ elem_classes="pipeline-help",
214
+ )
215
+
216
+ with gr.Row():
217
+ run_button = gr.Button("Digitize", variant="primary", scale=0, min_width=200)
218
+ edit_table_fmt_button = gr.Button(
219
+ "Edit table format", variant="secondary", scale=0, min_width=200
220
+ )
221
+
222
+ # All events interactions below
223
+
224
+ examples.select(
225
+ get_selected_example_image,
226
+ (first_page, last_page),
227
+ (batch_image_gallery, batch_book_state),
228
+ )
229
+
230
+ @batch_image_gallery.upload(
231
+ inputs=batch_image_gallery,
232
+ outputs=[batch_image_gallery],
233
+ )
234
+ def validate_images(images):
235
+ if len(images) > MAX_IMAGES:
236
+ gr.Warning(f"Maximum images you can upload is set to: {MAX_IMAGES}")
237
+ return gr.update(value=None)
238
+ return images
239
+
240
+ run_button.click(
241
+ fn=run_dawsonia,
242
+ inputs=[table_fmt_config_override, batch_image_gallery, batch_book_state],
243
+ outputs=[collection_submit_state, batch_image_gallery],
244
+ )
245
+ edit_table_fmt_button.click(lambda: Modal(visible=True), None, edit_table_fmt_modal)
pyproject.toml CHANGED
@@ -6,7 +6,8 @@ readme = "README.md"
6
  requires-python = ">=3.10"
7
  dependencies = [
8
  "dawsonia[cuda]",
9
- "htrflow>=0.2.2",
 
10
  "pyarrow>=19.0.0",
11
  "typer<0.14",
12
  ]
 
6
  requires-python = ">=3.10"
7
  dependencies = [
8
  "dawsonia[cuda]",
9
+ "gradio>=5.15.0",
10
+ "gradio-modal>=0.0.4",
11
  "pyarrow>=19.0.0",
12
  "typer<0.14",
13
  ]
uv.lock CHANGED
The diff for this file is too large to render. See raw diff