CesarLeblanc commited on
Commit
0979f5c
·
1 Parent(s): fa0e8e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +331 -170
app.py CHANGED
@@ -1,174 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
- from transformers import pipeline
3
- import requests
4
- import pygbif
5
- from bs4 import BeautifulSoup
6
- import pandas as pd
7
-
8
- classification_model = pipeline("text-classification", model="models/text_classification_model", tokenizer="models/text_classification_model", top_k=5)
9
- masking_model = pipeline("fill-mask", model="models/fill_mask_model", tokenizer="models/fill_mask_model", top_k=100)
10
-
11
- eunis_habitats = pd.read_excel('data/eunis_habitats.xlsx')
12
-
13
- image_not_found = gr.Image("https://img.freepik.com/premium-vector/file-folder-mascot-character-design-vector_166742-4413.jpg")
14
-
15
- def return_image(task, label):
16
- image = None
17
- if task == "classification":
18
- floraveg_url = f"https://floraveg.eu/habitat/overview/{label}"
19
- floraveg_tag = "https://files.ibot.cas.cz/cevs/images/syntaxa/thumbs/"
20
- elif task == "masking":
21
- floraveg_url = f"https://floraveg.eu/taxon/overview/{label}"
22
- floraveg_tag = "https://files.ibot.cas.cz/cevs/images/taxa/large/"
23
- response = requests.get(floraveg_url)
24
- if response.status_code == 200:
25
- soup = BeautifulSoup(response.text, 'html.parser')
26
- img_tag = soup.find('img', src=lambda x: x and x.startswith(floraveg_tag))
27
- if img_tag:
28
- image_url = img_tag['src']
29
- image = gr.Image(value=image_url)
30
- return image
31
-
32
- def gbif_normalization(text):
33
- all_species = text.split(',')
34
- all_species = [species.strip() for species in all_species]
35
- species_gbif = []
36
- for species in all_species:
37
- gbif_match_result = pygbif.species.name_backbone(species, taxonRank="SPECIES")
38
- if 'usage' in gbif_match_result:
39
- r = gbif_match_result["usage"]["canonicalName"]
40
- else:
41
- r = species
42
- species_gbif.append(r)
43
- text = ", ".join(species_gbif)
44
- text = text.lower()
45
- return text
46
-
47
- def classification(text, k):
48
- text = gbif_normalization(text)
49
- result = classification_model(text)
50
- habitat_labels = [res['label'] for res in result[0][:k]]
51
- habitat_name = eunis_habitats[eunis_habitats['EUNIS 2020 code'] == habitat_labels[0]]['EUNIS-2021 habitat name'].values[0]
52
- if k == 1:
53
- text = f"This vegetation plot probably belongs to the habitat type {habitat_labels[0]}."
54
- text += f"\nThis habitat type is named '{habitat_name}'."
55
- elif k == 2:
56
- text = f"This vegetation plot probably belongs to the habitat type {', '.join(habitat_labels[:-1])} or {habitat_labels[-1]}."
57
- text += f"\nThe most likely habitat type (i.e., {habitat_labels[0]}) is named '{habitat_name}'."
58
- else:
59
- text = f"This vegetation plot probably belongs to the habitat type {', '.join(habitat_labels[:-1])}, or {habitat_labels[-1]}."
60
- text += f"\nThe most likely habitat type (i.e., {habitat_labels[0]}) is named '{habitat_name}'."
61
- image = return_image("classification", habitat_labels[0])
62
- if image is not None:
63
- text += "\nBelow is an example of this habitat type taken from the website FloraVEG."
64
- else:
65
- text += f"\nNo image found for this habitat type."
66
- image = image_not_found
67
- return text, image
68
-
69
- def masking(text, k):
70
- text = gbif_normalization(text)
71
- text_split = text.split(', ')
72
-
73
- best_predictions = []
74
-
75
- for _ in range(k):
76
- max_score = 0
77
- best_prediction = None
78
- best_position = None
79
- best_sentence = None
80
-
81
- for i in range(len(text_split) + 1):
82
- masked_text = ', '.join(text_split[:i] + ['[MASK]'] + text_split[i:])
83
-
84
- j = 0
85
- while True:
86
- prediction = masking_model(masked_text)[j]
87
- species = prediction['token_str']
88
- if species in text_split or species in best_predictions:
89
- j += 1
90
- else:
91
- break
92
-
93
- score = prediction['score']
94
- sentence = prediction['sequence']
95
-
96
- if score > max_score:
97
- max_score = score
98
- best_prediction = species
99
- best_position = i
100
- best_sentence = sentence
101
-
102
- best_predictions.append(best_prediction)
103
- text_split.insert(best_position, best_prediction)
104
-
105
- best_positions = [text_split.index(prediction) for prediction in best_predictions]
106
- best_predictions = [s.strip().capitalize() for s in best_predictions]
107
- best_sentence = ", ".join([s.strip().capitalize() for s in best_sentence.split(",")])
108
-
109
- if k == 1:
110
- text = f"The most likely missing species is '{best_predictions[0]}' (position {best_positions[0]})."
111
- elif k == 2:
112
- text = f"The most likely missing species are '{best_predictions[0]}' and '{best_predictions[1]}' (positions {best_positions[0]} and {best_positions[1]})."
113
- else:
114
- text = f"The most likely missing species are " + ', '.join(f"'{s}'" for s in best_predictions[:-1]) + f", and '{best_predictions[-1]}' (positions {', '.join(map(str, best_positions[:-1]))}, and {best_positions[-1]})."
115
- text += f"\nThe completed vegetation plot is thus '{best_sentence}'."
116
- image = return_image("masking", best_predictions[0])
117
- if image is not None:
118
- text += f"\nBelow is an image of the first missing species (i.e., {best_predictions[0]}) taken from the website FloraVEG."
119
- else:
120
- text += f"\nNo image found for the first missing species (i.e., {best_predictions[0]})."
121
- image = image_not_found
122
- return text, image
123
-
124
- with gr.Blocks() as demo:
125
-
126
- gr.Markdown("""<h1 style="text-align: center;">Pl@ntBERT</h1>""")
127
-
128
- with gr.Tab("Vegetation plot classification"):
129
- gr.Markdown("""<h3 style="text-align: center;">Habitat identification of vegetation plots!</h3>""")
130
- with gr.Row():
131
- with gr.Column():
132
- species_classification = gr.Textbox(lines=2, label="Species", placeholder="Enter a list of comma-separated binomial names here.")
133
- k_classification = gr.Slider(1, 5, value=1, step=1, label="Top-k", info="Choose the number of habitat types to display.")
134
- with gr.Column():
135
- text_classification = gr.Textbox(label="Prediction")
136
- image_classification = gr.Image()
137
- button_classification = gr.Button("Classify")
138
- gr.Markdown("""<h5 style="text-align: center;">An example of input</h5>""")
139
- gr.Examples(
140
- examples=[
141
- ["phragmites australis, lemna minor, typha latifolia", 3],
142
- ["fagus sylvatica, prenanthes purpurea, abies alba, cardamine bulbifera, orthilia secunda, oxalis acetosella, rubus idaeus", 1]
143
- ],
144
- inputs=[species_classification, k_classification],
145
- outputs=[text_classification, image_classification],
146
- fn=classification,
147
- cache_examples=True)
148
-
149
- with gr.Tab("Missing species finding"):
150
- gr.Markdown("""<h3 style="text-align: center;">Missing vascular plant species retrieval!</h3>""")
151
- with gr.Row():
152
- with gr.Column():
153
- species_masking = gr.Textbox(lines=2, label="Species", placeholder="Enter a list of comma-separated binomial names here.")
154
- k_masking = gr.Slider(1, 5, value=1, step=1, label="Top-k", info="Choose the number of missing species to find.")
155
- with gr.Column():
156
- text_masking = gr.Textbox(label="Prediction")
157
- image_masking = gr.Image()
158
- button_masking = gr.Button("Find")
159
- gr.Markdown("""<h5 style="text-align: center;">An example of input</h5>""")
160
- gr.Examples(
161
- examples=[
162
- ["calamagrostis arenaria, medicago marina, pancratium maritimum, thinopyrum junceum", 1],
163
- ["trapa natans, lemna minor, phragmites australis, sparganium erectum", 1]
164
- ],
165
- inputs=[species_masking, k_masking],
166
- outputs=[text_masking, image_masking],
167
- fn=masking,
168
- cache_examples=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- button_classification.click(classification, inputs=[species_classification, k_classification], outputs=[text_classification, image_classification])
172
- button_masking.click(masking, inputs=[species_masking, k_masking], outputs=[text_masking, image_masking])
173
 
174
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pl@ntBERT's Gradio interface. Run `python app.py` for a local preview."""
2
+
3
+ import atexit
4
+ import csv
5
+ import json
6
+ import logging
7
+ import os
8
+ import shutil
9
+ import tempfile
10
+ import time
11
+ from pathlib import Path
12
+
13
+ os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
14
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
15
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
16
+
17
  import gradio as gr
18
+
19
+ from illustrations import render_illustration
20
+ from plantbert import ROOT, InputError, ModelUnavailable, TaxonomyError, run_inference
21
+ from presentation import (
22
+ ABOUT,
23
+ ACKNOWLEDGEMENTS,
24
+ CONTACT,
25
+ HERO,
26
+ empty_result,
27
+ error_result,
28
+ render_audit,
29
+ render_normalization,
30
+ render_results,
31
+ )
32
+
33
+ LOGGER = logging.getLogger(__name__)
34
+ EXPORT_DIRECTORY = Path(tempfile.mkdtemp(prefix="plantbert-exports-"))
35
+ atexit.register(shutil.rmtree, EXPORT_DIRECTORY, ignore_errors=True)
36
+
37
+ EXAMPLES = {
38
+ "habitat": [
39
+ ("Tall-helophyte bed", "Phragmites australis, Lemna minor, Typha latifolia"),
40
+ (
41
+ "Fagus forest on non-acid soils",
42
+ "Fagus sylvatica, Prenanthes purpurea, Abies alba, Cardamine bulbifera, Orthilia secunda, Oxalis acetosella, Rubus idaeus",
43
+ ),
44
+ ],
45
+ "species": [
46
+ (
47
+ "Eryngium maritimum",
48
+ "Calamagrostis arenaria, Medicago marina, Pancratium maritimum, Thinopyrum junceum",
49
+ ),
50
+ (
51
+ "Typha angustifolia",
52
+ "Trapa natans, Lemna minor, Phragmites australis, Sparganium erectum",
53
+ ),
54
+ ],
55
+ }
56
+
57
+
58
+ def export_result(result: dict) -> str:
59
+ for old in EXPORT_DIRECTORY.glob("*"):
60
+ if old.stat().st_mtime < time.time() - 3600:
61
+ old.unlink(missing_ok=True)
62
+ with tempfile.NamedTemporaryFile(
63
+ mode="w",
64
+ suffix=".json",
65
+ prefix=f"plantbert-{result['task']}-",
66
+ dir=EXPORT_DIRECTORY,
67
+ delete=False,
68
+ encoding="utf-8",
69
+ ) as handle:
70
+ json.dump(result, handle, ensure_ascii=False, indent=2, allow_nan=False)
71
+ return handle.name
72
+
73
+
74
+ def export_csv(result):
75
+ with tempfile.NamedTemporaryFile(
76
+ mode="w",
77
+ suffix=".csv",
78
+ prefix="plantbert-results-",
79
+ dir=EXPORT_DIRECTORY,
80
+ delete=False,
81
+ encoding="utf-8",
82
+ newline="",
83
+ ) as handle:
84
+ fields = [
85
+ "rank_or_step",
86
+ "code",
87
+ "name",
88
+ "model_score",
89
+ "final_position",
90
+ "normalized_input",
91
+ ]
92
+ writer = csv.DictWriter(handle, fieldnames=fields)
93
+ writer.writeheader()
94
+ for rank, prediction in enumerate(result["predictions"], 1):
95
+ writer.writerow(
96
+ {
97
+ "rank_or_step": prediction.get("step", rank),
98
+ "code": prediction.get("code", ""),
99
+ "name": prediction.get("name", prediction.get("species", "")),
100
+ "model_score": prediction["score"],
101
+ "final_position": prediction.get("final_position", ""),
102
+ "normalized_input": ", ".join(result["input"]["names"]),
103
+ }
104
+ )
105
+ return handle.name
106
+
107
+
108
+ def analyze(text, k, task, progress):
109
+ try:
110
+ result = run_inference(text, k, task, progress)
111
+ return (
112
+ render_results(result),
113
+ render_audit(result),
114
+ result,
115
+ gr.update(value=export_result(result), interactive=True),
116
+ gr.update(value=export_csv(result), interactive=True),
117
+ )
118
+ except (InputError, ModelUnavailable) as error:
119
+ audit = render_normalization(error.entries) if isinstance(error, TaxonomyError) else ""
120
+ return (
121
+ error_result(str(error)),
122
+ audit,
123
+ None,
124
+ gr.update(value=None, interactive=False),
125
+ gr.update(value=None, interactive=False),
126
+ )
127
+ except Exception:
128
+ LOGGER.exception("Pl@ntBERT inference failed")
129
+ return (
130
+ error_result(
131
+ "The model could not complete this request. Please retry with a shorter plot. "
132
+ "If the problem persists, contact the developer through Methods & citation."
133
+ ),
134
+ "",
135
+ None,
136
+ gr.update(value=None, interactive=False),
137
+ gr.update(value=None, interactive=False),
138
+ )
139
+
140
+
141
+ def classify(text, k, progress=gr.Progress()): # noqa: B008 - Gradio injects this default
142
+ return analyze(text, k, "habitat", progress)
143
+
144
+
145
+ def complete(text, k, progress=gr.Progress()): # noqa: B008 - Gradio injects this default
146
+ return analyze(text, k, "species", progress)
147
+
148
+
149
+ def clear_results(task):
150
+ return (
151
+ empty_result(task),
152
+ "Run a prediction to review the names used by the model.",
153
+ None,
154
+ gr.update(value=None, interactive=False),
155
+ gr.update(value=None, interactive=False),
156
+ "",
157
+ )
158
+
159
+
160
+ def build_task(task):
161
+ habitat = task == "habitat"
162
+ with gr.Row(equal_height=True, elem_classes="workspace"):
163
+ with gr.Column(scale=1, min_width=300, elem_classes=["work-panel", "input-panel"]):
164
+ gr.HTML('<div class="section-kicker">01 &nbsp; YOUR VEGETATION PLOT</div>')
165
+ gr.Markdown("### Which plants occur together?")
166
+ species = gr.Textbox(
167
+ label="Scientific plant names",
168
+ lines=4,
169
+ max_lines=10,
170
+ placeholder="Phragmites australis, Lemna minor, Typha latifolia",
171
+ value=EXAMPLES[task][0][1],
172
+ info="Separate names with commas or new lines. Order by decreasing cover when known.",
173
+ elem_classes="species-input",
174
+ )
175
+ gr.HTML('<p class="example-label">START WITH AN EXAMPLE</p>')
176
+ example_buttons = []
177
+ with gr.Row(elem_classes="example-buttons"):
178
+ for label, text in EXAMPLES[task]:
179
+ button = gr.Button(label, size="sm", variant="secondary")
180
+ example_buttons.append(button)
181
+ button.click(
182
+ lambda value=text: value,
183
+ outputs=species,
184
+ queue=False,
185
+ api_visibility="private",
186
+ )
187
+ k = gr.Slider(
188
+ 1,
189
+ 5,
190
+ value=3 if habitat else 1,
191
+ step=1,
192
+ label="Habitats to display" if habitat else "Species to add sequentially",
193
+ info="Compare the highest-scoring habitat classes."
194
+ if habitat
195
+ else "Each addition informs the next. More additions take longer.",
196
+ )
197
+ run = gr.Button(
198
+ "Identify habitat →" if habitat else "Suggest missing species →",
199
+ variant="primary",
200
+ elem_classes="run-button",
201
+ )
202
+ with gr.Column(scale=1, min_width=300, elem_classes=["work-panel", "output-panel"]):
203
+ gr.HTML('<div class="section-kicker">02 &nbsp; MODEL EXPLORATION</div>')
204
+ result = gr.HTML(empty_result(task))
205
+ photo = gr.HTML()
206
+ with gr.Column(elem_classes="record-panel"):
207
+ gr.HTML(
208
+ '<div class="section-kicker">03 &nbsp; CHECK NAMES & SAVE RESULTS</div>'
209
+ '<p class="record-intro">Review the species used, then take your results with you.</p>'
210
  )
211
+ audit = gr.HTML("Run a prediction to review the names used by the model.")
212
+ with gr.Row(elem_classes="download-row"):
213
+ csv_download = gr.DownloadButton(
214
+ "Download results · CSV", interactive=False, variant="primary"
215
+ )
216
+ download = gr.DownloadButton("Download full record · JSON", interactive=False)
217
+ with gr.Accordion(
218
+ "Scientific record · settings, name checks & checkpoint provenance", open=False
219
+ ):
220
+ record = gr.JSON(label="Reproducibility record", value=None)
221
+ outputs = [result, audit, record, download, csv_download]
222
+ controls = [species, k, run, *example_buttons]
223
+ run.click(
224
+ lambda: [gr.update(interactive=False) for _ in controls],
225
+ outputs=controls,
226
+ queue=False,
227
+ api_visibility="private",
228
+ ).then(
229
+ classify if habitat else complete,
230
+ inputs=[species, k],
231
+ outputs=outputs,
232
+ api_name="classify_habitat" if habitat else "suggest_species",
233
+ concurrency_limit=1,
234
+ concurrency_id="plantbert-cpu",
235
+ trigger_mode="once",
236
+ ).then(
237
+ render_illustration,
238
+ inputs=record,
239
+ outputs=photo,
240
+ concurrency_id="illustrations",
241
+ concurrency_limit=4,
242
+ api_visibility="private",
243
+ ).then(
244
+ lambda: [gr.update(interactive=True) for _ in controls],
245
+ outputs=controls,
246
+ queue=False,
247
+ api_visibility="private",
248
+ )
249
+ for component in (species, k):
250
+ component.change(
251
+ lambda: clear_results(task),
252
+ outputs=[*outputs, photo],
253
+ queue=False,
254
+ api_visibility="private",
255
+ )
256
+
257
+
258
+ def build_demo():
259
+ with gr.Blocks(
260
+ title="Pl@ntBERT · Plant community explorer",
261
+ analytics_enabled=False,
262
+ delete_cache=(3600, 3600),
263
+ ) as demo:
264
+ with gr.Row(elem_classes="theme-toolbar"):
265
+ theme_toggle = gr.Button("Dark mode", size="sm", elem_id="theme-toggle")
266
+ demo.load(fn=None, js=(ROOT / "assets/theme.js").read_text(), queue=False)
267
+ theme_toggle.click(
268
+ fn=None,
269
+ js=(ROOT / "assets/theme-change.js").read_text(),
270
+ queue=False,
271
+ )
272
+ gr.HTML(HERO)
273
+ with gr.Tabs(elem_id="task-tabs"):
274
+ with gr.Tab("Identify a habitat", id="habitat"):
275
+ build_task("habitat")
276
+ with gr.Tab("Find missing species", id="species"):
277
+ build_task("species")
278
+ with gr.Tab("Methods & citation", id="methods"):
279
+ gr.Markdown(ABOUT, elem_classes="methods")
280
+ gr.HTML(ACKNOWLEDGEMENTS, elem_classes="methods-funding")
281
+ with gr.Accordion(
282
+ "Contact the developer", open=False, elem_classes="contact-accordion"
283
+ ):
284
+ gr.HTML(CONTACT)
285
+ gr.HTML(
286
+ '<footer class="research-footer"><span><strong>Pl@ntBERT</strong> &nbsp; · &nbsp; Learning the syntax of plant assemblages</span>'
287
+ "<span>Research demo &nbsp; / &nbsp; Interpret with ecological context</span></footer>"
288
+ )
289
+ return demo.queue(max_size=12, default_concurrency_limit=1)
290
+
291
+
292
+ THEME = gr.themes.Base(
293
+ primary_hue="emerald",
294
+ secondary_hue="stone",
295
+ neutral_hue="stone",
296
+ font=["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
297
+ font_mono=["ui-monospace", "monospace"],
298
+ radius_size="lg",
299
+ ).set(
300
+ body_background_fill="#f8f9f5",
301
+ body_background_fill_dark="#14241f",
302
+ body_text_color="#203c32",
303
+ body_text_color_dark="#e2ede6",
304
+ block_background_fill="#ffffff",
305
+ block_background_fill_dark="#1c3028",
306
+ block_border_color="#dce5dd",
307
+ block_border_color_dark="#3c5146",
308
+ button_primary_background_fill="#24664d",
309
+ button_primary_background_fill_hover="#184b38",
310
+ button_primary_text_color="#ffffff",
311
+ button_primary_border_color="#24664d",
312
+ button_primary_background_fill_dark="#79b99a",
313
+ button_primary_text_color_dark="#10271d",
314
+ button_secondary_background_fill="#f1f5ee",
315
+ button_secondary_background_fill_dark="#263e32",
316
+ input_background_fill="#fbfcf9",
317
+ input_background_fill_dark="#14271e",
318
+ input_border_color="#cfdbd0",
319
+ input_border_color_focus="#327557",
320
+ block_shadow="none",
321
+ block_title_text_weight="500",
322
+ )
323
 
 
 
324
 
325
+ if __name__ == "__main__":
326
+ build_demo().launch(
327
+ theme=THEME,
328
+ css_paths=[ROOT / "assets" / "style.css"],
329
+ server_name="0.0.0.0" if os.environ.get("SPACE_ID") else "127.0.0.1",
330
+ server_port=int(os.environ.get("PORT", "7860")),
331
+ share=False,
332
+ ssr_mode=False,
333
+ footer_links=[],
334
+ show_error=False,
335
+ )