carpelan commited on
Commit
c3d2674
·
2 Parent(s): e176209 c115883

Merge remote-tracking branch 'origin/main'

Browse files
.gitignore CHANGED
@@ -2,6 +2,10 @@ venv/
2
  .vscode/
3
  .cache/
4
  outputs/
 
 
 
 
5
 
6
  # Byte-compiled / optimized / DLL files
7
  */__pycache__
 
2
  .vscode/
3
  .cache/
4
  outputs/
5
+ alto-outputs/
6
+ page-outputs/
7
+ text-outputs/
8
+ tmp/
9
 
10
  # Byte-compiled / optimized / DLL files
11
  */__pycache__
app/main.py CHANGED
@@ -1,20 +1,39 @@
1
  import gradio as gr
2
-
3
  from app.gradio_config import css, theme
4
- from app.tabs.submit import submit, custom_template_yaml
5
- from app.tabs.examples_tab import examples
 
 
 
 
 
6
  from app.tabs.templating import (
7
  templating_block,
8
  TEMPLATE_IMAGE_FOLDER,
9
  TEMPLATE_YAML_FOLDER,
10
  template_output_yaml_code,
11
  )
12
- from app.utils.md_helper import load_markdown
 
13
 
14
  gr.set_static_paths(paths=[TEMPLATE_IMAGE_FOLDER])
15
  gr.set_static_paths(paths=[TEMPLATE_YAML_FOLDER])
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  with gr.Blocks(title="HTRflow", theme=theme, css=css) as demo:
19
  with gr.Row():
20
  with gr.Column(scale=1):
@@ -31,8 +50,8 @@ with gr.Blocks(title="HTRflow", theme=theme, css=css) as demo:
31
  with gr.Tab(label="Submit Job") as tab_submit:
32
  submit.render()
33
 
34
- with gr.Tab(label="Output & Visualize") as tab_examples:
35
- examples.render()
36
 
