Islam Mamedov commited on
Commit
ef3d1e2
·
0 Parent(s):

Initial commit: herbarium baseline + app UI

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pth filter=lfs diff=lfs merge=lfs -text
2
+ *.joblib filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Plant Species Classification
3
+ emoji: 🌿
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # 🌿 Plant Species Classification
14
+
15
+ This is a Gradio app for the AML Group Project by PsychicFireSong.
16
+
17
+ It uses a `ConvNextV2` model fine-tuned on the Herbarium Field dataset to classify plant species from images.
18
+
19
+ **Models Available:**
20
+ - **Herbarium Species Classifier:** The primary model for classification.
21
+ - **Future Model 1 (Placeholder):** Not yet implemented.
22
+ - **Future Model 2 (Placeholder):** Not yet implemented.
app.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from baseline.baseline_convnext import predict_convnext
4
+ from baseline.baseline_infer import predict_baseline
5
+
6
+
7
+ # --- Placeholder models (for future extensions) ---
8
+ def predict_placeholder_1(image):
9
+ if image is None:
10
+ return "Please upload an image."
11
+ return "Model 2 is not available yet. Please check back later."
12
+
13
+
14
+ def predict_placeholder_2(image):
15
+ if image is None:
16
+ return "Please upload an image."
17
+ return "Model 3 is not available yet. Please check back later."
18
+
19
+
20
+ # --- Main Prediction Logic ---
21
+ def predict(model_choice, image):
22
+ if model_choice == "Herbarium Species Classifier":
23
+ # Friend's ConvNeXt mix-stream CNN baseline
24
+ return predict_convnext(image)
25
+ elif model_choice == "Baseline (DINOv2 + LogReg)":
26
+ # Your plant-pretrained DINOv2 + Logistic Regression baseline
27
+ return predict_baseline(image)
28
+ elif model_choice == "Future Model 1 (Placeholder)":
29
+ return predict_placeholder_1(image)
30
+ elif model_choice == "Future Model 2 (Placeholder)":
31
+ return predict_placeholder_2(image)
32
+ else:
33
+ return "Invalid model selected."
34
+
35
+
36
+ # --- Gradio Interface ---
37
+ with gr.Blocks(theme=gr.themes.Soft(), css="style.css") as demo:
38
+ with gr.Column(elem_id="app-wrapper"):
39
+ # Header
40
+ gr.Markdown(
41
+ """
42
+ <div id="app-header">
43
+ <h1>🌿 Plant Species Classification</h1>
44
+ <h3>AML Group Project – PsychicFireSong</h3>
45
+ </div>
46
+ """,
47
+ elem_id="app-header",
48
+ )
49
+
50
+ # Badges row
51
+ gr.Markdown(
52
+ """
53
+ <div id="badge-row">
54
+ <span class="badge">Herbarium + Field images</span>
55
+ <span class="badge">ConvNeXtV2 mix-stream CNN</span>
56
+ <span class="badge">DINOv2 + Logistic Regression</span>
57
+ </div>
58
+ """,
59
+ elem_id="badge-row",
60
+ )
61
+
62
+ # Main card
63
+ with gr.Row(elem_id="main-card"):
64
+ # Left side: model + image
65
+ with gr.Column(scale=1, elem_id="left-panel"):
66
+ model_selector = gr.Dropdown(
67
+ label="Select model",
68
+ choices=[
69
+ "Herbarium Species Classifier",
70
+ "Baseline (DINOv2 + LogReg)",
71
+ "Future Model 1 (Placeholder)",
72
+ "Future Model 2 (Placeholder)",
73
+ ],
74
+ value="Herbarium Species Classifier",
75
+ )
76
+
77
+ gr.Markdown(
78
+ """
79
+ <div id="model-help">
80
+ <b>Herbarium Species Classifier</b> – end-to-end ConvNeXtV2 CNN.<br>
81
+ <b>Baseline</b> – plant-pretrained DINOv2 features + logistic regression head.
82
+ </div>
83
+ """,
84
+ elem_id="model-help",
85
+ )
86
+
87
+ image_input = gr.Image(
88
+ type="pil",
89
+ label="Upload plant image",
90
+ )
91
+ submit_button = gr.Button("Classify 🌱", variant="primary")
92
+
93
+ # Right side: predictions
94
+ with gr.Column(scale=1, elem_id="right-panel"):
95
+ output_label = gr.Label(
96
+ label="Top 5 predictions",
97
+ num_top_classes=5,
98
+ )
99
+
100
+ submit_button.click(
101
+ fn=predict,
102
+ inputs=[model_selector, image_input],
103
+ outputs=output_label,
104
+ )
105
+
106
+ # Optional examples (keep empty if you don't have images)
107
+ gr.Examples(
108
+ examples=[],
109
+ inputs=image_input,
110
+ outputs=output_label,
111
+ fn=lambda img: predict("Herbarium Species Classifier", img),
112
+ cache_examples=False,
113
+ )
114
+
115
+ gr.Markdown(
116
+ "Built for the AML course – compare CNN vs. DINOv2 feature-extractor baselines.",
117
+ elem_id="footer",
118
+ )
119
+
120
+ if __name__ == "__main__":
121
+ demo.launch()
baseline/__pycache__/baseline_convnext.cpython-311.pyc ADDED
Binary file (4.13 kB). View file
 
