danielle2035 commited on
Commit
b308285
·
1 Parent(s): ebd8fd8

fix: add HTML routes for all pages

Browse files
TRAFFIC_ROAD_APP/TRAFFIC_ROAD_APP/Dockerfile DELETED
@@ -1,28 +0,0 @@
1
- FROM python:3.10-slim-bullseye
2
-
3
- RUN apt-get update && apt-get install -y \
4
- libgl1 \
5
- libglib2.0-0 \
6
- libsm6 \
7
- libxext6 \
8
- libxrender-dev \
9
- ffmpeg \
10
- libopencv-dev \
11
- && rm -rf /var/lib/apt/lists/*
12
-
13
- WORKDIR /app
14
-
15
- COPY requirements.txt requirements.txt
16
-
17
- RUN pip install --no-cache-dir -r requirements.txt
18
-
19
- COPY app.py .
20
- COPY static/ ./static/
21
- COPY templates/ ./templates/
22
-
23
- RUN mkdir -p models uploads outputs logs
24
-
25
- ENV PORT=7860
26
- EXPOSE 7860
27
-
28
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/TRAFFIC_ROAD_APP/requirements.txt DELETED
@@ -1,8 +0,0 @@
1
- flask==3.1.3
2
- flask-cors==6.0.2
3
- opencv-python-headless==4.10.0.84
4
- ultralytics==8.4.39
5
- torch==2.11.0
6
- torchvision==0.26.0
7
- numpy==1.26.4
8
- pillow==10.4.0
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/requirements_hf.txt DELETED
@@ -1,13 +0,0 @@
1
- flask==3.1.3
2
- flask-cors==6.0.2
3
- opencv-python-headless==4.13.0.92
4
- ultralytics==8.4.39
5
- torch==2.11.0
6
- torchvision==0.26.0
7
- numpy==2.3.4
8
- pillow==12.0.0
9
- pandas==2.3.3
10
- python-dotenv==1.2.1
11
- requests==2.32.5
12
- pyyaml==6.0.3
13
- tqdm==4.67.3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/templates/dashboard.html DELETED
@@ -1,418 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="light">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TrackIQ Dashboard</title>
7
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
9
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
10
- <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script>
11
- <style>
12
- :root{--bg:#0c1117;--s:#151c25;--s2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--shlg:0 8px 36px rgba(0,0,0,0.55);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.12);--gr:#16a34a;--grb:#052e16;--bl:#3b82f6;--blb:#0f2040;--yw:#f59e0b;--ywb:#1a0f00;--re:#ef4444;--reb:#2d0000;--sw:60px;--r:14px;--rs:9px;}
13
- [data-theme="light"]{--bg:#f0f4f8;--s:#fff;--s2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--grb:#dcfce7;--blb:#dbeafe;--ywb:#fef9c3;--reb:#fee2e2;}
14
- *{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
15
- body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;height:100vh;overflow:hidden;transition:background .3s,color .3s}
16
- .sb{width:var(--sw);background:var(--s);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;flex-shrink:0}
17
- .sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
18
- .sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0}
19
- .sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
20
- .sbb:hover{background:var(--s2);color:var(--t)}.sbb.on{background:var(--acl);color:var(--ac)}
21
- .sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--bg);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300}.sbb:hover .tip{opacity:1}
22
- .sbsp{flex:1}.pw{flex:1;display:flex;flex-direction:column;overflow:hidden}
23
- .topb{height:50px;background:var(--s);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 18px;flex-shrink:0}
24
- .topl,.topr{display:flex;align-items:center;gap:9px}.bn{font-size:14px;font-weight:600}.bn b{color:var(--ac)}
25
- .pill{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--gr);background:var(--grb);padding:2px 8px;border-radius:20px}
26
- .ld{width:6px;height:6px;border-radius:50%;background:var(--gr);animation:blink 2s infinite}
27
- @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
28
- .clk{font-size:10px;font-family:'DM Mono',monospace;color:var(--t2)}
29
- .lg{display:flex;background:var(--s2);border-radius:5px;padding:2px;border:1px solid var(--bd)}
30
- .lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all .2s}.lb.on{background:var(--ac);color:#fff}
31
- .ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--s);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;position:relative;transition:all .2s}.ib:hover{background:var(--s2)}
32
- .nd{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--ac)}
33
- main{flex:1;padding:12px 14px;display:grid;grid-template-columns:minmax(0,1.4fr) 360px;gap:12px;overflow:auto;min-height:0}
34
- .left-col{display:grid;grid-template-rows:auto minmax(190px,1fr) minmax(190px,1fr);gap:10px;min-height:0}
35
- .right-col{display:grid;grid-template-rows:minmax(300px,1.15fr) auto auto;gap:10px;min-height:0}
36
- .mr{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
37
- .mc,.cd{background:var(--s);border:1px solid var(--bd);box-shadow:var(--sh);overflow:hidden}
38
- .mc{border-radius:var(--rs);padding:11px 13px;transition:transform .2s}.mc:hover{transform:translateY(-2px)}
39
- .cd{border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column}.cd:hover{box-shadow:var(--shlg)}
40
- .mct{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
41
- .mcl{font-size:10px;color:var(--t2)}.bx{font-size:9px;font-weight:500;padding:1px 5px;border-radius:4px}
42
- .mv{font-size:20px;font-weight:300;letter-spacing:-.5px;line-height:1}.mu{font-size:10px;color:var(--t2)}.ms{font-size:9px;color:var(--t3);margin-top:2px}
43
- .ct{font-size:11px;font-weight:500;margin-bottom:8px;display:flex;align-items:center;gap:5px;flex-shrink:0}.ct i{color:var(--ac);font-size:10px}
44
- .chart-wrap{flex:1;min-height:0}
45
- .stack{display:grid;grid-template-columns:1fr 1fr;gap:10px}
46
- .hmcd{padding:0}
47
- .hmhd{padding:10px 12px 6px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
48
- .hmhd .ct{margin-bottom:0}
49
- .hmbd{flex:1;position:relative;min-height:220px}
50
- #hmc2{position:absolute;inset:0;width:100%!important;height:100%!important;display:block}
51
- .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}
52
- .statbox{background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:10px 11px}
53
- .statbox .k{font-size:9px;color:var(--t2);text-transform:uppercase;letter-spacing:.04em}
54
- .statbox .v{font-size:18px;font-weight:300;margin-top:4px}
55
- .subline{font-size:10px;color:var(--t2)}
56
- .listbox{display:flex;flex-direction:column;gap:6px}
57
- .vr{display:flex;align-items:center;justify-content:space-between;background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:8px 10px}
58
- .vrl{display:flex;align-items:center;gap:7px;font-size:11px}.vic{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px}.vp{font-size:13px;font-weight:500}
59
- .al{display:flex;align-items:flex-start;gap:8px;padding:7px 0;border-bottom:1px solid var(--bd)}.al:last-child{border-bottom:none}
60
- .ai{width:22px;height:22px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;margin-top:1px}
61
- .at2{font-size:10px;font-weight:500;margin-bottom:1px}.as2{font-size:9px;color:var(--t2)}
62
- .pill-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}.mini-pill{font-size:9px;padding:3px 7px;border-radius:999px;background:var(--s2);border:1px solid var(--bd);color:var(--t2)}
63
- .empty-note{font-size:10px;color:var(--t3);padding-top:4px}
64
- @media(max-width:1180px){main{grid-template-columns:1fr}.right-col{grid-template-rows:minmax(260px,auto) auto auto}.stack{grid-template-columns:1fr}.mr{grid-template-columns:repeat(2,minmax(0,1fr))}}
65
- </style>
66
- </head>
67
- <body>
68
- <nav class="sb">
69
- <a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a>
70
- <div class="sbsep"></div>
71
- <a href="/home" class="sbb"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
72
- <a href="/dashboard" class="sbb on"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
73
- <a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
74
- <a href="/history" class="sbb"><i class="fas fa-film"></i><span class="tip">History</span></a>
75
- <div class="sbsp"></div>
76
- <button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button>
77
- </nav>
78
-
79
- <div class="pw">
80
- <header class="topb">
81
- <div class="topl">
82
- <span class="bn">Track<b>IQ</b></span>
83
- <span style="color:var(--bd);font-size:16px;margin:0 1px;">|</span>
84
- <span style="font-size:12px;font-weight:500;" id="t-dash">Dashboard</span>
85
- <div class="pill"><span class="ld"></span> <span id="liveBadge">Live</span></div>
86
- </div>
87
- <div class="topr">
88
- <span class="clk" id="clk">--:--:--</span>
89
- <div class="lg">
90
- <button class="lb on" onclick="setLang('en',this)">EN</button>
91
- <button class="lb" onclick="setLang('fr',this)">FR</button>
92
- <button class="lb" onclick="setLang('wo',this)">WO</button>
93
- </div>
94
- <button class="ib" onclick="toggleNotifs()"><i class="fas fa-bell"></i><span class="nd" id="nd"></span></button>
95
- <button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button>
96
- </div>
97
- </header>
98
-
99
- <div id="notifP" style="display:none;position:fixed;top:57px;right:65px;z-index:500;width:255px;background:var(--s);border:1px solid var(--bd);border-radius:var(--r);box-shadow:var(--shlg);padding:12px 14px;">
100
- <div style="font-size:11px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between;"><span id="t-notif">Notifications</span><button onclick="clearN()" style="font-size:10px;color:var(--ac);background:none;border:none;cursor:pointer;">Clear all</button></div>
101
- <div id="nl">
102
- <div style="padding:6px 0;border-bottom:1px solid var(--bd);"><div style="font-size:10px;font-weight:500;color:var(--re);" id="t-a1">Congestion Zone A</div><div style="font-size:9px;color:var(--t2);">4 min ago</div></div>
103
- <div style="padding:6px 0;"><div style="font-size:10px;font-weight:500;" id="t-source-loaded">Source ready</div><div style="font-size:9px;color:var(--t2);">Just now</div></div>
104
- </div>
105
- </div>
106
-
107
- <main>
108
- <section class="left-col">
109
- <div class="mr">
110
- <div class="mc"><div class="mct"><span class="mcl" id="t-m1">Objects Today</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="m1Trend">+0%</span></div><div><span class="mv" id="mvToday">0</span> <span class="mu" id="t-veh">obj.</span></div><div class="ms" id="m1Sub">Latest analyzed scenes</div></div>
111
- <div class="mc"><div class="mct"><span class="mcl" id="t-m2">Current Flow</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="flowBadge">Smooth</span></div><div><span class="mv" id="mvFlow">0</span> <span class="mu">obj/min</span></div><div class="ms" id="m2Sub">Based on recent activity</div></div>
112
- <div class="mc"><div class="mct"><span class="mcl" id="t-m3">Avg FPS</span><span class="bx" style="background:var(--blb);color:var(--bl);" id="fpsBadge">Stable</span></div><div><span class="mv" id="mvFps">0</span> <span class="mu">fps</span></div><div class="ms" id="m3Sub">Last completed analysis</div></div>
113
- <div class="mc"><div class="mct"><span class="mcl" id="t-m4">Latest clip</span><span class="bx" style="background:var(--ywb);color:var(--yw);" id="accBadge">Current</span></div><div><span class="mv" id="mvAcc">0</span> <span class="mu">s</span></div><div class="ms" id="m4Sub">Duration of latest processed video</div></div>
114
- </div>
115
-
116
- <div class="stack">
117
- <div class="cd">
118
- <div class="ct"><i class="fas fa-chart-area"></i><span id="t-c1">Flow & speed</span></div>
119
- <div class="chart-wrap"><canvas id="fc"></canvas></div>
120
- </div>
121
- <div class="cd">
122
- <div class="ct"><i class="fas fa-chart-bar"></i><span id="t-c2">Classification</span></div>
123
- <div class="chart-wrap"><canvas id="tc"></canvas></div>
124
- </div>
125
- </div>
126
-
127
- <div class="cd">
128
- <div class="ct"><i class="fas fa-layer-group"></i><span id="t-scenes">Scene summary</span></div>
129
- <div class="stats-grid">
130
- <div class="statbox"><div class="k" id="t-live-source">Source</div><div class="v" id="liveSource">No source</div><div class="subline" id="liveSourceMeta">Ready after analysis</div></div>
131
- <div class="statbox"><div class="k" id="t-live-objects">Objects</div><div class="v" id="liveObjects" id="liveObjects">0</div><div class="subline" id="liveObjectsMeta">Peak visible count</div></div>
132
- <div class="statbox"><div class="k" id="t-live-fps">FPS</div><div class="v" id="liveFps" id="liveFps">0</div><div class="subline" id="liveFpsMeta">Captured stream rate</div></div>
133
- <div class="statbox"><div class="k" id="t-live-duration">Duration</div><div class="v" id="liveDuration" id="liveDuration">0s</div><div class="subline" id="liveDurationMeta">Latest processed clip</div></div>
134
- </div>
135
- <div class="pill-row" id="liveClassPills"></div>
136
- <div class="empty-note" id="liveNote">Current summary follows the latest processed analysis instead of the old floating video preview.</div>
137
- <div id="sceneSummaryGrid" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;"></div>
138
- </div>
139
- </section>
140
-
141
- <aside class="right-col">
142
- <div class="cd hmcd">
143
- <div class="hmhd"><div class="ct"><i class="fas fa-map-marker-alt"></i><span id="t-hm">Position heatmap</span></div><span style="font-size:9px;color:var(--t3);" id="t-hms">Object density</span></div>
144
- <div class="hmbd"><canvas id="hmc2"></canvas></div>
145
- </div>
146
-
147
- <div class="stack">
148
- <div class="cd" style="padding:11px 12px;">
149
- <div class="ct">
150
- <i class="fas fa-chart-pie"></i>
151
- <span>Object Type Distribution</span>
152
- </div>
153
- <!-- Pie chart + légende -->
154
- <div style="position:relative;display:flex;align-items:center;gap:10px;min-height:140px;">
155
- <div style="width:145px;height:145px;flex-shrink:0;">
156
- <canvas id="pieChart"></canvas>
157
-
158
- </div>
159
- <!-- Légende dynamique -->
160
- <div id="pieLegend" style="flex:1;display:flex;flex-direction:column;gap:5px;">
161
- <div style="font-size:10px;color:var(--t3);text-align:center;">No data yet</div>
162
- </div>
163
- </div>
164
- </div>
165
- <div class="cd" style="padding:11px 12px;">
166
- <div class="ct"><i class="fas fa-wave-square"></i><span id="t-live-panel">Live stats</span></div>
167
- <div class="stats-grid">
168
- <div class="statbox"><div class="k" id="t-det">Detected</div><div class="v" id="sbdet">0</div></div>
169
- <div class="statbox"><div class="k" id="t-acc">Latest FPS</div><div class="v" id="sbacc">0</div></div>
170
- </div>
171
- <div class="empty-note" id="livePanelNote">Heatmap and key stats stay visible at all times.</div>
172
- </div>
173
- </div>
174
- </aside>
175
- </main>
176
- </div>
177
-
178
- <script>
179
- setInterval(()=>{document.getElementById('clk').textContent=new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit'});},1000);
180
-
181
- function toggleTheme(){
182
- const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark';
183
- d.setAttribute('data-theme',dk?'light':'dark');
184
- ['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';});
185
- rebuildCharts();
186
- setTimeout(drawHm,50);
187
- rebuildPie();
188
- try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){}
189
- }
190
-
191
- const TL={
192
- en:{dash:'Dashboard',m1:'Objects Today',veh:'obj.',m2:'Current Flow',m3:'Avg FPS',m4:'Latest clip',c1:'Flow from video',c2:'Classification',hm:'Position heatmap',hms:'Detected positions',bk:'Breakdown',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Alerts',notif:'Notifications',scenes:'Scene summary',livesource:'Source',liveobjects:'Objects',livefps:'FPS',liveduration:'Duration',livepanel:'Live stats',det:'Detected',acc:'Latest FPS'},
193
- fr:{dash:'Tableau de bord',m1:'Objets du jour',veh:'obj.',m2:'Flux actuel',m3:'FPS moy.',m4:'Dernier clip',c1:'Flux vidéo',c2:'Classification',hm:'Carte de densité',hms:'Positions détectées',bk:'Répartition',ca:'Voitures',mo:'Motos',tr:'Camions',al:'Alertes',notif:'Notifications',scenes:'Résumé scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Durée',livepanel:'Stats live',det:'Détectés',acc:'FPS récent'},
194
- wo:{dash:'Dashboard',m1:'Objets tey',veh:'obj.',m2:'Yóbbu',m3:'FPS moy.',m4:'Clip mujj',c1:'Flux video',c2:'Wërsëg',hm:'Kàrt',hms:'Posision yi',bk:'Wërsëg',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Yëgël',notif:'Yëgël yi',scenes:'Melo scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Yoon',livepanel:'Stats live',det:'Yi réy',acc:'FPS mujj'}
195
- };
196
-
197
- function setLang(l,btn){
198
- document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on'));
199
- btn.classList.add('on');
200
- const t=TL[l];
201
- const mp={dash:'t-dash',m1:'t-m1',veh:'t-veh',m2:'t-m2',m3:'t-m3',m4:'t-m4',c1:'t-c1',c2:'t-c2',hm:'t-hm',hms:'t-hms',bk:'t-bk',ca:'t-ca',mo:'t-mo',tr:'t-tr',al:'t-al',notif:'t-notif',scenes:'t-scenes',livesource:'t-live-source',liveobjects:'t-live-objects',livefps:'t-live-fps',liveduration:'t-live-duration',livepanel:'t-live-panel',det:'t-det',acc:'t-acc'};
202
- Object.entries(mp).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];});
203
- try{localStorage.setItem('tiq_lang',l);}catch(e){}
204
- }
205
-
206
- let no=false;
207
- function toggleNotifs(){no=!no;document.getElementById('notifP').style.display=no?'block':'none';if(no)document.getElementById('nd').style.display='none';}
208
- function clearN(){document.getElementById('nl').innerHTML='<div style="font-size:10px;color:var(--t3);text-align:center;padding:10px;">No notifications</div>';no=false;document.getElementById('notifP').style.display='none';}
209
- document.addEventListener('click',e=>{if(no&&!e.target.closest('#notifP')&&!e.target.closest('.ib'))toggleNotifs();});
210
-
211
- window.fch=null;
212
- window.tch=null;
213
- window._tiqHeatPoints=[];
214
-
215
- function gc(){
216
- const dk=document.documentElement.getAttribute('data-theme')==='dark';
217
- return{grid:dk?'rgba(255,255,255,0.05)':'rgba(0,0,0,0.06)',tick:dk?'#4a5a6a':'#94a3b8'};
218
- }
219
-
220
- /* ── Pie chart — Object Type Distribution ── */
221
- window.pch = null;
222
-
223
- function rebuildPie(labels=[], data=[], colors=[]){
224
- // Enregistre datalabels seulement si dispo
225
- if(window.ChartDataLabels) Chart.register(ChartDataLabels);
226
- if(window.pch) window.pch.destroy();
227
- const canvas = document.getElementById('pieChart');
228
- if(!canvas) return;
229
- const dk = document.documentElement.getAttribute('data-theme')==='dark';
230
-
231
- const defaultColors = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ec4899','#06b6d4','#ef4444','#f97316'];
232
-
233
- window.pch = new Chart(canvas, {
234
- type: 'pie',
235
- data: {
236
- labels: labels.length ? labels : ['No data'],
237
- datasets: [{
238
- data: data.length ? data : [1],
239
- backgroundColor: colors.length ? colors : [dk?'#1c2531':'#e2e8f0'],
240
- borderColor: dk ? '#151c25' : '#ffffff',
241
- borderWidth: 2,
242
- hoverOffset: 6,
243
- }]
244
- },
245
- options: {
246
- responsive: true,
247
- maintainAspectRatio: false,
248
- plugins: {
249
- legend: { display: false },
250
- tooltip: {
251
- callbacks: {
252
- label: ctx => {
253
- const tot = ctx.dataset.data.reduce((a,b)=>a+b,0);
254
- return ` ${ctx.label}: ${ctx.raw} (${Math.round(ctx.raw/tot*100)}%)`;
255
- }
256
- }
257
- },
258
- /* Affiche les % à l'intérieur de chaque part */
259
- datalabels: {
260
- color: '#fff',
261
- font: { size: 11, weight: 'bold' },
262
- formatter: (value, ctx) => {
263
- const tot = ctx.dataset.data.reduce((a,b)=>a+b,0);
264
- const pct = Math.round(value/tot*100);
265
- return pct >= 5 ? pct+'%' : ''; // cache si trop petit
266
- }
267
- }
268
- }
269
- }
270
- });
271
- }
272
-
273
- function updatePie(uc){
274
- const COLORS = {
275
- 'Vehicle':'#3b82f6','Motorcycle':'#10b981','Truck':'#f59e0b',
276
- 'Bus':'#8b5cf6','Human':'#ec4899','Bicycle':'#06b6d4',
277
- 'Traffic light':'#ef4444','Road sign':'#f97316'
278
- };
279
- const EMO = {
280
- 'Vehicle':'🚗','Motorcycle':'🏍','Truck':'🚚','Bus':'🚌',
281
- 'Human':'🚶','Bicycle':'🚲','Traffic light':'🚦','Road sign':'🛑'
282
- };
283
-
284
- const labels = Object.keys(uc);
285
- const data = Object.values(uc);
286
- const colors = labels.map(l => COLORS[l] || '#888');
287
- const total = data.reduce((a,b)=>a+b,0);
288
-
289
- // Recrée le chart
290
- rebuildPie(labels, data, colors);
291
-
292
- // Met à jour le label central
293
- const ptn = document.getElementById('pieTotalNum');
294
- if(ptn) ptn.textContent = total;
295
-
296
- // Légende dynamique
297
- const leg = document.getElementById('pieLegend');
298
- if(!leg) return;
299
- leg.innerHTML = '';
300
- labels.forEach((lbl, i) => {
301
- const pct = Math.round(data[i]/Math.max(total,1)*100);
302
- const row = document.createElement('div');
303
- row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:6px;';
304
- row.innerHTML = `
305
- <div style="display:flex;align-items:center;gap:5px;font-size:11px;overflow:hidden;">
306
- <span style="width:9px;height:9px;border-radius:2px;background:${colors[i]};flex-shrink:0;display:inline-block;"></span>
307
- <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${EMO[lbl]||'📦'} ${lbl}</span>
308
- </div>
309
- <span style="font-size:10px;color:var(--t2);flex-shrink:0;">${data[i]}</span>
310
- `;
311
- leg.appendChild(row);
312
- });
313
- }
314
-
315
- function rebuildCharts(){
316
- if(window.fch)window.fch.destroy();
317
- if(window.tch)window.tch.destroy();
318
- const {grid,tick}=gc();
319
- /* Flow chart : 2 datasets (Objects + FPS) — compatible avec trackiq-api.js */
320
- window.fch=new Chart(document.getElementById('fc'),{
321
- data:{labels:['—'],datasets:[
322
- {type:'line',label:'Objects',data:[0],borderColor:'#3b82f6',backgroundColor:'rgba(59,130,246,0.08)',borderWidth:1.5,tension:0.45,pointRadius:0,fill:true,yAxisID:'y'},
323
- {type:'line',label:'FPS',data:[0],borderColor:'#10b981',backgroundColor:'rgba(16,185,129,0.05)',borderWidth:1.5,tension:0.45,pointRadius:0,yAxisID:'y2'}
324
- ]},
325
- options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},plugins:{legend:{labels:{boxWidth:8,font:{size:9},color:tick}}},scales:{x:{ticks:{font:{size:9},color:tick,maxTicksLimit:8},grid:{color:grid},border:{display:false}},y:{position:'left',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y2:{position:'right',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{drawOnChartArea:false},border:{display:false}}}}
326
- });
327
- /* Classification chart : NON stacké, 1 dataset — compatible avec trackiq-api.js */
328
- window.tch=new Chart(document.getElementById('tc'),{
329
- type:'bar',
330
- data:{labels:['—'],datasets:[
331
- {label:'Count',data:[0],backgroundColor:'#3b82f6',borderRadius:3,borderSkipped:false}
332
- ]},
333
- options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y:{beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}}}}
334
- });
335
- }
336
-
337
- function drawHm(){
338
- const c=document.getElementById('hmc2'),b=c.parentElement;
339
- const w=b.offsetWidth,h=b.offsetHeight;
340
- if(!w||!h)return;
341
- c.width=w;c.height=h;
342
- const ctx=c.getContext('2d');
343
- const dk=document.documentElement.getAttribute('data-theme')==='dark';
344
- ctx.fillStyle=dk?'#0c1219':'#e8eef5';
345
- ctx.fillRect(0,0,w,h);
346
- ctx.strokeStyle=dk?'rgba(255,255,255,0.07)':'rgba(0,0,0,0.07)';
347
- ctx.lineWidth=1;
348
- [0.25,0.5,0.75].forEach(r=>{ctx.beginPath();ctx.moveTo(0,h*r);ctx.lineTo(w,h*r);ctx.stroke();ctx.beginPath();ctx.moveTo(w*r,0);ctx.lineTo(w*r,h);ctx.stroke();});
349
- const pts=Array.isArray(window._tiqHeatPoints)?window._tiqHeatPoints:[];
350
- if(pts.length){
351
- let mxR=0,mxH=0;
352
- pts.forEach(p=>{if(p.r>mxR)mxR=p.r;if(p.h>mxH)mxH=p.h;});
353
- mxR=Math.max(mxR,0.01);mxH=Math.max(mxH,0.01);
354
- pts.forEach(p=>{
355
- const px=(p.x/mxR)*w,py=(p.y/mxH)*h;
356
- const rad=Math.max(10,Math.min(w,h)*0.015);
357
- const grd=ctx.createRadialGradient(px,py,0,px,py,rad);
358
- grd.addColorStop(0,'rgba(220,38,38,0.5)');
359
- grd.addColorStop(0.4,'rgba(180,0,0,0.18)');
360
- grd.addColorStop(1,'rgba(0,0,0,0)');
361
- ctx.fillStyle=grd;ctx.fillRect(0,0,w,h);
362
- });
363
- ctx.font="9px 'DM Mono',monospace";
364
- ctx.fillStyle=dk?'rgba(255,255,255,0.3)':'rgba(0,0,0,0.25)';
365
- ctx.textAlign='left';ctx.fillText(pts.length+' points',8,13);
366
- }else{
367
- ctx.fillStyle=dk?'rgba(255,255,255,0.18)':'rgba(0,0,0,0.2)';
368
- ctx.font="12px 'DM Sans',sans-serif";ctx.textAlign='center';
369
- ctx.fillText('Run an analysis to build the heatmap',w/2,h/2);
370
- ctx.textAlign='left';
371
- }
372
- ctx.font="10px 'DM Mono',monospace";
373
- ctx.fillStyle=dk?'rgba(255,255,255,0.24)':'rgba(0,0,0,0.2)';
374
- ctx.fillText('N',w/2-3,12);ctx.fillText('S',w/2-3,h-5);ctx.fillText('W',5,h/2+3);ctx.fillText('E',w-10,h/2+3);
375
- }
376
-
377
- async function loadHeatmap(){
378
- try{
379
- const r=await fetch('/api/logs/rows');
380
- if(!r.ok)return;
381
- const rows=await r.json();
382
- if(!rows.length)return;
383
- const pts=[];let mxX=0,mxY=0;
384
- rows.slice(0,4000).forEach(r=>{
385
- if(r.cx>0&&r.cy>0){pts.push({x:r.cx,y:r.cy,r:0,h:0});if(r.cx>mxX)mxX=r.cx;if(r.cy>mxY)mxY=r.cy;}
386
- });
387
- if(!pts.length)return;
388
- pts.forEach(p=>{p.r=mxX;p.h=mxY;});
389
- window._tiqHeatPoints=pts;
390
- drawHm();
391
- }catch(e){}
392
- }
393
-
394
- setTimeout(rebuildCharts,120);
395
- setTimeout(rebuildPie,150);
396
- setTimeout(drawHm,180);
397
- setTimeout(loadHeatmap,600);
398
- setInterval(loadHeatmap,20000);
399
- window.addEventListener('resize',()=>setTimeout(drawHm,50));
400
-
401
- (function(){
402
- try{
403
- const th=localStorage.getItem('tiq_theme');
404
- if(th){
405
- document.documentElement.setAttribute('data-theme',th);
406
- if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});}
407
- }
408
- const lg=localStorage.getItem('tiq_lang');
409
- if(lg&&lg!=='en'){
410
- const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]');
411
- if(btn)setLang(lg,btn);
412
- }
413
- }catch(e){}
414
- })();
415
- </script>
416
- <script src="/static/trackiq-api.js"></script>
417
- </body>
418
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/templates/history.html DELETED
@@ -1,295 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="light">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TrackIQ — History</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
10
- <style>
11
- :root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--green:#16a34a;--green-bg:#dcfce7;--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
12
- [data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;--green-bg:#052e16;}
13
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
14
- ::-webkit-scrollbar{width:0;}
15
- body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh;transition:background 0.3s,color 0.3s;}
16
-
17
- .sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200;box-shadow:2px 0 12px rgba(0,0,0,0.04);}
18
- .sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:white;font-size:14px;margin-bottom:10px;text-decoration:none;}
19
- .sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0;}
20
- .sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all 0.2s;text-decoration:none;border:none;background:none;position:relative;}
21
- .sb-btn:hover{background:var(--surface2);color:var(--text);}
22
- .sb-btn.active{background:var(--accent-light);color:var(--accent);}
23
- .sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity 0.15s;z-index:300;}
24
- .sb-btn:hover .tip{opacity:1;}
25
- .sb-spacer{flex:1;}
26
-
27
- .page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column;}
28
- .topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px;position:sticky;top:0;z-index:100;}
29
- .topbar-l{display:flex;align-items:center;gap:10px;}
30
- .brand-name{font-size:15px;font-weight:600;} .brand-name b{color:var(--accent);}
31
- .sep{color:var(--border);font-size:18px;margin:0 2px;}
32
- .topbar-r{display:flex;align-items:center;gap:7px;}
33
- .lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border);}
34
- .lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif;transition:all 0.2s;}
35
- .lang-btn.active{background:var(--accent);color:white;}
36
- .icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px;}
37
- .icon-btn:hover{background:var(--surface2);}
38
-
39
- main{flex:1;padding:18px 20px;max-width:1380px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:16px;}
40
-
41
- /* PLAYER CARD — full width */
42
- .player-card{grid-column:1/3;background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow-lg);overflow:hidden;}
43
- .player-head{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
44
- .player-title{font-size:13px;font-weight:500;display:flex;align-items:center;gap:7px;}
45
- .player-title i{color:var(--accent);}
46
- .player-body{position:relative;background:#000;}
47
- .player-body video{width:100%;display:block;max-height:420px;object-fit:contain;}
48
- .player-hud{position:absolute;top:11px;left:13px;color:white;font-family:'DM Mono',monospace;font-size:12px;line-height:1.8;text-shadow:1px 1px 4px rgba(0,0,0,0.98);pointer-events:none;}
49
- .player-empty{padding:80px 20px;text-align:center;color:var(--text3);}
50
- .player-empty i{font-size:44px;display:block;margin-bottom:12px;}
51
- .player-empty p{font-size:13px;}
52
-
53
- /* VIDEO GRID */
54
- .vid-card{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow);overflow:hidden;cursor:pointer;transition:all 0.2s;}
55
- .vid-card:hover{box-shadow:var(--shadow-lg);transform:translateY(-3px);}
56
- .vid-card.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent);}
57
- .vid-thumb{position:relative;background:#0a0a0a;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;overflow:hidden;}
58
- .vid-thumb video{width:100%;height:100%;object-fit:cover;display:block;}
59
- .vid-thumb-ph{color:rgba(255,255,255,0.3);font-size:32px;}
60
- .play-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.3);opacity:0;transition:opacity 0.2s;}
61
- .play-overlay i{font-size:32px;color:white;}
62
- .vid-card:hover .play-overlay{opacity:1;}
63
- .vid-info{padding:11px 13px;}
64
- .vid-name{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px;}
65
- .vid-meta{font-size:10px;color:var(--text2);display:flex;align-items:center;justify-content:space-between;}
66
- .vid-meta span{display:flex;align-items:center;gap:4px;}
67
- .vid-actions{display:flex;gap:6px;margin-top:8px;}
68
- .btn-vi{flex:1;padding:5px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text2);font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:4px;transition:all 0.2s;}
69
- .btn-vi:hover{background:var(--accent);color:white;border-color:var(--accent);}
70
- .btn-vi.danger:hover{background:#dc2626;border-color:#dc2626;}
71
-
72
- /* EMPTY HISTORY */
73
- .empty-hist{text-align:center;padding:60px 20px;color:var(--text3);}
74
- .empty-hist i{font-size:48px;display:block;margin-bottom:14px;}
75
- .empty-hist p{font-size:13px;margin-bottom:16px;}
76
- .empty-hist a{display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:white;padding:9px 18px;border-radius:9px;text-decoration:none;font-size:12px;}
77
-
78
- /* BADGE */
79
- .badge{font-size:10px;font-weight:500;padding:1px 6px;border-radius:4px;}
80
- .bg{background:var(--green-bg);color:var(--green);}
81
- </style>
82
- </head>
83
- <body>
84
-
85
- <nav class="sidebar">
86
- <a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
87
- <div class="sb-sep"></div>
88
- <a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip" id="tip-analyze">Analyze</span></a>
89
- <a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip" id="tip-dash">Dashboard</span></a>
90
- <a href="/logs" class="sb-btn"><i class="fas fa-list-ul"></i><span class="tip" id="tip-logs">Logs</span></a>
91
- <a href="/history" class="sb-btn active"><i class="fas fa-film"></i><span class="tip" id="tip-hist">History</span></a>
92
- <div class="sb-spacer"></div>
93
- <button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
94
- </nav>
95
-
96
- <div class="page-wrap">
97
- <header class="topbar">
98
- <div class="topbar-l">
99
- <span class="brand-name">Track<b>IQ</b></span>
100
- <span class="sep">|</span>
101
- <span style="font-size:13px;font-weight:500;" id="t-title">Annotated History</span>
102
- </div>
103
- <div class="topbar-r">
104
- <div class="lang-group">
105
- <button class="lang-btn active" onclick="setLang('en',this)">EN</button>
106
- <button class="lang-btn" onclick="setLang('fr',this)">FR</button>
107
- <button class="lang-btn" onclick="setLang('wo',this)">WO</button>
108
- </div>
109
- <button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
110
- </div>
111
- </header>
112
-
113
- <main id="mainGrid">
114
-
115
- <!-- Videos grid -->
116
- <div id="videoGrid" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;"></div>
117
-
118
- </main>
119
- </div>
120
-
121
- <script>
122
- /* THEME */
123
- function toggleTheme(){
124
- const d=document.documentElement,dark=d.getAttribute('data-theme')==='dark';
125
- d.setAttribute('data-theme',dark?'light':'dark');
126
- ['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
127
- try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
128
- }
129
-
130
- /* LANG */
131
- const T={
132
- en:{title:'Annotated History',player:'Video player',dl:'Download','select-vid':'Select a video below to play it',tipAnalyze:'Analyze',tipDash:'Dashboard',tipLogs:'Logs',tipHist:'History',reanalyze:'Re-analyze',download:'Download',delete:'Delete',empty:'No annotated videos yet.',goAnalyze:'Go to Analyze'},
133
- fr:{title:'Historique annoté',player:'Lecteur vidéo',dl:'Télécharger','select-vid':'Sélectionnez une vidéo ci-dessous',tipAnalyze:'Analyser',tipDash:'Tableau de bord',tipLogs:'Logs',tipHist:'Historique',reanalyze:'Ré-analyser',download:'Télécharger',delete:'Supprimer',empty:'Aucune vidéo annotée.',goAnalyze:'Aller sur Analyser'},
134
- wo:{title:'Màgg yi',player:'Video bi',dl:'Télécharger','select-vid':'Tànn video ci suuf',tipAnalyze:'Jàng',tipDash:'Dashboard',tipLogs:'Logs',tipHist:'Màgg',reanalyze:'Wërtëk',download:'Télécharger',delete:'Bañ',empty:'Amul video.',goAnalyze:'Dem Analyser'}
135
- };
136
- let curLang='en';
137
- function setLang(l,btn){
138
- curLang=l;
139
- document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');
140
- const t=T[l];
141
- Object.entries({title:'t-title',player:'t-player',dl:'t-dl','select-vid':'t-select-vid',tipAnalyze:'tip-analyze',tipDash:'tip-dash',tipLogs:'tip-logs',tipHist:'tip-hist'}).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];});
142
- renderGrid();
143
- try{localStorage.setItem('tiq_lang',l);}catch(e){}
144
- }
145
-
146
- /* HISTORY DATA */
147
- function getHistory(){try{return JSON.parse(localStorage.getItem('tiq_history')||'[]');}catch(e){return[];}}
148
- function saveHistory(h){try{localStorage.setItem('tiq_history',JSON.stringify(h));}catch(e){}}
149
-
150
- /* FORMAT DATE */
151
- function fmtDate(iso){
152
- try{const d=new Date(iso);return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})+' '+d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'});}catch(e){return 'Unknown date';}
153
- }
154
-
155
- let activeUrl=null,activeIdx=null;
156
-
157
- function dlVideo(url,name){
158
- const a=document.createElement('a');a.href=url;
159
- a.download=name||'trackiq_video.mp4';a.click();
160
- }
161
-
162
- function playVideo(url,name,idx){
163
- activeUrl=url;activeIdx=idx;
164
- const op=document.getElementById('overlayPlayer');
165
- op.src=url;op.play().catch(()=>{});
166
- document.getElementById('overlayName').textContent=name;
167
- document.getElementById('vidOverlay').style.display='flex';
168
- }
169
- function closeOverlay(){
170
- const op=document.getElementById('overlayPlayer');op.pause();op.src='';
171
- document.getElementById('vidOverlay').style.display='none';
172
- activeUrl=null;activeIdx=null;
173
- }
174
- function dlOverlay(){if(activeUrl){const a=document.createElement('a');a.href=activeUrl;a.download='trackiq.mp4';a.click();}}
175
-
176
- function dlActive(){
177
- if(!activeUrl)return;
178
- const a=document.createElement('a');a.href=activeUrl;a.download='trackiq_annotated.mp4';a.click();
179
- }
180
-
181
- function deleteVideo(idx){
182
- const h=getHistory();h.splice(idx,1);saveHistory(h);
183
- if(activeIdx===idx){
184
- closeModal();
185
- activeUrl=null;activeIdx=null;
186
- }
187
- renderGrid();
188
- }
189
-
190
- function renderGrid(){
191
- const hist=getHistory();
192
- const t=T[curLang];
193
- const grid=document.getElementById('videoGrid');grid.innerHTML='';
194
-
195
- if(!hist.length){
196
- const empty=document.createElement('div');empty.className='empty-hist';
197
- empty.innerHTML=`<i class="fas fa-film"></i><p>${t.empty}</p><a href="/home">${t.goAnalyze} <i class="fas fa-arrow-right"></i></a>`;
198
- grid.appendChild(empty);return;
199
- }
200
-
201
- hist.forEach((item,i)=>{
202
- const card=document.createElement('div');card.className='vid-card'+(activeIdx===i?' selected':'');
203
- const isBlob=item.url&&item.url.startsWith('blob:');
204
- const isBackend=item.url&&(item.url.startsWith('/api/')||item.url.startsWith('http'));
205
- const canPlay=isBlob||isBackend;
206
- card.innerHTML=`
207
- <div class="vid-thumb">
208
- ${isBlob
209
- ? `<video src="${item.url}" style="width:100%;height:100%;object-fit:cover;" muted preload="metadata" onloadeddata="this.currentTime=1"></video>`
210
- : `<div class="vid-thumb-ph" style="background:#0a0e15;display:flex;align-items:center;justify-content:center;height:100%;">
211
- <i class="fas fa-film" style="font-size:32px;color:rgba(255,255,255,0.2);"></i>
212
- </div>`
213
- }
214
- <div class="play-overlay"><i class="fas fa-play"></i></div>
215
- </div>
216
- <div class="vid-info">
217
- <div class="vid-name" title="${item.name}">${item.name||'Unnamed'}</div>
218
- <div class="vid-meta">
219
- <span><i class="fas fa-calendar" style="font-size:9px;"></i> ${fmtDate(item.date)}</span>
220
- <span class="badge bg">${isBlob?'Local':'Server'}</span>
221
- </div>
222
- <div class="vid-actions">
223
- <button class="btn-vi" onclick="event.stopPropagation();playVideo('${item.url}','${item.name}',${i})">
224
- <i class="fas fa-play"></i> Play
225
- </button>
226
- <button class="btn-vi" onclick="event.stopPropagation();dlVideo('${item.url}','${item.name}')">
227
- <i class="fas fa-download"></i>
228
- </button>
229
- <button class="btn-vi danger" onclick="event.stopPropagation();deleteVideo(${i})">
230
- <i class="fas fa-trash"></i>
231
- </button>
232
- </div>
233
- </div>
234
- `;
235
- card.onclick=(ev)=>{if(!ev.target.closest('.btn-vi'))playVideo(item.url,item.name,i);};
236
- grid.appendChild(card);
237
- });
238
- }
239
-
240
- /* HUD ticker on player */
241
- setInterval(()=>{
242
- if(!activeUrl)return;
243
- const f=570+Math.floor(Math.random()*80),s=295+Math.floor(Math.random()*40);
244
- const hf=document.getElementById('oHudF');if(hf)hf.textContent=`F: = ${f} ▶ ${s}`;
245
- const car=2+Math.floor(Math.random()*6),van=Math.floor(Math.random()*3),bike=Math.floor(Math.random()*6),p=Math.floor(Math.random()*7);
246
- const hc=document.getElementById('oHudC');if(hc)hc.innerHTML=`Car = ${car}<br>Van = ${van}<br>bike = ${bike}<br>person = ${p}`;
247
- },2200);
248
-
249
- document.addEventListener('keydown',e=>{if(e.key==='Escape')closeOverlay();});
250
- /* INIT */
251
- (function(){
252
- try{
253
- const th=localStorage.getItem('tiq_theme');if(th){document.documentElement.setAttribute('data-theme',th);if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}}
254
- const lg=localStorage.getItem('tiq_lang');if(lg&&lg!=='en'){const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);if(btn)setLang(lg,btn);}
255
- }catch(e){}
256
- renderGrid();
257
- })();
258
- </script>
259
- <script src="/static/trackiq-api.js"></script>
260
-
261
- <!-- ── Modal lecteur vidéo ── -->
262
- <div id="vidOverlay" style="display:none;position:fixed;inset:0;z-index:1000;
263
- background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
264
- align-items:center;justify-content:center;flex-direction:column;gap:12px;">
265
-
266
- <!-- Header -->
267
- <div style="display:flex;align-items:center;justify-content:space-between;
268
- width:min(90vw,860px);padding:0 4px;">
269
- <span id="overlayName" style="color:#fff;font-size:13px;font-weight:500;
270
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%;"></span>
271
- <div style="display:flex;gap:8px;">
272
- <button onclick="dlOverlay()" style="background:rgba(255,255,255,0.12);color:#fff;
273
- border:none;border-radius:7px;padding:6px 13px;font-size:12px;cursor:pointer;
274
- font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:5px;">
275
- <i class="fas fa-download"></i> Download
276
- </button>
277
- <button onclick="closeOverlay()" style="background:rgba(220,38,38,0.7);color:#fff;
278
- border:none;border-radius:7px;padding:6px 13px;font-size:12px;cursor:pointer;
279
- font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:5px;">
280
- <i class="fas fa-times"></i> Close
281
- </button>
282
- </div>
283
- </div>
284
-
285
- <!-- Vidéo -->
286
- <video id="overlayPlayer" controls autoplay
287
- style="width:min(90vw,860px);max-height:75vh;border-radius:10px;
288
- background:#000;object-fit:contain;box-shadow:0 20px 60px rgba(0,0,0,0.6);">
289
- </video>
290
-
291
- <!-- Hint ESC -->
292
- <span style="color:rgba(255,255,255,0.3);font-size:10px;">Press ESC to close</span>
293
- </div>
294
- </body>
295
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/templates/home.html DELETED
@@ -1,684 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="light">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TrackIQ — Analyze</title>
7
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
9
- <style>
10
- :root{--bg:#f0f4f8;--sur:#fff;--sur2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.07);--gr:#16a34a;--sw:60px;--r:14px;--rs:9px;}
11
- [data-theme="dark"]{--bg:#0c1117;--sur:#151c25;--sur2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;}
12
- *{box-sizing:border-box;margin:0;padding:0;}::-webkit-scrollbar{width:0;}
13
- body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;min-height:100vh;}
14
-
15
- /* Sidebar */
16
- .sb{width:var(--sw);background:var(--sur);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200;}
17
- .sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none;}
18
- .sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0;}
19
- .sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all 0.2s;text-decoration:none;border:none;background:none;position:relative;}
20
- .sbb:hover{background:var(--sur2);color:var(--t);}.sbb.on{background:var(--acl);color:var(--ac);}
21
- .sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--sur);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300;}.sbb:hover .tip{opacity:1;}
22
- .sbsp{flex:1;}
23
-
24
- /* Layout */
25
- .pw{margin-left:var(--sw);flex:1;display:flex;flex-direction:column;}
26
- .topb{height:50px;background:var(--sur);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100;}
27
- .topl{display:flex;align-items:center;gap:9px;}
28
- .bn{font-size:14px;font-weight:600;}.bn b{color:var(--ac);}
29
- .topr{display:flex;align-items:center;gap:6px;}
30
- .lg{display:flex;background:var(--sur2);border-radius:5px;padding:2px;border:1px solid var(--bd);}
31
- .lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all 0.2s;}.lb.on{background:var(--ac);color:#fff;}
32
- .ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--sur);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;}
33
- .ib:hover{background:var(--sur2);}
34
-
35
- main{flex:1;padding:14px 16px;display:grid;grid-template-columns:340px 1fr;gap:12px;max-width:1400px;width:100%;margin:0 auto;}
36
-
37
- /* Card */
38
- .card{background:var(--sur);border-radius:var(--r);border:1px solid var(--bd);box-shadow:var(--sh);padding:14px 16px;}
39
- .ct{font-size:12px;font-weight:500;margin-bottom:11px;display:flex;align-items:center;gap:6px;}
40
- .ct i{color:var(--ac);}
41
-
42
- /* Source tabs */
43
- .stabs{display:flex;gap:3px;background:var(--sur2);border-radius:7px;padding:3px;border:1px solid var(--bd);margin-bottom:11px;}
44
- .stab{flex:1;padding:6px 4px;border-radius:5px;font-size:11px;cursor:pointer;color:var(--t2);border:none;background:none;font-family:'DM Sans',sans-serif;transition:all 0.2s;display:flex;align-items:center;justify-content:center;gap:4px;}
45
- .stab.on{background:var(--sur);color:var(--t);box-shadow:0 1px 4px rgba(0,0,0,0.1);font-weight:500;}
46
-
47
- /* Upload zone */
48
- .uzone{border:2px dashed var(--bd);border-radius:var(--rs);min-height:110px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:all 0.2s;padding:14px;position:relative;overflow:hidden;}
49
- .uzone:hover,.uzone.drag{border-color:var(--ac);background:var(--acl);}
50
- .uzone.has{border-style:solid;border-color:var(--ac);}
51
- .uzone i{font-size:22px;color:var(--t3);}
52
- .uzone.has i{color:var(--ac);}
53
- .uzone p{font-size:11px;color:var(--t2);text-align:center;}
54
- .uzone input{position:absolute;inset:0;opacity:0;cursor:pointer;}
55
- .fname{font-size:11px;font-weight:500;color:var(--ac);display:none;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
56
- .prog{display:none;background:var(--sur2);border-radius:3px;height:4px;margin-top:7px;overflow:hidden;}
57
- .progb{height:100%;background:var(--ac);width:0%;transition:width 0.3s;}
58
- .progt{font-size:10px;color:var(--t2);margin-top:4px;display:none;}
59
-
60
- /* URL */
61
- .urow{display:flex;gap:6px;}
62
- .uin{flex:1;padding:7px 9px;border:1px solid var(--bd);border-radius:7px;font-size:12px;background:var(--sur2);color:var(--t);font-family:'DM Sans',sans-serif;outline:none;}
63
- .uin:focus{border-color:var(--ac);}
64
- .uin::placeholder{color:var(--t3);}
65
- .bsm{padding:7px 12px;background:var(--ac);color:#fff;border:none;border-radius:7px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;}
66
-
67
- /* Webcam */
68
- .wcbox{background:#000;border-radius:8px;overflow:hidden;aspect-ratio:16/9;position:relative;margin-bottom:10px;}
69
- .wcbox video,.wcbox img{width:100%;height:100%;object-fit:cover;display:block;}
70
- .wcph{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:rgba(255,255,255,0.3);font-size:11px;}
71
- .wcph i{font-size:22px;}
72
- .recbdg{position:absolute;top:7px;left:8px;display:none;align-items:center;gap:4px;background:rgba(220,38,38,0.85);color:#fff;font-size:10px;padding:2px 8px;border-radius:10px;}
73
- @keyframes blink{0%,100%{opacity:1}50%{opacity:0.3}}
74
- .recbdg i{font-size:8px;animation:blink 1s infinite;}
75
-
76
- /* Classes */
77
- .clshd{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px;}
78
- .clshd span{font-size:11px;color:var(--t2);}
79
- .clshd button{font-size:11px;color:var(--ac);background:none;border:none;cursor:pointer;font-family:'DM Sans',sans-serif;}
80
- .clsg{display:grid;grid-template-columns:1fr 1fr;gap:5px;}
81
- .clsi{display:flex;align-items:center;gap:5px;padding:5px 8px;border-radius:6px;border:1px solid var(--bd);cursor:pointer;transition:all 0.2s;font-size:11px;background:var(--sur2);}
82
- .clsi:hover{border-color:var(--ac);}.clsi.on{border-color:var(--ac);background:var(--acl);}
83
- .cldot{width:8px;height:8px;border-radius:3px;flex-shrink:0;}
84
-
85
- /* Buttons */
86
- .runbtn{width:100%;padding:10px;background:var(--ac);color:#fff;border:none;border-radius:9px;font-size:13px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:7px;transition:all 0.2s;margin-top:4px;}
87
- .runbtn:hover{background:var(--ac2);}.runbtn:disabled{opacity:0.4;cursor:not-allowed;}
88
- .rstbtn{width:100%;padding:8px;background:var(--sur2);color:var(--t2);border:1px solid var(--bd);border-radius:9px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:6px;transition:all 0.2s;}
89
- .rstbtn:hover{border-color:var(--ac);color:var(--ac);}
90
-
91
- /* Right col */
92
- .rc{display:flex;flex-direction:column;gap:11px;}
93
-
94
- /* Big panel */
95
- .bp{background:var(--sur);border-radius:var(--r);border:1px solid var(--bd);box-shadow:var(--shlg);overflow:hidden;}
96
- .bph{padding:10px 14px;border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;}
97
- .bpht{font-size:12px;font-weight:500;display:flex;align-items:center;gap:6px;}
98
- .bpht i{color:var(--ac);}
99
- .bpb{position:relative;background:#080e15;height:420px;}
100
-
101
- /* Video dans le panel */
102
- .bpb video,.bpb img#wcYoloImg{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:none;}
103
-
104
- /* HUD top-left */
105
- .hud{position:absolute;top:10px;left:11px;color:#fff;font-family:'DM Mono',monospace;font-size:11px;line-height:1.85;text-shadow:1px 1px 4px rgba(0,0,0,0.98);pointer-events:none;display:none;z-index:4;}
106
- .hudf{font-size:9px;color:rgba(255,255,255,0.4);margin-bottom:2px;}
107
-
108
- /* Count overlay top-right */
109
- .cov{position:absolute;top:10px;right:11px;display:none;flex-direction:column;gap:4px;pointer-events:none;z-index:4;}
110
- .cbdg{display:flex;flex-direction:column;background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);border-left:3px solid transparent;border-radius:5px;padding:4px 9px;min-width:115px;}
111
- .cbdg .cn{font-size:13px;font-weight:600;line-height:1.2;}
112
- .cbdg .cu{font-size:9px;color:#ccc;line-height:1.2;}
113
-
114
- /* No-object banner */
115
- .noobj{display:none;position:absolute;bottom:14px;left:50%;transform:translateX(-50%);z-index:5;
116
- background:rgba(220,38,38,0.9);color:#fff;font-size:12px;font-weight:500;
117
- padding:6px 16px;border-radius:20px;align-items:center;gap:7px;white-space:nowrap;}
118
-
119
- /* Empty state */
120
- .es{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;color:rgba(255,255,255,0.18);}
121
- .es i{font-size:38px;}.es p{font-size:12px;}
122
-
123
- /* Stats row */
124
- .sr{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;}
125
- .sc{background:var(--sur);border-radius:var(--rs);border:1px solid var(--bd);box-shadow:var(--sh);padding:10px 12px;}
126
- .sc .sl{font-size:10px;color:var(--t2);margin-bottom:3px;}
127
- .sc .sv{font-size:18px;font-weight:300;letter-spacing:-0.5px;}
128
- .sc .su{font-size:10px;color:var(--t2);}
129
-
130
- /* Download bar */
131
- .dlb{display:flex;align-items:center;justify-content:space-between;background:var(--sur);border-radius:var(--rs);border:1px solid var(--bd);box-shadow:var(--sh);padding:9px 13px;}
132
- .dlb span{font-size:11px;color:var(--t2);}
133
- .btndl{display:flex;align-items:center;gap:5px;background:var(--ac);color:#fff;border:none;border-radius:7px;padding:7px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;}
134
- .btndl:hover{background:var(--ac2);}.btndl:disabled{opacity:0.4;cursor:not-allowed;background:var(--t3);}
135
- </style>
136
- </head>
137
- <body>
138
-
139
- <nav class="sb">
140
- <a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a>
141
- <div class="sbsep"></div>
142
- <a href="/home" class="sbb on"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
143
- <a href="/dashboard" class="sbb"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
144
- <a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
145
- <a href="/history" class="sbb"><i class="fas fa-film"></i><span class="tip">History</span></a>
146
- <div class="sbsp"></div>
147
- <button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button>
148
- </nav>
149
-
150
- <div class="pw">
151
- <header class="topb">
152
- <div class="topl">
153
- <span class="bn">Track<b>IQ</b></span>
154
- <span style="color:var(--bd);font-size:16px;">|</span>
155
- <span style="font-size:12px;font-weight:500;color:var(--t2);">Video Analysis</span>
156
- </div>
157
- <div class="topr">
158
- <div class="lg">
159
- <button class="lb on" onclick="setLang('en',this)">EN</button>
160
- <button class="lb" onclick="setLang('fr',this)">FR</button>
161
- <button class="lb" onclick="setLang('wo',this)">WO</button>
162
- </div>
163
- <button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button>
164
- </div>
165
- </header>
166
-
167
- <main>
168
- <!-- Colonne gauche -->
169
- <div style="display:flex;flex-direction:column;gap:11px;">
170
-
171
- <!-- Input source -->
172
- <div class="card">
173
- <div class="ct"><i class="fas fa-video"></i> Input source</div>
174
- <div class="stabs">
175
- <button class="stab on" onclick="switchSrc('upload',this)"><i class="fas fa-upload"></i> Video</button>
176
- <button class="stab" onclick="switchSrc('url',this)"><i class="fas fa-link"></i> URL</button>
177
- <button class="stab" onclick="switchSrc('webcam',this)"><i class="fas fa-camera"></i> Webcam</button>
178
- </div>
179
-
180
- <!-- Upload -->
181
- <div id="src-upload">
182
- <label class="uzone" id="uzone">
183
- <i class="fas fa-cloud-upload-alt"></i>
184
- <p>Drop video here or click to browse</p>
185
- <p style="font-size:10px;color:var(--t3);">MP4, AVI, MOV — max 500 MB</p>
186
- <span class="fname" id="fname"></span>
187
- <input type="file" id="fileInput" accept="video/*" onchange="onFile(event)">
188
- </label>
189
- <div class="prog" id="progWrap"><div class="progb" id="progBar"></div></div>
190
- <div class="progt" id="progTxt"></div>
191
- </div>
192
-
193
- <!-- URL -->
194
- <div id="src-url" style="display:none;">
195
- <div class="urow">
196
- <input type="text" class="uin" id="urlIn" placeholder="https://… or rtsp://…">
197
- <button class="bsm" onclick="loadUrl()">Load</button>
198
- </div>
199
- </div>
200
-
201
- <!-- Webcam -->
202
- <div id="src-webcam" style="display:none;">
203
- <!-- Mini preview : flux YOLO annoté -->
204
- <div class="wcbox" id="wcbox">
205
- <img id="wcMiniYolo" alt="YOLO stream" style="display:none;">
206
- <div class="wcph" id="wcPh"><i class="fas fa-camera-slash"></i><span>Click Start</span></div>
207
- <div class="recbdg" id="recBadge"><i class="fas fa-circle"></i> LIVE</div>
208
- </div>
209
- <!-- Bouton stop webcam seulement (plus de bouton Enregistrer) -->
210
- <button class="runbtn" id="wcBtn" onclick="startWc()">
211
- <i class="fas fa-circle" style="color:#f87171;font-size:8px;"></i> Start webcam
212
- </button>
213
- </div>
214
- </div>
215
-
216
- <!-- Detection classes -->
217
- <div class="card">
218
- <div class="ct"><i class="fas fa-tags"></i> Detection classes</div>
219
- <div class="clshd">
220
- <span id="clsCnt">0 selected</span>
221
- <button onclick="toggleAll()">Select all</button>
222
- </div>
223
- <div class="clsg" id="clsGrid"></div>
224
- </div>
225
-
226
- <button class="runbtn" id="runBtn" onclick="runAnalysis()" disabled>
227
- <i class="fas fa-play"></i> Run Analysis
228
- </button>
229
- <button class="rstbtn" onclick="resetAll()">
230
- <i class="fas fa-rotate-right"></i> Reset
231
- </button>
232
- </div>
233
-
234
- <!-- Colonne droite -->
235
- <div class="rc">
236
-
237
- <!-- Big result panel -->
238
- <div class="bp">
239
- <div class="bph">
240
- <div class="bpht"><i class="fas fa-film"></i> Result</div>
241
- <span style="font-size:11px;color:var(--t2);" id="srcLabel"></span>
242
- </div>
243
- <div class="bpb" id="bigBody">
244
- <!-- Vidéo annotée (upload/url) -->
245
- <video id="resultVid" controls></video>
246
- <!-- Flux YOLO webcam (grand) -->
247
- <img id="wcYoloImg" alt="YOLO stream">
248
- <!-- HUD top-left -->
249
- <div class="hud" id="hudEl">
250
- <div class="hudf" id="hudFr">—</div>
251
- <div id="hudCo">—</div>
252
- </div>
253
- <!-- Compteurs top-right -->
254
- <div class="cov" id="cOverlay"></div>
255
- <!-- Banner no-object -->
256
- <div class="noobj" id="noObjBanner">
257
- <i class="fas fa-eye-slash"></i> No object detected
258
- </div>
259
- <!-- Canvas caché pour enregistrement webcam -->
260
- <canvas id="wcCanvas" style="display:none;"></canvas>
261
- <!-- Empty state -->
262
- <div class="es" id="emptyState">
263
- <i class="fas fa-film"></i>
264
- <p>Result will appear here</p>
265
- </div>
266
- </div>
267
- </div>
268
-
269
- <!-- Stats -->
270
- <div class="sr">
271
- <div class="sc"><div class="sl">Frames</div><div class="sv" id="stF">—</div></div>
272
- <div class="sc"><div class="sl">Objects</div><div class="sv" id="stO">—</div></div>
273
- <div class="sc"><div class="sl">FPS</div><div class="sv" id="stFps">—</div></div>
274
- <div class="sc"><div class="sl">Duration</div><div class="sv" id="stD">—</div></div>
275
- </div>
276
-
277
- <!-- Download -->
278
- <div class="dlb">
279
- <span id="dlName">No result yet</span>
280
- <button class="btndl" id="dlBtn" disabled onclick="doDownload()">
281
- <i class="fas fa-download"></i> Download
282
- </button>
283
- </div>
284
-
285
- </div>
286
- </main>
287
- </div>
288
-
289
- <script>
290
- /* ── Theme ── */
291
- function toggleTheme(){
292
- const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark';
293
- d.setAttribute('data-theme',dk?'light':'dark');
294
- ['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';});
295
- try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){}
296
- }
297
- function setLang(l,btn){
298
- document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
299
- try{localStorage.setItem('tiq_lang',l);}catch(e){}
300
- }
301
-
302
- /* ── Classes ── */
303
- const CLASSES=[
304
- {name:'Vehicle',color:'#3b82f6'},{name:'Motorcycle',color:'#10b981'},
305
- {name:'Truck',color:'#f59e0b'},{name:'Bus',color:'#8b5cf6'},
306
- {name:'Human',color:'#ec4899'},{name:'Bicycle',color:'#06b6d4'},
307
- {name:'Traffic light',color:'#ef4444'},{name:'Road sign',color:'#f97316'},
308
- ];
309
- let sel=new Set(['Human']);
310
-
311
- function buildCls(){
312
- const g=document.getElementById('clsGrid'); g.innerHTML='';
313
- CLASSES.forEach(c=>{
314
- const d=document.createElement('div');
315
- d.className='clsi'+(sel.has(c.name)?' on':'');
316
- d.innerHTML=`<span class="cldot" style="background:${c.color}"></span>${c.name}`;
317
- d.onclick=()=>{sel.has(c.name)?sel.delete(c.name):sel.add(c.name);d.classList.toggle('on');upCls();};
318
- g.appendChild(d);
319
- });
320
- upCls();
321
- }
322
- function toggleAll(){sel.size===CLASSES.length?sel.clear():CLASSES.forEach(c=>sel.add(c.name));buildCls();}
323
- function upCls(){document.getElementById('clsCnt').textContent=sel.size+' selected';}
324
- buildCls();
325
-
326
- /* ── State ── */
327
- let curSrc='upload', vidFile=null, vidUrl=null, wcStream=null;
328
- let _jobId=null, _sse=null, _wcPollIv=null, _annotatedUrl=null;
329
-
330
- /* ── Source switch ── */
331
- function switchSrc(s,btn){
332
- curSrc=s;
333
- document.querySelectorAll('.stab').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
334
- ['upload','url','webcam'].forEach(id=>{
335
- const el=document.getElementById('src-'+id);if(el)el.style.display='none';
336
- });
337
- document.getElementById('src-'+s).style.display='block';
338
- checkReady();
339
- }
340
-
341
- /* ── File upload ── */
342
- function onFile(e){
343
- const f=e.target.files[0];if(!f)return;
344
- vidFile=f;
345
- const fn=document.getElementById('fname');fn.textContent=f.name;fn.style.display='block';
346
- document.getElementById('uzone').classList.add('has');
347
- checkReady();
348
- }
349
- function loadUrl(){
350
- const u=document.getElementById('urlIn').value.trim();if(!u)return;
351
- vidUrl=u;checkReady();
352
- }
353
-
354
- /* ── Run Analysis ── */
355
- window.runAnalysis=async function(){
356
- const btn=document.getElementById('runBtn');
357
- btn.disabled=true;
358
- btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Processing…';
359
- document.getElementById('progWrap').style.display='block';
360
- document.getElementById('progTxt').style.display='block';
361
- document.getElementById('progTxt').textContent='Uploading…';
362
- document.getElementById('progBar').style.width='5%';
363
- document.getElementById('emptyState').style.display='none';
364
-
365
- /* Upload si pas encore fait */
366
- if(!_jobId){
367
- if(!vidFile&&!vidUrl){
368
- btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';return;
369
- }
370
- const fd=new FormData();
371
- if(vidFile) fd.append('video',vidFile);
372
- try{
373
- const r=await fetch('/api/upload',{method:'POST',body:fd});
374
- const d=await r.json();
375
- _jobId=d.job_id;
376
- }catch(err){
377
- console.error('[TrackIQ] Upload:',err);
378
- btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';return;
379
- }
380
- }
381
-
382
- document.getElementById('progTxt').textContent='Starting detection…';
383
- document.getElementById('progBar').style.width='10%';
384
-
385
- try{
386
- await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},
387
- body:JSON.stringify({job_id:_jobId,classes:[...sel],model:'yolo11n'})});
388
- }catch(err){console.error('[TrackIQ] Run:',err);return;}
389
-
390
- /* SSE */
391
- if(_sse)_sse.close();
392
- _sse=new EventSource(`/api/stream/${_jobId}`);
393
- _sse.onmessage=(e)=>{
394
- let d;try{d=JSON.parse(e.data);}catch{return;}
395
-
396
- if(d.event==='done'){
397
- _sse.close();
398
- _showBanner(false);
399
- document.getElementById('progBar').style.width='100%';
400
- document.getElementById('progTxt').textContent='Done!';
401
- btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';
402
-
403
- /* ✅ Affiche la vidéo annotée */
404
- const rv=document.getElementById('resultVid');
405
- rv.src=`/api/video/${_jobId}`;
406
- rv.style.display='block';
407
- rv.load();
408
- document.getElementById('srcLabel').textContent=vidFile?vidFile.name:'video';
409
- document.getElementById('hudEl').style.display='block';
410
- document.getElementById('cOverlay').style.display='flex';
411
- _annotatedUrl=`/api/video/${_jobId}`;
412
-
413
- /* Stats */
414
- const s=d.stats||{};
415
- _set('stF',s.total_frames??'—');_set('stO',s.total_unique??'—');
416
- _set('stFps',s.fps?s.fps.toFixed(1):'—');_set('stD',s.duration_s?s.duration_s+'s':'—');
417
- _showCounts(s.unique_counts||{});
418
- _set('dlName',vidFile?vidFile.name:'annotated.mp4');
419
- document.getElementById('dlBtn').removeAttribute('disabled');
420
-
421
- /* Sauvegarde localStorage */
422
- try{
423
- localStorage.setItem('tiq_job_id',_jobId);
424
- localStorage.setItem('tiq_video_url',`/api/video/${_jobId}`);
425
- localStorage.setItem('tiq_video_name',s.video_name||'');
426
- localStorage.setItem('tiq_stats',JSON.stringify(s));
427
- }catch(ex){}
428
- return;
429
- }
430
-
431
- /* Progression */
432
- if(d.pct!=null){
433
- document.getElementById('progBar').style.width=d.pct+'%';
434
- document.getElementById('progTxt').textContent=`Processing… ${d.pct.toFixed(0)}%`;
435
- }
436
- _showBanner(d.no_objects===true);
437
- if(d.hud){
438
- _set('hudFr',d.hud.frame_str);
439
- const hco=document.getElementById('hudCo');
440
- if(hco) hco.innerHTML=Object.entries(d.hud.counts||{})
441
- .map(([k,v])=>`${k} = ${v}`).join('<br>')||'No object';
442
- }
443
- if(d.unique_counts) _showCounts(d.unique_counts);
444
- };
445
- _sse.onerror=()=>_sse.close();
446
- };
447
-
448
- /* ── Webcam ── */
449
- let _wcFrameIv=null, _wcHiddenVideo=null, _wcCanvas2=null;
450
-
451
- async function startWc(){
452
- try{
453
- wcStream=await navigator.mediaDevices.getUserMedia({video:{width:640,height:480},audio:false});
454
- }catch(e){alert('Webcam unavailable: '+e.message);return;}
455
-
456
- document.getElementById('wcPh').style.display='none';
457
- document.getElementById('recBadge').style.display='flex';
458
- document.getElementById('runBtn').style.display='none';
459
-
460
- const btn=document.getElementById('wcBtn');
461
- btn.innerHTML='<i class="fas fa-stop" style="color:#f87171;font-size:9px;"></i> Stop webcam';
462
- btn.onclick=stopWc;
463
-
464
- /* Lance YOLO backend */
465
- await fetch('/api/webcam/start',{method:'POST',headers:{'Content-Type':'application/json'},
466
- body:JSON.stringify({classes:[...sel],model:'yolo11n'})}).catch(()=>{});
467
-
468
- /* Setup hidden video + canvas pour capturer frames */
469
- _wcHiddenVideo=document.createElement('video');
470
- _wcHiddenVideo.srcObject=wcStream;
471
- _wcHiddenVideo.setAttribute('playsinline','');
472
- _wcHiddenVideo.muted=true;
473
- _wcHiddenVideo.play();
474
- _wcCanvas2=document.createElement('canvas');
475
- _wcCanvas2.width=640;_wcCanvas2.height=480;
476
- const ctx2=_wcCanvas2.getContext('2d');
477
-
478
- /* Envoie une frame toutes les 200ms → reçoit JPEG annoté → l'affiche */
479
- const miniImg=document.getElementById('wcMiniYolo');
480
- const bigImg=document.getElementById('wcYoloImg');
481
- miniImg.style.display='block';
482
- bigImg.style.display='block';
483
-
484
- _wcFrameIv=setInterval(async()=>{
485
- if(!wcStream||!_wcHiddenVideo)return;
486
- ctx2.drawImage(_wcHiddenVideo,0,0,640,480);
487
- _wcCanvas2.toBlob(async(blob)=>{
488
- if(!blob)return;
489
- try{
490
- const resp=await fetch('/api/webcam/frame',{method:'POST',
491
- headers:{'Content-Type':'image/jpeg'},body:blob});
492
- if(!resp.ok)return;
493
- const url=URL.createObjectURL(await resp.blob());
494
- miniImg.src=url;bigImg.src=url;
495
- }catch(e){}
496
- },'image/jpeg',0.75);
497
- },200);
498
-
499
- /* Grand panel droite */
500
- document.getElementById('emptyState').style.display='none';
501
- document.getElementById('resultVid').style.display='none';
502
- document.getElementById('hudEl').style.display='block';
503
- document.getElementById('cOverlay').style.display='flex';
504
- document.getElementById('srcLabel').textContent='Webcam — LIVE';
505
-
506
- /* Démarre l'enregistrement automatiquement */
507
- setTimeout(_startRec, 800);
508
- /* Polling stats toutes les 800ms */
509
- clearInterval(_wcPollIv);
510
- _wcPollIv=setInterval(_pollWc,800);
511
- checkReady();
512
- }
513
-
514
- /* ── Recording webcam → grand Result ── */
515
- let _recIv=null, _recChunks=[], _mediaRec=null;
516
-
517
- function _startRec(){
518
- const canvas=document.getElementById('wcCanvas');
519
- const img=document.getElementById('wcYoloImg');
520
- if(!canvas||!img)return;
521
- canvas.width=640;canvas.height=480;
522
- const ctx=canvas.getContext('2d');
523
- _recIv=setInterval(()=>{
524
- if(img.naturalWidth>0){canvas.width=img.naturalWidth;canvas.height=img.naturalHeight;ctx.drawImage(img,0,0);}
525
- },40);
526
- const mime=MediaRecorder.isTypeSupported('video/webm;codecs=vp9')?'video/webm;codecs=vp9':'video/webm';
527
- _mediaRec=new MediaRecorder(canvas.captureStream(25),{mimeType:mime,videoBitsPerSecond:2000000});
528
- _recChunks=[];
529
- _mediaRec.ondataavailable=e=>{if(e.data.size>0)_recChunks.push(e.data);};
530
- _mediaRec.onstop=_saveRec;
531
- _mediaRec.start(500);
532
- }
533
-
534
- function _saveRec(){
535
- clearInterval(_recIv);_recIv=null;
536
- if(!_recChunks.length)return;
537
- const blob=new Blob(_recChunks,{type:'video/webm'});
538
- const url=URL.createObjectURL(blob);
539
- _annotatedUrl=url;
540
- /* Affiche dans le grand Result */
541
- const rv=document.getElementById('resultVid');
542
- rv.src=url;rv.style.display='block';rv.controls=true;
543
- rv.onloadeddata=()=>rv.play().catch(()=>{});rv.load();
544
- document.getElementById('srcLabel').textContent='Webcam — recorded';
545
- document.getElementById('emptyState').style.display='none';
546
- document.getElementById('hudEl').style.display='block';
547
- /* Active download */
548
- _set('dlName','webcam_trackiq.webm');
549
- document.getElementById('dlBtn').removeAttribute('disabled');
550
- document.getElementById('dlBtn').onclick=()=>{
551
- const a=document.createElement('a');a.href=url;a.download='webcam_trackiq.webm';a.click();
552
- };
553
- }
554
-
555
- function stopWc(){
556
- /* 1. Arrête enregistrement → déclenche _saveRec → vidéo dans Result */
557
- if(_mediaRec&&_mediaRec.state==='recording') _mediaRec.stop();
558
- clearInterval(_wcFrameIv);_wcFrameIv=null;
559
- if(_wcHiddenVideo){_wcHiddenVideo.srcObject=null;_wcHiddenVideo=null;}
560
- _wcCanvas2=null;
561
- if(wcStream){wcStream.getTracks().forEach(t=>t.stop());wcStream=null;}
562
- fetch('/api/webcam/stop',{method:'POST'}).catch(()=>{});
563
- clearInterval(_wcPollIv);_wcPollIv=null;
564
-
565
- /* 2. Cache flux live */
566
- const mi=document.getElementById('wcMiniYolo');mi.src='';mi.style.display='none';
567
- const bi=document.getElementById('wcYoloImg');bi.src='';bi.style.display='none';
568
-
569
- document.getElementById('wcPh').style.display='flex';
570
- document.getElementById('recBadge').style.display='none';
571
- document.getElementById('runBtn').style.display='flex';
572
- document.getElementById('cOverlay').style.display='none';
573
- document.getElementById('noObjBanner').style.display='none';
574
-
575
- const btn=document.getElementById('wcBtn');
576
- btn.innerHTML='<i class="fas fa-circle" style="color:#f87171;font-size:8px;"></i> Start webcam';
577
- btn.onclick=startWc;
578
- checkReady();
579
- }
580
-
581
- async function _pollWc(){
582
- try{
583
- const r=await fetch('/api/webcam/stats');
584
- if(!r.ok)return;
585
- const d=await r.json();
586
- const fc=d.frame_counts||{};
587
- const uc=d.unique_counts||{};
588
- _set('hudFr',`F:${d.frame||0} ▶ LIVE`);
589
- const hco=document.getElementById('hudCo');
590
- if(hco) hco.innerHTML=Object.entries(fc).map(([k,v])=>`${k} = ${v}`).join('<br>')||
591
- '<span style="color:#ef4444">No object</span>';
592
- _showCountsWc(fc,uc);
593
- _showBanner(!Object.keys(fc).length);
594
- }catch(e){}
595
- }
596
-
597
- /* ── Affichage compteurs ── */
598
- function _showCounts(uc){
599
- const ov=document.getElementById('cOverlay');
600
- ov.innerHTML='';
601
- if(!Object.keys(uc).length){
602
- _showBanner(true);return;
603
- }
604
- Object.entries(uc).forEach(([cls,cnt])=>{
605
- const cl=CLASSES.find(c=>c.name===cls);
606
- const color=cl?cl.color:'#a78bfa';
607
- const div=document.createElement('div');div.className='cbdg';
608
- div.style.borderLeftColor=color;
609
- div.innerHTML=`<span class="cn" style="color:${color}">${cls} = ${cnt}</span>`;
610
- ov.appendChild(div);
611
- });
612
- _showBanner(false);
613
- }
614
-
615
- function _showCountsWc(fc,uc){
616
- const ov=document.getElementById('cOverlay');
617
- ov.innerHTML='';
618
- const all=new Set([...Object.keys(fc),...Object.keys(uc)]);
619
- if(!all.size){_showBanner(true);return;}
620
- all.forEach(cls=>{
621
- const cl=CLASSES.find(c=>c.name===cls);
622
- const color=cl?cl.color:'#a78bfa';
623
- const div=document.createElement('div');div.className='cbdg';
624
- div.style.borderLeftColor=color;
625
- div.innerHTML=`<span class="cn" style="color:${color}">${cls} = ${fc[cls]||0}</span>
626
- <span class="cu">Total unique : ${uc[cls]||0}</span>`;
627
- ov.appendChild(div);
628
- });
629
- _showBanner(false);
630
- }
631
-
632
- function _showBanner(show){
633
- const b=document.getElementById('noObjBanner');
634
- b.style.display=show?'flex':'none';
635
- }
636
-
637
- /* ── Helpers ── */
638
- function _set(id,val){const el=document.getElementById(id);if(el)el.textContent=val;}
639
- function checkReady(){
640
- const ok=(curSrc==='upload'&&vidFile)||(curSrc==='url'&&vidUrl)||(curSrc==='webcam'&&wcStream);
641
- document.getElementById('runBtn').disabled=!ok;
642
- }
643
- function doDownload(){
644
- const url=_annotatedUrl;if(!url)return;
645
- const a=document.createElement('a');a.href=url;a.download='trackiq_annotated.mp4';a.click();
646
- }
647
- function resetAll(){
648
- if(wcStream)stopWc();
649
- if(_sse)_sse.close();
650
- _jobId=null;_annotatedUrl=null;vidFile=null;vidUrl=null;
651
- const rv=document.getElementById('resultVid');rv.src='';rv.style.display='none';
652
- document.getElementById('fname').style.display='none';
653
- document.getElementById('fname').textContent='';
654
- document.getElementById('uzone').classList.remove('has');
655
- document.getElementById('urlIn').value='';
656
- document.getElementById('progWrap').style.display='none';
657
- document.getElementById('progBar').style.width='0%';
658
- document.getElementById('progTxt').style.display='none';
659
- document.getElementById('emptyState').style.display='flex';
660
- document.getElementById('cOverlay').style.display='none';
661
- document.getElementById('hudEl').style.display='none';
662
- document.getElementById('noObjBanner').style.display='none';
663
- document.getElementById('dlBtn').disabled=true;
664
- document.getElementById('dlName').textContent='No result yet';
665
- document.getElementById('srcLabel').textContent='';
666
- ['stF','stO','stFps','stD'].forEach(id=>_set(id,'—'));
667
- const fi=document.getElementById('fileInput');if(fi)fi.value='';
668
- checkReady();
669
- }
670
-
671
- /* ── Init ── */
672
- (function(){
673
- try{
674
- const th=localStorage.getItem('tiq_theme');
675
- if(th){document.documentElement.setAttribute('data-theme',th);
676
- if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});}}
677
- const lg=localStorage.getItem('tiq_lang');
678
- if(lg&&lg!=='en'){const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]');if(btn)setLang(lg,btn);}
679
- }catch(e){}
680
- })();
681
- </script>
682
- <script src="/static/trackiq-api.js"></script>
683
- </body>
684
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/templates/index.html DELETED
@@ -1,486 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="light">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TrackIQ — Urban Traffic Detection</title>
7
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300&family=DM+Mono:wght@400&display=swap" rel="stylesheet">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
9
- <style>
10
- :root {
11
- --ac: #880000;
12
- --ac2: #660000;
13
- --bg: #f9fafb;
14
- --s: #fff;
15
- --bd: rgba(0,0,0,0.07);
16
- --t: #111827;
17
- --t2: #6b7280;
18
- --t3: #d1d5db;
19
- --sh: 0 4px 24px rgba(0,0,0,0.09);
20
- --shlg: 0 20px 25px -5px rgba(0,0,0,0.10), 0 10px 10px -5px rgba(0,0,0,0.04);
21
- --r: 16px;
22
- }
23
- [data-theme="dark"] {
24
- --bg: #0c1117;
25
- --s: #151c25;
26
- --t: #e8edf2;
27
- --t2: #7a8fa0;
28
- --t3: #3a4a5a;
29
- --bd: rgba(255,255,255,0.07);
30
- --sh: 0 4px 24px rgba(0,0,0,0.5);
31
- --shlg: 0 20px 40px rgba(0,0,0,0.5);
32
- }
33
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
34
- html, body { min-height: 100%; }
35
- body {
36
- font-family: "DM Sans", sans-serif;
37
- background: var(--bg);
38
- color: var(--t);
39
- transition: background 0.3s, color 0.3s;
40
- }
41
- ::-webkit-scrollbar { width: 6px; }
42
- ::-webkit-scrollbar-track { background: transparent; }
43
- ::-webkit-scrollbar-thumb { background: var(--t3); border-radius: 3px; }
44
-
45
- /* ── FLOATING NAV ── */
46
- .floatnav {
47
- position: absolute;
48
- top: 0; left: 0; right: 0;
49
- z-index: 50;
50
- pointer-events: none;
51
- }
52
- .floatnav-inner {
53
- max-width: 1280px;
54
- margin: 0 auto;
55
- padding: 16px 24px;
56
- display: flex;
57
- justify-content: space-between;
58
- align-items: flex-start;
59
- }
60
- .nav-brand {
61
- pointer-events: auto;
62
- background: rgba(255,255,255,0.92);
63
- backdrop-filter: blur(12px);
64
- border-radius: 10px;
65
- box-shadow: 0 4px 16px rgba(0,0,0,0.12);
66
- padding: 8px 16px;
67
- display: flex;
68
- align-items: center;
69
- gap: 8px;
70
- text-decoration: none;
71
- transition: all 0.2s;
72
- }
73
- .nav-brand:hover { background: #fff; box-shadow: 0 8px 24px rgba(0,0,0,0.16); }
74
- .nav-brand-icon {
75
- width: 28px; height: 28px;
76
- background: var(--ac);
77
- border-radius: 7px;
78
- display: flex; align-items: center; justify-content: center;
79
- color: #fff; font-size: 12px;
80
- }
81
- .nav-brand-name {
82
- font-size: 14px; font-weight: 600;
83
- color: #111827; letter-spacing: -0.3px;
84
- }
85
- .nav-brand-name b { color: var(--ac); }
86
- .nav-right {
87
- pointer-events: auto;
88
- display: flex; gap: 8px; align-items: center;
89
- }
90
- .nav-lang {
91
- background: #fff;
92
- border-radius: 10px;
93
- box-shadow: 0 4px 16px rgba(0,0,0,0.12);
94
- padding: 4px;
95
- display: flex; gap: 4px;
96
- }
97
- .lb {
98
- font-size: 12px; font-weight: 500;
99
- padding: 4px 10px;
100
- border: none; background: none;
101
- cursor: pointer; border-radius: 6px;
102
- color: #6b7280;
103
- font-family: "DM Sans", sans-serif;
104
- transition: all 0.2s;
105
- }
106
- .lb.on { background: var(--ac); color: #fff; }
107
- .lb:not(.on):hover { background: #f3f4f6; color: #111827; }
108
- .nav-theme {
109
- background: #fff;
110
- border-radius: 10px;
111
- box-shadow: 0 4px 16px rgba(0,0,0,0.12);
112
- width: 38px; height: 38px;
113
- border: none; cursor: pointer;
114
- display: flex; align-items: center; justify-content: center;
115
- color: #6b7280; font-size: 13px;
116
- transition: all 0.2s;
117
- }
118
- .nav-theme:hover { color: var(--ac); }
119
-
120
- /* ── HERO ── */
121
- .hero {
122
- background-color: var(--ac);
123
- background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23991111' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
124
- color: #fff;
125
- padding-top: 90px;
126
- padding-bottom: 90px;
127
- padding-left: 24px;
128
- padding-right: 24px;
129
- text-align: center;
130
- position: relative;
131
- overflow: hidden;
132
- }
133
- .hero-inner {
134
- max-width: 860px;
135
- margin: 0 auto;
136
- position: relative;
137
- z-index: 10;
138
- animation: fadeUp 0.6s ease both;
139
- }
140
- @keyframes fadeUp {
141
- from { opacity: 0; transform: translateY(24px); }
142
- to { opacity: 1; transform: translateY(0); }
143
- }
144
- .hero-logo-wrap {
145
- display: flex;
146
- justify-content: center;
147
- margin-bottom: 16px;
148
- }
149
- .hero-logo-box {
150
- background: #fff;
151
- padding: 14px;
152
- border-radius: 20px;
153
- box-shadow: 0 8px 32px rgba(0,0,0,0.2);
154
- display: flex; align-items: center; justify-content: center;
155
- width: 80px; height: 80px;
156
- }
157
- .hero-logo-box i {
158
- font-size: 36px;
159
- color: var(--ac);
160
- }
161
- .hero h1 {
162
- font-size: clamp(32px, 5vw, 60px);
163
- font-weight: 800;
164
- letter-spacing: -2px;
165
- line-height: 1.1;
166
- margin-bottom: 14px;
167
- }
168
- .hero h1 .dim { opacity: 0.75; font-weight: 300; font-style: italic; }
169
- .hero-sub {
170
- font-size: clamp(14px, 1.8vw, 18px);
171
- color: rgba(255,255,255,0.88);
172
- font-weight: 400;
173
- line-height: 1.65;
174
- margin-bottom: 24px;
175
- max-width: 560px;
176
- margin-left: auto;
177
- margin-right: auto;
178
- }
179
- .hero-cta {
180
- display: inline-flex; align-items: center; gap: 10px;
181
- background: #fff;
182
- color: var(--ac);
183
- font-weight: 700; font-size: 16px;
184
- padding: 14px 32px;
185
- border-radius: 50px;
186
- text-decoration: none;
187
- box-shadow: 0 8px 24px rgba(0,0,0,0.18);
188
- transition: all 0.3s;
189
- }
190
- .hero-cta:hover {
191
- background: #f3f4f6;
192
- transform: scale(1.05);
193
- box-shadow: 0 16px 40px rgba(0,0,0,0.22);
194
- }
195
- /* Wave bottom */
196
- .hero-wave {
197
- position: absolute;
198
- bottom: 0; left: 0; right: 0;
199
- pointer-events: none;
200
- }
201
-
202
- /* ── CARDS GRID ── */
203
- .cards-section {
204
- max-width: 1280px;
205
- margin: 0 auto;
206
- padding: 0 24px 64px;
207
- margin-top: -64px;
208
- position: relative;
209
- z-index: 20;
210
- }
211
- .cards-grid {
212
- display: grid;
213
- grid-template-columns: repeat(1, 1fr);
214
- gap: 24px;
215
- }
216
- @media (min-width: 640px) { .cards-grid { grid-template-columns: repeat(2, 1fr); } }
217
- @media (min-width: 1024px) { .cards-grid { grid-template-columns: repeat(4, 1fr); } }
218
-
219
- .card {
220
- background: var(--s);
221
- border-radius: 20px;
222
- padding: 32px 24px;
223
- box-shadow: var(--shlg);
224
- border: 1px solid rgba(0,0,0,0.05);
225
- text-decoration: none;
226
- display: block;
227
- transition: all 0.3s ease;
228
- animation: fadeUp 0.6s ease both;
229
- }
230
- .card:nth-child(1) { animation-delay: 0.05s; }
231
- .card:nth-child(2) { animation-delay: 0.12s; }
232
- .card:nth-child(3) { animation-delay: 0.18s; }
233
- .card:nth-child(4) { animation-delay: 0.24s; }
234
- .card:hover {
235
- transform: translateY(-8px);
236
- box-shadow: 0 20px 25px -5px rgba(0,0,0,0.13), 0 10px 10px -5px rgba(0,0,0,0.05);
237
- border-color: var(--ac);
238
- }
239
- .card-icon-wrap {
240
- width: 80px; height: 80px;
241
- background: #fff1f1;
242
- color: var(--ac);
243
- border-radius: 50%;
244
- display: flex; align-items: center; justify-content: center;
245
- font-size: 28px;
246
- margin: 0 auto 24px;
247
- transition: all 0.3s;
248
- box-shadow: 0 2px 8px rgba(136,0,0,0.08);
249
- }
250
- [data-theme="dark"] .card-icon-wrap {
251
- background: rgba(136,0,0,0.18);
252
- }
253
- .card:hover .card-icon-wrap {
254
- background: var(--ac);
255
- color: #fff;
256
- }
257
- .card-title {
258
- text-align: center;
259
- font-size: 18px; font-weight: 700;
260
- color: var(--t);
261
- margin-bottom: 10px;
262
- transition: color 0.2s;
263
- }
264
- .card:hover .card-title { color: var(--ac); }
265
- .card-desc {
266
- text-align: center;
267
- font-size: 13px;
268
- color: var(--t2);
269
- line-height: 1.65;
270
- }
271
- .card-launch {
272
- margin-top: 24px;
273
- display: flex; justify-content: center;
274
- }
275
- .card-launch span {
276
- font-size: 13px; font-weight: 600;
277
- color: var(--ac);
278
- display: flex; align-items: center; gap: 6px;
279
- opacity: 0;
280
- transition: opacity 0.2s;
281
- }
282
- .card:hover .card-launch span { opacity: 1; }
283
-
284
- /* ── FOOTER ── */
285
- footer {
286
- background: var(--s);
287
- border-top: 1px solid var(--bd);
288
- padding: 28px 24px;
289
- text-align: center;
290
- transition: background 0.3s;
291
- }
292
- footer p {
293
- font-size: 13px;
294
- color: var(--t2);
295
- line-height: 1.7;
296
- }
297
- footer p span {
298
- font-size: 11px;
299
- opacity: 0.6;
300
- display: block;
301
- margin-top: 4px;
302
- }
303
- </style>
304
- </head>
305
- <body>
306
-
307
- <!-- FLOATING NAV -->
308
- <div class="floatnav">
309
- <div class="floatnav-inner">
310
- <a href="#" class="nav-brand">
311
- <div class="nav-brand-icon"><i class="fas fa-satellite-dish"></i></div>
312
- <span class="nav-brand-name">Track<b>IQ</b></span>
313
- </a>
314
- <div class="nav-right">
315
- <div class="nav-lang">
316
- <button class="lb on" onclick="setLang('en',this)">EN</button>
317
- <button class="lb" onclick="setLang('fr',this)">FR</button>
318
- <button class="lb" onclick="setLang('wo',this)">WO</button>
319
- </div>
320
- <button class="nav-theme" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i></button>
321
- </div>
322
- </div>
323
- </div>
324
-
325
- <!-- HERO -->
326
- <div class="hero">
327
- <div class="hero-inner">
328
- <div class="hero-logo-wrap">
329
- <div class="hero-logo-box">
330
- <i class="fas fa-satellite-dish"></i>
331
- </div>
332
- </div>
333
- <h1 id="t-h1">
334
- TrackIQ Urban Traffic<br>
335
- <span class="dim">Surveillance &amp; Detection</span>
336
- </h1>
337
- <p class="hero-sub" id="t-sub">
338
- Based on Computer Vision — real-time vehicle detection and classification, speed estimation, congestion analysis and traffic sign identification.
339
- </p>
340
- <a href="/home" class="hero-cta" id="t-launch">
341
- <i class="fas fa-play"></i>
342
- <span>Launch</span>
343
- </a>
344
- </div>
345
- <div class="hero-wave">
346
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 120" style="display:block;width:100%;">
347
- <path fill="var(--bg)" fill-opacity="1"
348
- d="M0,64L80,69.3C160,75,320,85,480,80C640,75,800,53,960,48C1120,43,1280,53,1360,58.7L1440,64L1440,120L1360,120C1280,120,1120,120,960,120C800,120,640,120,480,120C320,120,160,120,80,120L0,120Z"/>
349
- </svg>
350
- </div>
351
- </div>
352
-
353
- <!-- CARDS -->
354
- <div class="cards-section">
355
- <div class="cards-grid">
356
-
357
- <!-- Card 1 -->
358
- <a href="/home" class="card">
359
- <div class="card-icon-wrap"><i class="fas fa-car-side"></i></div>
360
- <div class="card-title" id="t-c1t">Vehicle Detection</div>
361
- <p class="card-desc" id="t-c1d">Real-time detection and classification of vehicles: cars, trucks, motorcycles, buses and more.</p>
362
- <div class="card-launch"><span id="t-launch2">Launch <i class="fas fa-arrow-right"></i></span></div>
363
- </a>
364
-
365
- <!-- Card 2 -->
366
- <a href="/home" class="card">
367
- <div class="card-icon-wrap"><i class="fas fa-tachometer-alt"></i></div>
368
- <div class="card-title" id="t-c2t">Speed Estimation</div>
369
- <p class="card-desc" id="t-c2d">Automatic estimation of vehicle speed from video stream without additional sensors.</p>
370
- <div class="card-launch"><span id="t-launch3">Launch <i class="fas fa-arrow-right"></i></span></div>
371
- </a>
372
-
373
- <!-- Card 3 -->
374
- <a href="/home" class="card">
375
- <div class="card-icon-wrap"><i class="fas fa-traffic-light"></i></div>
376
- <div class="card-title" id="t-c3t">Signs &amp; Lights</div>
377
- <p class="card-desc" id="t-c3d">Identification of traffic signs and lights, red light detection, stop signs and more.</p>
378
- <div class="card-launch"><span id="t-launch4">Launch <i class="fas fa-arrow-right"></i></span></div>
379
- </a>
380
-
381
- <!-- Card 4 -->
382
- <a href="/home" class="card">
383
- <div class="card-icon-wrap"><i class="fas fa-chart-bar"></i></div>
384
- <div class="card-title" id="t-c4t">Flow &amp; Congestion</div>
385
- <p class="card-desc" id="t-c4d">Congestion analysis, flow measurement and annotated video export for reporting.</p>
386
- <div class="card-launch"><span id="t-launch5">Launch <i class="fas fa-arrow-right"></i></span></div>
387
- </a>
388
-
389
- </div>
390
- </div>
391
-
392
- <!-- FOOTER -->
393
- <footer>
394
- <p>
395
- © 2026 <strong>TrackIQ</strong> — Computer Vision Project. All rights reserved.
396
- <span id="t-foot">Academic Project — Version 1.0</span>
397
- </p>
398
- </footer>
399
-
400
- <script>
401
- const T = {
402
- en: {
403
- h1: 'TrackIQ Urban Traffic<br><span class="dim">Surveillance &amp; Detection</span>',
404
- sub: "Based on Computer Vision — real-time vehicle detection and classification, speed estimation, congestion analysis and traffic sign identification.",
405
- launch: '<i class="fas fa-play"></i><span>Launch</span>',
406
- c1t: "Vehicle Detection", c1d: "Real-time detection and classification of vehicles: cars, trucks, motorcycles, buses and more.",
407
- c2t: "Speed Estimation", c2d: "Automatic estimation of vehicle speed from video stream without additional sensors.",
408
- c3t: "Signs & Lights", c3d: "Identification of traffic signs and lights, red light detection, stop signs and more.",
409
- c4t: "Flow & Congestion", c4d: "Congestion analysis, flow measurement and annotated video export for reporting.",
410
- launchbtn: 'Launch <i class="fas fa-arrow-right"></i>',
411
- foot: "Academic Project — Version 1.0"
412
- },
413
- fr: {
414
- h1: 'TrackIQ Trafic Urbain<br><span class="dim">Surveillance &amp; Détection</span>',
415
- sub: "Basé sur Computer Vision — détection et classification en temps réel des véhicules, estimation de vitesse, analyse de congestion et identification des feux et panneaux.",
416
- launch: '<i class="fas fa-play"></i><span>Lancer</span>',
417
- c1t: "Détection Véhicules", c1d: "Détection et classification en temps réel des véhicules : voitures, camions, motos, bus et plus.",
418
- c2t: "Estimation Vitesse", c2d: "Estimation automatique de la vitesse des véhicules depuis un flux vidéo, sans capteur additionnel.",
419
- c3t: "Feux &amp; Panneaux", c3d: "Identification des panneaux et feux de signalisation, détection des feux rouges, stops et plus.",
420
- c4t: "Flux &amp; Congestion",c4d: "Analyse de congestion, mesure de flux et export vidéo annoté pour rapport.",
421
- launchbtn: 'Lancer <i class="fas fa-arrow-right"></i>',
422
- foot: "Projet Académique — Version 1.0"
423
- },
424
- wo: {
425
- h1: 'TrackIQ Trafik Kanam<br><span class="dim">Xam ak Réy</span>',
426
- sub: "Moom Computer Vision la tax — léegi-léegi di réy wërsëg yi, ak xam njariñ yoom yi, ci kanam.",
427
- launch: '<i class="fas fa-play"></i><span>Tambali</span>',
428
- c1t: "Réy Wërsëg", c1d: "Réy ak xam njariñ wërsëg yi ci kanam: yëgël, camion, moto, bus ak yeneen.",
429
- c2t: "Yóbbu Yoom", c2d: "Xam yóbbu yoom wërsëg yi ci video, benn jëriñ mujj dafa soxor.",
430
- c3t: "Siñali", c3d: "Xam siñali yi ak dëkkandiku, réy feux rouge, stop ak yeneen.",
431
- c4t: "Rafet &amp; Xonq", c4d: "Xam xonq, jëf rafet ci kanam ak dëkkandiku video bu annotéwoon.",
432
- launchbtn: 'Tambali <i class="fas fa-arrow-right"></i>',
433
- foot: "Liggéey Académique — Version 1.0"
434
- }
435
- };
436
-
437
- function setLang(l, btn) {
438
- document.querySelectorAll(".lb").forEach(b => b.classList.remove("on"));
439
- btn.classList.add("on");
440
- const t = T[l];
441
- if (!t) return;
442
- document.getElementById("t-h1").innerHTML = t.h1;
443
- document.getElementById("t-sub").textContent = t.sub;
444
- document.getElementById("t-launch").innerHTML = t.launch;
445
- ["c1t","c2t","c3t","c4t"].forEach(k => {
446
- const el = document.getElementById("t-"+k);
447
- if (el) el.innerHTML = t[k];
448
- });
449
- ["c1d","c2d","c3d","c4d"].forEach(k => {
450
- const el = document.getElementById("t-"+k);
451
- if (el) el.textContent = t[k];
452
- });
453
- ["t-launch2","t-launch3","t-launch4","t-launch5"].forEach(id => {
454
- const el = document.getElementById(id);
455
- if (el) el.innerHTML = t.launchbtn;
456
- });
457
- document.getElementById("t-foot").textContent = t.foot;
458
- try { localStorage.setItem("tiq_lang", l); } catch(e) {}
459
- }
460
-
461
- function toggleTheme() {
462
- const d = document.documentElement;
463
- const dk = d.getAttribute("data-theme") === "dark";
464
- d.setAttribute("data-theme", dk ? "light" : "dark");
465
- document.getElementById("ti").className = dk ? "fas fa-moon" : "fas fa-sun";
466
- try { localStorage.setItem("tiq_theme", dk ? "light" : "dark"); } catch(e) {}
467
- }
468
-
469
- (function() {
470
- try {
471
- const th = localStorage.getItem("tiq_theme");
472
- if (th) {
473
- document.documentElement.setAttribute("data-theme", th);
474
- if (th === "dark") document.getElementById("ti").className = "fas fa-sun";
475
- }
476
- const lg = localStorage.getItem("tiq_lang");
477
- if (lg && lg !== "en") {
478
- const btn = document.querySelector('.lb[onclick*="\'' + lg + '\'"]');
479
- if (btn) setLang(lg, btn);
480
- }
481
- } catch(e) {}
482
- })();
483
- </script>
484
- <script src="/static/trackiq-api.js"></script>
485
- </body>
486
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
TRAFFIC_ROAD_APP/templates/logs.html DELETED
@@ -1,415 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-theme="light">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TrackIQ Logs</title>
7
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
9
- <style>
10
- :root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
11
- [data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--shadow-lg:0 8px 36px rgba(0,0,0,0.55);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;}
12
- *{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
13
- body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh}
14
- .sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200}
15
- .sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
16
- .sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0}
17
- .sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
18
- .sb-btn:hover,.sb-btn.active{background:var(--accent-light);color:var(--accent)}
19
- .sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none}.sb-btn:hover .tip{opacity:1}
20
- .sb-spacer{flex:1}.page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column}
21
- .topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px}
22
- .topbar-l,.topbar-r{display:flex;align-items:center;gap:10px}
23
- .brand-name{font-size:15px;font-weight:600}.brand-name b{color:var(--accent)}.sep{color:var(--border);font-size:18px;margin:0 2px}
24
- .lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border)}
25
- .lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif}.lang-btn.active{background:var(--accent);color:white}
26
- .icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px}
27
- main{flex:1;padding:16px 18px;max-width:1440px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:12px}
28
- .sum-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
29
- .sc,.card,.tcard{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow)}
30
- .sc{padding:13px 15px}.sc-l{font-size:11px;color:var(--text2);margin-bottom:5px}.sc-v{font-size:22px;font-weight:300}.sc-s{font-size:10px;color:var(--text3);margin-top:3px}
31
- .card{padding:14px 16px}.filter-row{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap}.fg{display:flex;flex-direction:column;gap:4px}.fg label{font-size:11px;color:var(--text2)}
32
- .fin{padding:8px 10px;border:1px solid var(--border);border-radius:7px;font-size:12px;background:var(--surface2);color:var(--text);font-family:'DM Sans',sans-serif;outline:none;min-width:160px}.fin:focus{border-color:var(--accent)}
33
- .f-sp{flex:1}
34
- .btn{display:flex;align-items:center;gap:5px;border:none;border-radius:7px;padding:8px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif}
35
- .btn-exp{background:var(--accent);color:white}.btn-exp:hover{background:var(--accent2)}
36
- .save-opt{display:flex;align-items:center;gap:10px;width:100%;padding:9px 14px;border:none;background:none;cursor:pointer;font-size:12px;color:var(--text);font-family:'DM Sans',sans-serif;text-align:left}.save-opt:hover{background:var(--surface2)}
37
- .btn-clr{background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
38
- .tcard{overflow:hidden}.twrap{overflow:auto}
39
- table{width:100%;border-collapse:collapse}
40
- thead th{padding:10px 12px;text-align:left;font-size:11px;font-weight:500;color:var(--text2);background:var(--surface2);border-bottom:1px solid var(--border);white-space:nowrap}
41
- tbody tr{border-bottom:1px solid var(--border)} tbody tr:last-child{border-bottom:none}
42
- tbody td{padding:10px 12px;font-size:12px;vertical-align:middle}
43
- .mono{font-family:'DM Mono',monospace}
44
- .pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--surface2);border-radius:999px;padding:3px 8px}
45
- .dot{width:8px;height:8px;border-radius:50%}
46
- .empty{padding:38px;text-align:center;color:var(--text3)}
47
- @media(max-width:1100px){.sum-row{grid-template-columns:repeat(2,minmax(0,1fr))}}
48
- @media(max-width:760px){.sum-row{grid-template-columns:1fr}}
49
- </style>
50
- </head>
51
- <body>
52
- <nav class="sidebar">
53
- <a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
54
- <div class="sb-sep"></div>
55
- <a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
56
- <a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
57
- <a href="/logs" class="sb-btn active"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
58
- <a href="/history" class="sb-btn"><i class="fas fa-film"></i><span class="tip">History</span></a>
59
- <div class="sb-spacer"></div>
60
- <button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
61
- </nav>
62
-
63
- <div class="page-wrap">
64
- <header class="topbar">
65
- <div class="topbar-l">
66
- <span class="brand-name">Track<b>IQ</b></span>
67
- <span class="sep">|</span>
68
- <span style="font-size:13px;font-weight:500;" id="t-title">Detection Logs</span>
69
- </div>
70
- <div class="topbar-r">
71
- <div class="lang-group">
72
- <button class="lang-btn active" onclick="setLang('en',this)">EN</button>
73
- <button class="lang-btn" onclick="setLang('fr',this)">FR</button>
74
- <button class="lang-btn" onclick="setLang('wo',this)">WO</button>
75
- </div>
76
- <button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
77
- </div>
78
- </header>
79
-
80
- <main>
81
- <div class="sum-row">
82
- <div class="sc"><div class="sc-l" id="t-s1">Detections</div><div class="sc-v" id="sumDetections">0</div><div class="sc-s" id="sumDetectionsSub">Real rows from generated videos</div></div>
83
- <div class="sc"><div class="sc-l" id="t-s2">Scenes</div><div class="sc-v" id="sumScenes">0</div><div class="sc-s" id="sumScenesSub">Processed analyses</div></div>
84
- <div class="sc"><div class="sc-l" id="t-s3">Classes</div><div class="sc-v" id="sumClasses">0</div><div class="sc-s" id="sumClassesSub">Distinct detected labels</div></div>
85
- <div class="sc"><div class="sc-l" id="t-s4">Latest video</div><div class="sc-v" id="sumLatest">-</div><div class="sc-s" id="sumLatestSub">Most recent analyzed clip</div></div>
86
- </div>
87
-
88
- <div class="card">
89
- <div class="filter-row">
90
- <div class="fg"><label id="t-search">Search video or scene</label><input id="searchIn" class="fin" oninput="applyFilters()"></div>
91
- <div class="fg"><label id="t-scene">Scene</label><select id="sceneF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
92
- <div class="fg"><label id="t-type">Class</label><select id="classF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
93
- <div class="fg"><label id="t-direction">Direction</label><select id="dirF" class="fin" onchange="applyFilters()"><option value="">All</option><option>up</option><option>down</option><option>left</option><option>right</option><option>unknown</option></select></div>
94
- <div class="f-sp"></div>
95
- <button class="btn btn-clr" onclick="clearFilters()" ondblclick="clearAllLogs()" title="Click=clear filters | Double-click=delete all logs"><i class="fas fa-rotate-left"></i> <span id="t-clear">Clear</span></button>
96
- <div style="position:relative">
97
- <button class="btn btn-exp" onclick="toggleSaveMenu(event)"><i class="fas fa-cloud-arrow-up"></i> <span id="t-export">Save Logs</span> <i class="fas fa-chevron-down" style="font-size:9px;margin-left:2px"></i></button>
98
- <div id="saveMenu" style="display:none;position:absolute;right:0;top:calc(100% + 6px);background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);min-width:240px;z-index:999;overflow:hidden">
99
- <div style="padding:10px 14px 6px;font-size:11px;color:var(--text2);font-weight:500;border-bottom:1px solid var(--border)">
100
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
101
- <label style="font-size:11px;color:var(--text2)">Group name:</label>
102
- <input id="groupNameIn" value="trackiq" style="flex:1;padding:4px 7px;border:1px solid var(--border);border-radius:5px;font-size:11px;background:var(--surface2);color:var(--text);outline:none" placeholder="e.g. session1">
103
- </div>
104
- <span style="color:var(--text3);font-size:10px">File: <span id="csvPreview">trackiq_all.csv</span></span>
105
- </div>
106
- <button class="save-opt" onclick="saveLocal()"><i class="fas fa-download" style="color:#3b82f6"></i> Download locally</button>
107
- <button class="save-opt" onclick="showGithubPanel()"><i class="fab fa-github" style="color:#e2e8f0"></i> Push to GitHub</button>
108
- <button class="save-opt" onclick="saveDrive()"><i class="fab fa-google-drive" style="color:#34a853"></i> Save to Google Drive</button>
109
- </div>
110
- </div>
111
- <!-- GitHub panel -->
112
- <div id="githubPanel" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:none;align-items:center;justify-content:center">
113
- <div style="background:var(--surface);border-radius:14px;padding:22px 24px;width:420px;box-shadow:var(--shadow-lg)">
114
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
115
- <span style="font-weight:600;font-size:14px"><i class="fab fa-github"></i> Push to GitHub</span>
116
- <button onclick="closeGithubPanel()" style="background:none;border:none;color:var(--text2);cursor:pointer;font-size:16px"><i class="fas fa-times"></i></button>
117
- </div>
118
- <div style="display:flex;flex-direction:column;gap:9px">
119
- <div class="fg"><label>Personal Access Token (PAT)</label><input id="ghToken" class="fin" type="password" placeholder="ghp_xxxxxxxxxxxx"></div>
120
- <div class="fg"><label>Repository (owner/repo)</label><input id="ghRepo" class="fin" placeholder="username/trackiq-logs"></div>
121
- <div class="fg"><label>Branch</label><input id="ghBranch" class="fin" value="main" placeholder="main"></div>
122
- <div class="fg"><label>Path in repo</label><input id="ghPath" class="fin" placeholder="logs/" value="logs/"></div>
123
- </div>
124
- <div id="ghStatus" style="margin-top:10px;font-size:12px;color:var(--text2);min-height:18px"></div>
125
- <div style="display:flex;gap:8px;margin-top:14px;justify-content:flex-end">
126
- <button class="btn btn-clr" onclick="closeGithubPanel()">Cancel</button>
127
- <button class="btn btn-exp" onclick="pushToGithub()"><i class="fab fa-github"></i> Push CSV</button>
128
- </div>
129
- </div>
130
- </div>
131
- </div>
132
- </div>
133
-
134
- <div class="tcard">
135
- <div class="twrap">
136
- <table>
137
- <thead>
138
- <tr>
139
- <th id="th-video">Video</th>
140
- <th id="th-scene">Scene</th>
141
- <th id="th-time">Time</th>
142
- <th id="th-frame">Frame</th>
143
- <th id="th-track">Track</th>
144
- <th id="th-class">Class</th>
145
- <th id="th-confidence">Confidence</th>
146
- <th id="th-direction">Direction</th>
147
- <th id="th-bbox">Bounding Box</th>
148
- <th id="th-position">Center</th>
149
- </tr>
150
- </thead>
151
- <tbody id="tbody">
152
- <tr><td colspan="9" class="empty">Loading real logs...</td></tr>
153
- </tbody>
154
- </table>
155
- </div>
156
- </div>
157
- </main>
158
- </div>
159
-
160
- <script>
161
- let allRows = [];
162
- let filteredRows = [];
163
-
164
- function toggleTheme(){
165
- const d=document.documentElement;
166
- const dark=d.getAttribute('data-theme')==='dark';
167
- d.setAttribute('data-theme',dark?'light':'dark');
168
- ['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
169
- try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
170
- }
171
-
172
- const TL = {
173
- en:{title:'Detection Logs',s1:'Detections',s2:'Scenes',s3:'Classes',s4:'Latest video',search:'Search video or scene',scene:'Scene',type:'Class',direction:'Direction',clear:'Clear',export:'Save Logs',video:'Video',time:'Time',frame:'Frame',track:'Track',class:'Class',confidence:'Confidence',bbox:'Bounding Box',position:'Center'},
174
- fr:{title:'Logs de détection',s1:'Détections',s2:'Scènes',s3:'Classes',s4:'Dernière vidéo',search:'Recherche vidéo ou scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Effacer',export:'Sauvegarder',video:'Vidéo',time:'Temps',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Boîte englobante',position:'Centre'},
175
- wo:{title:'Logs yi',s1:'Réy',s2:'Scène yi',s3:'Classe yi',s4:'Video mujj',search:'Seet video walla scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Baň',export:'Sauvegarder',video:'Video',time:'Waxtu',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Bounding Box',position:'Centre'}
176
- };
177
-
178
- function setLang(l, btn){
179
- document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));
180
- btn.classList.add('active');
181
- const t = TL[l];
182
- const ids = {title:'t-title',s1:'t-s1',s2:'t-s2',s3:'t-s3',s4:'t-s4',search:'t-search',scene:'t-scene',type:'t-type',direction:'t-direction',clear:'t-clear',export:'t-export',video:'th-video',time:'th-time',frame:'th-frame',track:'th-track',class:'th-class',confidence:'th-confidence',bbox:'th-bbox',position:'th-position'};
183
- Object.entries(ids).forEach(([k,id])=>{const el=document.getElementById(id);if(el)el.textContent=t[k];});
184
- document.getElementById('th-scene').textContent = t.scene;
185
- document.getElementById('th-direction').textContent = t.direction;
186
- try{localStorage.setItem('tiq_lang',l);}catch(e){}
187
- }
188
-
189
- async function loadLogs(){
190
- try{
191
- const [rowsRes, scenesRes] = await Promise.all([fetch('/api/logs/rows'), fetch('/api/logs')]);
192
- allRows = await rowsRes.json();
193
- const scenes = await scenesRes.json();
194
- fillFilters(scenes);
195
- fillSummary(scenes, allRows);
196
- applyFilters();
197
- }catch(err){
198
- const tb=document.getElementById('tbody');
199
- tb.innerHTML = '<tr><td colspan="9" class="empty">Unable to load real logs.</td></tr>';
200
- console.warn('[TrackIQ] real logs:', err);
201
- }
202
- }
203
-
204
- function fillFilters(scenes){
205
- const sceneF = document.getElementById('sceneF');
206
- const classF = document.getElementById('classF');
207
- sceneF.innerHTML = '<option value="">All</option>' + scenes.map(s=>`<option value="${s.scene_id}">${s.video_name || s.scene_id}</option>`).join('');
208
- const classes = [...new Set(allRows.map(r=>r.class_name).filter(Boolean))].sort();
209
- classF.innerHTML = '<option value="">All</option>' + classes.map(c=>`<option value="${c}">${c}</option>`).join('');
210
- }
211
-
212
- function fillSummary(scenes, rows){
213
- document.getElementById('sumDetections').textContent = rows.length.toLocaleString();
214
- document.getElementById('sumScenes').textContent = scenes.length.toLocaleString();
215
- document.getElementById('sumClasses').textContent = new Set(rows.map(r=>r.class_name).filter(Boolean)).size.toLocaleString();
216
- document.getElementById('sumLatest').textContent = scenes[0]?.video_name || '-';
217
- }
218
-
219
- function applyFilters(){
220
- const search = document.getElementById('searchIn').value.toLowerCase();
221
- const scene = document.getElementById('sceneF').value;
222
- const cls = document.getElementById('classF').value;
223
- const dir = document.getElementById('dirF').value;
224
- filteredRows = allRows.filter(r =>
225
- (!search || (r.video_name || '').toLowerCase().includes(search) || (r.scene_id || '').toLowerCase().includes(search)) &&
226
- (!scene || r.scene_id === scene) &&
227
- (!cls || r.class_name === cls) &&
228
- (!dir || r.direction === dir)
229
- );
230
- renderRows();
231
- }
232
-
233
- async function clearAllLogs(){
234
- if(!confirm('Delete all logs? This cannot be undone.')) return;
235
- try{
236
- const scenes = await fetch('/api/logs').then(r=>r.json());
237
- await Promise.all(scenes.map(s=>fetch(`/api/logs/${s.scene_id}`,{method:'DELETE'})));
238
- location.reload();
239
- }catch(e){ alert('Error: '+e.message); }
240
- }
241
-
242
- function clearFilters(){
243
- ['searchIn','sceneF','classF','dirF'].forEach(id=>{document.getElementById(id).value='';});
244
- applyFilters();
245
- }
246
-
247
- function renderRows(){
248
- const tb = document.getElementById('tbody');
249
- if(!filteredRows.length){
250
- tb.innerHTML = '<tr><td colspan="10" class="empty">No real detections found for this filter.</td></tr>';
251
- return;
252
- }
253
- const colors = {Vehicle:'#3b82f6',Motorcycle:'#10b981',Truck:'#f59e0b',Bus:'#8b5cf6',Human:'#ec4899',Bicycle:'#06b6d4'};
254
- tb.innerHTML = '';
255
- filteredRows.forEach(r=>{
256
- const tr = document.createElement('tr');
257
- tr.innerHTML = `
258
- <td title="${r.video_name || ''}">${(r.video_name || '-').slice(0,28)}</td>
259
- <td class="mono">${r.scene_id}</td>
260
- <td class="mono">${Number(r.timestamp_s || 0).toFixed(2)}s</td>
261
- <td class="mono">${r.frame_id}</td>
262
- <td class="mono">#${r.track_id}</td>
263
- <td><span class="pill"><span class="dot" style="background:${colors[r.class_name] || '#888'}"></span>${r.class_name || '-'}</span></td>
264
- <td class="mono">${Math.round((r.confidence || 0) * 100)}%</td>
265
- <td>${r.direction || 'unknown'}</td>
266
- <td class="mono" style="font-size:11px">(${r.x1},${r.y1})→(${r.x2},${r.y2})</td>
267
- <td class="mono">(${r.cx}, ${r.cy})</td>
268
- `;
269
- tb.appendChild(tr);
270
- });
271
- }
272
-
273
- /* ── Helpers CSV ─────────────────────────────────────────────── */
274
- function getGroupName(){ return (document.getElementById('groupNameIn')?.value||'trackiq').trim().replace(/\s+/g,'_')||'trackiq'; }
275
- function getCsvFilename(){
276
- const g=getGroupName();
277
- const scenes=[...new Set(filteredRows.map(r=>r.scene_id))];
278
- return `${g}_${scenes.length===1?scenes[0]:'all'}.csv`;
279
- }
280
- function buildCsvContent(){
281
- const hdr = 'group,scene_id,video_name,timestamp_s,frame_id,track_id,class_name,confidence,x1,y1,x2,y2,cx,cy,direction\n';
282
- const g = getGroupName();
283
- const rows = filteredRows.map(r => [g,r.scene_id,`"${(r.video_name||'').replace(/"/g,'""')}"`,r.timestamp_s,r.frame_id,r.track_id,r.class_name,r.confidence,r.x1,r.y1,r.x2,r.y2,r.cx,r.cy,r.direction].join(',')).join('\n');
284
- return hdr + rows;
285
- }
286
- function updateCsvPreview(){
287
- const el = document.getElementById('csvPreview');
288
- if(el) el.textContent = getCsvFilename();
289
- }
290
-
291
- /* ── Save menu ───────────────────────────────────���───────────── */
292
- function toggleSaveMenu(e){
293
- e.stopPropagation();
294
- const m = document.getElementById('saveMenu');
295
- m.style.display = m.style.display==='none'?'block':'none';
296
- updateCsvPreview();
297
- }
298
- document.addEventListener('click', ()=>{ const m=document.getElementById('saveMenu'); if(m) m.style.display='none'; });
299
-
300
- /* ── 1. Download local ───────────────────────────────────────── */
301
- function saveLocal(){
302
- const a = document.createElement('a');
303
- a.href = URL.createObjectURL(new Blob([buildCsvContent()], {type:'text/csv'}));
304
- a.download = getCsvFilename();
305
- a.click();
306
- document.getElementById('saveMenu').style.display='none';
307
- }
308
-
309
- /* ── 2. GitHub push ──────────────────────────────────────────── */
310
- function showGithubPanel(){
311
- document.getElementById('saveMenu').style.display='none';
312
- const p = document.getElementById('githubPanel');
313
- p.style.display='flex';
314
- updateCsvPreview();
315
- }
316
- function closeGithubPanel(){ document.getElementById('githubPanel').style.display='none'; }
317
-
318
- async function pushToGithub(){
319
- const token = document.getElementById('ghToken').value.trim();
320
- const repo = document.getElementById('ghRepo').value.trim();
321
- const branch = document.getElementById('ghBranch').value.trim() || 'main';
322
- const path = (document.getElementById('ghPath').value.trim().replace(/\/+$/,'')||'logs') + '/' + getCsvFilename();
323
- const status = document.getElementById('ghStatus');
324
-
325
- if(!token||!repo){ status.textContent='⚠ Token and repo are required.'; return; }
326
- status.textContent='Pushing…';
327
-
328
- const content = btoa(unescape(encodeURIComponent(buildCsvContent())));
329
- // Check if file exists (to get SHA for update)
330
- let sha = null;
331
- try{
332
- const check = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{headers:{Authorization:`token ${token}`}});
333
- if(check.ok){ const d=await check.json(); sha=d.sha; }
334
- }catch(e){}
335
-
336
- try{
337
- const body = {message:`TrackIQ logs export — ${getCsvFilename()}`, content, branch};
338
- if(sha) body.sha = sha;
339
- const res = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{
340
- method:'PUT', headers:{Authorization:`token ${token}`,'Content-Type':'application/json'},
341
- body: JSON.stringify(body)
342
- });
343
- if(res.ok){ status.innerHTML='<span style="color:#10b981">✓ Pushed successfully!</span>'; }
344
- else{ const e=await res.json(); status.innerHTML=`<span style="color:#ef4444">Error: ${e.message}</span>`; }
345
- }catch(err){ status.innerHTML=`<span style="color:#ef4444">Network error: ${err.message}</span>`; }
346
- }
347
-
348
- /* ── 3. Google Drive ─────────────────────────────────────────── */
349
- async function saveDrive(){
350
- document.getElementById('saveMenu').style.display='none';
351
- // Uses Google Drive File Picker API via OAuth implicit flow
352
- const CLIENT_ID = ''; // User must fill their own GCP OAuth client ID
353
- if(!CLIENT_ID){
354
- const ok = confirm('To save to Google Drive you need a Google OAuth Client ID.\n\nWould you like instructions on how to set it up?\n\n(For now the file will be downloaded locally as a fallback)');
355
- if(ok){ window.open('https://developers.google.com/drive/api/guides/about-auth','_blank'); }
356
- saveLocal();
357
- return;
358
- }
359
- // Full Drive upload with OAuth token
360
- const scope = 'https://www.googleapis.com/auth/drive.file';
361
- const redirect = location.origin + location.pathname;
362
- const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(redirect)}&response_type=token&scope=${encodeURIComponent(scope)}`;
363
- const popup = window.open(authUrl,'_blank','width=500,height=600');
364
- // Listen for token in URL hash after redirect
365
- const interval = setInterval(()=>{
366
- try{
367
- if(popup.closed){ clearInterval(interval); return; }
368
- const hash = popup.location.hash;
369
- if(hash && hash.includes('access_token')){
370
- clearInterval(interval); popup.close();
371
- const token = new URLSearchParams(hash.replace('#','')).get('access_token');
372
- uploadToDrive(token);
373
- }
374
- }catch(e){}
375
- },500);
376
- }
377
-
378
- async function uploadToDrive(token){
379
- const csv = buildCsvContent();
380
- const fname = getCsvFilename();
381
- const meta = JSON.stringify({name:fname,mimeType:'text/csv'});
382
- const form = new FormData();
383
- form.append('metadata', new Blob([meta],{type:'application/json'}));
384
- form.append('file', new Blob([csv], {type:'text/csv'}));
385
- try{
386
- const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',{
387
- method:'POST', headers:{Authorization:`Bearer ${token}`}, body:form
388
- });
389
- if(res.ok){ alert(`✓ "${fname}" saved to Google Drive!`); }
390
- else{ const e=await res.json(); alert('Drive error: '+e.error?.message); }
391
- }catch(err){ alert('Drive upload failed: '+err.message); }
392
- }
393
-
394
- function exportFiltered(){ saveLocal(); } // legacy alias
395
-
396
- (function(){
397
- try{
398
- const th=localStorage.getItem('tiq_theme');
399
- if(th){
400
- document.documentElement.setAttribute('data-theme',th);
401
- if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}
402
- }
403
- const lg=localStorage.getItem('tiq_lang');
404
- if(lg&&lg!=='en'){
405
- const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);
406
- if(btn)setLang(lg,btn);
407
- }
408
- }catch(e){}
409
- loadLogs();
410
- // Update CSV filename preview when group name changes
411
- document.addEventListener('input', e=>{ if(e.target.id==='groupNameIn') updateCsvPreview(); });
412
- })();
413
- </script>
414
- </body>
415
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -49,7 +49,7 @@ def _load_model_background():
49
  print("⏳ Chargement du modèle YOLO...")
50
  _model = _get_model()
51
  _model_ready = True
52
- print(" Modèle YOLO chargé !")
53
 
54
  def _get_model(key="yolo11n"):
55
  p = MODELS_DIR / f"{key}.pt"
 
49
  print("⏳ Chargement du modèle YOLO...")
50
  _model = _get_model()
51
  _model_ready = True
52
+ print(" Modèle YOLO chargé !")
53
 
54
  def _get_model(key="yolo11n"):
55
  p = MODELS_DIR / f"{key}.pt"
models/yolo11n.pt CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:49da7a898d364366a0925c144b6701980a3bf0525c5b904020a0352228cb2e45
3
- size 22523050
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ebbc80d4a7680d14987a577cd21342b65ecfd94632bd9a8da63ae6417644ee1
3
+ size 5613764
templates/home.html CHANGED
@@ -306,7 +306,8 @@ const CLASSES=[
306
  {name:'Human',color:'#ec4899'},{name:'Bicycle',color:'#06b6d4'},
307
  {name:'Traffic light',color:'#ef4444'},{name:'Road sign',color:'#f97316'},
308
  ];
309
- let sel=new Set(['Human']);
 
310
 
311
  function buildCls(){
312
  const g=document.getElementById('clsGrid'); g.innerHTML='';
@@ -450,8 +451,43 @@ let _wcFrameIv=null, _wcHiddenVideo=null, _wcCanvas2=null;
450
 
451
  async function startWc(){
452
  try{
453
- wcStream=await navigator.mediaDevices.getUserMedia({video:{width:640,height:480},audio:false});
454
- }catch(e){alert('Webcam unavailable: '+e.message);return;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
  document.getElementById('wcPh').style.display='none';
457
  document.getElementById('recBadge').style.display='flex';
@@ -461,16 +497,48 @@ async function startWc(){
461
  btn.innerHTML='<i class="fas fa-stop" style="color:#f87171;font-size:9px;"></i> Stop webcam';
462
  btn.onclick=stopWc;
463
 
 
 
 
 
 
 
 
 
 
464
  /* Lance YOLO backend */
465
- await fetch('/api/webcam/start',{method:'POST',headers:{'Content-Type':'application/json'},
466
- body:JSON.stringify({classes:[...sel],model:'yolo11n'})}).catch(()=>{});
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
  /* Setup hidden video + canvas pour capturer frames */
469
  _wcHiddenVideo=document.createElement('video');
470
  _wcHiddenVideo.srcObject=wcStream;
471
- _wcHiddenVideo.setAttribute('playsinline','');
472
  _wcHiddenVideo.muted=true;
473
- _wcHiddenVideo.play();
 
 
 
 
 
 
 
 
 
 
474
  _wcCanvas2=document.createElement('canvas');
475
  _wcCanvas2.width=640;_wcCanvas2.height=480;
476
  const ctx2=_wcCanvas2.getContext('2d');
@@ -481,19 +549,59 @@ async function startWc(){
481
  miniImg.style.display='block';
482
  bigImg.style.display='block';
483
 
 
484
  _wcFrameIv=setInterval(async()=>{
485
- if(!wcStream||!_wcHiddenVideo)return;
486
- ctx2.drawImage(_wcHiddenVideo,0,0,640,480);
487
- _wcCanvas2.toBlob(async(blob)=>{
488
- if(!blob)return;
489
- try{
490
- const resp=await fetch('/api/webcam/frame',{method:'POST',
491
- headers:{'Content-Type':'image/jpeg'},body:blob});
492
- if(!resp.ok)return;
493
- const url=URL.createObjectURL(await resp.blob());
494
- miniImg.src=url;bigImg.src=url;
495
- }catch(e){}
496
- },'image/jpeg',0.75);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  },200);
498
 
499
  /* Grand panel droite */
@@ -681,4 +789,4 @@ function resetAll(){
681
  </script>
682
  <script src="/static/trackiq-api.js"></script>
683
  </body>
684
- </html>
 
306
  {name:'Human',color:'#ec4899'},{name:'Bicycle',color:'#06b6d4'},
307
  {name:'Traffic light',color:'#ef4444'},{name:'Road sign',color:'#f97316'},
308
  ];
309
+ // CORRIGÉ: Sélectionne TOUTES les classes par défaut (pas juste Human!)
310
+ let sel=new Set(CLASSES.map(c=>c.name));
311
 
312
  function buildCls(){
313
  const g=document.getElementById('clsGrid'); g.innerHTML='';
 
451
 
452
  async function startWc(){
453
  try{
454
+ // ✅ CORRECTION 1: Vérifier que le modèle est prêt
455
+ const healthRes=await fetch('/health');
456
+ const health=await healthRes.json();
457
+ if(!health.model_ready){
458
+ alert('⏳ Model is still loading... Please wait 30 seconds and try again');
459
+ return;
460
+ }
461
+
462
+ // ✅ CORRECTION 2: Vérifier que getUserMedia est disponible
463
+ if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){
464
+ alert('⚠️ Webcam not available on this device or browser. Try uploading a video instead.');
465
+ return;
466
+ }
467
+
468
+ // ✅ CORRECTION 3: Ajouter les flags pour Hugging Face Spaces
469
+ wcStream=await navigator.mediaDevices.getUserMedia({
470
+ video:{
471
+ width:640,
472
+ height:480,
473
+ facingMode:'user' // Force la caméra frontale
474
+ },
475
+ audio:false
476
+ });
477
+ }catch(e){
478
+ // ✅ CORRECTION 4: Messages d'erreur détaillés
479
+ console.error('Webcam error:',e);
480
+ if(e.name==='NotAllowedError'){
481
+ alert('❌ Camera permission denied. Please allow camera access in your browser settings.');
482
+ }else if(e.name==='NotFoundError'){
483
+ alert('❌ No camera found. Please connect a webcam or upload a video instead.');
484
+ }else if(e.name==='NotReadableError'){
485
+ alert('❌ Camera is in use by another application. Please close it and try again.');
486
+ }else{
487
+ alert('❌ Webcam unavailable: '+e.message);
488
+ }
489
+ return;
490
+ }
491
 
492
  document.getElementById('wcPh').style.display='none';
493
  document.getElementById('recBadge').style.display='flex';
 
497
  btn.innerHTML='<i class="fas fa-stop" style="color:#f87171;font-size:9px;"></i> Stop webcam';
498
  btn.onclick=stopWc;
499
 
500
+ /* ✅ CORRECTION 5: Ajouter un timeout pour éviter que ça reste en attente */
501
+ const timeoutId=setTimeout(()=>{
502
+ console.warn('Webcam frame sending timeout - checking if stream is alive');
503
+ if(!wcStream || !wcStream.active){
504
+ stopWc();
505
+ alert('⚠️ Webcam stream lost. Stopping...');
506
+ }
507
+ }, 30000); // 30 secondes max
508
+
509
  /* Lance YOLO backend */
510
+ try{
511
+ const startRes=await fetch('/api/webcam/start',{
512
+ method:'POST',
513
+ headers:{'Content-Type':'application/json'},
514
+ body:JSON.stringify({classes:[...sel],model:'yolo11n'})
515
+ });
516
+ if(!startRes.ok){
517
+ throw new Error('Backend webcam start failed');
518
+ }
519
+ }catch(startErr){
520
+ console.error('Failed to start backend webcam:',startErr);
521
+ alert('⚠️ Could not start backend processing. Try uploading a video instead.');
522
+ clearTimeout(timeoutId);
523
+ return;
524
+ }
525
 
526
  /* Setup hidden video + canvas pour capturer frames */
527
  _wcHiddenVideo=document.createElement('video');
528
  _wcHiddenVideo.srcObject=wcStream;
529
+ _wcHiddenVideo.setAttribute('playsinline',''); // ✅ Important pour iOS/mobile
530
  _wcHiddenVideo.muted=true;
531
+
532
+ // ✅ CORRECTION 6: Attendre que la vidéo soit prête avant de jouer
533
+ _wcHiddenVideo.onloadedmetadata=()=>{
534
+ _wcHiddenVideo.play().catch(err=>console.warn('Video play error:',err));
535
+ };
536
+
537
+ // ✅ Si déjà chargée (rare mais possible)
538
+ if(_wcHiddenVideo.readyState>=2){
539
+ _wcHiddenVideo.play().catch(err=>console.warn('Video play error:',err));
540
+ }
541
+
542
  _wcCanvas2=document.createElement('canvas');
543
  _wcCanvas2.width=640;_wcCanvas2.height=480;
544
  const ctx2=_wcCanvas2.getContext('2d');
 
549
  miniImg.style.display='block';
550
  bigImg.style.display='block';
551
 
552
+ let frameErrors=0;
553
  _wcFrameIv=setInterval(async()=>{
554
+ if(!wcStream||!wcStream.active||!_wcHiddenVideo) {
555
+ console.warn('Webcam stream lost - stopping');
556
+ clearInterval(_wcFrameIv);
557
+ return;
558
+ }
559
+
560
+ try{
561
+ // ✅ Vérifier que la vidéo a des frames
562
+ if(_wcHiddenVideo.readyState<2){
563
+ console.warn('Video not ready yet, frame:', _wcHiddenVideo.readyState);
564
+ return;
565
+ }
566
+
567
+ ctx2.drawImage(_wcHiddenVideo,0,0,640,480);
568
+ _wcCanvas2.toBlob(async(blob)=>{
569
+ if(!blob)return;
570
+ try{
571
+ const resp=await fetch('/api/webcam/frame',{
572
+ method:'POST',
573
+ headers:{'Content-Type':'image/jpeg'},
574
+ body:blob,
575
+ timeout:5000 // ✅ Timeout 5s par frame
576
+ });
577
+
578
+ if(!resp.ok){
579
+ frameErrors++;
580
+ if(frameErrors>5){
581
+ console.error('Too many frame errors - stopping webcam');
582
+ clearInterval(_wcFrameIv);
583
+ alert('⚠️ Backend is not responding. Stopping webcam.');
584
+ stopWc();
585
+ }
586
+ return;
587
+ }
588
+ frameErrors=0; // Reset counter on success
589
+ const url=URL.createObjectURL(await resp.blob());
590
+ miniImg.src=url;
591
+ bigImg.src=url;
592
+ }catch(e){
593
+ frameErrors++;
594
+ console.warn('Frame send error:',e);
595
+ if(frameErrors>5){
596
+ console.error('Too many frame errors - stopping');
597
+ clearInterval(_wcFrameIv);
598
+ stopWc();
599
+ }
600
+ }
601
+ },'image/jpeg',0.75);
602
+ }catch(e){
603
+ console.error('Frame interval error:',e);
604
+ }
605
  },200);
606
 
607
  /* Grand panel droite */
 
789
  </script>
790
  <script src="/static/trackiq-api.js"></script>
791
  </body>
792
+ </html>