nsankura commited on
Commit
0ee7aef
·
0 Parent(s):

Deploy Aquaculture Vision

Browse files
Files changed (12) hide show
  1. .gitattributes +37 -0
  2. Dockerfile +16 -0
  3. README.md +9 -0
  4. UAPB-logo.png +3 -0
  5. ara-logo.png +3 -0
  6. dr-ramena.png +3 -0
  7. gsla-header-logo.png +3 -0
  8. gsla-logo.png +3 -0
  9. index.html +1511 -0
  10. render.yaml +10 -0
  11. requirements.txt +10 -0
  12. server.py +726 -0
.gitattributes ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ *.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+ ENV PATH="/home/user/.local/bin:$PATH"
5
+ WORKDIR /app
6
+
7
+ USER root
8
+ RUN apt-get update && apt-get install -y libgl1-mesa-glx libglib2.0-0 && rm -rf /var/lib/apt/lists/*
9
+ USER user
10
+
11
+ COPY --chown=user ./requirements.txt requirements.txt
12
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
13
+ COPY --chown=user . /app
14
+
15
+ EXPOSE 7860
16
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: UAPB Aquaculture Vision
3
+ emoji: 🐟
4
+ colorFrom: yellow
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
UAPB-logo.png ADDED

Git LFS Details

  • SHA256: 13fac83f7b7a459b45f74ba4c844d7089dd3f96a85ef26efc20a3c9ab47a19bd
  • Pointer size: 130 Bytes
  • Size of remote file: 30 kB
ara-logo.png ADDED

Git LFS Details

  • SHA256: 63e3011aa5965f281f0ea9ab8019a93201ba021bb2b71e5b9ec23b02f03b2c51
  • Pointer size: 130 Bytes
  • Size of remote file: 36.9 kB
dr-ramena.png ADDED

Git LFS Details

  • SHA256: 77ffc3cd4eb1ba8d370a5ae2900e70cb62a5c684eb861f0f04c6a1d35521c757
  • Pointer size: 131 Bytes
  • Size of remote file: 474 kB
gsla-header-logo.png ADDED

Git LFS Details

  • SHA256: 19d89bd5aec37ee5d0ba8578cb9b19d27a9048c3aa6b5c405dabdfb2c7307271
  • Pointer size: 129 Bytes
  • Size of remote file: 7.13 kB
gsla-logo.png ADDED

Git LFS Details

  • SHA256: 19d89bd5aec37ee5d0ba8578cb9b19d27a9048c3aa6b5c405dabdfb2c7307271
  • Pointer size: 129 Bytes
  • Size of remote file: 7.13 kB
index.html ADDED
@@ -0,0 +1,1511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Department of Aquaculture and Fisheries</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --uapb-gold: #E8A317;
13
+ --uapb-gold-light: #F0B830;
14
+ --uapb-gold-dark: #D4950F;
15
+ --uapb-black: #1A1A2E;
16
+ --uapb-dark: #16213E;
17
+
18
+ --bg-primary: #FFFFFF;
19
+ --bg-secondary: #F8FAFC;
20
+ --bg-tertiary: #F1F5F9;
21
+
22
+ --text-primary: #0F172A;
23
+ --text-secondary: #475569;
24
+ --text-tertiary: #94A3B8;
25
+
26
+ --border-light: #E2E8F0;
27
+ --border-default: #CBD5E1;
28
+
29
+ --accent-blue: #0066FF;
30
+ --accent-green: #00C48C;
31
+ --accent-purple: #7B61FF;
32
+ --accent-orange: #FF6B35;
33
+
34
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
35
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
36
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
37
+
38
+ --radius-sm: 6px;
39
+ --radius-md: 10px;
40
+ --radius-lg: 16px;
41
+ --radius-full: 9999px;
42
+ }
43
+ .nav-wrapper {
44
+ position: relative;
45
+ overflow: hidden;
46
+ background: rgba(10, 15, 30, 0.65);
47
+ }
48
+
49
+ * { box-sizing: border-box; margin: 0; padding: 0; }
50
+
51
+ body {
52
+ font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, sans-serif;
53
+ background: var(--bg-primary);
54
+ color: var(--text-primary);
55
+ min-height: 100vh;
56
+ line-height: 1.6;
57
+ }
58
+
59
+ /* UAPB Header - Black with Gradient Fade */
60
+ .uapb-header {
61
+ background: linear-gradient(to right, var(--uapb-black) 0%, var(--uapb-black) 55%, #2d3748 100%);
62
+ border-bottom: 4px solid var(--uapb-gold);
63
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
64
+ }
65
+ /* Line 59 - existing */
66
+ }
67
+
68
+ /* ← PASTE HERE starting at line 60 */
69
+ /* GSLA Header */
70
+ .gsla-header {
71
+ background: linear-gradient(to right, #1a3a4a 0%, #1a3a4a 55%, #2d5566 100%);
72
+ border-bottom: 4px solid #7BA7A0;
73
+ box-shadow: 0 2px 10px rgba(0,0,0,0.3);
74
+ }
75
+ .gsla-header .header-text h1 {
76
+ font-family: Oswald, sans-serif;
77
+ font-size: 1.8rem;
78
+ font-weight: 700;
79
+ color: var(--uapb-gold);
80
+ text-transform: uppercase;
81
+ letter-spacing: 1px;
82
+ }
83
+
84
+ .gsla-header .header-text p {
85
+ font-size: 1rem;
86
+ color: #C4A882; /* ← bright white, fully visible */
87
+ margin-top: 6px;
88
+ font-weight: 300;
89
+ letter-spacing: 2px;
90
+ text-transform: uppercase;
91
+ }
92
+
93
+
94
+ /* ← Then line 62 continues with .header-content as normal */
95
+ .header-content {
96
+ max-width: 1400px;
97
+ margin: 0 auto;
98
+ padding: 0 24px;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ min-height: 170px;
103
+ }
104
+ .gsla-header .header-content {
105
+ min-height: 100px; /* smaller than UAPB's 170px */
106
+ }
107
+
108
+
109
+ .header-left {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 20px;
113
+ }
114
+
115
+ .dept-logo {
116
+ height: 100%;
117
+ max-height: 160px;
118
+ width: auto;
119
+ padding: 5px 0;
120
+ }
121
+
122
+ .header-text h1 {
123
+ font-family: 'Oswald', sans-serif;
124
+ font-size: 1.8rem;
125
+ font-weight: 700;
126
+ color: var(--uapb-gold);
127
+ text-transform: uppercase;
128
+ letter-spacing: 1px;
129
+ }
130
+
131
+ .header-text p {
132
+ font-size: 0.95rem;
133
+ color: rgba(255,255,255,0.7);
134
+ margin-top: 4px;
135
+ }
136
+
137
+ .header-right {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 20px;
141
+ }
142
+
143
+ .partner-logo {
144
+ display: flex;
145
+ flex-direction: column;
146
+ align-items: center;
147
+ gap: 6px;
148
+ padding: 10px 16px;
149
+ text-decoration: none;
150
+ transition: all 0.2s ease;
151
+ }
152
+
153
+ .partner-logo:hover {
154
+ opacity: 0.85;
155
+ }
156
+
157
+ .partner-logo img {
158
+ height: 55px;
159
+ width: auto;
160
+ }
161
+
162
+ .partner-label {
163
+ font-size: 0.6rem;
164
+ color: rgba(255,255,255,0.7);
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.5px;
167
+ font-weight: 600;
168
+ }
169
+
170
+ .status-badge {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 8px;
174
+ padding: 8px 16px;
175
+ background: rgba(232, 163, 23, 0.15);
176
+ border-radius: var(--radius-full);
177
+ font-size: 0.8rem;
178
+ color: rgba(255,255,255,0.8);
179
+ }
180
+
181
+ .status-dot {
182
+ width: 8px;
183
+ height: 8px;
184
+ border-radius: 50%;
185
+ background: #64748B;
186
+ }
187
+
188
+ .status-dot.online { background: var(--accent-green); box-shadow: 0 0 8px var(--accent-green); }
189
+ .status-dot.offline { background: #EF4444; }
190
+
191
+ /* Main Navigation - Gold Bar */
192
+ .main-nav
193
+ {
194
+ background: none;
195
+ position: relative;
196
+ overflow: hidden;
197
+ }
198
+ .nav-video-bg {
199
+ position: absolute;
200
+ top: 50%; left: 50%;
201
+ transform: translate(-50%, -50%);
202
+ min-width: 100%; min-height: 100%;
203
+ width: auto; height: auto;
204
+ z-index: 0;
205
+ opacity: 1;
206
+ object-fit: cover;
207
+ pointer-events: none;
208
+ }
209
+ .nav-content, .sub-nav-content { position: relative; z-index: 1; }
210
+
211
+ .nav-content {
212
+ max-width: 1400px;
213
+ margin: 0 auto;
214
+ padding: 0 24px;
215
+ display: flex;
216
+ align-items: center;
217
+ }
218
+
219
+ .nav-tab {
220
+ font-family: 'Oswald', sans-serif;
221
+ font-size: 1rem;
222
+ font-weight: 600;
223
+ text-transform: uppercase;
224
+ letter-spacing: 1px;
225
+ color: #FFFFFF;
226
+ padding: 16px 28px;
227
+ border: none;
228
+ background: none;
229
+ cursor: pointer;
230
+ transition: background 0.2s;
231
+ text-decoration: none;
232
+ }
233
+
234
+ .nav-tab:hover {
235
+ background: rgba(255,255,255,0.2);
236
+ }
237
+
238
+ .nav-tab.active {
239
+ background: rgba(255,255,255,0.25);
240
+ }
241
+
242
+ /* Sub Navigation for Species */
243
+ /* Sub Navigation for Species */
244
+ .sub-nav {
245
+ background: none;
246
+ border-bottom: none; /* ← remove the border line */
247
+ position: relative;
248
+ overflow: hidden;
249
+ }
250
+ .sub-nav-content {
251
+ max-width: 1400px;
252
+ margin: 0 auto;
253
+ padding: 0 24px;
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 8px;
257
+ min-height: 110px;
258
+ }
259
+
260
+ .sub-nav-tab {
261
+ display: flex;
262
+ flex-direction: column;
263
+ align-items: center;
264
+ padding: 14px 24px;
265
+ font-size: 0.85rem;
266
+ font-weight: 600;
267
+ color: #FFFFFF;
268
+ text-decoration: none;
269
+ border: none;
270
+ background: none;
271
+ cursor: pointer;
272
+ transition: all 0.2s;
273
+ border-bottom: 3px solid transparent;
274
+ }
275
+
276
+ .sub-nav-tab:hover {
277
+ color: var(--uapb-gold-dark);
278
+ background: var(--bg-tertiary);
279
+ }
280
+
281
+ .sub-nav-tab.active {
282
+ color: var(--uapb-gold-dark);
283
+ border-bottom-color: var(--uapb-gold);
284
+ background: var(--bg-primary);
285
+ }
286
+
287
+ .sub-nav-tab .scientific-name {
288
+ font-weight: 700;
289
+ font-style: italic;
290
+ }
291
+
292
+ .sub-nav-tab .common-name {
293
+ font-size: 0.75rem;
294
+ font-weight: 400;
295
+ color: var(--text-tertiary);
296
+ margin-top: 2px;
297
+ }
298
+
299
+ .sub-nav-tab.active .common-name {
300
+ color: var(--text-secondary);
301
+ }
302
+
303
+ /* Tab Content */
304
+ .tab-content {
305
+ display: none;
306
+ }
307
+
308
+ .tab-content.active {
309
+ display: block;
310
+ }
311
+
312
+ /* Species Content */
313
+ .species-content {
314
+ display: none;
315
+ }
316
+
317
+ .species-content.active {
318
+ display: block;
319
+ }
320
+
321
+ /* Main Content */
322
+ .main-content {
323
+ max-width: 1400px;
324
+ margin: 0 auto;
325
+ padding: 32px 24px;
326
+ }
327
+
328
+ /* Page Header */
329
+ .page-header {
330
+ margin-bottom: 32px;
331
+ }
332
+
333
+ .page-header h2 {
334
+ font-family: 'Oswald', sans-serif;
335
+ font-size: 2rem;
336
+ font-weight: 700;
337
+ color: var(--text-primary);
338
+ text-transform: uppercase;
339
+ letter-spacing: 1px;
340
+ margin-bottom: 8px;
341
+ }
342
+
343
+ .page-header h2 span {
344
+ color: var(--uapb-gold-dark);
345
+ }
346
+
347
+ .page-header p {
348
+ font-size: 1rem;
349
+ color: var(--text-secondary);
350
+ max-width: 700px;
351
+ }
352
+
353
+ .species-badge {
354
+ display: inline-flex;
355
+ align-items: center;
356
+ gap: 8px;
357
+ padding: 10px 18px;
358
+ background: rgba(232, 163, 23, 0.1);
359
+ border: 1px solid var(--uapb-gold);
360
+ border-radius: var(--radius-full);
361
+ font-size: 0.9rem;
362
+ color: var(--uapb-gold-dark);
363
+ margin-top: 16px;
364
+ }
365
+
366
+ .species-badge em {
367
+ font-weight: 600;
368
+ }
369
+
370
+ /* Analysis Grid */
371
+ .analysis-grid {
372
+ display: grid;
373
+ grid-template-columns: 1fr 380px;
374
+ gap: 24px;
375
+ }
376
+
377
+ @media (max-width: 1100px) {
378
+ .analysis-grid { grid-template-columns: 1fr; }
379
+ }
380
+
381
+ /* Cards */
382
+ .card {
383
+ background: var(--bg-primary);
384
+ border: 1px solid var(--border-light);
385
+ border-radius: var(--radius-lg);
386
+ overflow: hidden;
387
+ box-shadow: var(--shadow-sm);
388
+ }
389
+
390
+ .card-header {
391
+ display: flex;
392
+ align-items: center;
393
+ justify-content: space-between;
394
+ padding: 16px 20px;
395
+ border-bottom: 1px solid var(--border-light);
396
+ background: var(--bg-secondary);
397
+ }
398
+
399
+ .card-title {
400
+ font-family: 'Oswald', sans-serif;
401
+ font-size: 0.9rem;
402
+ font-weight: 600;
403
+ color: var(--text-primary);
404
+ text-transform: uppercase;
405
+ letter-spacing: 0.5px;
406
+ }
407
+
408
+ .card-badge {
409
+ padding: 4px 10px;
410
+ background: rgba(232, 163, 23, 0.15);
411
+ border-radius: var(--radius-full);
412
+ font-size: 0.65rem;
413
+ font-weight: 600;
414
+ color: var(--uapb-gold-dark);
415
+ text-transform: uppercase;
416
+ letter-spacing: 0.5px;
417
+ }
418
+
419
+ .card-body {
420
+ padding: 20px;
421
+ }
422
+
423
+ /* Stats Grid */
424
+ .stats-grid {
425
+ display: grid;
426
+ grid-template-columns: repeat(4, 1fr);
427
+ gap: 16px;
428
+ margin-bottom: 24px;
429
+ }
430
+
431
+ @media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
432
+ @media (max-width: 640px) { .stats-grid { grid-template-columns: 1fr; } }
433
+
434
+ .stat-card {
435
+ background: var(--bg-primary);
436
+ border: 1px solid var(--border-light);
437
+ border-radius: var(--radius-lg);
438
+ padding: 20px;
439
+ border-top: 4px solid var(--uapb-gold);
440
+ }
441
+
442
+ .stat-label {
443
+ font-family: 'Oswald', sans-serif;
444
+ font-size: 0.7rem;
445
+ font-weight: 600;
446
+ color: var(--text-tertiary);
447
+ text-transform: uppercase;
448
+ letter-spacing: 1px;
449
+ margin-bottom: 8px;
450
+ }
451
+
452
+ .stat-value {
453
+ font-family: 'Oswald', sans-serif;
454
+ font-size: 2rem;
455
+ font-weight: 700;
456
+ color: var(--text-primary);
457
+ }
458
+
459
+ .stat-value .unit {
460
+ font-size: 0.9rem;
461
+ font-weight: 500;
462
+ color: var(--text-tertiary);
463
+ margin-left: 4px;
464
+ }
465
+
466
+ /* Charts Grid */
467
+ .charts-grid {
468
+ display: grid;
469
+ grid-template-columns: 1fr 1fr;
470
+ gap: 16px;
471
+ margin-bottom: 24px;
472
+ }
473
+
474
+ @media (max-width: 800px) { .charts-grid { grid-template-columns: 1fr; } }
475
+
476
+ .chart-placeholder {
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ height: 150px;
481
+ background: var(--bg-tertiary);
482
+ border-radius: var(--radius-md);
483
+ color: var(--text-tertiary);
484
+ font-size: 0.85rem;
485
+ }
486
+
487
+ .chart-body img {
488
+ width: 100%;
489
+ height: 150px;
490
+ object-fit: contain;
491
+ border-radius: var(--radius-md);
492
+ cursor: pointer;
493
+ transition: transform 0.2s;
494
+ }
495
+
496
+ .chart-body img:hover {
497
+ transform: scale(1.02);
498
+ }
499
+
500
+ /* Upload Zone */
501
+ .upload-zone {
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ padding: 40px 20px;
507
+ border: 2px dashed var(--border-default);
508
+ border-radius: var(--radius-lg);
509
+ background: var(--bg-tertiary);
510
+ cursor: pointer;
511
+ transition: all 0.2s;
512
+ }
513
+
514
+ .upload-zone:hover, .upload-zone.drag-over {
515
+ border-color: var(--uapb-gold);
516
+ background: rgba(232, 163, 23, 0.05);
517
+ }
518
+
519
+ .upload-zone input { display: none; }
520
+
521
+ .upload-icon {
522
+ width: 56px;
523
+ height: 56px;
524
+ background: var(--uapb-gold);
525
+ border-radius: var(--radius-lg);
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ margin-bottom: 16px;
530
+ }
531
+
532
+ .upload-icon svg {
533
+ width: 28px;
534
+ height: 28px;
535
+ color: white;
536
+ }
537
+
538
+ .upload-zone h3 {
539
+ font-family: 'Oswald', sans-serif;
540
+ font-size: 1rem;
541
+ font-weight: 600;
542
+ text-transform: uppercase;
543
+ margin-bottom: 4px;
544
+ }
545
+
546
+ .upload-zone p {
547
+ font-size: 0.8rem;
548
+ color: var(--text-tertiary);
549
+ }
550
+
551
+ /* File Info & Progress */
552
+ .file-info {
553
+ display: flex;
554
+ justify-content: space-between;
555
+ align-items: center;
556
+ padding: 12px 16px;
557
+ background: var(--bg-tertiary);
558
+ border-radius: var(--radius-md);
559
+ margin-top: 16px;
560
+ font-size: 0.85rem;
561
+ }
562
+
563
+ .file-count { font-weight: 600; }
564
+ .file-size { color: var(--text-tertiary); font-family: monospace; font-size: 0.75rem; }
565
+
566
+ .progress-container { margin-top: 16px; }
567
+
568
+ .progress-bar {
569
+ height: 6px;
570
+ background: var(--bg-tertiary);
571
+ border-radius: var(--radius-full);
572
+ overflow: hidden;
573
+ }
574
+
575
+ .progress-fill {
576
+ height: 100%;
577
+ width: 0%;
578
+ background: var(--uapb-gold);
579
+ transition: width 0.3s;
580
+ }
581
+
582
+ .progress-text {
583
+ display: flex;
584
+ justify-content: space-between;
585
+ margin-top: 6px;
586
+ font-size: 0.7rem;
587
+ color: var(--text-tertiary);
588
+ }
589
+
590
+ /* Buttons */
591
+ .button-group {
592
+ display: flex;
593
+ gap: 12px;
594
+ margin-top: 20px;
595
+ }
596
+
597
+ .btn {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ gap: 8px;
602
+ padding: 12px 24px;
603
+ font-family: 'Oswald', sans-serif;
604
+ font-size: 0.9rem;
605
+ font-weight: 600;
606
+ text-transform: uppercase;
607
+ letter-spacing: 1px;
608
+ border-radius: var(--radius-md);
609
+ border: none;
610
+ cursor: pointer;
611
+ transition: all 0.2s;
612
+ }
613
+
614
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
615
+ .btn svg { width: 18px; height: 18px; }
616
+
617
+ .btn-primary {
618
+ background: var(--uapb-gold);
619
+ color: #FFFFFF;
620
+ }
621
+
622
+ .btn-primary:hover:not(:disabled) {
623
+ background: var(--uapb-gold-dark);
624
+ }
625
+
626
+ .btn-secondary {
627
+ background: var(--bg-tertiary);
628
+ color: var(--text-secondary);
629
+ border: 1px solid var(--border-default);
630
+ }
631
+
632
+ .btn-secondary:hover:not(:disabled) {
633
+ background: var(--bg-primary);
634
+ border-color: var(--uapb-gold);
635
+ }
636
+
637
+ /* Error Box */
638
+ .error-box {
639
+ padding: 14px;
640
+ background: #FEF2F2;
641
+ border-left: 4px solid #EF4444;
642
+ border-radius: var(--radius-md);
643
+ color: #DC2626;
644
+ font-size: 0.85rem;
645
+ margin-top: 16px;
646
+ display: none;
647
+ }
648
+
649
+ .error-box.visible { display: block; }
650
+
651
+ /* Results Section */
652
+ .section-header {
653
+ display: flex;
654
+ justify-content: space-between;
655
+ align-items: center;
656
+ margin-bottom: 16px;
657
+ }
658
+
659
+ .section-title {
660
+ font-family: 'Oswald', sans-serif;
661
+ font-size: 1.1rem;
662
+ font-weight: 600;
663
+ text-transform: uppercase;
664
+ letter-spacing: 0.5px;
665
+ }
666
+
667
+ .section-count {
668
+ font-size: 0.8rem;
669
+ color: var(--text-tertiary);
670
+ }
671
+
672
+ .results-grid {
673
+ display: grid;
674
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
675
+ gap: 16px;
676
+ }
677
+
678
+ .result-card {
679
+ background: var(--bg-primary);
680
+ border: 1px solid var(--border-light);
681
+ border-radius: var(--radius-lg);
682
+ overflow: hidden;
683
+ transition: all 0.2s;
684
+ }
685
+
686
+ .result-card:hover {
687
+ box-shadow: var(--shadow-lg);
688
+ }
689
+
690
+ .result-header {
691
+ display: flex;
692
+ justify-content: space-between;
693
+ align-items: center;
694
+ padding: 12px 16px;
695
+ background: var(--bg-secondary);
696
+ border-bottom: 1px solid var(--border-light);
697
+ }
698
+
699
+ .result-filename {
700
+ font-size: 0.8rem;
701
+ font-weight: 600;
702
+ max-width: 140px;
703
+ overflow: hidden;
704
+ text-overflow: ellipsis;
705
+ white-space: nowrap;
706
+ }
707
+
708
+ .result-stats {
709
+ display: flex;
710
+ gap: 6px;
711
+ }
712
+
713
+ .result-stat {
714
+ padding: 3px 8px;
715
+ background: var(--bg-primary);
716
+ border: 1px solid var(--border-light);
717
+ border-radius: var(--radius-full);
718
+ font-size: 0.65rem;
719
+ }
720
+
721
+ .result-stat strong {
722
+ color: var(--uapb-gold-dark);
723
+ }
724
+
725
+ .result-image {
726
+ position: relative;
727
+ cursor: pointer;
728
+ }
729
+
730
+ .result-image img {
731
+ display: block;
732
+ width: 100%;
733
+ height: 180px;
734
+ object-fit: cover;
735
+ }
736
+
737
+ .click-hint {
738
+ position: absolute;
739
+ bottom: 8px;
740
+ right: 8px;
741
+ background: rgba(0,0,0,0.7);
742
+ color: white;
743
+ padding: 4px 8px;
744
+ border-radius: 4px;
745
+ font-size: 0.6rem;
746
+ opacity: 0;
747
+ transition: opacity 0.2s;
748
+ }
749
+
750
+ .result-image:hover .click-hint { opacity: 1; }
751
+
752
+ .result-data {
753
+ padding: 10px 16px;
754
+ background: var(--bg-tertiary);
755
+ font-family: monospace;
756
+ font-size: 0.6rem;
757
+ color: var(--text-secondary);
758
+ max-height: 50px;
759
+ overflow-y: auto;
760
+ }
761
+
762
+ /* Extension Page Styles */
763
+ .profile-section {
764
+ display: grid;
765
+ grid-template-columns: 300px 1fr;
766
+ gap: 32px;
767
+ margin-bottom: 40px;
768
+ }
769
+
770
+ @media (max-width: 900px) {
771
+ .profile-section { grid-template-columns: 1fr; }
772
+ }
773
+
774
+ .profile-card {
775
+ background: var(--bg-primary);
776
+ border: 1px solid var(--border-light);
777
+ border-radius: var(--radius-lg);
778
+ padding: 32px;
779
+ text-align: center;
780
+ border-top: 4px solid var(--uapb-gold);
781
+ }
782
+
783
+ .profile-avatar {
784
+ width: 130px;
785
+ height: 130px;
786
+ background: var(--uapb-gold);
787
+ border-radius: 50%;
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: center;
791
+ margin: 0 auto 20px;
792
+ font-family: 'Oswald', sans-serif;
793
+ font-size: 3rem;
794
+ font-weight: 700;
795
+ color: white;
796
+ overflow: hidden;
797
+ border: 4px solid var(--uapb-gold);
798
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
799
+ }
800
+
801
+ .profile-avatar img {
802
+ width: 100%;
803
+ height: 100%;
804
+ object-fit: cover;
805
+ }
806
+
807
+ .profile-name {
808
+ font-family: 'Oswald', sans-serif;
809
+ font-size: 1.4rem;
810
+ font-weight: 700;
811
+ color: var(--text-primary);
812
+ margin-bottom: 8px;
813
+ }
814
+
815
+ .profile-title {
816
+ font-size: 0.9rem;
817
+ color: var(--uapb-gold-dark);
818
+ font-weight: 600;
819
+ margin-bottom: 4px;
820
+ }
821
+
822
+ .profile-dept {
823
+ font-size: 0.85rem;
824
+ color: var(--text-secondary);
825
+ margin-bottom: 20px;
826
+ }
827
+
828
+ .profile-links {
829
+ display: flex;
830
+ flex-direction: column;
831
+ gap: 8px;
832
+ }
833
+
834
+ .profile-link {
835
+ padding: 10px 16px;
836
+ background: var(--bg-tertiary);
837
+ border-radius: var(--radius-md);
838
+ font-size: 0.85rem;
839
+ color: var(--text-secondary);
840
+ text-decoration: none;
841
+ transition: all 0.2s;
842
+ }
843
+
844
+ .profile-link:hover {
845
+ background: var(--uapb-gold);
846
+ color: #FFFFFF;
847
+ }
848
+
849
+ .research-areas {
850
+ display: grid;
851
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
852
+ gap: 20px;
853
+ }
854
+
855
+ .research-card {
856
+ background: var(--bg-primary);
857
+ border: 1px solid var(--border-light);
858
+ border-radius: var(--radius-lg);
859
+ padding: 24px;
860
+ transition: all 0.2s;
861
+ }
862
+
863
+ .research-card:hover {
864
+ box-shadow: var(--shadow-md);
865
+ border-color: var(--uapb-gold);
866
+ }
867
+
868
+ .research-icon {
869
+ width: 48px;
870
+ height: 48px;
871
+ background: rgba(232, 163, 23, 0.1);
872
+ border-radius: var(--radius-md);
873
+ display: flex;
874
+ align-items: center;
875
+ justify-content: center;
876
+ margin-bottom: 16px;
877
+ color: var(--uapb-gold-dark);
878
+ }
879
+
880
+ .research-card h4 {
881
+ font-family: 'Oswald', sans-serif;
882
+ font-size: 1rem;
883
+ font-weight: 600;
884
+ margin-bottom: 8px;
885
+ color: var(--text-primary);
886
+ }
887
+
888
+ .research-card p {
889
+ font-size: 0.85rem;
890
+ color: var(--text-secondary);
891
+ line-height: 1.6;
892
+ }
893
+
894
+ /* Modal */
895
+ .modal-overlay {
896
+ display: none;
897
+ position: fixed;
898
+ top: 0; left: 0; right: 0; bottom: 0;
899
+ background: rgba(0,0,0,0.9);
900
+ z-index: 1000;
901
+ align-items: center;
902
+ justify-content: center;
903
+ padding: 20px;
904
+ }
905
+
906
+ .modal-overlay.active { display: flex; }
907
+
908
+ .modal-content {
909
+ max-width: 95vw;
910
+ max-height: 95vh;
911
+ position: relative;
912
+ }
913
+
914
+ .modal-content img {
915
+ max-width: 100%;
916
+ max-height: 90vh;
917
+ border-radius: var(--radius-lg);
918
+ }
919
+
920
+ .modal-close {
921
+ position: absolute;
922
+ top: -40px; right: 0;
923
+ background: white;
924
+ border: none;
925
+ width: 36px; height: 36px;
926
+ border-radius: 50%;
927
+ font-size: 24px;
928
+ cursor: pointer;
929
+ }
930
+
931
+ .modal-title {
932
+ color: white;
933
+ text-align: center;
934
+ margin-top: 12px;
935
+ font-size: 0.9rem;
936
+ }
937
+
938
+ /* Footer */
939
+ .footer {
940
+ background: var(--uapb-black);
941
+ border-top: 4px solid var(--uapb-gold);
942
+ padding: 24px 0;
943
+ margin-top: 48px;
944
+ }
945
+
946
+ .footer-content {
947
+ max-width: 1400px;
948
+ margin: 0 auto;
949
+ padding: 0 24px;
950
+ display: flex;
951
+ justify-content: space-between;
952
+ align-items: center;
953
+ }
954
+
955
+ .footer-left {
956
+ font-size: 0.85rem;
957
+ color: rgba(255,255,255,0.7);
958
+ }
959
+
960
+ .footer-links {
961
+ display: flex;
962
+ gap: 24px;
963
+ }
964
+
965
+ .footer-link {
966
+ font-size: 0.85rem;
967
+ color: var(--uapb-gold);
968
+ text-decoration: none;
969
+ }
970
+
971
+ .footer-link:hover { text-decoration: underline; }
972
+
973
+ /* Spinner */
974
+ .spinner {
975
+ width: 18px; height: 18px;
976
+ border: 2px solid #FFFFFF;
977
+ border-top-color: transparent;
978
+ border-radius: 50%;
979
+ animation: spin 0.8s linear infinite;
980
+ }
981
+
982
+ @keyframes spin { to { transform: rotate(360deg); } }
983
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
984
+ .animate-in { animation: fadeIn 0.4s ease forwards; }
985
+
986
+ .partners-block { display: flex; flex-direction: column; align-items: center; gap: 4px; }
987
+ .partners-eyebrow { font-size: 0.52rem; font-weight: 700; letter-spacing: 2.5px; text-transform: uppercase; color: rgba(255,255,255,0.28); padding-right: 3px; }
988
+ .partners-logos { display: flex; align-items: center; background: transparent; border: none; border-radius: 12px; overflow: hidden; transition: border-color 0.2s; }
989
+ .partners-logos:hover { border-color: rgba(232,163,23,0.25); }
990
+ .partner-logo-item { display: flex; align-items: center; justify-content: center; padding: 11px 22px; text-decoration: none; transition: background 0.2s ease; height: 64px; }
991
+ .partner-logo-item:hover { background: rgba(255,255,255,0.07); }
992
+ .partner-logo-item img { height: 49px; width: auto; display: block; }
993
+ .partner-logo-item.gsla-item img { height: 45px; }
994
+ .partner-divider { width: 1px; height: 32px; background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.15), transparent); flex-shrink: 0; }
995
+ </style>
996
+ </head>
997
+ <body>
998
+ <!-- Header -->
999
+ <header class="uapb-header">
1000
+ <div class="header-content">
1001
+ <div class="header-left">
1002
+ <img src="/static/UAPB-logo.png" alt="Logo" class="dept-logo">
1003
+ <div class="header-text">
1004
+ <h1>Department of Aquaculture and Fisheries</h1>
1005
+ <p>Fish Nutrigenomics and AI Lab | Dr. Yathish Ramena, Director, Center of Excellence</p>
1006
+ </div>
1007
+ </div>
1008
+ <div class="header-right">
1009
+ <div class="partners-block">
1010
+ <span class="partners-eyebrow"> Research Partners</span>
1011
+ <div class="partners-logos">
1012
+ <a href="https://aralliance.org" target="_blank" class="partner-logo-item" title="Arkansas Research Alliance">
1013
+ <img src="/static/ara-logo.png" alt="Arkansas Research Alliance">
1014
+ </a>
1015
+ </div>
1016
+ </div>
1017
+ </div>
1018
+ </div>
1019
+ </header>
1020
+ <!-- GSLA Header -->
1021
+ <header class="gsla-header">
1022
+ <div class="header-content">
1023
+ <div class="header-left">
1024
+ <img src="static/gsla-header-logo.png" alt="GSLA Logo" class="dept-logo">
1025
+ <div class="header-text">
1026
+ <h1>The Artemia You Can Trust!</h1>
1027
+ <p>Highest Quality | Best Nutrition</p>
1028
+ </div>
1029
+ </div>
1030
+ </div>
1031
+ </header>
1032
+
1033
+ <!-- Main Navigation - Gold Bar -->
1034
+ <!-- Main Navigation - Gold Bar -->
1035
+ <div class="nav-wrapper">
1036
+ <video class="nav-video-bg" autoplay muted playsinline loop>
1037
+ <source src="static/gsla-video.mp4" type="video/mp4">
1038
+ </video>
1039
+ <nav class="main-nav">
1040
+ <div class="nav-content">
1041
+ <button class="nav-tab active" data-tab="research">Research</button>
1042
+ <button class="nav-tab" data-tab="extension">Extension</button>
1043
+ </div>
1044
+ </nav>
1045
+
1046
+
1047
+ <!-- Research Tab Content -->
1048
+ <nav class="sub-nav">
1049
+ </video>
1050
+ <div class="sub-nav-content">
1051
+
1052
+ <button class="sub-nav-tab active" data-species="vannamei">
1053
+ <span class="scientific-name">L. vannamei</span>
1054
+ <span class="common-name">(Pacific White Shrimp)</span>
1055
+ </button>
1056
+ <button class="sub-nav-tab" data-species="monodon">
1057
+ <span class="scientific-name">P. monodon</span>
1058
+ <span class="common-name">(Tiger Shrimp)</span>
1059
+ </button>
1060
+ <button class="sub-nav-tab" data-species="bass">
1061
+ <span class="scientific-name">M. salmoides</span>
1062
+ <span class="common-name">(Largemouth Bass)</span>
1063
+ </button>
1064
+ <button class="sub-nav-tab" data-species="prawn">
1065
+ <span class="scientific-name">M. rosenbergii</span>
1066
+ <span class="common-name">(Giant River Prawn)</span>
1067
+ </button>
1068
+ </div>
1069
+ </nav>
1070
+ </div><!-- end nav-wrapper -->
1071
+
1072
+ <div id="tab-research" class="tab-content active">
1073
+
1074
+ <!-- Species Analysis Panels -->
1075
+ <div id="species-vannamei" class="species-content active">
1076
+ <div class="main-content">
1077
+ <div class="page-header">
1078
+ <h2><em>Litopenaeus vannamei</em> <span>Analysis</span></h2>
1079
+ <p>AI-powered detection, length measurement, and weight estimation using computer vision and deep learning.</p>
1080
+ <div class="species-badge">Pacific White Shrimp — <em>L. vannamei</em></div>
1081
+ </div>
1082
+
1083
+ <div class="stats-grid">
1084
+ <div class="stat-card"><div class="stat-label">Total Detected</div><div class="stat-value" id="statCount-vannamei">—</div></div>
1085
+ <div class="stat-card"><div class="stat-label">Avg Length</div><div class="stat-value"><span id="statLength-vannamei">—</span><span class="unit">mm</span></div></div>
1086
+ <div class="stat-card"><div class="stat-label">Avg Weight</div><div class="stat-value"><span id="statWeight-vannamei">—</span><span class="unit">g</span></div></div>
1087
+ <div class="stat-card"><div class="stat-label">Total Biomass</div><div class="stat-value"><span id="statBiomass-vannamei">—</span><span class="unit">g</span></div></div>
1088
+ </div>
1089
+
1090
+ <div class="analysis-grid">
1091
+ <div>
1092
+ <div class="charts-grid">
1093
+ <div class="card"><div class="card-header"><span class="card-title">Length Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="lengthChart-vannamei"><div class="chart-placeholder">Upload images to generate</div></div></div>
1094
+ <div class="card"><div class="card-header"><span class="card-title">Weight Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="weightChart-vannamei"><div class="chart-placeholder">Upload images to generate</div></div></div>
1095
+ </div>
1096
+ <div class="section-header"><h3 class="section-title">Detection Results</h3><span class="section-count" id="processedCount-vannamei">0 images processed</span></div>
1097
+ <div class="results-grid" id="resultsGrid-vannamei"></div>
1098
+ </div>
1099
+ <div>
1100
+ <div class="card">
1101
+ <div class="card-header"><span class="card-title">Upload Images</span><span class="card-badge">Analysis</span></div>
1102
+ <div class="card-body">
1103
+ <label class="upload-zone" id="dropzone-vannamei"><input type="file" id="fileInput-vannamei" multiple accept="image/*"><div class="upload-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div><h3>Drop images here</h3><p>or click to browse</p></label>
1104
+ <div class="file-info"><span class="file-count" id="fileCount-vannamei">No files selected</span><span class="file-size" id="fileSize-vannamei">—</span></div>
1105
+ <div class="progress-container"><div class="progress-bar"><div class="progress-fill" id="progressFill-vannamei"></div></div><div class="progress-text"><span id="progressStatus-vannamei">Ready</span><span id="progressPercent-vannamei">0%</span></div></div>
1106
+ <div class="button-group"><button class="btn btn-primary" id="analyzeBtn-vannamei" onclick="runAnalysis('vannamei')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Analyze</button><button class="btn btn-secondary" onclick="resetTab('vannamei')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>Reset</button></div>
1107
+ <div class="error-box" id="errorBox-vannamei"></div>
1108
+ </div>
1109
+ </div>
1110
+ </div>
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+
1115
+ <!-- P. monodon -->
1116
+ <div id="species-monodon" class="species-content">
1117
+ <div class="main-content">
1118
+ <div class="page-header">
1119
+ <h2><em>Penaeus monodon</em> <span>Analysis</span></h2>
1120
+ <p>AI-powered detection and biomass estimation for Black Tiger Shrimp aquaculture research.</p>
1121
+ <div class="species-badge">Tiger Shrimp — <em>P. monodon</em></div>
1122
+ </div>
1123
+ <div class="stats-grid">
1124
+ <div class="stat-card"><div class="stat-label">Total Detected</div><div class="stat-value" id="statCount-monodon">—</div></div>
1125
+ <div class="stat-card"><div class="stat-label">Avg Length</div><div class="stat-value"><span id="statLength-monodon">—</span><span class="unit">mm</span></div></div>
1126
+ <div class="stat-card"><div class="stat-label">Avg Weight</div><div class="stat-value"><span id="statWeight-monodon">—</span><span class="unit">g</span></div></div>
1127
+ <div class="stat-card"><div class="stat-label">Total Biomass</div><div class="stat-value"><span id="statBiomass-monodon">—</span><span class="unit">g</span></div></div>
1128
+ </div>
1129
+ <div class="analysis-grid">
1130
+ <div>
1131
+ <div class="charts-grid">
1132
+ <div class="card"><div class="card-header"><span class="card-title">Length Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="lengthChart-monodon"><div class="chart-placeholder">Upload images to generate</div></div></div>
1133
+ <div class="card"><div class="card-header"><span class="card-title">Weight Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="weightChart-monodon"><div class="chart-placeholder">Upload images to generate</div></div></div>
1134
+ </div>
1135
+ <div class="section-header"><h3 class="section-title">Detection Results</h3><span class="section-count" id="processedCount-monodon">0 images processed</span></div>
1136
+ <div class="results-grid" id="resultsGrid-monodon"></div>
1137
+ </div>
1138
+ <div>
1139
+ <div class="card">
1140
+ <div class="card-header"><span class="card-title">Upload Images</span><span class="card-badge">Analysis</span></div>
1141
+ <div class="card-body">
1142
+ <label class="upload-zone" id="dropzone-monodon"><input type="file" id="fileInput-monodon" multiple accept="image/*"><div class="upload-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div><h3>Drop images here</h3><p>or click to browse</p></label>
1143
+ <div class="file-info"><span class="file-count" id="fileCount-monodon">No files selected</span><span class="file-size" id="fileSize-monodon">—</span></div>
1144
+ <div class="progress-container"><div class="progress-bar"><div class="progress-fill" id="progressFill-monodon"></div></div><div class="progress-text"><span id="progressStatus-monodon">Ready</span><span id="progressPercent-monodon">0%</span></div></div>
1145
+ <div class="button-group"><button class="btn btn-primary" id="analyzeBtn-monodon" onclick="runAnalysis('monodon')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Analyze</button><button class="btn btn-secondary" onclick="resetTab('monodon')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>Reset</button></div>
1146
+ <div class="error-box" id="errorBox-monodon"></div>
1147
+ </div>
1148
+ </div>
1149
+ </div>
1150
+ </div>
1151
+ </div>
1152
+ </div>
1153
+
1154
+ <!-- M. salmoides -->
1155
+ <div id="species-bass" class="species-content">
1156
+ <div class="main-content">
1157
+ <div class="page-header">
1158
+ <h2><em>Micropterus salmoides</em> <span>Analysis</span></h2>
1159
+ <p>AI-powered detection and biomass estimation for Largemouth Bass aquaculture research.</p>
1160
+ <div class="species-badge">Largemouth Bass — <em>M. salmoides</em></div>
1161
+ </div>
1162
+ <div class="stats-grid">
1163
+ <div class="stat-card"><div class="stat-label">Total Detected</div><div class="stat-value" id="statCount-bass">—</div></div>
1164
+ <div class="stat-card"><div class="stat-label">Avg Length</div><div class="stat-value"><span id="statLength-bass">—</span><span class="unit">mm</span></div></div>
1165
+ <div class="stat-card"><div class="stat-label">Avg Weight</div><div class="stat-value"><span id="statWeight-bass">—</span><span class="unit">g</span></div></div>
1166
+ <div class="stat-card"><div class="stat-label">Total Biomass</div><div class="stat-value"><span id="statBiomass-bass">—</span><span class="unit">g</span></div></div>
1167
+ </div>
1168
+ <div class="analysis-grid">
1169
+ <div>
1170
+ <div class="charts-grid">
1171
+ <div class="card"><div class="card-header"><span class="card-title">Length Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="lengthChart-bass"><div class="chart-placeholder">Upload images to generate</div></div></div>
1172
+ <div class="card"><div class="card-header"><span class="card-title">Weight Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="weightChart-bass"><div class="chart-placeholder">Upload images to generate</div></div></div>
1173
+ </div>
1174
+ <div class="section-header"><h3 class="section-title">Detection Results</h3><span class="section-count" id="processedCount-bass">0 images processed</span></div>
1175
+ <div class="results-grid" id="resultsGrid-bass"></div>
1176
+ </div>
1177
+ <div>
1178
+ <div class="card">
1179
+ <div class="card-header"><span class="card-title">Upload Images</span><span class="card-badge">Analysis</span></div>
1180
+ <div class="card-body">
1181
+ <label class="upload-zone" id="dropzone-bass"><input type="file" id="fileInput-bass" multiple accept="image/*"><div class="upload-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div><h3>Drop images here</h3><p>or click to browse</p></label>
1182
+ <div class="file-info"><span class="file-count" id="fileCount-bass">No files selected</span><span class="file-size" id="fileSize-bass">—</span></div>
1183
+ <div class="progress-container"><div class="progress-bar"><div class="progress-fill" id="progressFill-bass"></div></div><div class="progress-text"><span id="progressStatus-bass">Ready</span><span id="progressPercent-bass">0%</span></div></div>
1184
+ <div class="button-group"><button class="btn btn-primary" id="analyzeBtn-bass" onclick="runAnalysis('bass')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Analyze</button><button class="btn btn-secondary" onclick="resetTab('bass')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>Reset</button></div>
1185
+ <div class="error-box" id="errorBox-bass"></div>
1186
+ </div>
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ </div>
1191
+ </div>
1192
+
1193
+ <!-- M. rosenbergii -->
1194
+ <div id="species-prawn" class="species-content">
1195
+ <div class="main-content">
1196
+ <div class="page-header">
1197
+ <h2><em>Macrobrachium rosenbergii</em> <span>Analysis</span></h2>
1198
+ <p>AI-powered detection and biomass estimation for Giant Freshwater Prawn aquaculture research.</p>
1199
+ <div class="species-badge">Giant River Prawn — <em>M. rosenbergii</em></div>
1200
+ </div>
1201
+ <div class="stats-grid">
1202
+ <div class="stat-card"><div class="stat-label">Total Detected</div><div class="stat-value" id="statCount-prawn">—</div></div>
1203
+ <div class="stat-card"><div class="stat-label">Avg Length</div><div class="stat-value"><span id="statLength-prawn">—</span><span class="unit">mm</span></div></div>
1204
+ <div class="stat-card"><div class="stat-label">Avg Weight</div><div class="stat-value"><span id="statWeight-prawn">—</span><span class="unit">g</span></div></div>
1205
+ <div class="stat-card"><div class="stat-label">Total Biomass</div><div class="stat-value"><span id="statBiomass-prawn">—</span><span class="unit">g</span></div></div>
1206
+ </div>
1207
+ <div class="analysis-grid">
1208
+ <div>
1209
+ <div class="charts-grid">
1210
+ <div class="card"><div class="card-header"><span class="card-title">Length Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="lengthChart-prawn"><div class="chart-placeholder">Upload images to generate</div></div></div>
1211
+ <div class="card"><div class="card-header"><span class="card-title">Weight Distribution</span><span class="card-badge">Histogram</span></div><div class="card-body" id="weightChart-prawn"><div class="chart-placeholder">Upload images to generate</div></div></div>
1212
+ </div>
1213
+ <div class="section-header"><h3 class="section-title">Detection Results</h3><span class="section-count" id="processedCount-prawn">0 images processed</span></div>
1214
+ <div class="results-grid" id="resultsGrid-prawn"></div>
1215
+ </div>
1216
+ <div>
1217
+ <div class="card">
1218
+ <div class="card-header"><span class="card-title">Upload Images</span><span class="card-badge">Analysis</span></div>
1219
+ <div class="card-body">
1220
+ <label class="upload-zone" id="dropzone-prawn"><input type="file" id="fileInput-prawn" multiple accept="image/*"><div class="upload-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div><h3>Drop images here</h3><p>or click to browse</p></label>
1221
+ <div class="file-info"><span class="file-count" id="fileCount-prawn">No files selected</span><span class="file-size" id="fileSize-prawn">—</span></div>
1222
+ <div class="progress-container"><div class="progress-bar"><div class="progress-fill" id="progressFill-prawn"></div></div><div class="progress-text"><span id="progressStatus-prawn">Ready</span><span id="progressPercent-prawn">0%</span></div></div>
1223
+ <div class="button-group"><button class="btn btn-primary" id="analyzeBtn-prawn" onclick="runAnalysis('prawn')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Analyze</button><button class="btn btn-secondary" onclick="resetTab('prawn')"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>Reset</button></div>
1224
+ <div class="error-box" id="errorBox-prawn"></div>
1225
+ </div>
1226
+ </div>
1227
+ </div>
1228
+ </div>
1229
+ </div>
1230
+ </div>
1231
+ </div>
1232
+
1233
+ <!-- Extension Tab Content -->
1234
+ <div id="tab-extension" class="tab-content">
1235
+ <div class="main-content">
1236
+ <div class="page-header">
1237
+ <h2>Extension <span>Services</span></h2>
1238
+ <p>Outreach programs, industry partnerships, and technology transfer from the Fish Nutrigenomics and AI Lab.</p>
1239
+ </div>
1240
+
1241
+ <div class="profile-section">
1242
+ <div class="profile-card">
1243
+ <div class="profile-avatar">
1244
+ <img src="/static/dr-ramena.png" alt="Dr. Yathish Ramena" onerror="this.style.display='none'; this.parentElement.innerHTML='YR';">
1245
+ </div>
1246
+ <div class="profile-name">Dr. Yathish Ramena</div>
1247
+ <div class="profile-title">Director, Fish Nutrigenomics and AI Lab</div>
1248
+ <div class="profile-dept">Aquaculture Species Analysis</div>
1249
+ <div class="profile-links">
1250
+ <a href="mailto:ramenay@uapb.edu" class="profile-link">✉️ ramenay@uapb.edu</a>
1251
+ </div>
1252
+ </div>
1253
+
1254
+ <div>
1255
+ <h3 style="font-family: 'Oswald', sans-serif; font-size: 1.2rem; margin-bottom: 20px; text-transform: uppercase;">Research Focus Areas</h3>
1256
+ <div class="research-areas">
1257
+ <div class="research-card">
1258
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></div>
1259
+ <h4>Fish Nutrigenomics</h4>
1260
+ <p>Investigating the interaction between nutrition and gene expression in aquaculture species to optimize growth and health outcomes.</p>
1261
+ </div>
1262
+ <div class="research-card">
1263
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
1264
+ <h4>AI & Computer Vision</h4>
1265
+ <p>Developing deep learning models for automated species detection, biomass estimation, and health monitoring in aquaculture systems.</p>
1266
+ </div>
1267
+ <div class="research-card">
1268
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg></div>
1269
+ <h4>Precision Aquaculture</h4>
1270
+ <p>Integrating IoT sensors, real-time monitoring, and predictive analytics for sustainable and efficient aquaculture production.</p>
1271
+ </div>
1272
+ <div class="research-card">
1273
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
1274
+ <h4>Growth Modeling</h4>
1275
+ <p>Developing mathematical models for predicting growth trajectories and optimizing feeding strategies in commercial aquaculture.</p>
1276
+ </div>
1277
+ </div>
1278
+ </div>
1279
+ </div>
1280
+
1281
+ <h3 style="font-family: 'Oswald', sans-serif; font-size: 1.2rem; margin: 40px 0 20px; text-transform: uppercase;">Extension Programs</h3>
1282
+ <div class="research-areas">
1283
+ <div class="research-card">
1284
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
1285
+ <h4>Industry Training Workshops</h4>
1286
+ <p>Hands-on training sessions for aquaculture farmers on best practices, disease management, and technology adoption.</p>
1287
+ </div>
1288
+ <div class="research-card">
1289
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/></svg></div>
1290
+ <h4>Technology Transfer</h4>
1291
+ <p>Bridging research and industry by providing AI-powered tools and decision support systems to aquaculture producers.</p>
1292
+ </div>
1293
+ <div class="research-card">
1294
+ <div class="research-icon"><svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg></div>
1295
+ <h4>Educational Resources</h4>
1296
+ <p>Developing curricula, online courses, and educational materials for the next generation of aquaculture professionals.</p>
1297
+ </div>
1298
+ </div>
1299
+ </div>
1300
+ </div>
1301
+
1302
+ <!-- Image Modal -->
1303
+ <div class="modal-overlay" id="imageModal" onclick="closeModal()">
1304
+ <div class="modal-content" onclick="event.stopPropagation()">
1305
+ <button class="modal-close" onclick="closeModal()">×</button>
1306
+ <img id="modalImage" src="" alt="Expanded view">
1307
+ <div class="modal-title" id="modalTitle"></div>
1308
+ </div>
1309
+ </div>
1310
+
1311
+ <!-- Footer -->
1312
+ <footer class="footer">
1313
+ <div class="footer-content">
1314
+ <div class="footer-left">© 2026 Aquaculture Species Analysis • Fish Nutrigenomics and AI Lab</div>
1315
+ <div class="footer-links">
1316
+ <a href="/docs" class="footer-link">API Docs</a>
1317
+ </div>
1318
+ </div>
1319
+ </footer>
1320
+
1321
+ <script>
1322
+ const $ = id => document.getElementById(id);
1323
+
1324
+ // Modal
1325
+ function openModal(src, title) {
1326
+ $('modalImage').src = src;
1327
+ $('modalTitle').textContent = title || '';
1328
+ $('imageModal').classList.add('active');
1329
+ document.body.style.overflow = 'hidden';
1330
+ }
1331
+ function closeModal() {
1332
+ $('imageModal').classList.remove('active');
1333
+ document.body.style.overflow = '';
1334
+ }
1335
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
1336
+
1337
+ // Main Tab Navigation
1338
+ document.querySelectorAll('.nav-tab').forEach(tab => {
1339
+ tab.addEventListener('click', () => {
1340
+ document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1341
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1342
+ tab.classList.add('active');
1343
+ $('tab-' + tab.dataset.tab).classList.add('active');
1344
+ });
1345
+ });
1346
+
1347
+ // Species Sub-Navigation
1348
+ document.querySelectorAll('.sub-nav-tab').forEach(tab => {
1349
+ tab.addEventListener('click', () => {
1350
+ document.querySelectorAll('.sub-nav-tab').forEach(t => t.classList.remove('active'));
1351
+ document.querySelectorAll('.species-content').forEach(c => c.classList.remove('active'));
1352
+ tab.classList.add('active');
1353
+ $('species-' + tab.dataset.species).classList.add('active');
1354
+ });
1355
+ });
1356
+
1357
+ // Species setup
1358
+ const species = ['vannamei', 'monodon', 'bass', 'prawn'];
1359
+ species.forEach(sp => {
1360
+ const fileInput = $('fileInput-' + sp);
1361
+ const dropzone = $('dropzone-' + sp);
1362
+ fileInput.addEventListener('change', () => updateFileInfo(sp));
1363
+ dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('drag-over'); });
1364
+ dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over'));
1365
+ dropzone.addEventListener('drop', e => {
1366
+ e.preventDefault();
1367
+ dropzone.classList.remove('drag-over');
1368
+ fileInput.files = e.dataTransfer.files;
1369
+ updateFileInfo(sp);
1370
+ });
1371
+ });
1372
+
1373
+ const formatBytes = b => {
1374
+ if (b === 0) return '0 B';
1375
+ const k = 1024, s = ['B', 'KB', 'MB', 'GB'];
1376
+ const i = Math.floor(Math.log(b) / Math.log(k));
1377
+ return parseFloat((b / Math.pow(k, i)).toFixed(2)) + ' ' + s[i];
1378
+ };
1379
+
1380
+ function updateFileInfo(sp) {
1381
+ const files = $('fileInput-' + sp).files;
1382
+ if (!files.length) { $('fileCount-' + sp).textContent = 'No files selected'; $('fileSize-' + sp).textContent = '—'; return; }
1383
+ let size = 0; for (const f of files) size += f.size;
1384
+ $('fileCount-' + sp).textContent = files.length + ' file' + (files.length > 1 ? 's' : '') + ' selected';
1385
+ $('fileSize-' + sp).textContent = formatBytes(size);
1386
+ showError(sp, '');
1387
+ }
1388
+
1389
+ function setProgress(sp, pct, status) {
1390
+ $('progressFill-' + sp).style.width = pct + '%';
1391
+ $('progressPercent-' + sp).textContent = pct + '%';
1392
+ if (status) $('progressStatus-' + sp).textContent = status;
1393
+ }
1394
+
1395
+ function showError(sp, msg) {
1396
+ const box = $('errorBox-' + sp);
1397
+ box.textContent = msg;
1398
+ box.classList.toggle('visible', !!msg);
1399
+ }
1400
+
1401
+ async function checkHealth() {
1402
+ try {
1403
+ await fetch('/health');
1404
+ $('statusDot').className = 'status-dot online';
1405
+ $('statusLabel').textContent = 'API Online';
1406
+ } catch {
1407
+ $('statusDot').className = 'status-dot offline';
1408
+ $('statusLabel').textContent = 'API Offline';
1409
+ }
1410
+ }
1411
+ checkHealth(); setInterval(checkHealth, 10000);
1412
+
1413
+ async function runAnalysis(sp) {
1414
+ const files = $('fileInput-' + sp).files;
1415
+ if (!files.length) { showError(sp, 'Please select images first.'); return; }
1416
+ const btn = $('analyzeBtn-' + sp);
1417
+ btn.disabled = true;
1418
+ btn.innerHTML = '<span class="spinner"></span> Analyzing...';
1419
+ showError(sp, '');
1420
+ setProgress(sp, 10, 'Uploading...');
1421
+ const form = new FormData();
1422
+ for (const f of files) form.append('files', f);
1423
+ try {
1424
+ setProgress(sp, 30, 'Processing...');
1425
+ const res = await fetch('/detect?species=' + sp, { method: 'POST', body: form });
1426
+ setProgress(sp, 70, 'Analyzing...');
1427
+ const text = await res.text();
1428
+ let data; try { data = JSON.parse(text); } catch { throw new Error(text); }
1429
+ if (!res.ok) throw new Error(data.detail || data.error || 'Failed');
1430
+ setProgress(sp, 90, 'Rendering...');
1431
+ displayResults(sp, data);
1432
+ setProgress(sp, 100, 'Complete ✓');
1433
+ } catch (err) {
1434
+ showError(sp, err.message);
1435
+ setProgress(sp, 0, 'Error');
1436
+ } finally {
1437
+ btn.disabled = false;
1438
+ btn.innerHTML = '<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Analyze';
1439
+ setTimeout(() => setProgress(sp, 0, 'Ready'), 2000);
1440
+ }
1441
+ }
1442
+
1443
+ function displayResults(sp, data) {
1444
+ const s = data.overall_summary || {};
1445
+ const h = data.histograms || {};
1446
+ const imgs = data.per_image || [];
1447
+ $('statCount-' + sp).textContent = s.total_specimens ?? data.overall_total_shrimp ?? '—';
1448
+ $('statLength-' + sp).textContent = s.average_length_mm ?? data.overall_average_length_mm ?? '—';
1449
+ $('statWeight-' + sp).textContent = s.average_weight_g ?? '—';
1450
+ $('statBiomass-' + sp).textContent = s.total_biomass_g ?? '—';
1451
+ if (h.length_histogram_base64 || data.histogram_png_base64) {
1452
+ const src = 'data:image/png;base64,' + (h.length_histogram_base64 || data.histogram_png_base64);
1453
+ $('lengthChart-' + sp).innerHTML = '<img src="' + src + '" onclick="openModal(\'' + src + '\', \'Length Distribution\')">';
1454
+ }
1455
+ if (h.weight_histogram_base64) {
1456
+ const src = 'data:image/png;base64,' + h.weight_histogram_base64;
1457
+ $('weightChart-' + sp).innerHTML = '<img src="' + src + '" onclick="openModal(\'' + src + '\', \'Weight Distribution\')">';
1458
+ }
1459
+ $('processedCount-' + sp).textContent = imgs.length + ' image' + (imgs.length !== 1 ? 's' : '') + ' processed';
1460
+ const grid = $('resultsGrid-' + sp);
1461
+ grid.innerHTML = '';
1462
+ imgs.forEach((item, i) => {
1463
+ const card = document.createElement('div');
1464
+ card.className = 'result-card animate-in';
1465
+ card.style.animationDelay = (i * 80) + 'ms';
1466
+ if (item.error) {
1467
+ card.innerHTML = '<div class="result-header"><span class="result-filename">' + (item.filename || 'Error') + '</span></div><div style="padding:16px;color:#DC2626;font-size:0.85rem;">' + item.error + '</div>';
1468
+ } else {
1469
+ const cnt = item.specimen_count ?? item.shrimp_count ?? 0;
1470
+ const avg = item.summary?.average_length_mm ?? item.average_length_mm ?? 0;
1471
+ const wgt = item.summary?.average_weight_g ?? 0;
1472
+ const lens = item.weights_g?.map((w,j) => item.lengths_mm[j] + 'mm/' + w + 'g') || item.lengths_mm?.map(l => l + 'mm') || [];
1473
+ const imgSrc = item.annotated_image_png_base64 ? 'data:image/png;base64,' + item.annotated_image_png_base64 : '';
1474
+ card.innerHTML = '<div class="result-header"><span class="result-filename">' + (item.filename || 'Image') + '</span><div class="result-stats"><span class="result-stat">Count: <strong>' + cnt + '</strong></span><span class="result-stat"><strong>' + avg + '</strong>mm</span>' + (wgt ? '<span class="result-stat"><strong>' + wgt + '</strong>g</span>' : '') + '</div></div>' + (imgSrc ? '<div class="result-image" onclick="openModal(\'' + imgSrc + '\', \'' + (item.filename || 'Result') + '\')"><img src="' + imgSrc + '"><span class="click-hint">Click to expand</span></div>' : '') + (lens.length ? '<div class="result-data">' + lens.join(' • ') + '</div>' : '');
1475
+ }
1476
+ grid.appendChild(card);
1477
+ });
1478
+ }
1479
+
1480
+ function resetTab(sp) {
1481
+ $('fileInput-' + sp).value = '';
1482
+ $('fileCount-' + sp).textContent = 'No files selected';
1483
+ $('fileSize-' + sp).textContent = '—';
1484
+ setProgress(sp, 0, 'Ready');
1485
+ showError(sp, '');
1486
+ ['statCount', 'statLength', 'statWeight', 'statBiomass'].forEach(id => { const el = $(id + '-' + sp); if (el) el.textContent = '—'; });
1487
+ $('lengthChart-' + sp).innerHTML = '<div class="chart-placeholder">Upload images to generate</div>';
1488
+ $('weightChart-' + sp).innerHTML = '<div class="chart-placeholder">Upload images to generate</div>';
1489
+ $('resultsGrid-' + sp).innerHTML = '';
1490
+ $('processedCount-' + sp).textContent = '0 images processed';
1491
+ }
1492
+ </script>
1493
+ </body>
1494
+ </html>
1495
+
1496
+
1497
+
1498
+
1499
+
1500
+
1501
+
1502
+
1503
+
1504
+
1505
+
1506
+
1507
+
1508
+
1509
+
1510
+
1511
+
render.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: aquaculture-vision
4
+ env: python
5
+ runtime: python-3.11
6
+ buildCommand: pip install -r requirements.txt
7
+ startCommand: uvicorn server:app --host 0.0.0.0 --port $PORT
8
+ envVars:
9
+ - key: PYTHON_VERSION
10
+ value: 3.11
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ ultralytics
4
+ opencv-python-headless
5
+ python-multipart
6
+ Pillow
7
+ numpy
8
+ matplotlib
9
+ requests
10
+ gdown
server.py ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Department of Aquaculture and Fisheries
3
+ Fish Nutrigenomics and AI Lab | Dr. Yathish Ramena, Director
4
+ Advanced Species Detection with Length and Weight Estimation
5
+
6
+ Now with Roboflow Integration for Largemouth Bass Detection!
7
+ Version 3.1 - Fixed: Bass shows species + confidence only (no estimated measurements)
8
+ """
9
+
10
+ from fastapi import FastAPI, UploadFile, File, Query
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
13
+ from fastapi.staticfiles import StaticFiles
14
+ from typing import List, Optional
15
+ from ultralytics import YOLO
16
+ import numpy as np
17
+ import cv2
18
+ import base64
19
+ import tempfile
20
+ import os
21
+ import io
22
+ import requests
23
+ from datetime import datetime
24
+
25
+ # Matplotlib setup - MUST be before pyplot import
26
+ import matplotlib
27
+ matplotlib.use('Agg')
28
+ import matplotlib.pyplot as plt
29
+
30
+ # ═══════════════════════════════════════════════════════════════════════════════
31
+ # ROBOFLOW CONFIGURATION FOR BASS DETECTION
32
+ # ═══════════════════════════════════════════════════════════════════════════════
33
+
34
+ ROBOFLOW_API_KEY = "tya4HWqSPsfoQAmR03ES" # Your Roboflow Private API Key
35
+ ROBOFLOW_MODEL_ENDPOINT = "https://serverless.roboflow.com/bass-fish-detection-06gec/1"
36
+
37
+ # ═══════════════════════════════════════════════════════════════════════════════
38
+ # DOWNLOAD WEIGHTS FROM GOOGLE DRIVE (for shrimp model)
39
+ # ═══════════════════════════════════════════════════════════════════════════════
40
+
41
+ MODEL_PATH = "weights.pt"
42
+ GOOGLE_DRIVE_FILE_ID = "14VSgbeQJyBizH-wTAq36WTph1miZoG5b"
43
+
44
+ def download_weights_from_gdrive(file_id: str, destination: str):
45
+ """Download file from Google Drive using gdown"""
46
+ if os.path.exists(destination):
47
+ file_size = os.path.getsize(destination)
48
+ if file_size > 1000000: # > 1MB means it's likely valid
49
+ print(f"✓ Weights file already exists: {destination} ({file_size} bytes)")
50
+ return True
51
+ else:
52
+ print(f"⚠ Existing file too small ({file_size} bytes), re-downloading...")
53
+ os.remove(destination)
54
+
55
+ print(f"⬇ Downloading weights from Google Drive...")
56
+ print(f" File ID: {file_id}")
57
+
58
+ try:
59
+ import gdown
60
+ url = f"https://drive.google.com/uc?id={file_id}"
61
+ print(f" URL: {url}")
62
+ output = gdown.download(url, destination, quiet=False, fuzzy=True)
63
+
64
+ if output and os.path.exists(destination):
65
+ file_size = os.path.getsize(destination)
66
+ print(f"✓ Downloaded: {destination} ({file_size} bytes)")
67
+ if file_size > 1000000:
68
+ return True
69
+ else:
70
+ print(f"✗ File too small, might be an error page")
71
+ return False
72
+ else:
73
+ print(f"✗ gdown returned: {output}")
74
+ return False
75
+
76
+ except Exception as e:
77
+ print(f"✗ gdown failed: {e}")
78
+
79
+ # Fallback: try with confirm parameter
80
+ try:
81
+ import gdown
82
+ url = f"https://drive.google.com/uc?id={file_id}&confirm=t"
83
+ print(f" Trying fallback URL: {url}")
84
+ output = gdown.download(url, destination, quiet=False)
85
+
86
+ if output and os.path.exists(destination) and os.path.getsize(destination) > 1000000:
87
+ print(f"✓ Fallback download successful")
88
+ return True
89
+ except Exception as e:
90
+ print(f"✗ Fallback also failed: {e}")
91
+
92
+ return False
93
+
94
+ # Download weights if not present
95
+ print("=" * 60)
96
+ print("INITIALIZING AQUACULTURE VISION API")
97
+ print("=" * 60)
98
+
99
+ if not download_weights_from_gdrive(GOOGLE_DRIVE_FILE_ID, MODEL_PATH):
100
+ print("=" * 60)
101
+ print("ERROR: Could not download weights.pt from Google Drive!")
102
+ print("Please check:")
103
+ print(" 1. File ID is correct: " + GOOGLE_DRIVE_FILE_ID)
104
+ print(" 2. File is shared as 'Anyone with the link'")
105
+ print(" 3. Google Drive link is accessible")
106
+ print("=" * 60)
107
+ raise FileNotFoundError(f"Could not download {MODEL_PATH} from Google Drive")
108
+
109
+ print(f"Loading YOLO model from {MODEL_PATH}...")
110
+ print(f"Roboflow API configured for Bass detection")
111
+
112
+ app = FastAPI(
113
+ title="Aquaculture Vision API",
114
+ description="AI-powered detection and biomass estimation for aquaculture species",
115
+ version="3.1 - Bass detection shows species + confidence only"
116
+ )
117
+
118
+ # Mount static files for logo and other assets
119
+ app.mount("/static", StaticFiles(directory="."), name="static")
120
+
121
+ app.add_middleware(
122
+ CORSMiddleware,
123
+ allow_origins=["*"],
124
+ allow_credentials=True,
125
+ allow_methods=["*"],
126
+ allow_headers=["*"],
127
+ )
128
+
129
+ # ═══════════════════════════════════════════════════════════════════════════════
130
+ # CONFIGURATION
131
+ # ═══════════════════════════════════════════════════════════════════════════════
132
+
133
+ MODEL_PATH = "weights.pt"
134
+ PIXELS_PER_MM = 6.5
135
+ CONF_THRESHOLD = 0.40
136
+ MASK_ALPHA = 0.4
137
+
138
+ # Load YOLO model for shrimp
139
+ model = YOLO(MODEL_PATH)
140
+
141
+ # ═══════════════════════════════════════════════════════════════════════════════
142
+ # SPECIES CONFIG - Length-Weight Relationships: W = a × L^b
143
+ # ═══════════════════════════════════════════════════════════════════════════════
144
+
145
+ SPECIES_CONFIG = {
146
+ "vannamei": {
147
+ "display_name": "Pacific White Shrimp",
148
+ "scientific_name": "Litopenaeus vannamei",
149
+ "weight_a": 8.54e-6,
150
+ "weight_b": 2.997,
151
+ "color": (0, 255, 127),
152
+ "min_harvest_mm": 100,
153
+ "optimal_harvest_mm": 130,
154
+ "use_roboflow": False,
155
+ },
156
+ "monodon": {
157
+ "display_name": "Tiger Shrimp",
158
+ "scientific_name": "Penaeus monodon",
159
+ "weight_a": 7.2e-6,
160
+ "weight_b": 3.05,
161
+ "color": (255, 165, 0),
162
+ "min_harvest_mm": 120,
163
+ "optimal_harvest_mm": 150,
164
+ "use_roboflow": False,
165
+ },
166
+ "bass": {
167
+ "display_name": "Largemouth Bass",
168
+ "scientific_name": "Micropterus salmoides",
169
+ "weight_a": 7.0e-6,
170
+ "weight_b": 3.19,
171
+ "color": (100, 149, 237), # Cornflower blue
172
+ "min_harvest_mm": 250,
173
+ "optimal_harvest_mm": 350,
174
+ "use_roboflow": True, # Use Roboflow API for bass!
175
+ },
176
+ "prawn": {
177
+ "display_name": "Giant River Prawn",
178
+ "scientific_name": "Macrobrachium rosenbergii",
179
+ "weight_a": 6.8e-6,
180
+ "weight_b": 3.08,
181
+ "color": (147, 112, 219),
182
+ "min_harvest_mm": 150,
183
+ "optimal_harvest_mm": 200,
184
+ "use_roboflow": False,
185
+ },
186
+ "default": {
187
+ "display_name": "Unknown Species",
188
+ "scientific_name": "N/A",
189
+ "weight_a": 8.54e-6,
190
+ "weight_b": 3.0,
191
+ "color": (0, 255, 127),
192
+ "min_harvest_mm": 100,
193
+ "optimal_harvest_mm": 150,
194
+ "use_roboflow": False,
195
+ }
196
+ }
197
+
198
+
199
+ def get_species_config(species_key: str) -> dict:
200
+ return SPECIES_CONFIG.get(species_key, SPECIES_CONFIG["default"])
201
+
202
+
203
+ def is_target_class(class_name: str) -> bool:
204
+ """Check if detected class is a target species"""
205
+ if class_name == "shrimp - v1 2025-10-24 5-22pm":
206
+ return True
207
+ lower = class_name.lower()
208
+ return any(kw in lower for kw in ["shrimp", "fish", "prawn", "bass"])
209
+
210
+
211
+ def max_pairwise_distance(points_xy: np.ndarray) -> float:
212
+ if points_xy.shape[0] < 2:
213
+ return 0.0
214
+ diff = points_xy[:, None, :] - points_xy[None, :, :]
215
+ dist = np.sqrt((diff ** 2).sum(axis=2))
216
+ return float(dist.max())
217
+
218
+
219
+ def estimate_weight(length_mm: float, species_config: dict) -> float:
220
+ """W = a × L^b"""
221
+ a = species_config["weight_a"]
222
+ b = species_config["weight_b"]
223
+ if length_mm <= 0:
224
+ return 0.0
225
+ return a * (length_mm ** b)
226
+
227
+
228
+ def get_size_category(length_mm: float, species_config: dict) -> str:
229
+ min_harvest = species_config["min_harvest_mm"]
230
+ optimal_harvest = species_config["optimal_harvest_mm"]
231
+ if length_mm < min_harvest * 0.7:
232
+ return "juvenile"
233
+ elif length_mm < min_harvest:
234
+ return "sub-harvest"
235
+ elif length_mm < optimal_harvest:
236
+ return "harvestable"
237
+ return "optimal"
238
+
239
+
240
+ # ═══════════════════════════════════════════════════════════════════════════════
241
+ # ROBOFLOW BASS DETECTION FUNCTION
242
+ # ═══════════════════════════════════════════════════════════════════════════════
243
+
244
+ def detect_bass_with_roboflow(image_path: str, confidence: float = 0.40):
245
+ """
246
+ Detect bass fish using Roboflow API
247
+ Returns list of detections with bounding boxes
248
+ """
249
+ try:
250
+ # Read image and encode to base64
251
+ with open(image_path, "rb") as f:
252
+ image_data = base64.b64encode(f.read()).decode("utf-8")
253
+
254
+ # Call Roboflow API
255
+ response = requests.post(
256
+ ROBOFLOW_MODEL_ENDPOINT,
257
+ params={
258
+ "api_key": ROBOFLOW_API_KEY,
259
+ "confidence": 40, # Roboflow uses 0-100
260
+ },
261
+ data=image_data,
262
+ headers={
263
+ "Content-Type": "application/x-www-form-urlencoded"
264
+ },
265
+ timeout=30
266
+ )
267
+
268
+ if response.status_code == 200:
269
+ result = response.json()
270
+ print(f"✓ Roboflow API response: {len(result.get('predictions', []))} detections")
271
+ return result
272
+ else:
273
+ print(f"✗ Roboflow API error: {response.status_code} - {response.text}")
274
+ return {"predictions": [], "error": response.text}
275
+
276
+ except Exception as e:
277
+ print(f"✗ Roboflow API exception: {str(e)}")
278
+ return {"predictions": [], "error": str(e)}
279
+
280
+
281
+ def process_bass_image(image_path: str, species_config: dict, calibration: float):
282
+ """
283
+ Process a single image for bass detection using Roboflow
284
+ Returns detection results with annotations
285
+ NOW: Shows only species + confidence (no fake length/weight)
286
+ """
287
+ # Get Roboflow detections
288
+ roboflow_result = detect_bass_with_roboflow(image_path, CONF_THRESHOLD)
289
+ predictions = roboflow_result.get("predictions", [])
290
+
291
+ # Read image for annotation
292
+ bgr = cv2.imread(image_path)
293
+ if bgr is None:
294
+ return None, 0, [], [], "Could not read image"
295
+
296
+ rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
297
+ overlay = rgb.copy()
298
+
299
+ detection_count = 0
300
+ species_detected = []
301
+ confidences = []
302
+
303
+ for pred in predictions:
304
+ # Get bounding box
305
+ x = pred.get("x", 0)
306
+ y = pred.get("y", 0)
307
+ width = pred.get("width", 0)
308
+ height = pred.get("height", 0)
309
+ confidence = pred.get("confidence", 0)
310
+ class_name = pred.get("class", "bass")
311
+
312
+ detection_count += 1
313
+ species_detected.append(class_name)
314
+ confidences.append(confidence)
315
+
316
+ # Calculate bounding box corners
317
+ x1 = int(x - width / 2)
318
+ y1 = int(y - height / 2)
319
+ x2 = int(x + width / 2)
320
+ y2 = int(y + height / 2)
321
+
322
+ # Draw bounding box
323
+ color = species_config["color"]
324
+ cv2.rectangle(overlay, (x1, y1), (x2, y2), color, -1) # Filled for overlay effect
325
+
326
+ # Blend overlay
327
+ annotated = cv2.addWeighted(rgb, 1 - MASK_ALPHA, overlay, MASK_ALPHA, 0)
328
+
329
+ # Draw bounding boxes and labels (species + confidence ONLY)
330
+ for pred in predictions:
331
+ x = pred.get("x", 0)
332
+ y = pred.get("y", 0)
333
+ width = pred.get("width", 0)
334
+ height = pred.get("height", 0)
335
+ confidence = pred.get("confidence", 0)
336
+ class_name = pred.get("class", "bass")
337
+
338
+ x1 = int(x - width / 2)
339
+ y1 = int(y - height / 2)
340
+ x2 = int(x + width / 2)
341
+ y2 = int(y + height / 2)
342
+
343
+ # Draw box outline
344
+ cv2.rectangle(annotated, (x1, y1), (x2, y2), species_config["color"], 3)
345
+
346
+ # Draw label: SPECIES + CONFIDENCE only (no length/weight)
347
+ label = f"{class_name}"
348
+ conf_label = f"{confidence*100:.0f}%"
349
+
350
+ font = cv2.FONT_HERSHEY_SIMPLEX
351
+ font_scale = 0.7
352
+ thickness = 2
353
+
354
+ # Get text sizes
355
+ (tw, th), _ = cv2.getTextSize(label, font, font_scale, thickness)
356
+ (cw, ch), _ = cv2.getTextSize(conf_label, font, 0.5, 1)
357
+
358
+ # Position text above bounding box
359
+ text_x = max(0, x1)
360
+ text_y = max(th + 10, y1 - 10)
361
+
362
+ # Background rectangle for species name
363
+ cv2.rectangle(annotated, (text_x - 2, text_y - th - 8), (text_x + tw + 4, text_y + 4), (0, 0, 0), -1)
364
+ # Species name
365
+ cv2.putText(annotated, label, (text_x, text_y - 2), font, font_scale, (255, 255, 255), thickness, cv2.LINE_AA)
366
+
367
+ # Confidence badge (green)
368
+ cv2.rectangle(annotated, (text_x - 2, text_y + 6), (text_x + cw + 4, text_y + ch + 12), (0, 200, 100), -1)
369
+ cv2.putText(annotated, conf_label, (text_x, text_y + ch + 8), font, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
370
+
371
+ return annotated, detection_count, species_detected, confidences, None
372
+
373
+
374
+ # ═══════════════════════════════════════════════════════════════════════════════
375
+ # API ENDPOINTS
376
+ # ═══════════════════════════════════════════════════════════════════════════════
377
+
378
+ @app.get("/health")
379
+ def health():
380
+ return {
381
+ "ok": True,
382
+ "model_loaded": model is not None,
383
+ "roboflow_configured": bool(ROBOFLOW_API_KEY),
384
+ "version": "3.1",
385
+ "timestamp": datetime.now().isoformat()
386
+ }
387
+
388
+
389
+ @app.get("/", response_class=HTMLResponse)
390
+ def home():
391
+ with open("index.html", "r", encoding="utf-8") as f:
392
+ return f.read()
393
+
394
+
395
+ @app.get("/species")
396
+ def list_species():
397
+ """List all supported species configurations"""
398
+ return {k: {
399
+ "display_name": v["display_name"],
400
+ "scientific_name": v["scientific_name"],
401
+ "detection_method": "Roboflow API" if v.get("use_roboflow") else "Local YOLO"
402
+ } for k, v in SPECIES_CONFIG.items() if k != "default"}
403
+
404
+
405
+ @app.post("/detect")
406
+ async def detect(
407
+ files: List[UploadFile] = File(...),
408
+ pixels_per_mm: Optional[float] = Query(default=None),
409
+ species: Optional[str] = Query(default="vannamei")
410
+ ):
411
+ calibration = pixels_per_mm if pixels_per_mm else PIXELS_PER_MM
412
+ species_config = get_species_config(species)
413
+ use_roboflow = species_config.get("use_roboflow", False)
414
+
415
+ per_image = []
416
+ all_lengths: List[float] = []
417
+ all_weights: List[float] = []
418
+ overall_total = 0
419
+
420
+ for up in files:
421
+ suffix = os.path.splitext(up.filename)[1] or ".jpg"
422
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
423
+ tmp.write(await up.read())
424
+ image_path = tmp.name
425
+
426
+ try:
427
+ # ═══════════════════════════════════════════════════════════════════
428
+ # ROUTE: Use Roboflow for Bass, Local YOLO for Shrimp/Prawn
429
+ # ═══════════════════════════════════════════════════════════════════
430
+
431
+ if use_roboflow:
432
+ # BASS DETECTION via Roboflow API
433
+ # Shows: COUNT + SPECIES + CONFIDENCE only (no length/weight)
434
+ print(f"🐟 Using Roboflow API for bass detection: {up.filename}")
435
+ annotated, detection_count, species_list, confidences, error = process_bass_image(
436
+ image_path, species_config, calibration
437
+ )
438
+
439
+ if error:
440
+ per_image.append({
441
+ "filename": up.filename,
442
+ "error": error,
443
+ "shrimp_count": 0,
444
+ "specimen_count": 0,
445
+ "average_length_mm": 0.0,
446
+ "lengths_mm": [],
447
+ "annotated_image_png_base64": ""
448
+ })
449
+ continue
450
+
451
+ overall_total += detection_count
452
+
453
+ # Encode annotated image
454
+ annotated_bgr = cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR)
455
+ ok, png = cv2.imencode(".png", annotated_bgr)
456
+ b64 = base64.b64encode(png.tobytes()).decode("utf-8") if ok else ""
457
+
458
+ # For bass: NO length/weight data (just count and species)
459
+ per_image.append({
460
+ "filename": up.filename,
461
+ "shrimp_count": detection_count,
462
+ "specimen_count": detection_count,
463
+ "species_detected": species_list,
464
+ "confidences": [round(c * 100, 1) for c in confidences],
465
+ "average_length_mm": 0, # Not available for bass
466
+ "lengths_mm": [], # Not available for bass
467
+ "weights_g": [], # Not available for bass
468
+ "summary": {
469
+ "note": "Length/weight requires calibrated camera setup",
470
+ "detection_count": detection_count,
471
+ "species_found": list(set(species_list)),
472
+ "avg_confidence": round(sum(confidences) / len(confidences) * 100, 1) if confidences else 0
473
+ },
474
+ "annotated_image_png_base64": b64,
475
+ "detection_method": "Roboflow API"
476
+ })
477
+
478
+ else:
479
+ # SHRIMP/PRAWN DETECTION via Local YOLO model
480
+ # Shows: COUNT + LENGTH + WEIGHT (calibrated for shrimp tank)
481
+ print(f"🦐 Using local YOLO model for detection: {up.filename}")
482
+ results = model(image_path, verbose=False, conf=CONF_THRESHOLD)
483
+ r = results[0]
484
+
485
+ bgr = cv2.imread(image_path)
486
+ if bgr is None:
487
+ per_image.append({
488
+ "filename": up.filename,
489
+ "error": "Could not read image.",
490
+ "shrimp_count": 0,
491
+ "average_length_mm": 0.0,
492
+ "lengths_mm": [],
493
+ "annotated_image_png_base64": ""
494
+ })
495
+ continue
496
+
497
+ rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
498
+ overlay = rgb.copy()
499
+
500
+ lengths_mm = []
501
+ weights_g = []
502
+ text_labels = []
503
+
504
+ if r.masks is not None and r.boxes is not None:
505
+ for mask, box in zip(r.masks, r.boxes):
506
+ class_id = int(box.cls[0])
507
+ class_name = model.names.get(class_id, str(class_id))
508
+ conf = float(box.conf[0])
509
+
510
+ if not is_target_class(class_name) or conf < CONF_THRESHOLD:
511
+ continue
512
+ if mask.xy is None or len(mask.xy) == 0:
513
+ continue
514
+
515
+ pts = np.array(mask.xy[0], dtype=np.float32)
516
+ if pts.shape[0] < 2:
517
+ continue
518
+
519
+ max_px = max_pairwise_distance(pts)
520
+ length_mm = max_px / calibration
521
+ weight_g = estimate_weight(length_mm, species_config)
522
+
523
+ lengths_mm.append(float(length_mm))
524
+ weights_g.append(float(weight_g))
525
+
526
+ # Draw mask
527
+ pts_int = pts.astype(np.int32).reshape((-1, 1, 2))
528
+ cv2.fillPoly(overlay, [pts_int], color=species_config["color"])
529
+
530
+ # Label
531
+ x1 = int(box.xyxy[0][0])
532
+ y1 = int(box.xyxy[0][1])
533
+ text_labels.append((x1, y1, f"{length_mm:.1f}mm | {weight_g:.2f}g"))
534
+
535
+ annotated = cv2.addWeighted(rgb, 1 - MASK_ALPHA, overlay, MASK_ALPHA, 0)
536
+
537
+ # Draw text
538
+ for (x, y, text) in text_labels:
539
+ font = cv2.FONT_HERSHEY_SIMPLEX
540
+ font_scale = 0.55
541
+ thickness = 2
542
+ (tw, th), _ = cv2.getTextSize(text, font, font_scale, thickness)
543
+ x = max(0, x)
544
+ y = max(th + 8, y - 8)
545
+ cv2.rectangle(annotated, (x - 2, y - th - 8), (x + tw + 4, y + 4), (0, 0, 0), -1)
546
+ cv2.putText(annotated, text, (x, y - 2), font, font_scale, (255, 255, 255), thickness, cv2.LINE_AA)
547
+
548
+ specimen_count = len(lengths_mm)
549
+ avg_len = float(np.mean(lengths_mm)) if specimen_count > 0 else 0.0
550
+ avg_weight = float(np.mean(weights_g)) if specimen_count > 0 else 0.0
551
+ total_biomass = sum(weights_g)
552
+
553
+ overall_total += specimen_count
554
+ all_lengths.extend(lengths_mm)
555
+ all_weights.extend(weights_g)
556
+
557
+ # Encode image
558
+ annotated_bgr = cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR)
559
+ ok, png = cv2.imencode(".png", annotated_bgr)
560
+ b64 = base64.b64encode(png.tobytes()).decode("utf-8") if ok else ""
561
+
562
+ per_image.append({
563
+ "filename": up.filename,
564
+ "shrimp_count": specimen_count,
565
+ "specimen_count": specimen_count,
566
+ "average_length_mm": round(avg_len, 2),
567
+ "lengths_mm": [round(x, 2) for x in lengths_mm],
568
+ "weights_g": [round(x, 3) for x in weights_g],
569
+ "summary": {
570
+ "average_length_mm": round(avg_len, 2),
571
+ "average_weight_g": round(avg_weight, 3),
572
+ "total_biomass_g": round(total_biomass, 3),
573
+ },
574
+ "annotated_image_png_base64": b64,
575
+ "detection_method": "Local YOLO"
576
+ })
577
+
578
+ except Exception as e:
579
+ per_image.append({
580
+ "filename": up.filename,
581
+ "error": f"Processing failed: {str(e)}",
582
+ "shrimp_count": 0,
583
+ "average_length_mm": 0.0,
584
+ "lengths_mm": [],
585
+ "annotated_image_png_base64": ""
586
+ })
587
+ finally:
588
+ try:
589
+ os.remove(image_path)
590
+ except:
591
+ pass
592
+
593
+ # Overall stats
594
+ overall_avg_length = float(np.mean(all_lengths)) if all_lengths else 0.0
595
+ overall_avg_weight = float(np.mean(all_weights)) if all_weights else 0.0
596
+ total_biomass_g = sum(all_weights)
597
+
598
+ # Size distribution (only for shrimp with actual measurements)
599
+ size_dist = {"juvenile": 0, "sub-harvest": 0, "harvestable": 0, "optimal": 0}
600
+ for length in all_lengths:
601
+ cat = get_size_category(length, species_config)
602
+ size_dist[cat] += 1
603
+
604
+ total = len(all_lengths) if all_lengths else 1
605
+ size_pct = {k: round(v / total * 100, 1) for k, v in size_dist.items()}
606
+
607
+ # Generate histograms (only for shrimp with actual measurements)
608
+ histograms = {}
609
+
610
+ if all_lengths and not use_roboflow:
611
+ try:
612
+ # Length histogram - compact size
613
+ fig1, ax1 = plt.subplots(figsize=(6, 3), facecolor='#F8FAFC')
614
+ ax1.set_facecolor('#F8FAFC')
615
+ ax1.hist(all_lengths, bins=20, color='#0066FF', edgecolor='white', alpha=0.85)
616
+ ax1.axvline(x=overall_avg_length, color='#00C48C', linestyle='--', linewidth=2, label=f'Mean: {overall_avg_length:.1f}mm')
617
+ ax1.set_xlabel('Length (mm)', fontsize=10, color='#0F172A')
618
+ ax1.set_ylabel('Frequency', fontsize=10, color='#0F172A')
619
+ ax1.set_title(f'{species_config["display_name"]} Length Distribution', fontsize=11, fontweight='bold', color='#0F172A')
620
+ ax1.legend(loc='upper right', fontsize=8)
621
+ ax1.tick_params(colors='#0F172A', labelsize=8)
622
+ ax1.spines['top'].set_visible(False)
623
+ ax1.spines['right'].set_visible(False)
624
+ ax1.spines['bottom'].set_color('#CBD5E1')
625
+ ax1.spines['left'].set_color('#CBD5E1')
626
+ plt.tight_layout()
627
+ buf1 = io.BytesIO()
628
+ fig1.savefig(buf1, format="png", dpi=120, facecolor='#F8FAFC', bbox_inches='tight')
629
+ plt.close(fig1)
630
+ histograms["length_histogram_base64"] = base64.b64encode(buf1.getvalue()).decode("utf-8")
631
+ except Exception as e:
632
+ print(f"Length histogram error: {e}")
633
+
634
+ try:
635
+ # Weight histogram - compact size
636
+ fig2, ax2 = plt.subplots(figsize=(6, 3), facecolor='#F8FAFC')
637
+ ax2.set_facecolor('#F8FAFC')
638
+ ax2.hist(all_weights, bins=20, color='#7B61FF', edgecolor='white', alpha=0.85)
639
+ ax2.axvline(x=overall_avg_weight, color='#00C48C', linestyle='--', linewidth=2, label=f'Mean: {overall_avg_weight:.1f}g')
640
+ ax2.set_xlabel('Weight (g)', fontsize=10, color='#0F172A')
641
+ ax2.set_ylabel('Frequency', fontsize=10, color='#0F172A')
642
+ ax2.set_title(f'{species_config["display_name"]} Weight Distribution', fontsize=11, fontweight='bold', color='#0F172A')
643
+ ax2.legend(loc='upper right', fontsize=8)
644
+ ax2.tick_params(colors='#0F172A', labelsize=8)
645
+ ax2.spines['top'].set_visible(False)
646
+ ax2.spines['right'].set_visible(False)
647
+ ax2.spines['bottom'].set_color('#CBD5E1')
648
+ ax2.spines['left'].set_color('#CBD5E1')
649
+ plt.tight_layout()
650
+ buf2 = io.BytesIO()
651
+ fig2.savefig(buf2, format="png", dpi=120, facecolor='#F8FAFC', bbox_inches='tight')
652
+ plt.close(fig2)
653
+ histograms["weight_histogram_base64"] = base64.b64encode(buf2.getvalue()).decode("utf-8")
654
+ except Exception as e:
655
+ print(f"Weight histogram error: {e}")
656
+
657
+ # Build response based on detection type
658
+ if use_roboflow:
659
+ # BASS response - no length/weight data
660
+ return JSONResponse({
661
+ "timestamp": datetime.now().isoformat(),
662
+ "species": species,
663
+ "species_info": {
664
+ "display_name": species_config["display_name"],
665
+ "scientific_name": species_config["scientific_name"]
666
+ },
667
+ "detection_method": "Roboflow API",
668
+ "calibration_pixels_per_mm": calibration,
669
+ "overall_summary": {
670
+ "total_specimens": overall_total,
671
+ "note": "Length/weight measurement requires calibrated camera setup. Currently showing detection count only.",
672
+ "average_length_mm": 0,
673
+ "average_weight_g": 0,
674
+ "total_biomass_g": 0,
675
+ },
676
+ "histograms": {}, # No histograms for bass
677
+ "per_image": per_image,
678
+ # Legacy fields
679
+ "overall_total_shrimp": overall_total,
680
+ "overall_average_length_mm": 0,
681
+ "histogram_png_base64": ""
682
+ })
683
+ else:
684
+ # SHRIMP response - full length/weight data
685
+ return JSONResponse({
686
+ "timestamp": datetime.now().isoformat(),
687
+ "species": species,
688
+ "species_info": {
689
+ "display_name": species_config["display_name"],
690
+ "scientific_name": species_config["scientific_name"]
691
+ },
692
+ "detection_method": "Local YOLO",
693
+ "calibration_pixels_per_mm": calibration,
694
+ "overall_summary": {
695
+ "total_specimens": overall_total,
696
+ "average_length_mm": round(overall_avg_length, 2),
697
+ "average_weight_g": round(overall_avg_weight, 3),
698
+ "total_biomass_g": round(total_biomass_g, 3),
699
+ "total_biomass_kg": round(total_biomass_g / 1000, 6),
700
+ "size_distribution": {
701
+ "counts": size_dist,
702
+ "percentages": size_pct
703
+ },
704
+ "length_stats": {
705
+ "min": round(min(all_lengths), 2) if all_lengths else 0,
706
+ "max": round(max(all_lengths), 2) if all_lengths else 0,
707
+ "std": round(float(np.std(all_lengths)), 2) if all_lengths else 0
708
+ },
709
+ "weight_stats": {
710
+ "min": round(min(all_weights), 3) if all_weights else 0,
711
+ "max": round(max(all_weights), 3) if all_weights else 0,
712
+ "std": round(float(np.std(all_weights)), 3) if all_weights else 0
713
+ }
714
+ },
715
+ "histograms": histograms,
716
+ "per_image": per_image,
717
+ # Legacy fields
718
+ "overall_total_shrimp": overall_total,
719
+ "overall_average_length_mm": round(overall_avg_length, 2),
720
+ "histogram_png_base64": histograms.get("length_histogram_base64", "")
721
+ })
722
+
723
+
724
+ if __name__ == "__main__":
725
+ import uvicorn
726
+ uvicorn.run(app, host="0.0.0.0", port=8002, reload=True)