Spaces:
Running
on
Zero
Running
on
Zero
carpelan
commited on
Commit
·
60d8ae5
1
Parent(s):
c6fd70c
Made some inital changes for sprint 2
Browse files- .github/README.md +155 -55
- Makefile +0 -48
- app/assets/images/how_to_1.png +3 -0
- app/assets/images/how_to_2.png +3 -0
- app/assets/images/how_to_3.png +3 -0
- app/assets/images/how_to_4.png +3 -0
- app/assets/images/how_to_5.png +3 -0
- app/assets/images/how_to_6.png +3 -0
- app/assets/templates/c_nested_labels.yaml +0 -41
- app/assets/templates/c_nested_reading_order.yaml +0 -24
- app/assets/templates/c_nested_with_filter.yaml +0 -28
- app/assets/templates/c_simple_gensettings.yaml +0 -21
- app/assets/templates/c_simple_multi_output.yaml +0 -24
- app/assets/templates/{2_nested.yaml → nested.yaml} +1 -6
- app/assets/templates/{1_simple.yaml → simple.yaml} +1 -5
- app/content/how_it_works.md +0 -44
- app/gradio_config.py +8 -0
- app/main.py +20 -27
- app/tabs/export.py +67 -0
- app/tabs/submit.py +101 -131
- app/tabs/visualizer.py +33 -12
- pyproject.toml +1 -1
- uv.lock +0 -0
.github/README.md
CHANGED
@@ -1,95 +1,195 @@
|
|
1 |
-
#
|
2 |
|
3 |
-
|
4 |
|
5 |
-
|
6 |
|
7 |
-
We're thrilled to introduce [htrflow](https://huggingface.co/spaces/Riksarkivet/htr_demo), our demonstration platform that brings to life the process of transcribing Swedish handwritten documents from the 17th to the 19th century.
|
8 |
|
9 |
<p align="center">
|
10 |
-
<img src="https://github.
|
11 |
</p>
|
12 |
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
-
All code is open-source, all our models are on [Hugging Face](https://huggingface.co/collections/Riksarkivet/models-for-handwritten-text-recognition-652692c6871f915e766de688) and are free to use, and all data will be made available for download and use on [Hugging Face](https://huggingface.co/datasets/Riksarkivet/placeholder_htr) as well.
|
17 |
|
18 |
-
|
19 |
|
20 |
-
##
|
21 |
|
22 |
-
|
23 |
|
24 |
-
|
25 |
-
python3 -m venv .venv
|
26 |
-
source .venv/bin/activate
|
27 |
-
```
|
28 |
|
29 |
-
|
30 |
|
31 |
-
|
32 |
-
make install
|
33 |
-
```
|
34 |
|
35 |
-
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
40 |
|
41 |
-
|
42 |
|
43 |
-
|
44 |
-
gradio app.py
|
45 |
-
```
|
46 |
|
47 |
-
|
|
|
|
|
48 |
|
49 |
-
|
50 |
|
51 |
-
|
52 |
|
53 |
-
|
|
|
54 |
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
|
59 |
-
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
-
|
|
|
|
|
|
|
|
|
|
|
66 |
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
docker run -it -p 7860:7860 --platform=linux/amd64 --gpus all \
|
71 |
-
|
72 |
```
|
73 |
|
74 |
---
|
75 |
|
76 |
-
## Instructions for documentation
|
77 |
|
78 |
-
|
79 |
-
- Naming convention of file is based on subtabs
|
80 |
-
- If subtab uses columns and rows
|
81 |
-
- Use suffix such as col1, row1 or tab1, to indicate differences in postion of text.
|
82 |
|
83 |
-
|
|
|
|
|
|
|
|
|
|
|
84 |
|
85 |
-
|
86 |
-
<img src="https://github.com/Borg93/htr_gradio_file_placeholder/blob/main/layout_structure.png?raw=true" alt="Badge 1">
|
87 |
-
</p>
|
88 |
-
|
89 |
-
## Assets and file sharing with app
|
90 |
|
91 |
-
|
92 |
|
93 |
-
|
94 |
|
95 |
-
**Note**: this repo is an work in progress
|
|
|
1 |
+
# HTRflow_app
|
2 |
|
3 |
+
[HTRflow_app](https://huggingface.co/spaces/Riksarkivet/htr_demo), our interactive demo application that visualizes the entire Handwritten Text Recognition (HTR) process. With this demo, users can explore, step by step, how AI transforms historical manuscripts into digital text.
|
4 |
|
5 |
+
Please note that this is a demo application—not intended for production use—but it highlights the immense potential of HTR technology for cultural heritage institutions worldwide.
|
6 |
|
|
|
7 |
|
8 |
<p align="center">
|
9 |
+
<img src="https://ai-riksarkivet.github.io/htrflow/latest/assets/background_htrflow_2.png" alt="HTRflow App Demo" width="80%">
|
10 |
</p>
|
11 |
|
12 |
+
---
|
13 |
+
<!-- https://ecotrust-canada.github.io/markdown-toc/ -->
|
14 |
+
- [HTRflow_app](#htrflow-app)
|
15 |
+
* [Overview](#overview)
|
16 |
+
* [Guide](#guide)
|
17 |
+
* [How to use app..](#how-to-use-app)
|
18 |
+
* [Getting Started](#getting-started)
|
19 |
+
+ [Prerequisites](#prerequisites)
|
20 |
+
+ [Installation](#installation)
|
21 |
+
+ [Running the Application Locally](#running-the-application-locally)
|
22 |
+
* [Running with Docker](#running-with-docker)
|
23 |
+
+ [Locally with Docker](#locally-with-docker)
|
24 |
+
+ [On Hugging Face Spaces](#on-hugging-face-spaces)
|
25 |
+
* [Contributing](#contributing)
|
26 |
+
* [License](#license)
|
27 |
|
|
|
28 |
|
29 |
+
---
|
30 |
|
31 |
+
## Guide
|
32 |
|
33 |
+
The demo consist of 3 tabs: Upload, Results and Export. You navigate through the app by first uploading 1 or many images in
|
34 |
|
35 |
+
Upload:
|
|
|
|
|
|
|
36 |
|
37 |
+
Result:
|
38 |
|
39 |
+
Export:
|
|
|
|
|
40 |
|
41 |
+
## Pipeline Configuration
|
42 |
|
43 |
+
HTRflow powers the application's engine with a structured pipeline design pattern. This pattern uses declarative YAML schemas as blueprints to define step-by-step processing instructions. For detailed documentation, visit the [HTRflow Pipeline Guide](https://ai-riksarkivet.github.io/htrflow/latest/getting_started/pipeline.html#yaml).
|
44 |
+
|
45 |
+
<p align="center">
|
46 |
+
<img src="../app/assets/images/3_worker.png" alt="HTRflow Worker Pipeline" width="20%">
|
47 |
+
</p>
|
48 |
|
49 |
+
### Understanding YAML Pipeline Templates
|
50 |
|
51 |
+
The following series of images demonstrates how YAML pipeline templates function. Each template is designed for specific document types - the example below shows a template optimized for single-column running text, such as letters, notes, and individual pages.
|
|
|
|
|
52 |
|
53 |
+
<p align="center">
|
54 |
+
<img src="../app/assets/images/how_to_1.png" alt="YAML Template Structure" width="70%">
|
55 |
+
</p>
|
56 |
|
57 |
+
### Pipeline Steps
|
58 |
|
59 |
+
Each pipeline consists of sequential steps executed from top to bottom. In this example, we focus on two primary steps:
|
60 |
|
61 |
+
1. **Segmentation**: Identifies and extracts text lines from the image
|
62 |
+
2. **Text Recognition**: Performs Handwritten Text Recognition (HTR) on the segmented lines
|
63 |
|
64 |
+
<p align="center">
|
65 |
+
<img src="../app/assets/images/how_to_2.png" alt="Pipeline Steps Overview" width="50%">
|
66 |
+
</p>
|
67 |
|
68 |
+
### Model Integration
|
69 |
|
70 |
+
Models specified in the pipeline can be downloaded directly from the [Huggingface model hub](https://huggingface.co/models?library=htrflow). For a comprehensive list of supported models, refer to the [HTRflow Models Documentation](https://ai-riksarkivet.github.io/htrflow/latest/getting_started/models.html#models).
|
71 |
+
|
72 |
+
> **Note**: For English text recognition, you'll need to specify an appropriate model ID, such as the [Microsoft TrOCR base handwritten model](https://huggingface.co/microsoft/trocr-base-handwritten).
|
73 |
+
|
74 |
+
<p align="center">
|
75 |
+
<img src="../app/assets/images/how_to_3.png" alt="Model Configuration" width="50%">
|
76 |
+
</p>
|
77 |
+
|
78 |
+
### Processing Workflow
|
79 |
+
|
80 |
+
#### Text Line Detection
|
81 |
+
The following image illustrates the text line segmentation process:
|
82 |
|
83 |
+
<p align="center">
|
84 |
+
<img src="../app/assets/images/how_to_4.png" alt="Text Line Detection Process" width="90%">
|
85 |
+
</p>
|
86 |
+
|
87 |
+
#### Text Recognition
|
88 |
+
After segmentation, the detected text lines are processed by the HTR component:
|
89 |
|
90 |
+
<p align="center">
|
91 |
+
<img src="../app/assets/images/how_to_5.png" alt="Text Recognition Process" width="80%">
|
92 |
+
</p>
|
93 |
+
|
94 |
+
#### Reading Order Determination
|
95 |
+
The final pipeline step determines the reading order of the text. In this example, it applies a simple top-down ordering transformation:
|
96 |
+
|
97 |
+
<p align="center">
|
98 |
+
<img src="../app/assets/images/how_to_6.png" alt="Reading Order Determination" width="85%">
|
99 |
+
</p>
|
100 |
|
101 |
+
|
102 |
+
|
103 |
+
|
104 |
+
|
105 |
+
## Development
|
106 |
+
|
107 |
+
### Prerequisites
|
108 |
+
|
109 |
+
- **Python:** Version 3.7 or higher
|
110 |
+
- **pip:** Python package installer
|
111 |
+
- **(Optional) Docker:** For containerized deployment
|
112 |
+
- **(Optional) Nvidia GPU:** For faster predictions..
|
113 |
+
|
114 |
+
|
115 |
+
### Installation
|
116 |
+
|
117 |
+
1. **Clone the Repository:**
|
118 |
+
|
119 |
+
```bash
|
120 |
+
git clone https://github.com/your_username/htrflow_app.git
|
121 |
+
cd htrflow_app
|
122 |
+
```
|
123 |
+
|
124 |
+
2. **Set Up a Virtual Environment:**
|
125 |
+
|
126 |
+
```bash
|
127 |
+
python3 -m venv .venv
|
128 |
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
129 |
+
```
|
130 |
+
|
131 |
+
3. **Install Dependencies:**
|
132 |
+
|
133 |
+
Since we are no longer using a Makefile, install the required packages with:
|
134 |
+
|
135 |
+
```bash
|
136 |
+
pip install -r requirements.txt
|
137 |
+
```
|
138 |
+
|
139 |
+
### Running the Application Locally
|
140 |
+
|
141 |
+
Launch the Gradio demo by running:
|
142 |
+
|
143 |
+
```bash
|
144 |
+
gradio app/main.py
|
145 |
```
|
146 |
+
|
147 |
+
Then open your web browser and navigate to `http://localhost:7860` (or the address displayed in your terminal) to interact with the demo.
|
148 |
+
|
149 |
+
---
|
150 |
+
|
151 |
+
## Running with Docker
|
152 |
+
|
153 |
+
### Locally with Docker
|
154 |
+
|
155 |
+
1. **Build the Docker Image:**
|
156 |
+
|
157 |
+
```bash
|
158 |
+
docker build --tag htrflow/htrflow-app .
|
159 |
+
```
|
160 |
+
|
161 |
+
2. **Run the Docker Container:**
|
162 |
+
|
163 |
+
```bash
|
164 |
+
docker run -it -d --name htrflow-app -p 7000:7860 htrflow/htrflow-app:latest
|
165 |
+
```
|
166 |
+
|
167 |
+
Now, visit `http://localhost:7000` in your browser.
|
168 |
+
|
169 |
+
### On Hugging Face Spaces
|
170 |
+
|
171 |
+
Alternatively, you can run HTRflow_app directly on Hugging Face with:
|
172 |
+
|
173 |
+
```bash
|
174 |
docker run -it -p 7860:7860 --platform=linux/amd64 --gpus all \
|
175 |
+
-e registry.hf.space/riksarkivet-htr-demo:latest
|
176 |
```
|
177 |
|
178 |
---
|
179 |
|
|
|
180 |
|
181 |
+
## Contributing
|
|
|
|
|
|
|
182 |
|
183 |
+
We welcome community contributions! If you’d like to contribute:
|
184 |
+
1. Fork the repository.
|
185 |
+
2. Create a feature branch (`git checkout -b feature/YourFeature`).
|
186 |
+
3. Commit your changes (`git commit -m 'Add some feature'`).
|
187 |
+
4. Push to your branch (`git push origin feature/YourFeature`).
|
188 |
+
5. Open a pull request.
|
189 |
|
190 |
+
---
|
|
|
|
|
|
|
|
|
191 |
|
192 |
+
## License
|
193 |
|
194 |
+
This project is open source. See the [LICENSE](./LICENSE) file for details.
|
195 |
|
|
Makefile
DELETED
@@ -1,48 +0,0 @@
|
|
1 |
-
.PHONY: install
|
2 |
-
|
3 |
-
venv:
|
4 |
-
python -m venv venv
|
5 |
-
|
6 |
-
|
7 |
-
activate:
|
8 |
-
source ./venv/bin/activate
|
9 |
-
|
10 |
-
install: local_install install_openmmlab
|
11 |
-
|
12 |
-
docker_install: local_install install_openmmlab_with_mim
|
13 |
-
|
14 |
-
local_install:
|
15 |
-
@echo "Running requirements install"
|
16 |
-
pip install --upgrade pip
|
17 |
-
pip install -r requirements.txt
|
18 |
-
|
19 |
-
install_openmmlab_with_mim:
|
20 |
-
@echo "Running Openmmlab requirements install"
|
21 |
-
pip install -U openmim
|
22 |
-
mim install mmengine
|
23 |
-
mim install mmcv
|
24 |
-
mim install mmdet
|
25 |
-
mim install mmocr
|
26 |
-
|
27 |
-
install_openmmlab:
|
28 |
-
@echo "Running Openmmlab requirements install"
|
29 |
-
pip install mmengine==0.7.4
|
30 |
-
pip install mmcv==2.0.1
|
31 |
-
pip install mmdet==3.0.0
|
32 |
-
pip install mmocr==1.0.0
|
33 |
-
|
34 |
-
build:
|
35 |
-
pip install -e .
|
36 |
-
gradio app.py
|
37 |
-
|
38 |
-
docker_build:
|
39 |
-
docker build -t htrflow-app -f .docker/Dockerfile .
|
40 |
-
|
41 |
-
# clean_for_actions:
|
42 |
-
# git lfs prune
|
43 |
-
# git filter-branch --force --index-filter "git rm --cached --ignore-unmatch helper/text/videos/eating_spaghetti.mp4" --prune-empty --tag-name-filter cat -- --all
|
44 |
-
# git push --force origin main
|
45 |
-
|
46 |
-
# add_space:
|
47 |
-
# git remote add demo https://huggingface.co/spaces/Riksarkivet/htr_demo
|
48 |
-
# git push --force demo main
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/assets/images/how_to_1.png
ADDED
![]() |
Git LFS Details
|
app/assets/images/how_to_2.png
ADDED
![]() |
Git LFS Details
|
app/assets/images/how_to_3.png
ADDED
![]() |
Git LFS Details
|
app/assets/images/how_to_4.png
ADDED
![]() |
Git LFS Details
|
app/assets/images/how_to_5.png
ADDED
![]() |
Git LFS Details
|
app/assets/images/how_to_6.png
ADDED
![]() |
Git LFS Details
|
app/assets/templates/c_nested_labels.yaml
DELETED
@@ -1,41 +0,0 @@
|
|
1 |
-
steps:
|
2 |
-
- step: Segmentation
|
3 |
-
settings:
|
4 |
-
model: yolo
|
5 |
-
model_settings:
|
6 |
-
model: Riksarkivet/yolov9-regions-1
|
7 |
-
generation_settings:
|
8 |
-
batch_size: 2
|
9 |
-
- step: Segmentation
|
10 |
-
settings:
|
11 |
-
model: yolo
|
12 |
-
model_settings:
|
13 |
-
model: Riksarkivet/yolov9-lines-within-regions-1
|
14 |
-
generation_settings:
|
15 |
-
batch_size: 2
|
16 |
-
- step: TextRecognition
|
17 |
-
settings:
|
18 |
-
model: WordLevelTrocr
|
19 |
-
model_settings:
|
20 |
-
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
21 |
-
generation_settings:
|
22 |
-
batch_size: 4
|
23 |
-
num_beams: 1
|
24 |
-
- step: ReadingOrderMarginalia
|
25 |
-
settings:
|
26 |
-
two_page: auto
|
27 |
-
- step: Export
|
28 |
-
settings:
|
29 |
-
dest: outputs/alto
|
30 |
-
format: alto
|
31 |
-
- step: Export
|
32 |
-
settings:
|
33 |
-
dest: outputs/page
|
34 |
-
format: page
|
35 |
-
labels:
|
36 |
-
level_labels:
|
37 |
-
- region
|
38 |
-
- line
|
39 |
-
- word
|
40 |
-
sep: _
|
41 |
-
template: "{label}{number}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/assets/templates/c_nested_reading_order.yaml
DELETED
@@ -1,24 +0,0 @@
|
|
1 |
-
steps:
|
2 |
-
- step: Segmentation
|
3 |
-
settings:
|
4 |
-
model: yolo
|
5 |
-
model_settings:
|
6 |
-
model: Riksarkivet/yolov9-regions-1
|
7 |
-
- step: Segmentation
|
8 |
-
settings:
|
9 |
-
model: yolo
|
10 |
-
model_settings:
|
11 |
-
model: Riksarkivet/yolov9-lines-within-regions-1
|
12 |
-
- step: TextRecognition
|
13 |
-
settings:
|
14 |
-
model: TrOCR
|
15 |
-
model_settings:
|
16 |
-
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
17 |
-
- step: ReadingOrderMarginalia
|
18 |
-
settings:
|
19 |
-
two_page: always
|
20 |
-
- step: Export
|
21 |
-
settings:
|
22 |
-
format: txt
|
23 |
-
dest: text-outputs
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/assets/templates/c_nested_with_filter.yaml
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
steps:
|
2 |
-
- step: Segmentation
|
3 |
-
settings:
|
4 |
-
model: yolo
|
5 |
-
model_settings:
|
6 |
-
model: Riksarkivet/yolov9-regions-1
|
7 |
-
- step: Segmentation
|
8 |
-
settings:
|
9 |
-
model: yolo
|
10 |
-
model_settings:
|
11 |
-
model: Riksarkivet/yolov9-lines-within-regions-1
|
12 |
-
- step: TextRecognition
|
13 |
-
settings:
|
14 |
-
model: TrOCR
|
15 |
-
model_settings:
|
16 |
-
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
17 |
-
- step: OrderLines
|
18 |
-
- step: Export
|
19 |
-
settings:
|
20 |
-
format: txt
|
21 |
-
dest: raw-outputs
|
22 |
-
- step: RemoveLowTextConfidenceLines
|
23 |
-
settings:
|
24 |
-
threshold: 0.95
|
25 |
-
- step: Export
|
26 |
-
settings:
|
27 |
-
format: txt
|
28 |
-
dest: cleaned-outputs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/assets/templates/c_simple_gensettings.yaml
DELETED
@@ -1,21 +0,0 @@
|
|
1 |
-
steps:
|
2 |
-
- step: Segmentation
|
3 |
-
settings:
|
4 |
-
model: yolo
|
5 |
-
model_settings:
|
6 |
-
model: Riksarkivet/yolov9-lines-within-regions-1
|
7 |
-
generation_settings:
|
8 |
-
batch_size: 2
|
9 |
-
- step: TextRecognition
|
10 |
-
settings:
|
11 |
-
model: TrOCR
|
12 |
-
model_settings:
|
13 |
-
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
14 |
-
generation_settings:
|
15 |
-
batch_size: 4
|
16 |
-
num_beams: 1
|
17 |
-
- step: OrderLines
|
18 |
-
- step: Export
|
19 |
-
settings:
|
20 |
-
format: txt
|
21 |
-
dest: outputs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/assets/templates/c_simple_multi_output.yaml
DELETED
@@ -1,24 +0,0 @@
|
|
1 |
-
steps:
|
2 |
-
- step: Segmentation
|
3 |
-
settings:
|
4 |
-
model: yolo
|
5 |
-
model_settings:
|
6 |
-
model: Riksarkivet/yolov9-lines-within-regions-1
|
7 |
-
- step: TextRecognition
|
8 |
-
settings:
|
9 |
-
model: TrOCR
|
10 |
-
model_settings:
|
11 |
-
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
12 |
-
- step: OrderLines
|
13 |
-
- step: Export
|
14 |
-
settings:
|
15 |
-
format: txt
|
16 |
-
dest: text-outputs
|
17 |
-
- step: Export
|
18 |
-
settings:
|
19 |
-
format: page
|
20 |
-
dest: page-outputs
|
21 |
-
- step: Export
|
22 |
-
settings:
|
23 |
-
format: alto
|
24 |
-
dest: alto-outputs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/assets/templates/{2_nested.yaml → nested.yaml}
RENAMED
@@ -14,9 +14,4 @@ steps:
|
|
14 |
model: TrOCR
|
15 |
model_settings:
|
16 |
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
17 |
-
- step: ReadingOrderMarginalia
|
18 |
-
- step: Export
|
19 |
-
settings:
|
20 |
-
format: txt
|
21 |
-
dest: text-outputs
|
22 |
-
|
|
|
14 |
model: TrOCR
|
15 |
model_settings:
|
16 |
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
17 |
+
- step: ReadingOrderMarginalia
|
|
|
|
|
|
|
|
|
|
app/assets/templates/{1_simple.yaml → simple.yaml}
RENAMED
@@ -9,8 +9,4 @@ steps:
|
|
9 |
model: TrOCR
|
10 |
model_settings:
|
11 |
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
12 |
-
- step: OrderLines
|
13 |
-
- step: Export
|
14 |
-
settings:
|
15 |
-
format: txt
|
16 |
-
dest: outputs
|
|
|
9 |
model: TrOCR
|
10 |
model_settings:
|
11 |
model: Riksarkivet/trocr-base-handwritten-hist-swe-2
|
12 |
+
- step: OrderLines
|
|
|
|
|
|
|
|
app/content/how_it_works.md
DELETED
@@ -1,44 +0,0 @@
|
|
1 |
-
# Nocebant Achilles de vallis meminere fugit
|
2 |
-
|
3 |
-
## Corpus exta frondes pectora neque
|
4 |
-
|
5 |
-
Lorem markdownum animi, resistere praefertur recenti de vocor data levibus.
|
6 |
-
Lucifer cupidine pugnandi alter, quies modestos, nec aut quae Cancri diva
|
7 |
-
Latiis. Morerne est bonis ingentibus luctantemque corpore ad consistuntque
|
8 |
-
Cereris clausit.
|
9 |
-
|
10 |
-
## Putat inclita si parte se
|
11 |
-
|
12 |
-
Accipe fit explevit pessima in timebat querellas qui. Peti fuit: summa et
|
13 |
-
adstantem vulnere artus is utque orbes suis exsangues me saepe! Hominis et Troia
|
14 |
-
pater contigit, dolor fecit illis in.
|
15 |
-
|
16 |
-
## Nos celer bracchia curvari hiemsque
|
17 |
-
|
18 |
-
Corpus Alcmene omnia hiemes viros sic nepotum *pater* soporem, tenebat modo
|
19 |
-
Lethaea adstupet artis et cur. Optatis tendebant posita pudore Hennaeis dicere
|
20 |
-
visa tanti cornua laevam et faciebat et transfert sanguineam iussos. Aliquid
|
21 |
-
occupat [sagittis](http://densihabet.org/) tributuram si nihil fugamque
|
22 |
-
Bienoris.
|
23 |
-
|
24 |
-
> Os docuisse posse tectus, nisi pronas, trabes annos amor porrigitur. Corpora
|
25 |
-
> retemptantem fulvas. Et letum semianimem exclamat omnia et amisso.
|
26 |
-
|
27 |
-
## Aquae tibi insanis se quas
|
28 |
-
|
29 |
-
[Hyperione dare](http://www.fama.org/)? Cum certam virique fugacis magnos, sedes
|
30 |
-
iuverat Canens fera, mox cervus, res dea equorum vocant, vocandus.
|
31 |
-
|
32 |
-
traceroute.mac_active.partition_widget_optical(restoreBare(irqMeme,
|
33 |
-
reimage_file), -1, network.ethics(socketUdp, tagAddressLaser, 54));
|
34 |
-
processScanMainframe.uncPipeline(flash_graphics_kilobit(dsl, imapCircuit,
|
35 |
-
suffix(driveMultitaskingDrive)));
|
36 |
-
var guidSmishing = 881375;
|
37 |
-
ipv(vgaPointPeripheral);
|
38 |
-
|
39 |
-
Ureris totidemque mihi sed pendens amantes praesens ambos tua planxerunt
|
40 |
-
**lumine**, huius bracchia Cepheusque ne invida circum etiam! Exsistere
|
41 |
-
cornuque, non oblatae quid servat quae tecto potiere exhibuit annos qui vulnera.
|
42 |
-
Eras sic non passus peragit frequens, quae creati vix. Regno per cortice ignea,
|
43 |
-
versus non omni missus: cervice sub *foedoque ferali*. Troiana hiemes solidumve
|
44 |
-
et timori; quod, deus clamor barba; mea aulam furtum saltus suo catenas.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/gradio_config.py
CHANGED
@@ -76,4 +76,12 @@ hr.region-divider {
|
|
76 |
margin: auto;
|
77 |
color: var(--body-text-color);
|
78 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
"""
|
|
|
76 |
margin: auto;
|
77 |
color: var(--body-text-color);
|
78 |
}
|
79 |
+
|
80 |
+
.button-group-viz {
|
81 |
+
margin: auto;
|
82 |
+
display: flex;
|
83 |
+
justify-content: center;
|
84 |
+
gap: 1rem;
|
85 |
+
text-align: center;
|
86 |
+
}
|
87 |
"""
|
app/main.py
CHANGED
@@ -1,20 +1,18 @@
|
|
1 |
-
import shutil
|
2 |
-
import gradio as gr
|
3 |
import os
|
4 |
-
from app.gradio_config import css, theme
|
5 |
-
from app.tabs.submit import submit, collection_submit_state
|
6 |
-
from app.tabs.visualizer import visualizer, collection as collection_viz_state
|
7 |
-
from gradio_modal import Modal
|
8 |
|
|
|
9 |
from htrflow.models.huggingface.trocr import TrOCR
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
TEMPLATE_YAML_FOLDER = "app/assets/templates"
|
12 |
gr.set_static_paths(paths=[TEMPLATE_YAML_FOLDER])
|
13 |
|
14 |
-
# TODO: fix api/ endpoints..
|
15 |
-
# TODO add colab
|
16 |
-
# TDOO addd eexmaple for api
|
17 |
-
|
18 |
|
19 |
def load_markdown(language, section, content_dir="app/content"):
|
20 |
"""Load markdown content from files."""
|
@@ -50,29 +48,25 @@ matomo = """
|
|
50 |
<!-- End Matomo Code -->
|
51 |
"""
|
52 |
|
|
|
53 |
with gr.Blocks(title="HTRflow", theme=theme, css=css, head=matomo) as demo:
|
54 |
with gr.Row():
|
55 |
with gr.Column(scale=1):
|
56 |
-
|
57 |
-
with Modal(visible=False) as help_modal:
|
58 |
-
# TODO: tutorial material?
|
59 |
-
with gr.Tab("How to use App"):
|
60 |
-
gr.Markdown(load_markdown(None, "how_it_works"))
|
61 |
-
with gr.Tab("Contact"):
|
62 |
-
pass
|
63 |
-
|
64 |
with gr.Column(scale=2):
|
65 |
gr.Markdown(load_markdown(None, "main_title"))
|
66 |
with gr.Column(scale=1):
|
67 |
gr.Markdown(load_markdown(None, "main_sub_title"))
|
68 |
|
69 |
with gr.Tabs(elem_classes="top-navbar") as navbar:
|
70 |
-
with gr.Tab(label="
|
71 |
submit.render()
|
72 |
-
|
73 |
with gr.Tab(label="Result") as tab_visualizer:
|
74 |
visualizer.render()
|
75 |
|
|
|
|
|
|
|
76 |
@demo.load()
|
77 |
def inital_trocr_load():
|
78 |
TrOCR("Riksarkivet/trocr-base-handwritten-hist-swe-2")
|
@@ -88,14 +82,13 @@ with gr.Blocks(title="HTRflow", theme=theme, css=css, head=matomo) as demo:
|
|
88 |
fn=sync_gradio_object_state,
|
89 |
)
|
90 |
|
91 |
-
|
|
|
|
|
|
|
|
|
92 |
|
93 |
demo.queue()
|
94 |
|
95 |
if __name__ == "__main__":
|
96 |
-
demo.launch(
|
97 |
-
server_name="0.0.0.0",
|
98 |
-
server_port=7860,
|
99 |
-
enable_monitoring=True,
|
100 |
-
# show_error=True,
|
101 |
-
)
|
|
|
|
|
|
|
1 |
import os
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
import gradio as gr
|
4 |
from htrflow.models.huggingface.trocr import TrOCR
|
5 |
|
6 |
+
from app.gradio_config import css, theme
|
7 |
+
from app.tabs.export import collection as collection_export_state
|
8 |
+
from app.tabs.export import export
|
9 |
+
from app.tabs.submit import collection_submit_state, submit
|
10 |
+
from app.tabs.visualizer import collection as collection_viz_state
|
11 |
+
from app.tabs.visualizer import visualizer
|
12 |
+
|
13 |
TEMPLATE_YAML_FOLDER = "app/assets/templates"
|
14 |
gr.set_static_paths(paths=[TEMPLATE_YAML_FOLDER])
|
15 |
|
|
|
|
|
|
|
|
|
16 |
|
17 |
def load_markdown(language, section, content_dir="app/content"):
|
18 |
"""Load markdown content from files."""
|
|
|
48 |
<!-- End Matomo Code -->
|
49 |
"""
|
50 |
|
51 |
+
|
52 |
with gr.Blocks(title="HTRflow", theme=theme, css=css, head=matomo) as demo:
|
53 |
with gr.Row():
|
54 |
with gr.Column(scale=1):
|
55 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
with gr.Column(scale=2):
|
57 |
gr.Markdown(load_markdown(None, "main_title"))
|
58 |
with gr.Column(scale=1):
|
59 |
gr.Markdown(load_markdown(None, "main_sub_title"))
|
60 |
|
61 |
with gr.Tabs(elem_classes="top-navbar") as navbar:
|
62 |
+
with gr.Tab(label="Upload") as tab_submit:
|
63 |
submit.render()
|
|
|
64 |
with gr.Tab(label="Result") as tab_visualizer:
|
65 |
visualizer.render()
|
66 |
|
67 |
+
with gr.Tab(label="Export") as tab_export:
|
68 |
+
export.render()
|
69 |
+
|
70 |
@demo.load()
|
71 |
def inital_trocr_load():
|
72 |
TrOCR("Riksarkivet/trocr-base-handwritten-hist-swe-2")
|
|
|
82 |
fn=sync_gradio_object_state,
|
83 |
)
|
84 |
|
85 |
+
tab_export.select(
|
86 |
+
inputs=[collection_submit_state, collection_export_state],
|
87 |
+
outputs=[collection_export_state],
|
88 |
+
fn=sync_gradio_object_state,
|
89 |
+
)
|
90 |
|
91 |
demo.queue()
|
92 |
|
93 |
if __name__ == "__main__":
|
94 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, enable_monitoring=True, show_api=False)
|
|
|
|
|
|
|
|
|
|
app/tabs/export.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import yaml
|
3 |
+
from htrflow.pipeline.pipeline import Pipeline
|
4 |
+
from htrflow.volume.volume import Collection
|
5 |
+
|
6 |
+
|
7 |
+
def run_htrflow(custom_template_yaml, collection, progress=gr.Progress()):
|
8 |
+
"""
|
9 |
+
Executes the HTRflow pipeline based on the provided YAML configuration and batch images.
|
10 |
+
Args:
|
11 |
+
custom_template_yaml (str): YAML string specifying the HTRflow pipeline configuration.
|
12 |
+
batch_image_gallery (list): List of uploaded images to process in the pipeline.
|
13 |
+
Returns:
|
14 |
+
tuple: A collection of processed items, list of exported file paths, and a Gradio update object.
|
15 |
+
"""
|
16 |
+
|
17 |
+
if custom_template_yaml is None or len(custom_template_yaml) < 1:
|
18 |
+
gr.Warning("HTRflow: Please insert a HTRflow-yaml template")
|
19 |
+
try:
|
20 |
+
config = yaml.safe_load(custom_template_yaml)
|
21 |
+
except Exception as e:
|
22 |
+
gr.Warning(f"HTRflow: Error loading YAML configuration: {e}")
|
23 |
+
return gr.skip()
|
24 |
+
|
25 |
+
pipe = Pipeline.from_config(config)
|
26 |
+
|
27 |
+
collection: Collection = pipe.run(collection, progress=progress)
|
28 |
+
|
29 |
+
gr.Info("HTRflow: Export complete!")
|
30 |
+
|
31 |
+
yield collection, gr.skip()
|
32 |
+
|
33 |
+
|
34 |
+
with gr.Blocks() as export:
|
35 |
+
collection = gr.State()
|
36 |
+
|
37 |
+
gr.Markdown("## Export")
|
38 |
+
with gr.Group():
|
39 |
+
with gr.Row(equal_height=True):
|
40 |
+
with gr.Column(scale=1):
|
41 |
+
selected_output = gr.Dropdown(
|
42 |
+
label="Export file format",
|
43 |
+
info="Select (multiple) what export format you want",
|
44 |
+
choices=["txt", "alto", "page", "json"],
|
45 |
+
multiselect=True,
|
46 |
+
interactive=True,
|
47 |
+
)
|
48 |
+
name_of_files = gr.Textbox(
|
49 |
+
label="File name",
|
50 |
+
info="All files will be given the same name with a suffix of the file extension",
|
51 |
+
placeholder="my_htr_file",
|
52 |
+
)
|
53 |
+
|
54 |
+
with gr.Column(scale=1):
|
55 |
+
download_files = gr.Files(interactive=False)
|
56 |
+
with gr.Row():
|
57 |
+
export_button = gr.Button("Export", scale=0, min_width=200, variant="primary")
|
58 |
+
|
59 |
+
@export_button.click(inputs=[], outputs=[])
|
60 |
+
def blable():
|
61 |
+
pass
|
62 |
+
|
63 |
+
|
64 |
+
# TODO: test pylaia works...
|
65 |
+
# TODO: add other pipeliens for other language like english and hebrew model?
|
66 |
+
# TODO: add other pipeliens for other language like english and hebrew model?
|
67 |
+
# TODO kolla över toast. toast vid export?
|
app/tabs/submit.py
CHANGED
@@ -1,16 +1,14 @@
|
|
1 |
-
import
|
|
|
2 |
import time
|
3 |
-
|
4 |
import gradio as gr
|
|
|
|
|
5 |
from htrflow.pipeline.pipeline import Pipeline
|
6 |
-
from htrflow.pipeline.steps import init_step
|
7 |
-
import os
|
8 |
-
import logging
|
9 |
from htrflow.volume.volume import Collection
|
10 |
|
11 |
-
from htrflow.pipeline.steps import auto_import
|
12 |
-
import yaml
|
13 |
-
|
14 |
logger = logging.getLogger(__name__)
|
15 |
|
16 |
# Max number of images a user can upload at once
|
@@ -19,23 +17,23 @@ MAX_IMAGES = int(os.environ.get("MAX_IMAGES", 5))
|
|
19 |
# Example pipelines
|
20 |
PIPELINES = {
|
21 |
"Running text (Swedish)": {
|
22 |
-
"file": "app/assets/templates/
|
23 |
"description": "This pipeline works well on documents with multiple text regions.",
|
24 |
"examples": [
|
25 |
"R0003364_00005.jpg",
|
26 |
"30002027_00008.jpg",
|
27 |
"A0070302_00201.jpg",
|
28 |
-
]
|
29 |
},
|
30 |
"Letters and snippets (Swedish)": {
|
31 |
-
"file": "app/assets/templates/
|
32 |
"description": "This pipeline works well on letters and other documents with only one text region.",
|
33 |
"examples": [
|
34 |
"451511_1512_01.jpg",
|
35 |
"A0062408_00006.jpg",
|
36 |
"C0000546_00085_crop.png",
|
37 |
"A0073477_00025.jpg",
|
38 |
-
]
|
39 |
},
|
40 |
}
|
41 |
|
@@ -46,21 +44,14 @@ GRADIO_CACHE = ".gradio_cache"
|
|
46 |
EXAMPLES_DIRECTORY = os.path.join(GRADIO_CACHE, "examples")
|
47 |
|
48 |
if os.environ.get("GRADIO_CACHE_DIR", GRADIO_CACHE) != GRADIO_CACHE:
|
49 |
-
logger.warning(
|
50 |
-
"Setting GRADIO_CACHE_DIR to '%s' (overriding a previous value)."
|
51 |
-
)
|
52 |
|
53 |
|
54 |
class PipelineWithProgress(Pipeline):
|
55 |
@classmethod
|
56 |
def from_config(cls, config: dict[str, str]):
|
57 |
"""Init pipeline from config, ensuring the correct subclass is instantiated."""
|
58 |
-
return cls(
|
59 |
-
[
|
60 |
-
init_step(step["step"], step.get("settings", {}))
|
61 |
-
for step in config["steps"]
|
62 |
-
]
|
63 |
-
)
|
64 |
|
65 |
def run(self, collection, start=0, progress=None):
|
66 |
"""
|
@@ -88,31 +79,6 @@ class PipelineWithProgress(Pipeline):
|
|
88 |
return collection
|
89 |
|
90 |
|
91 |
-
def rewrite_export_dests(config):
|
92 |
-
"""
|
93 |
-
Rewrite the 'dest' in all 'Export' steps to include 'tmp' and a UUID.
|
94 |
-
Returns:
|
95 |
-
- A new config object with the updated 'dest' values.
|
96 |
-
- A list of all updated 'dest' paths.
|
97 |
-
"""
|
98 |
-
new_config = {"steps": []}
|
99 |
-
updated_paths = []
|
100 |
-
|
101 |
-
unique_id = str(uuid.uuid4())
|
102 |
-
|
103 |
-
for step in config.get("steps", []):
|
104 |
-
new_step = step.copy()
|
105 |
-
if new_step.get("step") == "Export":
|
106 |
-
settings = new_step.get("settings", {})
|
107 |
-
if "dest" in settings:
|
108 |
-
new_dest = os.path.join("tmp", unique_id, settings["dest"])
|
109 |
-
settings["dest"] = new_dest
|
110 |
-
updated_paths.append(new_dest)
|
111 |
-
new_config["steps"].append(new_step)
|
112 |
-
|
113 |
-
return new_config, updated_paths
|
114 |
-
|
115 |
-
|
116 |
def run_htrflow(custom_template_yaml, batch_image_gallery, progress=gr.Progress()):
|
117 |
"""
|
118 |
Executes the HTRflow pipeline based on the provided YAML configuration and batch images.
|
@@ -131,68 +97,32 @@ def run_htrflow(custom_template_yaml, batch_image_gallery, progress=gr.Progress(
|
|
131 |
gr.Warning(f"HTRflow: Error loading YAML configuration: {e}")
|
132 |
return gr.skip()
|
133 |
|
134 |
-
temp_config, tmp_output_paths = rewrite_export_dests(config)
|
135 |
-
|
136 |
progress(0, desc="HTRflow: Starting")
|
137 |
time.sleep(0.3)
|
138 |
|
139 |
-
print(temp_config)
|
140 |
-
|
141 |
if batch_image_gallery is None:
|
142 |
gr.Warning("HTRflow: You must upload atleast 1 image or more")
|
143 |
|
144 |
images = [temp_img[0] for temp_img in batch_image_gallery]
|
145 |
|
146 |
-
pipe = PipelineWithProgress.from_config(
|
147 |
collections = auto_import(images)
|
148 |
|
149 |
-
gr.Info(
|
150 |
-
f"HTRflow: processing {len(images)} {'image' if len(images) == 1 else 'images'}."
|
151 |
-
)
|
152 |
progress(0.1, desc="HTRflow: Processing")
|
153 |
|
154 |
for collection in collections:
|
155 |
-
if "labels" in
|
156 |
-
collection.set_label_format(**
|
157 |
|
158 |
collection.label = "HTRflow_demo_output"
|
159 |
collection: Collection = pipe.run(collection, progress=progress)
|
160 |
|
161 |
-
exported_files = tracking_exported_files(tmp_output_paths)
|
162 |
-
|
163 |
-
time.sleep(0.5)
|
164 |
progress(1, desc="HTRflow: Finish")
|
|
|
165 |
gr.Info("HTRflow: Finish")
|
166 |
|
167 |
-
yield collection,
|
168 |
-
|
169 |
-
|
170 |
-
def tracking_exported_files(tmp_output_paths):
|
171 |
-
"""
|
172 |
-
Look for files with specific extensions in the provided tmp_output_paths,
|
173 |
-
including subdirectories. Eliminates duplicate files.
|
174 |
-
|
175 |
-
Args:
|
176 |
-
tmp_output_paths (list): List of temporary output directories to search.
|
177 |
-
|
178 |
-
Returns:
|
179 |
-
list: Unique paths of all matching files found in the directories.
|
180 |
-
"""
|
181 |
-
accepted_extensions = {".txt", ".xml", ".json"}
|
182 |
-
|
183 |
-
exported_files = set()
|
184 |
-
|
185 |
-
print(tmp_output_paths)
|
186 |
-
|
187 |
-
# TODO: fix so that we get the file extension for page and alto...
|
188 |
-
|
189 |
-
for tmp_folder in tmp_output_paths:
|
190 |
-
for ext in accepted_extensions:
|
191 |
-
search_pattern = os.path.join(tmp_folder, "**", f"*{ext}")
|
192 |
-
matching_files = glob.glob(search_pattern, recursive=True)
|
193 |
-
exported_files.update(matching_files)
|
194 |
-
|
195 |
-
return sorted(exported_files)
|
196 |
|
197 |
|
198 |
def get_pipeline_description(pipeline: str) -> str:
|
@@ -229,6 +159,7 @@ def get_selected_example_image(event: gr.SelectData) -> str:
|
|
229 |
"""
|
230 |
Get path to the selected example image.
|
231 |
"""
|
|
|
232 |
return [event.value["image"]["path"]]
|
233 |
|
234 |
|
@@ -242,55 +173,88 @@ def get_selected_example_pipeline(event: gr.SelectData) -> str | None:
|
|
242 |
|
243 |
|
244 |
with gr.Blocks() as submit:
|
245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
|
|
|
247 |
with gr.Group():
|
248 |
with gr.Row(equal_height=True):
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
266 |
|
267 |
with gr.Column(variant="panel", elem_classes="pipeline-panel"):
|
268 |
gr.HTML("Pipeline", elem_classes="pipeline-header", padding=False)
|
269 |
|
270 |
with gr.Row():
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
with gr.Group():
|
279 |
-
with gr.Accordion("Edit pipeline", open=False):
|
280 |
-
custom_template_yaml = gr.Code(
|
281 |
-
value=get_yaml, inputs=pipeline_dropdown, language="yaml", container=False
|
282 |
-
)
|
283 |
-
url = "https://ai-riksarkivet.github.io/htrflow/latest/getting_started/pipeline.html#example-pipelines"
|
284 |
-
gr.HTML(
|
285 |
-
f'See the <a href="{url}">documentation</a> for a detailed description on how to customize HTRflow pipelines.',
|
286 |
-
padding=False,
|
287 |
-
elem_classes="pipeline-help",
|
288 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
|
290 |
with gr.Row():
|
291 |
run_button = gr.Button("Submit", variant="primary", scale=0, min_width=200)
|
292 |
progess_bar = gr.Textbox(visible=False, show_label=False)
|
293 |
-
collection_output_files = gr.Files(label="Output Files", scale=0, min_width=400, visible=False)
|
294 |
|
295 |
@batch_image_gallery.upload(
|
296 |
inputs=batch_image_gallery,
|
@@ -302,20 +266,26 @@ with gr.Blocks() as submit:
|
|
302 |
return gr.update(value=None)
|
303 |
return images
|
304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
305 |
run_button.click(
|
306 |
-
lambda:
|
307 |
-
outputs=[progess_bar
|
308 |
).then(
|
309 |
fn=run_htrflow,
|
310 |
inputs=[custom_template_yaml, batch_image_gallery],
|
311 |
-
outputs=[collection_submit_state,
|
312 |
).then(
|
313 |
-
lambda:
|
314 |
-
outputs=[progess_bar
|
315 |
)
|
316 |
|
317 |
examples.select(get_selected_example_image, None, batch_image_gallery)
|
318 |
examples.select(get_selected_example_pipeline, None, pipeline_dropdown)
|
319 |
|
320 |
-
|
321 |
-
# TODO: Add toast gr.Warning: Lose previues run...
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
import time
|
4 |
+
|
5 |
import gradio as gr
|
6 |
+
import yaml
|
7 |
+
from gradio_modal import Modal
|
8 |
from htrflow.pipeline.pipeline import Pipeline
|
9 |
+
from htrflow.pipeline.steps import auto_import, init_step
|
|
|
|
|
10 |
from htrflow.volume.volume import Collection
|
11 |
|
|
|
|
|
|
|
12 |
logger = logging.getLogger(__name__)
|
13 |
|
14 |
# Max number of images a user can upload at once
|
|
|
17 |
# Example pipelines
|
18 |
PIPELINES = {
|
19 |
"Running text (Swedish)": {
|
20 |
+
"file": "app/assets/templates/nested.yaml",
|
21 |
"description": "This pipeline works well on documents with multiple text regions.",
|
22 |
"examples": [
|
23 |
"R0003364_00005.jpg",
|
24 |
"30002027_00008.jpg",
|
25 |
"A0070302_00201.jpg",
|
26 |
+
],
|
27 |
},
|
28 |
"Letters and snippets (Swedish)": {
|
29 |
+
"file": "app/assets/templates/simple.yaml",
|
30 |
"description": "This pipeline works well on letters and other documents with only one text region.",
|
31 |
"examples": [
|
32 |
"451511_1512_01.jpg",
|
33 |
"A0062408_00006.jpg",
|
34 |
"C0000546_00085_crop.png",
|
35 |
"A0073477_00025.jpg",
|
36 |
+
],
|
37 |
},
|
38 |
}
|
39 |
|
|
|
44 |
EXAMPLES_DIRECTORY = os.path.join(GRADIO_CACHE, "examples")
|
45 |
|
46 |
if os.environ.get("GRADIO_CACHE_DIR", GRADIO_CACHE) != GRADIO_CACHE:
|
47 |
+
logger.warning("Setting GRADIO_CACHE_DIR to '%s' (overriding a previous value).")
|
|
|
|
|
48 |
|
49 |
|
50 |
class PipelineWithProgress(Pipeline):
|
51 |
@classmethod
|
52 |
def from_config(cls, config: dict[str, str]):
|
53 |
"""Init pipeline from config, ensuring the correct subclass is instantiated."""
|
54 |
+
return cls([init_step(step["step"], step.get("settings", {})) for step in config["steps"]])
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
def run(self, collection, start=0, progress=None):
|
57 |
"""
|
|
|
79 |
return collection
|
80 |
|
81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
def run_htrflow(custom_template_yaml, batch_image_gallery, progress=gr.Progress()):
|
83 |
"""
|
84 |
Executes the HTRflow pipeline based on the provided YAML configuration and batch images.
|
|
|
97 |
gr.Warning(f"HTRflow: Error loading YAML configuration: {e}")
|
98 |
return gr.skip()
|
99 |
|
|
|
|
|
100 |
progress(0, desc="HTRflow: Starting")
|
101 |
time.sleep(0.3)
|
102 |
|
|
|
|
|
103 |
if batch_image_gallery is None:
|
104 |
gr.Warning("HTRflow: You must upload atleast 1 image or more")
|
105 |
|
106 |
images = [temp_img[0] for temp_img in batch_image_gallery]
|
107 |
|
108 |
+
pipe = PipelineWithProgress.from_config(config)
|
109 |
collections = auto_import(images)
|
110 |
|
111 |
+
gr.Info(f"HTRflow: processing {len(images)} {'image' if len(images) == 1 else 'images'}.")
|
|
|
|
|
112 |
progress(0.1, desc="HTRflow: Processing")
|
113 |
|
114 |
for collection in collections:
|
115 |
+
if "labels" in config:
|
116 |
+
collection.set_label_format(**config["labels"])
|
117 |
|
118 |
collection.label = "HTRflow_demo_output"
|
119 |
collection: Collection = pipe.run(collection, progress=progress)
|
120 |
|
|
|
|
|
|
|
121 |
progress(1, desc="HTRflow: Finish")
|
122 |
+
time.sleep(1)
|
123 |
gr.Info("HTRflow: Finish")
|
124 |
|
125 |
+
yield collection, gr.skip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
|
127 |
|
128 |
def get_pipeline_description(pipeline: str) -> str:
|
|
|
159 |
"""
|
160 |
Get path to the selected example image.
|
161 |
"""
|
162 |
+
print([event.value["image"]["path"]])
|
163 |
return [event.value["image"]["path"]]
|
164 |
|
165 |
|
|
|
173 |
|
174 |
|
175 |
with gr.Blocks() as submit:
|
176 |
+
gr.Markdown("# Upload")
|
177 |
+
gr.Markdown("Start Here! ")
|
178 |
+
gr.Markdown(
|
179 |
+
"First you upload upload 1 image or multiple images (max 5 images). You can also use directly the Image ID from the National Archives of Sweden to request an image"
|
180 |
+
)
|
181 |
+
gr.Markdown(
|
182 |
+
"Afterward, choice a template from the examples based on your material. This will configure a certain pipeline that fits your image."
|
183 |
+
)
|
184 |
|
185 |
+
collection_submit_state = gr.State()
|
186 |
with gr.Group():
|
187 |
with gr.Row(equal_height=True):
|
188 |
+
with gr.Column(scale=5):
|
189 |
+
batch_image_gallery = gr.Gallery(
|
190 |
+
file_types=["image"],
|
191 |
+
label="Image to transcribe",
|
192 |
+
interactive=True,
|
193 |
+
object_fit="scale-down",
|
194 |
+
scale=3,
|
195 |
+
preview=True,
|
196 |
+
)
|
197 |
+
|
198 |
+
with gr.Column(scale=2):
|
199 |
+
examples = gr.Gallery(
|
200 |
+
all_example_images(),
|
201 |
+
label="Examples",
|
202 |
+
interactive=False,
|
203 |
+
allow_preview=False,
|
204 |
+
object_fit="scale-down",
|
205 |
+
min_width=250,
|
206 |
+
)
|
207 |
+
|
208 |
+
image_iiif_url = gr.Textbox(
|
209 |
+
label="Images from the National Archives of Sweden",
|
210 |
+
info="e.g <a href='https://sok.riksarkivet.se/bildvisning/R0002231_00005' target='_blank'>R0002231_00005</a> - Press enter to submit",
|
211 |
+
placeholder="R0002231_00005",
|
212 |
+
)
|
213 |
+
|
214 |
+
iiif_image_placeholder = gr.Image(visible=False)
|
215 |
|
216 |
with gr.Column(variant="panel", elem_classes="pipeline-panel"):
|
217 |
gr.HTML("Pipeline", elem_classes="pipeline-header", padding=False)
|
218 |
|
219 |
with gr.Row():
|
220 |
+
with gr.Column(scale=0):
|
221 |
+
pipeline_dropdown = gr.Dropdown(
|
222 |
+
PIPELINES,
|
223 |
+
container=False,
|
224 |
+
min_width=240,
|
225 |
+
scale=0,
|
226 |
+
elem_classes="pipeline-dropdown",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
)
|
228 |
+
with gr.Column():
|
229 |
+
with gr.Row():
|
230 |
+
pipeline_description = gr.HTML(
|
231 |
+
value=get_pipeline_description,
|
232 |
+
inputs=pipeline_dropdown,
|
233 |
+
elem_classes="pipeline-description",
|
234 |
+
padding=False,
|
235 |
+
)
|
236 |
+
help_button = gr.Button(
|
237 |
+
"Edit Pipeline",
|
238 |
+
scale=0,
|
239 |
+
)
|
240 |
+
|
241 |
+
with Modal(visible=False) as help_modal:
|
242 |
+
custom_template_yaml = gr.Code(
|
243 |
+
value=get_yaml,
|
244 |
+
inputs=pipeline_dropdown,
|
245 |
+
language="yaml",
|
246 |
+
container=False,
|
247 |
+
)
|
248 |
+
url = "https://ai-riksarkivet.github.io/htrflow/latest/getting_started/pipeline.html#example-pipelines"
|
249 |
+
gr.HTML(
|
250 |
+
f'See the <a href="{url}">documentation</a> for a detailed description on how to customize HTRflow pipelines.',
|
251 |
+
padding=False,
|
252 |
+
elem_classes="pipeline-help",
|
253 |
+
)
|
254 |
|
255 |
with gr.Row():
|
256 |
run_button = gr.Button("Submit", variant="primary", scale=0, min_width=200)
|
257 |
progess_bar = gr.Textbox(visible=False, show_label=False)
|
|
|
258 |
|
259 |
@batch_image_gallery.upload(
|
260 |
inputs=batch_image_gallery,
|
|
|
266 |
return gr.update(value=None)
|
267 |
return images
|
268 |
|
269 |
+
def return_iiif_url(image_iiif_url):
|
270 |
+
return f"https://lbiiif.riksarkivet.se/arkis!{image_iiif_url}/full/max/0/default.jpg"
|
271 |
+
|
272 |
+
image_iiif_url.submit(fn=return_iiif_url, inputs=image_iiif_url, outputs=iiif_image_placeholder).then(
|
273 |
+
fn=lambda x: [x], inputs=iiif_image_placeholder, outputs=batch_image_gallery
|
274 |
+
)
|
275 |
+
|
276 |
run_button.click(
|
277 |
+
lambda: gr.update(visible=True),
|
278 |
+
outputs=[progess_bar],
|
279 |
).then(
|
280 |
fn=run_htrflow,
|
281 |
inputs=[custom_template_yaml, batch_image_gallery],
|
282 |
+
outputs=[collection_submit_state, progess_bar],
|
283 |
).then(
|
284 |
+
lambda: gr.update(visible=False),
|
285 |
+
outputs=[progess_bar],
|
286 |
)
|
287 |
|
288 |
examples.select(get_selected_example_image, None, batch_image_gallery)
|
289 |
examples.select(get_selected_example_pipeline, None, pipeline_dropdown)
|
290 |
|
291 |
+
help_button.click(lambda: Modal(visible=True), None, help_modal)
|
|
app/tabs/visualizer.py
CHANGED
@@ -1,18 +1,22 @@
|
|
1 |
import gradio as gr
|
2 |
from jinja2 import Environment, FileSystemLoader
|
3 |
|
4 |
-
|
5 |
_ENV = Environment(loader=FileSystemLoader("app/assets/jinja-templates"))
|
6 |
_IMAGE_TEMPLATE = _ENV.get_template("image")
|
7 |
_TRANSCRIPTION_TEMPLATE = _ENV.get_template("transcription")
|
8 |
|
9 |
|
10 |
def render_image(collection, current_page_index):
|
11 |
-
return _IMAGE_TEMPLATE.render(
|
|
|
|
|
|
|
12 |
|
13 |
|
14 |
def render_transcription(collection, current_page_index):
|
15 |
-
regions = collection[current_page_index].traverse(
|
|
|
|
|
16 |
return _TRANSCRIPTION_TEMPLATE.render(regions=regions)
|
17 |
|
18 |
|
@@ -46,7 +50,8 @@ def update_image_caption(collection, current_page_index):
|
|
46 |
|
47 |
|
48 |
with gr.Blocks() as visualizer:
|
49 |
-
|
|
|
50 |
with gr.Row():
|
51 |
# Columns are needed here to get the scale right. The documentation
|
52 |
# claims all components have the `scale` argument but it doesn't
|
@@ -62,10 +67,10 @@ with gr.Blocks() as visualizer:
|
|
62 |
gr.Markdown("## Annotated image")
|
63 |
image = gr.HTML(padding=False, elem_classes="svg-image", container=True)
|
64 |
|
65 |
-
image_caption = gr.Markdown()
|
66 |
-
with gr.Row():
|
67 |
-
left = gr.Button("← Previous", visible=False, interactive=False)
|
68 |
-
right = gr.Button("Next →", visible=False)
|
69 |
|
70 |
collection = gr.State()
|
71 |
current_page_index = gr.State(0)
|
@@ -80,18 +85,34 @@ with gr.Blocks() as visualizer:
|
|
80 |
# - toggle visibility of navigation buttons (don't show them for single pages)
|
81 |
# - update the image caption
|
82 |
collection.change(render_image, inputs=[collection, current_page_index], outputs=image)
|
83 |
-
collection.change(
|
|
|
|
|
|
|
|
|
84 |
collection.change(lambda _: 0, current_page_index, current_page_index)
|
85 |
collection.change(toggle_navigation_button, collection, left)
|
86 |
collection.change(toggle_navigation_button, collection, right)
|
87 |
-
collection.change(
|
|
|
|
|
|
|
|
|
88 |
|
89 |
# Updates on page change:
|
90 |
# - update the view
|
91 |
# - activate/deactivate buttons
|
92 |
# - update the image caption
|
93 |
current_page_index.change(render_image, inputs=[collection, current_page_index], outputs=image)
|
94 |
-
current_page_index.change(
|
|
|
|
|
|
|
|
|
95 |
current_page_index.change(activate_left_button, current_page_index, left)
|
96 |
current_page_index.change(activate_right_button, [collection, current_page_index], right)
|
97 |
-
current_page_index.change(
|
|
|
|
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
from jinja2 import Environment, FileSystemLoader
|
3 |
|
|
|
4 |
_ENV = Environment(loader=FileSystemLoader("app/assets/jinja-templates"))
|
5 |
_IMAGE_TEMPLATE = _ENV.get_template("image")
|
6 |
_TRANSCRIPTION_TEMPLATE = _ENV.get_template("transcription")
|
7 |
|
8 |
|
9 |
def render_image(collection, current_page_index):
|
10 |
+
return _IMAGE_TEMPLATE.render(
|
11 |
+
page=collection[current_page_index],
|
12 |
+
lines=collection[current_page_index].traverse(lambda node: node.is_line()),
|
13 |
+
)
|
14 |
|
15 |
|
16 |
def render_transcription(collection, current_page_index):
|
17 |
+
regions = collection[current_page_index].traverse(
|
18 |
+
lambda node: node.children and all(child.is_line() for child in node)
|
19 |
+
)
|
20 |
return _TRANSCRIPTION_TEMPLATE.render(regions=regions)
|
21 |
|
22 |
|
|
|
50 |
|
51 |
|
52 |
with gr.Blocks() as visualizer:
|
53 |
+
gr.Markdown("# Results")
|
54 |
+
gr.Markdown("Below is the results from the job that were submitted")
|
55 |
with gr.Row():
|
56 |
# Columns are needed here to get the scale right. The documentation
|
57 |
# claims all components have the `scale` argument but it doesn't
|
|
|
67 |
gr.Markdown("## Annotated image")
|
68 |
image = gr.HTML(padding=False, elem_classes="svg-image", container=True)
|
69 |
|
70 |
+
image_caption = gr.Markdown(elem_classes="button-group-viz")
|
71 |
+
with gr.Row(elem_classes="button-group-viz"):
|
72 |
+
left = gr.Button("← Previous", visible=False, interactive=False, scale=0)
|
73 |
+
right = gr.Button("Next →", visible=False, scale=0)
|
74 |
|
75 |
collection = gr.State()
|
76 |
current_page_index = gr.State(0)
|
|
|
85 |
# - toggle visibility of navigation buttons (don't show them for single pages)
|
86 |
# - update the image caption
|
87 |
collection.change(render_image, inputs=[collection, current_page_index], outputs=image)
|
88 |
+
collection.change(
|
89 |
+
render_transcription,
|
90 |
+
inputs=[collection, current_page_index],
|
91 |
+
outputs=transcription,
|
92 |
+
)
|
93 |
collection.change(lambda _: 0, current_page_index, current_page_index)
|
94 |
collection.change(toggle_navigation_button, collection, left)
|
95 |
collection.change(toggle_navigation_button, collection, right)
|
96 |
+
collection.change(
|
97 |
+
update_image_caption,
|
98 |
+
inputs=[collection, current_page_index],
|
99 |
+
outputs=image_caption,
|
100 |
+
)
|
101 |
|
102 |
# Updates on page change:
|
103 |
# - update the view
|
104 |
# - activate/deactivate buttons
|
105 |
# - update the image caption
|
106 |
current_page_index.change(render_image, inputs=[collection, current_page_index], outputs=image)
|
107 |
+
current_page_index.change(
|
108 |
+
render_transcription,
|
109 |
+
inputs=[collection, current_page_index],
|
110 |
+
outputs=transcription,
|
111 |
+
)
|
112 |
current_page_index.change(activate_left_button, current_page_index, left)
|
113 |
current_page_index.change(activate_right_button, [collection, current_page_index], right)
|
114 |
+
current_page_index.change(
|
115 |
+
update_image_caption,
|
116 |
+
inputs=[collection, current_page_index],
|
117 |
+
outputs=image_caption,
|
118 |
+
)
|
pyproject.toml
CHANGED
@@ -18,7 +18,7 @@ requires-python = ">=3.10,<3.13"
|
|
18 |
|
19 |
dependencies = [
|
20 |
"htrflow==0.2.0",
|
21 |
-
"gradio>=5.
|
22 |
"datasets>=3.2.0",
|
23 |
"pandas>=2.2.3",
|
24 |
"tqdm>=4.67.1",
|
|
|
18 |
|
19 |
dependencies = [
|
20 |
"htrflow==0.2.0",
|
21 |
+
"gradio>=5.15.0",
|
22 |
"datasets>=3.2.0",
|
23 |
"pandas>=2.2.3",
|
24 |
"tqdm>=4.67.1",
|
uv.lock
CHANGED
The diff for this file is too large to render.
See raw diff
|
|