euler314 commited on
Commit
be7a3f2
·
verified ·
1 Parent(s): 89408c1

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +241 -183
  2. requirements.txt +2 -2
app.py CHANGED
@@ -1,190 +1,248 @@
1
- # app.py
2
-
3
- from flask import Flask, Response, request, jsonify
4
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- app = Flask(__name__)
7
-
8
- # ——————————————————————————————
9
- # Configuration: Test size = 1 GiB
10
- # ——————————————————————————————
11
- DOWNLOAD_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GiB
12
-
13
- # ——————————————————————————————
14
- # Generator for streaming 1 GiB of random bytes in 10 MiB chunks
15
- # ——————————————————————————————
16
- def generate_random_blob():
17
- sent = 0
18
- chunk_size = 10 * 1024 * 1024 # 10 MiB per chunk
19
- while sent < DOWNLOAD_SIZE_BYTES:
20
- to_send = min(chunk_size, DOWNLOAD_SIZE_BYTES - sent)
21
- yield os.urandom(to_send)
22
- sent += to_send
23
-
24
-
25
- # ——————————————————————————————
26
- # Route: “/” – serve the HTML + JS client
27
- # ——————————————————————————————
28
- @app.route("/")
29
- def index():
30
- html = """
31
- <!DOCTYPE html>
32
- <html lang="en">
33
- <head>
34
- <meta charset="UTF-8" />
35
- <title>Client-Side Speed Test (1 GiB)</title>
36
- <style>
37
- body { font-family: sans-serif; max-width: 600px; margin: 2em auto; }
38
- button { padding: 0.5em 1em; font-size: 1rem; }
39
- .result { margin-top: 1.5em; }
40
- .label { font-weight: bold; }
41
- </style>
42
- </head>
43
- <body>
44
- <h2>Wi-Fi / Ethernet Speed Test (Up to 1 GiB)</h2>
45
- <p>This test will measure your real Internet speed by downloading <strong>1 GiB</strong> of random data and uploading <strong>1 GiB</strong> back to the server. All timing runs entirely in your browser.</p>
46
- <button id="startBtn">Start Speed Test</button>
47
-
48
- <div class="result" id="results" style="display: none;">
49
- <p><span class="label">Download Speed:</span> <span id="dlSpeed">–</span></p>
50
- <p><span class="label">Upload Speed:</span> <span id="ulSpeed">–</span></p>
51
- <p><span class="label">Ping:</span> <span id="ping">–</span></p>
52
- </div>
53
 
54
- <script>
55
- const startBtn = document.getElementById("startBtn");
56
- const resultsDiv = document.getElementById("results");
57
- const dlSpan = document.getElementById("dlSpeed");
58
- const ulSpan = document.getElementById("ulSpeed");
59
- const pingSpan = document.getElementById("ping");
60
-
61
- // Convert bits/sec to Mbps string
62
- function toMbps(bits) {
63
- return (bits / 1e6).toFixed(2) + " Mbps";
64
- }
65
-
66
- startBtn.addEventListener("click", async () => {
67
- startBtn.disabled = true;
68
- resultsDiv.style.display = "block";
69
- dlSpan.textContent = "Testing…";
70
- ulSpan.textContent = "Testing…";
71
- pingSpan.textContent = "Testing…";
72
-
73
- // 1) Measure ping: 5 tiny GETs to /ping_test
74
- let pingTimes = [];
75
- for (let i = 0; i < 5; i++) {
76
- const t0 = performance.now();
77
- await fetch("/ping_test?t=" + i, { cache: "no-store" });
78
- const t1 = performance.now();
79
- pingTimes.push(t1 - t0);
80
- }
81
- const avgPing = pingTimes.reduce((a, b) => a + b, 0) / pingTimes.length;
82
- pingSpan.textContent = avgPing.toFixed(2) + " ms";
83
-
84
- // 2) Download test: stream 1 GiB from /download_test
85
- const dlStart = performance.now();
86
- const response = await fetch("/download_test", { cache: "no-store" });
87
- const reader = response.body.getReader();
88
- let dlBytes = 0;
89
- while (true) {
90
- const { done, value } = await reader.read();
91
- if (done) break;
92
- dlBytes += value.length;
93
- }
94
- const dlEnd = performance.now();
95
- const dlBits = dlBytes * 8;
96
- const dlDurationSec = (dlEnd - dlStart) / 1000;
97
- const dlSpeedBps = dlBits / dlDurationSec;
98
- dlSpan.textContent = toMbps(dlSpeedBps);
99
-
100
- // 3) Upload test: stream 1 GiB of random data to /upload_test
101
- const uploadTotalBytes = dlBytes; // should be 1 GiB
102
- const chunkSize = 10 * 1024 * 1024; // 10 MiB
103
- let sentBytes = 0;
104
-
105
- const uploadStream = new ReadableStream({
106
- start(controller) {
107
- function pushChunk() {
108
- if (sentBytes >= uploadTotalBytes) {
109
- controller.close();
110
- return;
111
- }
112
- const size = Math.min(chunkSize, uploadTotalBytes - sentBytes);
113
- const chunk = new Uint8Array(size);
114
- window.crypto.getRandomValues(chunk);
115
- controller.enqueue(chunk);
116
- sentBytes += size;
117
- // immediately queue next
118
- pushChunk();
119
- }
120
- pushChunk();
121
- }
122
- });
123
-
124
- const ulStart = performance.now();
125
- await fetch("/upload_test", {
126
- method: "POST",
127
- headers: { "Content-Type": "application/octet-stream" },
128
- body: uploadStream
129
- });
130
- const ulEnd = performance.now();
131
- const ulDurationSec = (ulEnd - ulStart) / 1000;
132
- const ulBits = uploadTotalBytes * 8;
133
- const ulSpeedBps = ulBits / ulDurationSec;
134
- ulSpan.textContent = toMbps(ulSpeedBps);
135
-
136
- startBtn.disabled = false;
137
- });
138
- </script>
139
- </body>
140
- </html>
141
- """
142
- return Response(html, mimetype="text/html")
143
-
144
-
145
- # ——————————————————————————————
146
- # Route: /ping_test – empty 200 response for ping
147
- # ——————————————————————————————
148
- @app.route("/ping_test")
149
- def ping_test():
150
- return Response(status=200)
151
-
152
-
153
- # ——————————————————————————————
154
- # Route: /download_test – stream 1 GiB of random data
155
- # ——————————————————————————————
156
- @app.route("/download_test")
157
- def download_test():
158
  headers = {
159
- "Content-Disposition": f"attachment; filename=\"test_{DOWNLOAD_SIZE_BYTES}.bin\"",
160
  "Content-Length": str(DOWNLOAD_SIZE_BYTES),
161
- "Cache-Control": "no-store"
162
  }