37
  @demo.load(
38
  inputs=[template_output_yaml_code],
@@ -41,20 +60,41 @@ with gr.Blocks(title="HTRflow", theme=theme, css=css) as demo:
41
  def inital_yaml_code(template_output_yaml_code):
42
  return template_output_yaml_code
43
 
44
- def sync_yaml_state(input_value, state_value):
 
 
 
 
45
  """Synchronize the YAML state if there is a mismatch."""
46
  return input_value if input_value != state_value else gr.skip()
47
 
 
 
 
 
 
 
 
 
 
 
 
48
  tab_submit.select(
49
  inputs=[template_output_yaml_code, custom_template_yaml],
50
  outputs=[custom_template_yaml],
51
- fn=sync_yaml_state,
52
  )
53
 
54
- tab_templating.select(
55
- inputs=[custom_template_yaml, template_output_yaml_code],
56
- outputs=[template_output_yaml_code],
57
- fn=sync_yaml_state,
 
 
 
 
 
 
58
  )
59
 
60
 
@@ -63,7 +103,7 @@ demo.queue()
63
  if __name__ == "__main__":
64
  demo.launch(
65
  server_name="0.0.0.0",
66
- server_port=7862,
67
  enable_monitoring=False,
68
- show_error=True,
69
  )
 
1
  import gradio as gr
2
+ import os
3
  from app.gradio_config import css, theme
4
+ from app.tabs.submit import (
5
+ submit,
6
+ custom_template_yaml,
7
+ collection_submit_state,
8
+ batch_image_gallery,
9
+ )
10
+ from app.tabs.visualizer import visualizer, collection_viz_state, viz_image_gallery
11
  from app.tabs.templating import (
12
  templating_block,
13
  TEMPLATE_IMAGE_FOLDER,
14
  TEMPLATE_YAML_FOLDER,
15
  template_output_yaml_code,
16
  )
17
+
18
+ from htrflow.models.huggingface.trocr import TrOCR
19
 
20
  gr.set_static_paths(paths=[TEMPLATE_IMAGE_FOLDER])
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
  with gr.Blocks(title="HTRflow", theme=theme, css=css) as demo:
38
  with gr.Row():
39
  with gr.Column(scale=1):
 
50
  with gr.Tab(label="Submit Job") as tab_submit:
51
  submit.render()
52
 
53
+ with gr.Tab(label="Visualize Result") as tab_visualizer:
54
+ visualizer.render()
55
 
56
  @demo.load(
57
  inputs=[template_output_yaml_code],
 
60
  def inital_yaml_code(template_output_yaml_code):
61
  return template_output_yaml_code
62
 
63
+ @demo.load()
64
+ def inital_trocr_load():
65
+ return TrOCR("Riksarkivet/trocr-base-handwritten-hist-swe-2")
66
+
67
+ def sync_gradio_objects(input_value, state_value):
68
  """Synchronize the YAML state if there is a mismatch."""
69
  return input_value if input_value != state_value else gr.skip()
70
 
71
+ def sync_gradio_object_state(input_value, state_value):
72
+ """Synchronize the Collection."""
73
+ state_value = input_value
74
+ return state_value if state_value is not None else gr.skip()
75
+
76
+ tab_templating.select(
77
+ inputs=[custom_template_yaml, template_output_yaml_code],
78
+ outputs=[template_output_yaml_code],
79
+ fn=sync_gradio_objects,
80
+ )
81
+
82
  tab_submit.select(
83
  inputs=[template_output_yaml_code, custom_template_yaml],
84
  outputs=[custom_template_yaml],
85
+ fn=sync_gradio_objects,
86
  )
87
 
88
+ # tab_visualizer.select(
89
+ # inputs=[batch_image_gallery, viz_image_gallery],
90
+ # outputs=[viz_image_gallery],
91
+ # fn=sync_gradio_objects,
92
+ # )
93
+
94
+ tab_visualizer.select(
95
+ inputs=[collection_submit_state, collection_viz_state],
96
+ outputs=[collection_viz_state],
97
+ fn=sync_gradio_object_state,
98
  )
99
 
100
 
 
103
  if __name__ == "__main__":
104
  demo.launch(
105
  server_name="0.0.0.0",
106
+ server_port=7864,
107
  enable_monitoring=False,
108
+ # show_error=True,
109
  )
app/tabs/examples_tab.py DELETED
@@ -1,89 +0,0 @@
1
- import gradio as gr
2
- from gradio_modal import Modal
3
- import pandas as pd
4
-
5
-
6
- output_image_placehholder = gr.Image(
7
- label="Output image",
8
- height=400,
9
- show_share_button=True,
10
- )
11
- markdown_selected_option = gr.Markdown(value=" ", container=True)
12
-
13
-
14
- def htr_image_placehholder(txt, method, image):
15
- needs_yaml_to_forward_tohtrflow_ = """steps:
16
- """
17
- print(method)
18
-
19
- return txt, method, image
20
-
21
-
22
- with gr.Blocks() as examples:
23
- with gr.Row(variant="panel"):
24
- with gr.Column():
25
- example_text_input_placeholder = gr.Markdown(visible=False, container=False)
26
- example_method_input_placeholder = gr.Markdown(
27
- visible=False, container=False
28
- )
29
- example_text_output_placeholder = gr.Markdown(
30
- visible=False, container=False
31
- )
32
-
33
- user_image_input = gr.Image(visible=False, container=False)
34
-
35
- # gr.Examples( # TODO: add so example has a alto xml file on hf datasets
36
- # fn=htr_image_placehholder,
37
- # examples=images_for_demo.examples_list,
38
- # inputs=[
39
- # example_text_input_placeholder,
40
- # example_method_input_placeholder,
41
- # user_image_input,
42
- # ],
43
- # outputs=[example_text_output_placeholder, markdown_selected_option, output_image_placehholder],
44
- # cache_examples=True,
45
- # cache_mode="eager",
46
- # label="Example images",
47
- # examples_per_page=6,
48
- # )
49
-
50
- with gr.Column():
51
- with gr.Tabs():
52
- with gr.Tab("Viewer"):
53
- with gr.Group():
54
- with gr.Row():
55
- output_image_placehholder.render()
56
- with gr.Row():
57
- markdown_selected_option.render()
58
- show_output_modal = gr.Button(
59
- "Show output", variant="secondary", scale=0
60
- )
61
- with gr.Row():
62
- output_dataframe_pipeline = gr.Textbox(
63
- label="Click text", info="click on image bla bla.."
64
- )
65
- with gr.Tab("YAML, DF, Graph?") as htrflow_output_table_tab:
66
- with gr.Group():
67
- with gr.Row():
68
- output_dataframe_pipeline = gr.Image(
69
- label="Output image", interactive=False, height="100"
70
- )
71
- with gr.Row():
72
- output_dataframe_pipeline = gr.Dataframe(
73
- label="Output image", col_count=2
74
- )
75
-
76
- with Modal(visible=False) as output_modal:
77
- output_files_pipeline = gr.Files(
78
- label="Output files", height=100, visible=True, scale=0
79
- )
80
- output_yaml_code = gr.Code(
81
- language="yaml",
82
- label="yaml",
83
- interactive=True,
84
- visible=True,
85
- scale=0,
86
- )
87
- # TODO: yaml should parse from datasets.. and show the yaml code
88
-
89
- show_output_modal.click(lambda: Modal(visible=True), None, output_modal)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/tabs/submit.py CHANGED
@@ -1,14 +1,152 @@
 
1
  import time
 
2
  import gradio as gr
3
  from htrflow.pipeline.pipeline import Pipeline
 
 
 
 
4
  from htrflow.pipeline.steps import auto_import
5
  import yaml
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- MAX_IMAGES = 5 # Maximum allowed images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
  with gr.Blocks() as submit:
 
 
12
  with gr.Column(variant="panel"):
13
  with gr.Group():
14
  with gr.Row():
@@ -20,6 +158,7 @@ with gr.Blocks() as submit:
20
  height=400,
21
  object_fit="cover",
22
  columns=5,
 
23
  )
24
 
25
  with gr.Column(scale=1):
@@ -30,7 +169,9 @@ with gr.Blocks() as submit:
30
  interactive=True,
31
  )
