htrnguyen commited on
Commit
64dacd3
·
1 Parent(s): 1773215

Minimalist design: Remove emoji icons

Browse files
Files changed (2) hide show
  1. README.md +9 -7
  2. templates/index.html +273 -285
README.md CHANGED
@@ -7,16 +7,16 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- # 🏌️ Golf Tech Analysis
11
 
12
  Phân tích kỹ thuật golf swing bằng AI - Tự động nhận diện 8 giai đoạn, chấm điểm và đưa ra coaching.
13
 
14
  ## Tính năng
15
 
16
- - 🎯 Nhận diện 8 giai đoạn swing (SwingNet AI)
17
- - 💪 Phân tích tư thế (MediaPipe Pose)
18
- - 📊 Chấm điểm tự động (0-10 điểm)
19
- - 🎓 Đề xuất bài tập khắc phục
20
 
21
  ## Cài đặt
22
 
@@ -31,6 +31,7 @@ pip install -r requirements.txt
31
  ```bash
32
  python main.py video.mp4
33
  # Kết quả: results/[video_id]/master_data.json
 
34
  ```
35
 
36
  ### API Server
@@ -43,9 +44,10 @@ python api_server.py
43
  **Endpoints:**
44
 
45
  - `GET /` - Giao diện test upload
46
- - `POST /api/analyze` - API endpoint (nhận video, trả JSON)
 
47
 
48
- ### Tạo video có overlay
49
 
50
  ```bash
51
  python reengineer.py --json results/[id]/master_data.json --video video.mp4 --output output.mp4
 
7
  pinned: false
8
  ---
9
 
10
+ # Golf Tech Analysis
11
 
12
  Phân tích kỹ thuật golf swing bằng AI - Tự động nhận diện 8 giai đoạn, chấm điểm và đưa ra coaching.
13
 
14
  ## Tính năng
15
 
16
+ - Nhận diện 8 giai đoạn swing (SwingNet AI)
17
+ - Phân tích tư thế (MediaPipe Pose)
18
+ - Chấm điểm tự động (0-10 điểm)
19
+ - Đề xuất bài tập khắc phục
20
 
21
  ## Cài đặt
22
 
 
31
  ```bash
32
  python main.py video.mp4
33
  # Kết quả: results/[video_id]/master_data.json
34
+ # Video overlay: results/[video_id]/analyzed_video.mp4
35
  ```
36
 
37
  ### API Server
 
44
  **Endpoints:**
45
 
46
  - `GET /` - Giao diện test upload
47
+ - `POST /` - Tạo video overlay (trả về file .mp4)
48
+ - `POST /api/analyze` - API endpoint (chỉ trả JSON)
49
 
50
+ ### Tạo video có overlay từ JSON
51
 
52
  ```bash
53
  python reengineer.py --json results/[id]/master_data.json --video video.mp4 --output output.mp4
templates/index.html CHANGED
@@ -1,306 +1,294 @@
1
  <!DOCTYPE html>
2
  <html lang="vi">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Golf Tech Analysis - API Test</title>