163
- return Response(
164
- generate_random_blob(),
165
- mimetype="application/octet-stream",
166
- headers=headers
167
- )
168
-
169
-
170
- # ——————————————————————————————
171
- # Route: /upload_test – consume uploaded data in chunks and return JSON
172
- # ——————————————————————————————
173
- @app.route("/upload_test", methods=["POST"])
174
- def upload_test():
175
- total = 0
176
- # Read the incoming stream in 1 MiB chunks and discard
177
- while True:
178
- chunk = request.stream.read(1 * 1024 * 1024)
179
- if not chunk:
180
- break
181
- total += len(chunk)
182
- # total should be near 1 GiB if upload completed
183
- return jsonify({"received_bytes": total})
184
-
185
-
186
- # ——————————————————————————————
187
- # Run the Flask app on port 7860
188
- # ——————————————————————————————
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  if __name__ == "__main__":
190
- app.run(host="0.0.0.0", port=7860, debug=False)
 
 
 
 
 
 
1
  import os
2
+ import time
3
+
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.responses import JSONResponse, Response
6
+ import gradio as gr
7
+
8
+ DOWNLOAD_SIZE_BYTES = 10 * 1024 * 1024
9
+ _RANDOM_BLOB = os.urandom(DOWNLOAD_SIZE_BYTES)
10
+
11
+ NO_CACHE_HEADERS = {
12
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
13
+ "Pragma": "no-cache",
14
+ "Expires": "0",
15
+ }
16
+
17
+ fastapi_app = FastAPI()
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ @fastapi_app.get("/ping_test")
21
+ async def ping_test() -> Response:
22
+ headers = {**NO_CACHE_HEADERS, "Content-Length": "4", "X-Server-Timestamp": str(time.time())}
23
+ return Response(content="pong", media_type="text/plain", headers=headers)
24
+
25
+
26
+ @fastapi_app.get("/download_test")
27
+ async def download_test() -> Response:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  headers = {
29
+ **NO_CACHE_HEADERS,
30
  "Content-Length": str(DOWNLOAD_SIZE_BYTES),
31
+ "Content-Disposition": f'attachment; filename="speedtest_{DOWNLOAD_SIZE_BYTES}.bin"',
32
  }
