开源项目
🔥 huggingface / transformers - 🤗 Transformers: the model-definition framework for state-of-
GitHub热门项目 | 🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training. | Stars: 163,607 | 69 stars today | 语言: Python
AI 资讯
I Built a Signed Webhook Receiver for Cross-Server Communication
Sometimes your application can reach an external service from one server, but not from another. I ran into this problem while working on one of my projects. I needed my server in Iran to communicate with Telegram, but the connection wasn't reliable from inside Iran. Instead of moving the whole application, I built a small intermediate service: Signed Webhook Receiver It is a lightweight FastAPI service that receives requests signed with an RSA private key and verifies them using the corresponding public key before processing them. Your Server | | RSA Signed Request v Webhook Receiver | | HTTP Request v External Service The receiver can be useful for: Secure server-to-server communication Webhooks and internal APIs Acting as a controlled proxy/gateway Connecting servers across different network environments Payment integrations where a provider requires requests from an Iranian IP For example, if your main application is hosted outside Iran but a payment gateway only accepts requests from Iranian IP addresses, an Iranian server can act as the intermediate gateway: Foreign Server | | Signed Request v Iranian Gateway Server | v Payment Gateway The important part is that this isn't an open proxy. Requests can be authenticated and the gateway can be restricted to specific operations and destinations. The project is built with Python, FastAPI, Cryptography, Docker, and Traefik and is open source. View the project on GitHub I also wrote more technical notes and development articles on my website: Building a Secure Webhook Receiver for Server-to-Server Communication | CyberHuginn
开发者
Pdf to Docx using Pyhton
Here Some Simple Python Script that convert PDF to word using pdf2docx library on python first install library on pip : pip install --user termcolor opencv-python-headless fire pdf2docx make sure example.pdf as source for convert exist with script ,after that build this sample Simple Python Script for converter : from pdf2docx import Converter pdf_file = ' example.pdf ' docx_file = ' example.docx ' cv = Converter ( pdf_file ) cv . convert ( docx_file ) cv . close ()
AI 资讯
NVIDIA's NOOA turns an AI agent into one Python class
NVIDIA Labs open-sourced NOOA (NVIDIA Object-Oriented Agents) this week, and the pitch is unusually simple: an agent is a Python class. Not a graph, not a chain, not a YAML pipeline. A class. I cloned it and got it running the same day. Here's what it actually looks like, what broke, and why I think the core idea matters more than the framework itself. The whole idea in one code block from nooa import Agent class InventoryAgent ( Agent , llm = llm ): """ You are an agent that checks inventory using deterministic helper methods. """ # Plain Python — automatically available as a tool for the LLM def get_stock ( self , item : str ) -> int : """ Get current stock for an item. """ return self . inventory . get ( item , {}). get ( " stock " , 0 ) # `...` body — the LLM implements this at runtime, calling the methods above async def can_fulfill_order ( self , items : list [ str ], budget : float ) -> Result : """ Check if order can be fulfilled within budget. """ ... That's from the repo's quickstart, lightly trimmed. The mapping is: Fields are agent state Methods with real bodies are deterministic tools Methods with ... bodies are implemented by an LLM loop at runtime Docstrings are the prompts Type annotations are contracts the runtime enforces, with auto-retry on mismatch No separate tool-schema JSON. No registration step. The model acts by writing Python in a REPL with access to self , so your method signatures are the tool definitions. Two install gotchas before you try it The README says pip install nooa . Two things I hit on a clean machine: 1. It's not on PyPI yet. As of today, pip install nooa returns No matching distribution found . Install from source instead: git clone https://github.com/NVIDIA-NeMo/labs-OO-Agents.git uv venv --python 3.13 && uv pip install ./labs-OO-Agents 2. No Python 3.14 support. The package pins >=3.12,<3.14 . My default interpreter is 3.14, and the install fails with a version error. Use 3.12 or 3.13. After that, everything imported clean
AI 资讯
Generating daily horoscopes and zodiac videos with an automated AI pipeline
Astrology content has a brutal property: it has to be fresh every single day , for every sign, ideally in a few languages, forever. Writing that by hand doesn't scale. For AstroZodify I built a pipeline that generates daily horoscopes and short zodiac videos on a schedule, with humans reviewing rather than writing. Here's the shape of it. The content problem Per day you need: 12 signs x N content types (daily horoscope, love, career) x M languages. That's hundreds of pieces of copy a day that all have to feel written, not templated, and stay consistent with each sign's "voice". Templating alone reads robotic. Free-form generation drifts. The trick is constraining an LLM enough to stay on-brand while still sounding human. The generation pipeline Structured prompts per sign. Each sign has a persona and constraints (tone, themes, length). The model fills the daily specifics, not the whole thing from scratch. Scheduled batch runs. A cron job kicks off generation ahead of time so content is ready before it's needed, never on the critical path of a page request. Validation. Output is checked for length, banned phrasing, and structure before it's allowed near the site. Store, then serve. Everything lands in Postgres. Pages are SSR and just read pre-generated rows, so the LLM is never in the user's request path. Keeping generation offline from serving is the single most important decision - it keeps pages fast and costs predictable. Adding video Text was step one. Short vertical zodiac videos (for social) are step two, and that's a heavier pipeline: script -> imagery -> voiceover -> render. That part runs on Cloud Run as a separate job so a slow render never touches the web app, and we pilot one item before any batch. Cost and safety rails Anything that calls a paid API in a loop is a footgun. The rules I follow: Always pilot on 1-10 items before a full batch. Never an unbounded loop against a paid API. Cache and pre-generate so serving is basically free. Takeaways Separate
开源项目
🔥 stanfordnlp / dspy - DSPy: The framework for programming—not prompting—language m
GitHub热门项目 | DSPy: The framework for programming—not prompting—language models | Stars: 37,001 | 152 stars today | 语言: Python
AI 资讯
Whisper + Deepgram + Piper: I Parallelized a Voice AI Pipeline and Cut Latency From 1,200ms to 340ms
My first voice agent took 1,200ms to answer a spoken sentence. Then I rewrote three seams in the pipeline and it dropped to 340ms. No new hardware, no new models, no smaller LLM. The words the user says, the words the agent says back, the same. What changed was the shape of the wait. If you have ever built a voice agent that felt polite but slow, this is the part of the pipeline where the seconds hide. The 1,200ms baseline was polite and wrong Here is what my first version did, in the order it did it: Record until the user stops talking (~200ms of tail silence). Send the whole clip to Whisper. Wait for the transcript. Send the transcript to the LLM. Wait for the full response. Send the full response to Piper. Wait for the WAV. Play the WAV. Each stage was fine on its own. The pipeline was a one-lane road. Whisper could not start until recording finished. The LLM could not start until Whisper finished. Piper could not start until the LLM was done. The user waited for the sum. The car metaphor gets old fast, so I will use a real one. This is what the timeline looked like on my machine: [record]--[200ms silence]--[whisper 380ms]--[LLM 480ms]--[piper 340ms]--[playback] ^ 1,200ms Every one of those bars was blocking the next. I had built a relay race where each runner waited for the previous runner to sit down. Trick 1: Frame-based STT so Whisper starts before the user stops The first fix is to stop treating the user's speech as a single file. Feed the audio to Whisper in 20-30ms frames as it is captured. By the time the user hits the tail silence, most of the transcription is already done. You only wait for the last few frames plus a short flush. Pipecat is the reference implementation. Its whole model is frame-based: every stage processes 20-30ms chunks and hands them forward as soon as they are ready. There is no batch, no full-clip handoff, no "wait for this stage to complete." Its own docs quote sub-500ms voice-to-voice when all models are hosted on the same GPU clu
开发者
140 Bugs Were Hiding in One Function, and My Tests Couldn't See Any of Them
Anyone can port a library. Point a translator at the source, clean up the output, get it to compile, and you have something that looks like a port. The actual engineering problem is different and much harder: proving that the new code means the same thing as the old code, across thirty algorithms, hundreds of edge cases, and a test suite written by people who were not thinking about you. This is the story of porting textdistance , a Python library for measuring string similarity, to Rust. The result is textdistance-rs . The porting took a fraction of the time. Everything else: the differential fuzzing, the 140 divergences, the 35 year old threshold I violated, the floating-point drift at the 15th decimal place, is what this writeup is actually about. Thirty algorithms and one architectural bet The original textdistance covers a lot of ground: edit-based distances (Levenshtein, Damerau-Levenshtein, Hamming, Jaro-Winkler), token-based measures (Jaccard, Sørensen-Dice, cosine, Tversky), sequence-based methods (LCS, Ratcliff-Obershelp), phonetic algorithms (MRA, Editex), and compression-based distances built on normalized compression distance. Over thirty algorithms in total, all reimplemented in Rust. But before writing a single algorithm, I had to make the decision that shaped everything downstream: how does the existing Python test suite (397 tests I did not write) talk to the Rust code? The obvious answer is PyO3: wrap every algorithm in a #[pyclass] , build a native extension, and the Python tests import Rust directly. The answer I chose instead was a subprocess CLI. The Rust core is a standalone binary that speaks JSON over stdin/stdout, and a thin Python adapter shells out to it: // The entire cross-language surface is one struct. #[derive(Deserialize)] struct Request { algorithm : String , s1 : String , s2 : String , qval : Option < usize > , external : Option < bool > , } fn dispatch ( req : & Request ) -> Response { match req .algorithm .as_str () { "hamming"
AI 资讯
My Commit-Message Script Has 8 Assertions in --selftest. None of Them Touch the Code That Can Actually Fail.
I have three files in this repo that shell out to something over the network or a subprocess and can fail in interesting ways: publish_devto.py , server.py , and git_commit.py . Two of them have --selftest blocks that stub the risky call and exercise the actual failure branches. One doesn't, and I only noticed because I went looking for a reason to be suspicious of my own test coverage after seeing a trending post about counting assertions in a test suite and not liking what you find. git_commit.py reads a staged diff and calls claude -p to turn it into a commit message. It has five distinct exit paths, all guarding real failure modes I've hit before in this project: try : diff = subprocess . check_output ([ " git " , " diff " , " --staged " ], text = True , timeout = 20 ) except subprocess . TimeoutExpired : print ( " git diff --staged timed out after 20s " , file = sys . stderr ) raise SystemExit ( 1 ) if not diff . strip (): print ( " Nothing staged. Run `git add` first. " ) raise SystemExit ( 1 ) try : raw = subprocess . check_output ( [ " claude " , " -p " , " --safe-mode " , SYSTEM + " \n\n " + diff ], text = True , timeout = 20 , stderr = subprocess . PIPE , ). strip () except subprocess . TimeoutExpired : print ( " claude -p timed out after 20s " , file = sys . stderr ) raise SystemExit ( 1 ) except subprocess . CalledProcessError as e : print ( f " claude -p exited { e . returncode } : { ( e . stderr or '' ). strip ()[ : 200 ] } " , file = sys . stderr ) raise SystemExit ( 1 ) except FileNotFoundError : print ( " claude CLI not found on PATH " , file = sys . stderr ) raise SystemExit ( 1 ) That's a held index lock hanging git diff , an empty staging area, a claude -p call that times out, one that exits non-zero, and one where the claude binary isn't even on PATH . Real scenarios — the timeout on this exact git diff --staged call was itself a bug I'd already found and fixed once ( docs/project_notes/bugs.md , 2026-08-06: a prior fix claimed to add a timeout
AI 资讯
The Security Gap in MCP Tool Servers (And What I Built to Fix It)
MCP (Model Context Protocol) is how AI agents connect to tools. Claude Desktop uses it, Cursor uses it, and thousands of developers are building MCP servers to give AI access to their APIs, databases, and infrastructure. There's one problem: MCP has no security model. The protocol defines how a client talks to a server, but says nothing about what that server is allowed to do. No authentication between client and server. No authorization on which tools can be called. No audit trail of what happened. The spec assumes you'll handle all of that yourself. Most people don't. What Actually Goes Wrong I run a self-hosted server with Prometheus, Grafana, Ollama, Gitea, and a handful of other services. I wanted Claude Desktop to query all of them through MCP. The standard approach is to write a Python FastMCP server for each one — a few dozen lines per service, hardcode the API key, register the tools, done. That works until you think about what you've actually built: Every MCP server has full access to whatever its process can reach. Your Prometheus tool can also hit your Grafana API, your Gitea API, and anything else on localhost. There's no scoping. API keys live in environment variables or config files. If you have 9 MCP servers, you have 9 places where credentials sit in plaintext with no access policy. Nothing is logged. If Claude calls a tool that restarts a service or deletes data, there's no record of which tool was called, with what parameters, by which agent, at what time. There's no concept of read-only vs. write. A tool either exists or it doesn't. MCP doesn't know that query_prometheus is safe to call freely but restart_service should require approval. Tool composition creates emergent risks. When Claude has access to multiple MCP servers, it can chain calls across them. Server A reads sensitive data, Server B posts to an external API — Claude could combine them in ways neither server was designed for. These aren't theoretical risks. During development, I decla
AI 资讯
Stop Slouching! Build a Real-Time Spine Posture Monitor using MediaPipe and Python
We’ve all been there: hunched over a keyboard at 3 AM, neck craned forward like a turtle, debugging a race condition. "Tech neck" isn't just a meme; it’s a productivity killer. As developers, our spine is our most underrated hardware. In this tutorial, we are going to build a Real-Time Spine Posture Monitor . We will leverage real-time human pose estimation and MediaPipe Python libraries to track your posture via your webcam. By the end of this guide, you'll have a system that detects when you're slouching and sends a system notification to keep your ergonomics in check. This project is perfect for those looking into OpenCV computer vision and developer ergonomics solutions. The Architecture 🏗️ The logic is straightforward: we capture video frames, process them through a pre-trained neural network to find body landmarks, and apply some basic geometry to determine if your posture is healthy. graph TD A[Webcam Feed] --> B[OpenCV Frame Processing] B --> C[MediaPipe Pose Landmark Detection] C --> D{Extract Shoulder & Ear Coordinates} D --> E[Calculate Neck Inclination Angle] E --> F{Angle > Threshold?} F -- Yes --> G[Trigger System Notification] F -- No --> H[Continue Monitoring] G --> B H --> B Prerequisites 🛠️ Before we dive into the code, ensure you have the following installed: Python 3.9+ MediaPipe : Google’s framework for cross-platform ML. OpenCV : For video stream handling. PyObjC : (For macOS) to trigger native system alerts. pip install mediapipe opencv-python pyobjc Step 1: Initialize the Pose Engine MediaPipe makes pose estimation incredibly easy. We’ll use the Pose solution, which provides 33 3D landmarks for the human body. import cv2 import mediapipe as mp import math # Initialize MediaPipe Pose mp_pose = mp . solutions . pose pose = mp_pose . Pose ( static_image_mode = False , model_complexity = 1 , enable_segmentation = False , min_detection_confidence = 0.5 ) mp_drawing = mp . solutions . drawing_utils Step 2: Calculating the "Slouch" Angle 📐 To detect
AI 资讯
Trading the Gap: How We Built a 91% Win-Rate Basis Bot After a 217% Buy-and-Hold Reality Check
After months of letting the agent build technical indicator bots, we finally asked it to run the simplest test possible: what if we just bought BTC/JPY eight years ago and did absolutely nothing? The result was a +217.1% return. Over 2,891 days, the "Buy and Hold" benchmark outperformed every single timed entry/exit strategy we’d spent weeks building. The closest runner-up (C5) only managed a fraction of that gain (~7.9M JPY vs the benchmark's ~21.7M JPY). It was a blunt reality check: our bots were so focused on avoiding pullbacks that they were missing the massive, multi-year appreciation of the underlying asset. The flip side, of course, was the pain. The buy-and-hold strategy suffered a maximum drawdown of 54.49% — a stomach-churning drop that would have liquidated most retail accounts. Our bots, meanwhile, kept drawdowns in the 13–17% range. This reframed the entire experiment. The goal wasn't just to "beat" the market; it was to find a way to capture that upside without the 50% wipeout risk. Trying to build a "Free Lunch" via Portfolio Blending The agent's next move was to stop looking for one perfect bot and start looking for a portfolio. We tested three different blends: The 6-leg blend (Buy-and-hold + C3 through C7): This produced a +65.4% return with a 22.7% drawdown. The 3-leg blend (Buy-and-hold + the two "survivor" bots, C3 and C7): This hit a +99.5% return, but the drawdown spiked to 31.8%. The 5-leg "Optimized" blend : By removing a known-loser (C4), the return jumped to +84.8%, but the drawdown actually rose to 23.6%. We found a counterintuitive reality: even the losing bot (C4) was providing diversification because its failures didn't correlate with the others. Removing it made the equity curve "cleaner" but more fragile. It was a reminder that in a portfolio, "bad" strategies can sometimes act as insurance for "good" ones. The 91% Win-Rate Basis Trade When I told the agent that even doubling the money felt "too low" for the complexity of automated
AI 资讯
From Raw Text to Cryptographic Seal: Building a Legal Document Factory in Python
When people think of Artificial Intelligence, they usually think of chat boxes. You type a prompt, text scrolls across the screen, and you copy-paste it. In the legal world, a chat box isn't enough. A contract on a screen is just a suggestion. A contract in hand—signed, sealed, and cryptographically verified—is a binding asset. As we build Lawyie (Sunverse AI’s intelligent legal infrastructure for Africa), one of our core mandates was moving beyond the chat interface. We needed a Document Factory. Here is the engineering breakdown of how we built an in-memory PDF generation pipeline that creates cryptographically-sealed legal documents in Python. 1. The Problem with Standard File Writing In standard Python web apps, saving a file usually means writing it to the local hard drive and then serving it. In a cloud environment like Streamlit Cloud, doing this at scale causes concurrency issues (multiple users overwriting the same contract.pdf file) and unnecessary disk read/write latency. The Solution: Everything must happen in-memory. 2. The In-Memory Buffer ( io.BytesIO / Byte-Streams) Instead of saving a file to the disk, we use Python’s io module to capture the PDF output directly as a byte-stream and feed it straight into the user's browser download button. Here is how the pipeline works using fpdf2 : from fpdf import FPDF import io def generate_legal_pdf ( contract_text , signature_id ): # 1. Initialize the PDF engine pdf = FPDF () pdf . add_page () pdf . set_font ( " Arial " , size = 11 ) # 2. Clean text (Handling special characters for Latin-1 encoding) clean_text = contract_text . replace ( " ₦ " , " NGN " ). replace ( " — " , " - " ) final_content = f " { clean_text } \n\n SECURE HASH ID: { signature_id } " # 3. Write to the document pdf . multi_cell ( 0 , 10 , txt = final_content ) # 4. Capture the output as bytes (Crucial for fpdf2) pdf_output = pdf . output () pdf_bytes = bytes ( pdf_output ) if isinstance ( pdf_output , bytearray ) else pdf_output return pdf
AI 资讯
Building LoanAI: AI-Powered Loan Default Prediction System using Flask & Scikit-Learn
Hi everyone! 👋 I recently developed LoanAI , a real-time credit risk assessment platform that predicts loan default probabilities using machine learning models. Key Features Instant Risk Scoring: Real-time credit risk assessment for loan applicants. Explainable AI: Transparent prediction logic for financial decision-making. Clean UI: Built with Flask, Bootstrap 5, and Python. Live Demo Check out the live web app here: LoanAI Web Application I would love to hear your feedback on the project structure and prediction engine!
AI 资讯
Absorber les +50 % de l'API Claude sans couper une feature
Le 1er septembre 2026, le tarif de lancement de Claude Sonnet 5 s'arrête. L'input passe de 2 $ à 3 $ le million de tokens, l'output de 10 $ à 15 $ : +50 % sur les deux lignes, pour tout le monde qui appelle l'API en paiement à l'usage. La panique par défaut, c'est de couper des fonctionnalités ou de rétrograder vers un modèle plus faible. Il y a mieux, et c'est déjà dans l'API. Deux mécanismes — le prompt caching et le batch — encaissent la hausse à ta place, souvent avec de la marge. Voici le code, les chiffres, et les pièges que j'ai payés pour que tu ne les paies pas. Ce qui bouge exactement le 1er septembre Trois lignes suffisent à raisonner. Le reste du barème (Opus, Haiku, contexte 1M) ne change pas. Poste Sonnet 5 (par M de tokens) Jusqu'au 31 août Dès le 1er sept. Input standard 2 $ 3 $ Output 10 $ 15 $ Lecture cache (hit) 0,20 $ 0,30 $ Retiens la troisième ligne, parce que c'est elle qui gagne la partie. Un cache hit coûte 10 % du prix d'input . Même après la hausse, lire depuis le cache à 0,30 $ reste moins cher que l'ancien input plein à 2 $. Autrement dit, le contexte que tu répètes à chaque appel — un system prompt costaud, une doc, des exemples few-shot — peut être payé une fois puis relu pour trois fois rien. Le prompt caching, concrètement Le principe est simple : tu marques un bloc stable avec cache_control , et tout ce qui précède ce marqueur est mis en cache. Le premier appel paie une écriture ; les suivants, dans la fenêtre TTL, lisent à 10 %. import anthropic client = anthropic . Anthropic () DOCS = load_docs () # ~20 000 tokens, identiques à chaque requête def ask ( question : str ): return client . messages . create ( model = " claude-sonnet-5 " , max_tokens = 1024 , system = [ { " type " : " text " , " text " : " Assistant support de l ' app Lumière. " }, { " type " : " text " , " text " : DOCS , " cache_control " : { " type " : " ephemeral " }, # TTL 5 min }, ], messages = [{ " role " : " user " , " content " : question }], ) La question de
AI 资讯
When is it safe to open the microphone? Building a realtime voice agent on Twilio
Wiring up a phone agent looks like a weekend project. Twilio Media Streams gives you a WebSocket with raw audio, you push it into a streaming STT, you feed the transcript to an LLM, you stream the reply into a TTS and send the bytes back. A few hundred lines. It works on the first call. Then you listen to a recording and the agent is talking to itself. Agent: "Hello, how can I help you?" STT: "hello how can i help you" ← its own voice LLM: "Sure! What can I help you with?" STT: "sure what can i help you with" ← and again Nobody said a word. The call is in a loop. This post is about the part that took the real time — not the signal path, but the state machine sitting on top of it. I run this in production on a German phone line, and every rule below exists because something broke on a real call. The single-channel problem A phone line is not a mixing desk. There is one channel, and your own output comes back into it: through the caller's speaker, through network echo, through the conference bridge on the other end. Your STT does not know which words came from a human and which are your own TTS coming home. So you need a gate. While the agent speaks, the microphone is closed and incoming transcripts are discarded. When the agent finishes, it reopens. The whole difficulty is in the word finishes . The obvious fix, and why it doesn't hold The first instinct is to close the microphone when TTS starts and reopen it when the TTS stream ends. This is wrong, and it's wrong in a way that hides from you. The end of your TTS stream is not the moment the caller hears the sentence. Between the last audio chunk you send and playback at the caller's ear sit the telephony platform's buffers and the network: anywhere from a couple of hundred milliseconds to well over a second, depending on the connection. Release on stream end and the microphone opens while the caller is still hearing your voice . That's the feedback loop, right there. And here's the part that costs you a day: it nev
开源项目
🔥 vladmandic / sdnext - SD.Next: All-in-one WebUI for AI generative image and video
GitHub热门项目 | SD.Next: All-in-one WebUI for AI generative image and video creation, captioning and processing | Stars: 7,261 | 28 stars today | 语言: Python
开源项目
🔥 funstory-ai / BabelDOC - Yet Another Document Translator
GitHub热门项目 | Yet Another Document Translator | Stars: 9,225 | 69 stars today | 语言: Python
开源项目
🔥 harveyai / harvey-labs - A benchmark built to evaluate and improve agent capabilities
GitHub热门项目 | A benchmark built to evaluate and improve agent capabilities for supporting legal work. | Stars: 697 | 47 stars today | 语言: Python
AI 资讯
"My Comment-Reply Pipeline Was Feeding Me Garbled HTML Entities Instead of the Actual Comment"
I have a small script, reply_comments.py , that pulls unanswered comments off my DEV.to articles and drafts replies to a markdown file so I can paste them in by hand. The API doesn't let a normal account post comments (that's its own bug I've written about before), so this draft-then-paste loop is the whole workflow. Every reply I've ever sent has come from reading the body field this script prints. Today I went looking for a bug distinct from everything already logged for this repo, and I ended up re-reading strip_html() , the function that turns a comment's raw body_html into the plain text I actually read: def strip_html ( h ): return re . sub ( r " \s+ " , " " , re . sub ( r " <[^>]+> " , " " , h )). strip () It does exactly one thing: strip HTML tags with a regex, then collapse whitespace. It's been in the file since the script was written and nobody had audited it on its own — every prior pass through this pipeline was about pagination, thread-depth walking, or dedup keys, never the text-extraction step itself. Here's the problem. DEV.to's API returns body_html as rendered HTML. A correct renderer has to HTML-entity-escape a commenter's own literal < , > , & , and quote characters, or they'd get mistaken for markup. So a comment that reads, in plain English: isn't it faster with a Q&A cache? Try List instead. comes back from the API as something like: <p> isn ' t it faster with a Q & A cache? Try List < String > instead. </p> strip_html() 's regex only ever targets <[^>]+> — actual tags. It has no idea what to do with ' , & , < , > . Those aren't tags, so the regex leaves them untouched. The whitespace collapse doesn't touch them either. What comes out the other end, into the exact field I read to draft a reply, is: isn't it faster with a Q&A cache? Try List<String> instead. That's not a cosmetic nit. On a dev-focused comment section, & , < , and > show up constantly — generics, comparisons, "foo & bar," code snippets