7
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
- body {
14
- font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
15
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
16
- min-height: 100vh;
17
- display: flex;
18
- align-items: center;
19
- justify-content: center;
20
- padding: 20px;
21
- }
22
- .container {
23
- background: rgba(255, 255, 255, 0.95);
24
- border-radius: 20px;
25
- padding: 40px;
26
- max-width: 600px;
27
- width: 100%;
28
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
29
- }
30
- h1 {
31
- text-align: center;
32
- color: #333;
33
- margin-bottom: 10px;
34
- font-size: 28px;
35
- }
36
- .subtitle {
37
- text-align: center;
38
- color: #666;
39
- margin-bottom: 30px;
40
- font-size: 14px;
41
- }
42
- .upload-area {
43
- border: 3px dashed #667eea;
44
- border-radius: 15px;
45
- padding: 40px;
46
- text-align: center;
47
- cursor: pointer;
48
- transition: all 0.3s;
49
- margin-bottom: 20px;
50
- }
51
- .upload-area:hover {
52
- border-color: #764ba2;
53
- background: rgba(102, 126, 234, 0.05);
54
- }
55
- .upload-area.dragover {
56
- background: rgba(102, 126, 234, 0.1);
57
- border-color: #764ba2;
58
- }
59
- input[type="file"] {
60
- display: none;
61
- }
62
- button {
63
- width: 100%;
64
- padding: 15px;
65
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
66
- color: white;
67
- border: none;
68
- border-radius: 10px;
69
- font-size: 16px;
70
- font-weight: bold;
71
- cursor: pointer;
72
- transition: transform 0.2s;
73
- }
74
- button:hover:not(:disabled) {
75
- transform: translateY(-2px);
76
- }
77
- button:disabled {
78
- opacity: 0.6;
79
- cursor: not-allowed;
80
- }
81
- .progress {
82
- display: none;
83
- margin: 20px 0;
84
- }
85
- .progress-bar {
86
- height: 30px;
87
- background: #e0e0e0;
88
- border-radius: 15px;
89
- overflow: hidden;
90
- }
91
- .progress-fill {
92
- height: 100%;
93
- background: linear-gradient(90deg, #667eea, #764ba2);
94
- width: 0%;
95
- transition: width 0.3s;
96
- display: flex;
97
- align-items: center;
98
- justify-content: center;
99
- color: white;
100
- font-weight: bold;
101
- }
102
- .result {
103
- display: none;
104
- margin-top: 20px;
105
- padding: 20px;
106
- background: #f5f5f5;
107
- border-radius: 10px;
108
- }
109
- .result h3 {
110
- margin-bottom: 10px;
111
- color: #667eea;
112
- }
113
- .score {
114
- font-size: 48px;
115
- font-weight: bold;
116
- text-align: center;
117
- color: #764ba2;
118
- margin: 20px 0;
119
- }
120
- .info {
121
- margin: 10px 0;
122
- }
123
- .info strong {
124
- color: #333;
125
- }
126
- .download-btn {
127
- margin-top: 15px;
128
- background: #28a745;
129
- }
130
- .download-btn:hover {
131
- background: #218838;
132
- }
133
  </style>
134
- </head>
135
- <body>
136
  <div class="container">
137
- <h1>🏌️ Golf Tech Analysis</h1>
138
- <p class="subtitle">API Testing Interface</p>
139
-
140
- <div class="upload-area" id="uploadArea">
141
- <div>
142
- <p style="font-size: 48px">📹</p>
143
- <p style="font-size: 18px; margin-top: 10px">
144
- Click hoặc kéo video vào đây
145
- </p>
146
- <p style="font-size: 14px; color: #999; margin-top: 5px">
147
- Định dạng: MP4, MOV, AVI
148
- </p>
149
  </div>
150
- <input type="file" id="fileInput" accept="video/*" />
151
- </div>
152
-
153
- <button id="analyzeBtn" disabled>Phân tích Video</button>
154
-
155
- <div class="progress" id="progress">
156
- <div class="progress-bar">
157
- <div class="progress-fill" id="progressFill">0%</div>
158
  </div>
159
- </div>
160
-
161
- <div class="result" id="result">
162
- <h3>Kết quả phân tích</h3>
163
- <div class="score" id="score">—</div>
164
- <div class="info">
165
- <strong>Trình độ:</strong> <span id="level">—</span>
166
  </div>
167
- <div class="info">
168
- <strong>Lỗi chính:</strong> <span id="faults">—</span>
 
 
 
 
 
 
169
  </div>
170
- <button class="download-btn" id="downloadBtn">📥 Tải xuống JSON</button>
171
- </div>
172
  </div>
173
 
174
  <script>
175
- const uploadArea = document.getElementById("uploadArea");
176
- const fileInput = document.getElementById("fileInput");
177
- const analyzeBtn = document.getElementById("analyzeBtn");
178
- const progress = document.getElementById("progress");
179
- const progressFill = document.getElementById("progressFill");
180
- const result = document.getElementById("result");
181
- const downloadBtn = document.getElementById("downloadBtn");
182
-
183
- let selectedFile = null;
184
- let jsonResult = null;
 
 
 
185
 
186
- uploadArea.addEventListener("click", () => fileInput.click());
187
-
188
- uploadArea.addEventListener("dragover", (e) => {
189
- e.preventDefault();
190
- uploadArea.classList.add("dragover");
191
- });
192
-
193
- uploadArea.addEventListener("dragleave", () => {
194
- uploadArea.classList.remove("dragover");
195
- });
196
-
197
- uploadArea.addEventListener("drop", (e) => {
198
- e.preventDefault();
199
- uploadArea.classList.remove("dragover");
200
- const files = e.dataTransfer.files;
201
- if (files.length > 0) {
202
- selectedFile = files[0];
203
- updateUI();
204
- }
205
- });
206
-
207
- fileInput.addEventListener("change", (e) => {
208
- if (e.target.files.length > 0) {
209
- selectedFile = e.target.files[0];
210
- updateUI();
211
- }
212
- });
213
-
214
- function updateUI() {
215
- if (selectedFile) {
216
- uploadArea.innerHTML = `
217
- <p style="font-size: 24px;">✅</p>
218
  <p><strong>${selectedFile.name}</strong></p>
219
- <p style="font-size: 14px; color: #999;">${(
220
- selectedFile.size /
221
- 1024 /
222
- 1024
223
- ).toFixed(2)} MB</p>
224
  `;
225
- analyzeBtn.disabled = false;
 
 
226
  }
227
- }
228
-
229
- analyzeBtn.addEventListener("click", async () => {
230
- if (!selectedFile) return;
231
-
232
- analyzeBtn.disabled = true;
233
- progress.style.display = "block";
234
- result.style.display = "none";
235
-
236
- const formData = new FormData();
237
- formData.append("file", selectedFile);
238
-
239
- // Simulate progress
240
- let prog = 0;
241
- const interval = setInterval(() => {
242
- prog += 5;
243
- if (prog <= 90) {
244
- progressFill.style.width = prog + "%";
245
- progressFill.textContent = prog + "%";
246
- }
247
- }, 200);
248
-
249
- try {
250
- const response = await fetch("/api/analyze", {
251
- method: "POST",
252
- body: formData,
253
- });
254
-
255
- clearInterval(interval);
256
-
257
- if (response.ok) {
258
- jsonResult = await response.json();
259
- progressFill.style.width = "100%";
260
- progressFill.textContent = "100%";
261
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  setTimeout(() => {
263
- progress.style.display = "none";
264
- showResult(jsonResult);
 
 
265
  }, 500);
266
- } else {
267
- throw new Error("Analysis failed");
268
- }
269
- } catch (error) {
270
- clearInterval(interval);
271
- alert("Lỗi: " + error.message);
272
- analyzeBtn.disabled = false;
273
- progress.style.display = "none";
274
- }
275
- });
276
-
277
- function showResult(data) {
278
- document.getElementById("score").textContent =
279
- data.coaching.final_score + "/10";
280
- document.getElementById("level").textContent =
281
- data.coaching.skill_level;
282
- document.getElementById("faults").textContent =
283
- data.coaching.key_faults.length > 0
284
- ? data.coaching.key_faults[0]
285
- : "Không có lỗi đáng kể";
286
-
287
- result.style.display = "block";
288
- analyzeBtn.disabled = false;
289
- }
290
-
291
- downloadBtn.addEventListener("click", () => {
292
- if (jsonResult) {
293
- const blob = new Blob([JSON.stringify(jsonResult, null, 2)], {
294
- type: "application/json",
295
- });
296
- const url = URL.createObjectURL(blob);
297
- const a = document.createElement("a");
298
- a.href = url;
299
- a.download = "analysis_result.json";
300
- a.click();
301
- URL.revokeObjectURL(url);
302
  }
303
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  </script>
305
- </body>
306
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Golf Tech Analysis</title>
7
  <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ padding: 20px;
17
+ }
18
+ .container {
19
+ background: rgba(255, 255, 255, 0.95);
20
+ border-radius: 20px;
21
+ padding: 40px;
22
+ max-width: 600px;
23
+ width: 100%;
24
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
25
+ }
26
+ h1 {
27
+ text-align: center;
28
+ color: #333;
29
+ margin-bottom: 10px;
30
+ font-size: 28px;
31
+ }
32
+ .subtitle {
33
+ text-align: center;
34
+ color: #666;
35
+ margin-bottom: 30px;
36
+ font-size: 14px;
37
+ }
38
+ .upload-area {
39
+ border: 3px dashed #667eea;
40
+ border-radius: 15px;
41
+ padding: 40px;
42
+ text-align: center;
43
+ cursor: pointer;
44
+ transition: all 0.3s;
45
+ margin-bottom: 20px;
46
+ }
47
+ .upload-area:hover {
48
+ border-color: #764ba2;
49
+ background: rgba(102, 126, 234, 0.05);
50
+ }
51
+ input[type="file"] { display: none; }
52
+
53
+ .buttons {
54
+ display: grid;
55
+ grid-template-columns: 1fr 1fr;
56
+ gap: 10px;
57
+ margin-bottom: 20px;
58
+ }
59
+ button {
60
+ padding: 15px;
61
+ color: white;
62
+ border: none;
63
+ border-radius: 10px;
64
+ font-size: 16px;
65
+ font-weight: bold;
66
+ cursor: pointer;
67
+ transition: transform 0.2s;
68
+ }
69
+ button:hover:not(:disabled) { transform: translateY(-2px); }
70
+ button:disabled { opacity: 0.6; cursor: not-allowed; }
71
+
72
+ .btn-json {
73
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
74
+ }
75
+ .btn-video {
76
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
77
+ }
78
+
79
+ .progress {
80
+ display: none;
81
+ margin: 20px 0;
82
+ }
83
+ .progress-bar {
84
+ height: 30px;
85
+ background: #e0e0e0;
86
+ border-radius: 15px;
87
+ overflow: hidden;
88
+ }
89
+ .progress-fill {
90
+ height: 100%;
91
+ background: linear-gradient(90deg, #667eea, #764ba2);
92
+ width: 0%;
93
+ transition: width 0.3s;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ color: white;
98
+ font-weight: bold;
99
+ }
100
+ .result {
101
+ display: none;
102
+ margin-top: 20px;
103
+ padding: 20px;
104
+ background: #f5f5f5;
105
+ border-radius: 10px;
106
+ }
107
+ .result h3 { color: #667eea; margin-bottom: 10px; }
108
+ .score {
109
+ font-size: 48px;
110
+ font-weight: bold;
111
+ text-align: center;
112
+ color: #764ba2;
113
+ margin: 20px 0;
114
+ }
115
+ .info { margin: 10px 0; }
116
+ .download-btn {
117
+ width: 100%;
118
+ margin-top: 15px;
119
+ background: #28a745;
120
+ }
 
 
 
 
 
 
 
 
 
 
 
 
121
  </style>
122
+ </head>
123
+ <body>
124
  <div class="container">
125
+ <h1>Golf Tech Analysis</h1>
126
+ <p class="subtitle">Upload video để phân tích</p>
127
+
128
+ <div class="upload-area" id="uploadArea">
129
+ <div>
130
+ <p style="font-size: 48px; font-weight: bold;">+</p>
131
+ <p style="font-size: 18px; margin-top: 10px;">Click hoặc kéo video vào đây</p>
132
+ <p style="font-size: 14px; color: #999; margin-top: 5px;">MP4, MOV, AVI</p>
133
+ </div>
134
+ <input type="file" id="fileInput" accept="video/*">
 
 
135
  </div>
136
+
137
+ <div class="buttons">
138
+ <button class="btn-json" id="analyzeJsonBtn" disabled>Chỉ JSON (Nhanh)</button>
139
+ <button class="btn-video" id="analyzeVideoBtn" disabled>JSON + Video</button>
 
 
 
 
140
  </div>
141
+
142
+ <div class="progress" id="progress">
143
+ <div class="progress-bar">
144
+ <div class="progress-fill" id="progressFill">0%</div>
145
+ </div>
 
 
146
  </div>
147
+
148
+ <div class="result" id="result">
149
+ <h3>Kết quả phân tích</h3>
150
+ <div class="score" id="score">—</div>
151
+ <div class="info"><strong>Trình độ:</strong> <span id="level">—</span></div>
152
+ <div class="info"><strong>Lỗi chính:</strong> <span id="faults">—</span></div>
153
+ <button class="download-btn" id="downloadJsonBtn">Tải JSON</button>
154
+ <button class="download-btn" id="downloadVideoBtn" style="display:none; background:#667eea;">Tải Video</button>
155
  </div>
 
 
156
  </div>
157
 
158
  <script>
159
+ const uploadArea = document.getElementById('uploadArea');
160
+ const fileInput = document.getElementById('fileInput');
161
+ const analyzeJsonBtn = document.getElementById('analyzeJsonBtn');
162
+ const analyzeVideoBtn = document.getElementById('analyzeVideoBtn');
163
+ const progress = document.getElementById('progress');
164
+ const progressFill = document.getElementById('progressFill');
165
+ const result = document.getElementById('result');
166
+ const downloadJsonBtn = document.getElementById('downloadJsonBtn');
167
+ const downloadVideoBtn = document.getElementById('downloadVideoBtn');
168
+
169
+ let selectedFile = null;
170
+ let jsonResult = null;
171
+ let videoBlob = null;
172
 
173
+ uploadArea.addEventListener('click', () => fileInput.click());
174
+ fileInput.addEventListener('change', (e) => {
175
+ if (e.target.files.length > 0) {
176
+ selectedFile = e.target.files[0];
177
+ updateUI();
178
+ }
179
+ });
180
+
181
+ function updateUI() {
182
+ if (selectedFile) {
183
+ uploadArea.innerHTML = `
184
+ <p style="font-size: 24px; font-weight: bold;">✓</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  <p><strong>${selectedFile.name}</strong></p>
186
+ <p style="font-size: 14px; color: #999;">${(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
 
 
 
 
187
  `;
188
+ analyzeJsonBtn.disabled = false;
189
+ analyzeVideoBtn.disabled = false;
190
+ }
191
  }
192
+
193
+ analyzeJsonBtn.addEventListener('click', () => analyze(false));
194
+ analyzeVideoBtn.addEventListener('click', () => analyze(true));
195
+
196
+ async function analyze(withVideo) {
197
+ if (!selectedFile) return;
198
+
199
+ analyzeJsonBtn.disabled = true;
200
+ analyzeVideoBtn.disabled = true;
201
+ progress.style.display = 'block';
202
+ result.style.display = 'none';
203
+
204
+ const formData = new FormData();
205
+ formData.append('file', selectedFile);
206
+
207
+ let prog = 0;
208
+ const interval = setInterval(() => {
209
+ prog += withVideo ? 2 : 5;
210
+ if (prog <= 90) {
211
+ progressFill.style.width = prog + '%';
212
+ progressFill.textContent = prog + '%';
213
+ }
214
+ }, withVideo ? 500 : 200);
215
+
216
+ try {
217
+ const endpoint = withVideo ? '/' : '/api/analyze';
218
+ const response = await fetch(endpoint, {
219
+ method: 'POST',
220
+ body: formData
221
+ });
222
+
223
+ clearInterval(interval);
224
+ progressFill.style.width = '100%';
225
+ progressFill.textContent = '100%';
226
+
227
+ if (response.ok) {
228
+ if (withVideo) {
229
+ videoBlob = await response.blob();
230
+ // Lấy JSON từ API
231
+ formData.delete('file');
232
+ formData.append('file', selectedFile);
233
+ const jsonResp = await fetch('/api/analyze', { method: 'POST', body: formData });
234
+ jsonResult = await jsonResp.json();
235
+ showResult(true);
236
+ } else {
237
+ jsonResult = await response.json();
238
+ videoBlob = null;
239
+ showResult(false);
240
+ }
241
+ } else {
242
+ throw new Error('Analysis failed');
243
+ }
244
+ } catch (error) {
245
+ clearInterval(interval);
246
+ alert('Lỗi: ' + error.message);
247
+ analyzeJsonBtn.disabled = false;
248
+ analyzeVideoBtn.disabled = false;
249
+ progress.style.display = 'none';
250
+ }
251
+ }
252
+
253
+ function showResult(hasVideo) {
254
+ document.getElementById('score').textContent = jsonResult.coaching.final_score + '/10';
255
+ document.getElementById('level').textContent = jsonResult.coaching.skill_level;
256
+ document.getElementById('faults').textContent = jsonResult.coaching.key_faults.length > 0
257
+ ? jsonResult.coaching.key_faults[0]
258
+ : 'Không có lỗi đáng kể';
259
+
260
+ downloadVideoBtn.style.display = hasVideo ? 'block' : 'none';
261
+
262
  setTimeout(() => {
263
+ progress.style.display = 'none';
264
+ result.style.display = 'block';
265
+ analyzeJsonBtn.disabled = false;
266
+ analyzeVideoBtn.disabled = false;
267
  }, 500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
269
+
270
+ downloadJsonBtn.addEventListener('click', () => {
271
+ if (jsonResult) {
272
+ const blob = new Blob([JSON.stringify(jsonResult, null, 2)], {type: 'application/json'});
273
+ const url = URL.createObjectURL(blob);
274
+ const a = document.createElement('a');
275
+ a.href = url;
276
+ a.download = 'golf_analysis.json';
277
+ a.click();
278
+ URL.revokeObjectURL(url);
279
+ }
280
+ });
281
+
282
+ downloadVideoBtn.addEventListener('click', () => {
283
+ if (videoBlob) {
284
+ const url = URL.createObjectURL(videoBlob);
285
+ const a = document.createElement('a');
286
+ a.href = url;
287
+ a.download = 'golf_analysis.mp4';
288
+ a.click();
289
+ URL.revokeObjectURL(url);
290
+ }
291
+ });
292
  </script>
293
+ </body>
294
  </html>