33
+ return Response(content=_RANDOM_BLOB, media_type="application/octet-stream", headers=headers)
34
+
35
+
36
+ @fastapi_app.post("/upload_test")
37
+ async def upload_test(request: Request) -> JSONResponse:
38
+ payload = await request.body()
39
+ headers = dict(NO_CACHE_HEADERS)
40
+ return JSONResponse({"received_bytes": len(payload)}, headers=headers)
41
+
42
+
43
+ LIBRESPEED_HTML = """
44
+ <style>
45
+ .speedtest-container {
46
+ font-family: 'Segoe UI', Arial, sans-serif;
47
+ max-width: 640px;
48
+ margin: 0 auto;
49
+ padding: 32px 24px 40px;
50
+ background: #ffffff;
51
+ border-radius: 16px;
52
+ box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
53
+ }
54
+ .speedtest-heading { margin: 0 0 8px; font-size: 1.9rem; }
55
+ .speedtest-lead { color: #475569; line-height: 1.55; }
56
+ .speedtest-button {
57
+ appearance: none;
58
+ border: none;
59
+ border-radius: 999px;
60
+ padding: 14px 28px;
61
+ font-size: 1rem;
62
+ font-weight: 600;
63
+ color: #fff;
64
+ background: linear-gradient(135deg, #2563eb, #9333ea);
65
+ cursor: pointer;
66
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
67
+ }
68
+ .speedtest-button:disabled { opacity: 0.5; cursor: progress; transform: none; box-shadow: none; }
69
+ .speedtest-button:not(:disabled):hover { transform: translateY(-1px); box-shadow: 0 10px 20px rgba(79, 70, 229, 0.25); }
70
+ .speedtest-results { margin-top: 32px; display: none; }
71
+ .speedtest-grid {
72
+ display: grid;
73
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
74
+ gap: 20px;
75
+ }
76
+ .metric-card {
77
+ background: #f8fafc;
78
+ border-radius: 12px;
79
+ padding: 18px 20px;
80
+ text-align: center;
81
+ border: 1px solid #e2e8f0;
82
+ }
83
+ .metric-label { display: block; font-weight: 600; color: #334155; margin-bottom: 6px; }
84
+ .metric-value { font-size: 1.6rem; font-variant-numeric: tabular-nums; color: #111827; }
85
+ .metric-subtext { margin-top: 10px; color: #64748b; font-size: 0.9rem; }
86
+ .speedtest-log {
87
+ margin-top: 28px;
88
+ padding: 16px;
89
+ font-family: 'JetBrains Mono', 'Fira Mono', Menlo, monospace;
90
+ font-size: 0.85rem;
91
+ line-height: 1.45;
92
+ background: #0f172a;
93
+ color: #e2e8f0;
94
+ border-radius: 12px;
95
+ max-height: 220px;
96
+ overflow-y: auto;
97
+ white-space: pre-wrap;
98
+ }
99
+ </style>
100
+ <div class="speedtest-container">
101
+ <h2 class="speedtest-heading">Wi-Fi / Ethernet Speed Test</h2>
102
+ <p class="speedtest-lead">Run a download, upload, and latency check right from your browser. Results reflect the real connection between this device and Hugging Face Spaces.</p>
103
+ <button id="startBtn" class="speedtest-button">Start Speed Test</button>
104
+ <div id="results" class="speedtest-results">
105
+ <div class="speedtest-grid">
106
+ <div class="metric-card">
107
+ <span class="metric-label">Download</span>
108
+ <span id="dlSpeed" class="metric-value">–</span>
109
+ <div class="metric-subtext">Average throughput in Mbps</div>
110
+ </div>
111
+ <div class="metric-card">
112
+ <span class="metric-label">Upload</span>
113
+ <span id="ulSpeed" class="metric-value">–</span>
114
+ <div class="metric-subtext">Average throughput in Mbps</div>
115
+ </div>
116
+ <div class="metric-card">
117
+ <span class="metric-label">Ping</span>
118
+ <span id="ping" class="metric-value">–</span>
119
+ <div class="metric-subtext">Round-trip latency in ms</div>
120
+ </div>
121
+ </div>
122
+ <div id="log" class="speedtest-log"></div>
123
+ </div>
124
+ </div>
125
+ <script>
126
+ const startBtn = document.getElementById('startBtn');
127
+ const resultsDiv = document.getElementById('results');
128
+ const dlSpan = document.getElementById('dlSpeed');
129
+ const ulSpan = document.getElementById('ulSpeed');
130
+ const pingSpan = document.getElementById('ping');
131
+ const logView = document.getElementById('log');
132
+
133
+ function log(message) {
134
+ const timestamp = new Date().toLocaleTimeString();
135
+ logView.textContent += '[' + timestamp + '] ' + message + '\n';
136
+ logView.scrollTop = logView.scrollHeight;
137
+ }
138
+
139
+ function formatMbps(bitsPerSecond) {
140
+ if (!Number.isFinite(bitsPerSecond) || bitsPerSecond <= 0) {
141
+ return '0.00 Mbps';
142
+ }
143
+ return (bitsPerSecond / 1e6).toFixed(2) + ' Mbps';
144
+ }
145
+
146
+ async function measurePing(attempts = 5) {
147
+ const samples = [];
148
+ for (let i = 0; i < attempts; i++) {
149
+ const start = performance.now();
150
+ const response = await fetch('/ping_test?i=' + i + '&cacheBust=' + Math.random(), { cache: 'no-store' });
151
+ if (!response.ok) {
152
+ throw new Error('Ping request failed with status ' + response.status);
153
+ }
154
+ await response.text();
155
+ const end = performance.now();
156
+ samples.push(end - start);
157
+ }
158
+ const avg = samples.reduce((acc, cur) => acc + cur, 0) / samples.length;
159
+ return avg;
160
+ }
161
+
162
+ async function measureDownload() {
163
+ const start = performance.now();
164
+ const response = await fetch('/download_test', { cache: 'no-store' });
165
+ if (!response.ok) {
166
+ throw new Error('Download request failed with status ' + response.status);
167
+ }
168
+ const blob = await response.blob();
169
+ const end = performance.now();
170
+ const durationSeconds = (end - start) / 1000;
171
+ const bitsTransferred = blob.size * 8;
172
+ return { bitsTransferred, durationSeconds };
173
+ }
174
+
175
+ async function measureUpload(bytesToSend) {
176
+ const randomData = new Uint8Array(bytesToSend);
177
+ crypto.getRandomValues(randomData);
178
+ const start = performance.now();
179
+ const response = await fetch('/upload_test', {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/octet-stream' },
182
+ body: randomData,
183
+ });
184
+ const end = performance.now();
185
+ if (!response.ok) {
186
+ throw new Error('Upload request failed with status ' + response.status);
187
+ }
188
+ const json = await response.json();
189
+ if (!json || typeof json.received_bytes !== 'number') {
190
+ throw new Error('Upload response missing received_bytes.');
191
+ }
192
+ const durationSeconds = (end - start) / 1000;
193
+ const bitsTransferred = json.received_bytes * 8;
194
+ return { bitsTransferred, durationSeconds };
195
+ }
196
+
197
+ startBtn.addEventListener('click', async () => {
198
+ startBtn.disabled = true;
199
+ resultsDiv.style.display = 'block';
200
+ dlSpan.textContent = 'Testing…';
201
+ ulSpan.textContent = 'Testing…';
202
+ pingSpan.textContent = 'Testing…';
203
+ logView.textContent = '';
204
+
205
+ try {
206
+ log('Measuring ping…');
207
+ const pingMs = await measurePing();
208
+ pingSpan.textContent = pingMs.toFixed(2) + ' ms';
209
+ log('Average ping: ' + pingSpan.textContent);
210
+
211
+ log('Running download test (10 MiB)…');
212
+ const downloadResult = await measureDownload();
213
+ const downloadBps = downloadResult.bitsTransferred / downloadResult.durationSeconds;
214
+ dlSpan.textContent = formatMbps(downloadBps);
215
+ log('Download duration: ' + downloadResult.durationSeconds.toFixed(2) + ' s');
216
+ log('Download speed: ' + dlSpan.textContent);
217
+
218
+ log('Running upload test (10 MiB)…');
219
+ const uploadResult = await measureUpload(downloadResult.bitsTransferred / 8);
220
+ const uploadBps = uploadResult.bitsTransferred / uploadResult.durationSeconds;
221
+ ulSpan.textContent = formatMbps(uploadBps);
222
+ log('Upload duration: ' + uploadResult.durationSeconds.toFixed(2) + ' s');
223
+ log('Upload speed: ' + ulSpan.textContent);
224
+
225
+ log('Speed test complete. Start again for a fresh run.');
226
+ } catch (error) {
227
+ console.error(error);
228
+ const message = error && error.message ? error.message : String(error);
229
+ log('Error: ' + message);
230
+ dlSpan.textContent = 'Error';
231
+ ulSpan.textContent = 'Error';
232
+ pingSpan.textContent = 'Error';
233
+ } finally {
234
+ startBtn.disabled = false;
235
+ }
236
+ });
237
+ </script>
238
+ """
239
+
240
+ with gr.Blocks(fill_height=True) as demo:
241
+ gr.HTML(value=LIBRESPEED_HTML, sanitize=False)
242
+
243
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/")
244
+
245
  if __name__ == "__main__":
246
+ import uvicorn
247
+
248
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
  # requirements.txt
2
- speedtest-cli==2.1.3
3
- flask==2.3.2
 
1
  # requirements.txt
2
+ gradio==5.33.0
3
+ uvicorn[standard]==0.30.1