开源项目
Ship multi-language audio in HLS: author the manifest, wire the hls.js switcher
📦 Code: github.com/USER/hls-multi-audio - replace before publishing TL;DR We'll add a working language picker to an HLS player. The hard part isn't the dropdown, it's the manifest. We'll author alternate audio with EXT-X-MEDIA audio groups, package it correctly, debug the classic "zero audio tracks" bug, and wire a switcher on hls.js v1.7 . Adaptive video, captions, the whole pipeline already works. Now someone wants an English/Spanish audio toggle. In HLS, "which audio can the viewer pick" is decided at packaging time and written into the master playlist. The player just displays it. Let's build it in that order. 1. Understand the structure (audio groups) HLS decouples video variants from audio renditions: Each audio rendition is an #EXT-X-MEDIA:TYPE=AUDIO entry pointing at its own media playlist. Renditions are bundled into a named audio group via GROUP-ID . Each video variant ( #EXT-X-STREAM-INF ) references a group with AUDIO="..." . A correct master playlist: #EXTM3U #EXT-X-VERSION:6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="Espanol",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio/es.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=2128000,CODECS="avc1.640028,mp4a.40.2",AUDIO="aud" video/720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1128000,CODECS="avc1.640020,mp4a.40.2",AUDIO="aud" video/480p.m3u8 Every attribute earns its place: LANGUAGE - BCP-47 code, used for the label. DEFAULT - plays when the viewer has no preference. AUTOSELECT - may be auto-picked from the OS language. CHANNELS - needed so the player can reason about stereo vs surround. BANDWIDTH on each video variant must include the audio group's bitrate , or your ABR logic works from a wrong total. 2. Author the renditions with FFmpeg Extract/encode each language's audio, then package. First, encode video-only and audio-only renditions: # video only (no audio), two ladder rungs
AI 资讯
From Angular.js to Fine-Grained Reactivity: Part 2 — The JS Proxy Runtime
In the first article of this series, we saw how a custom build-time compiler can transform a legacy Angular.js template into raw, optimized JavaScript. To recap, starting from this template: <!-- simple.html --> <p> Hello {{ name }}! </p> Our Go compiler generates the following JavaScript module: // simple.js export function template () { const p_0 = document . createElement ( " p " ); const text_1 = document . createTextNode ( "" ); p_0 . append ( text_1 ); return { mount ( container ) { container . append ( p_0 ); }, update ( change ) { if ( " name " in change ) { text_1 . data = " Hello " + change . name + " ! " ; } } } } This is incredibly clean. By running template() , we get an object with mount and update methods. Using mount is fully intuitive: we pass a reference to a DOM element, and it injects our empty paragraph ( p_0 ) into it: import { template } from ' ./simple.js ' ; const { mount , update } = template (); const container = document . getElementById ( ' view-container ' ); mount ( container ); // The DOM now contains: <p></p> (waiting for data) However, the paragraph remains empty until we call update with a change object like this: let changes = { name : " Mario " , }; update ( changes ); // The DOM surgically updates to: <p>Hello Mario!</p> But who is responsible for tracking changes in our application state, building this changes object, and calling update ? The answer lies in marrying the legacy Angular.js $scope with the modern JavaScript Proxy API . The Legacy State Pattern In a traditional Angular.js application, developers mutate the state directly inside a controller by assigning properties to the $scope object: // simple-controller.js export function SimpleController ( $scope ) { $scope . name = " Mario " ; } To bridge the gap between this legacy controller and our new build-time template, we need a way to automatically capture the assignment $scope.name = "Mario" and translate it into a structured update: let changes = { name : " Mario " }
AI 资讯
OrinIDE v1.0.9 — local AI, an Agentic dev squad, and a bug fix I owe you an explanation for
Hey devs 👋 OrinIDE is an AI-powered code editor that runs entirely in your browser — no...
AI 资讯
10 Cool CodePen Demos (June 2026)
Sand bottle - WebGPU Remember those bottles filled with colored sand that you can find in many souvenir stores? Liam Egan created a digital version using JavaScript WebGPU API. Click to drop sand, use the arrows to tilt and shake the bottles, and relax while enjoying this sandy demo. Button State Builder Margarita shared this button builder that allows to customize a control with icons, text, color, shape... even all the behavior in the different states of the button. Then you can easily get the HTML, CSS, and JS code to put it on any website. Pretty cool. WebGL Switch Button In this demo, the whole page turns into a giant three-dimensional toggle switch that can be activated clicking anywhere on the viewport. Explore the component: mouse over to make the component tilt or scroll to zoom in and out. A nice job by Toc. Animated radial gradient mask over text This demo is exactly what the title says: a radial gradient applied as a mask to some text. Cassidy aligned it perfectly with the hole in the O from "Hello" that makes this effect chef's kiss . You will need to uncomment the animation property in CSS to see the demo in action. 221. ycw always creates impressive and original content. And this demo delivers. It's not only the effect in itself, but the use of light and shadows, and the perfecto choice of color that adds a timeless atmosphere. Beautiful. vRLbdoSAIsoSQvisac Mustafa Enes created different versions of this idea over the past month, all of them are great, but I picked this one as it is more interactive. Click on the screen to regenerate the pattern and move the mouse around to animate the colors. I don't know why, but there's a feeling of peace and joy while doing it. beach sunset I saw several demos by Vivi Tseng that caught my attention this month. Really enjoyed the general minimalistic style they all had and finally picked this animated one because it feels simple and pure, almost like a drawing a child would do during a vacation. I really enjoyed it
AI 资讯
At Last, I clasp: Escaping the G's Apps Script Copy-Paste Gauntlet
Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is...
AI 资讯
Your fetch() Is Still Running After the User Left
When you fire a fetch() and the component that triggered it unmounts, the request keeps going. The server still processes it. When the response arrives, it calls back into whatever JavaScript it finds — a stale closure, a dead state setter, a global store that has already moved on. React's "Can't perform a state update on an unmounted component" warning is the polite version of this. The silent version is worse: results from an old query overwriting the current UI. These aren't mysterious race conditions. They're the predictable result of starting async work and never telling it to stop. The race condition hiding in every search box The search input is the clearest example. The user types "reac", your debounce fires a request. Before it lands, they finish typing "react" and you fire another. Two requests, in flight at the same time, and no guarantee about which one finishes first. If the "reac" request happens to be slower — network jitter, a cache miss, a heavier result set — it will land after "react" and overwrite the correct results with the wrong ones. The bug reproduces maybe one time in twenty on a local dev server, and consistently in production on a slow connection. The fix isn't smarter debouncing. It's cancelling the previous request when a new one starts. AbortController in plain terms AbortController is a browser-native API for cancelling async work. You create a controller, pass its signal to fetch() , and call controller.abort() to cancel. If the response hasn't arrived yet, the fetch promise rejects with an AbortError . const controller = new AbortController (); fetch ( ' /api/search?q=react ' , { signal : controller . signal }) . then ( res => res . json ()) . then ( data => setResults ( data )) . catch ( err => { if ( err . name === ' AbortError ' ) return ; // expected — not a real error setError ( err ); }); // Somewhere else, when we no longer need this request: controller . abort (); Two things to internalize: signal is how the controller knows
AI 资讯
Why v7 UUIDs beat v4 for database keys (and how to hand-roll both)
I build one small browser tool a day and write down what I learned. Day 25 was a UUID generator. What started as "make some random IDs" turned into a proper look at how the bits are laid out, and why the newer v7 format is quietly the better default for a primary key. Live tool: https://dev48v.infy.uk/solve/day25-uuid.html A UUID is just 16 bytes with a few fixed bits A UUID is a 128-bit number, written as 32 hex digits grouped 8-4-4-4-12 . That is about 3.4x10^38 possible values, which is the whole point: any machine can pick one and trust it will not clash with any other UUID minted anywhere, ever. It carries no meaning — it is an identifier, not data. The reason UUIDs exist at all is coordination. The classic database ID is 1, 2, 3... from a central counter, and that works great until you have more than one writer. Two servers, an offline mobile app, or a sharded database cannot all ask one counter for the next number without a round-trip and a lock. UUIDs sidestep that entirely: each node generates its own IDs locally, with zero coordination, and they still do not collide. A client can even create the ID before the row ever reaches the server. Version 4: 122 random bits v4 is the one most people mean by "UUID". Fill all 16 bytes with cryptographic randomness, then overwrite two small fields so tools can recognise the format: const b = new Uint8Array ( 16 ); crypto . getRandomValues ( b ); // never Math.random() b [ 6 ] = ( b [ 6 ] & 0x0f ) | 0x40 ; // version 4 b [ 8 ] = ( b [ 8 ] & 0x3f ) | 0x80 ; // variant 10xx Two things get pinned. The high nibble of byte 6 becomes 4 — that is the digit right after the second hyphen, and it is how any parser knows the scheme. The top two bits of byte 8 become 10 , which is why the 17th hex digit of almost every UUID you see is 8 , 9 , a or b . Everything else stays random: 122 bits of it. Is "random and never collides" a contradiction? The birthday paradox says collisions become likely around the square root of the space, w
AI 资讯
How to Build an Unblockable AI Agent for Browser Automation with Node.js, Bright Data, Gemini, and Playwright
In this full guide, you’ll learn: 📛 Why most AI browser agents fail on modern websites. 🧱 How browser fingerprinting and anti-bot systems work. ⛑️ How to build an AI browser agent using JavaScript (Node.js) that combines Gemini, Playwright , and Bright Data to browse real websites, extract live data, analyze, reason, and generate reports locally without maintaining fragile anti-bot infrastructure ourselves that breaks 5 days later. 🗃️ How to setup Bright Data production-ready browser sessions for AI agent automation without user’s assistance manually. 🪁Introduction Building unrestricted anonymous browser automation has developed far beyond writing Playwright scripts that click buttons and scrape HTML. Modern websites actively detect automated traffic using browser fingerprints , TLS signatures , IP reputation, and behavioral analysis, making reliable automation significantly more challenging than it was just a few years ago. Modern AI browser agents don’t usually fail because they’re arbitrary. Their reasoning, prompts, and planning loops are often sophisticated. The execution layer underneath is fragile. Most tutorials show how to connect an LLM to a browser, execute a few Playwright commands , and declare you’ve built an autonomous agent. await page . goto ( url ) await page . click ( selector ) await page . type ( selector , text ) In reality, you’ve ONLY automated a browser. Commercial sites don’t gauge how intelligent your agent is. They judge whether they believe your browser is genuine. Before a page even finishes loading, they inspect what your browser actually is: the TLS handshake , IP reputation, browser fingerprints, canvas and WebGL fingerprints , cookies, device characteristics, and even the rhythm of your connection. Dozens of signals are examined in the time it takes the page to start loading. If those signals don’t look authentic, your agent rarely reaches the real application. Instead, it encounters CAPTCHA challenges, verification pages, silent re
AI 资讯
Building a real-time gold & FX price ticker with WebSocket (Socket.IO)
If you build apps for jewelers, fintech dashboards, or e-commerce price automation, you eventually need one thing: reliable, low-latency gold and currency prices . Scraping fragile sources breaks constantly. A dedicated price API solves this. In this post I'll show how to consume real-time gold (gram, quarter, coin) and FX rates over both REST and WebSocket (Socket.IO) using the Hasfiyat Gold & Currency API . Why a price API instead of scraping? Stability — a documented contract instead of HTML that changes without notice. Low latency — prices are pushed as the market moves, not on a slow cron. Multiple sources with failover — if one provider drops, the feed keeps flowing. 1. Polling with REST The simplest integration: request the prices you need with your API key. curl -X GET \ 'https://api.hasfiyat.com/api/prices?symbols=HAS,GRAM,CEYREK' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Accept: application/json' // Node.js const res = await fetch ( " https://api.hasfiyat.com/api/prices?symbols=HAS,GRAM,CEYREK " , { headers : { Authorization : " Bearer YOUR_API_KEY " } } ); const data = await res . json (); console . log ( data ); REST is ideal for periodic reporting, server-side jobs, and updating e-commerce product prices. 2. Live updates with Socket.IO For price screens, signage, and mobile apps where every tick matters, keep a connection open and let the server push changes: import { io } from " socket.io-client " ; const socket = io ( " https://api.hasfiyat.com " , { auth : { token : " YOUR_API_KEY " } }); socket . on ( " gold_prices " , ( data ) => { // { symbol: "HAS", type: "Has Altın", buy: 2450.85, sell: 2455.10, timestamp: "14:32:01.045" } console . log ( data ); }); No polling, no hammering the server — each market move arrives instantly. 3. A minimal live ticker in the browser <div id= "gold" ></div> <script src= "https://cdn.socket.io/4.7.5/socket.io.min.js" ></script> <script> const socket = io ( " https://api.hasfiyat.com " , { auth : { token : " YOUR
AI 资讯
Stop Creating a React Project Just to Preview a JSX File
If you're using AI coding assistants like ChatGPT, Claude, Cursor, or Lovable, you've probably accumulated dozens of JSX components. Generating them is incredibly fast. Previewing them? Not so much. The Typical Workflow Every time I received a JSX component, I found myself repeating the same process. Create a React project (or open an existing one) Copy the JSX file Install dependencies Fix missing imports Run the development server Wait for everything to compile All of that... just to see one component. It felt like unnecessary overhead. There Had to Be a Better Way I asked myself a simple question: Why can't I just double-click a JSX file and preview it? We can instantly open images, PDFs, videos, and text files. Why should JSX files require an entire development environment? That's what inspired me to build PreviewKit . What is PreviewKit ? PreviewKit is a lightweight Windows application that lets you preview frontend components instantly. Supported file types include: ✅ JSX ✅ Vue ✅ HTML No project setup. No dependency installation. No terminal commands. Just open the file and see the result. Why I Built It AI has dramatically changed frontend development. We're no longer spending most of our time writing components—we're reviewing, comparing, and refining them. That means fast visual feedback is more important than ever. I wanted a tool that removed the repetitive setup process so I could focus on building better interfaces instead of preparing a preview environment. Who Is It For? PreviewKit is useful if you: Build React applications Work with Vue components Test standalone HTML files Generate UI with AI tools Review components from teammates Prototype interfaces quickly If opening frontend files feels slower than it should, PreviewKit was built for you. The Goal Isn't to Replace Your Framework You'll still use React. You'll still use Vue. You'll still use Vite or Next.js. PreviewKit isn't trying to replace your existing workflow. It simply removes one frustrat
开发者
"Four Remote Job Boards Have Free Public APIs. Here Is One Schema for All of Them"
If you want remote job data, you do not need to scrape HTML or sign up for anything. Four of the bigger remote job boards publish keyless public feeds. The catch is that they all speak different dialects, so the real work is normalization. Here are the endpoints and the traps. The four feeds RemoteOK returns its whole current board as one JSON array: GET https://remoteok.com/api The first element is a legal notice, not a job: they ask for a link back with attribution as a condition of using the feed. Skip element zero, and honor the attribution if you republish. Jobs carry salary_min and salary_max as numbers, tags, and ISO dates. Remotive has the friendliest API of the four, including server side search: GET https://remotive.com/api/remote-jobs?search=python&limit=100 Salary here is free text ( "$120k - $160k" ), so do not expect numbers. Attribution with a link back is required here too. WeWorkRemotely publishes RSS: GET https://weworkremotely.com/remote-jobs.rss Two quirks: the company name is not a field, it is baked into the title as Company: Role , so split on the first colon. And useful data hides in nonstandard tags like <region> , <skills> , and <category> that generic RSS parsers drop on the floor. Himalayas has a proper paginated API with a surprisingly deep catalog (100k+ listings): GET https://himalayas.app/jobs/api?limit=100&offset=0 It gives structured minSalary / maxSalary with a currency and period, seniority arrays, location restrictions, and even timezone restrictions as UTC offsets. Dates are epoch seconds, not ISO strings. The normalization layer The row schema that survived contact with all four sources: { "source" : "Remotive" , "title" : "Senior Backend Engineer" , "company" : "Acme Corp" , "tags" : [ "python" , "aws" ], "salaryMin" : null , "salaryMax" : null , "salaryText" : "$120k - $160k" , "location" : "Worldwide" , "postedAt" : "2026-07-03T20:01:13.000Z" , "applyUrl" : "https://..." } Rules that mattered in practice: Keep both salary sh
开源项目
🔥 spicetify / cli - Command-line tool to customize Spotify client. Supports Wind
GitHub热门项目 | Command-line tool to customize Spotify client. Supports Windows, macOS, and Linux. | Stars: 23,582 | 28 stars today | 语言: JavaScript
AI 资讯
wa.me/username doesn't work yet — I verified it two ways
wa.me/username doesn't work yet — I verified it two ways, here's what to use instead If you've tried to build a "share my WhatsApp" link using a @username instead of a phone number, you've probably assumed wa.me/username (or wa.me/u/username ) works the same way wa.me/15551234567 does. It doesn't — at least not yet, as of writing this. I wanted a definitive answer instead of trusting blog posts or AI chatbot answers (more on that below), so I tested it two independent ways. Test 1: server response curl -I https://wa.me/u/some_real_reserved_username Every username path I tried — including a certified-real, currently-reserved username — 302-redirects to: api.whatsapp.com/resolve/?deeplink=...¬_found=1 Compare that to the phone-number path, which redirects to: api.whatsapp.com/send/?phone=...&type=phone_number Different resolver, different outcome. The server-side route for usernames exists, but every lookup currently resolves as "not found" — even for real, live usernames. Test 2: real device Server response alone doesn't rule out Universal Links / App Links intercepting the URL client-side before it ever hits a server — curl can't see that. So I also opened all three link variants ( wa.me/username , wa.me/u/username , and a redirect through my own domain) on a real phone with WhatsApp installed. None of them opened a chat. Why this matters if you're building anything around WhatsApp usernames WhatsApp has rolled out @username handles as a real, user-facing feature — but it hasn't published a public deep-link spec for opening a chat from one, the way it has for phone numbers for years. If you're building a tool, a profile page, a business card generator, anything that assumes wa.me/username "just works," it doesn't, for anyone. One more data point: I asked Meta AI directly about this, with the counter-evidence above in hand. It kept asserting the link already works and didn't engage with the evidence when pushed. That's a useful reminder that chatbot answers about
开发者
From 0 Likes to Meme Engineer
We have all been there. You are sitting at your desk late at night, your code is throwing errors that...
AI 资讯
How to Compress Images in the Browser with Canvas API (No Uploads, No Server)
How to Compress Images in the Browser with Canvas API Every image you upload to a "free" online compressor is sent to a server — often without you knowing what happens to it afterward. For a tool that processes your private photos, that's a terrible design. Here's how to build (or use) an image compressor that runs entirely in the browser using the HTML5 Canvas API. No uploads, no server costs, and unlimited file sizes. The Core Technique: Canvas toBlob() The key API is HTMLCanvasElement.toBlob() : js const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; ctx.drawImage(img, 0, 0); canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); }, 'image/jpeg', 0.8); }; img.src = 'your-image.jpg'; The second parameter is the MIME type (image/jpeg, image/png, image/webp, image/avif). The third is quality (0–1). Step-Down Resizing for Large Images If you're compressing a 6000×4000 px photo, drawing it at full resolution onto a canvas can eat 70+ MB of memory. Step-down resizing halves the dimensions repeatedly: function stepDownEncode(img, maxDim, quality) { let w = img.naturalWidth; let h = img.naturalHeight; let src = img; while (w > maxDim * 2 || h > maxDim * 2) { w = Math.floor(w / 2); h = Math.floor(h / 2); const temp = document.createElement('canvas'); temp.width = w; temp.height = h; temp.getContext('2d').drawImage(src, 0, 0, w, h); src = temp; } const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(src, 0, 0, w, h); return new Promise((resolve) => { canvas.toBlob((blob) => resolve(blob), 'image/jpeg', quality); }); } This prevents memory crashes and actually produces better quality (step-down preserves more detail than a single jump). Comparing Real-World Results Format Avg Original Avg Compressed Avg Savings JPEG → JPEG (Q80) 3.2 MB 0.8 MB 75% PNG → We
AI 资讯
Why I Ditched Socket.IO for Raw WebSockets (And What I Learned)
When you google "how to build a chat app in Node.js," the very first result will almost certainly point you to Socket.IO. It is the de facto standard for a reason. When I started my project, I used it without a second thought. It worked like magic. But as I got deeper into the project, that magic started to feel more like a black box. I eventually ripped out Socket.IO and replaced it with raw, native WebSockets. It was a daunting decision, but having built and managed it myself, I have some strong opinions on what Socket.IO abstracts away, what I had to build from scratch, and whether the headache was actually worth it. The Magic of Socket.IO (And Why We Use It) To understand why walking away from Socket.IO is hard, you have to understand exactly how much heavy lifting it does for you behind the scenes. It isn't just a WebSocket library; it is a real-time framework. The Polling Fallback: Historically, if a user's corporate firewall blocked WebSockets, Socket.IO would seamlessly downgrade to HTTP long-polling. Automatic Reconnections: If a user drives through a tunnel and loses the connection, Socket.IO automatically handles the exponential backoff to reconnect them when they emerge. Rooms and Namespaces: It gives you a beautiful socket.to("room-1").emit() API for broadcasting messages to specific groups of users. Heartbeats: It manages ping/pong messages under the hood to ensure the connection hasn't silently died. When you drop Socket.IO, you lose all of this for free. So, Why Did I Walk Away? First, the fallback mechanism is largely a relic of the past. Today, native WebSocket support across modern browsers and network infrastructure is essentially ubiquitous. I didn't need to ship a massive client bundle just to support HTTP polling for the 0.1% of edge cases. Second, the lock-in is real. If you use Socket.IO on the client, you must use a Socket.IO server implementation. You can't just connect to a standard WebSocket server. I wanted the freedom to swap out my ba
AI 资讯
Your PDF tool is storing your files. Here's proof.
Upload a file to any random "free" PDF tool online. Then check their privacy policy. Most of them say something like: "We may retain uploaded files for up to 24 hours" or "Files may be used to improve our services" Your client's contract. Your salary slip. Your ID card. Sitting on someone's server. I got tired of this and built a tool where your files never leave your browser. No upload happens at all. 80+ tools, nothing stored, no account needed. Roast it, use it, or ignore it. Up to you.
AI 资讯
TypeScript Branded Types vs. Nominal Types: Which Pattern Should You Use in 2026
TypeScript Branded Types vs. Nominal Types: Which Pattern Should You Use in 2026 Most type safety failures in TypeScript stem from treating all strings as interchangeable. The structural type system that makes TypeScript flexible also creates subtle bugs when developers pass a UserId where a PostId was expected. Both are strings at runtime, and TypeScript's compiler sees them as compatible. This compatibility becomes expensive in production. When an engineer accidentally passes an email address to a function expecting a username, the compiler stays silent. The bug surfaces only when users report authentication failures or data corruption. Teams that rely purely on structural typing pay this cost repeatedly. Branded types solve this by adding phantom properties that exist only at compile time. They transform primitives into distinct types without runtime overhead. The pattern has matured significantly since 2023, and production codebases now demonstrate clear advantages over both structural typing and runtime validation alone. Key Takeaways Branded types prevent primitive type confusion at compile time with zero runtime cost The unique symbol pattern creates true nominal typing behavior in TypeScript's structural system Combining brands with validation functions provides both type safety and runtime guarantees Branded types excel for domain identifiers, measurements, and validated strings Choose branded types when preventing accidental type substitution matters more than implementation flexibility Understanding Branded Types: Adding Identity to Primitives Branded types attach compile-time metadata to primitives through intersection with phantom properties. A UserId becomes structurally distinct from a plain string even though both compile to identical JavaScript. The technique exploits TypeScript's structural typing: if two types have different shapes, the compiler treats them as incompatible. Adding a property that exists only in the type system creates this distinc
AI 资讯
Fix Your "Developer Slouch": Building a Real-time AI Posture Monitor with MediaPipe and Electron
We’ve all been there. You start your morning feeling like a Productivity God, sitting straight and typing at 120 WPM. Fast forward four hours, and you've morphed into a literal shrimp, face inches away from the monitor, hunting for a missing semicolon. 🦐 In this era of remote work, real-time posture correction and computer vision for health have become more than just "cool projects"—they are spinal lifesavers. Today, we’re going to build a desktop application using MediaPipe , WebRTC , and Electron that monitors your neck angle and sends a desktop notification the moment you start slouching. By leveraging MediaPipe Pose and TensorFlow.js , we can calculate the Forward Head Posture (FHP) ratio with surgical precision directly in the browser environment. The Architecture 🏗️ Before we dive into the code, let’s look at how the data flows from your webcam to that "Sit up straight!" notification. graph TD A[Webcam Feed] -->|MediaStream| B(WebRTC API) B -->|Video Frames| C[MediaPipe Pose Model] C -->|Landmarks| D{Geometry Engine} D -->|Calculate Ear-Shoulder Angle| E{Threshold Check} E -->|Angle > 30°| F[Electron Main Process] F -->|Trigger| G[System Desktop Notification] E -->|Healthy| H[Continue Monitoring] style G fill:#f96,stroke:#333,stroke-width:2px Prerequisites 🛠️ To follow along, you'll need the following tech stack: MediaPipe Pose : For high-fidelity body tracking. WebRTC : To capture the video stream from your webcam. Electron : To wrap our logic into a desktop app that runs in the background. TensorFlow.js : The backbone for running ML models in JavaScript. Step 1: Setting up the Video Stream (WebRTC) First, we need to grab the camera feed. In a modern browser environment (or Electron's Chromium), we use navigator.mediaDevices.getUserMedia . async function setupCamera () { const videoElement = document . getElementById ( ' input_video ' ); const stream = await navigator . mediaDevices . getUserMedia ({ video : { width : 640 , height : 480 }, audio : false }); v
AI 资讯
Scrape Google Trends Without an API Key (Including the Scraper Flag Google Hands You)
Google Trends has no official API, and most wrapper libraries rot within months. But the Trends site itself runs on a keyless JSON API that anyone can call, and it serves the exact numbers you see in the UI. Here is the full recipe, including one gotcha where Google quietly labels your session a scraper. The two step flow Trends works in two steps. First you call explore , which returns a list of widgets, one per chart on the page, each with a signed token: GET https://trends.google.com/trends/api/explore ?hl=en-US&tz=0 &req={"comparisonItem":[{"keyword":"web scraping","geo":"US","time":"today 12-m"}],"category":0,"property":""} Then you call a widget data endpoint with that widget's request and token : GET https://trends.google.com/trends/api/widgetdata/multiline?hl=en-US&tz=0&req=<widget.request>&token=<widget.token> The widget kinds map to endpoints: TIMESERIES uses multiline , GEO_MAP uses comparedgeo , and both RELATED_QUERIES and RELATED_TOPICS use relatedsearches . The cookie trick Call explore cold and you get a 429. The API wants a NID cookie, and here is the counterintuitive part: you get it by requesting the public explore page first, and that page may itself respond 429 while still setting the cookie you need. const res = await fetch ( ' https://trends.google.com/trends/explore?geo=US&q=test ' ); // res.status may be 429. The Set-Cookie header is still there. const cookies = res . headers . getSetCookie (); Grab the cookie from the 429 response, retry explore , and everything works. Strip the anti JSON prefix Every Trends response starts with a junk line like )]}' to break naive JSON.parse calls. Drop everything up to the first newline: const body = await res . text (); const data = JSON . parse ( body . slice ( body . indexOf ( ' \n ' ) + 1 )); The scraper flag Here is the part I have not seen documented. Look inside the widget request object that explore returns to a keyless session: "userConfig" : { "userType" : "USER_TYPE_SCRAPER" } Google knows. And