akashyadav758 Claude Opus 4.8 (1M context) commited on
Commit
c6e6dac
·
1 Parent(s): 8bb42ac

Dockerize stack, strip watermarking, add navigator monitor

Browse files

Compose the headless-Chrome monitor with the three backend servers and
make the live monitor a full in-browser navigator.

Infra:
- Add docker-compose.yml wiring chrome + chatgpt + gemini + flow on a
shared loopback (network_mode: service:chrome); only chrome publishes.
- Add Dockerfiles for chatgpt-free-api and flow-agent.
- flow: run the FastAPI API on 8101 so it no longer collides with the
ExtensionBridge HTTP callback on 8100 (was crash-looping).
- Update extension cookie-WS ports: gemini 9226, flow 9227.

Cleanup:
- Remove watermark code from flow-agent (omniflash/watermark.py) and
gemini (gemini/watermark.go); drop ffmpeg usage.

Monitor navigator:
- Tabs are now discovered live from CDP (known services first) instead of
being hardcoded.
- Add address bar + back/forward/reload and new/close-tab, backed by new
/api/navigate, /api/reload, /api/back, /api/forward, /api/newtab and
/api/closetab endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Dockerfile CHANGED
@@ -1,3 +1,9 @@
 
 
 
 
 
 
1
  FROM akashyadav758/chrome:latest
2
 
3
  USER root
@@ -7,6 +13,9 @@ COPY chatgpt-free-api/gpt-extension /opt/gpt-extension
7
  COPY free-gemini-api/gemini-extension /opt/gemini-extension
8
  COPY flow-agent/Flow-extension /opt/flow-extension
9
 
 
 
 
10
  # Install Chrome for Testing (unbranded). Branded google-chrome-stable (>=128)
11
  # silently ignores --load-extension, so unpacked extensions never load. CfT is the
12
  # unbranded build of the same Chrome version where --load-extension still works.
@@ -20,8 +29,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends unzip && \
20
 
21
  # Set correct permissions and ownership
22
  RUN rm -rf /opt/flow-extension/_metadata && \
23
- chown -R 1000:1000 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 && \
24
- chmod -R 755 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64
25
 
26
  # Replace the start script with the non-root version
27
  COPY --chmod=755 start_hf.sh /start.sh
 
1
+ # Build the 3-tab monitor server (GPT / Gemini / Flow live view) from source.
2
+ FROM golang:1.22-bookworm AS monitor-build
3
+ WORKDIR /src
4
+ COPY monitor/ ./
5
+ RUN go mod tidy && CGO_ENABLED=0 go build -ldflags="-s -w" -o /monitor .
6
+
7
  FROM akashyadav758/chrome:latest
8
 
9
  USER root
 
13
  COPY free-gemini-api/gemini-extension /opt/gemini-extension
14
  COPY flow-agent/Flow-extension /opt/flow-extension
15
 
16
+ # 3-tab monitor binary (replaces the single-view base monitor)
17
+ COPY --from=monitor-build /monitor /opt/monitor3/monitor
18
+
19
  # Install Chrome for Testing (unbranded). Branded google-chrome-stable (>=128)
20
  # silently ignores --load-extension, so unpacked extensions never load. CfT is the
21
  # unbranded build of the same Chrome version where --load-extension still works.
 
29
 
30
  # Set correct permissions and ownership
31
  RUN rm -rf /opt/flow-extension/_metadata && \
32
+ chown -R 1000:1000 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 /opt/monitor3 && \
33
+ chmod -R 755 /opt/gpt-extension /opt/gemini-extension /opt/flow-extension /opt/chrome-linux64 /opt/monitor3
34
 
35
  # Replace the start script with the non-root version
36
  COPY --chmod=755 start_hf.sh /start.sh
README.md CHANGED
@@ -8,4 +8,163 @@ app_port: 3001
8
  pinned: false
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  pinned: false
9
  ---
10
 