baseline/__pycache__/baseline_infer.cpython-311.pyc ADDED
Binary file (9.91 kB). View file
 
baseline/baseline_convnext.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # baseline/baseline_convnext.py
2
+ from pathlib import Path
3
+ import torch
4
+ import pandas as pd
5
+ from PIL import Image
6
+ from torchvision import transforms
7
+ from transformers import ConvNextV2ForImageClassification
8
+
9
+ ROOT_DIR = Path(__file__).resolve().parent.parent
10
+ BASELINE_DIR = Path(__file__).resolve().parent
11
+ LIST_DIR = ROOT_DIR / "list"
12
+ MODEL_PATH = BASELINE_DIR / "herbarium_convnext_v2_base.pth"
13
+
14
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
15
+
16
+ # species list
17
+ species_df = pd.read_csv(
18
+ LIST_DIR / "species_list.txt",
19
+ sep=";",
20
+ header=None,
21
+ names=["class_id", "species_name"],
22
+ )
23
+ class_names = list(species_df["species_name"])
24
+ num_labels = len(class_names)
25
+
26
+ data_transforms = transforms.Compose([
27
+ transforms.Resize(256),
28
+ transforms.CenterCrop(224),
29
+ transforms.ToTensor(),
30
+ transforms.Normalize([0.485, 0.456, 0.406],
31
+ [0.229, 0.224, 0.225]),
32
+ ])
33
+
34
+ def _load_model():
35
+ model = ConvNextV2ForImageClassification.from_pretrained(
36
+ "facebook/convnextv2-base-22k-224",
37
+ num_labels=num_labels,
38
+ ignore_mismatched_sizes=True,
39
+ )
40
+ if MODEL_PATH.is_file():
41
+ state = torch.load(MODEL_PATH, map_location=DEVICE)
42
+ model.load_state_dict(state)
43
+ else:
44
+ print(f"[convnext] WARNING: {MODEL_PATH} not found, using HF weights only.")
45
+ model.to(DEVICE)
46
+ model.eval()
47
+ return model
48
+
49
+ convnext_model = _load_model()
50
+
51
+ def predict_convnext(image: Image.Image):
52
+ if image is None:
53
+ return "Please upload an image."
54
+ x = data_transforms(image).unsqueeze(0).to(DEVICE)
55
+ with torch.no_grad():
56
+ logits = convnext_model(x).logits
57
+ prob = torch.softmax(logits, dim=1)[0]
58
+ top5_prob, top5_idx = torch.topk(prob, 5)
59
+ return {class_names[i]: float(p)
60
+ for i, p in zip(top5_idx.cpu().numpy(), top5_prob.cpu().numpy())}
baseline/baseline_infer.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from typing import Dict
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from PIL import Image
8
+
9
+ import torch
10
+ from torchvision import transforms
11
+ import timm
12
+ from timm.models.vision_transformer import resize_pos_embed
13
+ import joblib
14
+
15
+
16
+ # ----------------------- paths & device -----------------------
17
+ ROOT_DIR = Path(__file__).resolve().parent.parent # AMLGroupSpaceFinal/
18
+ BASELINE_DIR = ROOT_DIR / "baseline"
19
+ LIST_DIR = ROOT_DIR / "list"
20
+
21
+ PLANT_CKPT_PATH = BASELINE_DIR / "plant_dinov2_patch14.pth"
22
+ LOGREG_PATH = BASELINE_DIR / "logreg_baseline.joblib"
23
+ SCALER_PATH = BASELINE_DIR / "scaler_baseline.joblib"
24
+ SPECIES_LIST_PATH = LIST_DIR / "species_list.txt"
25
+
26
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
27
+
28
+
29
+ # ----------------------- helpers (trimmed from evaluate.py) -----------------------
30
+ def read_species(p: Path):
31
+ """Read species_list.txt and return list of species names in index order."""
32
+ rows = []
33
+ with open(p, "r", encoding="utf-8") as f:
34
+ for ln in f:
35
+ ln = ln.strip()
36
+ if not ln or ln.startswith("#"):
37
+ continue
38
+ if ";" in ln:
39
+ cid, name = ln.split(";", 1)
40
+ else:
41
+ parts = ln.split()
42
+ cid, name = parts[0], " ".join(parts[1:]) if len(parts) > 1 else ""
43
+ try:
44
+ cid = int(cid)
45
+ except ValueError:
46
+ continue
47
+ rows.append((cid, name))
48
+ df = pd.DataFrame(rows, columns=["class_id", "species_name"])
49
+ # same order as in training: iterrows order
50
+ names = list(df["species_name"])
51
+ return names
52
+
53
+
54
+ def pool_feats(out):
55
+ feats = out
56
+ if isinstance(out, dict):
57
+ for key in ("pooled", "x_norm_clstoken", "cls_token", "x"):
58
+ if key in out:
59
+ feats = out[key]
60
+ break
61
+ if isinstance(feats, (list, tuple)):
62
+ feats = feats[0]
63
+ if isinstance(feats, torch.Tensor) and feats.dim() == 3:
64
+ feats = feats[:, 0] if feats.size(1) > 1 else feats.mean(dim=1)
65
+ if isinstance(feats, torch.Tensor) and feats.dim() > 2:
66
+ feats = feats.flatten(1)
67
+ return feats
68
+
69
+
70
+ def _unwrap_state_dict(obj):
71
+ if isinstance(obj, dict):
72
+ for k in ("state_dict", "model", "module", "ema", "shadow",
73
+ "backbone", "net", "student", "teacher"):
74
+ if k in obj and isinstance(obj[k], dict):
75
+ return obj[k]
76
+ return obj
77
+
78
+
79
+ def _strip_prefixes(sd, prefixes=("module.", "backbone.", "model.", "student.")):
80
+ out = {}
81
+ for k, v in sd.items():
82
+ for p in prefixes:
83
+ if k.startswith(p):
84
+ k = k[len(p):]
85
+ out[k] = v
86
+ return out
87
+
88
+
89
+ def maybe_load_plant_ckpt(model, ckpt_path: Path):
90
+ if not ckpt_path.is_file():
91
+ print(f"[baseline] plant ckpt not found at {ckpt_path}, using generic DINOv2 weights.")
92
+ return
93
+ try:
94
+ sd = torch.load(ckpt_path, map_location="cpu")
95
+ sd = _unwrap_state_dict(sd)
96
+ sd = _strip_prefixes(sd)
97
+
98
+ msd = model.state_dict()
99
+ if "pos_embed" in sd and "pos_embed" in msd and sd["pos_embed"].shape != msd["pos_embed"].shape:
100
+ sd["pos_embed"] = resize_pos_embed(sd["pos_embed"], msd["pos_embed"])
101
+ print(f"[baseline] interpolated pos_embed to {tuple(msd['pos_embed'].shape)}")
102
+
103
+ missing, unexpected = model.load_state_dict(sd, strict=False)
104
+ print(f"[baseline] loaded plant ckpt; missing={len(missing)} unexpected={len(unexpected)}")
105
+ except Exception as e:
106
+ print(f"[baseline] failed to load '{ckpt_path}': {e}")
107
+
108
+
109
+ def build_backbone(size: int = 224):
110
+ model = timm.create_model(
111
+ "vit_base_patch14_dinov2",
112
+ pretrained=True, # generic DINOv2 as fallback
113
+ num_classes=0, # features only
114
+ img_size=size,
115
+ pretrained_cfg_overlay=dict(input_size=(3, size, size)),
116
+ ).to(DEVICE)
117
+
118
+ pe = getattr(model, "patch_embed", None)
119
+ if pe is not None:
120
+ if hasattr(pe, "img_size"):
121
+ pe.img_size = (size, size)
122
+ if hasattr(pe, "strict_img_size"):
123
+ pe.strict_img_size = False
124
+
125
+ maybe_load_plant_ckpt(model, PLANT_CKPT_PATH)
126
+ model.eval()
127
+ for p in model.parameters():
128
+ p.requires_grad = False
129
+ return model
130
+
131
+
132
+ # ----------------------- global objects (loaded once) -----------------------
133
+ IMAGE_SIZE = 224
134
+
135
+ species_names = read_species(SPECIES_LIST_PATH)
136
+ num_classes = len(species_names)
137
+
138
+ backbone = build_backbone(IMAGE_SIZE)
139
+
140
+ transform = transforms.Compose([
141
+ transforms.Resize(int(IMAGE_SIZE * 1.12)),
142
+ transforms.CenterCrop(IMAGE_SIZE),
143
+ transforms.ToTensor(),
144
+ transforms.Normalize([0.485, 0.456, 0.406],
145
+ [0.229, 0.224, 0.225]),
146
+ ])
147
+
148
+ scaler = joblib.load(SCALER_PATH)
149
+ logreg = joblib.load(LOGREG_PATH)
150
+
151
+
152
+ # ----------------------- public API for Gradio -----------------------
153
+ def predict_baseline(image: Image.Image, top_k: int = 5) -> Dict[str, float]:
154
+ """
155
+ Run DINOv2 + Logistic Regression baseline on a single PIL image.
156
+ Returns {class_name: probability} for the top_k classes.
157
+ """
158
+ if image is None:
159
+ return {}
160
+
161
+ x = transform(image).unsqueeze(0).to(DEVICE)
162
+
163
+ with torch.no_grad():
164
+ out = backbone.forward_features(x)
165
+ feats = pool_feats(out).cpu().numpy()
166
+
167
+ feats_scaled = scaler.transform(feats)
168
+ probs = logreg.predict_proba(feats_scaled)[0] # shape [num_classes]
169
+
170
+ top_idx = np.argsort(-probs)[:top_k]
171
+ result = {species_names[i]: float(probs[i]) for i in top_idx}
172
+ return result
baseline/herbarium_convnext_v2_base.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:837cca126e235c0ae822770470e38a3621b81b0ba7e915aaef2b15a7f66914e6
3
+ size 351335085
baseline/logreg_baseline.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c886ec6469fadd59f092adc6f3e08e3cc0859f18c7e7847f20299b775f05a4ba
3
+ size 616839
baseline/plant_dinov2_patch14.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1fe189d76a0ec0128e8a9d4959a218e10c6adc60ab21d0d23b65c7080d1a4407
3
+ size 346384519
baseline/scaler_baseline.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5f569d9cc90e8119c2820ef64b790d4f3bb75faadd7d5bf6d5581a4ea07e8c57
3
+ size 19047
list/species_list.txt ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 105951; Maripa glabra Choisy
2
+ 106023; Merremia umbellata (L.) Hallier f.
3
+ 106387; Costus arabicus L.
4
+ 106461; Costus scaber Ruiz Pav.
5
+ 106466; Costus spiralis (Jacq.) Roscoe
6
+ 110432; Evodianthus funifer (Poit.) Lindm.
7
+ 116853; Pteridium arachnoideum (Kaulf.) Maxon
8
+ 119986; Olfersia cervina (L.) Kunze
9
+ 120497; Diospyros capreifolia Mart. ex Hiern
10
+ 121836; Sloanea grandiflora Sm.
11
+ 121841; Sloanea guianensis (Aubl.) Benth.
12
+ 12254; Anacardium occidentale L.
13
+ 12518; Mangifera indica L.
14
+ 125412; Sphyrospermum cordifolium Benth.
15
+ 126895; Syngonanthus caulescens (Poir.) Ruhland
16
+ 127007; Tonina fluviatilis Aubl.
17
+ 127097; Erythroxylum fimbriatum Peyr.
18
+ 127151; Erythroxylum macrophyllum Cav.
19
+ 127242; Erythroxylum squamatum Sw.
20
+ 12910; Spondias mombin L.
21
+ 12922; Tapirira guianensis Aubl.
22
+ 129645; Croton schiedeanus Schltdl.
23
+ 130657; Euphorbia cotinifolia L.
24
+ 131079; Euphorbia heterophylla L.
25
+ 131736; Euphorbia prostrata Aiton
26
+ 132107; Euphorbia thymifolia L.
27
+ 132113; Euphorbia tithymaloides L.
28
+ 132431; Hura crepitans L.
29
+ 132476; Jatropha curcas L.
30
+ 132501; Jatropha gossypiifolia L.
31
+ 13276; Annona ambotay Aubl.
32
+ 13325; Annona foetida Mart.
33
+ 13330; Annona glabra L.
34
+ 133595; Ricinus communis L.
35
+ 133617; Sapium glandulosum (L.) Morong
36
+ 13370; Annona muricata L.
37
+ 136761; Potalia amara Aubl.
38
+ 138662; Chrysothemis pulchella (Donn ex Sims) Decne.
39
+ 140367; Lembocarpus amoenus Leeuwenb.
40
+ 141068; Sinningia incarnata (Aubl.) D.L.Denham
41
+ 141332; Dicranopteris flexuosa (Schrad.) Underw.
42
+ 141336; Dicranopteris pectinata (Willd.) Underw.
43
+ 142550; Heliconia chartacea Lane ex Barreiros
44
+ 142736; Hernandia guianensis Aubl.
45
+ 143496; Hymenophyllum hirsutum (L.) Sw.
46
+ 14353; Guatteria ouregou (Aubl.) Dunal
47
+ 143706; Trichomanes diversifrons (Bory) Mett. ex Sadeb.
48
+ 143758; Trichomanes punctatum Poir.
49
+ 14401; Guatteria scandens Ducke
50
+ 144394; Didymochlaena truncatula (Sw.) J. Sm.
51
+ 145020; Cipura paludosa Aubl.
52
+ 148220; Aegiphila macrantha Ducke
53
+ 148977; Clerodendrum paniculatum L.
54
+ 149264; Congea tomentosa Roxb.
55
+ 149682; Gmelina philippensis Cham.
56
+ 149919; Holmskioldia sanguinea Retz.
57
+ 150135; Hyptis lanceolata Poir.
58
+ 15014; Rollinia mucosa (Jacq.) Baill.
59
+ 151469; Ocimum campechianum Mill.
60
+ 151593; Orthosiphon aristatus (Blume) Miq.
61
+ 15318; Xylopia aromatica (Lam.) Mart.
62
+ 15330; Xylopia cayennensis Maas
63
+ 15355; Xylopia frutescens Aubl.
64
+ 156516; Aniba guianensis Aubl.
65
+ 156526; Aniba megaphylla Mez
66
+ 158341; Nectandra cissiflora Nees
67
+ 158592; Ocotea cernua (Nees) Mez
68
+ 158653; Ocotea floribunda (Sw.) Mez
69
+ 158736; Ocotea longifolia Kunth
70
+ 158793; Ocotea oblonga (Meisn.) Mez
71
+ 158833; Ocotea puberula (Rich.) Nees
72
+ 159434; Couratari guianensis Aubl.
73
+ 159516; Eschweilera parviflora (Aubl.) Miers
74
+ 159518; Eschweilera pedicellata (Rich.) S.A.Mori
75
+ 160570; Acacia mangium Willd.
76
+ 166822; Caesalpinia pulcherrima (L.) Sw.
77
+ 166869; Cajanus cajan (L.) Millsp.
78
+ 169293; Crotalaria retusa L.
79
+ 171727; Erythrina fusca Lour.
80
+ 173914; Inga alba (Sw.) Willd.
81
+ 173972; Inga capitata Desv.
82
+ 174017; Inga edulis Mart.
83
+ 177730; Mimosa pigra L.
84
+ 177775; Mimosa pudica L.
85
+ 189669; Punica granatum L.
86
+ 191642; Adansonia digitata L.
87
+ 19165; Allamanda cathartica L.
88
+ 192311; Ceiba pentandra (L.) Gaertn.
89
+ 194035; Hibiscus rosa-sinensis L.
90
+ 19489; Asclepias curassavica L.
91
+ 209328; Psidium guineense Sw.
92
+ 211059; Nephrolepis biserrata (Sw.) Schott
93
+ 244705; Averrhoa carambola L.
94
+ 248392; Turnera ulmifolia L.
95
+ 254180; Piper peltatum L.
96
+ 275029; Eichhornia crassipes (Mart.) Solms
97
+ 280085; Ceratopteris thalictroides (L.) Brongn.
98
+ 280698; Pityrogramma calomelanos (L.) Link
99
+ 285398; Cassipourea guianensis Aubl.
100
+ 29686; Oreopanax capitatus (Jacq.) Decne. Planch.
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ transformers
4
+ pandas
5
+ gradio
6
+ accelerate
style.css ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ---------- Global background & typography ---------- */
2
+
3
+ body {
4
+ background: radial-gradient(circle at top, #e0f2fe 0, #f9fafb 45%, #f8fafc 100%);
5
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
6
+ }
7
+
8
+ /* ---------- Main wrapper ---------- */
9
+
10
+ #app-wrapper {
11
+ max-width: 1100px;
12
+ margin: 0 auto;
13
+ padding: 24px 16px 40px;
14
+ }
15
+
16
+ /* ---------- Header ---------- */
17
+
18
+ #app-header h1 {
19
+ font-size: 2.2rem;
20
+ margin-bottom: 0.2rem;
21
+ }
22
+
23
+ #app-header h3 {
24
+ margin-top: 0;
25
+ font-weight: 500;
26
+ color: #6b7280;
27
+ }
28
+
29
+ /* ---------- Info chips under title ---------- */
30
+
31
+ #badge-row {
32
+ margin-top: 6px;
33
+ margin-bottom: 6px;
34
+ }
35
+
36
+ .badge {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ padding: 4px 10px;
40
+ margin-right: 8px;
41
+ margin-bottom: 4px;
42
+ border-radius: 999px;
43
+ background: #ecfdf5;
44
+ border: 1px solid #bbf7d0;
45
+ font-size: 12px;
46
+ color: #166534;
47
+ }
48
+
49
+ /* ---------- Main card ---------- */
50
+
51
+ #main-card {
52
+ margin-top: 18px;
53
+ padding: 18px 20px 22px;
54
+ border-radius: 20px;
55
+ background: rgba(255, 255, 255, 0.98);
56
+ box-shadow: 0 22px 48px rgba(15, 23, 42, 0.18);
57
+ }
58
+
59
+ /* Left (controls) / right (outputs) panels */
60
+
61
+ #left-panel {
62
+ border-right: 1px solid #e5e7eb;
63
+ padding-right: 18px;
64
+ }
65
+
66
+ #right-panel {
67
+ padding-left: 18px;
68
+ }
69
+
70
+ /* Small helper text under model dropdown */
71
+
72
+ #model-help {
73
+ font-size: 12px;
74
+ color: #6b7280;
75
+ margin-top: 4px;
76
+ }
77
+
78
+ /* Make the main button a bit more pill-like */
79
+
80
+ button.primary,
81
+ .gr-button-primary {
82
+ border-radius: 999px !important;
83
+ padding: 8px 18px !important;
84
+ font-weight: 600 !important;
85
+ }
86
+
87
+ /* Footer */
88
+
89
+ #footer {
90
+ margin-top: 18px;
91
+ text-align: center;
92
+ font-size: 12px;
93
+ color: #94a3b8;
94
+ }