32
  with gr.Row():
33
- run_button = gr.Button("Submit", variant="primary", scale=0, min_width=160)
 
 
34
 
35
  @batch_image_gallery.upload(
36
  inputs=batch_image_gallery,
@@ -42,38 +183,14 @@ with gr.Blocks() as submit:
42
  return gr.update(value=None)
43
  return images
44
 
45
- def my_function(custom_template_yaml, batch_image_gallery, progress=gr.Progress()):
46
- config = yaml.safe_load(custom_template_yaml)
47
-
48
- image, _ = batch_image_gallery[0]
49
-
50
- pipe = Pipeline.from_config(config)
51
- print(batch_image_gallery)
52
- collections = auto_import(image)
53
-
54
- label = "HTRflow demo"
55
-
56
- for collection in collections:
57
- if "labels" in config:
58
- collection.set_label_format(**config["labels"])
59
- if label:
60
- collection.label = label
61
- collection = pipe.run(collection)
62
-
63
- # progress(0, desc="Starting...")
64
- # time.sleep(1)
65
- # for i in progress.tqdm(range(100)):
66
- # if i == 20:
67
- # gr.Info("hej morgan")
68
- # if i == 50:
69
- # gr.Info("hej morgan2")
70
- # time.sleep(0.1)
71
- # gr.Info("hej morgan nu är jag klar")
72
- print(collection)
73
- return gr.skip()
74
-
75
  run_button.click(
76
- fn=my_function,
 
 
 
77
  inputs=[custom_template_yaml, batch_image_gallery],
78
- outputs=batch_image_gallery,
 
 
 
79
  )
 
1
+ import glob
2
  import time
3
+ import uuid
4
  import gradio as gr
5
  from htrflow.pipeline.pipeline import Pipeline
6
+ from htrflow.pipeline.steps import init_step
7
+ import os
8
+ from htrflow.volume.volume import Collection
9
+
10
  from htrflow.pipeline.steps import auto_import
11
  import yaml
12
 
13
+ MAX_IMAGES = int(os.environ.get("MAX_IMAGES", 5)) # env: Maximum allowed images
14
+
15
+
16
+ class PipelineWithProgress(Pipeline):
17
+ @classmethod
18
+ def from_config(cls, config: dict[str, str]):
19
+ """Init pipeline from config, ensuring the correct subclass is instantiated."""
20
+ return cls([init_step(step["step"], step.get("settings", {})) for step in config["steps"]])
21
+
22
+ def run(self, collection, start=0, progress=None):
23
+ """
24
+ Run pipeline on collection with Gradio progress support.
25
+ If progress is provided, it updates the Gradio progress bar during execution.
26
+ """
27
+ total_steps = len(self.steps[start:])
28
+ for i, step in enumerate(self.steps[start:]):
29
+ step_name = f"{step} (step {start + i + 1} / {total_steps})"
30
+
31
+ try:
32
+ progress((i + 1) / total_steps, desc=f"Running {step_name}")
33
+ collection = step.run(collection)
34
+
35
+ except Exception:
36
+ if self.pickle_path:
37
+ gr.Error(
38
+ f"HTRflow: Pipeline failed on step {step_name}. A backup collection is saved at {self.pickle_path}"
39
+ )
40
+ else:
41
+ gr.Error(
42
+ f"HTRflow: Pipeline failed on step {step_name}",
43
+ )
44
+ raise
45
+ return collection
46
+
47
+
48
+ def rewrite_export_dests(config):
49
+ """
50
+ Rewrite the 'dest' in all 'Export' steps to include 'tmp' and a UUID.
51
+ Returns:
52
+ - A new config object with the updated 'dest' values.
53
+ - A list of all updated 'dest' paths.
54
+ """
55
+ new_config = {"steps": []}
56
+ updated_paths = []
57
+
58
+ unique_id = str(uuid.uuid4())
59
+
60
+ for step in config.get("steps", []):
61
+ new_step = step.copy()
62
+ if new_step.get("step") == "Export":
63
+ settings = new_step.get("settings", {})
64
+ if "dest" in settings:
65
+ new_dest = os.path.join("tmp", unique_id, settings["dest"])
66
+ settings["dest"] = new_dest
67
+ updated_paths.append(new_dest)
68
+ new_config["steps"].append(new_step)
69
+
70
+ return new_config, updated_paths
71
+
72
+
73
+ def run_htrflow(custom_template_yaml, batch_image_gallery, progress=gr.Progress()):
74
+ """
75
+ Executes the HTRflow pipeline based on the provided YAML configuration and batch images.
76
+ Args:
77
+ custom_template_yaml (str): YAML string specifying the HTRflow pipeline configuration.
78
+ batch_image_gallery (list): List of uploaded images to process in the pipeline.
79
+ Returns:
80
+ tuple: A collection of processed items, list of exported file paths, and a Gradio update object.
81
+ """
82
+
83
+ if custom_template_yaml is None or len(custom_template_yaml) < 1:
84
+ gr.Warning("HTRflow: Please insert a HTRflow-yaml template")
85
+ try:
86
+ config = yaml.safe_load(custom_template_yaml)
87
+ except Exception as e:
88
+ gr.Warning(f"HTRflow: Error loading YAML configuration: {e}")
89
+ return gr.skip()
90
+
91
+ temp_config, tmp_output_paths = rewrite_export_dests(config)
92
+
93
+ progress(0, desc="HTRflow: Starting")
94
+ time.sleep(0.3)
95
 
96
+ if batch_image_gallery is None:
97
+ gr.Warning("HTRflow: You must upload atleast 1 image or more")
98
+
99
+ images = [temp_img[0] for temp_img in batch_image_gallery]
100
+
101
+ pipe = PipelineWithProgress.from_config(temp_config)
102
+ collections = auto_import(images)
103
+
104
+ gr.Info(f"HTRflow: processing {len(images)} {'image' if len(images) == 1 else 'images'}.")
105
+ progress(0.1, desc="HTRflow: Processing")
106
+
107
+ for collection in collections:
108
+ if "labels" in temp_config:
109
+ collection.set_label_format(**temp_config["labels"])
110
+
111
+ collection.label = "HTRflow_demo_output"
112
+ collection: Collection = pipe.run(collection, progress=progress)
113
+
114
+ exported_files = tracking_exported_files(tmp_output_paths)
115
+
116
+ time.sleep(0.5)
117
+ progress(1, desc="HTRflow: Finish")
118
+ gr.Info("HTRflow: Finish")
119
+
120
+ yield collection, exported_files, gr.skip()
121
+
122
+
123
+ def tracking_exported_files(tmp_output_paths):
124
+ """
125
+ Look for files with specific extensions in the provided tmp_output_paths,
126
+ including subdirectories. Eliminates duplicate files.
127
+
128
+ Args:
129
+ tmp_output_paths (list): List of temporary output directories to search.
130
+
131
+ Returns:
132
+ list: Unique paths of all matching files found in the directories.
133
+ """
134
+ accepted_extensions = {".txt", ".xml", ".json"}
135
+
136
+ exported_files = set()
137
+
138
+ for tmp_folder in tmp_output_paths:
139
+ for ext in accepted_extensions:
140
+ search_pattern = os.path.join(tmp_folder, "**", f"*{ext}")
141
+ matching_files = glob.glob(search_pattern, recursive=True)
142
+ exported_files.update(matching_files)
143
+
144
+ return sorted(exported_files)
145
 
146
 
147
  with gr.Blocks() as submit:
148
+ collection_submit_state = gr.State()
149
+
150
  with gr.Column(variant="panel"):
151
  with gr.Group():
152
  with gr.Row():
 
158
  height=400,
159
  object_fit="cover",
160
  columns=5,
161
+ # preview=True,
162
  )