11
+ # Chrome Space Headless Chrome + Unpacked Extensions
12
+
13
+ A [Hugging Face Space](https://huggingface.co/spaces/akash1313/chrome) that runs a
14
+ persistent, headless **Chrome for Testing** browser inside Docker with three custom
15
+ unpacked extensions auto-loaded, plus a live web monitor to watch the browser in real time.
16
+
17
+ The browser stays logged in across restarts (persistent profile), and the bundled
18
+ extensions sync cookies / automate workflows for ChatGPT, Google Gemini, and Google Flow.
19
+
20
+ ---
21
+
22
+ ## Bundled extensions
23
+
24
+ | Extension | Source dir | Loaded at | What it does |
25
+ |-----------|-----------|-----------|--------------|
26
+ | **Gpt Agent Api** | `chatgpt-free-api/gpt-extension` | `/opt/gpt-extension` | Auto-syncs ChatGPT cookies to a local ChatGPT Free API server |
27
+ | **Gemini Api Agent** | `free-gemini-api/gemini-extension` | `/opt/gemini-extension` | Auto-syncs Google Gemini cookies to a local Free Gemini API server |
28
+ | **Flow Agent** | `flow-agent/Flow-extension` | `/opt/flow-extension` | Automates Google Flow — T2V / V2V / I2V video + T2I / I2I image generation |
29
+
30
+ All three are Manifest V3 extensions with background service workers. Each subfolder
31
+ also contains the matching backend server and its own README.
32
+
33
+ ---
34
+
35
+ ## Backend servers
36
+
37
+ Each extension is the browser half of a standalone project. The extension uses your
38
+ authenticated browser session and forwards requests to a **local** API server (which you
39
+ run yourself — see each subproject's README for setup):
40
+
41
+ | Project | Server | Talks to | Purpose |
42
+ |---------|--------|----------|---------|
43
+ | [`chatgpt-free-api`](chatgpt-free-api/README.md) | Go | `127.0.0.1:9225` | ChatGPT backend API from your logged-in session |
44
+ | [`free-gemini-api`](free-gemini-api/README.md) | Go reverse-proxy | local server | Gemini text / Imagen 3 images / Gemini video |
45
+ | [`flow-agent`](flow-agent/README.md) | Python FastAPI + CLI | `127.0.0.1:8100` | Google Flow T2V / V2V / I2V video + T2I / I2I images |
46
+
47
+ > ⚠️ This Space runs **only the browser + the 3 extensions**. It does **not** start the
48
+ > backend servers — run those locally (or in their own containers) and the extensions will
49
+ > connect once the matching port is reachable.
50
+
51
+ ---
52
+
53
+ ## How it works
54
+
55
+ ```
56
+ HF Space (port 3001)
57
+
58
+
59
+ ┌─────────────────────────────────────────────────────────┐
60
+ │ Docker container (Debian 12) │
61
+ │ │
62
+ │ monitor-server ──┐ serves the live UI + /chrome.log │
63
+ │ (Go, :3001) │ │
64
+ │ ▼ │
65
+ │ Chrome for Testing 145 (--headless=new) │
66
+ │ --load-extension=/opt/gpt,/opt/gemini,/opt/flow │
67
+ │ --remote-debugging-port=9222 (CDP) │
68
+ │ --user-data-dir=/home/chrome/data (persistent) │
69
+ │ ▲ │
70
+ │ socat :9223 ─────┘ forwards external CDP → 127.0.0.1 │
71
+ └─────────────────────────────────────────────────────────┘
72
+ ```
73
+
74
+ `start_hf.sh` is the boot sequence:
75
+
76
+ 1. **Profile repair & cleanup** — fixes a nested `data/data` layout and removes cache/junk
77
+ to keep the persistent profile small.
78
+ 2. **Lock removal** — clears `Singleton*` / `LOCK` files left by an unclean shutdown.
79
+ 3. **Preferences injection** — enables `extensions.ui.developer_mode` and sets a clean
80
+ `exit_type` so the "restore pages?" popup never appears.
81
+ 4. **Monitor server** — starts the Go monitor on `:3001` and symlinks `chrome.log` into it.
82
+ 5. **CDP proxy** — `socat` exposes the DevTools port (9223 → 127.0.0.1:9222).
83
+ 6. **Auto-navigate** — a tiny WebSocket client drives the first tab to `chrome://extensions/`.
84
+ 7. **Launch Chrome** — Chrome for Testing in `--headless=new` with all three extensions.
85
+
86
+ ---
87
+
88
+ ## Why Chrome for Testing (not regular Chrome)
89
+
90
+ Branded **Google Chrome ≥ 128 silently ignores `--load-extension`** (removed by the
91
+ [March 2025 Chrome RFC](https://developer.chrome.com/blog/remote-debugging-port) to curb
92
+ abuse). On a branded build the extensions never load and `chrome://extensions/` shows an
93
+ empty list with **no error in the log**.
94
+
95
+ The fix is to run **[Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/)**
96
+ — the unbranded build of the same Chrome version where `--load-extension` still works. The
97
+ Dockerfile downloads CfT `145.0.7632.117` to `/opt/chrome-linux64` and `start_hf.sh` launches
98
+ that binary. Everything else (monitor, CDP proxy, flags) is unchanged.
99
+
100
+ > Alternatives if you ever upgrade: use the CDP `Extensions.loadUnpacked` method
101
+ > (requires `--remote-debugging-pipe` + `--enable-unsafe-extension-debugging`), or plain
102
+ > Chromium. Both also keep `--load-extension`-style loading working.
103
+
104
+ ---
105
+
106
+ ## Endpoints
107
+
108
+ | URL | Description |
109
+ |-----|-------------|
110
+ | `https://akash1313-chrome.hf.space/` | Live Chrome monitor UI |
111
+ | `https://akash1313-chrome.hf.space/chrome.log` | Streaming Chrome log (debugging) |
112
+ | `127.0.0.1:9222` (in-container) | Chrome DevTools Protocol |
113
+ | `:9223` | CDP proxied for external tools (via socat) |
114
+
115
+ ---
116
+
117
+ ## Project layout
118
+
119
+ ```
120
+ chrome-space/
121
+ ├── Dockerfile # builds on akashyadav758/chrome, installs CfT, copies extensions
122
+ ├── start_hf.sh # container entrypoint / Chrome boot sequence
123
+ ├── chatgpt-free-api/ # ChatGPT Free API server + gpt-extension
124
+ ├── free-gemini-api/ # Free Gemini API server + gemini-extension
125
+ └── flow-agent/ # Flow automation CLI + Flow-extension
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Local development
131
+
132
+ Requires Docker. The base image is `linux/amd64` only.
133
+
134
+ ```bash
135
+ # Build
136
+ docker build --platform linux/amd64 -t chrome-space .
137
+
138
+ # Run
139
+ docker run -d --name chrome-space -p 3001:3001 --platform linux/amd64 chrome-space
140
+
141
+ # Verify the 3 extensions loaded (look for service_worker targets)
142
+ docker exec chrome-space curl -s http://127.0.0.1:9222/json \
143
+ | python3 -c 'import json,sys;[print(t["type"],t["url"]) for t in json.load(sys.stdin)]'
144
+ ```
145
+
146
+ Open <http://localhost:3001> to watch the browser.
147
+
148
+ ---
149
+
150
+ ## Deploy
151
+
152
+ This repo **is** the HF Space — pushing to `main` triggers an automatic rebuild:
153
+
154
+ ```bash
155
+ git push origin main
156
+ ```
157
+
158
+ > `hf-token` is gitignored — keep your Hugging Face token out of commits.
159
+
160
+ ---
161
+
162
+ ## Troubleshooting
163
+
164
+ | Symptom | Cause / fix |
165
+ |---------|-------------|
166
+ | `chrome://extensions/` empty, no error in log | Branded Chrome ignoring `--load-extension` → use Chrome for Testing (see above) |
167
+ | `COPY failed: ... not found` at build | Extension source dir name in the Dockerfile doesn't match disk |
168
+ | Extension listed but service worker idle | MV3 headless quirk — open the extension's toggle or hit **Update** on the extensions page |
169
+ | `Failed to connect to the bus` spam in log | Harmless D-Bus warnings in a container with no system bus |
170
+ | "Restore pages?" popup | Handled by the `exit_type=Normal` Preferences injection in `start_hf.sh` |
chatgpt-free-api/Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChatGPT Free API agent — talks to the gpt-extension over ws/http on 127.0.0.1:9225.
2
+ FROM golang:1.26-bookworm AS builder
3
+ WORKDIR /app
4
+ COPY go.mod go.sum ./
5
+ RUN go mod download
6
+ COPY . .
7
+ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o agent .
8
+
9
+ FROM debian:12-slim
10
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
11
+ rm -rf /var/lib/apt/lists/*
12
+ WORKDIR /app
13
+ COPY --from=builder /app/agent .
14
+ COPY config.json .
15
+
16
+ EXPOSE 9225
17
+ CMD ["./agent"]
chatgpt-free-api/config.json CHANGED
@@ -10,5 +10,5 @@
10
  "bulk_delay_seconds": 2,
11
  "browser_wake_cooldown_seconds": 60,
12
  "timezone_offset_min": -330,
13
- "raw_sse_output_path": "/Users/akashyadav/.gemini/antigravity-ide/scratch/raw_sse.txt"
14
  }
 
10
  "bulk_delay_seconds": 2,
11
  "browser_wake_cooldown_seconds": 60,
12
  "timezone_offset_min": -330,
13
+ "raw_sse_output_path": ""
14
  }
docker-compose.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Runs the headless Chrome + 3-tab monitor together with the three backend servers.
2
+ #
3
+ # The server containers join the chrome container's network namespace
4
+ # (network_mode: "service:chrome"), so they all share one 127.0.0.1 — exactly what the
5
+ # extensions expect. Only the chrome service publishes ports.
6
+ #
7
+ # Port map (shared loopback):
8
+ # chrome : 9222 CDP, 3001 monitor UI
9
+ # chatgpt : 9225 (ws+http)
10
+ # gemini : 8000 http, 9226 cookie-WS
11
+ # flow : 8101 http API, 8100 ext-callback, 9227 cookie-WS
12
+
13
+ services:
14
+ chrome:
15
+ build: .
16
+ platform: linux/amd64
17
+ ports:
18
+ - "3001:3001" # 3-tab monitor UI (also the only published port)
19
+ restart: unless-stopped
20
+
21
+ chatgpt:
22
+ build: ./chatgpt-free-api
23
+ platform: linux/amd64
24
+ network_mode: "service:chrome"
25
+ depends_on:
26
+ - chrome
27
+ restart: unless-stopped
28
+
29
+ gemini:
30
+ build: ./free-gemini-api
31
+ platform: linux/amd64
32
+ environment:
33
+ WS_PORT: "9226"
34
+ PORT: "8000"
35
+ network_mode: "service:chrome"
36
+ depends_on:
37
+ - chrome
38
+ restart: unless-stopped
39
+
40
+ flow:
41
+ build: ./flow-agent
42
+ platform: linux/amd64
43
+ environment:
44
+ WS_PORT: "9227"
45
+ HTTP_PORT: "8100"
46
+ network_mode: "service:chrome"
47
+ depends_on:
48
+ - chrome
49
+ restart: unless-stopped
flow-agent/Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flow Agent FastAPI server — talks to the Flow extension.
2
+ # Public HTTP API : 127.0.0.1:8101 (uvicorn / FastAPI)
3
+ # Extension callback HTTP : 127.0.0.1:8100 (HTTP_PORT, ExtensionBridge — hardcoded in extension)
4
+ # cookie/token WebSocket : 127.0.0.1:9227 (WS_PORT)
5
+ FROM python:3.11-slim
6
+
7
+ WORKDIR /app
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+ COPY . .
11
+
12
+ ENV WS_PORT=9227 \
13
+ HTTP_PORT=8100
14
+
15
+ EXPOSE 8100 8101 9227
16
+ CMD ["uvicorn", "cli.api:app", "--host", "127.0.0.1", "--port", "8101"]
flow-agent/Flow-extension/background.js CHANGED
@@ -5,7 +5,7 @@
5
  * Captures bearer token, solves reCAPTCHA, proxies API calls through browser.
6
  */
7
 
8
- const AGENT_WS_URL = 'ws://127.0.0.1:9222';
9
  // NOTE: This is a browser-restricted public API key — safe to ship in extension bundles.
10
  const API_KEY = 'AIzaSyBtrm0o5ab1c-Ec8ZuLcGt3oJAA5VWt3pY';
11
 
 
5
  * Captures bearer token, solves reCAPTCHA, proxies API calls through browser.
6
  */
7
 
8
+ const AGENT_WS_URL = 'ws://127.0.0.1:9227';
9
  // NOTE: This is a browser-restricted public API key — safe to ship in extension bundles.
10
  const API_KEY = 'AIzaSyBtrm0o5ab1c-Ec8ZuLcGt3oJAA5VWt3pY';
11
 
flow-agent/cli/__init__.py ADDED
File without changes
flow-agent/cli/api.py CHANGED
@@ -352,20 +352,9 @@ async def api_generate_video(req: VideoGenerationRequest, download: bool = Query
352
  temp_path = os.path.join(TEMP_DIR, filename)
353
 
354
  if await download_video(active_bridge, media_id, temp_path):
355
- if not req.no_clean:
356
- try:
357
- from omniflash.watermark import remove_watermark_video
358
- remove_watermark_video(temp_path, out_path)
359
- try:
360
- os.remove(temp_path)
361
- except OSError:
362
- pass
363
- except Exception as e:
364
- log.warning(f"Watermark removal failed: {e}. Fallback to raw video.")
365
- os.replace(temp_path, out_path)
366
- else:
367
- os.replace(temp_path, out_path)
368
-
369
  outputs.append({
370
  "media_id": media_id,
371
  "filename": filename,
@@ -482,18 +471,9 @@ async def api_edit_video(req: VideoEditRequest):
482
  temp_path = os.path.join(TEMP_DIR, filename)
483
 
484
  if await download_video(active_bridge, media_id, temp_path):
485
- # V2V edited segments might also have watermarks
486
- try:
487
- from omniflash.watermark import remove_watermark_video
488
- remove_watermark_video(temp_path, out_path)
489
- try:
490
- os.remove(temp_path)
491
- except OSError:
492
- pass
493
- except Exception as e:
494
- log.warning(f"Watermark removal failed: {e}. Fallback to raw video.")
495
- os.replace(temp_path, out_path)
496
-
497
  outputs.append({
498
  "media_id": media_id,
499
  "filename": filename,
 
352
  temp_path = os.path.join(TEMP_DIR, filename)
353
 
354
  if await download_video(active_bridge, media_id, temp_path):
355
+ # Watermark removal disabled — save the raw downloaded video as-is.
356
+ os.replace(temp_path, out_path)
357
+
 
 
 
 
 
 
 
 
 
 
 
358
  outputs.append({
359
  "media_id": media_id,
360
  "filename": filename,
 
471
  temp_path = os.path.join(TEMP_DIR, filename)
472
 
473
  if await download_video(active_bridge, media_id, temp_path):
474
+ # Watermark removal disabled save the raw downloaded video as-is.
475
+ os.replace(temp_path, out_path)
476
+
 
 
 
 
 
 
 
 
 
477
  outputs.append({
478
  "media_id": media_id,
479
  "filename": filename,
flow-agent/cli/edit.py CHANGED
@@ -115,14 +115,8 @@ async def edit_segment(bridge, prompt, aspect, project_id, media_id,
115
  temp_path = os.path.join(temp_dir, f"segment_{segment_num:03d}.mp4")
116
 
117
  if await download_video(bridge, result_media_id, temp_path):
118
- # Auto-remove watermark
119
- try:
120
- from omniflash.watermark import remove_watermark_video
121
- remove_watermark_video(temp_path, out_path, show_progress=False)
122
- os.remove(temp_path)
123
- except Exception as e:
124
- os.replace(temp_path, out_path)
125
- log.warning("⚠️ Watermark removal failed for segment %d: %s", segment_num, e)
126
  # Cleanup empty .temp dir
127
  try:
128
  os.rmdir(temp_dir)
@@ -213,13 +207,6 @@ async def run(args):
213
  "-i", concat_file, "-c", "copy", merge_path
214
  ], capture_output=True)
215
  os.remove(concat_file)
216
- # Auto-remove watermark from merged output
217
- try:
218
- from omniflash.watermark import remove_watermark_video
219
- clean_path = remove_watermark_video(merge_path, show_progress=False)
220
- os.replace(clean_path, merge_path)
221
- except Exception:
222
- pass
223
  log.info("✅ Merged: %s", merge_path)
224
  except Exception as e:
225
  log.warning("⚠️ Merge failed (ffmpeg needed): %s", e)
 
115
  temp_path = os.path.join(temp_dir, f"segment_{segment_num:03d}.mp4")
116
 
117
  if await download_video(bridge, result_media_id, temp_path):
118
+ # Watermark removal disabled — save the raw downloaded segment as-is.
119
+ os.replace(temp_path, out_path)
 
 
 
 
 
 
120
  # Cleanup empty .temp dir
121
  try:
122
  os.rmdir(temp_dir)
 
207
  "-i", concat_file, "-c", "copy", merge_path
208
  ], capture_output=True)
209
  os.remove(concat_file)
 
 
 
 
 
 
 
210
  log.info("✅ Merged: %s", merge_path)
211
  except Exception as e:
212
  log.warning("⚠️ Merge failed (ffmpeg needed): %s", e)
flow-agent/cli/generate.py CHANGED
@@ -99,19 +99,8 @@ async def run(args):
99
  temp_path = os.path.join(temp_dir, os.path.basename(out_path))
100
 
101
  if await download_video(bridge, media_id, temp_path):
102
- # Auto-remove watermark unless --no-clean
103
- if not args.no_clean:
104
- try:
105
- from omniflash.watermark import remove_watermark_video
106
- remove_watermark_video(temp_path, out_path)
107
- os.remove(temp_path)
108
- print(f"🧹 Watermark removed!")
109
- except Exception as e:
110
- # Fallback: move temp to output as-is
111
- os.replace(temp_path, out_path)
112
- print(f"⚠️ Watermark removal failed: {e}")
113
- else:
114
- os.replace(temp_path, out_path)
115
 
116
  # Cleanup empty .temp dir
117
  try:
 
99
  temp_path = os.path.join(temp_dir, os.path.basename(out_path))
100
 
101
  if await download_video(bridge, media_id, temp_path):
102
+ # Watermark removal disabled — save the raw downloaded video as-is.
103
+ os.replace(temp_path, out_path)
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  # Cleanup empty .temp dir
106
  try:
flow-agent/omniflash/watermark.py DELETED
@@ -1,375 +0,0 @@
1
- """Omni Flash — Video Watermark Remover.
2
-
3
- Removes Gemini/Flow watermark from generated videos using reverse alpha blending.
4
- Ported from Gemini-Watermark-Remover-UI (TypeScript → Python).
5
-
6
- Algorithm:
7
- Gemini adds watermark: watermarked = α × logo + (1 - α) × original
8
- Reverse solve: original = (watermarked - α × logo) / (1 - α)
9
- """
10
-
11
- import logging
12
- import os
13
- import cv2
14
- import numpy as np
15
- import subprocess
16
- import json
17
- import sys
18
-
19
- log = logging.getLogger("omniflash.watermark")
20
-
21
- # ── Constants ──────────────────────────────────────────────
22
- ALPHA_THRESHOLD = 0.002 # Ignore very small alpha values (noise)
23
- MAX_ALPHA = 0.99 # Avoid division by near-zero values
24
- LOGO_VALUE = 255 # White watermark color value
25
- VIDEO_ALPHA_SCALE = 0.6 # Gemini videos use 60% opacity watermark
26
-
27
-
28
- # ── Embedded watermark assets (base64-encoded PNGs) ──
29
- _BG_48_B64 = "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"
30
-
31
- _BG_96_B64 = "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"
32
-
33
-
34
- def _load_alpha_map(size: int) -> np.ndarray:
35
- """Load alpha map from embedded base64 PNG data.
36
-
37
- Alpha map is the max RGB channel of the watermark on black background,
38
- normalized to [0, 1].
39
- """
40
- import base64
41
-
42
- b64 = _BG_48_B64 if size == 48 else _BG_96_B64
43
- png_bytes = base64.b64decode(b64)
44
- arr = np.frombuffer(png_bytes, dtype=np.uint8)
45
- bg = cv2.imdecode(arr, cv2.IMREAD_COLOR)
46
-
47
- if bg is None:
48
- raise ValueError(f"Failed to decode embedded bg_{size}.png")
49
-
50
- # Resize to exact size if needed
51
- if bg.shape[0] != size or bg.shape[1] != size:
52
- bg = cv2.resize(bg, (size, size))
53
-
54
- # Max of RGB channels, normalized to [0, 1]
55
- alpha_map = bg.max(axis=2).astype(np.float32) / 255.0
56
- return alpha_map
57
-
58
-
59
- # Cache alpha maps
60
- _alpha_cache: dict[int, np.ndarray] = {}
61
-
62
-
63
- def get_alpha_map(size: int) -> np.ndarray:
64
- """Get cached alpha map for given watermark size."""
65
- if size not in _alpha_cache:
66
- _alpha_cache[size] = _load_alpha_map(size)
67
- return _alpha_cache[size]
68
-
69
-
70
- def detect_watermark_config(width: int, height: int, is_video: bool = True):
71
- """Detect watermark size and position based on dimensions."""
72
- if is_video:
73
- short_dim = min(width, height)
74
- if short_dim >= 1080:
75
- logo_size = 96
76
- margin_right = 64
77
- margin_bottom = 64
78
- else:
79
- logo_size = 48
80
- margin_right = 72
81
- margin_bottom = 72
82
- else:
83
- if width == 720 and height == 1280:
84
- logo_size = 48
85
- margin_right = 72
86
- margin_bottom = 72
87
- else:
88
- short_dim = min(width, height)
89
- if short_dim > 800:
90
- logo_size = 96
91
- margin_right = 64
92
- margin_bottom = 64
93
- else:
94
- logo_size = 48
95
- margin_right = 32
96
- margin_bottom = 32
97
-
98
- x = width - margin_right - logo_size
99
- y = height - margin_bottom - logo_size
100
-
101
- return {
102
- "logo_size": logo_size,
103
- "x": x,
104
- "y": y,
105
- "width": logo_size,
106
- "height": logo_size,
107
- }
108
-
109
-
110
- def remove_watermark_frame(frame: np.ndarray, is_video: bool = True) -> np.ndarray:
111
- """Remove watermark from a single frame (BGR numpy array).
112
-
113
- Uses reverse alpha blending:
114
- original = (watermarked - α × logo) / (1 - α)
115
- """
116
- h, w = frame.shape[:2]
117
- config = detect_watermark_config(w, h, is_video)
118
- alpha_map = get_alpha_map(config["logo_size"])
119
-
120
- x, y = config["x"], config["y"]
121
- size = config["logo_size"]
122
-
123
- # Extract watermark region
124
- region = frame[y:y+size, x:x+size].astype(np.float32)
125
-
126
- # Scale alpha for video
127
- alpha = alpha_map.copy()
128
- if is_video:
129
- alpha = alpha * VIDEO_ALPHA_SCALE
130
-
131
- # Create mask for significant alpha values
132
- mask = alpha >= ALPHA_THRESHOLD
133
- alpha_clamped = np.clip(alpha, 0, MAX_ALPHA)
134
- one_minus_alpha = 1.0 - alpha_clamped
135
-
136
- # Apply reverse alpha blending to each channel
137
- for c in range(3): # BGR
138
- channel = region[:, :, c]
139
- # original = (watermarked - α × LOGO_VALUE) / (1 - α)
140
- original = (channel - alpha_clamped * LOGO_VALUE) / one_minus_alpha
141
- # Only modify pixels where alpha is significant
142
- channel[mask] = original[mask]
143
- region[:, :, c] = np.clip(channel, 0, 255)
144
-
145
- # Write back
146
- frame[y:y+size, x:x+size] = region.astype(np.uint8)
147
- return frame
148
-
149
-
150
- def remove_watermark_yuv_frame(frame_bytes: bytes, width: int, height: int, config: dict) -> bytes:
151
- """Apply YUV watermark reverse blending directly in raw yuv420p buffer using NumPy."""
152
- frame_arr = np.frombuffer(frame_bytes, dtype=np.uint8).copy()
153
-
154
- y_size = width * height
155
- uv_size = (width // 2) * (height // 2)
156
-
157
- # Reshape planes into 2D views
158
- Y = frame_arr[0:y_size].reshape((height, width))
159
- U = frame_arr[y_size:y_size + uv_size].reshape((height // 2, width // 2))
160
- V = frame_arr[y_size + uv_size:].reshape((height // 2, width // 2))
161
-
162
- x, y = config["x"], config["y"]
163
- size = config["logo_size"]
164
-
165
- # Align coordinates to even numbers for YUV420p subsampling compatibility
166
- x0 = (x // 2) * 2
167
- y0 = (y // 2) * 2
168
- x1 = ((x + size + 1) // 2) * 2
169
- y1 = ((y + size + 1) // 2) * 2
170
-
171
- # Ensure bounds are within image dimensions (which must also be even)
172
- x0 = max(0, min(x0, width))
173
- y0 = max(0, min(y0, height))
174
- x1 = max(0, min(x1, width))
175
- y1 = max(0, min(y1, height))
176
-
177
- # Intersect aligned bounds with actual watermark bounding box
178
- w_x0, w_y0 = x, y
179
- w_x1, w_y1 = x + size, y + size
180
-
181
- int_x0 = max(x0, w_x0)
182
- int_y0 = max(y0, w_y0)
183
- int_x1 = min(x1, w_x1)
184
- int_y1 = min(y1, w_y1)
185
-
186
- # Create target alpha map for coordinate-aligned region
187
- alpha_map_full = np.zeros((y1 - y0, x1 - x0), dtype=np.float32)
188
-
189
- if int_x1 > int_x0 and int_y1 > int_y0:
190
- src_x0 = int_x0 - w_x0
191
- src_y0 = int_y0 - w_y0
192
- src_x1 = int_x1 - w_x0
193
- src_y1 = int_y1 - w_y0
194
-
195
- dst_x0 = int_x0 - x0
196
- dst_y0 = int_y0 - y0
197
- dst_x1 = int_x1 - x0
198
- dst_y1 = int_y1 - y0
199
-
200
- alpha_map_full[dst_y0:dst_y1, dst_x0:dst_x1] = get_alpha_map(size)[src_y0:src_y1, src_x0:src_x1]
201
-
202
- scaled_alpha = alpha_map_full * VIDEO_ALPHA_SCALE
203
- scaled_alpha = np.clip(scaled_alpha, 0, MAX_ALPHA)
204
- one_minus_alpha = 1.0 - scaled_alpha
205
- mask = alpha_map_full >= ALPHA_THRESHOLD
206
-
207
- # 1. Process Y channel (Luminance)
208
- Y_roi = Y[y0:y1, x0:x1].astype(np.float32)
209
- new_Y = (Y_roi - scaled_alpha * 235.0) / one_minus_alpha
210
- Y[y0:y1, x0:x1] = np.where(mask, np.clip(new_Y, 0, 255).astype(np.uint8), Y[y0:y1, x0:x1])
211
-
212
- # 2. Process U & V channels (Chroma, subsampled 2x2)
213
- x0_sub, y0_sub = x0 // 2, y0 // 2
214
- x1_sub, y1_sub = x1 // 2, y1 // 2
215
-
216
- scaled_alpha_sub = scaled_alpha[::2, ::2]
217
- one_minus_alpha_sub = one_minus_alpha[::2, ::2]
218
- mask_sub = mask[::2, ::2]
219
-
220
- # U Channel
221
- U_roi = U[y0_sub:y1_sub, x0_sub:x1_sub].astype(np.float32)
222
- new_U = (U_roi - 128.0) / one_minus_alpha_sub + 128.0
223
- U[y0_sub:y1_sub, x0_sub:x1_sub] = np.where(mask_sub, np.clip(new_U, 0, 255).astype(np.uint8), U[y0_sub:y1_sub, x0_sub:x1_sub])
224
-
225
- # V Channel
226
- V_roi = V[y0_sub:y1_sub, x0_sub:x1_sub].astype(np.float32)
227
- new_V = (V_roi - 128.0) / one_minus_alpha_sub + 128.0
228
- V[y0_sub:y1_sub, x0_sub:x1_sub] = np.where(mask_sub, np.clip(new_V, 0, 255).astype(np.uint8), V[y0_sub:y1_sub, x0_sub:x1_sub])
229
-
230
- return frame_arr.tobytes()
231
-
232
-
233
- def remove_watermark_video(input_path: str, output_path: str = None,
234
- show_progress: bool = True) -> str:
235
- """Remove watermark from a video file using ffmpeg YUV420p pipes.
236
-
237
- Args:
238
- input_path: Path to input video file
239
- output_path: Path for output video (default: adds _clean suffix)
240
- show_progress: Show progress log
241
-
242
- Returns:
243
- Path to cleaned video file
244
- """
245
- input_path = str(input_path)
246
- if output_path is None:
247
- base, ext = os.path.splitext(input_path)
248
- output_path = f"{base}_clean{ext}"
249
-
250
- abs_input = os.path.abspath(input_path)
251
- abs_output = os.path.abspath(output_path)
252
-
253
- # Get video info via ffprobe
254
- probe = subprocess.run(
255
- ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", abs_input],
256
- capture_output=True, text=True
257
- )
258
- info = json.loads(probe.stdout)
259
-
260
- video_stream = next(s for s in info["streams"] if s["codec_type"] == "video")
261
- width = int(video_stream["width"])
262
- height = int(video_stream["height"])
263
-
264
- # Parse fps
265
- fps_str = video_stream.get("r_frame_rate", "24/1")
266
- if "/" in fps_str:
267
- num, den = fps_str.split("/")
268
- fps = float(num) / float(den)
269
- else:
270
- fps = float(fps_str)
271
-
272
- duration = float(info.get("format", {}).get("duration", 0))
273
- total_frames = int(duration * fps) if duration > 0 else 0
274
-
275
- log.info("🎬 Processing YUV video: %dx%d @ %.1f fps (%d frames)", width, height, fps, total_frames)
276
-
277
- config = detect_watermark_config(width, height, is_video=True)
278
-
279
- # Temp file for safe rewrite
280
- temp_out = abs_output + ".tmp.mp4"
281
- if os.path.exists(temp_out):
282
- os.remove(temp_out)
283
-
284
- # Dynamic macOS Hardware Acceleration vs standard CPU fallback
285
- if sys.platform == "darwin":
286
- read_cmd = [
287
- "ffmpeg", "-hwaccel", "videotoolbox", "-i", abs_input,
288
- "-f", "rawvideo", "-pix_fmt", "yuv420p",
289
- "-v", "quiet", "-"
290
- ]
291
- encoder_args = ["-c:v", "h264_videotoolbox", "-b:v", "4000k"]
292
- else:
293
- read_cmd = [
294
- "ffmpeg", "-i", abs_input,
295
- "-f", "rawvideo", "-pix_fmt", "yuv420p",
296
- "-v", "quiet", "-"
297
- ]
298
- encoder_args = ["-c:v", "libx264", "-preset", "fast", "-crf", "18"]
299
-
300
- write_cmd = [
301
- "ffmpeg", "-y",
302
- "-f", "rawvideo", "-pix_fmt", "yuv420p",
303
- "-s", f"{width}x{height}", "-r", str(fps_str),
304
- "-i", "-",
305
- "-i", abs_input,
306
- "-map", "0:v", "-map", "1:a?",
307
- ] + encoder_args + [
308
- "-c:a", "copy",
309
- "-movflags", "+faststart",
310
- "-v", "quiet",
311
- temp_out
312
- ]
313
-
314
- reader = subprocess.Popen(read_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
315
- writer = subprocess.Popen(write_cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
316
-
317
- y_size = width * height
318
- uv_size = (width // 2) * (height // 2)
319
- frame_size = y_size + 2 * uv_size
320
- frame_count = 0
321
-
322
- try:
323
- while True:
324
- raw = reader.stdout.read(frame_size)
325
- if len(raw) < frame_size:
326
- break
327
-
328
- clean_raw = remove_watermark_yuv_frame(raw, width, height, config)
329
- writer.stdin.write(clean_raw)
330
-
331
- frame_count += 1
332
- if show_progress and frame_count % 30 == 0:
333
- pct = int(frame_count / total_frames * 100) if total_frames > 0 else 0
334
- log.info(" Processing: %d%% (%d/%d frames)", pct, frame_count, total_frames)
335
- finally:
336
- writer.stdin.close()
337
- writer.wait()
338
- reader.wait()
339
-
340
- if os.path.exists(temp_out):
341
- if os.path.exists(abs_output):
342
- os.remove(abs_output)
343
- os.rename(temp_out, abs_output)
344
- else:
345
- raise RuntimeError("Failed to compile clean watermark-free video")
346
-
347
- size_mb = os.path.getsize(abs_output) / (1024 * 1024)
348
- log.info("✅ Watermark removed: %s (%.1f MB, %d frames)", abs_output, size_mb, frame_count)
349
- return abs_output
350
-
351
-
352
- def remove_watermark_image(input_path: str, output_path: str = None) -> str:
353
- """Remove watermark from a single image file.
354
-
355
- Args:
356
- input_path: Path to input image
357
- output_path: Path for output (default: adds _clean suffix)
358
-
359
- Returns:
360
- Path to cleaned image
361
- """
362
- input_path = str(input_path)
363
- if output_path is None:
364
- base, ext = os.path.splitext(input_path)
365
- output_path = f"{base}_clean{ext}"
366
-
367
- img = cv2.imread(input_path)
368
- if img is None:
369
- raise ValueError(f"Cannot read image: {input_path}")
370
-
371
- clean = remove_watermark_frame(img, is_video=False)
372
- cv2.imwrite(output_path, clean)
373
-
374
- log.info("✅ Watermark removed: %s", output_path)
375
- return output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
flow-agent/requirements.txt CHANGED
@@ -1,7 +1,4 @@
1
  websockets>=12.0
2
- opencv-python-headless>=4.8
3
- numpy>=1.24
4
  fastapi>=0.100.0
5
  uvicorn>=0.22.0
6
  python-multipart>=0.0.6
7
-
 
1
  websockets>=12.0
 
 
2
  fastapi>=0.100.0
3
  uvicorn>=0.22.0
4
  python-multipart>=0.0.6
 
free-gemini-api/Dockerfile CHANGED
@@ -8,14 +8,14 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o free-gemini-api .
8
 
9
  # Runtime stage
10
  FROM alpine:3.19
11
- RUN apk --no-cache add ca-certificates curl ffmpeg
12
  WORKDIR /app
13
  COPY --from=builder /app/free-gemini-api .
14
  RUN mkdir -p /app/output
15
 
16
- EXPOSE 8000 9222
17
 
18
- ENV WS_PORT="9222" \
19
  PORT="8000"
20
 
21
  CMD ["./free-gemini-api"]
 
8
 
9
  # Runtime stage
10
  FROM alpine:3.19
11
+ RUN apk --no-cache add ca-certificates curl
12
  WORKDIR /app
13
  COPY --from=builder /app/free-gemini-api .
14
  RUN mkdir -p /app/output
15
 
16
+ EXPOSE 8000 9226
17
 
18
+ ENV WS_PORT="9226" \
19
  PORT="8000"
20
 
21
  CMD ["./free-gemini-api"]
free-gemini-api/gemini-extension/background.js CHANGED
@@ -3,7 +3,7 @@
3
  * Proactively keeps Google Gemini cookies fresh and auto-syncs them on change.
4
  */
5
 
6
- const LOCAL_WS_URL = 'ws://127.0.0.1:9222';
7
  let ws = null;
8
  let lastSyncTime = null;
9
  let hasSyncedOnce = false;
 
3
  * Proactively keeps Google Gemini cookies fresh and auto-syncs them on change.
4
  */
5
 
6
+ const LOCAL_WS_URL = 'ws://127.0.0.1:9226';
7
  let ws = null;
8
  let lastSyncTime = null;
9
  let hasSyncedOnce = false;
free-gemini-api/gemini/client.go CHANGED
@@ -1166,24 +1166,6 @@ func (c *GeminiClient) DownloadFile(urlStr, savePath string) error {
1166
  return fmt.Errorf("file too small (%d bytes), likely failed download", info.Size())
1167
  }
1168
 
1169
- // Run watermark remover if file is image or video
1170
- ext := strings.ToLower(filepath.Ext(savePath))
1171
- var fileType string
1172
- if ext == ".png" || ext == ".jpg" || ext == ".jpeg" {
1173
- fileType = "image"
1174
- } else if ext == ".mp4" {
1175
- fileType = "video"
1176
- }
1177
-
1178
- if fileType != "" {
1179
- log.Printf("🧹 Removing watermark from %s (%s) natively in Go...", savePath, fileType)
1180
- if err := RemoveWatermark(savePath, fileType); err != nil {
1181
- log.Printf("⚠️ Failed to remove watermark natively: %v", err)
1182
- } else {
1183
- log.Printf("✨ Watermark removed successfully natively in Go: %s", savePath)
1184
- }
1185
- }
1186
-
1187
  return nil
1188
  }
1189
 
 
1166
  return fmt.Errorf("file too small (%d bytes), likely failed download", info.Size())
1167
  }
1168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1169
  return nil
1170
  }
1171
 
free-gemini-api/gemini/watermark.go DELETED
@@ -1,424 +0,0 @@
1
- package gemini
2
-
3
- import (
4
- "bytes"
5
- "encoding/base64"
6
- "encoding/json"
7
- "fmt"
8
- "image"
9
- "image/color"
10
- "image/draw"
11
- "image/jpeg"
12
- "image/png"
13
- "io"
14
- "log"
15
- "math"
16
- "os"
17
- "os/exec"
18
- "path/filepath"
19
- "strings"
20
- )
21
-
22
- // Constants for mathematical reverse alpha blending
23
- const (
24
- alphaThreshold = 0.002
25
- maxAlpha = 0.99
26
- logoValue = 255.0
27
- videoAlphaScale = 0.6
28
- )
29
-
30
- // Embedded base64-encoded PNG assets for the 48px and 96px watermarks
31
- const (
32
- bg48B64 = "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"
33
- bg96B64 = "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"
34
- )
35
-
36
- // Global cache for decoded alpha maps
37
- var (
38
- alphaCache48 []float32
39
- alphaCache96 []float32
40
- )
41
-
42
- func init() {
43
- // Pre-decode and cache the watermark grids
44
- var err error
45
- alphaCache48, err = loadAlphaMap(48)
46
- if err != nil {
47
- log.Printf("⚠️ Failed to load 48px alpha map: %v", err)
48
- }
49
- alphaCache96, err = loadAlphaMap(96)
50
- if err != nil {
51
- log.Printf("⚠️ Failed to load 96px alpha map: %v", err)
52
- }
53
- }
54
-
55
- // loadAlphaMap decodes base64 PNGs and extracts the maximum RGB channel as alpha mapping
56
- func loadAlphaMap(size int) ([]float32, error) {
57
- b64Str := bg48B64
58
- if size == 96 {
59
- b64Str = bg96B64
60
- }
61
-
62
- data, err := base64.StdEncoding.DecodeString(b64Str)
63
- if err != nil {
64
- return nil, err
65
- }
66
-
67
- img, _, err := image.Decode(bytes.NewReader(data))
68
- if err != nil {
69
- return nil, err
70
- }
71
-
72
- bounds := img.Bounds()
73
- w, h := bounds.Dx(), bounds.Dy()
74
-
75
- // Resize using simple nearest-neighbor if dimensions mismatch
76
- alphaMap := make([]float32, size*size)
77
- for y := 0; y < size; y++ {
78
- srcY := int(float64(y) * float64(h) / float64(size))
79
- for x := 0; x < size; x++ {
80
- srcX := int(float64(x) * float64(w) / float64(size))
81
- r, g, b, _ := img.At(bounds.Min.X+srcX, bounds.Min.Y+srcY).RGBA()
82
-
83
- // Max of RGB, scale [0, 65535] down to [0.0, 1.0]
84
- maxVal := r
85
- if g > maxVal {
86
- maxVal = g
87
- }
88
- if b > maxVal {
89
- maxVal = b
90
- }
91
-
92
- alphaMap[y*size+x] = float32(maxVal) / 65535.0
93
- }
94
- }
95
-
96
- return alphaMap, nil
97
- }
98
-
99
- // WatermarkConfig holds dimensions and offset coordinates
100
- type WatermarkConfig struct {
101
- LogoSize int
102
- X int
103
- Y int
104
- }
105
-
106
- // DetectWatermarkConfig returns position configurations based on dimensions
107
- func DetectWatermarkConfig(width, height int, isVideo bool) WatermarkConfig {
108
- var logoSize, marginRight, marginBottom int
109
-
110
- if isVideo {
111
- shortDim := width
112
- if height < shortDim {
113
- shortDim = height
114
- }
115
-
116
- if shortDim >= 1080 {
117
- logoSize = 96
118
- marginRight = 64
119
- marginBottom = 64
120
- } else {
121
- logoSize = 48
122
- marginRight = 72
123
- marginBottom = 72
124
- }
125
- } else {
126
- // Portrait standard matching
127
- if width == 720 && height == 1280 {
128
- logoSize = 48
129
- marginRight = 72
130
- marginBottom = 72
131
- } else {
132
- shortDim := width
133
- if height < shortDim {
134
- shortDim = height
135
- }
136
-
137
- if shortDim > 800 {
138
- logoSize = 96
139
- marginRight = 64
140
- marginBottom = 64
141
- } else {
142
- logoSize = 48
143
- marginRight = 32
144
- marginBottom = 32
145
- }
146
- }
147
- }
148
-
149
- x := width - marginRight - logoSize
150
- y := height - marginBottom - logoSize
151
-
152
- return WatermarkConfig{
153
- LogoSize: logoSize,
154
- X: x,
155
- Y: y,
156
- }
157
- }
158
-
159
- // RemoveWatermark native hub. Dispatches based on file extensions.
160
- func RemoveWatermark(savePath string, fileType string) error {
161
- ext := strings.ToLower(filepath.Ext(savePath))
162
- if ext == ".png" || ext == ".jpg" || ext == ".jpeg" {
163
- return removeWatermarkFromImage(savePath)
164
- } else if ext == ".mp4" {
165
- return removeWatermarkFromVideo(savePath)
166
- }
167
- return fmt.Errorf("unsupported watermark file type: %s", ext)
168
- }
169
-
170
- // removeWatermarkFromImage performs native Go reverse-alpha editing on static images
171
- func removeWatermarkFromImage(imagePath string) error {
172
- file, err := os.Open(imagePath)
173
- if err != nil {
174
- return err
175
- }
176
- defer file.Close()
177
-
178
- img, format, err := image.Decode(file)
179
- if err != nil {
180
- return err
181
- }
182
- file.Close() // Close early for in-place write
183
-
184
- bounds := img.Bounds()
185
- width, height := bounds.Dx(), bounds.Dy()
186
- config := DetectWatermarkConfig(width, height, false)
187
-
188
- // Fetch cached alpha map
189
- alphaMap := alphaCache48
190
- if config.LogoSize == 96 {
191
- alphaMap = alphaCache96
192
- }
193
- if len(alphaMap) == 0 {
194
- return fmt.Errorf("alpha map cache for size %d is empty", config.LogoSize)
195
- }
196
-
197
- // Create writable canvas
198
- canvas := image.NewRGBA(bounds)
199
- draw.Draw(canvas, bounds, img, bounds.Min, draw.Src)
200
-
201
- // Apply math formula on watermark region
202
- for dy := 0; dy < config.LogoSize; dy++ {
203
- py := config.Y + dy
204
- if py < bounds.Min.Y || py >= bounds.Max.Y {
205
- continue
206
- }
207
-
208
- for dx := 0; dx < config.LogoSize; dx++ {
209
- px := config.X + dx
210
- if px < bounds.Min.X || px >= bounds.Max.X {
211
- continue
212
- }
213
-
214
- alpha := alphaMap[dy*config.LogoSize+dx]
215
- if alpha < alphaThreshold {
216
- continue
217
- }
218
-
219
- // Clamp alpha to avoid division by zero
220
- if alpha > maxAlpha {
221
- alpha = maxAlpha
222
- }
223
-
224
- r, g, b, a := canvas.At(px, py).RGBA()
225
-
226
- // Scale down values to standard float32 [0.0, 255.0]
227
- fR := float64(r) / 257.0
228
- fG := float64(g) / 257.0
229
- fB := float64(b) / 257.0
230
-
231
- // Solve original = (watermarked - alpha * 255.0) / (1 - alpha)
232
- oneMinusAlpha := 1.0 - float64(alpha)
233
- newR := (fR - float64(alpha)*logoValue) / oneMinusAlpha
234
- newG := (fG - float64(alpha)*logoValue) / oneMinusAlpha
235
- newB := (fB - float64(alpha)*logoValue) / oneMinusAlpha
236
-
237
- // Clamp to [0, 255]
238
- cR := uint8(math.Min(math.Max(newR, 0), 255))
239
- cG := uint8(math.Min(math.Max(newG, 0), 255))
240
- cB := uint8(math.Min(math.Max(newB, 0), 255))
241
- cA := uint8(a / 257)
242
-
243
- canvas.SetRGBA(px, py, color.RGBA{R: cR, G: cG, B: cB, A: cA})
244
- }
245
- }
246
-
247
- // Save back in-place
248
- outFile, err := os.Create(imagePath)
249
- if err != nil {
250
- return err
251
- }
252
- defer outFile.Close()
253
-
254
- if format == "png" {
255
- return png.Encode(outFile, canvas)
256
- }
257
- return jpeg.Encode(outFile, canvas, &jpeg.Options{Quality: 95})
258
- }
259
-
260
- // VideoStreamInfo is mapped from ffprobe JSON output
261
- type VideoStreamInfo struct {
262
- Streams []struct {
263
- Width int `json:"width"`
264
- Height int `json:"height"`
265
- CodecType string `json:"codec_type"`
266
- RFrameRate string `json:"r_frame_rate"`
267
- } `json:"streams"`
268
- }
269
-
270
- // removeWatermarkFromVideo runs native FFMPEG byte pipes for zero-Python video watermark removal
271
- func removeWatermarkFromVideo(videoPath string) error {
272
- absPath, err := filepath.Abs(videoPath)
273
- if err != nil {
274
- return err
275
- }
276
-
277
- // 1. Get dimensions using ffprobe
278
- probeCmd := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", absPath)
279
- probeOut, err := probeCmd.Output()
280
- if err != nil {
281
- return fmt.Errorf("ffprobe failed (is ffmpeg installed?): %w", err)
282
- }
283
-
284
- var info VideoStreamInfo
285
- if err := json.Unmarshal(probeOut, &info); err != nil {
286
- return fmt.Errorf("failed to parse ffprobe json: %w", err)
287
- }
288
-
289
- var width, height int
290
- var fps string
291
- for _, stream := range info.Streams {
292
- if stream.CodecType == "video" {
293
- width = stream.Width
294
- height = stream.Height
295
- fps = stream.RFrameRate
296
- break
297
- }
298
- }
299
-
300
- if width == 0 || height == 0 {
301
- return fmt.Errorf("could not extract video dimensions for: %s", videoPath)
302
- }
303
-
304
- if fps == "" {
305
- fps = "24"
306
- }
307
-
308
- config := DetectWatermarkConfig(width, height, true)
309
- alphaMap := alphaCache48
310
- if config.LogoSize == 96 {
311
- alphaMap = alphaCache96
312
- }
313
-
314
- // Setup temp path for safe in-place rewrite
315
- tempOut := absPath + ".tmp.mp4"
316
- defer os.Remove(tempOut)
317
-
318
- // FFMPEG Read command: Decode frames to raw BGR24 on stdout
319
- readCmd := exec.Command("ffmpeg", "-i", absPath, "-f", "rawvideo", "-pix_fmt", "bgr24", "-v", "quiet", "-")
320
- stdout, err := readCmd.StdoutPipe()
321
- if err != nil {
322
- return err
323
- }
324
-
325
- // FFMPEG Write command: Encode BGR24 frames back into H.264 mp4, copying audio from original
326
- writeCmd := exec.Command("ffmpeg", "-y",
327
- "-f", "rawvideo", "-pix_fmt", "bgr24",
328
- "-s", fmt.Sprintf("%dx%d", width, height), "-r", fps,
329
- "-i", "-",
330
- "-i", absPath, // Re-read for audio mapping
331
- "-map", "0:v", "-map", "1:a?",
332
- "-c:v", "libx264", "-preset", "fast", "-crf", "18",
333
- "-c:a", "copy",
334
- "-movflags", "+faststart",
335
- "-v", "quiet",
336
- tempOut,
337
- )
338
-
339
- stdin, err := writeCmd.StdinPipe()
340
- if err != nil {
341
- return err
342
- }
343
-
344
- if err := readCmd.Start(); err != nil {
345
- return fmt.Errorf("ffmpeg reader start failed: %w", err)
346
- }
347
- if err := writeCmd.Start(); err != nil {
348
- return fmt.Errorf("ffmpeg writer start failed: %w", err)
349
- }
350
-
351
- frameSize := width * height * 3 // BGR24
352
- frameBuf := make([]byte, frameSize)
353
-
354
- for {
355
- _, err := io.ReadFull(stdout, frameBuf)
356
- if err == io.EOF || err == io.ErrUnexpectedEOF {
357
- break
358
- }
359
- if err != nil {
360
- break
361
- }
362
-
363
- // Apply watermark reverse blending directly in BGR24 byte slice
364
- for dy := 0; dy < config.LogoSize; dy++ {
365
- py := config.Y + dy
366
- if py < 0 || py >= height {
367
- continue
368
- }
369
-
370
- for dx := 0; dx < config.LogoSize; dx++ {
371
- px := config.X + dx
372
- if px < 0 || px >= width {
373
- continue
374
- }
375
-
376
- alpha := alphaMap[dy*config.LogoSize+dx]
377
- if alpha < alphaThreshold {
378
- continue
379
- }
380
-
381
- // Scale down watermark intensity for videos
382
- scaledAlpha := alpha * videoAlphaScale
383
- if scaledAlpha > maxAlpha {
384
- scaledAlpha = maxAlpha
385
- }
386
-
387
- // Offset inside raw BGR24 frame byte slice
388
- pixelOffset := (py*width + px) * 3
389
-
390
- b := frameBuf[pixelOffset]
391
- g := frameBuf[pixelOffset+1]
392
- r := frameBuf[pixelOffset+2]
393
-
394
- // Solve original = (watermarked - alpha * 255.0) / (1 - alpha)
395
- oneMinusAlpha := 1.0 - float64(scaledAlpha)
396
-
397
- newB := (float64(b) - float64(scaledAlpha)*logoValue) / oneMinusAlpha
398
- newG := (float64(g) - float64(scaledAlpha)*logoValue) / oneMinusAlpha
399
- newR := (float64(r) - float64(scaledAlpha)*logoValue) / oneMinusAlpha
400
-
401
- // Set BGR byte values, clamped to [0, 255]
402
- frameBuf[pixelOffset] = byte(math.Min(math.Max(newB, 0), 255))
403
- frameBuf[pixelOffset+1] = byte(math.Min(math.Max(newG, 0), 255))
404
- frameBuf[pixelOffset+2] = byte(math.Min(math.Max(newR, 0), 255))
405
- }
406
- }
407
-
408
- // Write modified frame to encoder stdin pipe
409
- if _, err := stdin.Write(frameBuf); err != nil {
410
- break
411
- }
412
- }
413
-
414
- stdin.Close()
415
- writeCmd.Wait()
416
- readCmd.Wait()
417
-
418
- // Replace original video with clean copy
419
- if _, err := os.Stat(tempOut); err == nil {
420
- return os.Rename(tempOut, absPath)
421
- }
422
-
423
- return fmt.Errorf("failed to compile clean watermark-free video")
424
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
monitor/go.mod ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ module monitor
2
+
3
+ go 1.22
4
+
5
+ require github.com/gorilla/websocket v1.5.3
monitor/go.sum ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
2
+ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
monitor/index.html ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Chrome Navigator — GPT · Gemini · Flow</title>
8
+ <style>
9
+ :root { --accent: #007acc; }
10
+ * { box-sizing: border-box; }
11
+ body {
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ background: #1e1e1e; color: #d4d4d4; margin: 0; padding: 16px;
14
+ display: flex; flex-direction: column; align-items: center; min-height: 100vh;
15
+ }
16
+ .dashboard { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; justify-content: center; }
17
+ .card { background: #252526; border-radius: 8px; padding: 8px 15px; display: flex; align-items: center; gap: 12px; box-shadow: 0 4px 6px rgba(0,0,0,.3); }
18
+ .card-header { font-size: .72rem; color: #888; text-transform: uppercase; }
19
+ .card-value { font-size: 1.1rem; font-weight: 600; color: #fff; }
20
+ .progress-bg { background: #333; height: 6px; border-radius: 3px; overflow: hidden; width: 60px; }
21
+ .progress-fill { background: var(--accent); height: 100%; width: 0%; transition: width .5s ease; }
22
+
23
+ .tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; justify-content: center; max-width: 95vw; }
24
+ .tab-btn {
25
+ background: #252526; color: #d4d4d4; border: 1px solid #333; border-radius: 8px;
26
+ padding: 7px 10px 7px 14px; font-size: .9rem; font-weight: 600; cursor: pointer;
27
+ transition: all .15s; display: inline-flex; align-items: center; gap: 8px; max-width: 220px;
28
+ }
29
+ .tab-btn:hover { border-color: var(--accent); }
30
+ .tab-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); box-shadow: 0 0 14px rgba(0,122,204,.55); }
31
+ .tab-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
32
+ .tab-close {
33
+ border: none; background: transparent; color: inherit; opacity: .6;
34
+ font-size: 1rem; line-height: 1; cursor: pointer; padding: 0 2px; border-radius: 4px;
35
+ }
36
+ .tab-close:hover { opacity: 1; background: rgba(255,255,255,.15); }
37
+ .tab-new { font-weight: 700; padding: 7px 13px; }
38
+
39
+ .navbar { display: flex; gap: 6px; margin-bottom: 12px; width: min(95vw, 900px); }
40
+ .nav-btn {
41
+ background: #252526; color: #d4d4d4; border: 1px solid #333; border-radius: 8px;
42
+ width: 38px; min-width: 38px; font-size: 1rem; cursor: pointer; transition: all .15s;
43
+ }
44
+ .nav-btn:hover { border-color: var(--accent); color: #fff; }
45
+ #address {
46
+ flex: 1; background: #1b1b1b; color: #e8e8e8; border: 1px solid #333; border-radius: 8px;
47
+ padding: 8px 12px; font-size: .9rem; font-family: ui-monospace, Menlo, Consolas, monospace;
48
+ }
49
+ #address:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 10px rgba(0,122,204,.4); }
50
+ .nav-go { width: auto; padding: 0 16px; font-weight: 600; }
51
+
52
+ #screen-container {
53
+ border: 2px solid var(--accent); box-shadow: 0 0 20px rgba(0,122,204,.5);
54
+ background: #000; border-radius: 8px; overflow: hidden; line-height: 0; position: relative;
55
+ }
56
+ img { max-width: 95vw; max-height: 72vh; width: auto; height: auto; display: block; }
57
+ #hint { color: #777; font-size: .75rem; margin-top: 8px; }
58
+ </style>
59
+ </head>
60
+
61
+ <body>
62
+ <div class="dashboard">
63
+ <div class="card"><div class="card-header">Memory</div>
64
+ <div style="display:flex;align-items:center;gap:10px">
65
+ <div class="card-value" id="memVal">...</div>
66
+ <div class="progress-bg"><div class="progress-fill" id="memBar"></div></div>
67
+ </div>
68
+ </div>
69
+ <div class="card"><div class="card-header">CPU Load</div><div class="card-value" id="cpuVal">...</div></div>
70
+ <div class="card"><div class="card-header">Uptime</div><div class="card-value" id="uptimeVal">...</div></div>
71
+ </div>
72
+
73
+ <div class="tabs" id="tabs"></div>
74
+
75
+ <div class="navbar">
76
+ <button class="nav-btn" id="backBtn" title="Back">←</button>
77
+ <button class="nav-btn" id="fwdBtn" title="Forward">→</button>
78
+ <button class="nav-btn" id="reloadBtn" title="Reload">⟳</button>
79
+ <input id="address" type="text" placeholder="Enter URL and press Enter…" spellcheck="false" autocomplete="off" />
80
+ <button class="nav-btn nav-go" id="goBtn">Go</button>
81
+ </div>
82
+
83
+ <div id="screen-container">
84
+ <img id="monitor" alt="Live Stream" />
85
+ </div>
86
+ <div id="hint">Click and type directly on the page · use the bar above to navigate the active tab anywhere.</div>
87
+
88
+ <script>
89
+ let activeTab = null; // active CDP target id
90
+ let tabList = []; // last /api/tabs payload
91
+ const img = document.getElementById('monitor');
92
+ const tabsEl = document.getElementById('tabs');
93
+ const address = document.getElementById('address');
94
+
95
+ const urlOf = id => (tabList.find(t => t.id === id) || {}).url || '';
96
+
97
+ // Build the tab strip from the server's live target list.
98
+ async function buildTabs() {
99
+ try {
100
+ const res = await fetch('/api/tabs');
101
+ tabList = await res.json();
102
+ if (!tabList.length) { tabsEl.innerHTML = ''; return; }
103
+ if (!tabList.some(t => t.id === activeTab)) activeTab = tabList[0].id;
104
+
105
+ tabsEl.innerHTML = '';
106
+ tabList.forEach(t => {
107
+ const b = document.createElement('button');
108
+ b.className = 'tab-btn' + (t.id === activeTab ? ' active' : '');
109
+ b.dataset.key = t.id;
110
+ b.title = t.url;
111
+
112
+ const label = document.createElement('span');
113
+ label.className = 'tab-label';
114
+ label.textContent = t.title;
115
+ b.appendChild(label);
116
+
117
+ const x = document.createElement('button');
118
+ x.className = 'tab-close';
119
+ x.textContent = '×';
120
+ x.title = 'Close tab';
121
+ x.onclick = e => { e.stopPropagation(); closeTab(t.id); };
122
+ b.appendChild(x);
123
+
124
+ b.onclick = () => switchTab(t.id);
125
+ tabsEl.appendChild(b);
126
+ });
127
+
128
+ // "+ new" button
129
+ const plus = document.createElement('button');
130
+ plus.className = 'tab-btn tab-new';
131
+ plus.textContent = '+';
132
+ plus.title = 'New tab';
133
+ plus.onclick = newTab;
134
+ tabsEl.appendChild(plus);
135
+
136
+ syncAddress();
137
+ } catch (e) { console.error(e); }
138
+ }
139
+
140
+ function syncAddress() {
141
+ if (document.activeElement !== address) address.value = urlOf(activeTab);
142
+ }
143
+
144
+ function switchTab(id) {
145
+ activeTab = id;
146
+ document.querySelectorAll('.tab-btn').forEach(b =>
147
+ b.classList.toggle('active', b.dataset.key === id));
148
+ syncAddress();
149
+ refreshImage();
150
+ }
151
+
152
+ async function post(path) {
153
+ try { await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }
154
+ catch (e) { console.error(e); }
155
+ }
156
+
157
+ async function navigate() {
158
+ const url = address.value.trim();
159
+ if (!url || !activeTab) return;
160
+ try {
161
+ await fetch('/api/navigate?tab=' + activeTab, {
162
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({ url })
164
+ });
165
+ address.blur();
166
+ setTimeout(() => { buildTabs(); refreshImage(); }, 600);
167
+ } catch (e) { console.error(e); }
168
+ }
169
+
170
+ async function reload() { await post('/api/reload?tab=' + activeTab); setTimeout(refreshImage, 500); }
171
+ async function goBack() { await post('/api/back?tab=' + activeTab); setTimeout(() => { buildTabs(); refreshImage(); }, 500); }
172
+ async function goForward() { await post('/api/forward?tab=' + activeTab); setTimeout(() => { buildTabs(); refreshImage(); }, 500); }
173
+
174
+ async function newTab() {
175
+ const url = prompt('Open URL in a new tab:', 'https://');
176
+ if (url === null) return;
177
+ try {
178
+ const res = await fetch('/api/newtab', {
179
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify({ url: url.trim() })
181
+ });
182
+ const { id } = await res.json();
183
+ if (id) activeTab = id;
184
+ await buildTabs();
185
+ refreshImage();
186
+ } catch (e) { console.error(e); }
187
+ }
188
+
189
+ async function closeTab(id) {
190
+ try {
191
+ await fetch('/api/closetab?tab=' + id, { method: 'POST' });
192
+ if (id === activeTab) activeTab = null;
193
+ await buildTabs();
194
+ refreshImage();
195
+ } catch (e) { console.error(e); }
196
+ }
197
+
198
+ document.getElementById('goBtn').onclick = navigate;
199
+ document.getElementById('reloadBtn').onclick = reload;
200
+ document.getElementById('backBtn').onclick = goBack;
201
+ document.getElementById('fwdBtn').onclick = goForward;
202
+ address.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); navigate(); } });
203
+
204
+ async function sendInput(data) {
205
+ if (!activeTab) return;
206
+ try {
207
+ await fetch('/api/input?tab=' + activeTab, {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/json' },
210
+ body: JSON.stringify(data)
211
+ });
212
+ setTimeout(refreshImage, 120);
213
+ } catch (e) { console.error(e); }
214
+ }
215
+
216
+ function coords(e) {
217
+ const rect = img.getBoundingClientRect();
218
+ return {
219
+ x: Math.round((e.clientX - rect.left) * (img.naturalWidth / rect.width)),
220
+ y: Math.round((e.clientY - rect.top) * (img.naturalHeight / rect.height))
221
+ };
222
+ }
223
+ img.addEventListener('mousedown', e => sendInput({ type: 'mousedown', ...coords(e), button: 'left' }));
224
+ img.addEventListener('mouseup', e => sendInput({ type: 'mouseup', ...coords(e), button: 'left' }));
225
+
226
+ document.addEventListener('keydown', e => {
227
+ if (document.activeElement === address) return; // typing in the URL bar
228
+ if (e.key === 'r' && (e.metaKey || e.ctrlKey)) return;
229
+ if (e.key.length === 1) sendInput({ type: 'keydown', text: e.key });
230
+ else if (e.key === 'Enter') sendInput({ type: 'keydown', text: '\r' });
231
+ else if (e.key === 'Backspace') sendInput({ type: 'keydown', text: '\b' });
232
+ });
233
+
234
+ function refreshImage() {
235
+ if (!activeTab) return;
236
+ const src = `/api/screen?tab=${activeTab}&t=${Date.now()}`;
237
+ const tmp = new Image();
238
+ tmp.onload = () => { img.src = src; };
239
+ tmp.src = src;
240
+ }
241
+ setInterval(refreshImage, 1000);
242
+ setInterval(buildTabs, 5000); // pick up new/closed tabs and url changes
243
+
244
+ async function updateStats() {
245
+ try {
246
+ const d = await (await fetch('/api/stats')).json();
247
+ document.getElementById('memVal').innerText = `${d.memUsedGB}/${d.memTotalGB}GB`;
248
+ document.getElementById('memBar').style.width = `${d.memPct}%`;
249
+ document.getElementById('cpuVal').innerText = `${Math.round(d.cpu)}%`;
250
+ const h = Math.floor(d.uptime / 3600), m = Math.floor((d.uptime % 3600) / 60);
251
+ document.getElementById('uptimeVal').innerText = `${h}h ${m}m`;
252
+ } catch (e) { console.error(e); }
253
+ }
254
+ setInterval(updateStats, 2000);
255
+
256
+ buildTabs().then(refreshImage);
257
+ updateStats();
258
+ </script>
259
+ </body>
260
+
261
+ </html>
monitor/main.go ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Chrome live monitor with a built-in navigator.
2
+ //
3
+ // Talks to Chrome's DevTools Protocol on 127.0.0.1:9222 and serves a browser-like
4
+ // UI on :3001:
5
+ // - a live tab strip listing every open Chrome page (known AI services first),
6
+ // - an address bar + back / forward / reload to drive the active tab anywhere,
7
+ // - new-tab / close-tab buttons,
8
+ // - click + type forwarded straight into the active page.
9
+ // The active tab is brought to front before every screenshot so background tabs
10
+ // still render. Tabs are keyed by their live CDP target id (not hardcoded).
11
+ package main
12
+
13
+ import (
14
+ _ "embed"
15
+ "encoding/base64"
16
+ "encoding/json"
17
+ "fmt"
18
+ "io"
19
+ "log"
20
+ "net/http"
21
+ "os"
22
+ "sort"
23
+ "strings"
24
+ "sync"
25
+ "time"
26
+
27
+ "github.com/gorilla/websocket"
28
+ )
29
+
30
+ const cdpBase = "http://127.0.0.1:9222"
31
+
32
+ //go:embed index.html
33
+ var indexHTML []byte
34
+
35
+ // knownTab is one of the AI services we pre-open and label nicely.
36
+ type knownTab struct {
37
+ Title string
38
+ URL string // page to open on startup
39
+ Match string // substring used to find an already-open page target
40
+ }
41
+
42
+ var knownTabs = []knownTab{
43
+ {"ChatGPT", "https://chatgpt.com/", "chatgpt.com"},
44
+ {"Gemini", "https://gemini.google.com/", "gemini.google.com"},
45
+ {"Flow", "https://labs.google/fx/tools/flow", "labs.google"},
46
+ }
47
+
48
+ // cdpTarget mirrors the relevant fields of an entry from GET /json.
49
+ type cdpTarget struct {
50
+ ID string `json:"id"`
51
+ Type string `json:"type"`
52
+ URL string `json:"url"`
53
+ Title string `json:"title"`
54
+ WS string `json:"webSocketDebuggerUrl"`
55
+ }
56
+
57
+ // tabInfo is what the UI consumes from /api/tabs.
58
+ type tabInfo struct {
59
+ ID string `json:"id"`
60
+ Title string `json:"title"`
61
+ URL string `json:"url"`
62
+ }
63
+
64
+ // cdpConn is a serialized request/response wrapper around one DevTools websocket.
65
+ type cdpConn struct {
66
+ c *websocket.Conn
67
+ mu sync.Mutex
68
+ id int
69
+ }
70
+
71
+ func (cc *cdpConn) call(method string, params map[string]any) (json.RawMessage, error) {
72
+ cc.mu.Lock()
73
+ defer cc.mu.Unlock()
74
+ cc.id++
75
+ myID := cc.id
76
+ if err := cc.c.WriteJSON(map[string]any{"id": myID, "method": method, "params": params}); err != nil {
77
+ return nil, err
78
+ }
79
+ cc.c.SetReadDeadline(time.Now().Add(15 * time.Second))
80
+ for {
81
+ _, data, err := cc.c.ReadMessage()
82
+ if err != nil {
83
+ return nil, err
84
+ }
85
+ var m struct {
86
+ ID int `json:"id"`
87
+ Result json.RawMessage `json:"result"`
88
+ Error *struct {
89
+ Message string `json:"message"`
90
+ } `json:"error"`
91
+ }
92
+ if json.Unmarshal(data, &m) != nil {
93
+ continue
94
+ }
95
+ if m.ID == myID { // ignore CDP events (no matching id)
96
+ if m.Error != nil {
97
+ return nil, fmt.Errorf("cdp %s: %s", method, m.Error.Message)
98
+ }
99
+ return m.Result, nil
100
+ }
101
+ }
102
+ }
103
+
104
+ // connections, one persistent websocket per target id.
105
+ var (
106
+ conns = map[string]*cdpConn{}
107
+ connMu sync.Mutex
108
+ )
109
+
110
+ func httpGetJSON(url string, out any) error {
111
+ resp, err := http.Get(url)
112
+ if err != nil {
113
+ return err
114
+ }
115
+ defer resp.Body.Close()
116
+ return json.NewDecoder(resp.Body).Decode(out)
117
+ }
118
+
119
+ func listTargets() ([]cdpTarget, error) {
120
+ var t []cdpTarget
121
+ err := httpGetJSON(cdpBase+"/json", &t)
122
+ return t, err
123
+ }
124
+
125
+ // pageTargets returns the open page targets, known services first then the rest.
126
+ func pageTargets() ([]cdpTarget, error) {
127
+ targets, err := listTargets()
128
+ if err != nil {
129
+ return nil, err
130
+ }
131
+ var pages []cdpTarget
132
+ for _, t := range targets {
133
+ if t.Type != "page" || strings.HasPrefix(t.URL, "devtools://") {
134
+ continue
135
+ }
136
+ pages = append(pages, t)
137
+ }
138
+ rank := func(t cdpTarget) int {
139
+ for i, k := range knownTabs {
140
+ if strings.Contains(t.URL, k.Match) {
141
+ return i
142
+ }
143
+ }
144
+ return len(knownTabs) + 1
145
+ }
146
+ sort.SliceStable(pages, func(i, j int) bool { return rank(pages[i]) < rank(pages[j]) })
147
+ return pages, nil
148
+ }
149
+
150
+ // friendlyTitle prefers a known service label, then the page title, then the URL.
151
+ func friendlyTitle(t cdpTarget) string {
152
+ for _, k := range knownTabs {
153
+ if strings.Contains(t.URL, k.Match) {
154
+ return k.Title
155
+ }
156
+ }
157
+ if t.URL == "" || t.URL == "about:blank" {
158
+ return "New Tab"
159
+ }
160
+ if strings.TrimSpace(t.Title) != "" {
161
+ return t.Title
162
+ }
163
+ return t.URL
164
+ }
165
+
166
+ // browserWS returns the browser-level websocket URL (for Target.* calls).
167
+ func browserWS() (string, error) {
168
+ var v struct {
169
+ WS string `json:"webSocketDebuggerUrl"`
170
+ }
171
+ if err := httpGetJSON(cdpBase+"/json/version", &v); err != nil {
172
+ return "", err
173
+ }
174
+ return v.WS, nil
175
+ }
176
+
177
+ func dial(ws string) (*cdpConn, error) {
178
+ c, _, err := websocket.DefaultDialer.Dial(ws, nil)
179
+ if err != nil {
180
+ return nil, err
181
+ }
182
+ return &cdpConn{c: c}, nil
183
+ }
184
+
185
+ // browserCall runs a single browser-level command (createTarget / closeTarget).
186
+ func browserCall(method string, params map[string]any) (json.RawMessage, error) {
187
+ bws, err := browserWS()
188
+ if err != nil {
189
+ return nil, err
190
+ }
191
+ bc, err := dial(bws)
192
+ if err != nil {
193
+ return nil, err
194
+ }
195
+ defer bc.c.Close()
196
+ return bc.call(method, params)
197
+ }
198
+
199
+ // targetWSByID looks up a page target's websocket URL by its id.
200
+ func targetWSByID(id string) (string, error) {
201
+ targets, err := listTargets()
202
+ if err != nil {
203
+ return "", err
204
+ }
205
+ for _, t := range targets {
206
+ if t.ID == id {
207
+ return t.WS, nil
208
+ }
209
+ }
210
+ return "", fmt.Errorf("no target %q", id)
211
+ }
212
+
213
+ // ensureKnown finds an already-open page for a known service, creating it if absent.
214
+ func ensureKnown(k knownTab) error {
215
+ targets, err := listTargets()
216
+ if err != nil {
217
+ return err
218
+ }
219
+ for _, tg := range targets {
220
+ if tg.Type == "page" && strings.Contains(tg.URL, k.Match) {
221
+ return nil
222
+ }
223
+ }
224
+ _, err = browserCall("Target.createTarget", map[string]any{"url": k.URL})
225
+ return err
226
+ }
227
+
228
+ // conn returns a live cdpConn for the given target id, (re)dialing as needed.
229
+ func conn(id string) (*cdpConn, error) {
230
+ connMu.Lock()
231
+ defer connMu.Unlock()
232
+ if cc, ok := conns[id]; ok {
233
+ if _, err := cc.call("Target.getTargetInfo", nil); err == nil { // cheap liveness probe
234
+ return cc, nil
235
+ }
236
+ cc.c.Close()
237
+ delete(conns, id)
238
+ }
239
+ ws, err := targetWSByID(id)
240
+ if err != nil {
241
+ return nil, err
242
+ }
243
+ cc, err := dial(ws)
244
+ if err != nil {
245
+ return nil, err
246
+ }
247
+ conns[id] = cc
248
+ return cc, nil
249
+ }
250
+
251
+ // connForReq resolves the ?tab= target, writing an error response on failure.
252
+ func connForReq(w http.ResponseWriter, r *http.Request) (*cdpConn, bool) {
253
+ id := r.URL.Query().Get("tab")
254
+ if id == "" {
255
+ http.Error(w, "missing tab", http.StatusBadRequest)
256
+ return nil, false
257
+ }
258
+ cc, err := conn(id)
259
+ if err != nil {
260
+ http.Error(w, err.Error(), http.StatusBadGateway)
261
+ return nil, false
262
+ }
263
+ return cc, true
264
+ }
265
+
266
+ func screenHandler(w http.ResponseWriter, r *http.Request) {
267
+ cc, ok := connForReq(w, r)
268
+ if !ok {
269
+ return
270
+ }
271
+ cc.call("Page.bringToFront", nil) // ensure background tab renders
272
+ res, err := cc.call("Page.captureScreenshot", map[string]any{"format": "png"})
273
+ if err != nil {
274
+ http.Error(w, err.Error(), http.StatusBadGateway)
275
+ return
276
+ }
277
+ var out struct {
278
+ Data string `json:"data"`
279
+ }
280
+ if json.Unmarshal(res, &out) != nil || out.Data == "" {
281
+ http.Error(w, "no screenshot", http.StatusBadGateway)
282
+ return
283
+ }
284
+ png, err := base64.StdEncoding.DecodeString(out.Data)
285
+ if err != nil {
286
+ http.Error(w, err.Error(), http.StatusBadGateway)
287
+ return
288
+ }
289
+ w.Header().Set("Content-Type", "image/png")
290
+ w.Header().Set("Cache-Control", "no-store")
291
+ w.Write(png)
292
+ }
293
+
294
+ func inputHandler(w http.ResponseWriter, r *http.Request) {
295
+ var in struct {
296
+ Type string `json:"type"`
297
+ X float64 `json:"x"`
298
+ Y float64 `json:"y"`
299
+ Button string `json:"button"`
300
+ Text string `json:"text"`
301
+ }
302
+ if json.NewDecoder(r.Body).Decode(&in) != nil {
303
+ http.Error(w, "bad json", http.StatusBadRequest)
304
+ return
305
+ }
306
+ cc, ok := connForReq(w, r)
307
+ if !ok {
308
+ return
309
+ }
310
+ switch in.Type {
311
+ case "mousedown", "mouseup":
312
+ mt := "mousePressed"
313
+ if in.Type == "mouseup" {
314
+ mt = "mouseReleased"
315
+ }
316
+ btn := in.Button
317
+ if btn == "" {
318
+ btn = "left"
319
+ }
320
+ cc.call("Input.dispatchMouseEvent", map[string]any{
321
+ "type": mt, "x": in.X, "y": in.Y, "button": btn, "clickCount": 1,
322
+ })
323
+ case "keydown":
324
+ if in.Text == "\r" || in.Text == "\n" {
325
+ cc.call("Input.dispatchKeyEvent", map[string]any{
326
+ "type": "keyDown", "key": "Enter", "windowsVirtualKeyCode": 13, "text": "\r",
327
+ })
328
+ cc.call("Input.dispatchKeyEvent", map[string]any{"type": "keyUp", "key": "Enter", "windowsVirtualKeyCode": 13})
329
+ } else if in.Text == "\b" {
330
+ cc.call("Input.dispatchKeyEvent", map[string]any{"type": "keyDown", "key": "Backspace", "windowsVirtualKeyCode": 8})
331
+ cc.call("Input.dispatchKeyEvent", map[string]any{"type": "keyUp", "key": "Backspace", "windowsVirtualKeyCode": 8})
332
+ } else if in.Text != "" {
333
+ cc.call("Input.insertText", map[string]any{"text": in.Text})
334
+ }
335
+ }
336
+ w.WriteHeader(http.StatusNoContent)
337
+ }
338
+
339
+ // normalizeURL prepends https:// when the user typed a bare host.
340
+ func normalizeURL(u string) string {
341
+ u = strings.TrimSpace(u)
342
+ if u == "" {
343
+ return ""
344
+ }
345
+ if !strings.Contains(u, "://") {
346
+ u = "https://" + u
347
+ }
348
+ return u
349
+ }
350
+
351
+ func navigateHandler(w http.ResponseWriter, r *http.Request) {
352
+ var in struct {
353
+ URL string `json:"url"`
354
+ }
355
+ if json.NewDecoder(r.Body).Decode(&in) != nil {
356
+ http.Error(w, "bad json", http.StatusBadRequest)
357
+ return
358
+ }
359
+ url := normalizeURL(in.URL)
360
+ if url == "" {
361
+ http.Error(w, "empty url", http.StatusBadRequest)
362
+ return
363
+ }
364
+ cc, ok := connForReq(w, r)
365
+ if !ok {
366
+ return
367
+ }
368
+ if _, err := cc.call("Page.navigate", map[string]any{"url": url}); err != nil {
369
+ http.Error(w, err.Error(), http.StatusBadGateway)
370
+ return
371
+ }
372
+ w.WriteHeader(http.StatusNoContent)
373
+ }
374
+
375
+ func reloadHandler(w http.ResponseWriter, r *http.Request) {
376
+ cc, ok := connForReq(w, r)
377
+ if !ok {
378
+ return
379
+ }
380
+ cc.call("Page.reload", nil)
381
+ w.WriteHeader(http.StatusNoContent)
382
+ }
383
+
384
+ // historyStep moves the active tab back (delta -1) or forward (delta +1).
385
+ func historyStep(cc *cdpConn, delta int) error {
386
+ res, err := cc.call("Page.getNavigationHistory", nil)
387
+ if err != nil {
388
+ return err
389
+ }
390
+ var h struct {
391
+ CurrentIndex int `json:"currentIndex"`
392
+ Entries []struct {
393
+ ID int `json:"id"`
394
+ } `json:"entries"`
395
+ }
396
+ if err := json.Unmarshal(res, &h); err != nil {
397
+ return err
398
+ }
399
+ target := h.CurrentIndex + delta
400
+ if target < 0 || target >= len(h.Entries) {
401
+ return nil // nothing to do at the ends
402
+ }
403
+ _, err = cc.call("Page.navigateToHistoryEntry", map[string]any{"entryId": h.Entries[target].ID})
404
+ return err
405
+ }
406
+
407
+ func historyHandler(delta int) http.HandlerFunc {
408
+ return func(w http.ResponseWriter, r *http.Request) {
409
+ cc, ok := connForReq(w, r)
410
+ if !ok {
411
+ return
412
+ }
413
+ if err := historyStep(cc, delta); err != nil {
414
+ http.Error(w, err.Error(), http.StatusBadGateway)
415
+ return
416
+ }
417
+ w.WriteHeader(http.StatusNoContent)
418
+ }
419
+ }
420
+
421
+ func newtabHandler(w http.ResponseWriter, r *http.Request) {
422
+ var in struct {
423
+ URL string `json:"url"`
424
+ }
425
+ json.NewDecoder(r.Body).Decode(&in)
426
+ url := normalizeURL(in.URL)
427
+ if url == "" {
428
+ url = "about:blank"
429
+ }
430
+ res, err := browserCall("Target.createTarget", map[string]any{"url": url})
431
+ if err != nil {
432
+ http.Error(w, err.Error(), http.StatusBadGateway)
433
+ return
434
+ }
435
+ var out struct {
436
+ TargetID string `json:"targetId"`
437
+ }
438
+ json.Unmarshal(res, &out)
439
+ w.Header().Set("Content-Type", "application/json")
440
+ json.NewEncoder(w).Encode(map[string]string{"id": out.TargetID})
441
+ }
442
+
443
+ func closetabHandler(w http.ResponseWriter, r *http.Request) {
444
+ id := r.URL.Query().Get("tab")
445
+ if id == "" {
446
+ http.Error(w, "missing tab", http.StatusBadRequest)
447
+ return
448
+ }
449
+ if _, err := browserCall("Target.closeTarget", map[string]any{"targetId": id}); err != nil {
450
+ http.Error(w, err.Error(), http.StatusBadGateway)
451
+ return
452
+ }
453
+ connMu.Lock()
454
+ if cc, ok := conns[id]; ok {
455
+ cc.c.Close()
456
+ delete(conns, id)
457
+ }
458
+ connMu.Unlock()
459
+ w.WriteHeader(http.StatusNoContent)
460
+ }
461
+
462
+ func tabsHandler(w http.ResponseWriter, r *http.Request) {
463
+ pages, err := pageTargets()
464
+ if err != nil {
465
+ http.Error(w, err.Error(), http.StatusBadGateway)
466
+ return
467
+ }
468
+ out := make([]tabInfo, 0, len(pages))
469
+ for _, t := range pages {
470
+ out = append(out, tabInfo{ID: t.ID, Title: friendlyTitle(t), URL: t.URL})
471
+ }
472
+ w.Header().Set("Content-Type", "application/json")
473
+ json.NewEncoder(w).Encode(out)
474
+ }
475
+
476
+ func chromeLogHandler(w http.ResponseWriter, r *http.Request) {
477
+ f, err := os.Open("/home/chrome/chrome.log")
478
+ if err != nil {
479
+ http.Error(w, "no log", http.StatusNotFound)
480
+ return
481
+ }
482
+ defer f.Close()
483
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
484
+ io.Copy(w, f)
485
+ }
486
+
487
+ func statsHandler(w http.ResponseWriter, r *http.Request) {
488
+ mem := readMeminfo()
489
+ w.Header().Set("Content-Type", "application/json")
490
+ json.NewEncoder(w).Encode(map[string]any{
491
+ "memUsedGB": mem.usedGB,
492
+ "memTotalGB": mem.totalGB,
493
+ "memPct": mem.pct,
494
+ "cpu": readCPUPercent(),
495
+ "uptime": readUptime(),
496
+ })
497
+ }
498
+
499
+ func main() {
500
+ // Pre-open the known AI services in the background so they're ready by first view.
501
+ go func() {
502
+ for i := 0; i < 60; i++ {
503
+ ok := true
504
+ for _, k := range knownTabs {
505
+ if err := ensureKnown(k); err != nil {
506
+ ok = false
507
+ }
508
+ }
509
+ if ok {
510
+ log.Println("all known tabs ready")
511
+ return
512
+ }
513
+ time.Sleep(2 * time.Second)
514
+ }
515
+ }()
516
+
517
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
518
+ if r.URL.Path != "/" {
519
+ http.NotFound(w, r)
520
+ return
521
+ }
522
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
523
+ w.Write(indexHTML)
524
+ })
525
+ http.HandleFunc("/api/tabs", tabsHandler)
526
+ http.HandleFunc("/api/screen", screenHandler)
527
+ http.HandleFunc("/api/input", inputHandler)
528
+ http.HandleFunc("/api/navigate", navigateHandler)
529
+ http.HandleFunc("/api/reload", reloadHandler)
530
+ http.HandleFunc("/api/back", historyHandler(-1))
531
+ http.HandleFunc("/api/forward", historyHandler(+1))
532
+ http.HandleFunc("/api/newtab", newtabHandler)
533
+ http.HandleFunc("/api/closetab", closetabHandler)
534
+ http.HandleFunc("/api/stats", statsHandler)
535
+ http.HandleFunc("/chrome.log", chromeLogHandler)
536
+
537
+ addr := ":3001"
538
+ if p := os.Getenv("MONITOR_PORT"); p != "" {
539
+ addr = ":" + p
540
+ }
541
+ log.Printf("navigator monitor listening on %s", addr)
542
+ log.Fatal(http.ListenAndServe(addr, nil))
543
+ }
monitor/stats.go ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "os"
6
+ "strconv"
7
+ "strings"
8
+ "time"
9
+ )
10
+
11
+ type memInfo struct {
12
+ usedGB float64
13
+ totalGB float64
14
+ pct float64
15
+ }
16
+
17
+ func readMeminfo() memInfo {
18
+ f, err := os.Open("/proc/meminfo")
19
+ if err != nil {
20
+ return memInfo{}
21
+ }
22
+ defer f.Close()
23
+ var totalKB, availKB float64
24
+ sc := bufio.NewScanner(f)
25
+ for sc.Scan() {
26
+ fields := strings.Fields(sc.Text())
27
+ if len(fields) < 2 {
28
+ continue
29
+ }
30
+ v, _ := strconv.ParseFloat(fields[1], 64)
31
+ switch fields[0] {
32
+ case "MemTotal:":
33
+ totalKB = v
34
+ case "MemAvailable:":
35
+ availKB = v
36
+ }
37
+ }
38
+ if totalKB == 0 {
39
+ return memInfo{}
40
+ }
41
+ usedKB := totalKB - availKB
42
+ return memInfo{
43
+ usedGB: round1(usedKB / 1024 / 1024),
44
+ totalGB: round1(totalKB / 1024 / 1024),
45
+ pct: round1(usedKB / totalKB * 100),
46
+ }
47
+ }
48
+
49
+ func round1(f float64) float64 {
50
+ return float64(int(f*10+0.5)) / 10
51
+ }
52
+
53
+ // readCPUPercent samples /proc/stat over a short window.
54
+ func readCPUPercent() float64 {
55
+ idle1, total1 := cpuSample()
56
+ time.Sleep(200 * time.Millisecond)
57
+ idle2, total2 := cpuSample()
58
+ dt := total2 - total1
59
+ if dt == 0 {
60
+ return 0
61
+ }
62
+ return round1((1 - (idle2-idle1)/dt) * 100)
63
+ }
64
+
65
+ func cpuSample() (idle, total float64) {
66
+ f, err := os.Open("/proc/stat")
67
+ if err != nil {
68
+ return 0, 0
69
+ }
70
+ defer f.Close()
71
+ sc := bufio.NewScanner(f)
72
+ for sc.Scan() {
73
+ fields := strings.Fields(sc.Text())
74
+ if len(fields) > 0 && fields[0] == "cpu" {
75
+ for i, v := range fields[1:] {
76
+ n, _ := strconv.ParseFloat(v, 64)
77
+ total += n
78
+ if i == 3 { // idle
79
+ idle = n
80
+ }
81
+ }
82
+ return idle, total
83
+ }
84
+ }
85
+ return 0, 0
86
+ }
87
+
88
+ func readUptime() int {
89
+ b, err := os.ReadFile("/proc/uptime")
90
+ if err != nil {
91
+ return 0
92
+ }
93
+ fields := strings.Fields(string(b))
94
+ if len(fields) == 0 {
95
+ return 0
96
+ }
97
+ v, _ := strconv.ParseFloat(fields[0], 64)
98
+ return int(v)
99
+ }
start_hf.sh CHANGED
@@ -85,94 +85,15 @@ with open(pref_path, 'w') as f:
85
 
86
 
87
  # 5. Start Go Monitor Server
88
- echo "🚀 Starting Go Monitor Server..."
89
  touch /home/chrome/chrome.log
90
- ln -sf /home/chrome/chrome.log /opt/monitor-server/public/chrome.log
91
- cd /opt/monitor-server
92
- ./monitor-server > /home/chrome/monitor.log 2>&1 &
93
 
94
  # 6. Start CDP Proxy
95
  echo "🔌 Starting CDP Proxy..."
96
  socat TCP-LISTEN:9223,fork,bind=0.0.0.0 TCP:127.0.0.1:9222 &
97
 
98
- # 7. Start Auto-Navigation to chrome://extensions/ in background
99
- cat << 'EOF' > /tmp/navigate.py
100
- import urllib.request
101
- import json
102
- import socket
103
- import urllib.parse
104
- import time
105
-
106
- # Wait for Chrome to start and expose a page target
107
- page_target = None
108
- for _ in range(30):
109
- try:
110
- res = urllib.request.urlopen("http://127.0.0.1:9222/json")
111
- targets = json.loads(res.read().decode())
112
- pages = [t for t in targets if t["type"] == "page"]
113
- if pages:
114
- page_target = pages[0]
115
- break
116
- except:
117
- pass
118
- time.sleep(1)
119
-
120
- if not page_target:
121
- print("Failed to get targets: No active page found")
122
- exit(1)
123
-
124
- ws_url = page_target['webSocketDebuggerUrl']
125
-
126
- parsed = urllib.parse.urlparse(ws_url)
127
- host = '127.0.0.1'
128
- port = parsed.port or 9222
129
- path = parsed.path
130
-
131
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
132
- s.connect((host, port))
133
-
134
- handshake = (
135
- f"GET {path} HTTP/1.1\r\n"
136
- f"Host: {host}:{port}\r\n"
137
- "Upgrade: websocket\r\n"
138
- "Connection: Upgrade\r\n"
139
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
140
- "Sec-WebSocket-Version: 13\r\n\r\n"
141
- )
142
- s.sendall(handshake.encode())
143
- resp = s.recv(1024)
144
-
145
- payload = json.dumps({
146
- "id": 1,
147
- "method": "Page.navigate",
148
- "params": {"url": "chrome://extensions/"}
149
- })
150
-
151
- payload_bytes = payload.encode()
152
- length = len(payload_bytes)
153
-
154
- frame = bytearray([0x81])
155
- if length <= 125:
156
- frame.append(length | 0x80)
157
- elif length <= 65535:
158
- frame.append(126 | 0x80)
159
- frame.extend(length.to_bytes(2, byteorder='big'))
160
- else:
161
- frame.append(127 | 0x80)
162
- frame.extend(length.to_bytes(8, byteorder='big'))
163
-
164
- mask_key = b'\x11\x22\x33\x44'
165
- frame.extend(mask_key)
166
- masked_payload = bytearray(b ^ mask_key[i % 4] for i, b in enumerate(payload_bytes))
167
- frame.extend(masked_payload)
168
-
169
- s.sendall(frame)
170
- s.close()
171
- print("Successfully navigated to chrome://extensions/")
172
- EOF
173
- python3 -u /tmp/navigate.py >> /home/chrome/chrome.log 2>&1 &
174
-
175
- # 8. Launch Chrome Headless
176
  echo "✨ Launching Chrome Headless..."
177
  /opt/chrome-linux64/chrome \
178
  --headless=new \
@@ -232,4 +153,4 @@ echo "✨ Launching Chrome Headless..."
232
  --disable-site-isolation-trials \
233
  --js-flags="--max-old-space-size=256" \
234
  --metrics-recording-only \
235
- 'chrome://extensions/' >> /home/chrome/chrome.log 2>&1
 
85
 
86
 
87
  # 5. Start Go Monitor Server
88
+ echo "🚀 Starting 3-tab Monitor Server..."
89
  touch /home/chrome/chrome.log
90
+ /opt/monitor3/monitor > /home/chrome/monitor.log 2>&1 &
 
 
91
 
92
  # 6. Start CDP Proxy
93
  echo "🔌 Starting CDP Proxy..."
94
  socat TCP-LISTEN:9223,fork,bind=0.0.0.0 TCP:127.0.0.1:9222 &
95
 
96
+ # 7. Launch Chrome Headless
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  echo "✨ Launching Chrome Headless..."
98
  /opt/chrome-linux64/chrome \
99
  --headless=new \
 
153
  --disable-site-isolation-trials \
154
  --js-flags="--max-old-space-size=256" \
155
  --metrics-recording-only \
156
+ 'about:blank' >> /home/chrome/chrome.log 2>&1