163
 
164
  with gr.Column(scale=1):
 
169
  interactive=True,
170
  )
171
  with gr.Row():
172
+ run_button = gr.Button("Submit", variant="primary", scale=0, min_width=200)
173
+ progess_bar = gr.Textbox(visible=False, show_label=False)
174
+ collection_output_files = gr.Files(label="Output Files", scale=0, min_width=400, visible=False)
175
 
176
  @batch_image_gallery.upload(
177
  inputs=batch_image_gallery,
 
183
  return gr.update(value=None)
184
  return images
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  run_button.click(
187
+ lambda: (gr.update(visible=True), gr.update(visible=False)),
188
+ outputs=[progess_bar, collection_output_files],
189
+ ).then(
190
+ fn=run_htrflow,
191
  inputs=[custom_template_yaml, batch_image_gallery],
192
+ outputs=[collection_submit_state, collection_output_files, progess_bar],
193
+ ).then(
194
+ lambda: (gr.update(visible=False), gr.update(visible=True)),
195
+ outputs=[progess_bar, collection_output_files],
196
  )
app/tabs/templating.py CHANGED
@@ -6,11 +6,7 @@ import re
6
  def get_sorted_files(folder, extensions):
7
  """Retrieve sorted files by numeric value in their names."""
8
  return sorted(
9
- [
10
- os.path.join(folder, file)
11
- for file in os.listdir(folder)
12
- if file.lower().endswith(extensions)
13
- ],
14
  key=lambda x: (
15
  int(re.search(r"\d+", os.path.basename(x)).group())
16
  if re.search(r"\d+", os.path.basename(x))
@@ -49,9 +45,7 @@ TEMPLATE_IMAGE_FOLDER = "app/assets/images"
49
  TEMPLATE_YAML_FOLDER = "app/assets/templates"
50
 
51
  # File Retrieval
52
- image_files = get_sorted_files(
53
- TEMPLATE_IMAGE_FOLDER, (".png", ".jpg", ".jpeg", ".webp")
54
- )
55
  yaml_files = get_sorted_files(TEMPLATE_YAML_FOLDER, (".yaml",))
56
 
57
  # Categorize YAML Files
@@ -96,11 +90,8 @@ with gr.Blocks() as templating_block:
96
  with gr.Group():
97
  with gr.Row():
98
  with gr.Column(scale=1):
99
- template_image = gr.Image(
100
- label="Example Templates", value=image_files[0], height=400
101
- )
102
  with gr.Column(scale=1):
103
-
104
  template_output_yaml_code = gr.Code(
105
  language="yaml",
106
  label="Pipeline",
@@ -108,7 +99,7 @@ with gr.Blocks() as templating_block:
108
  visible=True,
109
  )
110
  docs_link = gr.HTML(
111
- value='<p><a href="https://ai-riksarkivet.github.io/htrflow/latest/getting_started/pipeline.html#example-pipelines" target="_blank">📚 Click here 📚</a> for a detailed description on how to customize the configuration</p>',
112
  visible=True,
113
  )
114
 
@@ -121,7 +112,6 @@ with gr.Blocks() as templating_block:
121
  ],
122
  )
123
  def on_template_select(dropdown_selection_template):
124
-
125
  if dropdown_selection_template == "Simple":
126
  yaml_content = get_yaml_content(yaml_files_numbered[0])
127
  return image_files[0], yaml_content, gr.update(visible=False)
@@ -132,9 +122,7 @@ with gr.Blocks() as templating_block:
132
  yaml_content = get_yaml_content(yaml_files_c_letter[0])
133
  return image_files[2], yaml_content, gr.update(visible=True)
134
  else:
135
- return gr.Error(
136
- f"{dropdown_selection_template} - is not a valid Template selection"
137
- )
138
 
139
  @custom_dropdown_selection_template.select(
140
  inputs=custom_dropdown_selection_template,
@@ -147,9 +135,7 @@ with gr.Blocks() as templating_block:
147
  yaml_content = get_yaml_content(yaml_path)
148
  return yaml_content
149
  else:
150
- return gr.Error(
151
- f"{custom_template_selection} - is not a valid Custom Template selection"
152
- )
153
 
154
  @dropdown_selection_template.select(
155
  inputs=dropdown_selection_template,
 
6
  def get_sorted_files(folder, extensions):
7
  """Retrieve sorted files by numeric value in their names."""
8
  return sorted(
9
+ [os.path.join(folder, file) for file in os.listdir(folder) if file.lower().endswith(extensions)],
 
 
 
 
10
  key=lambda x: (
11
  int(re.search(r"\d+", os.path.basename(x)).group())
12
  if re.search(r"\d+", os.path.basename(x))
 
45
  TEMPLATE_YAML_FOLDER = "app/assets/templates"
46
 
47
  # File Retrieval
48
+ image_files = get_sorted_files(TEMPLATE_IMAGE_FOLDER, (".png", ".jpg", ".jpeg", ".webp"))
 
 
49
  yaml_files = get_sorted_files(TEMPLATE_YAML_FOLDER, (".yaml",))
50
 
51
  # Categorize YAML Files
 
90
  with gr.Group():
91
  with gr.Row():
92
  with gr.Column(scale=1):
93
+ template_image = gr.Image(label="Example Templates", value=image_files[0], height=400)
 
 
94
  with gr.Column(scale=1):
 
95
  template_output_yaml_code = gr.Code(
96
  language="yaml",
97
  label="Pipeline",
 
99
  visible=True,
100
  )
101
  docs_link = gr.HTML(
102
+ value='<p><a href="https://ai-riksarkivet.github.io/htrflow/latest/getting_started/pipeline.html#example-pipelines" target="_blank">📚 Click here 📚</a> for a detailed description on how to customize the configuration for HTRflow</p>',
103
  visible=True,
104
  )
105
 
 
112
  ],
113
  )
114
  def on_template_select(dropdown_selection_template):
 
115
  if dropdown_selection_template == "Simple":
116
  yaml_content = get_yaml_content(yaml_files_numbered[0])
117
  return image_files[0], yaml_content, gr.update(visible=False)
 
122
  yaml_content = get_yaml_content(yaml_files_c_letter[0])
123
  return image_files[2], yaml_content, gr.update(visible=True)
124
  else:
125
+ return gr.Error(f"{dropdown_selection_template} - is not a valid Template selection")
 
 
126
 
127
  @custom_dropdown_selection_template.select(
128
  inputs=custom_dropdown_selection_template,
 
135
  yaml_content = get_yaml_content(yaml_path)
136
  return yaml_content
137
  else:
138
+ return gr.Error(f"{custom_template_selection} - is not a valid Custom Template selection")
 
 
139
 
140
  @dropdown_selection_template.select(
141
  inputs=dropdown_selection_template,
app/tabs/visualizer.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ from htrflow.volume.volume import Collection
5
+ from htrflow.utils.draw import draw_polygons
6
+ from htrflow.utils import imgproc
7
+
8
+ from htrflow.results import Segment
9
+
10
+
11
+ with gr.Blocks() as visualizer:
12
+ with gr.Column(variant="panel"):
13
+ with gr.Row():
14
+ collection_viz_state = gr.State()
15
+ result_collection_viz_state = gr.State()
16
+ with gr.Column():
17
+ viz_image_gallery = gr.Gallery(
18
+ file_types=["image"],
19
+ label="Visualized Images from HTRflow",
20
+ interactive=False,
21
+ height=400,
22
+ object_fit="cover",
23
+ columns=5,
24
+ preview=True,
25
+ )
26
+
27
+ visualize_button = gr.Button(
28
+ "Visualize", scale=0, min_width=200, variant="secondary"
29
+ )
30
+
31
+ with gr.Column():
32
+ # image_visualizer_annotation = gr.Image(
33
+ # interactive=False,
34
+ # )
35
+
36
+ line2 = gr.Gallery(
37
+ interactive=False,
38
+ )
39
+ textlines = gr.Dataframe()
40
+
41
+ # @viz_image_gallery.select(outputs=image_visualizer_annotation)
42
+ # def return_image_from_gallery(evt: gr.SelectData):
43
+ # return evt.value["image"]["path"]
44
+
45
+ @visualize_button.click(
46
+ outputs=[result_collection_viz_state, viz_image_gallery, line2, textlines]
47
+ )
48
+ def testie_load_pickle():
49
+ col = Collection.from_pickle(".cache/HTRflow_demo_output.pickle")
50
+
51
+ results = []
52
+ for page_idx, page_node in enumerate(col):
53
+ page_image = page_node.image.copy()
54
+
55
+ lines = list(page_node.traverse(lambda node: node.is_line()))
56
+
57
+ recog_conf_values = {
58
+ i: list(zip(tr.texts, tr.scores)) if (tr := ln.text_result) else []
59
+ for i, ln in enumerate(lines)
60
+ }
61
+
62
+ recog_df = pd.DataFrame(
63
+ [
64
+ {"Transcription": text, "Confidence Score": f"{score:.4f}"}
65
+ for values in recog_conf_values.values()
66
+ for text, score in values
67
+ ]
68
+ )
69
+
70
+ line_polygons = []
71
+ line_crops = []
72
+ for ln in lines:
73
+ seg: Segment = ln.data.get("segment")
74
+ if not seg:
75
+ continue
76
+
77
+ cropped_line_img = imgproc.crop(page_image, seg.bbox)
78
+ cropped_line_img = np.clip(cropped_line_img, 0, 255).astype(np.uint8)
79
+ line_crops.append(cropped_line_img)
80
+
81
+ if seg.polygon is not None:
82
+ line_polygons.append(seg.polygon)
83
+
84
+ annotated_image = draw_polygons(page_image, line_polygons)
85
+ annotated_page_node = np.clip(annotated_image, 0, 255).astype(np.uint8)
86
+ results.append(
87
+ {
88
+ "page_image": page_node,
89
+ "annotated_page_node": annotated_page_node,
90
+ "line_crops": line_crops,
91
+ "recog_conf_values": recog_df,
92
+ }
93
+ )
94
+
95
+ return (
96
+ results,
97
+ [results[0]["annotated_page_node"]],
98
+ results[0]["line_crops"],
99
+ results[0]["recog_conf_values"],
100
+ )
app/utils/__init__.py DELETED
File without changes
app/utils/md_helper.py DELETED
@@ -1,14 +0,0 @@
1
- import os
2
-
3
-
4
- def load_markdown(language, section, content_dir="app/content"):
5
- """Load markdown content from files."""
6
- if language is None:
7
- file_path = os.path.join(content_dir, f"{section}.md")
8
- else:
9
- file_path = os.path.join(content_dir, language, f"{section}.md")
10
-
11
- if os.path.exists(file_path):
12
- with open(file_path, "r", encoding="utf-8") as f:
13
- return f.read()
14
- return f"## Content missing for {file_path} in {language}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/utils/yaml_helper.py DELETED
@@ -1,91 +0,0 @@
1
- from jinja2 import Environment, FileSystemLoader
2
-
3
-
4
- def get_yaml_button_fn(
5
- method,
6
- output_formats,
7
- reading_order,
8
- simple_segment_model=None,
9
- simple_htr_model=None,
10
- simple_htr_model_type=None,
11
- simple_segment_model_type=None,
12
- nested_segment_model_1=None,
13
- nested_segment_model_2=None,
14
- nested_htr_model=None,
15
- nested_segment_model_1_type=None,
16
- nested_segment_model_2_type=None,
17
- nested_htr_model_type=None,
18
- ):
19
- env = Environment(loader=FileSystemLoader("app/templates"))
20
-
21
- if output_formats is None:
22
- output_formats = ["txt"]
23
-
24
- template_name = "steps_template.yaml.j2"
25
- try:
26
- if method == "Simple layout":
27
- steps = [
28
- {
29
- "step": "Segmentation",
30
- "model": simple_segment_model_type,
31
- "model_settings": {"model": simple_segment_model},
32
- },
33
- {
34
- "step": "TextRecognition",
35
- "model": simple_htr_model_type,
36
- "model_settings": {"model": simple_htr_model},
37
- },
38
- ]
39
- elif method == "Nested segmentation":
40
- steps = [
41
- {
42
- "step": "Segmentation",
43
- "model": nested_segment_model_1_type,
44
- "model_settings": {"model": nested_segment_model_1},
45
- },
46
- {
47
- "step": "Segmentation",
48
- "model": nested_segment_model_2_type,
49
- "model_settings": {"model": nested_segment_model_2},
50
- },
51
- {
52
- "step": "TextRecognition",
53
- "model": nested_htr_model_type,
54
- "model_settings": {"model": nested_htr_model},
55
- },
56
- ]
57
- else:
58
- return "Invalid method or not yet supported."
59
-
60
- steps.append({"step": reading_order})
61
-
62
- # TODO: fix reading order
63
- # - step: ReadingOrderMarginalia
64
- # settings:
65
- # two_page: always
66
-
67
- # TODO: fix labeling format
68
- # # Sets label format to regionX_lineY_wordZ
69
- # labels:
70
- # level_labels:
71
- # - region
72
- # - line
73
- # - word
74
- # sep: _
75
- # template: "{label}{number}"
76
-
77
- steps.extend(
78
- {
79
- "step": "Export",
80
- "settings": {"format": format, "dest": f"{format}-outputs"},
81
- }
82
- for format in output_formats
83
- )
84
-
85
- template = env.get_template(template_name)
86
-
87
- yaml_value = template.render(steps=steps)
88
- return yaml_value
89
-
90
- except Exception as e:
91
- return f"Error generating YAML: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -17,13 +17,12 @@ classifiers = [
17
  requires-python = ">=3.10,<3.13"
18
 
19
  dependencies = [
20
- "htrflow==0.1.3",
21
  "gradio>=5.11.0",
22
  "datasets>=3.2.0",
23
  "pandas>=2.2.3",
24
- "jinja2>=3.1.4",
25
- "gradio-modal>=0.0.4",
26
- "fastapi[standard]>=0.115.6",
27
  ]
28
 
29
  [project.urls]
@@ -46,6 +45,9 @@ openmmlab = [
46
  "mmdet==3.0.0",
47
  "mmocr==1.0.0"
48
  ]
 
 
 
49
 
50
 
51
  [build-system]
 
17
  requires-python = ">=3.10,<3.13"
18
 
19
  dependencies = [
20
+ "htrflow==0.2.0",
21
  "gradio>=5.11.0",
22
  "datasets>=3.2.0",
23
  "pandas>=2.2.3",
24
+ "tqdm>=4.67.1",
25
+ "pillow>=11.1.0",
 
26
  ]
27
 
28
  [project.urls]
 
45
  "mmdet==3.0.0",
46
  "mmocr==1.0.0"
47
  ]
48
+ teklia = [
49
+ "pylaia==1.1.2"
50
+ ]
51
 
52
 
53
  [build-system]
uv.lock CHANGED
@@ -851,18 +851,6 @@ wheels = [
851
  { url = "https://files.pythonhosted.org/packages/a6/3d/e05202dd42581c2a1e93c730d10a0ef45bc40921332c9aa0d6645bbf0e2b/gradio_client-1.5.4-py3-none-any.whl", hash = "sha256:ad38c9a6f7fc590e822627f5bf5685321a7822b8f1a88b76d00a0621a43162d6", size = 321364 },
852
  ]
853
 
854
- [[package]]
855
- name = "gradio-modal"
856
- version = "0.0.4"
857
- source = { registry = "https://pypi.org/simple" }
858
- dependencies = [
859
- { name = "gradio" },
860
- ]
861
- sdist = { url = "https://files.pythonhosted.org/packages/e2/fd/3b383f9ee8d60625e9e26871ba4adcacbedeab132041b94290758e02e543/gradio_modal-0.0.4.tar.gz", hash = "sha256:717ae699072a171648cfa1b84bc153be84e92d04e9ad58c1bc59af68ef332726", size = 1180812 }
862
- wheels = [
863
- { url = "https://files.pythonhosted.org/packages/05/3d/76f454de84ae1dccbf2b7023e933afb8dde5fdd89e9476786726ef770737/gradio_modal-0.0.4-py3-none-any.whl", hash = "sha256:d96e817d2e934d9e1b835b06474f45fd349b5ccea499d1536bfb4bd38f62dedb", size = 1106241 },
864
- ]
865
-
866
  [[package]]
867
  name = "h11"
868
  version = "0.14.0"
@@ -905,10 +893,10 @@ dependencies = [
905
  { name = "datasets" },
906
  { name = "fastapi", extra = ["standard"] },
907
  { name = "gradio" },
908
- { name = "gradio-modal" },
909
  { name = "htrflow" },
910
- { name = "jinja2" },
911
  { name = "pandas" },
 
 
912
  ]
913
 
914
  [package.optional-dependencies]
@@ -933,15 +921,15 @@ requires-dist = [
933
  { name = "datasets", specifier = ">=3.2.0" },
934
  { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" },
935
  { name = "gradio", specifier = ">=5.11.0" },
936
- { name = "gradio-modal", specifier = ">=0.0.4" },
937
  { name = "htrflow", specifier = "==0.1.3" },
938
- { name = "jinja2", specifier = ">=3.1.4" },
939
  { name = "mmcv", marker = "extra == 'openmmlab'", specifier = "==2.0.1" },
940
  { name = "mmdet", marker = "extra == 'openmmlab'", specifier = "==3.0.0" },
941
  { name = "mmengine", marker = "extra == 'openmmlab'", specifier = "==0.7.4" },
942
  { name = "mmocr", marker = "extra == 'openmmlab'", specifier = "==1.0.0" },
943
  { name = "openmim", marker = "extra == 'openmmlab'", specifier = "==0.3.9" },
944
  { name = "pandas", specifier = ">=2.2.3" },
 
 
945
  ]
946
 
947
  [package.metadata.requires-dev]
 
851
  { url = "https://files.pythonhosted.org/packages/a6/3d/e05202dd42581c2a1e93c730d10a0ef45bc40921332c9aa0d6645bbf0e2b/gradio_client-1.5.4-py3-none-any.whl", hash = "sha256:ad38c9a6f7fc590e822627f5bf5685321a7822b8f1a88b76d00a0621a43162d6", size = 321364 },
852
  ]
853
 
 
 
 
 
 
 
 
 
 
 
 
 
854
  [[package]]
855
  name = "h11"
856
  version = "0.14.0"
 
893
  { name = "datasets" },
894
  { name = "fastapi", extra = ["standard"] },
895
  { name = "gradio" },
 
896
  { name = "htrflow" },
 
897
  { name = "pandas" },
898
+ { name = "pillow" },
899
+ { name = "tqdm" },
900
  ]
901
 
902
  [package.optional-dependencies]
 
921
  { name = "datasets", specifier = ">=3.2.0" },
922
  { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" },
923
  { name = "gradio", specifier = ">=5.11.0" },
 
924
  { name = "htrflow", specifier = "==0.1.3" },
 
925
  { name = "mmcv", marker = "extra == 'openmmlab'", specifier = "==2.0.1" },
926
  { name = "mmdet", marker = "extra == 'openmmlab'", specifier = "==3.0.0" },
927
  { name = "mmengine", marker = "extra == 'openmmlab'", specifier = "==0.7.4" },
928
  { name = "mmocr", marker = "extra == 'openmmlab'", specifier = "==1.0.0" },
929
  { name = "openmim", marker = "extra == 'openmmlab'", specifier = "==0.3.9" },
930
  { name = "pandas", specifier = ">=2.2.3" },
931
+ { name = "pillow", specifier = ">=11.1.0" },
932
+ { name = "tqdm", specifier = ">=4.67.1" },
933
  ]
934
 
935
  [package.metadata.requires-dev]