[{"content":"","date":"2026-08-18","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"2026-08-18","externalUrl":null,"permalink":"/categories/blog/","section":"Categories","summary":"","title":"Blog","type":"categories"},{"content":"Notes on what I\u0026rsquo;ve been building — mostly self-hosting, AI tooling, and the occasional deep dive into a bug I couldn\u0026rsquo;t leave alone.\n","date":"2026-08-18","externalUrl":null,"permalink":"/posts/","section":"Blog","summary":"Notes on what I’ve been building — mostly self-hosting, AI tooling, and the occasional deep dive into a bug I couldn’t leave alone.\n","title":"Blog","type":"posts"},{"content":"I recently tinkered with something else on my Synology NAS.\nI\u0026rsquo;d already been using Podsync to auto-download a bunch of podcast shows, with new audio coming in pretty much every day — mostly Chinese, with some English and Portuguese shows mixed in.\nOnce the audio\u0026rsquo;s downloaded, I can obviously just listen to it directly — but I gradually noticed a problem. With podcasts, especially shows with a lot of back-and-forth between multiple speakers, there\u0026rsquo;s a fair amount of filler, and it\u0026rsquo;s not easy to quickly zero in on the actual content you need from the audio alone.\nSo I started thinking: since these shows are already getting auto-downloaded to the NAS, why not take it one step further — once the audio\u0026rsquo;s downloaded, automatically transcribe it to text, then have AI clean it up into something more readable.\nThat way, a given episode could be listened to, read, searched, and kept long-term, all at once.\nSo I started tinkering.\nFirst idea: just run Whisper directly on the NAS # My first thought was actually pretty simple: since the files are already on the NAS, just deploy Whisper there and transcribe locally right after download.\nBut after checking my Synology\u0026rsquo;s specs, I dropped that idea quickly. Mine is a DS218+, with an Intel Celeron J3355, dual-core. The RAM had already been expanded to about 10GB, but there\u0026rsquo;s no dedicated GPU, and the CPU doesn\u0026rsquo;t even support AVX/AVX2.\nA small model isn\u0026rsquo;t entirely out of the question. But being able to run it and being suited to running it long-term are two completely different things.\nMy Podsync setup pulls in roughly two hours of new audio every day. Using a tiny model for speed means accuracy suffers too much; using a bigger model means the NAS\u0026rsquo;s processing speed probably can\u0026rsquo;t keep up with the daily incoming volume. And the NAS already runs other services — I didn\u0026rsquo;t want to keep the CPU pegged long-term just to transcribe podcasts.\nThinking it over, I realized there\u0026rsquo;s no real need to force the NAS to do AI inference at all. What a NAS is genuinely good at is storage, scheduling, and automation.\nSo the whole approach became: the NAS handles discovering files, splitting audio, managing tasks, saving results, and serving the web page. The genuinely resource-hungry Whisper transcription gets handed off to the cloud.\nOnce I reframed it that way, the whole thing suddenly got a lot simpler.\nTwo hours of audio a day is basically covered by free tiers # I first checked how much Podsync had downloaded over the past 24 hours — normally around two hours of new audio a day. That\u0026rsquo;s not actually a huge amount.\nSo I ended up hooking up a service: Cloudflare Workers AI\u0026rsquo;s whisper-large-v3-turbo. The web page now separately tracks how many minutes Cloudflare has used that day, and roughly how much quota is left. So opening the page each day tells me roughly how much more audio I can process that day.\nAfter that, it basically runs itself # The whole thing ended up packaged as a standalone Docker app. Podsync keeps doing what it always did, just downloading podcasts. My program mounts Podsync\u0026rsquo;s audio directory.\nOnce the container starts, it scans once immediately, then by default scans again every 30 minutes. All of this lives in the web-based settings — which directory to scan, how often, how far back to look, and which takes priority between the primary service and Cloudflare — all configurable from there.\nThere\u0026rsquo;s also a fairly important detail: how to tell whether a file has already been processed. Since the container might restart, and the Podsync directory always has a lot of historical files sitting in it, it obviously can\u0026rsquo;t re-transcribe everything from scratch every time it starts. So the program generates a fingerprint based on the file path, file size, and modification time. Once a file\u0026rsquo;s been processed, that gets recorded, and the next time it\u0026rsquo;s scanned, it just gets skipped.\nThere\u0026rsquo;s another detail worth mentioning: when Podsync downloads a larger episode, the file may already show up in the directory while it\u0026rsquo;s actually still downloading. So the program doesn\u0026rsquo;t transcribe a new file immediately — it first confirms the file hasn\u0026rsquo;t changed for at least 60 seconds. Only once it\u0026rsquo;s confirmed stable does it get added to the transcription queue.\nBy default it only processes episodes added in the past 24 hours. If a task fails, I can just click to re-queue it directly on the web page — no need to SSH into the server and run commands.\nThe long-audio problem got solved along the way too # Podcasts have another annoying characteristic: files are often huge. An episode running one, even two hours, is completely normal — but the free Whisper API usually caps individual file size.\nSo I added an FFmpeg layer in the middle. If it detects audio over 22MB or longer than 20 minutes, it auto-processes it first — converting it uniformly to 16kHz, mono, 48kbps — then splitting it into roughly 20-minute segments. These segments get sent to Whisper separately. Once everything\u0026rsquo;s done, the transcripts get merged back together, with each segment\u0026rsquo;s timestamps restored to its position in the original audio.\nSo from the user\u0026rsquo;s side, you\u0026rsquo;d never notice the audio had been split at all — you just see one complete episode with one continuous set of subtitles. The temporary split segments also get automatically deleted once the task finishes, so they don\u0026rsquo;t keep eating up NAS space.\nOnce I got this far, I realized subtitles alone weren\u0026rsquo;t quite enough # The text Whisper produces is already quite useful for searching or jumping to a specific point. But if you actually want to read through it from start to finish, the experience is still a bit rough. Speech recognition output often has sentence-break issues, missing punctuation in places, and the occasional homophone typo — especially in longer episodes, reading the raw subtitles is genuinely tiring.\nSo I hooked in LiteLLM as well. Once Whisper finishes, the subtitles automatically move into another AI processing queue.\nWhat I actually wanted here wasn\u0026rsquo;t for LiteLLM to \u0026ldquo;summarize\u0026rdquo; for me — I deliberately put a fairly strict constraint on this. What I want is cleanup, not summarization. In other words, whatever the original episode covered should, as much as possible, all stay in there. The AI is only responsible for fixing obvious recognition errors and cleaning up punctuation, sentence breaks, and paragraphing — turning it from a machine transcript into an article a normal person can read comfortably.\nSo the instructions I gave LiteLLM specifically say: no summarizing, no abbreviating, no deleting opinions, facts, numbers, or examples, and no adding conclusions that weren\u0026rsquo;t in the original. Chinese stays Chinese, English stays English, Portuguese stays Portuguese — no translation either. Only fix recognition errors that can be confidently confirmed from context.\nI also added a length check specifically for this. Because when a large model processes long text, sometimes even when you explicitly tell it not to summarize, it\u0026rsquo;ll still take it upon itself to compress things down into a short piece. If the returned article comes back noticeably shorter than the original subtitles, the system treats that run as a failure and refuses to save it outright. That task shows as failed, and I can re-run just the LiteLLM step separately later — without burning through Whisper\u0026rsquo;s quota again.\nIn the end, I just built my own podcast reader # Now that I had both subtitles and cleaned-up articles, I ended up building a web page on top of it too. Opening it now shows different shows organized by Podsync\u0026rsquo;s directory structure. You can filter by show, or search directly by filename or show title. Each episode shows its audio length, whether transcription is complete, whether the primary service or Cloudflare handled it, and whether LiteLLM has finished organizing the article.\nClicking into an episode drops you straight into a player. The audio supports Range requests, so it doesn\u0026rsquo;t need to fully download before playing, and the progress bar can be dragged freely. Below that is the raw subtitle track, with every segment carrying a timestamp — click a line, and the player jumps straight to that moment. Conversely, while audio is playing, whatever subtitle line is currently playing gets automatically highlighted. So if I hear something I didn\u0026rsquo;t quite catch, I can just glance at the text below. Or if a particular line in the subtitles looks interesting, one click jumps straight to that point in the original audio.\nCloudflare\u0026rsquo;s timestamps brought a small problem of their own # The timestamps Cloudflare returns are quite fine-grained — down to the word level in some cases. At first I put all of that directly on the page, and a longer episode could end up generating tens of thousands of page elements. Desktop browsers handled it fine, but on mobile it started getting noticeably sluggish.\nSo I added an aggregation layer, automatically merging these word-level timestamps into roughly eight-second, one-sentence-ish paragraphs. That cut the number of page elements way down, while keeping the click-to-jump behavior intact.\nI also reworked the mobile layout separately. The first version was a full-screen overlay-style reader, which looked fine on desktop, but on mobile, opening the detail view covered the entire screen. Now it\u0026rsquo;s embedded inline in the page instead — the show list, subtitles, and AI article each have their own controlled height and scroll independently. Desktop, tablet, and mobile all work reasonably well now, and both dark and light themes are supported.\nThe first real run took nearly three hours # Once the whole system was deployed, the first real run found 4 new audio files, totaling nearly three hours. After that I basically left it alone. The system checked the files itself, compressed and split them itself, and first tried transcribing with the primary Whisper service. When the primary service hit a quota limit, it automatically switched to Cloudflare. Once all the subtitles were done, it automatically handed things off to LiteLLM to organize into articles. By the time I checked the web page again, all 4 episodes had been fully processed — no failed tasks, no backlog.\nThe full pipeline now essentially looks like: Podsync downloads a podcast → the NAS auto-discovers it → checks whether the download is complete → deduplicates by fingerprint → auto-compresses and splits long audio → Whisper transcribes → automatically falls back to Cloudflare if the primary service is unavailable → merges subtitles and timestamps → LiteLLM corrects recognition errors and organizes the article → play, jump to a point, and read, all from the web page.\nWhat I ended up with is a bit different from what I originally imagined # At the start, this was really just meant to solve a simple problem: how to automatically turn podcasts downloaded to the NAS into text. But by the time it was done, it had turned into more than just a transcription tool.\nNow, once Podsync downloads an episode, I basically don\u0026rsquo;t need to touch anything else. Come back to the web page a bit later, and the audio, subtitles, and cleaned-up article are all sitting there already. Want to listen — go ahead. Want to skim quickly — check the subtitles. See something interesting — click the timestamp and jump straight to it. Just want to read quietly — go straight to the LiteLLM-organized article. And every episode ultimately stays on my own NAS, searchable and kept long-term.\nI\u0026rsquo;ve increasingly come to feel that, for an older NAS like the DS218+, there\u0026rsquo;s no need to insist on running every AI workload locally. Making it run large models just isn\u0026rsquo;t what it\u0026rsquo;s good at. But treating the NAS as an automation hub — that, I think, actually fits it really well. The files are there, Podsync is there, Docker is there, and task scheduling, FFmpeg, the database, and the web page are all there too. When real compute is actually needed, just call out to the primary Whisper service, Cloudflare, and LiteLLM out in the world. That sidesteps the old hardware\u0026rsquo;s performance limits, while basically covering each day\u0026rsquo;s new podcast volume using free quota alone.\n","date":"2026-08-18","externalUrl":null,"permalink":"/posts/podcast-nas-ai-transcription-system/","section":"Blog","summary":"Rather than trying to run Whisper locally on an underpowered NAS, I offloaded transcription to Cloudflare Workers AI and article cleanup to LiteLLM, turning the NAS into an automation hub with its own web player for listening, searching, and reading.","title":"Building an Automatic Podcast Transcription and AI-Organizing System on My Synology NAS","type":"posts"},{"content":"","date":"2026-08-18","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"Hi, I\u0026rsquo;m Chengyu — a final-year Computer Science student at the University of Sydney.\nThis site is my running notebook: the things I build, the problems I run into, and how I fixed (or didn\u0026rsquo;t fix) them — plus hiking, travel, games, gadgets, and whatever else I\u0026rsquo;ve been into lately. More about me here.\n","date":"2026-08-18","externalUrl":null,"permalink":"/","section":"Chengyu Wang","summary":"Hi, I’m Chengyu — a final-year Computer Science student at the University of Sydney.\nThis site is my running notebook: the things I build, the problems I run into, and how I fixed (or didn’t fix) them — plus hiking, travel, games, gadgets, and whatever else I’ve been into lately. More about me here.\n","title":"Chengyu Wang","type":"page"},{"content":"","date":"2026-08-18","externalUrl":null,"permalink":"/tags/self-hosting/","section":"Tags","summary":"","title":"Self-Hosting","type":"tags"},{"content":"","date":"2026-08-18","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":" A dumb question # Imagine you\u0026rsquo;re teaching an assistant who\u0026rsquo;s never seen the world how to manage your household affairs. You tell it two sentences:\nOld Wang is my dad. I\u0026rsquo;m Xiao Ming.\nThen you ask it: what\u0026rsquo;s the relationship between Old Wang and Xiao Ming?\nA normal person answers in a fraction of a second. But for a machine that only stores data, this is a trap — its database has exactly two records, neither of which says \u0026ldquo;Old Wang is Xiao Ming\u0026rsquo;s father.\u0026rdquo; So the standard answer is: no such relationship found.\nThe traditional way to get it right is to hand-write another rule: \u0026ldquo;If A is B\u0026rsquo;s father, and B is the one asking, then A is the asker\u0026rsquo;s father.\u0026rdquo; That works — until you quickly discover this path doesn\u0026rsquo;t scale. Grandfather, nephew, cousins, brothers-in-law, a daughter-in-law\u0026rsquo;s sister\u0026rsquo;s husband — family relationships alone can tangle you up completely, and you\u0026rsquo;d need hundreds of rules. And switch domains (a company\u0026rsquo;s equity structure, a disease classification system, a telecom pricing structure) and you\u0026rsquo;re writing hundreds more rules from scratch.\nOntological reasoning is trying to solve exactly this: letting a machine, with just a small set of concept definitions, derive on its own the mountain of conclusions you never explicitly stated.\nWhat an ontology is: sketching a skeleton of the world # The word \u0026ldquo;ontology\u0026rdquo; sounds abstract, but it\u0026rsquo;s borrowed from philosophy. In philosophy, ontology studies \u0026ldquo;what actually exists in the world\u0026rdquo;; once computer science borrowed the term, the meaning got a lot more practical: write down, in a way a machine can read, what concepts exist in a given domain, what relationships hold between them, and what rules can never be violated.\nTake family relationships as an example — roughly what you\u0026rsquo;d need to write is: what classes exist (person, man, woman); who\u0026rsquo;s a subset of whom (man is a kind of person, woman is a kind of person); what relationships exist (someone is someone\u0026rsquo;s \u0026ldquo;parent,\u0026rdquo; \u0026ldquo;spouse,\u0026rdquo; \u0026ldquo;sibling\u0026rdquo;); and the rules (a person has exactly one biological father; man and woman don\u0026rsquo;t overlap; \u0026ldquo;father\u0026rdquo; equals \u0026ldquo;is male + is someone\u0026rsquo;s parent\u0026rdquo;).\nThat last point is the essence. You didn\u0026rsquo;t directly define what a \u0026ldquo;father\u0026rdquo; is — you assembled it out of other concepts. Give a machine this assembly instruction, and it can go search the database on its own: who is both male and someone\u0026rsquo;s parent? Whoever it finds, that person is a father — even if the database never had a \u0026ldquo;father\u0026rdquo; label to begin with.\nAdd another assembly rule: \u0026ldquo;grandfather = the father of a father,\u0026rdquo; and the machine can automatically derive a whole batch of grandfathers too. You wrote five definitions; it derived thousands of conclusions. That\u0026rsquo;s the basic idea behind ontological reasoning.\nWhat a reasoner actually does # The program that does this work is called a reasoner — common ones in the industry include HermiT and Pellet. It mainly does three things.\nFirst, it states what\u0026rsquo;s implied. The grandfather example above — nothing was stored directly, but it logically has to be true, and the reasoner fills it in for you.\nSecond, it auto-classifies things. You add a new person to the database and only tell the machine \u0026ldquo;he\u0026rsquo;s male, and he\u0026rsquo;s someone\u0026rsquo;s parent.\u0026rdquo; The reasoner automatically places him into the \u0026ldquo;father\u0026rdquo; category, and along the way places that someone into \u0026ldquo;someone\u0026rsquo;s child.\u0026rdquo; You never manually applied a single label.\nThird — the part most easily overlooked, and actually the most valuable — it catches contradictions. If you accidentally entered both \u0026ldquo;Xiao Hong is a man\u0026rdquo; and \u0026ldquo;Xiao Hong is a woman,\u0026rdquo; and the ontology explicitly states those two classes are mutually exclusive, the reasoner will flag it on the spot and tell you the knowledge base contradicts itself. It\u0026rsquo;s not just deriving new things — it can prove that certain things cannot possibly both be true. In compliance, auditing, and risk-control scenarios, this capability matters even more than the reasoning itself.\nThe fundamental difference from \u0026ldquo;guessing\u0026rdquo; # At this point you might be wondering: can\u0026rsquo;t a large language model do all of this too? If I ask ChatGPT how Old Wang and Xiao Ming are related, it can answer that as well.\nIt can — but the mechanism is completely different, and the difference comes down to one word: guarantee.\nAn LLM\u0026rsquo;s answer comes from statistical patterns learned across a massive amount of text. It gets it right because \u0026ldquo;my dad\u0026rdquo; and \u0026ldquo;father-son relationship\u0026rdquo; have appeared together in countless sentences. That\u0026rsquo;s an extremely powerful capability — it can handle expressions you never defined, that are vague, or roundabout. But it can\u0026rsquo;t give you a guarantee: phrase it differently and it might get it wrong, ask the exact same question twice and get inconsistent answers, and when it doesn\u0026rsquo;t actually know, it\u0026rsquo;ll still confidently make something up.\nOntological reasoning is deduction. Its conclusion isn\u0026rsquo;t \u0026ldquo;this looks about right\u0026rdquo; — it\u0026rsquo;s \u0026ldquo;starting from these premises, this must be true.\u0026rdquo; And it can lay out the derivation step by step for you: because Old Wang is Xiao Ming\u0026rsquo;s parent, because Old Wang is male, because the definition of father is a male parent, therefore Old Wang is Xiao Ming\u0026rsquo;s father. Every link in that chain can be checked by a human.\nHere\u0026rsquo;s one way to put it: an LLM is like a well-read expert with great intuition, who occasionally talks out of turn; ontological reasoning is like an accountant who only trusts hard logic, but never makes a mistake. Ask the former the wide-open, imaginative questions; go to the latter for the conclusion you need to sign your name to.\nWhere the cost lies # This all sounds great so far, but ontological reasoning\u0026rsquo;s actual adoption in industry isn\u0026rsquo;t very high, and the reasons are pretty concrete.\nWriting an ontology is absurdly expensive. That whole set of concepts, relationships, and rules has to be defined one by one by experts who genuinely understand the business, and the logic has to stay self-consistent throughout. A medium-sized domain ontology taking a few people several months is normal. And the world keeps changing — when business rules change, the ontology has to change with them, and changing one rule can ripple across a large chunk of the structure. A lot of enterprise ontologies, once built, end up untouched because nobody dares modify them, gradually drift away from the real business, and eventually get abandoned.\nIt can\u0026rsquo;t tolerate vagueness or exceptions. Real-world rules are rarely clean. \u0026ldquo;A customer with an overdue bill should be suspended\u0026rdquo; — sounds like an ironclad rule, but in actual business there\u0026rsquo;s a pile of exceptions hanging off it: VIPs aren\u0026rsquo;t suspended, government/enterprise accounts aren\u0026rsquo;t suspended, disputed bills aren\u0026rsquo;t suspended, emergency communications aren\u0026rsquo;t suspended. Ontological logic handles this \u0026ldquo;true in principle, but with exceptions\u0026rdquo; situation very clumsily — you have to enumerate every exception as a formal definition, and it quickly bloats into something unmaintainable.\nIt\u0026rsquo;s rigidly single-minded. If a premise is wrong, it will meticulously propagate that error across the entire knowledge base — the more thoroughly it reasons, the more broadly it\u0026rsquo;s wrong. And if a premise is simply missing, it stays silent — it\u0026rsquo;ll quietly tell you \u0026ldquo;unknown,\u0026rdquo; but it won\u0026rsquo;t flag \u0026ldquo;hey, a rule seems to be missing here.\u0026rdquo;\nIt also has a scale ceiling. Logical reasoning has high computational complexity, and once the number of entities grows large, it stops being tractable — in practice you often have to fall back to a weakened, simplified version.\nSo who actually benefits # Putting all of this together, the scenarios where ontological reasoning genuinely pays off are actually pretty narrow, and need to satisfy several conditions at once: the concept hierarchy is fairly complex, there are too many implicit relationships to hand-write as rules, the knowledge is relatively stable and doesn\u0026rsquo;t change daily, and — most critically — the conclusions must be able to produce a derivation that can withstand scrutiny.\nHealthcare is a classic example. The SNOMED CT medical terminology ontology contains hundreds of thousands of concepts; a doctor enters a specific diagnosis, and the system can automatically classify it into the correct disease category, used for statistics, insurance settlement, and clinical decision support. This needs both a complex concept hierarchy and every classification step to survive being questioned.\nAuditing, compliance, and financial risk control are the same. You need to be able to answer a regulator asking \u0026ldquo;on what basis did you determine this transaction violates the rules\u0026rdquo; — and the answer can\u0026rsquo;t be \u0026ldquo;the model thought it looked like it.\u0026rdquo;\nConversely, if your domain\u0026rsquo;s rules are simple, enumerable, and don\u0026rsquo;t change often, a plain rule engine or database constraints will do the job just fine, and run faster too. Bringing in an ontology for that is overkill.\nA more interesting ending # Interestingly, over the past few years the relationship between ontological reasoning and large language models has shifted from \u0026ldquo;which one replaces the other\u0026rdquo; to \u0026ldquo;each patching the other\u0026rsquo;s weaknesses.\u0026rdquo;\nAn LLM\u0026rsquo;s classic flaw is hallucination — it\u0026rsquo;ll confidently say things without them being reliably true. An ontology\u0026rsquo;s classic flaw is cost — it\u0026rsquo;s reliably true, but too expensive to build. So a natural combination emerged: let the LLM read documents, extract concepts, and draft the ontology (turning months of work into days, with experts just reviewing), and then, in reverse, use the completed ontology to validate the LLM\u0026rsquo;s output — if a generated conclusion references a concept combination that doesn\u0026rsquo;t exist in the ontology, or that\u0026rsquo;s mutually exclusive within it, it gets blocked outright.\nOne handles \u0026ldquo;being able to figure it out.\u0026rdquo; The other handles \u0026ldquo;not getting it wrong.\u0026rdquo; No single technology today can do both well at once, and there\u0026rsquo;s no sign that\u0026rsquo;s changing anytime soon.\n","date":"2026-08-14","externalUrl":null,"permalink":"/posts/ontology-reasoning-explained/","section":"Blog","summary":"Databases only know what you explicitly store; ontological reasoning lets a machine derive thousands of unstated facts from a handful of concept definitions — powerful, expensive to build, and increasingly paired with LLMs to keep each other’s weaknesses in check.","title":"How Does a Machine \"Figure Out\" Something You Never Told It? A Look at Ontological Reasoning","type":"posts"},{"content":"Saw yesterday that Synology had officially released DSM 7.4, and I was tempted for a moment.\nThat said, for my home DS218+, I\u0026rsquo;ve always followed one principle: stability trumps everything. This NAS already handles most of the services at home — Immich, Podsync, Tailscale, Docker, a dozen-plus containers — and I\u0026rsquo;d only just gotten everything stably debugged. If a system upgrade broke compatibility for some service, I could end up spending a lot more time tracking it down and fixing it.\nSo yesterday I couldn\u0026rsquo;t quite bring myself to pull the trigger.\nCommunity feedback basically put my worries to rest # This morning I spent some time looking at feedback on Reddit and various forums. What surprised me a bit was that people\u0026rsquo;s overall impression of DSM 7.4 was actually pretty good. The upgrade process generally went smoothly, with no widespread horror stories. Docker, virtual machines, and common packages all basically kept working fine, and quite a few users even said things like: \u0026ldquo;nothing changed after the upgrade — which is actually a sign it\u0026rsquo;s stable enough.\u0026rdquo;\nWhat actually sparked the most discussion wasn\u0026rsquo;t DSM 7.4 itself, though — it was Synology\u0026rsquo;s restrictions on the new Storage Efficiency feature.\nStorage Efficiency became the biggest draw # I was originally just planning to wait and watch for a while. But while reading up on DSM 7.4\u0026rsquo;s new features, Storage Efficiency immediately caught my attention. It bundles online deduplication and space-optimization capabilities — for a NAS that stores photos, videos, and documents long-term, that looked pretty appealing.\nEven though the DS218+ at home isn\u0026rsquo;t a particularly powerful model, I couldn\u0026rsquo;t help thinking: what if it\u0026rsquo;s actually supported?\nSo I decided to upgrade.\nJumping from DSM 7.2 to DSM 7.4 # This was a cross-version upgrade. Before doing it, I did a configuration backup and confirmed all the Docker data was saved properly. Then I clicked upgrade.\nThe whole process went a lot more smoothly than I expected. The system automatically downloaded, installed, restarted, and migrated the configuration — every Docker container, shared folder, and network setting came through fully intact.\nOnce it was back up, I checked everything: all Docker containers started normally; Immich was accessible; Podsync was updating normally; Tailscale was online; and none of the other shared services were affected. The whole upgrade felt almost like a non-event.\nUnfortunately, I got excited too soon # The first thing I did after the upgrade finished was go try out Storage Efficiency. Once I opened Storage Manager, though, I found out: my DS218+ doesn\u0026rsquo;t support this feature at all.\nChecking the official docs and community discussion afterward, I learned that Storage Efficiency doesn\u0026rsquo;t just require a newer model — it also has to satisfy a whole set of Synology restrictions around hardware platform and storage pool configuration, which locks out a lot of the classic older models.\nSo the thing I was most looking forward to from upgrading to DSM 7.4 ended up not happening after all. Kind of a letdown.\nClosing thoughts # Still, looking back, the upgrade was worth it. At the very least, DSM 7.4 turned out more stable than I expected — the whole process went off without a hitch, and everything I already had running kept working, which itself was reassuring.\nAs for Storage Efficiency, it\u0026rsquo;s really more a matter of Synology\u0026rsquo;s product strategy than the DS218+ genuinely lacking the horsepower for it. From the community discussion, people weren\u0026rsquo;t really complaining about DSM 7.4 itself — it\u0026rsquo;s more that an increasing number of new features are getting tied to newer models and official drives, which understandably frustrates a fair number of longtime users.\nFor me, the biggest takeaway from this upgrade wasn\u0026rsquo;t actually a new feature — it was confirming something else: as long as you back up properly ahead of time, a cross-version DSM upgrade is nowhere near as scary as it sounds.\nAs for that Storage Efficiency feature I never got to try, I suppose it\u0026rsquo;ll just have to wait for the next NAS.\n","date":"2026-07-20","externalUrl":null,"permalink":"/posts/dsm-7-4-upgrade-storage-efficiency/","section":"Blog","summary":"Community reports convinced me DSM 7.4 was stable enough to risk on a production NAS — the upgrade went flawlessly, but the one feature I actually wanted, Storage Efficiency, turned out to be locked out of older hardware entirely.","title":"DSM 7.4 Upgrade Log: A Smooth Update With One Small Letdown","type":"posts"},{"content":"Jumped on the bandwagon recently and bought a DJI 4G module. At first I just wanted to see if I could hook it up to my NAS and use it as an SMS send/receive management tool. After a fair bit of tinkering, it turned out to be genuinely interesting — but definitely not a plug-and-play kind of thing.\nThe original goal # My initial goal was simple: hook this DJI 4G module up to one of my own devices to centrally manage SMS sending and receiving — ideally on the NAS, since it\u0026rsquo;s on long-term, easy to manage, and well-suited to running web services.\nLater I found people online who had gotten this module to identify itself as a Quectel EC25 and used it with VoHive, managing SMS, eSIM, and device status, even sending notifications through Telegram — so I started looking into whether I should reflash it or modify its device identity.\nTo reflash or not to reflash? # At first I was a bit wary of the word \u0026ldquo;reflash,\u0026rdquo; since it usually involves firmware and NV parameters, which carries real risk. After digging in, though, I confirmed this wasn\u0026rsquo;t reflashing firmware in the traditional sense — it was modifying the USB identification info via AT commands.\nThis DJI 4G module is essentially a variant of the Quectel EG25-G. Its default USB ID is DJI\u0026rsquo;s private ID: 2ca3:4006. After modifying it, the system can recognize it as a more generic Quectel EC25: 2c7c:0125.\nOnce changed, the Linux kernel can load the option, qmi_wwan, cdc_wdm, and similar drivers fairly naturally, and the device shows up as /dev/ttyUSB*, /dev/cdc-wdm0, and wwan0. This step opened up a lot more possibilities.\nWhy it didn\u0026rsquo;t end up on the NAS # I really did want to plug it into the NAS at first — it\u0026rsquo;s stable, quiet, always online, and seemed like the ideal host. But testing showed my NAS could see the USB device itself, but was missing the key kernel modules — option, qmi_wwan, cdc_wdm, usb_wwan. In other words, the NAS could tell \u0026ldquo;a USB device got plugged in,\u0026rdquo; but couldn\u0026rsquo;t recognize it as a usable cellular modem, serial device, or QMI network device.\nThat\u0026rsquo;s awkward — even running VoHive on the NAS, it wouldn\u0026rsquo;t actually be able to take control of the module.\nSo the final plan became: run the module off an OpenWrt router, with VoHive deployed there too; the NAS, if needed at all, would just act as a reverse proxy or access point.\nThat actually made more sense. A router is a networking device to begin with, and OpenWrt has more direct support for USB network adapters, QMI, and serial ports.\nDeploying to OpenWrt # Once recognized on OpenWrt, the module\u0026rsquo;s status looked normal: 2c7c:0125, /dev/cdc-wdm0, /dev/ttyUSB0 through /dev/ttyUSB3, wwan0.\nI then deployed VoHive on OpenWrt. It provides a web management interface showing module status, SIM info, an SMS center, an eSIM page, notification configuration, and more. The final access address was http://192.168.5.1:7575. Once deployed, VoHive started up normally and correctly recognized the module.\nWhat changed after modifying the device identity # After switching the device from DJI\u0026rsquo;s private ID to a Quectel EC25-compatible identity, the biggest change was a clear jump in how well the system could recognize it. Before, it was basically \u0026ldquo;a module that only serves DJI\u0026rsquo;s own ecosystem\u0026rdquo;; afterward, it became a cellular module that Linux can manage normally.\nWhat became possible: reading SIM card info; checking signal strength, network status, and registration status; managing SMS send/receive; managing the cellular module via QMI; web-based management paired with VoHive; configuring notifications through Telegram, webhooks, or email; in theory, support for eSIM/eUICC management; and, with a SIM that supports cellular registration, further configuring data connectivity. This step was really the key turning point of the whole project.\nThe use case I\u0026rsquo;ve got working now # What I currently have working: a SIM card that can register normally on the cellular network, plugged into the DJI 4G module, with centralized SMS management handled through VoHive running on OpenWrt.\nI can now view SMS messages in the web interface, and I\u0026rsquo;ve also set up a Telegram bot so that incoming texts get pushed straight to Telegram. This means verification codes, notification texts, and messages to backup numbers no longer need some phone kept permanently powered on with a SIM in it. The module sits in the router, VoHive handles management, Telegram handles alerts — and overall it\u0026rsquo;s turned out to be a genuinely convenient setup.\n","date":"2026-07-10","externalUrl":null,"permalink":"/posts/dji-4g-module-new-tricks/","section":"Blog","summary":"A DJI 4G module identifies itself with a private USB ID that Linux won’t drive properly — spoofing it as a generic Quectel EC25 unlocked full modem support, and it ended up running SMS management on an OpenWrt router instead of the NAS.","title":"Tinkering Log: Getting New Tricks Out of a DJI 4G Module","type":"posts"},{"content":"","date":"2026-07-10","externalUrl":null,"permalink":"/tags/tools/","section":"Tags","summary":"","title":"Tools","type":"tags"},{"content":" Why upgrade? # I recently saw that Immich had officially released version 3.0. As one of the most important apps on my NAS, I naturally paid attention to this major version update right away.\nBefore upgrading, I first compared what new features in 3.0 might actually be worth it, relative to the 2.7 version I was currently running.\nThis 3.0 release isn\u0026rsquo;t a minor version bump — it\u0026rsquo;s a fairly substantial feature upgrade.\nA few new features that caught my eye: built-in AI photo editing, letting you do simple photo edits directly inside Immich without exporting to other software; a more polished photo-management experience, including better browsing and refined details; continually strengthened search and AI capability, with more room for vision-model-based expansion down the line; and an upgraded underlying architecture, with the database\u0026rsquo;s vector engine and other components optimized to support future features.\nFor me, the photo-editing feature is the most valuable part. A lot of my photos get auto-synced from my phone to the NAS, and occasionally I need to crop, adjust brightness, or make some simple edit — previously that meant downloading it to my phone or computer to process, then re-uploading. If I can now do that directly inside Immich, the whole photo-management workflow becomes a lot more convenient.\nAfter weighing it all, I decided to upgrade the family\u0026rsquo;s Synology DS218+ to Immich 3.0.\nMy environment # Before upgrading: Synology DS218+, DSM 7, deployed via Docker Compose, Immich 2.7.x, the official PostgreSQL database image, over one hundred thousand photos, with AI facial recognition already completed.\nSince the photo library was already fairly large, I decided to follow the official recommendation and do a full backup first before upgrading.\nPre-upgrade checks # First, I confirmed my current Docker Compose configuration. Checking it, I found I was already on the official new-style Compose file, and the database had already been switched to the new VectorChord image — so the database-migration steps in the official upgrade docs didn\u0026rsquo;t apply to my setup, and I didn\u0026rsquo;t need to touch the database config at all.\nI then backed up a few key files: the PostgreSQL database, docker-compose.yml, and .env. That way, even if something went wrong during the upgrade, I could recover quickly.\nStarting the upgrade # The upgrade process itself turned out to be very simple — basically just following the official docs. First, pull the latest images:\ndocker compose pull Then restart the containers:\ndocker compose up -d The only issue I ran into during the whole process was that the image download was fairly slow. Immich\u0026rsquo;s images are hosted on GitHub Container Registry (GHCR), and one layer of the immich-server image, over 300MB, downloaded very slowly — on my network it took quite a while to finish. Worth a bit of patience here, or configuring a proxy for the NAS ahead of time.\nThe first boot after upgrading # Once the image update finished, I immediately tried to open http://NAS_IP:2283 — and the browser just wouldn\u0026rsquo;t load it. My first thought was that the upgrade had failed. So I started troubleshooting step by step.\nFirst, I checked the container status with docker compose ps, and found: PostgreSQL fine, Redis fine, Machine Learning fine, and the Immich Server had started but was still showing health: starting.\nI kept looking at the logs with docker logs immich_server, which showed \u0026ldquo;Running migrations,\u0026rdquo; and I could see the system was working through the facial-recognition and vector indexes for over a hundred thousand photos.\nThat\u0026rsquo;s when I realized: it wasn\u0026rsquo;t a failed upgrade — Immich 3.0\u0026rsquo;s very first startup was running a database migration and index check. For a DS218+ with only two CPU cores, this step just takes a while. As long as I was patient, once the service finished initializing, the health status went from \u0026ldquo;starting\u0026rdquo; to \u0026ldquo;healthy,\u0026rdquo; and the web page came back up normally.\nWrap-up # The whole upgrade went more smoothly than I expected — no compatibility issues, and no need to re-import photos or re-run face recognition. Immich has been gradually evolving from a great open-source photo app into an increasingly full-featured personal photo-management platform. For me, the biggest thing I\u0026rsquo;m hoping to get out of this upgrade is being able to do everyday photo edits directly on the NAS, further cutting down on shuffling photos back and forth between different devices.\n","date":"2026-07-09","externalUrl":null,"permalink":"/posts/immich-3-upgrade-ds218plus/","section":"Blog","summary":"The official 3.0 release with built-in AI photo editing was tempting enough to risk an upgrade on a stable NAS setup — the process went smoothly, with the only surprise being a slow first-boot database migration that looked like a crash.","title":"Immich 3.0 Upgrade Log: Taking a DS218+ From 2.7 to 3.0","type":"posts"},{"content":"Three podsync containers run on my home NAS, converting a channel into podcast audio for my podcast client. Normally this setup is close to maintenance-free — until one day the logs started repeatedly showing the same error. This is the full troubleshooting record — worth writing down because the investigation went through three wrong turns, and each wrong turn taught me more about how the system actually works than getting it right on the first guess would have.\nI. The symptom # In the logs, one specific feed failed on every update cycle, while the others were fine:\nERRO ... failed to update feed: https://www.youtube.com/channel/UC.../videos error=\u0026#34;update failed: failed to parse duration : bad format string\u0026#34; Notice the value after \u0026ldquo;duration\u0026rdquo; in the error is empty. And it only affected feed A01 — A02 and A03 kept downloading normally, which tells you Podsync updates feeds one at a time, so one failing doesn\u0026rsquo;t drag down the others. The actual damage was small, but this error kept recurring, and as long as the trigger condition was present, it wasn\u0026rsquo;t going to fix itself.\nII. Tracking it down: three wrong turns # First guess: add a filter to block it — failed.\nMy first instinct was: there\u0026rsquo;s probably an \u0026ldquo;upcoming premiere/livestream\u0026rdquo; video at the top of the list with no duration, and it\u0026rsquo;s breaking the parser. So I added a yt-dlp match-filter to exclude that kind of video:\nyoutube_dl_args = [..., \u0026#34;--match-filter\u0026#34;, \u0026#34;live_status!=is_upcoming\u0026#34;] After restarting, the error was still there.\nSecond guess: the filter was applied at the wrong stage — partly right, but still incomplete.\nChecking the docs, I understood: youtube_dl_args only gets passed to yt-dlp at the download stage, but the crash was happening earlier, during list construction. The filter was applied on the download side, and couldn\u0026rsquo;t reach the parsing that happens on the list side — so naturally it did nothing. Similarly, Podsync\u0026rsquo;s own built-in filters (title, duration filtering) also only run after parsing is complete, so they couldn\u0026rsquo;t block it either.\nThis step got the direction right, but at the time I still assumed the list was also pulled via yt-dlp. Only once I really understood the nature of the error did I land on the third and final diagnosis.\nThird guess (final): this is a bug in Podsync\u0026rsquo;s Go binary — no config can fix it.\nThe key clue was the exact wording \u0026ldquo;bad format string\u0026rdquo; — it comes from how Podsync parses ISO8601 durations (the PT#H#M#S format) and errors out on an empty value. And ISO8601 durations are only ever returned by the YouTube Data API; the duration yt-dlp gives you is a floating-point number of seconds, in a completely different shape.\nSo the whole chain became clear: fetching the list = Podsync calling the YouTube Data API and parsing each video\u0026rsquo;s contentDetails.duration (ISO8601) one by one; downloading = yt-dlp (cookies / node / GetPOT all live on this side).\nThat channel had several long-pending scheduled premieres queued up (premiere times set for the next day), and for videos that haven\u0026rsquo;t gone live yet, the API returns an empty string for duration. Podsync\u0026rsquo;s older Go binary, hitting that empty value, threw \u0026ldquo;bad format string\u0026rdquo; directly and crashed the whole feed.\nConclusion: the crash lives inside Podsync\u0026rsquo;s Go code — no configuration-level filter can save it. One ironic bit of supporting evidence: both of the premiere titles that were tripping me up actually contained \u0026ldquo;XX minutes,\u0026rdquo; and I already had not_title = \u0026quot;XX minutes\u0026quot; configured specifically to exclude them — but the parsing crashed before the filter even got a chance to run.\nI found the official issue #638 covering exactly this problem — closed, but with no linked upstream PR. The most reasonable read: the person who filed it (fqx, also the maintainer of the podsync-with-yt-dlp fork) fixed it in their own fork and closed the upstream issue, but the fix was never merged into the main branch. So upgrading the official image was very likely not going to help — the patch only exists in fqx\u0026rsquo;s fork.\nIII. The fix: switching to a build that tolerates an empty duration # I confirmed I was running the official mxpv/podsync:latest image from three years ago (Docker Hub), and that the :latest tag doesn\u0026rsquo;t automatically refresh the Go binary inside an already-running container — yt-dlp stays current via self_update, but the Podsync binary itself was stuck on an old version. That explains why downloading kept working fine while parsing kept crashing: the download capability and the crash point were two entirely separate code paths.\nA detour: trying to graft the new binary into the old image — failed.\nMy first idea was surgical: use a multi-stage build to COPY just fqx\u0026rsquo;s fixed Podsync binary into my current, downloading-just-fine old image, leaving everything else untouched:\nFROM ghcr.io/fqx/podsync-with-yt-dlp:latest AS patched FROM mxpv/podsync:latest COPY --from=patched /app/podsync /app/podsync It built successfully, but on startup:\nexec /app/podsync: no such file or directory The file was definitely there. This \u0026ldquo;no such file or directory\u0026rdquo; is actually the classic misleading error on Linux for a missing dynamic library or loader — the gap between fqx\u0026rsquo;s binary (built against a 2024 environment) and the three-year-old image\u0026rsquo;s musl/libstdc++ versions was too wide, and the dependencies didn\u0026rsquo;t line up. Grafting a binary across base images from different eras just isn\u0026rsquo;t reliable.\nThe actual fix: use the fqx image as the runtime base.\nFlipping it around — use the fqx image directly as the runtime instead of stuffing things into the old one. It comes with a working binary and matching environment out of the box, and Podsync\u0026rsquo;s self_update upgrades the bundled, slightly older yt-dlp to the latest version on startup automatically. Once the test container was up:\nthe binary started normally (confirming the earlier issue really was a missing library); yt-dlp self-updated from 2024.05.27 to 2026.07.04 (exactly matching what it had before); A01 no longer threw \u0026ldquo;bad format string\u0026rdquo; and started building the feed normally. Main problem solved. I then switched all three containers over to the fqx image one at a time.\nOne rule I had to stick to strictly: all three containers mount the same /app/data, and Podsync\u0026rsquo;s badger database uses a single-writer lock, so the switch had to happen one container at a time — stop the old one first, then start the new one; at any given moment, only one Podsync process can touch a given data directory.\nDuring the switch I hit one small snag: copying the existing node binary straight from the mounted volume threw \u0026ldquo;symbol not found,\u0026rdquo; because that node binary had been compiled against the old image\u0026rsquo;s libstdc++. The fix was running apk add --upgrade libstdc++ in the new container first to bring the library up to date, after which node loaded fine.\nIV. An unexpected sequel: the PO Token had actually been empty the whole time # After the migration, I wanted to confirm whether the download safeguard (GetPOT) was still in place — and the probing kept going sideways. Running yt-dlp by hand returned \u0026ldquo;executable not found,\u0026rdquo; because Podsync still uses the old name youtube-dl, and it\u0026rsquo;s not on the default PATH. Once I used the right name, I finally saw the key line:\n[debug] [youtube] [pot] PO Token Providers: none Note: Providers: none. There are two separate concepts here — Extractor Plugins: GetPOT only tells you the GetPOT framework itself is loaded, but the framework needs an actual provider behind it to actually produce a PO Token. All three containers showed none — meaning the framework was present, but with no provider, no PO Token was actually being generated.\nDownloads had been working fine up to that point only because yt-dlp itself still had a fallback mechanism based on EJS, and YouTube hadn\u0026rsquo;t yet been strictly requiring a PO Token for these particular requests. It was \u0026ldquo;working, but not solidly.\u0026rdquo; Since I was fixing things anyway, I went ahead and set up the proper standard solution.\nFilling the gap: the bgutil PO Token provider (HTTP mode)\nThe architecture: run a standalone bgutil provider container (an HTTP service on port 4416) that all three Podsync containers share.\nStart the provider container: docker run --name bgutil-provider -d --restart unless-stopped --init \\ -p 4416:4416 brainicism/bgutil-ytdlp-pot-provider:latest Install the plugin side inside each Podsync container (the fqx image only ships the framework, not this provider\u0026rsquo;s plugin): docker exec \u0026lt;container\u0026gt; python3 -m pip install -U bgutil-ytdlp-pot-provider Append the provider address to each container\u0026rsquo;s yt-dlp config (all three Podsync containers sit on the default bridge network, so they reach the host-published port through the gateway address 172.17.0.1): --extractor-args \u0026#34;youtubepot-bgutilhttp:base_url=http://172.17.0.1:4416\u0026#34; After restarting, all three containers consistently showed:\nPO Token Providers: bgutil:http-1.3.1 (external) (The other two lines, script-node/script-deno showing \u0026ldquo;unavailable,\u0026rdquo; are expected — I only deployed HTTP mode, which is the officially preferred option anyway.)\nOne persistent gotcha: the pip-installed plugin and that config line both live inside the container, not on the mounted volume — so they disappear the moment the container gets rebuilt. So the whole set of steps — \u0026ldquo;upgrade libstdc++ + restore node + write the ejs/bgutil config + install the plugin\u0026rdquo; — needs to be baked into the container\u0026rsquo;s rebuild/init script, or it has to be redone from scratch every time.\nV. Verification # A01\u0026rsquo;s crash is genuinely fixed. The latest update cycles ran cleanly end to end: updating A01 → running cleaner → creating A01.xml → next update, with no more \u0026ldquo;bad format string.\u0026rdquo; Those \u0026ldquo;XX minute\u0026rdquo; premiere entries got quietly absorbed by fqx\u0026rsquo;s empty-duration handling and never entered the download queue — the result stayed just as clean.\nThe PO Token chain is genuinely working. All three containers show PO Token Providers: bgutil:http-1.3.1 (external) as available; debug output also confirms the plugin directory is recognized (Plugin directories: /usr/lib/python3.10/site-packages/yt_dlp_plugins).\nA false alarm about 429s. During manual testing I ran into HTTP Error 429: Too Many Requests, and briefly thought I\u0026rsquo;d need to route traffic through WARP to change the exit IP. But looking back, that was me manually re-requesting the same video seven or eight times in a short window, forcing the rate limit — not normal behavior. Checking the actual run logs across all three feeds, there wasn\u0026rsquo;t a single 429 / \u0026ldquo;Sign in to confirm\u0026rdquo; / bot error — just real \u0026ldquo;successfully downloaded file\u0026rdquo; entries. So the normal, low-frequency schedule (every 3 hours) never triggers it at all, and WARP wasn\u0026rsquo;t needed — if anything, WARP\u0026rsquo;s exit IPs are shared Cloudflare ranges that are more likely to get flagged as bot traffic by YouTube, so it probably would have hurt rather than helped.\n(Along the way I also spotted an unrelated playlistNotFound (404) in one container\u0026rsquo;s log — a source channel that had been deleted or had its ID changed — to be cleaned up separately.)\nVI. Takeaways # A few things worth carrying forward: the exact wording of an error is the best clue for locating a bug — \u0026ldquo;bad format string\u0026rdquo; plus an empty value pointed straight at ISO8601 parsing, which in turn pinned the problem down to the API list stage rather than the download stage, moving the whole investigation from yt-dlp to Podsync itself in one sentence. Understand exactly which stage each parameter applies to — youtube_dl_args only governs downloading, while list construction goes through the YouTube Data API; that mismatch was the root cause of my first failed attempt. Docker\u0026rsquo;s :latest tag doesn\u0026rsquo;t automatically refresh the binary inside an already-running container — yt-dlp staying current via self_update can mask the fact that the underlying Go binary is actually quite old; the two need to be considered separately. Don\u0026rsquo;t graft binaries across base images from different eras — \u0026ldquo;no such file or directory\u0026rdquo; is often really \u0026ldquo;missing library\u0026rdquo; in disguise; rather than stuffing a new binary into an old environment, it\u0026rsquo;s more reliable to just use the new environment as the base. A framework being loaded isn\u0026rsquo;t the same as a capability actually working — GetPOT being loaded doesn\u0026rsquo;t mean a PO Token is actually being generated; the real test is whether PO Token Providers lists an available provider. And distinguish \u0026ldquo;triggered by testing\u0026rdquo; from \u0026ldquo;real production behavior\u0026rdquo; — a 429 triggered by manual, high-frequency probing shouldn\u0026rsquo;t be treated as an everyday problem; confirm what\u0026rsquo;s actually happening in the real logs before deciding whether to change your exit path at all — often the best fix is doing nothing.\nFinal state: all three containers now run on the fqx image, yt-dlp at 2026.07.04, A01 no longer crashes, and downloads have a proper bgutil PO Token safety net — noticeably more solid than before the issue came up.\n(Side note: working through this kind of problem, I noticed Claude clearly outperforming ChatGPT.)\n","date":"2026-07-09","externalUrl":null,"permalink":"/posts/podsync-po-token-crash-troubleshooting/","section":"Blog","summary":"One feed kept crashing Podsync with a cryptic ‘bad format string’ error — three wrong turns later, the real fix meant switching Docker images and realizing YouTube’s PO Token wasn’t actually being generated at all.","title":"Tracking Down a Podsync 'Empty Duration' Crash: From a Bad Format String to Filling In the PO Token Gap","type":"posts"},{"content":"","date":"2026-07-09","externalUrl":null,"permalink":"/tags/troubleshooting/","section":"Tags","summary":"","title":"Troubleshooting","type":"tags"},{"content":"The recent incident where malware got deployed on my private VPS actually affected me less through the process of removing the virus, and more by making me realize another problem that\u0026rsquo;s easy to overlook — data safety.\nBefore, I mostly focused on server security: closing ports, minimal privilege, firewalls, monitoring alerts\u0026hellip; all of that matters. But once I really sat down and thought it through, I realized that no matter how thorough your security measures are, nobody can guarantee a server will never run into trouble.\nWhat actually reduces risk isn\u0026rsquo;t just defense — it\u0026rsquo;s recoverability.\nSo after finishing the Sentinel security-monitoring system, I started filling in another gap: an application data backup system.\nWhy bother with backups # For my website, the genuinely valuable part isn\u0026rsquo;t the code. Code can be restored from GitHub and redeployed; what\u0026rsquo;s genuinely hard to recover is the business data sitting in the SQLite database, the config files, and everything that accumulates as the site keeps running.\nIf a server gets deleted, a disk fails, or it gets attacked again, no backup means that data could be gone permanently. So this round of work had one clear goal: whichever server runs into trouble, the business should be recoverable in the shortest time possible.\nLayer one: automatic local backups on the server # First, I added a standalone backup script for the project. The backup strategy splits into two categories.\nData backups: run automatically every day, using the SQLite Backup API to export the database (avoiding a direct copy of a live SQLite file), automatically verifying database integrity (PRAGMA integrity_check = ok), and keeping 60 days of history by default.\nCode backups: packaged automatically every week, automatically excluding irrelevant content like .git, .venv, cache directories, runtime data, and the backup directory itself, keeping 12 weeks of history by default.\nThis way, every backup produces a timestamped archive, and I can roll back to any historical version at any time.\nLayer two: automated scheduling via systemd # Backups can\u0026rsquo;t depend on being run manually. So I added systemd timer jobs to fully automate the whole process. Currently: data backups run daily, code backups run weekly. All the timers are installed and verified to trigger correctly. Going forward, even if no one logs into the server for a long stretch, new backups will keep getting generated.\nLayer three: off-site backup to the NAS # Local backups don\u0026rsquo;t solve the problem of the server itself being destroyed. If the VPS gets deleted, the disk fails, or the whole machine becomes unrecoverable, every local backup gets lost right along with it.\nSo I added an off-site backup as well. The whole setup uses a NAS-initiated pull model:\nVPS │ SSH + rsync │ Synology NAS (Docker) The NAS connects to the VPS on a schedule through a Docker container and syncs the server\u0026rsquo;s backup directory down locally. Once synced, the backups live in their own dedicated data partition on the NAS, not taking up system-partition space, and leaving room to easily extend the setup to more projects later. This way, even if the VPS is completely destroyed, a full copy of both the data and the code backups remains intact.\nLayer four: recovery documentation # Backups only really prove their worth at the moment of recovery. So I also put together a complete backup document covering: the local backup method, how the automated backup is deployed, the NAS Docker configuration, the data-recovery process, and the code-recovery process. I also updated the project status doc and GitHub configuration alongside it, so the whole backup system stays maintainable long-term instead of depending on memory.\n","date":"2026-07-03","externalUrl":null,"permalink":"/posts/vps-breach-to-backup-system/","section":"Blog","summary":"After cleaning up malware off a VPS, the bigger realization was that defense alone isn’t enough without recoverability — so I built a four-layer backup system covering local snapshots, scheduled automation, off-site NAS sync, and recovery documentation.","title":"From a Compromised VPS to Building a Real Website Backup System","type":"posts"},{"content":"Recently spent nearly all my free time on VPS security investigation.\nIt started when an Oracle Cloud VPS was found running what looked like a malicious program. In the end it didn\u0026rsquo;t cause any obvious damage, but the whole incident drove home a point:\nThe real danger isn\u0026rsquo;t that a server gets attacked — it\u0026rsquo;s that it gets attacked for a long time while you have no idea.\nThis incident pushed me to redesign my entire VPS security-monitoring system from scratch. From the initial discovery of the anomaly, through emergency response, to building out the monitoring system and continuously upgrading it, it really shifted how I think about operations.\nI. Discovering the problem: a security risk that had persisted for days # What I first received wasn\u0026rsquo;t an intrusion alert — it was a notice from a self-written inspection script flagging a suspicious high-CPU process.\nLogging into the server, I quickly found something off. The process was running out of /tmp, with a randomized filename, chewing up a lot of CPU over a long period. Its parent process pointed to LiteLLM, and further digging turned up several Python loaders and already-exited zombie processes.\nBy the time I looked further, every program involved had already been deleted, leaving only traces of having run.\nThe whole behavior pattern matched a very common style of Linux cryptomining trojan: download the malicious program, run it temporarily, delete itself, leave no files behind.\nI never managed to find a mining-pool address or a sample of the program itself, so I couldn\u0026rsquo;t confirm it with 100% certainty — but taken together, the behavioral characteristics point pretty clearly to a typical CPU-mining trojan.\nWhat really unsettled me: based on the logs, this program had been running for close to twenty days. If the inspection script hadn\u0026rsquo;t happened to trigger, I wouldn\u0026rsquo;t have even known the server had ever executed a malicious program.\nII. Emergency response: contain the risk first, find the cause second # Once I confirmed the anomaly, I didn\u0026rsquo;t rush to find the entry point — I focused on containing the risk first.\nFirst I checked whether the system had been further compromised: SSH login records, authorized_keys, cron jobs, systemd services, network connections, newly listening ports, Docker containers, temp directories. Fortunately, I found no persistent backdoor and no unusual network connections.\nI then checked every API key stored in LiteLLM. Since the server had run an unknown program, and even though I found no signs of key abuse, I rotated all of them anyway to avoid the risk spreading further — including the Gemini API key and the LiteLLM master key.\nAt the same time, I reassessed the risk around Oracle Cloud\u0026rsquo;s Free Tier. Based on their official abuse policy, as long as there\u0026rsquo;s no sustained mining, DDoS, or spam activity, the odds of the account getting banned are fairly low.\nIn the end, I decided: this server would no longer be used — I\u0026rsquo;d reinstall it and rebuild a trusted environment. When it comes to security, there\u0026rsquo;s ultimately no meaningful difference between \u0026ldquo;suspected compromised\u0026rdquo; and \u0026ldquo;actually compromised.\u0026rdquo;\nIII. Rethinking monitoring: without monitoring, there is no security # One thing kept coming up throughout the investigation: a lot of the anomalies had already happened — but left no evidence behind. For example: the /tmp program had already been deleted, the Python loader had already been deleted, the temporary port had already closed, high CPU usage had already returned to normal. By the time someone logged into the server, the scene had already vanished.\nThat made me realize: the traditional approach of \u0026ldquo;notice a problem, then log in and investigate\u0026rdquo; is increasingly unable to keep up with today\u0026rsquo;s malware.\nSo I started redesigning the entire monitoring system from the ground up.\nIV. Building a multi-layer monitoring system # I\u0026rsquo;ve now put together an initial four-layer monitoring architecture.\nLayer one: Healthchecks. Monitors whether every scheduled task is still running normally. If any cron job stops running, I get notified immediately.\nLayer two: Sentinel self-checks. Every server periodically sends a self-check message to Telegram, e.g. \u0026ldquo;✅ Sentinel self-check: Telegram channel normal.\u0026rdquo; If a given server suddenly stops sending self-checks — even if Healthchecks itself looks fine — it means the monitoring program itself may have failed. During this investigation, I actually found three servers that had stopped sending Sentinel self-checks. The VPS itself was fine, Healthchecks was fine, but it turned out there were gaps in how Sentinel had been deployed. This confirmed: the monitoring program itself also needs to be monitored.\nLayer three: Security scanning. Currently able to automatically detect: new listening ports, suspicious high-CPU processes, SSH login changes, root logins, newly added users, systemd service changes, cron changes, Docker anomalies, API key risk, and executable files in temp directories. All anomalies get pushed to Telegram in one place.\nLayer four: Manual review. Automated monitoring is responsible for catching problems first. The actual judgment call still needs a human, because security\u0026rsquo;s biggest enemy isn\u0026rsquo;t necessarily a missed detection — it\u0026rsquo;s also false positives.\nV. The monitoring system\u0026rsquo;s first \u0026ldquo;exam\u0026rdquo; # Right after the monitoring went live, I quickly got a string of alerts. One flagged a new UDP listening port — I logged in right away to check, and it turned out to just be system components like WireGuard and rpcbind. Another flagged suspicious high CPU — and it turned out to be the inspection script itself, misidentifying its own ps command process as high-CPU usage.\nNone of these false positives were a real security risk, but they made something clear to me: monitoring not only needs to catch anomalies, it needs to minimize false positives as much as possible. Otherwise, over time, the genuinely important alerts start getting ignored too.\nVI. Continuing to upgrade the monitoring system # Based on these investigations, I\u0026rsquo;ve now planned out the next stage of upgrades for Sentinel, including: secondary confirmation for new ports to filter out transient listeners, a process whitelist mechanism, automatic evidence preservation, SHA256 verification, hash monitoring for executable files, detection of Docker config changes, monitoring for Caddy/Xray config changes, API key leak detection, automatic tracking of how many servers are online, and automatic consistency checks between Healthchecks and the server inventory.\nGoing forward, when I get an alert, I don\u0026rsquo;t just want to know \u0026ldquo;an anomaly was found\u0026rdquo; — I want to be able to see directly: which process, who started it, where it came from, whether it\u0026rsquo;s on the whitelist, its risk level, and whether evidence has been preserved. The real goal is being able to judge the risk the moment an alert arrives, instead of having to log into the server and start investigating from scratch.\nVII. The biggest takeaway # The biggest thing I got out of this wasn\u0026rsquo;t finding some specific vulnerability or confirming the exact attack vector — it was rethinking what security operations actually means.\nI used to think security meant a system that never gets attacked. Now I believe more in this: security means being able to detect an attack quickly and recover fast.\nFor someone maintaining several VPS instances on their own, absolute security just isn\u0026rsquo;t realistic. What\u0026rsquo;s actually achievable is building a continuously running monitoring system: one that can catch anomalies, verify that the monitoring itself is working, preserve evidence as much as possible, keep reducing false positives, and respond in the shortest time possible.\nSecurity isn\u0026rsquo;t a few installed tools, and it isn\u0026rsquo;t a one-time hardening pass — it\u0026rsquo;s an ongoing engineering effort. This suspected-trojan incident felt like an expensive but worthwhile hands-on lesson. It pushed me from a mindset of \u0026ldquo;investigate after something breaks\u0026rdquo; toward \u0026ldquo;continuously monitor, detect quickly, respond promptly.\u0026rdquo;\nGoing forward, I\u0026rsquo;ll keep refining Sentinel, turning it into a lightweight security-monitoring platform suited for managing multiple personal VPS instances — so my servers become genuinely observable, traceable, alertable, and recoverable.\n","date":"2026-07-02","externalUrl":null,"permalink":"/posts/vps-security-incident-reflection/","section":"Blog","summary":"A cryptomining-style trojan ran undetected on my Oracle Cloud VPS for nearly 20 days — the real lesson wasn’t fixing that one box, but realizing monitoring itself needs to be monitored.","title":"Reflections After a VPS Security Incident: From Discovery to Building a Full Monitoring System","type":"posts"},{"content":" Foreword # For years, Dropbox was the single most important platform for managing and syncing my files — work material, study notes, family documents, and various project files all relied on it to stay in sync across devices. This past weekend, I\u0026rsquo;d just finished an important piece of work: using Immich to replace Google Photos, bringing my photos under unified, self-hosted management. Once the photos had migrated, a new question naturally came up — if the photos had come home to my own NAS, shouldn\u0026rsquo;t my files too? So I started looking for a Dropbox alternative. After a round of research and testing, I settled on Seafile. And, more interestingly, along the way I discovered Seafile could do more than just replace Dropbox\u0026rsquo;s syncing — it could be paired with AI capability to gradually build out a real knowledge base.\nWhy Seafile # Within the NAS ecosystem there\u0026rsquo;s no shortage of file-sync options — Synology Drive, Nextcloud, OwnCloud, Resilio Sync, Seafile. I ended up going with Seafile, mainly for three reasons.\nExcellent sync performance. Dropbox\u0026rsquo;s biggest strength has always been syncing, and Seafile is one of the few open-source options that comes close to that experience. It uses block-level sync — only the changed parts of a file get uploaded — which makes large-file syncing efficient, uses network bandwidth better, and stays stable across multiple devices. In actual use, the sync experience fully met my expectations.\nData stays in my own hands. Dropbox is, at its core, still a SaaS service — convenient, but the data ultimately lives on a third party\u0026rsquo;s platform. Seafile runs on my own NAS: full control over the data, no subscription storage limits, the ability to layer on additional backups through the NAS itself, and no exposure to future platform policy changes. This lines up exactly with the same logic behind choosing Immich over Google Photos.\nLower resource usage. My hardware is a Synology DS218+ with 10GB of RAM. Compared to an all-in-one platform like Nextcloud, Seafile focuses specifically on file syncing itself, which means simpler deployment, lower resource usage, and stable long-term operation — very friendly to an older NAS.\nFrom file syncing to knowledge management # After using it for a while, I ran into an issue: Seafile handles file syncing well, but it doesn\u0026rsquo;t solve a different problem — how do you quickly find the knowledge buried inside your files? As the material kept piling up — project plans, meeting notes, study notes, technical docs, blog material — it was all safely stored in Seafile, but actually finding a specific piece of content still meant recalling a filename, recalling a directory structure, or manually searching.\nMeanwhile, Dropbox itself has gradually evolved toward a smart content platform in recent years — AI search, content summarization, smart Q\u0026amp;A, document linking — capabilities that the Seafile community edition simply doesn\u0026rsquo;t have.\nSo I started wondering: could Seafile just focus on storage, while the AI capability gets bolted on externally? That led to the architecture below.\nSeafile + Open WebUI + RAG # The overall architecture:\nSeafile │ ▼ Seaf-CLI real-time sync │ ▼ Local NAS directory │ ▼ oikb auto-sync │ ▼ Open WebUI Knowledge │ ▼ LiteLLM │ ├── Gemini Flash (Q\u0026amp;A) └── Gemini Embedding (vectorization) The overall idea is quite simple:\nLayer one: Seafile. Responsible for file storage, syncing, and version management. No AI functionality lives at this layer.\nLayer two: Seaf-CLI. This is the most critical step in the whole setup. Open WebUI can\u0026rsquo;t read Seafile directly, so I used the official seaf-cli client to sync Seafile libraries into a local NAS directory in real time. Once synced, that directory is just a normal folder — /work, /home, /study, /private — and every file change gets picked up automatically.\nLayer three: Open WebUI. Handles document parsing, vector storage, RAG retrieval, and AI conversation. No local model here — it connects to the already-deployed LiteLLM gateway instead.\nLayer four: LiteLLM. Manages all the models in one place. Current setup: a Q\u0026amp;A model, gemini-2.5-flash, responsible for generating the final answers; and an embedding model, gemini-embedding-001, responsible for vectorizing documents and questions and doing similarity search — an essential layer for building a real knowledge base.\nAutomatic incremental sync into the knowledge base # If every new file had to be manually uploaded into the knowledge base, this whole setup would lose its point. So I brought in oikb (Open WebUI Knowledge Base Sync), which handles automatically scanning the directory, ingesting new files automatically, updating modified files automatically, cleaning up deleted files automatically, and running incremental syncs on a schedule. That closes the loop end to end:\nFile placed into Seafile │ ▼ Auto-synced to NAS │ ▼ Open WebUI auto-updated │ ▼ Vectors auto-generated │ ▼ Knowledge base updates in real time The whole process requires no manual intervention.\nHow it actually works in practice # Once deployed, the biggest change is that I no longer rely on directory structure to find material — I just ask my own knowledge base directly. For a work-related question, something like \u0026ldquo;summarize the progress on the customer-service agent project over the past three months.\u0026rdquo; For a personal one, \u0026ldquo;what did I previously write about the Immich migration?\u0026rdquo;\nWhere things stand now # As of now, Seafile has basically met my syncing needs in place of Dropbox. But if you only look at syncing ability, it\u0026rsquo;s still just a file-management tool. What actually showed me its real value was pairing it with AI. I\u0026rsquo;ve increasingly come around to a particular way of thinking: storage and intelligence should be decoupled. Seafile handles saving the data. Open WebUI handles understanding the data. LiteLLM handles connecting the models. That way, each layer can evolve independently — even if Seafile eventually gets swapped for a different sync platform, or Gemini gets swapped for Claude, or Open WebUI gets swapped for a new RAG system, the overall architecture still holds.\nClosing thoughts # Migrating from Google Photos to Immich, then from Dropbox to Seafile, has completed two important steps in taking back my personal data: photos back under my own control, files back under my own control. And now a third step is underway: making that data genuinely valuable as knowledge. Seafile is still in a trial phase for me right now, and I haven\u0026rsquo;t fully decided whether it\u0026rsquo;ll completely replace Dropbox, since there\u0026rsquo;s still some gap between the two when it comes to intelligent document management. But as Open WebUI, RAG, and large-model capability keep improving, I find myself looking forward to a future where files just sit there in storage while the knowledge inside them gets actively surfaced, searched, and put to use. Maybe the day I no longer need to remember where a file is — and can just ask my own data a question instead — is the day this whole setup will really be finished.\n","date":"2026-06-15","externalUrl":null,"permalink":"/posts/dropbox-to-seafile-nas-knowledge-base/","section":"Blog","summary":"After bringing photos home to Immich, files were next — Seafile replaced Dropbox for syncing, then got paired with Open WebUI, RAG, and LiteLLM to turn a plain file store into a searchable, AI-queryable knowledge base.","title":"From Dropbox to Seafile: Letting the Files on My NAS Slowly Grow Into a Knowledge Base","type":"posts"},{"content":" Foreword # Photos capture the small moments of everyday life. For the past decade and more, I\u0026rsquo;ve relied on Google Photos as my main platform for storing and managing them.\nWhen Google Photos first launched, it was arguably one of the best photo management tools around — unlimited storage, powerful search, excellent facial recognition, and solid timeline organization meant I uploaded nearly every photo I had to it.\nBut once Google dropped the free unlimited-storage policy, photo storage gradually turned into an ongoing expense. As the number of photos and videos kept growing, I found myself paying for extra space every month. On top of that, network conditions made accessing Google Photos increasingly inconvenient.\nThe bigger problem, though, was that as I accumulated more devices, my photos started scattering across multiple platforms.\nGoogle Photos: my main photo library iCloud Photos: some of my iPhone photos Local photos on my Huawei phone Photos and videos from a DJI drone Action-camera footage Local storage on the NAS Because different devices and platforms sync in different ways, the same photo would often end up in multiple places at once.\nThe result: more and more photos, and yet actually finding a specific one got harder and harder.\nSo I decided to build a unified photo-management hub using Immich, to bring everything together and handle deduplication automatically.\nWhy Immich # When picking a photo-management platform, I compared several options: Synology Photos, PhotoPrism, Nextcloud Photos, and Immich.\nI ultimately went with Immich, for three main reasons.\nFirst, the experience is closest to Google Photos. Immich offers timeline browsing, map browsing, facial recognition, smart search, automatic phone backup, and multi-user management — for a NAS user, that\u0026rsquo;s already enough to replace Google Photos.\nSecond, full control over the data. Every photo lives on my own NAS. No more dependence on a third-party platform, and no more worrying about future policy changes bringing extra costs.\nThird, strong deduplication. This was actually the thing I cared about most for this migration. My photo sources were genuinely messy — Google Photos, iCloud, a Huawei phone, DJI gear, and various local directories — with a lot of overlap between them. Immich itself has solid duplicate-photo detection, and the immich-go tool further strengthens deduplication during import. That means no matter where a photo originally came from, only one copy ends up in Immich in the end — exactly the outcome I wanted.\nStep 1: upgrading Immich # Before starting the migration, I upgraded Immich itself. My installed version was fairly old, v1.135.3; the target was v2.7.5.\nSince this crossed several major versions, I did a full backup first — the PostgreSQL database, the Immich library, the docker-compose config, and the .env config — before running the upgrade. Afterward, I re-indexed the database and media files to get ready for the large-scale import to come.\nStep 2: importing Google Photos # Google Photos was my core photo library. Years of accumulation meant the exported Google Takeout data came out to hundreds of gigabytes.\nGoogle Takeout export. Exporting through Google Takeout eventually produced dozens of archive files.\nThe batch-download problem. With that many files, downloading them one by one manually would have been extremely tedious, so I wrote an automated download script using browser-cookie authentication. It handled reading the cookie automatically, downloading each archive, auto-retrying on failure, and resuming interrupted downloads — letting the whole thing run unattended.\nImporting with immich-go. Google Photos\u0026rsquo; biggest headache is metadata — a lot of the capture time and album info doesn\u0026rsquo;t live in the photo file itself, but in a matching JSON file. So I couldn\u0026rsquo;t just unzip everything and upload it directly. I ended up using immich-go upload from-google-photos for the import, which automatically restored capture times, restored albums, linked up metadata, and detected duplicates along the way. The whole process took a while, but it successfully preserved the complete history from Google Photos.\nStep 3: importing iCloud Photos # Compared to Google Photos, exporting from iCloud turned out to be even more of a hassle.\nApple\u0026rsquo;s data export. Apple offers a data-export feature; requesting it generates a download link. The catch: that link stays valid for a very short time — in my testing, it expired after roughly ten-odd minutes.\nBrute-force concurrent downloading. Facing that constraint, I went with the most direct approach: grab every download link at once and kick off all the downloads simultaneously, to make the most of the short validity window. Crude, but it worked — I successfully pulled down every photo and video.\nImporting with immich-go. The import method was similar to a regular directory import: automatically reading EXIF data, automatically identifying capture times, and automatically deduplicating. A large share of photos that already existed in Google Photos got correctly flagged as duplicate assets, so the final photo count didn\u0026rsquo;t end up doubling.\nStep 4: importing DJI, action-camera, and drone footage # Beyond phone photos, I also had a large amount of video from other devices — a DJI drone, DJI Action, GoPro, other action cameras, and more. Most of these files were already organized into directories on the NAS. Since this data doesn\u0026rsquo;t carry complex metadata, handling it was relatively simple — just imported straight through immich-go, which automatically recursively scans every directory while preserving video capture times, file metadata, and directory structure.\nimmich-go: the real workhorse of the whole migration # Across the entire migration, immich-go was the tool that genuinely made it all work. Even though the data sources were completely different from each other, all of them could ultimately be imported by just adjusting the command\u0026rsquo;s parameters — everything funneling into a single unified Immich library. That dramatically cut down the complexity of the whole migration.\nThe end result # After several days of organizing and importing, I finished consolidating my photo library. Every source now feeds into Immich, and thanks to its deduplication: duplicate photos get automatically identified, duplicate videos get automatically filtered out, and only a single unique copy of each is kept. The end result is one unified, clean, and sustainably maintainable photo library.\nImmich\u0026rsquo;s one drawback: my DS218+ is starting to struggle # Even though the migration itself went smoothly in the end, actually using the system afterward surfaced a new problem: Immich keeps getting more capable, and my DS218+ is starting to fall behind.\nMy NAS is a Synology DS218+, a pretty classic model, spec\u0026rsquo;d with an Intel Celeron J3355, 2 cores, and up to 6GB of RAM. Back when it was just running Synology Photos, that spec was basically no problem at all.\nBut Immich has developed rapidly in recent years, especially with the addition of facial recognition, smart search, CLIP vector search, video thumbnail generation, map indexing, and machine-learning models — all of which demand real CPU and memory resources behind the scenes.\nWhile importing tens of thousands of photos and hundreds of gigabytes of video, I could clearly feel the DS218+ approaching its limits. It was common to see immich-machine-learning, ffmpeg, and thumbnail-generation tasks sitting at high CPU usage for extended periods. Sometimes, even pausing the task queue, the background would still take a long while to finish jobs already submitted for indexing.\nEspecially during face re-recognition, video transcoding, large-scale imports, and machine-learning model updates, the DS218+\u0026rsquo;s responsiveness noticeably dropped — sometimes even affecting other apps running on the NAS. For simply browsing photos this isn\u0026rsquo;t a big deal, but if I want to make full use of everything Immich\u0026rsquo;s latest version offers, the hardware has already started to become the bottleneck.\nWhat\u0026rsquo;s next # Immich is now my sole photo-management platform, so the focus going forward shifts from migration to infrastructure upgrades. I\u0026rsquo;m planning to eventually move to more capable hardware — something like a DS923+, a DS1522+, a self-built NAS, or a mini PC running Docker with Immich — to let Immich\u0026rsquo;s AI capabilities run unconstrained.\nFor now, I think the DS218+ can still handle photo storage, everyday browsing, and automatic phone backup just fine. But once the photo count climbs past the tens of thousands with every AI feature enabled, upgrading the hardware becomes just a matter of time.\nAfterword # Looking back, the biggest payoff from this migration wasn\u0026rsquo;t the money saved by dropping a Google Photos subscription, and it wasn\u0026rsquo;t just consolidating photos from multiple platforms into one system either.\nWhat matters more is that I finally have a photo-management setup that\u0026rsquo;s entirely my own. From the free era of Google Photos, through paying for extra storage, to finally completing the move to Immich, photo storage for me has gone through a real shift — from depending on a cloud service to actually owning my data.\nToday, photos and videos from Google Photos, iCloud, a Huawei phone, a DJI drone, and various action cameras have all converged into Immich, kept unique through automatic deduplication. Going forward, even if I switch devices, switch cloud services, or even switch NAS hardware, the photo library itself won\u0026rsquo;t be affected.\nAnd for someone who\u0026rsquo;s accumulated more than a decade of photos and videos, that kind of certainty and control probably matters more than any single feature.\n","date":"2026-06-15","externalUrl":null,"permalink":"/posts/google-photos-icloud-to-immich-migration/","section":"Blog","summary":"Photos scattered across Google Photos, iCloud, a Huawei phone, a DJI drone, and action cameras finally got consolidated into one self-hosted, deduplicated Immich library — though my aging Synology DS218+ is starting to show its limits.","title":"From Google Photos and iCloud to Immich: Building My Own Photo Management Hub","type":"posts"},{"content":"While organizing the movie and TV files on my NAS recently, I ran into a genuinely practical problem: filenames in the download directory are often a mess — resource-site watermarks, resolution, the release group, audio track, subtitle info, sometimes even pinyin abbreviations, alternate titles, and typos mixed in. Sorting all of that by hand is not only slow, it\u0026rsquo;s also easy to get wrong.\nWith AI-assisted coding now part of my workflow, I\u0026rsquo;ve also cut back on reusing existing open-source code for this kind of thing — mostly to avoid security concerns, since it needs to run on the same NAS that holds all my other data.\nSo I built a small tool: AutoReel.\nIts goal is simple: watch an input directory, automatically identify movies and TV episodes, then move and rename the files into a directory structure that Emby / Jellyfin / Plex are happy with.\nProject repo:\nhttps://github.com/walker22026/AutoReel Writing this ended up taking far longer than I originally expected. Scaffolding the framework was easy; getting it to actually run and be usable — let alone pleasant to use — turned out to be a different story.\nThe biggest headache was that most of my content sources come from Aliyun Drive or Baidu Cloud shares, and to dodge takedowns, these shared resources get renamed in all sorts of mangled variations that are basically impossible to guess correctly. I tried having an LLM infer the real title from the filename, but the results weren\u0026rsquo;t good. In the end I went with a deliberately simplified approach: auto-process whatever can be reliably auto-processed, and drop anything unrecognized into a dedicated folder for a human to handle later — avoiding the kind of chaos that letting an LLM auto-process everything could cause.\nWhat AutoReel is (and isn\u0026rsquo;t) # AutoReel isn\u0026rsquo;t a complex media-library management system, and it doesn\u0026rsquo;t replace Emby, Jellyfin, or Plex.\nIt does exactly one thing: take the media files sitting in a download directory and organize them into a standard media-library structure.\nFor example:\nInput directory: /emby/source/Venom.The.Last.Dance.2160p.DV.HDR (2024)/xxx.mp4 After organizing: /emby/Movies/Venom The Last Dance (2024)/Venom The Last Dance (2024).mp4 TV episodes get organized similarly:\n/emby/TV Shows/Low IQ Crimes (2026)/Season 01/Low IQ Crimes - S01E01.mp4 Current processing strategy # Once started, AutoReel watches the input directory. The current design has three core directories:\nInput directory: /host/emby/source Movies directory: /host/emby/Movies TV directory: /host/emby/TV Shows Under the input directory, it also auto-generates:\n_unrecognized unrecognized items _duplicates duplicate files _pending_delete items with no media files, pending cleanup The overall flow:\nInput directory ├── A single video file │ ├── Recognized successfully -\u0026gt; moved to Movies/TV directory │ └── Recognition failed -\u0026gt; moved to _unrecognized │ ├── A subdirectory │ ├── Treated as one batch │ ├── All videos recognized -\u0026gt; whole directory organized │ ├── Any video fails recognition -\u0026gt; whole directory moved to _unrecognized │ └── Target file already exists -\u0026gt; whole directory moved to _duplicates │ └── _unrecognized / _pending_delete └── skipped during scans Why not hard links # I initially considered using hard links, so the download directory and the media-library directory could each \u0026ldquo;look\u0026rdquo; like they had an independent copy of the file, while actually only occupying one copy of disk space.\nIn the end, though, I went with a direct move mode: literally moving and renaming the original file. My use case leans toward pure media-library organization — I don\u0026rsquo;t need to keep seeding, and I don\u0026rsquo;t want a file appearing to exist in multiple places at once. It\u0026rsquo;s simpler logic and easier to reason about: once a file is organized, it no longer exists in the source directory.\nMovie handling rules # Movies are handled in two ways.\nSingle-video directories: if a directory contains only one video file, the directory name is used to identify the movie first. For example, Venom.The.Last.Dance.2160p.DV.HDR (2024)/xxx.mp4 gets cleaned up into \u0026ldquo;Venom: The Last Dance\u0026rdquo; and \u0026ldquo;2024,\u0026rdquo; then looked up via TMDB and organized as Movies/Venom The Last Dance (2024)/Venom The Last Dance (2024).mp4.\nMulti-video directories: if a directory contains multiple video files, it\u0026rsquo;s more likely a movie collection or franchise — for example, an \u0026ldquo;Indiana Jones\u0026rdquo; folder with three different films inside. In that case the directory name alone isn\u0026rsquo;t used to match a single movie; each video file is identified individually. Only once every file in the directory has been successfully identified does the whole batch get moved. If any single file can\u0026rsquo;t be identified, the entire directory gets moved to _unrecognized, to avoid a half-finished directory structure.\nTV episode handling rules # TV shows follow a \u0026ldquo;directory-first\u0026rdquo; principle.\nThe reason: a lot of episode filenames are extremely bare, like 01 4K.mp4, 02 4K.mp4, 03 4K.mp4 inside a folder named after the show. Feeding 01 4K.mp4 directly into a TMDB search could easily misidentify it as some unrelated movie. That\u0026rsquo;s a genuinely dangerous failure mode.\nSo the current strategy is: the directory name is responsible for identifying the show itself, and the filenames are only used to extract episode numbers — never searched as if they were movie titles on their own. If an episode number can\u0026rsquo;t be extracted from the filename, the whole directory goes into _unrecognized rather than risking an incorrect rename.\nFilename cleanup # Filenames from domestic resource sites are often packed with junk, things like site watermarks, \u0026ldquo;collect and bookmark us,\u0026rdquo; resolution tags, REMUX/Dolby Vision/audio-track labels, embedded-subtitle notes, and so on. AutoReel does local cleanup first, stripping out these common watermarks and technical tags before the cleaned-up title is submitted to TMDB for lookup.\nYear matching # The release year is an important safeguard against misidentification. If a filename includes a year like \u0026ldquo;(2019),\u0026rdquo; the TMDB result has to match that year (with a small tolerance, like 2018/2020). If the only search result is a same-titled movie from a completely different year, it won\u0026rsquo;t be treated as a match — this avoids the classic \u0026ldquo;same title, wrong year\u0026rdquo; mixup.\nAlias mechanism # Some movies and shows have inconsistent Chinese titles, English titles, or informal translated names. AutoReel supports an alias table at /config/aliases.json, for example mapping a Chinese title to its official English title. The alias table takes priority over a normal search, which is useful when TMDB can\u0026rsquo;t find a match, the Chinese title isn\u0026rsquo;t standardized, or the filename has been heavily abbreviated.\nWhy I dropped the LLM fallback # At one point I tried adding an LLM as a fallback identification method, hoping it could infer titles from messy filenames. Testing showed the results weren\u0026rsquo;t reliable enough. The reasons: movie/TV identification depends heavily on matching TMDB\u0026rsquo;s actual data; a lot of obscure titles, informal translations, and resource-site abbreviations fall outside what an LLM reliably knows; an LLM can produce something that \u0026ldquo;looks plausible\u0026rdquo; without actually being correct; and for automated file renaming specifically, a wrong guess is far more costly than simply failing to identify something.\nSo the LLM fallback was removed. The current principle: better to fail and land in _unrecognized than to confidently rename a file incorrectly — and that principle ended up guiding most of the project\u0026rsquo;s later refinement.\nHandling duplicates # If the target file already exists, AutoReel won\u0026rsquo;t overwrite it. If a newly scanned directory maps to a target path that\u0026rsquo;s already occupied, the whole directory gets moved to source/_unrecognized/_duplicates/, along with a text file explaining why — so it can be reviewed manually to decide whether it\u0026rsquo;s a redundant download, a different version worth keeping, or safe to delete.\nManually handling unrecognized items # Anything that fails recognition lands in source/_unrecognized, along with an explanatory text file describing why. Handling it manually is simple: go into _unrecognized, fix the file or directory name, move it back into the input directory, and AutoReel will re-scan and process it. This keeps a manual fallback available without letting the program blindly process things it isn\u0026rsquo;t confident about.\nSpecial handling for Synology NAS # On Synology, directories can contain @eaDir, a Synology-specific extended-attribute folder. AutoReel skips these, so they don\u0026rsquo;t get swept into the media library as if they were content. It also skips .DS_Store and __MACOSX.\nDocker deployment # The project deploys via Docker, with environment variables for the watch/movie/TV directories, the unrecognized/duplicate/pending-delete folder names, the TMDB API key and language, the file action mode (move), dry-run toggle, scan-on-start, a quiet period before processing, and a minimum file size threshold.\nThe role of dry run # On a first run, it\u0026rsquo;s worth enabling dry-run mode, where AutoReel only prints out its plan without actually moving any files — so you can confirm the identification results look right before flipping it to actually move files.\nCurrent known limitations # AutoReel is still a lightweight tool and isn\u0026rsquo;t trying to cover every media-management scenario. Known limitations right now: it depends on TMDB, so the NAS needs access to api.themoviedb.org; obscure titles, inconsistent translated names, or heavy abbreviations may still need the alias table; multiple versions of the same movie (director\u0026rsquo;s cut, extended edition, etc.) aren\u0026rsquo;t separately version-managed yet; raw disc directories, ISO, and BDMV structures don\u0026rsquo;t have full special handling yet; and when a USB external drive is used as the input directory, the watch mechanism may interfere with the drive\u0026rsquo;s sleep behavior — this needs more real-world testing on NAS hardware.\nGuiding principles # A few principles emerged over the course of building this: automation must never come at the cost of incorrect organization; the messier a filename is, the more conservative the handling should be; a failed identification can be fixed manually, but a wrong identification is far more trouble; a TV directory should never use a single episode\u0026rsquo;s bare filename to search for a movie; for NAS users, Docker deployment and a low setup bar really matter; and configuration should stay centralized in docker-compose.yml rather than depending on a web UI or extra config files.\nWrap-up # AutoReel solves a small but genuinely practical problem: download directories are messy, and a media library needs to be clean. It\u0026rsquo;s not a replacement for a full media-library system, but it works well as an auto-organizing layer between a download directory and a media-library directory.\nRight now it already handles automatic movie identification, batch TV organization, filename watermark cleanup, TMDB matching, year validation, alias mapping, duplicate isolation, a manual fallback for unrecognized files, Docker deployment, and skipping Synology-specific directories. Directions worth exploring further include better movie-version identification, stronger raw-disc-directory support, a friendlier unrecognized-items list, a lower-wake external-drive watch mode, and better alias-table maintenance.\n","date":"2026-05-24","externalUrl":null,"permalink":"/posts/autoreel-nas-media-organizer/","section":"Blog","summary":"Built a small self-hosted tool that watches a download folder, identifies movies and TV episodes, and renames/moves them into an Emby/Jellyfin/Plex-friendly structure — deliberately conservative, preferring to flag unrecognized files rather than guess and rename wrong.","title":"AutoReel: A NAS-Oriented Tool for Auto-Organizing Movie and TV Files","type":"posts"},{"content":"Self-hosting Podsync on a NAS is a genuinely useful setup, but it has one hidden risk: a site\u0026rsquo;s anti-scraping mechanism can silently invalidate your cookies, yt-dlp starts throwing errors, and you might not notice for weeks that the podcast has stopped updating.\nThis post documents setting up a simple monitoring script that pushes an alert through a messaging app the moment something goes wrong, plus a daily download-statistics report on a schedule.\nBackground: what can go wrong with Podsync # After running for a while, the three most common errors that show up in the logs are:\nCookie expiration\nThe provided YouTube account cookies are no longer valid. They have likely been rotated in the browser as a security measure. Google automatically rotates session cookies during normal browser use, which invalidates a previously exported cookies.txt. Under normal conditions cookies last 1–3 months, but with frequent use they might expire in as little as 2–4 weeks.\nBot verification\nERROR: Sign in to confirm you\u0026#39;re not a bot. Once cookies expire, yt-dlp has no login credentials left, and requests get rejected outright.\nIP rate limiting\nWARN: server responded with a \u0026#39;Too Many Requests\u0026#39; error The NAS\u0026rsquo;s outbound IP gets rate-limited from making requests too frequently, and this triggers more easily when multiple Podsync containers are running at once.\nAll three of these fail silently — Podsync keeps running, the error shows up in the logs, but nothing actively notifies you, and the podcast just quietly stops updating.\nSetting up the scheduled tasks # Configured in Synology DSM: Control Panel → Task Scheduler → Create → Scheduled Task → User-defined script.\nAnomaly check (every 10 minutes)\nSetting Value Task name podsync-check User root Frequency Daily, repeating every 10 minutes Script /volume1/docker/podsync/podsync_monitor.sh check Morning report (08:00 China time)\nSetting Value Task name podsync-report-morning User root Run time Daily at 08:00 Script /volume1/docker/podsync/podsync_monitor.sh report Evening report (19:00 China time)\nSetting Value Task name podsync-report-evening User root Run time Daily at 19:00 Script /volume1/docker/podsync/podsync_monitor.sh report Sample alerts # Anomaly alert (triggered by check):\n⚠️ Podsync Alert 🕐 2026-05-05 14:32:10 🍪 podsync-a: cookies expired, need to re-export 🤖 podsync-a: YouTube bot verification failed Daily report (triggered by report):\n📊 Podsync Daily Report 🕐 2026-05-05 08:00:01 📅 Period: past 24 hours 📁 podsync-a ✅ Succeeded: 12 episodes ❌ Failed: 2 times 🚫 Rate-limited: 1 time 🍪 Cookie expired: 0 times Wrap-up # The core idea behind this monitoring setup is simple: periodically scan the logs for known error keywords, push a notification the moment something\u0026rsquo;s wrong, and separately tally download stats each day as a health check.\nWith this script in place, cookie expiration and rate-limiting issues get flagged within 10 minutes, instead of only being discovered after noticing the podcast had already stopped updating.\n","date":"2026-05-05","externalUrl":null,"permalink":"/posts/podsync-monitoring-alerts/","section":"Blog","summary":"Self-hosted Podsync fails silently when YouTube cookies get rotated out — a simple log-scanning script paired with scheduled tasks now pushes an alert within 10 minutes, plus twice-daily download reports.","title":"Podsync Monitoring and Alerts: Catching Failures Before Episodes Stop Updating","type":"posts"},{"content":"Came across a site today, https://hnup.date/hn-sota. Unlike most leaderboards that focus on benchmark scores, this one primarily pulls authentic comments from top Hacker News developers and runs AI sentiment analysis on them to evaluate models.\nBased on what it shows, Claude (Anthropic) remains the strongest model in developers\u0026rsquo; eyes — recognized for a superior \u0026ldquo;engineering sensibility\u0026rdquo; in complex logical reasoning and code refactoring that\u0026rsquo;s currently considered top-tier. GPT (OpenAI) comes in second, keeping an edge in generation speed, ecosystem support, and reliable standard implementations across mainstream languages. And DeepSeek stands out as an impressive dark horse, earning a strong practical reputation in hardcore tech circles through extreme cost-effectiveness and deep language-understanding capability.\n","date":"2026-05-04","externalUrl":null,"permalink":"/posts/hacker-news-coding-model-reputation/","section":"Blog","summary":"A site that runs AI sentiment analysis on real Hacker News developer comments, rather than benchmark scores — Claude comes out on top for engineering sense, GPT close behind, with DeepSeek a notable dark horse.","title":"Gauging Coding Models' Real Reputation Through Hacker News","type":"posts"},{"content":"Aside from two days of overtime during the break, I spent the rest of it tinkering with an open-source project called TrendRadar — and it turned out to be worth writing up.\nWhy I needed it # I subscribe to a fair number of English finance and tech RSS feeds — Yahoo Finance, Bloomberg, Seeking Alpha, Hacker News are all on the list. The problem is these sources put out a lot of content every day, and with English headlines, just skimming them takes real time, let alone figuring out which ones are worth reading closely.\nWhat I actually wanted was simple: tell me when there\u0026rsquo;s something new, translate it for me, and give me a summary in the evening. No need to babysit an RSS reader, and no need to open it and see a pile of yesterday\u0026rsquo;s leftovers every time.\nThat\u0026rsquo;s basically exactly what TrendRadar does.\nWhat is TrendRadar # TrendRadar is an open-source public-opinion/trend monitoring tool. Its core function is aggregating trending lists and RSS subscriptions across multiple platforms, then filtering, translating, and analyzing them with AI before pushing the results to your phone.\nIt supports a wide range of push channels — Feishu, DingTalk, Telegram, email, ntfy, Bark are all there. I use Telegram, since it\u0026rsquo;s the simplest to set up.\nThe AI side runs on a unified LiteLLM interface, so in theory it supports basically any mainstream model provider. I connected it to Gemini Flash through my self-hosted LiteLLM gateway, which then forwards requests for TrendRadar to use.\nDeployment: still Docker on the NAS # My usual infrastructure philosophy: if it can run on the NAS, don\u0026rsquo;t spin up a separate VPS for it. TrendRadar officially provides a Docker image, so I ran it directly on the DS218plus.\nBy default it crawls every 30 minutes, which is well within what a low-power machine like the DS218plus can handle without breaking a sweat.\nRSS configuration: mostly English finance and Portuguese news # The RSS configuration in config.yaml is just a matter of adding feeds as needed.\nAI translation: bilingual titles # TrendRadar supports AI translation, translating RSS titles into a target language and pushing both together. I\u0026rsquo;ve configured it for a \u0026ldquo;Chinese-foreign bilingual\u0026rdquo; mode.\nPush strategy: incremental during the day, AI summary at night # This was the part of the config that took the most time. The nice thing about incremental mode is that if there\u0026rsquo;s nothing new between two runs, the program doesn\u0026rsquo;t send any push at all. The evening AI summary, meanwhile, analyzes everything accumulated over the full day.\nCurrent state # After running it for a while, it\u0026rsquo;s basically hitting what I wanted: scattered new-item pushes show up on my phone during the day without flooding it, and a single AI summary lands sometime after 7pm.\n","date":"2026-05-04","externalUrl":null,"permalink":"/posts/trendradar-information-feed-hub/","section":"Blog","summary":"Too many English finance and tech RSS feeds to skim by hand — TrendRadar, an open-source aggregator, filters, translates, and summarizes them via AI, then pushes the results to Telegram.","title":"Building My Own Information Feed Hub With TrendRadar","type":"posts"},{"content":"Today I mainly dealt with a broken cover-image issue across two of my podcast-generation projects, and along the way tuned up the episode count and change-log approach for a language-learning audio podcast.\nIn the first project, the podcast RSS had been using an external cover image URL. Once that image\u0026rsquo;s address stopped working, podcast clients could no longer display either the channel cover or the individual episode covers. This fix didn\u0026rsquo;t touch audio generation, transcription, AI summarization, syncing, the player, or deployment config at all — it just switched the RSS\u0026rsquo;s cover-image URLs over to a cover file stored in the local audio directory instead.\nOn the code side, I added a dedicated method for generating cover-image URLs, replacing the old hardcoded external image address with a single unified function call. That way, if the directory structure or access path ever changes again, there\u0026rsquo;s only one place to update. After the fix, the RSS\u0026rsquo;s regular channel cover, the channel\u0026rsquo;s podcast cover, and each episode\u0026rsquo;s cover all point to the same local cover file, removing the dependency on the dead external resource.\nOnce the change was in, I rebuilt and restarted the recording container and confirmed the new one was running properly. I also checked the RSS file, the cover file, and the container logs to make sure the cover URL had actually been written into the RSS and that the cover image could be read correctly by clients.\nThe second project is aimed at foreign-language-learning audio, turning a local audio directory into a subscribable podcast feed. I made a couple of small adjustments to it today: first, bumped the number of episodes generated per run from 50 to 80, which suits repeated, batch-style listening for study purposes better; second, added a proper cover-image configuration, filling in both the standard RSS cover field and the field Apple Podcasts uses, fixing an issue where the cover showed up blank in Apple Podcasts.\nWhile I was at it, I also fixed a mismatch between the configured cover-file path and where the file actually lived, and added a unified method for generating media-file URLs. This method preserves directory structure and correctly handles spaces, Chinese characters, or other special characters in filenames — making future URL generation for audio, images, and other shared resources more reliable.\nI also added a project changelog file to keep an ongoing record of each change. Today\u0026rsquo;s entry covers the episode-count adjustment, the new cover-image configuration, filling in the RSS and Apple Podcasts cover fields, the path fix, and the corresponding container-config sync.\nOverall, today\u0026rsquo;s work wasn\u0026rsquo;t about adding complex new features — it was about closing out the basics of the podcast-feed experience: covers no longer depend on a dead external resource, channel and episode covers stay consistent, and Apple Podcasts can now recognize and display them properly; the language-learning podcast\u0026rsquo;s episode count is also better suited to daily repeat listening. The scope of the changes was fairly restrained, the core logic stayed clear, and it leaves behind a more centralized place for configuration and change tracking going forward.\n","date":"2026-05-04","externalUrl":null,"permalink":"/posts/fixing-podcast-rss-cover-image/","section":"Blog","summary":"One podcast pipeline’s cover art broke when its external image URL died; the fix was routing RSS cover URLs through a single local-file method, plus a couple of quality-of-life tweaks for a language-learning audio feed.","title":"Fixing a Broken Cover Image in My Podcast RSS Feeds","type":"posts"},{"content":"In everyday life, we touch on a number of different domains — investing, work, journaling, day-to-day life, and so on. As AI assistants have matured, something like Hermes has increasingly become a genuine helper. But the deeper I got into using it, the more I noticed a real problem: despite its strong capabilities, the domains kept bleeding into each other.\nTalking about stocks would often drag in bits of my work content, and journaling about daily life would occasionally pull in investment thinking. That kind of cross-contamination got me thinking about how to properly separate different tasks and content domains so Hermes could serve me more efficiently and professionally in each one.\n1. The core principle: one entry point, multiple isolated roles # First, I decided to restructure Hermes as a single main entry point backed by several specialized sub-assistants. Specifically, I split Hermes into five modules:\nRouter Hermes: determines which domain the current task belongs to and hands it off to the right sub-assistant. Stock assistant: focused on stock analysis, asset allocation, long-term holdings, and related questions. Work assistant: covers the audit system, customer-service work, project reporting, AI-enablement initiatives, and similar content. Diary assistant: records personal reflection, blog content, tech tinkering, and so on. Life assistant: handles family, health, travel, Portuguese learning, and other everyday matters. Each domain module needs a strict boundary against the others, so content doesn\u0026rsquo;t cross over and each module stays focused only on tasks within its own domain.\n2. The most important piece: partitioning the knowledge base # To make domain isolation actually work, I gave Hermes\u0026rsquo;s knowledge base a clear partition structure. Content for each domain lives in its own directory, for example:\nknowledge/ ├── investment/ │ ├── portfolio.md │ ├── stock_notes.md │ ├── strategy.md │ └── risk_rules.md ├── work/ │ ├── audit_system.md │ ├── customer_service_ai.md │ ├── cbss_billing.md │ └── reports.md ├── diary/ │ ├── personal_journal.md │ ├── blog_drafts.md │ └── reflections.md ├── life/ │ ├── family.md │ ├── health.md │ ├── travel.md │ └── portuguese.md When I ask Hermes something, it first determines which domain the question belongs to, then only retrieves material from that domain — avoiding cross-domain interference.\n3. Adding an \u0026ldquo;intent routing\u0026rdquo; layer # On top of the knowledge-base partitioning, I added an \u0026ldquo;intent routing\u0026rdquo; system. This means that every time I ask a question, Hermes first determines which domain it falls under, then decides whether cross-domain reference material is even allowed.\nFor example: if I ask \u0026ldquo;How did the stock market do today?\u0026rdquo; Hermes only pulls stock-related material. If I ask \u0026ldquo;How\u0026rsquo;s the AI-enablement project at work going?\u0026rdquo; Hermes only touches work-domain content. If I ask \u0026ldquo;Help me plan a trip,\u0026rdquo; Hermes stays within the life domain and leaves investing or work out of it entirely.\nThis routing mechanism effectively ensures content from one domain doesn\u0026rsquo;t get misapplied to another.\n4. Per-module role prompts # Each domain module has its own dedicated role prompt, defining how it should behave when answering. For instance:\nStock assistant:\nYou are my stock-analysis assistant.\nResponsibilities: analyze stocks, ETFs, asset allocation, long-term holdings, rebalancing suggestions, and related questions. Focus on investment decisions around steady growth, retirement funds, and margin of safety. Prioritize stock-related material when answering.\nOff-limits: don\u0026rsquo;t proactively reference my work content; don\u0026rsquo;t proactively reference my family, diary, or Portuguese-learning content.\nWork assistant:\nYou are my work-material and project-analysis assistant.\nResponsibilities: help organize reporting material, project summaries, the audit system, customer-service tickets, AI applications, and similar work content. Tone should be concise and formal, suited for reporting to leadership, highlighting goals, actions, and results.\nOff-limits: don\u0026rsquo;t proactively reference my investment content; don\u0026rsquo;t proactively reference my family or diary content.\nDiary assistant:\nYou are my personal diary and blog-writing assistant.\nResponsibilities: organize personal experiences, tech tinkering, AI tool usage, and life observations. Voice should be first-person, conversational, with personal opinion.\nOff-limits: don\u0026rsquo;t proactively expose specific work content; don\u0026rsquo;t fold personal life experiences into work material.\nLife assistant:\nYou are my life-planning assistant.\nResponsibilities: handle family, health, travel, foreign-language learning, and other everyday matters. Answers should be practical, concrete, and actionable.\nOff-limits: don\u0026rsquo;t proactively pull in work content; don\u0026rsquo;t proactively reference investment positions unless it\u0026rsquo;s directly relevant to financial planning.\nEach module sticks strictly to its own defined scope when answering, staying on topic.\n5. Combining hard isolation with soft isolation # To make the domain separation even more reliable, I used two approaches together: hard isolation, meaning each domain has its own independent knowledge base so content never gets confused across domains; and soft isolation, using intent routing and role prompts to keep each module operating strictly within its scope, with Hermes only blending domains when I explicitly ask for a cross-domain answer.\n6. How the router is designed # The router Hermes\u0026rsquo;s main job is to determine a question\u0026rsquo;s domain and dispatch it to the right assistant. For example: \u0026ldquo;How did the stock market do today?\u0026rdquo; → routed to the stock assistant. \u0026ldquo;Help me write a report on the AI-driven collections system\u0026rdquo; → routed to the work assistant.\nThis \u0026ldquo;router + specialized sub-assistants\u0026rdquo; design lets Hermes operate efficiently across multiple domains while avoiding interference between them.\n7. Wrap-up # By restructuring Hermes with clear domain isolation, intent routing, and modular roles, I finally arrived at an assistant that actually works efficiently. Whether it\u0026rsquo;s stock analysis, work reporting, or personal journaling and life planning, Hermes now helps me within clearly defined boundaries. This hasn\u0026rsquo;t just made Hermes more efficient — it\u0026rsquo;s made it a genuinely capable helper across both my daily life and my work, with information from different domains no longer stepping on each other.\n","date":"2026-04-29","externalUrl":null,"permalink":"/posts/hermes-domain-isolation-optimization/","section":"Blog","summary":"Investing talk kept leaking into work notes and vice versa — the fix was splitting Hermes into a router plus dedicated stock, work, diary, and life sub-assistants, each with its own knowledge base partition and a strict no-cross-referencing prompt.","title":"How I Rebuilt My Hermes Assistant So My Life's Domains Stop Bleeding Into Each Other","type":"posts"},{"content":" The symptom # Podsync\u0026rsquo;s logs were repeatedly showing two kinds of errors.\nBot verification error:\nERROR: Sign in to confirm you\u0026#39;re not a bot. Use --cookies-from-browser or --cookies Rate limit error:\nWARN: server responded with a \u0026#39;Too Many Requests\u0026#39; error Root cause analysis # Error type Cause Bot verification yt-dlp wasn\u0026rsquo;t carrying any login credentials, so YouTube flagged it as a bot Too Many Requests The NAS\u0026rsquo;s outbound IP was making requests too frequently and got rate-limited, made worse by multiple containers downloading at once The fix: configuring cookies # Step 1: export cookies\nInstall the Chrome extension \u0026ldquo;Get cookies.txt LOCALLY,\u0026rdquo; open youtube.com and confirm you\u0026rsquo;re logged in, click the extension icon to export cookies.txt immediately, and avoid visiting YouTube again afterward to keep the cookies from getting rotated out.\nStep 2: upload the cookies to the NAS\nThe cookies file has to live inside the container\u0026rsquo;s mounted data directory, or the container won\u0026rsquo;t be able to see it.\nFirst confirm the container\u0026rsquo;s mount path:\ndocker inspect \u0026lt;container-id\u0026gt; | grep -A 10 \u0026#34;Mounts\u0026#34; Upload it to the right location (corresponding to /app/data/ inside the container):\nscp ~/Downloads/cookies.txt root@192.168.5.154:/volume1/docker/podsync/data/cookies.txt Step 3: edit the config file\nAdd this under each feed, or in the global [downloader] section:\n[downloader] self_update = true youtube_dl_args = [\u0026#34;--cookies\u0026#34;, \u0026#34;/app/data/cookies.txt\u0026#34;] Note: use the container-internal path /app/data/, not the host path.\nStep 4: restart the container\ndocker restart \u0026lt;container-id\u0026gt; Things to watch for # Cookies expire. YouTube automatically rotates session cookies during regular browser use, which invalidates a previously exported cookies.txt. Under normal conditions cookies last 1–3 months, but with heavy use they can expire in as little as 2–4 weeks. The symptom is the same bot-verification error resurfacing after things were working fine, along with a log line like:\nThe provided YouTube account cookies are no longer valid. They have likely been rotated When you see that, re-export and re-upload the cookies.\nToo Many Requests is IP-based rate limiting, and it can still happen occasionally even with cookies configured. To mitigate it: increase each feed\u0026rsquo;s update_period to reduce request frequency, and avoid having multiple containers hammer requests at the same time.\nWith multiple containers deployed, each container\u0026rsquo;s config file needs to be updated individually — a batch find-and-replace command can help:\nfor f in config-a.toml config-b.toml config-c.toml; do sed -i \u0026#39;s|old content|new content|g\u0026#39; \u0026#34;/volume1/docker/podsync/$f\u0026#34; done ","date":"2026-04-29","externalUrl":null,"permalink":"/posts/podsync-cookie-troubleshooting-diary/","section":"Blog","summary":"Podsync started throwing YouTube bot-verification and rate-limit errors — the fix was exporting browser cookies into the container’s mounted data directory and wiring them into yt-dlp’s args.","title":"Podsync Download Troubleshooting Diary","type":"posts"},{"content":"A few quick experiments with ChatGPT\u0026rsquo;s new image generation, version 2.0:\nHad ChatGPT draw a picture of my current life, based purely on its own understanding of me.\nAfter feeding it some character reference material, had it draw my ideal vision of retirement.\nDug up a journal entry from 2013-11-03 13:09 about orchids and had it turned into a comic strip.\nAnd what\u0026rsquo;s an image test without also trying out PowerPoint generation?\n","date":"2026-04-23","externalUrl":null,"permalink":"/posts/chatgpt-images-2-first-look/","section":"Blog","summary":"Four quick tests of the new ChatGPT Images 2.0: visualizing my current life as the AI understands it, an imagined retirement, a comic-strip take on a 2013 journal entry about orchids, and a PowerPoint-generation test.","title":"ChatGPT Images 2.0 Launched — Here's How It Performs","type":"posts"},{"content":"This weekend, I finally built out a piece of AI architecture I\u0026rsquo;d been mulling over in my head for a long time.\nIt\u0026rsquo;s not the kind of thing where you just \u0026ldquo;bolt on a few tools\u0026rdquo; — it\u0026rsquo;s starting to feel like an actual system. Three modules, each with its own role, and starting to show a bit of real coordination between them.\nThe first piece is a personal assistant # For this one, I basically carried over the Hermes agent I\u0026rsquo;d already been tinkering with.\nAs I\u0026rsquo;ve written before, I deployed it on my Synology NAS so it stays online long-term, handling information gathering and processing. Compared to a cloud-based setup, this kind of \u0026ldquo;locally resident\u0026rdquo; feel is more like a truly personal assistant — stable, controllable, and something you can slowly feed data over time.\nNext, I plan to gradually feed my own past journal entries into it, so it genuinely understands my behavior patterns and decision logic, rather than just being a \u0026ldquo;tool-style AI.\u0026rdquo;\nI want it to become more of a \u0026ldquo;long-term memory + cognitive augmentation\u0026rdquo; role going forward.\nThe second piece is a home assistant # I built this one myself, from scratch.\nFrom the architecture down to the features, it\u0026rsquo;s mostly built around \u0026ldquo;voice understanding + intent recognition.\u0026rdquo; Put simply, the goal is for it to understand what I\u0026rsquo;m saying and know what I want it to do.\nThe single most important thing right now is connecting it to my investment system.\nThat includes position information, rebalancing alerts, strategy triggers — gradually giving it access to all of this. For example: when the market moves, it can alert me; when a strategy condition is hit, it can notify me or even act; for day-to-day use, it can also give simple status summaries.\nThis piece is really the \u0026ldquo;most action-oriented\u0026rdquo; part of the whole system. It\u0026rsquo;s not focused on \u0026ldquo;thinking\u0026rdquo; the way the personal assistant is, and it\u0026rsquo;s not focused on \u0026ldquo;input\u0026rdquo; the way the intelligence-gathering system is — it\u0026rsquo;s closer to an \u0026ldquo;action layer.\u0026rdquo;\nThe third piece is an intelligence-gathering agent, handled by OpenClaw # I\u0026rsquo;m running this module on a VPS. The reason is simple: speed and network access.\nIts role is very clearly defined — \u0026ldquo;intelligence gathering\u0026rdquo;: quickly pulling in information, catching key changes, sending alerts as soon as something happens. No complex analysis, no long-term memory — just a \u0026ldquo;forward outpost.\u0026rdquo;\nOpenClaw already had the rough shape of this before; this is really a repositioning, upgrading it from a tool into the \u0026ldquo;perception layer\u0026rdquo; of the whole architecture.\nPutting it together # With the three pieces put together, for the first time it feels less like a handful of tools and more like a real system: the intelligence agent on the VPS handles \u0026ldquo;seeing the world,\u0026rdquo; the personal assistant on the NAS handles \u0026ldquo;understanding me,\u0026rdquo; and the local home assistant handles \u0026ldquo;acting on my behalf.\u0026rdquo; Something like a simplified \u0026ldquo;perceive–think–act\u0026rdquo; loop.\nOf course, right now it\u0026rsquo;s only at the stage of \u0026ldquo;it runs.\u0026rdquo; So next, I\u0026rsquo;m going to let it run for a while rather than rushing to add new features. Watching first for which parts are actually delivering value, which alerts are just noise, and which capabilities are worth turning into long-term, stable mechanisms. Once it\u0026rsquo;s running smoothly, I\u0026rsquo;ll think about the next round of tuning and upgrades.\n","date":"2026-04-20","externalUrl":null,"permalink":"/posts/trinity-ai-system-weekend-build/","section":"Blog","summary":"A personal assistant on the home NAS, a home assistant tied into an investment system, and an intelligence-gathering agent on a VPS — three modules starting to work together as a real perception-cognition-action loop, not just a pile of tools.","title":"This Weekend I Finally Built the Three-Part AI System I'd Been Thinking About","type":"posts"},{"content":"Spent some time today thoroughly reworking the LiteLLM gateway\u0026rsquo;s model routing strategy — writing down the full thinking and the final config here.\nWhy model routing is needed # My AI infrastructure looks like this:\nTelegram → Hermes → LiteLLM gateway → various models LiteLLM acts as a unified gateway, exposing a single OpenAI-compatible interface on top of a dozen-plus backend models. Hermes only needs to know one address, ai.XXX.com — it doesn\u0026rsquo;t need to care which model is actually running underneath. The benefit: models can be swapped, upgraded, or switched at any time, completely transparent to Hermes.\nThe model pool # The final configuration:\nPaid models (pay-as-you-go, very low cost)\nDeepSeek V3 — strongest for Chinese, $0.27/million input tokens, extremely cost-effective Gemini 3 Flash — Google\u0026rsquo;s latest fast model, multimodal, generous free quota Gemini 3.1 Pro — strong reasoning, used as a Pro-tier backup during rate limits Free NVIDIA NIM models\nmeta/llama-3.3-70b-instruct — general-purpose English workhorse minimaxai/minimax-m2.5 — strong Chinese ability moonshotai/kimi-k2.5 — strong reasoning z-ai/glm5 — Chinese backup (later swapped for the OpenRouter version due to timeouts) OpenRouter free model pool (final safety net)\nminimax/minimax-m2.5:free z-ai/glm-4.5-air:free meta-llama/llama-3.3-70b-instruct:free nvidia/nemotron-3-super-120b-a12b:free On-demand paid top-tier model\nClaude Sonnet 4.6 — accessed through OpenRouter, $3/$15 per million tokens, top-tier reasoning backup The final fallback chain # Whenever any model fails, there\u0026rsquo;s a complete degradation chain in place, with a final round-robin pool of four free models as the ultimate safety net.\n","date":"2026-04-19","externalUrl":null,"permalink":"/posts/litellm-model-routing-config/","section":"Blog","summary":"A full writeup of the LiteLLM gateway config behind my AI stack — free NVIDIA NIM and OpenRouter models as the safety net, cheap paid models like DeepSeek V3 for daily use, and Claude Sonnet as an on-demand top-tier fallback.","title":"Getting LiteLLM's Model Routing Set Up Properly","type":"posts"},{"content":"Got Hermes fully working on the VPS yesterday, and used it today — overall it feels better than OpenClaw: the information is more concise, and the config files are much clearer. I\u0026rsquo;m considering making it a service on my home network, with the goal of gradually turning it into the AI channel that handles interaction with core internal content like my investment platform and household journal. So I decided to migrate Hermes from the VPS to my Synology NAS, and took the opportunity to redesign the model routing strategy from scratch.\nWhy migrate to the NAS # After migrating to the NAS, Hermes runs via Docker still confined to my home network, but through gradual authorization it can progressively gain access to my household journal, investment records, and other information, becoming an internal AI hub. The end result: outbound information gathering handled by the VPS-based OpenClaw architecture, while internal content and investment-related work goes through Hermes on the internal Docker setup — a cleaner architecture overall. The VPS instance of Hermes has been stopped and disabled.\nDeployment process # Deployed via Docker Compose on a DS218plus — the config is very lean.\nDefault model configuration # Hermes accesses all models through a LiteLLM gateway, with the config pointing at a single unified endpoint, with the core goal still being the lowest possible token cost.\nAttempting smart routing for short messages # With the primary model configured, the next step was further cost optimization — short messages don\u0026rsquo;t need to go through the primary model; a cheaper model would do. Hermes offers a smart_model_routing feature for this.\nWhy the routing didn\u0026rsquo;t work # Testing showed the routing wasn\u0026rsquo;t behaving as expected. Sending a short Chinese sentence like \u0026ldquo;今天是星期日\u0026rdquo; (Today is Sunday) still came back through Gemini Flash. Only an extremely short English message like \u0026ldquo;hi\u0026rdquo; actually triggered MiniMax. Checking the logs revealed why.\nGiving up on routing, switching to a full fallback chain # Since smart routing was essentially ineffective for Chinese input, and forcing traffic to a cheaper model risked hurting response quality, I ultimately decided to drop the routing approach and build out a complete fallback chain instead.\nA hidden problem in the config # config.yaml had two smart_model_routing blocks — one at the top that I\u0026rsquo;d manually added (enabled: true), and one in the middle from the default template (enabled: false). When parsed as YAML, the later block overrode the earlier one, so smart routing was actually disabled the whole time.\nFinal state # Hermes on the NAS is now running stably, with the VPS instance stopped and its autostart disabled. The model setup uses Gemini Flash as the primary, with multiple fallback layers for reliability, and Sonnet 4.6 available on-demand for top-tier reasoning.\n","date":"2026-04-19","externalUrl":null,"permalink":"/posts/hermes-synology-nas-deployment/","section":"Blog","summary":"After getting Hermes running on a VPS, I moved it to my Synology NAS instead so it could gradually gain access to household journal and investment data as an internal AI hub — and redesigned the model routing along the way.","title":"Migrating Hermes to My Synology NAS","type":"posts"},{"content":"OpenClaw\u0026rsquo;s popularity is visibly cooling off, and lately a lot of people have been talking about using Hermes instead. I looked into this agent tonight, and setting everything else aside, what caught my attention most was its token consumption:\nIts token compression ratio can reach over 50%. Reportedly, in certain scenarios it can even hit 70%. That\u0026rsquo;s a big draw for me. As everyone knows, the biggest problem with using OpenClaw is its enormous token consumption. There\u0026rsquo;s a joke going around that some company used an LLM to replace certain human functions, but ended up spending more on the model than they would have on hiring people. So being able to deliver the same functionality at the lowest possible cost is where the real strength lies.\nAnother factor is that I think it fits my needs better — Hermes feels more like a work assistant, better suited to helping me with my actual work. By comparison, OpenClaw is \u0026ldquo;heavier,\u0026rdquo; relying more on calling a large number of external capabilities and extensions.\nAs the saying goes, \u0026ldquo;adults don\u0026rsquo;t make choices — they take both,\u0026rdquo; so today I grabbed a VPS and installed this agent to try it out. My impressions so far:\nInstall experience: since I installed OpenClaw fairly early, when the version was still unstable, I ran into a lot of issues and the setup was tedious. By comparison, this agent is much simpler, supporting a one-click install. Configuration and speed: the GUI-based configuration is very convenient — pair it with a free NVIDIA model, hook up a chatbot, and you\u0026rsquo;re good to go. It\u0026rsquo;s fast, and the documentation is relatively clear. Logging the whole process here for future reference.\nWhat is Hermes # Hermes is an open-source AI agent gateway that lets you interact with AI models through Telegram, Discord, WhatsApp, Slack, and other platforms. It\u0026rsquo;s not just a chatbot — it\u0026rsquo;s a genuine agent, capable of running terminal commands, searching the web, generating speech, managing memory, setting up scheduled tasks, and even controlling a browser.\nIn short, it\u0026rsquo;s an AI assistant that lives on a VPS, reachable anytime through Telegram.\nInstallation process # Hermes is installed on my VPS, managed by the hermes-gateway systemd service, set to start on boot. Its config file is at ~/.hermes/config.yaml, environment variables at ~/.hermes/.env.\nOn the first launch after install, Telegram responded right away — the basic framework came up without any issues.\nModel configuration # The default install used NVIDIA NIM\u0026rsquo;s llama-3.1-nemotron-70b-instruct, but it immediately errored out on startup:\nError code: 404 - Function Not found for account The cause: my account didn\u0026rsquo;t have access to that model\u0026rsquo;s NIM Function ID. Switching to meta/llama-3.3-70b-instruct fixed it.\nI then referenced my model configuration in OpenClaw and added three additional candidate models, all accessed through NVIDIA NIM:\nPrimary model: meta/llama-3.3-70b-instruct — stable, ample free quota Fallback 1: minimaxai/minimax-m2.5 — strong Chinese comprehension, good for daily Chinese work Fallback 2: z-ai/glm5 — Chinese backup Fallback 3: moonshotai/kimi-k2.5 — strong reasoning, the final safety net All four models share the existing NVIDIA_API_KEY, no extra application needed.\nTools enabled # Hermes manages its tool system per platform — by default, the Telegram platform only has web search turned on, everything else is off. Using hermes tools enable --platform telegram, I enabled the rest one by one: web (search and content extraction), terminal (command execution), memory (cross-session memory), vision (image recognition and analysis), tts (text-to-speech), skills (skill extensions), todo (task planning), and cronjob (scheduled tasks).\nIssues I ran into # Issue one: web search not working. After enabling the tool, search requests kept returning \u0026ldquo;unable to search due to tool restrictions.\u0026rdquo; Turned out Hermes\u0026rsquo;s web_tools only support four search backends — Tavily, Firecrawl, Exa, and Parallel — not Brave Search. The Brave API key I\u0026rsquo;d previously configured in OpenClaw was completely invalid for Hermes. Ended up registering for Tavily and configuring TAVILY_API_KEY, after which web search worked normally.\nIssue two: wrong tool name in config. I\u0026rsquo;d manually written hermes-web under platform_toolsets in config.yaml, but the actual tool name is web, which kept producing Unknown tool 'web' errors in the logs. I later found out the config should be managed through the hermes tools enable command, not by editing the config file directly.\nCurrent status # Hermes is now running stably, with normal Telegram interaction. Next up, I\u0026rsquo;m planning to configure a daily-briefing cron job to push a morning summary of stocks, oil prices, and exchange rates, along with a daily knowledge practice routine.\nAs a complement to OpenClaw, Hermes has stronger agent capabilities, especially around terminal execution and multi-tool coordination. The two systems each have their strengths, and for now I\u0026rsquo;m planning to run them in parallel.\n","date":"2026-04-19","externalUrl":null,"permalink":"/posts/hermes-new-ai-assistant-setup/","section":"Blog","summary":"OpenClaw’s buzz is fading and everyone’s talking about Hermes — its token compression alone (50%+, reportedly up to 70% in some cases) was reason enough to spin up a VPS and try it.","title":"Hermes: Setting Up My New AI Assistant","type":"posts"},{"content":"","date":"2026-04-18","externalUrl":null,"permalink":"/tags/language-learning/","section":"Tags","summary":"","title":"Language Learning","type":"tags"},{"content":"A note up front: I recently needed a model that\u0026rsquo;s strong specifically in Portuguese. When picking a model, the first thing we usually look at is parameter count — but does parameter count actually determine the outcome? It\u0026rsquo;s better to narrow down to a parameter range and then actually test candidate models against a real use case, letting real-world results speak for themselves. This post records a blind test I ran using real exam-style questions to compare two candidate models, along with the process and the conclusion. I initially favored one of them going in — the actual results proved me wrong.\nBackground: why these two models? # Within a 12GB VRAM budget, there really aren\u0026rsquo;t that many local models that can handle pt-PT (European Portuguese) well. After an initial round of filtering, two candidates stood out, both installable with a single ollama pull:\nCandidate A: aya-expanse:8b\nA multilingual flagship model from Cohere Labs, supporting 23 languages. Backed by a full paper, public benchmarks, DPO preference training, model merging — a complete methodology. On the multilingual Arena-Hard-Auto benchmark, it beat Gemma 2, Qwen 2.5, and Llama 3.1 in its class. High transparency, strong community reputation, industrial-grade production.\nCandidate B: jobautomation/OpenEuroLLM-Portuguese\nA personal project from an Ollama community user (jobautomation), fine-tuned from Gemma 3 on Portuguese-language corpora. No benchmarks, no technical report, just a README. The name borrows from the EU\u0026rsquo;s OpenEuroLLM project but has no relation to that official project.\nHonestly, before testing, I was leaning toward aya-expanse — it\u0026rsquo;s well-documented, has a name behind it, and has benchmark data. OpenEuroLLM-Portuguese was, to me, a black box — more of a gamble.\nWhy not just trust the benchmark scores? # Multilingual LLM benchmark scores share a common problem: nearly all of them are evaluated on data dominated by pt-BR (Brazilian Portuguese). Most \u0026ldquo;Portuguese\u0026rdquo; content on the internet is Brazilian Portuguese, so a model scoring 90 on a PORTUGUESE benchmark might be 90% pt-BR ability and only 10% pt-PT (European Portuguese) ability.\nAnd that 10% is exactly what I care about — writing \u0026ldquo;ônibus\u0026rdquo; instead of \u0026ldquo;autocarro\u0026rdquo; would cost you points, and so would \u0026ldquo;estou estudando\u0026rdquo; instead of \u0026ldquo;estou a estudar.\u0026rdquo; The differences between pt-PT and pt-BR in vocabulary, grammatical structure, verb conjugation, pronoun placement, and preposition usage are things no general-purpose benchmark can measure.\nSo I decided to test with what CIPLE actually asks.\nTest design: 10 A2-level practical questions # I designed 10 questions covering the main dimensions tested by CIPLE A2. Both models used the identical system prompt (both required to use pt-PT), the same temperature (0.5), the same max tokens (500) — only the model name changed.\n# Dimension What it tests 1 Core vocabulary translation pt-PT vs. pt-BR high-frequency word pairs (autocarro/ônibus, etc.) 2 Tu verb conjugation Standard in everyday pt-PT, almost never used in pt-BR 3 Second-person address forms When tu/você/o senhor are used in Portugal 4 Bakery-conversation scenario A common A2 scenario, testing natural phrasing 5 Self-introduction writing Directly matches CIPLE\u0026rsquo;s writing section 6 Future-tense tendency Whether ir+inf or futuro simples is more everyday 7 Preposition usage European preposition usage (ir a casa vs. ir para casa) 8 Authentic colloquialisms Meaning of fixe/giro/está-se bem 9 Grammar correction Rewriting a pt-BR passage into pt-PT 10 Listening comprehension Simulated CIPLE train-station announcement question Every question had a clear correct answer — this wasn\u0026rsquo;t a subjective \u0026ldquo;does it write well\u0026rdquo; evaluation, but a check of how solid the model\u0026rsquo;s grasp of pt-PT actually is.\nThe results were a shock # The bottom line first: OpenEuroLLM-Portuguese won decisively, 48/50 against aya-expanse\u0026rsquo;s 35/50. What was even more surprising was that aya-expanse\u0026rsquo;s mistakes weren\u0026rsquo;t minor details — they were systematic pt-BR contamination.\nHere are a few of the most telling examples.\nQ5 self-introduction: a single pronoun position gave it away. I had both models write an A2-level student self-introduction.\naya-expanse\u0026rsquo;s output (excerpt): \u0026ldquo;\u0026hellip;estou a estudar para me tornar um tradutor e interprete, e actualmente me preparo para o exame CIPLE\u0026hellip;\u0026rdquo;\nOpenEuroLLM\u0026rsquo;s output (excerpt): \u0026ldquo;\u0026hellip;Estou a preparar-me para o CIPLE para melhorar o meu português!\u0026rdquo;\nThere\u0026rsquo;s a key pt-PT grammar point here: clitic pronoun placement. In pt-PT, in a context that doesn\u0026rsquo;t trigger proclisis (a pronoun before the verb), the pronoun must come after the verb (enclisis) — i.e., \u0026ldquo;preparo-me.\u0026rdquo; Writing \u0026ldquo;me preparo,\u0026rdquo; with the pronoun placed first, is typical pt-BR grammar. Aya wrote \u0026ldquo;me preparo,\u0026rdquo; which would cost points on a CIPLE writing question. OpenEuroLLM not only got the pronoun position right, it also naturally used the pt-PT-specific progressive construction \u0026ldquo;estou a + infinitive\u0026rdquo; (pt-BR would say \u0026ldquo;estou me preparando\u0026rdquo;). This is one of the hardest pt-PT signals to get right, and OpenEuroLLM passed cleanly.\nQ6 future tense: aya got the facts backwards. I asked both models how to say \u0026ldquo;I am going to travel to Porto next week\u0026rdquo; in pt-PT, and which is more commonly used day-to-day, ir + infinitivo or futuro simples?\naya-expanse\u0026rsquo;s answer: \u0026ldquo;In everyday Portuguese, futuro simples (\u0026lsquo;Viajarei\u0026hellip;\u0026rsquo;) is more frequent and natural; ir + infinitivo is used less.\u0026rdquo;\nOpenEuroLLM\u0026rsquo;s answer: \u0026ldquo;In everyday conversation in Portugal, \u0026lsquo;Vou viajar\u0026hellip;\u0026rsquo; (ir + infinitivo) is more natural and common. Futuro simples is more formal and rarely used in speech.\u0026rdquo;\nOpenEuroLLM is actually correct here. Whether in Portugal or Brazil, spoken Portuguese favors ir + infinitivo; the simple future (Viajarei) carries a formal, written-register feel. Aya got this completely backwards — this isn\u0026rsquo;t a matter of stylistic preference, it\u0026rsquo;s a plain linguistic fact error. Worse, aya\u0026rsquo;s example sentence used \u0026ldquo;semana que vem,\u0026rdquo; a Brazilian-Portuguese collocation; people in Portugal are more likely to say \u0026ldquo;na próxima semana\u0026rdquo; or \u0026ldquo;para a semana.\u0026rdquo;\nQ3 the subtle status of \u0026ldquo;você\u0026rdquo;: a cultural trap. I asked both models when tu/você/o senhor are each used in pt-PT.\naya-expanse said \u0026ldquo;você\u0026rdquo; is a formal, neutral form, suitable for professional exchanges, strangers, or situations calling for politeness. OpenEuroLLM said \u0026ldquo;você\u0026rdquo; isn\u0026rsquo;t commonly used in Portugal — it can sound somewhat formal or distant, used when wanting to keep some distance without being overly formal.\nThis was the question that hit me hardest. What aya described is entirely the Brazilian usage pattern. In Portugal, saying \u0026ldquo;você\u0026rdquo; to a stranger isn\u0026rsquo;t just impolite — it can come across as condescending or distancing. In formal settings, people in Portugal either use \u0026ldquo;o senhor/a senhora\u0026rdquo; or drop the subject entirely (verb conjugation already carries the person). This is a genuinely sensitive point in Portuguese culture that foreign learners commonly trip over. OpenEuroLLM caught this nuance — \u0026ldquo;um pouco formal ou distante\u0026rdquo; (a bit formal or distant) — which is the real sense Portuguese speakers have of it. If a student followed aya\u0026rsquo;s advice and used \u0026ldquo;você\u0026rdquo; with a stranger on the street in Portugal, they\u0026rsquo;d likely get an awkward look in return.\nQ7 preposition: \u0026ldquo;vou para casa\u0026rdquo; vs. \u0026ldquo;vou a casa\u0026rdquo;. This is a classic pt-PT sticking point: both a and para translate to \u0026ldquo;to/going,\u0026rdquo; but they carry different meaning. Vou a casa implies a brief stop (\u0026ldquo;going home for a bit, then leaving again\u0026rdquo;); Vou para casa implies returning and staying a while (\u0026ldquo;heading home [for the day]\u0026rdquo;). I gave the prompt \u0026ldquo;Vou ___ casa\u0026rdquo; with no additional context, defaulting to the scenario of going home after work. aya filled in a, OpenEuroLLM filled in para. If you ask a Portuguese person where they\u0026rsquo;re heading after work, nine times out of ten they\u0026rsquo;ll say \u0026ldquo;vou para casa,\u0026rdquo; not \u0026ldquo;vou a casa.\u0026rdquo;\nWhy did aya-expanse trip up on these points? # Thinking it over afterward, this is actually explainable. Even though aya-expanse\u0026rsquo;s training data is large in volume, it\u0026rsquo;s shared across 23 languages, and the Portuguese portion is likely dominated by pt-BR (reflecting the real-world distribution of Portuguese content online). Its statistical understanding of \u0026ldquo;Portuguese\u0026rdquo; is dominated by pt-BR. When you explicitly ask for pt-PT, it can switch vocabulary (high-frequency words like autocarro, ginásio, telemóvel come out fine), but the deeper grammatical structure and feel for the language can\u0026rsquo;t be temporarily injected through a system prompt — pronoun placement, tense preference, culturally sensitive terms are baked into the model, and no prompt can undo that.\nThat\u0026rsquo;s exactly where OpenEuroLLM-Portuguese\u0026rsquo;s edge lies. It\u0026rsquo;s a niche model fine-tuned specifically on pt-PT corpora — no paper, no benchmark, but the author is very likely a Portuguese person who genuinely understands pt-PT — you can tell from its grasp of the subtle sense of \u0026ldquo;você,\u0026rdquo; its use of \u0026ldquo;Queria\u0026rdquo; rather than \u0026ldquo;Quero\u0026rdquo; as the polite way to order at a shop, and its natural use of the distinctly Portuguese collocation \u0026ldquo;para a semana.\u0026rdquo; A focused small model beating a general-purpose large one on a narrow domain — this isn\u0026rsquo;t the first time that\u0026rsquo;s happened.\nA reflection on \u0026ldquo;transparency\u0026rdquo; # Before testing, my main concern with OpenEuroLLM-Portuguese was that it\u0026rsquo;s a black box — no paper, no benchmarks, an anonymous author, no disclosed training details. By comparison, aya-expanse is practically a model of academic transparency.\nBut the actual test results made me realize: transparency and fitness-for-purpose are two different things. For industrial applications, for research that needs reproducibility, for commercial deployment, transparency obviously matters. But for an end user like me, treating the model as a tool, what actually matters is: does it work well for my specific use case?\nAll of aya-expanse\u0026rsquo;s benchmark scores are meaningless for this pt-PT-specific scenario — because none of those benchmarks actually measure pt-PT\u0026rsquo;s distinctive features. OpenEuroLLM-Portuguese, despite having no public scores at all, performed consistently, precisely, and in line with the CIPLE standard across my 10 real-scenario questions.\nA test of 10 real-scenario questions is worth more than 10 benchmark reports — provided those 10 questions actually cover the dimensions you care about.\nFinal workflow adjustment # After testing, I changed my configuration:\ncat \u0026gt; Modelfile.ciple \u0026lt;\u0026lt;EOF FROM jobautomation/OpenEuroLLM-Portuguese:latest SYSTEM \u0026#34;\u0026#34;\u0026#34;És um tutor de português europeu especializado em preparar estudantes para o exame CIPLE A2. Responde sempre em pt-PT rigoroso. Corrige erros típicos de quem aprendeu pt-BR. Sê conciso.\u0026#34;\u0026#34;\u0026#34; PARAMETER temperature 0.6 EOF ollama create ciple-tutor -f Modelfile.ciple I didn\u0026rsquo;t delete aya-expanse, just changed what it\u0026rsquo;s used for:\nScenario Which model Precise pt-PT proofreading, Portuguese writing OpenEuroLLM-Portuguese Quick multilingual switching (English/Chinese/Portuguese translation) aya-expanse Verifying Portuguese cultural expressions, authentic usage OpenEuroLLM-Portuguese Long-form comprehensive text needs aya-expanse OpenEuroLLM runs about twice as slow as aya (172 seconds total for the 10 questions vs. 87 seconds), but for exam prep, accuracy matters far more than speed.\nA few open questions # Who\u0026rsquo;s actually behind OpenEuroLLM-Portuguese? The README doesn\u0026rsquo;t say. Judging from output quality, it\u0026rsquo;s very likely a Portuguese person who understands pt-PT, but the specific background is unknown. How clean is its training data? No public information — it may have some pt-BR contamination mixed in (I noticed one or two questionable phrasings during testing), but overall it\u0026rsquo;s far cleaner than aya\u0026rsquo;s. Is it worth re-testing once official AMALIA and Gervásio GGUFs are out? These two are official pt-PT-specific models from Portuguese academic institutions, and in theory should be more specialized than a community fine-tune — but as of now there\u0026rsquo;s no ready-made GGUF or Ollama version, so I\u0026rsquo;ll test again once someone in the community packages it.\n","date":"2026-04-18","externalUrl":null,"permalink":"/posts/testing-pt-pt-language-models-real-scenario/","section":"Blog","summary":"Blind-testing two Ollama models against real CIPLE A2 Portuguese-exam questions, the well-documented, benchmark-topping model lost badly to an obscure community fine-tune — because the benchmarks were measuring Brazilian Portuguese, not European.","title":"Testing Models Against a Real Use Case, Not Just Benchmarks","type":"posts"},{"content":"I\u0026rsquo;ve recently been in charge of writing the project plan for AI-generated audit rules.\nWhen working on the AI plan for audit rules, we initially considered working our way up from the underlying data and business rules step by step, but quickly realized that path was too inefficient, with an unpredictable timeline.\nInstead, we leaned toward putting the assets already accumulated in our existing audit system to use — reverse-generating audit rules and dictionary libraries via scripts, effectively building the AI capability on top of what already exists rather than rebuilding the whole system from zero.\nWe put the focus of phase one on \u0026ldquo;existing stock\u0026rdquo; rather than \u0026ldquo;incremental growth.\u0026rdquo; The core reason: the existing audit scripts are themselves the product of long-term production validation — mature business logic, high reliability, making them the highest-quality training data available. At the same time, whether it\u0026rsquo;s the group-level revenue assurance system, cBSS auditing, or provincial custom auditing, these three systems together already cover the vast majority of mainstream scenarios — there\u0026rsquo;s no \u0026ldquo;not enough data\u0026rdquo; problem in the near term.\nWe\u0026rsquo;re prioritizing the path \u0026ldquo;script → AI parsing → human confirmation → ingestion,\u0026rdquo; using SQL scripts already validated in production as the core input, letting the AI understand and structure these rules, with humans reviewing before anything gets finalized into a standardized knowledge base. Only once this foundation is solid will we gradually consider opening up the ability to generate rules directly from natural language — but always on the condition that results stay controllable, auditable, and reversible.\nPrioritizing structured consolidation of existing assets this way lets us quickly stand up a usable AI capability on one hand, while continuously refining and iterating the model based on real business feedback on the other. In essence, we\u0026rsquo;re using \u0026ldquo;already-validated knowledge\u0026rdquo; to constrain the AI\u0026rsquo;s output boundaries, structurally avoiding the \u0026ldquo;hallucination\u0026rdquo; problem that comes with fully free-form generation. In other words, we\u0026rsquo;re not having the AI invent rules from scratch — we\u0026rsquo;re having it understand, abstract, and enhance within an existing rule system, which preserves both efficiency and a quality floor.\n","date":"2026-04-18","externalUrl":null,"permalink":"/posts/audit-rules-ai-existing-assets-first/","section":"Blog","summary":"Writing the plan for an AI-generated audit-rules project, we chose to reverse-engineer rules from years of proven production scripts rather than rebuilding a rule system from raw data and business logic.","title":"Build From What Already Exists, Then Generate: An AI Audit-Rules Project","type":"posts"},{"content":" I. Goal # Set up and validate a speech-processing pipeline:\naudio → VibeVoice → auto-generated SRT text → VibeVoice → audio file (exploratory) And evaluate its usability for:\nvideo subtitle generation multi-speaker identification Chinese/Portuguese speech generation quality eventually feeding into a video-generation pipeline (MoviePy) II. Environment setup # 1️⃣ Base environment\nOS: Ubuntu (GPU server) Python: 3.9 (already installed) GPU: CUDA already configured 2️⃣ Create a virtual environment\npython3 -m venv vibevoice-env source vibevoice-env/bin/activate III. Getting the code and installing # 1️⃣ Clone the project\ngit clone https://github.com/microsoft/VibeVoice.git cd VibeVoice 2️⃣ A key snag (logged)\nRunning pip install -r requirements.txt failed, because the project manages dependencies via pyproject.toml (the newer convention).\n3️⃣ The correct install method\npip install --upgrade pip pip install -e . Extra dependencies (to avoid missing packages):\npip install transformers accelerate librosa soundfile IV. Downloading the model (ASR) # ❗ A mistake (logged): typing Python code directly into bash failed — Python code got mistakenly run as a shell command.\n✅ The correct way — run it inside Python:\nfrom huggingface_hub import snapshot_download snapshot_download( repo_id=\u0026#34;microsoft/VibeVoice-ASR\u0026#34;, local_dir=\u0026#34;./models/asr\u0026#34; ) V. Core capability one: audio → SRT (the main focus) # ✔ Final result: audio → VibeVoice → multi-speaker subtitles → SRT file\n✔ Model used: microsoft/VibeVoice-ASR-HF (recommended). Supports long-form audio, speaker identification, and timestamps.\n✔ Running the script:\npython vibevoice_to_srt.py --audio test.wav Output: test.srt\n✔ Sample output:\n1 00:00:00,000 --\u0026gt; 00:00:03,200 [Speaker 1] Hello everyone, welcome to the speech recognition system. 2 00:00:03,200 --\u0026gt; 00:00:06,800 [Speaker 2] Today we\u0026#39;re testing multi-speaker subtitle functionality. VI. Core capability two: text → speech (TTS) # ✔ Available option: using VibeVoice-Realtime-0.5B\nInstall:\npip install -e .[streamingtts] ✔ Test method:\npython demo/realtime_model_inference_from_file.py \\ --model_path microsoft/VibeVoice-Realtime-0.5B \\ --txt_path demo/text_examples/test.txt \\ --speaker_name Carter VII. Multi-language test results (the key finding) # 🇵🇹 European Portuguese: ✅ can produce speech, but ⚠️ mediocre pronunciation, ⚠️ not very natural.\n🇨🇳 Chinese: ✅ can read it, but ❌ not natural, ❌ has an accent, ❌ not suitable for video voiceover.\nVIII. Summary of key issues # Issue Cause No requirements.txt Uses pyproject.toml instead Python code errored Was run in bash Chinese speech is poor Not a primary supported language TTS incomplete Official limitation IX. Capability assessment (important) # ✔ ASR capability (strongly recommended)\nCapability Rating Long-form audio ⭐⭐⭐⭐⭐ Multi-speaker ⭐⭐⭐⭐⭐ Timestamps ⭐⭐⭐⭐⭐ Subtitle generation ⭐⭐⭐⭐⭐ 👉 Ready to use directly in production (a subtitling system)\n❌ TTS capability (not recommended right now)\nCapability Rating English ⭐⭐⭐ Chinese ⭐ Portuguese ⭐⭐ Stability ⭐⭐ X. Final tech-stack decision # Recommended architecture:\nMarkdown ↓ Text generation (LLM / Ollama) ↓ TTS (Spark-TTS / Azure) ↓ Audio ↓ VibeVoice (ASR) ↓ SRT subtitles ↓ MoviePy ↓ Video XI. Next steps (planning) # 1️⃣ Automated subtitle system: audio → SRT (multi-speaker). Useful for video subtitles, meeting notes, podcast transcription.\n2️⃣ AI video generation system: Markdown → speech → subtitles → video\n3️⃣ AI podcast generation: topic → LLM → dialogue → speech → video\n4️⃣ Multi-language content production: Chinese → translation → English/Portuguese → voiceover\nXII. Where things stand # In one line: right now VibeVoice\u0026rsquo;s real value is in ASR (subtitle generation), not TTS (speech generation).\n","date":"2026-04-18","externalUrl":null,"permalink":"/posts/vibevoice-asr-tts-setup-diary/","section":"Blog","summary":"Setting up Microsoft’s VibeVoice to test audio-to-subtitle generation and text-to-speech: the ASR side (multi-speaker SRT generation) turned out production-ready, while TTS — especially Chinese — wasn’t there yet.","title":"VibeVoice Setup Diary (ASR + TTS Exploration)","type":"posts"},{"content":"Background\nI have a GPU server at home running Ubuntu (hostname: gpu-nvdia), normally configured with Wake-on-LAN (WOL) — when I need to run a large model, I send a magic packet remotely to wake it, and shut it down via SSH once I\u0026rsquo;m done.\nRecently a strange issue showed up: after running the shutdown command, the machine would power off successfully, but then reboot itself on its own about ten-odd seconds later.\nTroubleshooting\nStep 1: confirm the shutdown command itself wasn\u0026rsquo;t the issue\nThere\u0026rsquo;s a common pitfall with remote SSH shutdowns: when the SSH connection drops, the shutdown process gets interrupted by a SIGHUP signal, leaving the shutdown sequence incomplete. I switched to a method that keeps the shutdown process running regardless of the SSH disconnect. After running it, the machine did shut down cleanly — but it still rebooted itself ten-odd seconds later. So the issue wasn\u0026rsquo;t the shutdown command itself.\nStep 2: check the boot logs and wakeup devices\nThe query results showed both of today\u0026rsquo;s shutdowns auto-rebooting within about a minute. The output of /proc/acpi/wakeup showed several devices in the enabled state, with the network-card-related device looking suspicious.\nStep 3: rule out RTC scheduled wakeup\nAWAC is the ACPI real-time clock — enabled could mean a scheduled wakeup is set. The query came back empty, ruling out an RTC scheduled wakeup.\nStep 4: check the suspicious PCI device\nChecking the device showed it was the network card (Realtek Semiconductor Co., Ltd. RTL8111/8168/8411). Checking that card\u0026rsquo;s WOL status showed Wake-on: g — there\u0026rsquo;s the root cause.\nRoot cause\nThe network card had WOL enabled (magic packet mode), and its corresponding ACPI power/wakeup was also enabled. With both conditions stacked, once the machine shut down, ordinary network broadcast packets from other devices on the LAN — not just magic packets — could trigger a wakeup. Rebooting ten-odd seconds after shutdown lined up exactly with the LAN broadcast window.\nFix\nMagic packet wakeup is handled at the network card\u0026rsquo;s firmware level and doesn\u0026rsquo;t depend on ACPI\u0026rsquo;s power/wakeup. So the ACPI-level wakeup permission on the network card can be disabled while keeping magic packet wakeup intact.\nMade this persistent across reboots by creating a systemd service.\nSummary\nThe key was understanding that when a network card\u0026rsquo;s ACPI power/wakeup is enabled, LAN broadcast traffic can trigger a wakeup — whereas magic packet wakeup is handled by the card\u0026rsquo;s firmware and doesn\u0026rsquo;t depend on ACPI wakeup permissions at all.\n","date":"2026-04-18","externalUrl":null,"permalink":"/posts/ubuntu-wol-shutdown-auto-restart-fix/","section":"Blog","summary":"A GPU server configured for Wake-on-LAN would shut down cleanly, then reboot itself ~10 seconds later — the culprit was ACPI wakeup left enabled on the network card, letting ordinary LAN broadcast traffic trigger a wake, not just the magic packet.","title":"Tracking Down Why My Ubuntu Server Kept Auto-Restarting After Shutdown, Post Wake-on-LAN Setup","type":"posts"},{"content":"After switching to a new iPhone, I opened RemNote to review my Portuguese vocabulary cards and found the text on the flashcards uncomfortably small. Even with glasses on it was a strain, and getting through 280 cards left my eyes tired.\nMy first instinct was to look for a font setting — I went through every option under Appearance and found nothing. A search of the community turned up complaints about this exact issue going back over two years. RemNote\u0026rsquo;s own official reply was:\n\u0026ldquo;Currently the only workaround is custom CSS; we hope to add this setting in a future update.\u0026rdquo;\nTwo years later, the feature still hasn\u0026rsquo;t shipped. So I fixed it myself.\nStep 1: finding the Custom CSS entry point # RemNote → Settings → search \u0026ldquo;CSS\u0026rdquo; → Custom CSS → add a blank CSS block.\nStep 2: nuke everything first, to confirm CSS actually works # Most tutorials online were written back in 2020, and the class names have long since changed across versions. Rather than trying outdated selectors one by one, it\u0026rsquo;s better to first confirm CSS injection even works at all, with a wildcard:\n* { font-size: 24px !important; } Save it and open the flashcard review screen — if all the text got bigger, CSS injection is working and it\u0026rsquo;s just the selector that\u0026rsquo;s wrong. If nothing changed at all, the iOS app\u0026rsquo;s WebView is blocking CSS entirely, and this path is a dead end.\nIn my case, it worked — the whole interface got bigger, including the top nav bar and the bottom rating buttons.\nStep 3: targeting precisely — only enlarge the card body # Enlarging everything globally breaks the layout. The goal is to only make the card content area bigger, leaving everything else untouched.\nEventually landed on a working combination of selectors:\n.spacedRepetition .spacedRepetitionContent, .spacedRepetition .spaced-repetition__prompt, .spacedRepetition .rem-text { font-size: 20px; line-height: 2.0 !important; } Result: the card body text — like \u0026ldquo;cardinal number ↔ cardinais, /kɐrdi\u0026rsquo;najʃ/\u0026rdquo; — came out large and clear, while the ×😬😄👑 rating buttons at the bottom and the path breadcrumb at the top were completely unaffected.\nline-height: 2.0 is double the font size — Portuguese vocabulary comes with phonetics and example sentences, so a wider line height keeps it from feeling cramped. Adjust anywhere from 1.6 to 2.5 depending on preference.\nSummary # Method Effective? Adjusting font size directly in Settings ❌ No such option Following the iOS system font size ❌ App doesn\u0026rsquo;t respond Switching themes ❌ Themes only control color Custom CSS wildcard * ✅ Works, but affects everything Custom CSS targeted selectors ✅ Only affects card body If you\u0026rsquo;re also using RemNote to review language flashcards and find the text too small and cramped on your phone, this CSS is ready to use as-is — just adjust the font size to match your own eyesight and screen size.\n","date":"2026-04-14","externalUrl":null,"permalink":"/posts/remnote-ios-flashcard-font-size-css/","section":"Blog","summary":"RemNote’s iOS app has no font-size setting for flashcards — after confirming custom CSS actually gets through the WebView, a couple of targeted selectors made the card text readable without touching the rest of the UI.","title":"RemNote's iPhone Flashcard Text Too Small? Fixed It With CSS","type":"posts"},{"content":"","date":"2026-04-11","externalUrl":null,"permalink":"/tags/openclaw/","section":"Tags","summary":"","title":"OpenClaw","type":"tags"},{"content":" Background # This weekend I spent a good chunk of time doing a systematic overhaul of OpenClaw. The trigger was simple: my previously ordered $300 free Google Cloud credit expired, and Gemini API calls started returning widespread 429s (rate limit exceeded) — the bot stopped responding entirely.\nI needed to find a way to keep the system running at the lowest possible cost, ideally zero. I ended up settling on a three-tier model chain that, all told, runs at essentially zero additional cost.\nThe fix: a three-tier model chain # Primary model: ChatGPT Plus (openai-codex/gpt-5.4)\nSubscribed through a low-price region, so the cost itself isn\u0026rsquo;t high. With my language exam coming up soon, I\u0026rsquo;m not doing much Codex coding right now, so Premium usage is limited — this serves as my daily main model.\nMiddle-tier fallback: 3 free NVIDIA NIM models\nBrought in Kimi K2.5, MiniMax M2.5, and GLM-5. These switch in automatically whenever the primary model\u0026rsquo;s quota runs out or hits an issue — completely free.\nBottom-tier safety net: OpenRouter Free\nopenrouter/free automatically picks the best available free model, with 200 requests a day — the final fallback, at no cost at all.\nKey changes, logged # ① Wiring up the NVIDIA NIM API # Registered an account at build.nvidia.com and generated an nvapi-xxx API key. Added a new nvidia:default profile in auth-profiles.json with the API key. Added a new nvidia provider in models.json, pointing its base URL at https://integrate.api.nvidia.com/v1. Hung Kimi K2.5, MiniMax M2.5, and GLM-5 under the nvidia provider, sharing the same API key. Updated clawdbot.json to add all three models to models.providers. ② Pitfalls hit along the way # gateway and pm2 process conflict: having both systemd and pm2 manage the process caused port 18789 to keep getting grabbed repeatedly. Stopping pm2\u0026rsquo;s oc-gateway fixed it — systemd should be the sole manager. Model ID routing errors: splitting the third-party models (moonshotai / minimaxai / z-ai) out into their own separate providers caused 404s. Ended up hanging them all under the nvidia provider instead; OpenClaw\u0026rsquo;s correct routing format is nvidia/moonshotai/kimi-k2.5. clawdbot.json doesn\u0026rsquo;t support the alias field: only agents/main/agent/models.json supports aliases — writing one into clawdbot.json makes the config invalid and the gateway refuses to start. openai-codex token expired: re-ran the OAuth flow via openclaw configure; the new token was written to the openai-codex:walker.wangw@gmail.com profile, and I had to manually update the lastGood field to point at the new profile. Google Gemini kept 429ing: demoted it to fourth in the fallback order, only triggered once the first three tiers have all failed. ③ The final fallback chain # Primary: openai-codex/gpt-5.4 Fallback 1: nvidia/moonshotai/kimi-k2.5 Fallback 2: nvidia/minimaxai/minimax-m2.5 Fallback 3: nvidia/z-ai/glm5 Fallback 4: google/gemini-3-flash-preview Fallback 5: openrouter/free After all this time, how my usage has shifted # OpenClaw\u0026rsquo;s buzz domestically is cooling off fast, but I\u0026rsquo;ve kept using it — just with the use cases shifting. Some features stuck around, others I deliberately dropped.\n⏰ Scheduled reminders (kept) # Still using this. This time around I also set up a cron job to push a Telegram reminder 48 hours before the Codex token expires.\n📰 News summarization (kept) # My most-used feature right now. ChatGPT and Gemini often say they \u0026ldquo;can\u0026rsquo;t read the full article\u0026rdquo; on long pieces — just handing the link to OpenClaw gets a full read, no manual copy-pasting needed.\n📈 Investment platform automation (kept, logic reworked) # Financial info and account updates get pushed via Telegram. The role of the decision logic has fundamentally changed:\nBefore: the model drove decisions directly. Now: the model only handles intent recognition and wording — all decision strategy is implemented entirely in backend code. Why: this avoids the model making misjudgments from randomness on sensitive, important operations, keeping things deterministic. 🃏 Language learning / flashcards (removed) # Dropped this one deliberately, for two reasons:\nCost: using an LLM to memorize vocabulary burns an enormous amount of tokens — nowhere near as cost-effective as spending that same quota on coding. Switched to RemNote for flashcards instead, which syncs across computer, phone, and tablet, is fast, and honestly feels better. Accuracy: I\u0026rsquo;d previously had OpenClaw bulk-supplement knowledge points to help with studying, but because the model used wasn\u0026rsquo;t consistent call to call, its take on the same knowledge point would contradict itself over time, creating logical conflicts. An LLM\u0026rsquo;s randomness is a real liability for memorization tasks — you never know if today\u0026rsquo;s answer matches what it said last week. Wrap-up # This overhaul gave me a clearer sense of where an LLM\u0026rsquo;s usefulness actually ends: an LLM is good as an accelerator, not as an authoritative source. Using it to interpret, polish, and push notifications is a good fit; letting it directly drive decisions or build a knowledge base is where the risk gets amplified. Zero-cost survival achieved — still running.\n","date":"2026-04-11","externalUrl":null,"permalink":"/posts/openclaw-weekend-zero-cost-overhaul/","section":"Blog","summary":"When my free Google Cloud credits ran out and Gemini started rate-limiting, I rebuilt OpenClaw’s model chain into a three-tier free-and-cheap failover setup — and rethought which use cases were still worth the tokens.","title":"OpenClaw's Weekend \"Zero-Cost\" Overhaul","type":"posts"},{"content":"Helped my wife switch to a Huawei phone over the weekend. I figured migrating her data would be routine, but I got completely stuck on the notes app. After going in circles for a while, I used Claude\u0026rsquo;s Chrome extension to automate exporting all 352 notes in about 20 minutes. Here\u0026rsquo;s the full process, in case you\u0026rsquo;re facing the same kind of cross-platform notes migration.\nBackground: an ordinary phone switch, an unusual snag # Huawei\u0026rsquo;s own \u0026ldquo;Phone Clone\u0026rdquo; tool handled most of the migration smoothly — contacts, photos, apps. But when it came to the notes from her OnePlus phone, nothing worked: no matter what I tried, the official migration tool just couldn\u0026rsquo;t move the notes over to Huawei directly.\nFirst attempt: the \u0026ldquo;switching assistant\u0026rdquo; apps recommended online # A search turned up a wave of \u0026ldquo;phone switching assistant\u0026rdquo; apps, all recommended on social media. I gave it a shot and downloaded three of them from the OPPO app store — the experience was rough across the board: opening one meant a screen full of ads with endless popups to close, the actual feature was buried behind layer after layer of onboarding screens, and some that claimed to support \u0026ldquo;notes migration\u0026rdquo; simply couldn\u0026rsquo;t read OnePlus\u0026rsquo;s note data at all.\nConclusion: that path was a dead end. Those tools were basically ad shells with almost no real capability behind them.\nThe approach that worked: cloud, browser, and Claude\u0026rsquo;s Chrome extension # After giving up on those apps, I went back to basics: if the phone-to-phone path wasn\u0026rsquo;t working, move the data to a computer and handle it with something more flexible. The whole thing broke down into three stages.\nStep 1: sync the notes to OnePlus\u0026rsquo;s cloud. On the OnePlus phone, go to Settings → Account → Cloud Services, confirm cloud sync is turned on for Notes, and wait for everything to finish syncing (checkable from within Cloud Services). If cloud sync had never been turned on before, the first sync can take a few minutes — make sure the phone is on Wi-Fi and signed in properly.\nStep 2: open the OnePlus cloud notes in a browser. On a computer, open the OnePlus cloud web app (the exact URL varies by account region), sign in with the same OnePlus account, and go to the Notes section to confirm every note shows up on the web. At this point you can browse through notes one by one in the browser — but manually copying a few hundred of them clearly wasn\u0026rsquo;t realistic, which is where Claude\u0026rsquo;s Chrome extension came in.\nStep 3: automate the batch export with Claude in Chrome. This was the core of the whole approach — I used Claude\u0026rsquo;s Chrome browser extension (Claude in Chrome), which can interact directly with the current page\u0026rsquo;s content and run JavaScript.\nClaude\u0026rsquo;s first suggestion — screenshot each note, run OCR, convert to text — I ruled out. It\u0026rsquo;s a general approach, but far too slow for 352 notes, and OCR accuracy on Chinese text isn\u0026rsquo;t reliable enough anyway.\nSo I switched to an API-level approach instead. I opened the browser\u0026rsquo;s developer tools (F12) and watched the page\u0026rsquo;s network requests, which turned up the key pieces: an API for fetching the note list — the page calls an endpoint that returns JSON with every note\u0026rsquo;s ID, title, and summary — and an API for fetching a single note\u0026rsquo;s full content by its ID.\nI fed all of that to Claude: the list endpoint\u0026rsquo;s URL format and request method, the shape of the returned JSON (field names, nesting), and where the note content and its encoding lived in the response.\nOnce Claude confirmed the approach would work, it wrote and ran a JavaScript script in the extension that, roughly: called the list endpoint to get every note\u0026rsquo;s ID, requested the detail endpoint for each one in turn to get its full content, converted each note to Markdown (handling the title, body, and timestamp fields properly), and packaged everything up for batch download to local Markdown files.\nThe whole thing took about 20 minutes, and all 352 notes came out successfully.\nA few tips on the process: in the browser\u0026rsquo;s dev tools, switch to the Network panel and filter for XHR/Fetch requests, then click a note in the list to see the matching API call; paste the request URL and a sample of the returned JSON straight to Claude and it can work out the data structure itself; if there are a lot of notes, check whether the API uses pagination parameters (page, pageSize, offset) and let Claude know so it loops through them; and some cloud services rate-limit their APIs, so it\u0026rsquo;s worth adding a small delay between requests (500ms or so) in the script.\nStep 4: getting the Markdown notes onto the new phone # After all this, I made a decision: drop the phone\u0026rsquo;s built-in notes app entirely and move to Obsidian instead.\nThe reasoning is simple: the data format is universal — Obsidian just uses plain Markdown files, with no proprietary format, openable in any text editor; storage is fully under your control — the files just live in a local folder, and you can sync them however you like (cloud drive, Git, a NAS); it works seamlessly across platforms — iOS, Android, Windows, macOS, Linux, all supported; and you\u0026rsquo;re never locked in again — even if Obsidian shut down tomorrow, the .md files would still be intact and openable in any other tool.\nThe plan: install Obsidian on both phone and computer, drop the exported Markdown files into Obsidian\u0026rsquo;s vault folder, and use Obsidian\u0026rsquo;s sync options (iCloud, Remotely Save, Syncthing, etc.) to keep it available across devices.\nWrap-up # Step Action Tool 1 Sync notes to the cloud OnePlus Cloud Services 2 Log into the cloud web app to view notes Chrome 3 Reverse-engineer the API Browser dev tools (F12) 4 Automate the batch export to Markdown Claude in Chrome 5 Import into a universal notes tool Obsidian The core idea here is bypassing the phone\u0026rsquo;s own limitations and using the cloud web app plus browser automation to pull the data out directly. In principle this isn\u0026rsquo;t specific to OnePlus — it should work for any brand (OPPO, vivo, Xiaomi, and others) that offers a web version of its cloud notes service.\nIf you\u0026rsquo;re stuck on the same notes-migration headache during a phone switch, it\u0026rsquo;s worth trying this approach. And more importantly — it\u0026rsquo;s a good moment to seriously consider getting your own data out of a manufacturer\u0026rsquo;s walled garden for good.\n","date":"2026-03-15","externalUrl":null,"permalink":"/posts/batch-migrating-oneplus-notes/","section":"Blog","summary":"When ad-stuffed ‘phone switching’ apps couldn’t move 352 notes to a new Huawei phone, reverse-engineering the OnePlus cloud web app’s API and scripting it through Claude’s Chrome extension got the job done in 20 minutes.","title":"Breaking Free: Batch-Migrating OnePlus Notes With Claude in Chrome","type":"posts"},{"content":"","date":"2026-03-15","externalUrl":null,"permalink":"/tags/obsidian/","section":"Tags","summary":"","title":"Obsidian","type":"tags"},{"content":"Date: March 14, 2026 · Problem: adding a new quiz question was taking over 2 minutes to respond.\nThe symptom # Lately, sending OpenClaw a \u0026ldquo;add to quiz\u0026rdquo; command left the bot sitting in a \u0026ldquo;typing\u0026rdquo; state for a long stretch — 2 to 3 minutes before it would respond — and the delay kept getting worse as the quiz grew.\nRoot cause # The data layout. Digging in, I found the quiz data was split across two files: quiz_state.json (56KB, 225 entries) — the actual practice engine, holding SRS data like totalCorrect/totalWrong/weight — and pt_learning_quiz.json (77KB, 224 entries) — a backup of the original question bank with detailed explanations, categories, and tags. Practice sessions only ever read quiz_state.json; the other file sat essentially unused.\nWhy it was slow. quiz_state.json was stored as a standard JSON array. Every time a question got added, the agent had to read the entire file (56KB, 15,000+ tokens), parse the whole array, append the new entry, and write the whole thing back. The bigger the file got, the more tokens each operation burned, and the slower it got.\nExtra overhead from duplicate checking. The existing rules required checking for a duplicate ID before adding anything new, which meant scanning every entry — adding even more time to each request.\nFixes # Turning off duplicate checking. Added a rule to memory/quiz_rules.md:\nBulk Addition: Skip duplicate checking and verification during additions. Simply append new items to the records. Result: additions skip the full scan now, but still require a full read/write of the file.\nMigrating to NDJSON (the core fix). Converted quiz_state.json from a standard JSON array to NDJSON — one independent JSON object per line:\n{\u0026#34;id\u0026#34;:\u0026#34;verb_chegar...\u0026#34;,\u0026#34;question\u0026#34;:\u0026#34;...\u0026#34;,\u0026#34;srs\u0026#34;:{...}} {\u0026#34;id\u0026#34;:\u0026#34;sentence_ele...\u0026#34;,\u0026#34;question\u0026#34;:\u0026#34;...\u0026#34;,\u0026#34;srs\u0026#34;:{...}} {\u0026#34;id\u0026#34;:\u0026#34;sentence_eu...\u0026#34;,\u0026#34;question\u0026#34;:\u0026#34;...\u0026#34;,\u0026#34;srs\u0026#34;:{...}} The original file was backed up as quiz_state.json.bak (56KB); the migrated file came out at 44KB — 12KB smaller, with all 225 entries intact.\nAdded rules 8 and 9 to quiz_rules.md to document the new format:\n8. Storage Format: NDJSON, one independent JSON object per line 9. Operations: - Add: append a new line directly (no need to read the existing file) - Update: read the full file → edit the matching line → write it back Before and after # Operation Before After Adding a question Read the full 56KB file + edit the end + write it back Append one line directly ✅ Token cost (adding) 15,000+ tokens Under 100 tokens ✅ Updating SRS after answering Read full file + edit + write back Read full file + edit + write back (no change) File size 56KB 44KB What\u0026rsquo;s left unoptimized # Updating SRS after answering still can\u0026rsquo;t be sped up — every answer needs to update a specific entry\u0026rsquo;s totalCorrect/totalWrong, and NDJSON still requires a full read/write since there\u0026rsquo;s no way to edit a single line in place. With 225 entries it\u0026rsquo;s not a big deal right now; if it grows into the thousands, migrating to SQLite is worth considering.\npt_learning_quiz.json stays as a backup — I\u0026rsquo;m not folding its explanation field into quiz_state.json, to avoid bloating that file further.\nFiles involved # /root/clawd/memory/quiz_state.json # NDJSON format, currently in use /root/clawd/memory/quiz_state.json.bak # backup of the original JSON array format /root/clawd/memory/pt_learning_quiz.json # original question bank backup (unused) /root/clawd/memory/quiz_rules.md # SRS rules file (updated) ","date":"2026-03-15","externalUrl":null,"permalink":"/posts/openclaw-quiz-speed-optimization/","section":"Blog","summary":"Adding a quiz question was taking 2-3 minutes and getting slower over time — the fix was switching the data file from a JSON array to NDJSON so new items append in one line instead of a full rewrite.","title":"Optimizing OpenClaw's Quiz Response Speed","type":"posts"},{"content":"Date: March 14, 2026 · System: VPS (s877652) / OpenClaw v2026.3.8 · Logged by: Wei\nBackground # OpenClaw runs on a VPS, and lately it kept getting slow to respond to messages. I\u0026rsquo;d tried fixing it before by configuring multiple API keys — at one point I even had three configured — without much luck. Today it stopped responding again after sending a message, and I decided to actually get to the bottom of it. In earlier attempts I\u0026rsquo;d leaned mostly on whatever fix the model suggested; this time I specifically asked it to back up its recommendations against OpenClaw\u0026rsquo;s own official documentation, and the results felt noticeably more solid.\nInitial troubleshooting # Checking service status. Running systemctl status openclaw returned Unit openclaw.service could not be found, which I initially mistook for the service not being running.\nThe actual reason: OpenClaw\u0026rsquo;s systemd service is registered at the user level (~/.config/systemd/user/), so it needs systemctl --user instead:\nsystemctl --user status openclaw-gateway # the correct way systemctl --user status openclaw-node Both processes were confirmed running (uptime: 2 days), with service files at /root/.config/systemd/user/openclaw-gateway.service and openclaw-node.service.\nReading the logs. Checking /tmp/openclaw/openclaw-2026-03-14.log turned up a few key errors:\nTime Error type Content 20:22 API rate limit FailoverError: API rate limit reached 20:27 Telegram network drop Network request for 'sendMessage' failed! Repeated HEARTBEAT edit failure Could not find the exact text in HEARTBEAT.md Repeated Missing crontab command crontab: command not found Immediate cause: the Gemini API hit its rate limit, jamming the task queue, compounded by a brief Telegram network interruption.\nDigging deeper: the auth-profile mechanism # Finding a missing registration. I had two Google API keys configured, google:manual and google:default (a planned third, google:third, was never actually written to the config). After the rate limit hit, the system threw FailoverError instead of automatically switching keys. Checking auth-profiles.json turned up the problem: google:manual existed in the secrets file, but clawdbot.json\u0026rsquo;s auth.profiles only had google:default registered — google:manual was missing.\nPer OpenClaw\u0026rsquo;s official docs (/docs/concepts/model-failover.md), the rotation selection order is: an explicit auth.order config, then profiles registered under auth.profiles (this step was skipping the unregistered google:manual), then profiles stored in auth-profiles.json. So google:manual had never actually taken part in failover rotation at all.\nConfirming session stickiness. Checking sessions/sessions.json showed:\n\u0026#34;authProfileOverride\u0026#34;: \u0026#34;google:default\u0026#34;, \u0026#34;authProfileOverrideSource\u0026#34;: \u0026#34;auto\u0026#34; OpenClaw pins an auth profile when a session starts and won\u0026rsquo;t proactively switch away from it for the rest of that session, unless: /new or /reset resets the session, compaction completes, or the current profile enters cooldown.\nThat explained why the system kept using google:default even after restarting the gateway — the session state file retained the pin, and a restart doesn\u0026rsquo;t clear it.\nThe round-robin mechanism. When a new session starts, OpenClaw picks a profile by: preferring OAuth over API-key auth, then — within the same type — picking whichever was least recently used based on usageStats.lastUsed, with anything in cooldown pushed to the back. Checking the data:\ngoogle:manual lastUsed: 2026-01-29 10:39:31 (44 days ago) google:default lastUsed: 2026-03-14 21:07:36 (today) Even after fixing the registration issue, google:manual still wasn\u0026rsquo;t getting picked because of session stickiness — a /new reset was needed to actually trigger round-robin re-selection.\nHow switching behaves after a rate limit. Within the same session: hitting a rate limit triggers cooldown (exponential backoff: 1 minute → 5 minutes → 25 minutes → 1 hour), auto-switches to the other key, and then stays on that key rather than switching back on its own. After a session reset: round-robin re-selects based on lastUsed, picking whichever key has sat idle longest. If both keys are rate-limited: it throws FailoverError and falls into the model-fallback flow.\nWhat I changed # Refreshed the google:manual key — the old one had expired, so I requested a new one and updated auth-profiles.json.\nRegistered google:manual in clawdbot.json, adding it to auth.profiles so it actually participates in failover rotation:\n\u0026#34;auth\u0026#34;: { \u0026#34;profiles\u0026#34;: { \u0026#34;google:default\u0026#34;: { \u0026#34;provider\u0026#34;: \u0026#34;google\u0026#34;, \u0026#34;mode\u0026#34;: \u0026#34;api_key\u0026#34; }, \u0026#34;google:manual\u0026#34;: { \u0026#34;provider\u0026#34;: \u0026#34;google\u0026#34;, \u0026#34;mode\u0026#34;: \u0026#34;api_key\u0026#34; } } } Removed the explicit auth.order to let OpenClaw\u0026rsquo;s native round-robin mechanism balance the two keys automatically, switching via cooldown after a rate limit.\nRestarted the service to apply everything:\nsystemctl --user restart openclaw-gateway Lessons learned # systemctl --user is the key detail — OpenClaw\u0026rsquo;s service is registered at the user level, so system-level commands simply won\u0026rsquo;t find it. A key existing in the secrets file doesn\u0026rsquo;t mean it\u0026rsquo;s actually in effect — writing a key to auth-profiles.json isn\u0026rsquo;t enough on its own; it also has to be registered under auth.profiles in clawdbot.json to actually participate in failover. Session stickiness is by design — restarting the service doesn\u0026rsquo;t reset the session pin; you need to actively send /new to trigger round-robin to pick again. And switching after a rate limit is one-directional — once it switches within a session, it won\u0026rsquo;t switch back on its own until the next session reset.\n","date":"2026-03-14","externalUrl":null,"permalink":"/posts/openclaw-api-rate-limit-fix/","section":"Blog","summary":"Adding more API keys didn’t fix the slowdowns — the real issue was a key that existed in secrets but was never registered for failover, plus session pinning that kept using the same key after a restart.","title":"Chasing Down OpenClaw's Slow-Reply Problem: Auth Profiles and Session Stickiness","type":"posts"},{"content":"There\u0026rsquo;s more and more discussion about OpenClaw lately — is this \u0026ldquo;little crawfish\u0026rdquo; actually worth installing? Based on my time with it, here\u0026rsquo;s my honest take.\nI got in early, back when it was still called ClawdBot, and I\u0026rsquo;ve kept using and upgrading it through the name change and every version bump, all the way to the current 3.8.\nAfter using it for a while, I\u0026rsquo;ve found it genuinely valuable in a few specific areas.\n1. Memory # Normally, when a conversation with a large model runs long, it starts losing earlier context — and I don\u0026rsquo;t just mean dozens of exchanges in a single day, I mean the accumulated total over weeks and months. OpenClaw handles this reasonably well: it remembers every task you give it and stores it as a file.\nMy main use case is learning — both general knowledge and foreign languages. When I hit something I can\u0026rsquo;t quite retain while going through a course, I just toss it over (through Telegram, for instance) and have it added to a quiz set. I\u0026rsquo;ve set up rules so it repeats practice based on my right/wrong ratio, effectively turning it into a practice tool I can use anywhere, anytime.\nAnki does something similar with spaced repetition, but OpenClaw\u0026rsquo;s advantage is that adding content is dead simple — just describe a fact in plain language and it gets added, no need to format it into Anki\u0026rsquo;s specific card structure. The learning and practice rules can also be adjusted on the fly.\n2. Reminders # This is my second-most-used feature — mainly as a daily-assistant style reminder system: recurring daily reminders, one-off event reminders, renewal reminders for various subscriptions, and so on.\nIt\u0026rsquo;s simple to use — set the reminder through conversation, and once the task is done, the system logs it automatically, so I can look back later and clearly see whether I hit each day\u0026rsquo;s tasks on time.\nIf you rely on your own memory for these things, the attention cost is far higher than the cost of actually doing them. OpenClaw turns them into something that just runs automatically in the background — no need to think through reminder rules or trigger conditions, just describe it in plain language and it handles the rest. Reminders here are really just a more convenient path that saves time and lets you focus your actual attention on things that genuinely need it.\nThere are similar apps out there, but they either come with cluttered ads or a subscription fee, and the experience is nowhere near as direct as what OpenClaw offers. I also set up automatic mailbox checking, but honestly it hasn\u0026rsquo;t turned out very useful — I barely use that part.\n3. Information gathering and investing # We can look things up anytime, but not always at the right moment — I can\u0026rsquo;t be constantly checking some specific piece of information myself, and OpenClaw\u0026rsquo;s automated lookups have a real edge here.\nOn the investing side, OpenClaw actually solves an access problem. A lot of people assume this kind of system is only usable by people who can code, but I\u0026rsquo;d argue it\u0026rsquo;s better understood as a \u0026ldquo;middle layer.\u0026rdquo; As I mentioned in an earlier post, I used it to build a dynamic-rebalancing system for my stock investments, and to build out a website. For people who don\u0026rsquo;t have the time or energy to dig into building features from scratch, but just want to use something for a bit, that \u0026ldquo;middle-ground\u0026rdquo; capability is genuinely well suited: you can just tell it your investment strategy directly. The difference from a plain Q\u0026amp;A session is that you first sync your strategy\u0026rsquo;s key points with it, and then the system pulls live data aligned with that strategy and gives you recommendations based on it. This is a general-purpose capability, stronger than a typical Q\u0026amp;A system, able to string every step of the process together.\nWrap-up # The value is in the application, not the technical novelty. OpenClaw\u0026rsquo;s value isn\u0026rsquo;t some breakthrough in the underlying technology — it\u0026rsquo;s that, as a new kind of application, it bundles a bunch of capabilities together and makes them simple and convenient. Market adoption usually doesn\u0026rsquo;t hinge on how cutting-edge a technology is, but on whether it genuinely makes things more convenient.\nA general-purpose agent, well suited as a middle step. You could say it doesn\u0026rsquo;t do any one thing perfectly, but it does a bit of everything. As an intermediate step, with little learning cost or time investment, you can use conversation to explore what you actually need, and once things are clearer, harden the result into a real product using Codex or Claude Code.\nValuable, but don\u0026rsquo;t oversell it. Treating it like your own \u0026ldquo;personal cognitive extension\u0026rdquo; or \u0026ldquo;AI consciousness\u0026rdquo; is going too far. There\u0026rsquo;s still a real problem with memory loss — or \u0026ldquo;drift\u0026rdquo; — where a rule you\u0026rsquo;d settled on will just get forgotten at some point, and it\u0026rsquo;ll invent a different one out of nowhere. So using it directly for anything that needs to be formal or authoritative isn\u0026rsquo;t realistic. Even daily reminders sometimes get \u0026ldquo;forgotten.\u0026rdquo; So it\u0026rsquo;s fine to treat it as a personal companion or tutor, but it\u0026rsquo;s not quite ready to be a work assistant.\nWatch the security risks. People have already been hit by \u0026ldquo;AI scams\u0026rdquo; tied to this — there are injection-style scam prompts circulating online specifically targeting OpenClaw. So: don\u0026rsquo;t run it on a machine you use for anything formal, and don\u0026rsquo;t grant it permissions beyond what you\u0026rsquo;re comfortable with it having. Microsoft\u0026rsquo;s security team\u0026rsquo;s own advice is that OpenClaw isn\u0026rsquo;t suited for a standard personal or enterprise workstation, and should only be deployed in a fully isolated environment.\n","date":"2026-03-12","externalUrl":null,"permalink":"/posts/is-openclaw-worth-it/","section":"Blog","summary":"An honest review after months of daily use, from when it was still called ClawdBot: genuinely useful for memory, reminders, and lightweight investing research — but with real drift and security caveats.","title":"Is OpenClaw Actually Worth Installing?","type":"posts"},{"content":"","date":"2026-02-26","externalUrl":null,"permalink":"/tags/finance/","section":"Tags","summary":"","title":"Finance","type":"tags"},{"content":"Claude\u0026rsquo;s Finance Analysis feature has been getting a lot of buzz lately — the pitch is that a single model can replace the analytical work of multiple roles at a financial firm, from macro analysis to deep-dive stock research, from reading financial statements to building an investment strategy, start to finish.\nToday I couldn\u0026rsquo;t resist trying it myself.\nHonestly, I was completely lost at first — not clear on how to use it or where to even start. Claude\u0026rsquo;s own install instructions for the feature weren\u0026rsquo;t very clear either; what finally got me unstuck was handing it the documentation straight from GitHub and letting it work through the setup itself.\nThis post has two parts: the first covers installing and using it; the second — riding the current hype — has it actually analyze NVDA (NVIDIA) to see whether it looks over- or under-valued. The full report is below.\nPart 1: installing and using the plugin # I used the Cowork path to run Claude Finance Analysis. Setup was simple: open the Claude interface, click Customize in the left navigation, find the Finance Analysis plugin/tool in the Customize panel, follow the prompts to install and configure it, and it\u0026rsquo;s ready to call directly in a conversation once installed.\nOne tip: if you get stuck during setup, go straight to the GitHub repo for the official docs — they\u0026rsquo;re much clearer than Claude\u0026rsquo;s own description. Handing the doc link to Claude and letting it read through the doc itself works better than trying to muddle through on your own.\nPart 2: demo report — NVIDIA (NVDA), a deep dive and investment framework # Disclaimer: this piece is based on comparable-company financial data and is for research reference only — it is not investment advice. Investing carries risk; consult a professional financial advisor before making decisions. Data as of February 26, 2026, sourced from each company\u0026rsquo;s most recent annual filings and public disclosures.\nExecutive summary. NVIDIA (NASDAQ: NVDA), on the strength of its dominant position in AI-accelerated computing, has transformed from a traditional GPU chip company into the world\u0026rsquo;s most important compute-infrastructure provider. Full-year FY2026 revenue reached $215.9 billion, up 65% year over year, with an EBITDA margin of 70.2% — a combination essentially unmatched among comparable global semiconductor companies, and the core support behind its roughly $4.69 trillion market cap.\nLooking at comparable companies, NVIDIA\u0026rsquo;s valuation premium is significant but not unfounded: its EV/revenue multiple of roughly 21.7x is well above the industry median of about 10x, but its 65% growth rate is likewise far above the peer median of roughly 19%. The real question isn\u0026rsquo;t whether NVIDIA is expensive — it\u0026rsquo;s how long that growth rate can hold.\nBusiness overview. NVIDIA\u0026rsquo;s focus has shifted entirely from consumer GPUs to enterprise AI infrastructure centered on the data center. Its revenue mix breaks down roughly as: data center (85%+ of revenue) — H100, H200, and Blackwell-series GPUs covering both training and inference; gaming (~10%) — the GeForce line, with a stable market share but no longer a growth driver; and professional visualization plus automotive (~5%) — the DRIVE platform gradually ramping up. NVIDIA\u0026rsquo;s core logic is no longer \u0026ldquo;selling chips\u0026rdquo; — it\u0026rsquo;s building and controlling an AI computing platform ecosystem (the CUDA ecosystem, NVLink interconnects, InfiniBand networking), forming a highly sticky technical moat.\nRevenue scale and growth, by company (fiscal-year revenue in millions):\nCompany FY revenue YoY growth Notes NVIDIA (NVDA) $215,900 +65% FY2026, through Jan 2026 AMD $34,600 +34% FY2025, through Dec 2025 Broadcom (AVGO) $64,000 +24% FY2025, through Oct 2025 Qualcomm (QCOM) $44,284 +14% FY2025, through Sep 2025 Marvell (MRVL) $5,767 +25% FY2025, through Feb 2025 Intel (INTC) $52,900 0% FY2025, through Dec 2025 NVIDIA leads at 65% growth, almost double second-place AMD\u0026rsquo;s 34% — and maintaining that rate on top of an already massive $215.9 billion revenue base is genuinely rare. Intel\u0026rsquo;s flat revenue, by contrast, is a clear picture of the structural challenge traditional CPU architectures face in the AI era.\nProfitability comparison:\nCompany Gross margin EBITDA margin Free cash flow NVIDIA (NVDA) 71.1% 70.2% ~$45,000M Broadcom (AVGO) 78.3% 67.2% ~$22,000M Qualcomm (QCOM) 55.4% 31.2% ~$12,000M AMD 50.0% 19.4% ~$2,800M Marvell (MRVL) 41.3% 8.8% ~$400M Intel (INTC) 36.7% 5.5% -$1,500M NVIDIA and Broadcom have similar gross margins (Broadcom\u0026rsquo;s is slightly higher), but NVIDIA\u0026rsquo;s revenue is 3.4x Broadcom\u0026rsquo;s and its growth rate is nearly 3x. Broadcom\u0026rsquo;s high margin comes largely from post-acquisition cost optimization, while NVIDIA\u0026rsquo;s comes from pricing power — H100/H200 GPUs were in sustained short supply for a long stretch, letting NVIDIA essentially set its own prices. AMD\u0026rsquo;s profitability still trails NVIDIA by a wide margin, with an EBITDA margin (19.4%) less than a third of NVIDIA\u0026rsquo;s, reflecting the cost pressure AMD faces competing for AI GPU market share. Intel\u0026rsquo;s 5.5% EBITDA margin reveals a company deep in a difficult transition, with losses in manufacturing eating into the profit of its downstream chip design business.\nIndustry percentile summary (comparable metrics only):\nStatistic Revenue growth Gross margin EBITDA margin Max 65.0% 78.3% 70.2% 75th percentile ~30.5% ~67.8% ~55.0% Median ~24.5% ~52.7% ~25.3% 25th percentile ~12.5% ~42.7% ~12.1% Min 0.0% 36.7% 5.5% NVIDIA leads on both growth and EBITDA margin; Broadcom edges it slightly on gross margin (78.3% vs. 71.1%), but its overall earnings quality falls short of NVIDIA\u0026rsquo;s.\nValuation multiples:\nCompany Market cap EV EV/Revenue EV/EBITDA P/E NVIDIA (NVDA) $4,690B $4,689B 21.7x 30.9x 47.4x Broadcom (AVGO) $1,583B $1,632B 25.5x 38.0x 69.4x AMD $349B $347B 10.0x 51.8x 79.7x Qualcomm (QCOM) $150B $153B 3.4x 11.1x 28.7x Marvell (MRVL) $68B $70B 12.1x n/m 28.4x Intel (INTC) $229B $261B 4.9x n/m n/m Industry median — — ~10.1x ~34.5x ~47.4x Reading the valuation. Is NVIDIA\u0026rsquo;s valuation reasonable? Three angles: on EV/revenue (21.7x), it\u0026rsquo;s about 2.1x the industry median (10.1x) — but on a growth-adjusted basis (EV/revenue ÷ growth rate), NVIDIA comes out to roughly 0.33x, versus Broadcom\u0026rsquo;s 1.06x, AMD\u0026rsquo;s 0.29x, and Qualcomm\u0026rsquo;s 0.25x. That suggests NVIDIA isn\u0026rsquo;t actually the most expensive name once growth is accounted for — its premium reflects scale leadership and platform-monopoly value more than pure hype. On EV/EBITDA (30.9x), it\u0026rsquo;s actually below the effective peer median (~34.5x) — counterintuitively, given it\u0026rsquo;s the fastest-growing, highest-margin company in the group, but that\u0026rsquo;s because NVIDIA has entered a stage of scaled profitability, with absolute EBITDA ($151.5B) far exceeding peers, shrinking the multiple as the denominator grows. On P/E (47.4x), it\u0026rsquo;s essentially in line with the industry median (~47.4x); given NVIDIA\u0026rsquo;s growth rate, that P/E implies a PEG ratio of about 0.73 (47.4 ÷ 65) — below 1 generally suggests the valuation is reasonable, or even cheap, relative to growth.\nTaken together: NVIDIA\u0026rsquo;s valuation premium has fundamental support, but it already reflects fairly high growth expectations — where the stock goes from here depends heavily on whether growth can hold in the 40–50%+ range.\nCore competitive advantages. The CUDA ecosystem\u0026rsquo;s moat runs deep — nearly 20 years of accumulated tooling, with more than 4 million developers worldwide building AI/HPC work on CUDA, and switching costs are steep: neither AMD\u0026rsquo;s ROCm nor Intel\u0026rsquo;s oneAPI comes close to CUDA\u0026rsquo;s maturity, and switching platforms means not just recompiling code but abandoning years of accumulated optimization experience and engineering know-how — stickiness that\u0026rsquo;s especially pronounced in AI training. NVIDIA also sells whole systems, not just chips — DGX systems and the HGX platform integrate GPUs, ultra-fast NVLink interconnects, and InfiniBand networking into a system-level moat, so customers are buying a full AI infrastructure stack rather than a single component, which lifts the competition from chip-vs-chip to ecosystem-vs-ecosystem. Its relationship with TSMC is another core edge — by locking down TSMC\u0026rsquo;s advanced-node capacity (CoWoS packaging, 3nm/4nm), NVIDIA has effectively engineered a supply constraint during the AI compute boom, sustaining an unusually high gross margin (71%), not unlike how Apple\u0026rsquo;s control over iPhone components converts a supply-chain advantage directly into margin. And NVIDIA is gradually shifting toward software revenue through products like NVIDIA AI Enterprise (subscription software) and DGX Cloud — software revenue tends to carry higher renewal rates and margins, and if that shift succeeds, NVIDIA\u0026rsquo;s valuation story could evolve from \u0026ldquo;semiconductor cyclical\u0026rdquo; to \u0026ldquo;AI platform company,\u0026rdquo; commanding an even higher premium.\nKey risks. Competition is intensifying — AMD\u0026rsquo;s MI300 series has broken through in some inference workloads, and hyperscalers like Google (TPU v5), Meta (MTIA), and Amazon (Trainium) are accelerating their own in-house AI chip efforts to reduce dependence on NVIDIA; as those chips mature, NVIDIA\u0026rsquo;s data-center growth could slow. Demand sustainability is a real question, too — current AI compute demand is significantly driven by an \u0026ldquo;AI arms race\u0026rdquo; mentality, with major tech companies buying aggressively to avoid falling behind; if large-model ROI comes under scrutiny, or training efficiency improves sharply (algorithmic breakthroughs, like those from DeepSeek-style models, lowering the compute needed), capex could see a cyclical pullback that hits NVIDIA\u0026rsquo;s orders in the short term. Geopolitics and export controls are a high risk — continued tightening of US semiconductor export controls to China has already barred high-end GPUs like the A100 and H100 from export there; NVIDIA developed reduced-spec versions (like the H20) in response, but China revenue is now structurally capped, and further tightening could affect a China business that represents roughly 15–20% of NVIDIA\u0026rsquo;s global share. Valuation compression is a risk in its own right — the current 47x P/E already bakes in strong market expectations for the next 3–5 years, and if macro rates rise or risk appetite falls, high-multiple growth names get hit first, even with fundamentals intact. And customer concentration is a factor — NVIDIA\u0026rsquo;s top five customers (Microsoft, Google, Meta, Amazon, Oracle, and other hyperscalers) drive most of its data-center revenue, and that concentration risk could grow more visible as those customers\u0026rsquo; in-house chip strategies mature.\nAn investment framework (restating: this is an analytical framework based on public data, not investment advice or a price prediction — factor in your own risk tolerance and consult a licensed professional before deciding).\nThe core bull case: AI infrastructure buildout is the most certain direction of this tech cycle, and NVIDIA is the least replaceable supplier on that path\u0026rsquo;s supply side — whichever AI company ultimately \u0026ldquo;wins,\u0026rdquo; training and inference will remain heavily dependent on NVIDIA GPUs. It\u0026rsquo;s a similar logic to Cisco supplying internet-era network infrastructure, except NVIDIA\u0026rsquo;s technical moat runs deeper (the CUDA ecosystem) and its margins are considerably higher (70% EBITDA versus Cisco\u0026rsquo;s historical peak of around 40%).\nStrategy options for different risk appetites: a long-term core position (for investors with a 3+ year horizon and higher risk tolerance) bets on the long-run growth of AI compute demand and tolerates short-term volatility, watching data-center revenue growth, gross-margin trends, H-series/Blackwell shipment volumes, and CUDA developer counts, with a re-evaluation trigger if data-center revenue growth falls below 25% for two consecutive quarters; dollar-cost averaging (for investors managing timing risk) takes advantage of volatility to build a position gradually rather than all at once — NVIDIA has seen single-quarter drawdowns of 30–40% before (as in 2022), and any setback to the AI narrative could offer a better entry point; and a relative-value approach (for more sophisticated investors) who like the AI compute thesis but are wary of NVIDIA\u0026rsquo;s specific valuation might hedge with peers offering more moderate valuations and similar upside — Broadcom\u0026rsquo;s EV/EBITDA (38x) is higher than NVIDIA\u0026rsquo;s (31x) but its growth (24%) is far lower, and Qualcomm\u0026rsquo;s P/E (28.7x) is only 61% of NVIDIA\u0026rsquo;s, with edge-AI compute still to be unlocked — or go long the AI supply chain itself (TSMC, SK Hynix\u0026rsquo;s HBM business) to benefit from compute demand growth while avoiding single-company concentration risk.\nKey catalysts to watch: Blackwell Ultra\u0026rsquo;s shipment ramp (positive, validates the next product cycle) in H1 2026; hyperscaler capex guidance (positive or negative, sets near-term order visibility) each earnings season; US-China export-control developments (negative risk, could weigh on valuation) on an ongoing basis; AMD\u0026rsquo;s MI400 series launch (negative, a real competitive stress test) in 2026–2027; NVIDIA\u0026rsquo;s software/subscription revenue mix (positive, could elevate the valuation narrative) each quarter; and macro rate policy (negative risk, higher rates pressure high-multiple growth names) around Fed meetings.\nA valuation sensitivity sketch: target price ≈ next-12-months EPS × a reasonable P/E multiple. In an optimistic scenario (growth holding above 50%), EPS keeps growing fast and P/E could hold in the 50–60x range, implying meaningful further upside. In a base case (growth slowing to 30–40%), EPS growth moderates and P/E compresses to 35–45x, implying modest upside. In a pessimistic scenario (growth falling to 15–25%), EPS growth converges toward the industry average and P/E compresses to 20–30x, implying meaningful downside.\nBottom line: whether NVIDIA\u0026rsquo;s investment case holds up ultimately comes down to one judgment call — whether AI compute demand is durable, and whether NVIDIA can hold onto its platform-dominant position over the next 3–5 years. If the answer is yes, the current valuation is reasonable; if AI investment enters a return-on-investment reckoning and growth slows meaningfully, valuation-compression risk shouldn\u0026rsquo;t be ignored.\nPeer comparison summary table:\nMetric NVDA AMD AVGO INTC QCOM MRVL Median Revenue ($100M) 2,159 346 640 529 443 58 — YoY growth 65% 34% 24% 0% 14% 25% 24.5% Gross margin 71.1% 50.0% 78.3% 36.7% 55.4% 41.3% 52.7% EBITDA margin 70.2% 19.4% 67.2% 5.5% 31.2% 8.8% 25.3% EV/Revenue 21.7x 10.0x 25.5x 4.9x 3.4x 12.1x 10.1x EV/EBITDA 30.9x 51.8x 38.0x n/m 11.1x n/m 34.5x P/E 47.4x 79.7x 69.4x n/m 28.7x 28.4x 47.4x n/m = not meaningful; data as of February 26, 2026. Based on a comparable-company (comps) framework, citing NVIDIA\u0026rsquo;s FY2026 annual results (disclosed February 25, 2026) and peer public filings. For reference only — not a buy or sell recommendation.\nMy own quick take # I don\u0026rsquo;t really read financial statements myself, so I can\u0026rsquo;t personally judge how reliable this generated report actually is — but I did make sure it was working from the most recent filings available, and the conclusions it reached are ones I could actually follow and understand.\n","date":"2026-02-26","externalUrl":null,"permalink":"/posts/claude-finance-analysis-nvda-demo/","section":"Blog","summary":"Setting up Claude’s Finance Analysis tool through Cowork, then having it produce a full comps-based valuation report on NVIDIA against AMD, Broadcom, Qualcomm, Marvell, and Intel.","title":"Trying Out Claude's Finance Analysis Feature on NVDA","type":"posts"},{"content":"","date":"2026-02-26","externalUrl":null,"permalink":"/tags/hardware/","section":"Tags","summary":"","title":"Hardware","type":"tags"},{"content":" 1. What is Blackwell? # Blackwell is NVIDIA\u0026rsquo;s next-generation GPU architecture, formally announced at GTC in March 2024, named after the American mathematician and statistician David Blackwell — the first Black scholar admitted to the National Academy of Sciences, and a pioneer in game theory, probability, information theory, and statistics, all of which happen to be the mathematical bedrock of today\u0026rsquo;s generative AI.\nBlackwell succeeds the Hopper architecture (H100/H200) and is built to support training and real-time inference for trillion-parameter models. Put simply: if Hopper was the engine that made large AI models runnable at all, Blackwell is the next-generation engine that makes them run faster, cheaper, and bigger.\n2. The core technical breakthroughs # Dual-die packaging: getting past a physical limit. Chip manufacturing runs into a \u0026ldquo;reticle limit\u0026rdquo; — the largest area a lithography machine can etch in a single pass. Hopper\u0026rsquo;s GH100 die was already close to that limit (814mm²). Blackwell\u0026rsquo;s answer: fuse two full-size GB100 dies together over a 10 TB/s NV-HBI interconnect, presenting them to the outside world as a single unified GPU. That gives a single Blackwell GPU 208 billion transistors in total — about 2.6x Hopper\u0026rsquo;s 80 billion — built on TSMC\u0026rsquo;s custom 4NP process, with the two dies mounted on the same silicon interposer via CoWoS-L 2.5D packaging.\nSecond-generation Transformer Engine. Purpose-built for large language models and mixture-of-experts models, with key innovations including support for FP4 (4-bit floating point) inference precision — roughly 1.8x less memory use than FP8, while staying close to FP8 accuracy, meaning the same amount of memory can now hold a bigger model; micro-tensor scaling for finer-grained dynamic-range management that keeps output quality high even at low precision; and hardware-accelerated softmax computation in the attention layer, meaningfully boosting inference throughput.\nFifth-generation NVLink. AI training and inference often need hundreds or thousands of GPUs working together, and the bandwidth between them directly determines system efficiency. Blackwell\u0026rsquo;s fifth-generation NVLink delivers 1.8 TB/s of bidirectional bandwidth per GPU — about 2x Hopper\u0026rsquo;s NVLink — and supports direct interconnection across up to 576 GPUs, running at full speed without needing to fall back to an external network. That matters enormously for training trillion-parameter MoE models, which need to exchange activation data between GPUs constantly.\nOther key features: a dedicated RAS engine (reliability/availability/serviceability) that uses AI-based predictive maintenance to catch potential failures early, keeping large clusters running for weeks without interruption; confidential computing — the first GPU in the industry to support TEE-I/O, protecting sensitive data and models without sacrificing performance; and a hardware decompression engine supporting formats like LZ4, Snappy, and Deflate, speeding up database queries and data analysis.\n3. The performance leap: Blackwell vs. Hopper # NVIDIA\u0026rsquo;s officially published headline numbers:\nMetric Hopper (H100) Blackwell (B200) Transistor count 80 billion 208 billion (2.6x) Inference performance Baseline ~30x Energy efficiency Baseline ~25x NVLink bandwidth 900 GB/s 1.8 TB/s (2x) NVLink GPU interconnect Up to 256 Up to 576 Low-precision support FP8 FP4 An analogy: if Hopper is a high-performance sports car, Blackwell is something with a sports car\u0026rsquo;s speed, a truck\u0026rsquo;s cargo capacity, and a hybrid\u0026rsquo;s fuel efficiency all at once.\n4. The product lineup # Data center: the B200 is a single accelerator card for HGX servers; the GB200 pairs a Grace CPU with two Blackwell GPUs as a \u0026ldquo;superchip\u0026rdquo;; the GB200 NVL72 is a full-rack solution with 36 Grace CPUs and 72 Blackwell GPUs, aimed at frontier labs like xAI and OpenAI; and GB300 (Ultra) is the evolved Blackwell Ultra variant supporting NVFP4, aimed at the next generation of hyperscale clusters.\nConsumer: the Blackwell architecture also extends to the GeForce RTX 50 series, including the RTX 5090 and RTX 5080, built on TSMC\u0026rsquo;s 4N process (rather than the data-center 4NP), aimed at gaming and content creation, with AI-enhanced features like DLSS 4 multi-frame generation.\nEnterprise servers: the RTX PRO 6000 Blackwell Server Edition targets enterprise customers, offering up to 6x the inference performance of the previous-generation L40S. Server vendors including Cisco, Dell, HPE, Lenovo, and Supermicro already have products based on it.\n5. Who\u0026rsquo;s actually using Blackwell? # Adoption spans nearly every key player in the AI supply chain: cloud providers (AWS, Google Cloud, Microsoft Azure, Oracle) offering Blackwell compute to their customers; frontier model labs (OpenAI, Meta, xAI) training next-generation foundation models on it; tech giants like Tesla (autonomous driving) and Meta (recommendation systems); and enterprise customers deploying AI agents, data analysis, and scientific simulation workloads through RTX PRO servers. Reportedly, roughly 1,000 racks of Blackwell systems are being produced per week, and demand still outstrips supply.\n6. Why Blackwell matters # For the AI industry: Blackwell makes training and deploying trillion-parameter models practical. Its FP4 inference capability sharply cuts the cost per token of inference, which matters enormously for bringing AI services to a much broader user base — especially in the \u0026ldquo;AI agent\u0026rdquo; era, where explosive growth in inference workloads has made efficient inference silicon more important than ever.\nFor investors: Blackwell\u0026rsquo;s production ramp and demand trajectory are core variables driving NVIDIA\u0026rsquo;s (NVDA) and TSMC\u0026rsquo;s (TSM) results, and its manufacturing involves a genuinely complex supply chain — TSMC\u0026rsquo;s 4NP fabrication, HBM3E memory from SK Hynix and Micron, CoWoS packaging capacity — all worth watching.\nFor energy and infrastructure: a 30x gain in energy efficiency meaningfully lowers the power needed for a given amount of compute in an AI data center. But since total compute demand keeps growing exponentially, data-center power and liquid-cooling infrastructure remain an important investment theme regardless.\n7. After Blackwell: NVIDIA\u0026rsquo;s roadmap # NVIDIA has committed to roughly a one-architecture-per-year cadence:\nYear Architecture Notes 2022 Hopper H100/H200, kicked off the AI era 2024 Blackwell B200/GB200, the subject of this post 2025 Blackwell Ultra GB300, introduces NVFP4 precision 2026 Vera Rubin Next-gen architecture, expected on a more advanced process That fast a cadence means each generation\u0026rsquo;s lifecycle is shorter, but it also gives investors a more predictable rhythm of growth to track.\n8. Wrap-up # Blackwell isn\u0026rsquo;t just a faster chip — it\u0026rsquo;s the foundation of NVIDIA\u0026rsquo;s \u0026ldquo;AI factory\u0026rdquo; strategy. By pushing past a physical manufacturing limit with dual-die packaging, cutting cost with FP4 inference, and enabling unprecedented scale with fifth-generation NVLink, it redefines the performance ceiling for AI computing.\nFor anyone following tech and semiconductors, understanding Blackwell\u0026rsquo;s architecture and product lineup is key to reading where NVIDIA\u0026rsquo;s ecosystem goes next.\n","date":"2026-02-26","externalUrl":null,"permalink":"/posts/nvidia-blackwell-architecture/","section":"Blog","summary":"Two full-reticle dies fused into one GPU, FP4 inference, and a fifth-generation NVLink that connects up to 576 GPUs — the architecture behind NVIDIA’s ‘AI factory’ strategy.","title":"NVIDIA's Blackwell Architecture, Explained","type":"posts"},{"content":"","date":"2026-02-26","externalUrl":null,"permalink":"/tags/automation/","section":"Tags","summary":"","title":"Automation","type":"tags"},{"content":"If you\u0026rsquo;re running OpenClaw, macOS is genuinely the better ecosystem to build it on.\nI\u0026rsquo;d originally installed OpenClaw on a VPS — it ran, but the experience was never quite right. After redeploying it on a hackintosh instead, the whole experience improved noticeably. macOS\u0026rsquo;s native ecosystem advantages really show here: system-level automation, the way apps talk to each other, and compatibility with various CLI tools are all a lot smoother than on a plain Linux VPS.\nMore importantly, I got NotebookLM\u0026rsquo;s CLI feature installed on the hackintosh. It\u0026rsquo;s a genuinely convenient, free way to tap into Gemini\u0026rsquo;s Notebook capability and turn text directly into multimedia output. With that piece added, my whole content-publishing workflow finally came together end to end.\nThe full workflow # Step 1: organize and archive. I just hand OpenClaw my scattered thoughts and a rough outline, and it turns them into a well-structured document, cleaning up grammar and phrasing along the way. Once that\u0026rsquo;s done, it automatically saves the result into a designated Obsidian folder as a long-term journal entry or article. The real value here is that I don\u0026rsquo;t have to fuss over formatting or wording — I just get my thinking straight, and OpenClaw handles the rest.\nStep 2: automatic publishing. Once archived, publishing runs fully automatically, along two parallel paths. On the blog side, the pipeline from Obsidian to the blog is already wired up — once a post lands in Obsidian, it auto-syncs and publishes to the blog. On the WeChat side, an RSS subscription pushes the blog content automatically into the WeChat Official Account — write once, distribute everywhere. Neither path needs any manual step; it publishes the moment it\u0026rsquo;s written.\nStep 3: video generation. This is the capability NotebookLM added, and the part of the workflow I\u0026rsquo;m most excited about. NotebookLM does two things with the blog content: it summarizes the article into a conversational audio explainer, and it generates a slide deck automatically based on the content. Open WebUI then merges the slides and the audio into a finished narrated video.\nIn other words, going from a text blog post to a video with visuals and narration is now almost entirely automatic.\nCurrent shortcomings # This pipeline isn\u0026rsquo;t perfect yet. The main issue is how well the narration lines up with the slides — the audio might be discussing one topic while the deck hasn\u0026rsquo;t advanced to the matching slide yet; the timeline alignment is still fairly rough. But as a usable first version, it already covers day-to-day needs.\nWrap-up # Looking at the whole pipeline: idea → organized by OpenClaw → archived in Obsidian → auto-published to the blog → synced to WeChat → turned into video by NotebookLM. From input to multi-platform output, there\u0026rsquo;s barely any manual step in between. The macOS ecosystem is what really holds it together here, making the handoffs between tools feel natural and efficient.\nIf you\u0026rsquo;re building something similar for your own content pipeline, I\u0026rsquo;d genuinely recommend defaulting to macOS — having a complete, coherent ecosystem really does double your efficiency.\n","date":"2026-02-26","externalUrl":null,"permalink":"/posts/openclaw-macos-idea-to-video-pipeline/","section":"Blog","summary":"Moving OpenClaw off a Linux VPS and onto a hackintosh, then adding NotebookLM’s CLI to turn a finished blog post into a narrated slide video automatically.","title":"Idea to Blog, WeChat, and Short Video in One Shot With OpenClaw + macOS","type":"posts"},{"content":"","date":"2026-02-26","externalUrl":null,"permalink":"/tags/macos/","section":"Tags","summary":"","title":"MacOS","type":"tags"},{"content":"","date":"2026-02-25","externalUrl":null,"permalink":"/tags/economics/","section":"Tags","summary":"","title":"Economics","type":"tags"},{"content":"Some thoughts after reading Citrini Research\u0026rsquo;s piece \u0026ldquo;The 2028 Global Intelligence Crisis.\u0026rdquo;\nCitrini Research recently published a piece that went viral across the investing world. Framed as a \u0026ldquo;macro memo from June 2028,\u0026rdquo; it imagines a doomsday scenario where large-scale AI displacement of white-collar labor triggers an economic collapse — the S\u0026amp;P 500 down 38% from its peak, unemployment spiking to 10.2%, consumer spending falling off a cliff, and a wave of mortgage defaults. The piece was influential enough that it\u0026rsquo;s been pointed to as one of the catalysts behind the broad tech-stock selloff on February 24 — Michael Burry reposted it on X with the comment \u0026ldquo;Still think I\u0026rsquo;m just bearish?\u0026rdquo;, and that day IBM dropped nearly 13%, with DoorDash, American Express, and KKR all down more than 8%.\nI read the whole thing carefully. It\u0026rsquo;s genuinely well written, with a tight chain of logic: AI raises productivity → companies cut headcount → incomes fall → spending contracts → companies lean on AI even harder to cut costs further → a self-reinforcing \u0026ldquo;intelligence substitution spiral\u0026rdquo; → eventually a systemic financial crisis. The piece calls this \u0026ldquo;phantom GDP\u0026rdquo; — output that shows up in the national accounts but no longer flows through the real economy.\nClever as the argument is, I don\u0026rsquo;t share its conclusion.\n1. Technological revolutions have never shrunk the economy — they\u0026rsquo;ve always expanded it # Citrini\u0026rsquo;s core assumption is that once productive capacity grows far faster than income and demand can keep up, the economic structure loses its stability. That sounds reasonable, but history keeps proving the opposite.\nThe Industrial Revolution replaced manual labor — by the same logic, textile workers, blacksmiths, and coachmen all lost their jobs, so consumption should have collapsed. What actually happened: collapsing production costs created entirely new consumer markets, and new factories, new jobs, and new industries kept appearing one after another, and humanity\u0026rsquo;s material abundance took a genuine leap forward.\nThe computing and internet revolution replaced typists, mail carriers, telephone operators, bank tellers — but it gave rise to the entire internet industry, e-commerce, social media, and the mobile app economy. Behind every wave of \u0026ldquo;replacement\u0026rdquo; was a wave of \u0026ldquo;creation\u0026rdquo; ten or a hundred times its size.\nThe key economic logic: technological progress isn\u0026rsquo;t a zero-sum game — it expands the total size of the economy. Higher productivity makes goods and services cheaper, freeing up purchasing power that flows into new categories of consumption, creating demand that simply didn\u0026rsquo;t exist before. Over the past 200 years, every time someone has predicted \u0026ldquo;machines will put humanity out of work and collapse the economy,\u0026rdquo; the actual result has been an economy that grew multiple times over, with total employment rising rather than falling.\nCitrini\u0026rsquo;s argument makes a classic mistake: mistaking the friction of a transition period for the final outcome. AI adoption will genuinely cause structural unemployment in parts of the economy in the short term, and white-collar work will be hit first and hardest. But that\u0026rsquo;s a transitional phase, not a permanent state. When AI pushes the cost of writing code, building spreadsheets, or drafting legal documents down toward zero, what actually happens isn\u0026rsquo;t \u0026ldquo;nobody makes money anymore\u0026rdquo; — it\u0026rsquo;s \u0026ldquo;far more people can now do far more things they couldn\u0026rsquo;t do before.\u0026rdquo;\nThat\u0026rsquo;s also why I\u0026rsquo;m currently holding a heavy position in AI stocks — not because I\u0026rsquo;m ignoring the risk, but because I believe we\u0026rsquo;re in the early stage of a technological shift on the same scale as the Industrial Revolution or the internet. Panic narratives come and go, but the underlying, fundamental boost technology gives to productivity doesn\u0026rsquo;t reverse.\n2. What AI brings isn\u0026rsquo;t equality — it\u0026rsquo;s an unprecedentedly fast reshuffling of class # If I\u0026rsquo;m skeptical of Citrini\u0026rsquo;s \u0026ldquo;economic crisis\u0026rdquo; thesis, I\u0026rsquo;m genuinely worried about the distribution problem AI is creating.\nI used to believe in AI as an equalizer. AI would give everyone access to a top-tier coding assistant, translator, writing partner, legal advisor — technology, education, and cognitive ability all becoming more equal across the board.\nI now think that was naive. Equalization only works if everyone\u0026rsquo;s using the same AI. In reality, some people are using Claude Opus 4.6 to write production-grade code, while others are stuck with a free model producing something half-finished — and the output gap between the two starts at 10x, easily.\nMy own experience is the clearest example. When I first started using AI tools, I\u0026rsquo;d hesitate over a $3 secondhand Cursor subscription. Now, I pay for rising monthly AI subscriptions without a second thought — not because I\u0026rsquo;ve gotten that much richer, but because I\u0026rsquo;ve genuinely internalized that the price of a token is the price of productivity.\nThe logic behind this is fairly harsh: someone with access to frontier models can produce a production-ready solution in one pass and create value a thousand times over; someone without the means to pay is stuck with free or cheap models, spends three hours producing something half-baked, and only ever creates one unit of value. What someone else finishes in minutes, you might spend 100 hours on and still not match in quality — and the practical problem is that a weaker model tends to make code messier with every edit rather than better. That\u0026rsquo;s not a difference in effort — it\u0026rsquo;s a difference in tooling generations, and it\u0026rsquo;s a rout.\nThe reality in 2026: model capability is diverging faster, and token prices are climbing faster. The gap between the best models and the worst isn\u0026rsquo;t linear — it\u0026rsquo;s exponential. Which means the productivity gap between people who can afford top-tier AI and people who can\u0026rsquo;t is going to widen at a pace we haven\u0026rsquo;t seen before.\n3. The world ahead: paradise for the wealthy, a much harder climb for everyone else # Zooming out: from industrialization to the information age to the intelligence age, society has gone through three major waves of labor displacement. Industrialization replaced physical labor — assembly lines replaced artisans. The information age replaced repetitive mental labor — ERP systems replaced bookkeepers, search engines replaced librarians. The intelligence age is now replacing what used to be considered untouchable — high-level mental labor: AI can write code, make diagnoses, produce investment strategies, even write research papers.\nEach wave redefined who could actually earn a living. This one is especially disruptive, because it\u0026rsquo;s aimed squarely at what used to be the most stable, highest-earning group in society: white-collar knowledge workers.\nCitrini\u0026rsquo;s concern isn\u0026rsquo;t baseless — AI genuinely will disrupt the white-collar job market in the short term. But the crisis narrative misses the other side of it: the people who master AI will have their ability to create value amplified enormously.\nTomorrow\u0026rsquo;s winners won\u0026rsquo;t be the people who hold some specific piece of expertise — because AI is commoditizing knowledge itself — but the people who know how to direct AI. An ordinary person who\u0026rsquo;s good with AI can out-produce ten experts who aren\u0026rsquo;t. That\u0026rsquo;s not an exaggeration; it\u0026rsquo;s already happening. But the flip side is just as real: people without AI skills will lose more than just an efficiency edge — they\u0026rsquo;ll lose access to more and more jobs outright. Once a company realizes one AI-literate employee can do the work of five, the other four positions simply stop existing.\nThat points toward a polarized world: people who can master AI and afford to pay for it will see their quality of life amplified like never before — better healthcare, better education, better investment returns, more creative leverage. People without AI skills, or without the means to pay for AI, will find it harder and harder just to get by — not only will job opportunities keep shrinking, but the barrier to catching up will keep rising too.\nClosing thought: the real risk isn\u0026rsquo;t that AI is too powerful — it\u0026rsquo;s that you haven\u0026rsquo;t kept up # Citrini\u0026rsquo;s piece paints a doomsday picture of AI collapsing the economy. I think the more realistic picture is different: the economy won\u0026rsquo;t collapse, but how wealth gets distributed is going to be completely rewritten.\nAI won\u0026rsquo;t make everyone poorer, but it will make some people extraordinarily wealthy while leaving others far behind. This isn\u0026rsquo;t an economic crisis — it\u0026rsquo;s a reshuffling of class, and it\u0026rsquo;s happening faster than any before it.\nAs someone living through this shift firsthand, my advice is simple: embrace AI, get in early, and don\u0026rsquo;t wait. Looking back, you may have missed the wave of economic reform, missed Bitcoin when nobody wanted it early on, missed the early growth of the internet — not acting now means missing AI\u0026rsquo;s own founding year.\n","date":"2026-02-25","externalUrl":null,"permalink":"/posts/ai-era-wealth-redistribution/","section":"Blog","summary":"Reacting to Citrini Research’s viral 2028 doom scenario: technological revolutions have never shrunk the economy, but the gap between people who can afford frontier AI and those who can’t is opening faster than any previous divide.","title":"The Real Variable in the AI Era Isn't a Recession — It's Wealth Redistribution","type":"posts"},{"content":"Over the past couple of days I finished wiring up a full pipeline: write the framework and core ideas for a post in OpenClaw → OpenClaw\u0026rsquo;s model polishes it into a finished article → it auto-publishes to the blog → RSS carries it over → it auto-syncs into my WeChat Official Account drafts.\nNo more copy-pasting, no more manual formatting, no more checking whether images broke. The whole thing, from editing to distribution, runs on its own. Here\u0026rsquo;s the full write-up.\n1. The starting point: editing the blog directly from OpenClaw # The core goal is simple: ideas show up whenever they show up, so jot down the core thought and hand it to OpenClaw — it reads your existing blog to understand your writing style, then turns the idea into a complete post. Writing becomes something you can do anytime, anywhere, with publishing handled automatically in the background.\nI already had a blog with RSS output; what I needed was to turn OpenClaw into the writing front-end, the blog into the content source, and let a script handle distribution automatically. The shape of it: OpenClaw → Blog (generates the article) → RSS → a VPS script → WeChat Official Account. The blog only has to do two things: generate the article correctly, and output RSS correctly. RSS is the bridge between the two halves.\n2. Building the \u0026ldquo;cloud data bridge\u0026rdquo;: a VPS plus Python # My home connection has a dynamic IP, which isn\u0026rsquo;t stable or suitable for a long-term integration with the WeChat API. So I rented a VPS with a fixed public IP (costs next to nothing) to handle three jobs: periodically read the blog\u0026rsquo;s RSS feed, process the article body, and call the WeChat API to push a draft.\nSetup on the VPS: install Python, create a virtual environment, and install the core libraries — feedparser for parsing RSS, requests for calling the WeChat API.\npip install feedparser requests 3. Handling WeChat\u0026rsquo;s API auth (automatic token management) # The WeChat API has two core constraints: the access token is only valid for 2 hours, and there\u0026rsquo;s a rate limit on calls. My fix was straightforward:\nIP whitelisting — add the VPS\u0026rsquo;s public IP to the whitelist in the WeChat Official Account backend; otherwise every API call fails outright.\nAutomatic token refresh — I wrote a small token-management function: request a fresh access token roughly every 90 minutes, cache it locally, and just read from the cache whenever a call is made. Roughly:\ndef get_access_token(): if token_not_expired: return cached_token else: request a new token from the WeChat API save it locally return the token That keeps it from calling too often, never fails from an expired token, and runs completely hands-off.\n4. The real pain point: WeChat\u0026rsquo;s image hotlink blocking # This was the most frustrating part of the whole pipeline. Just handing the RSS\u0026rsquo;s raw HTML straight to WeChat? Every image comes out broken. The reason is simple: WeChat won\u0026rsquo;t reference external image URLs directly — everything has to go through its own official media API.\n5. Routing images through as a \u0026ldquo;porter\u0026rdquo; script # The full process: first, extract every image from the article HTML by matching the image tags with a regex and pulling out every image URL. Then, download each image straight into memory, without ever writing it to disk. Next, upload each one to WeChat\u0026rsquo;s permanent-media API, which returns a new WeChat-hosted image URL (WeChat\u0026rsquo;s own CDN address). Finally, go back through the article body and replace every original image URL with the new WeChat one — at that point, the article body is a \u0026ldquo;WeChat-ready\u0026rdquo; version.\n6. Submitting to the Official Account drafts # The last step: a POST request to WeChat\u0026rsquo;s draft API, submitting the title, author, cover image, and the rewritten HTML body. If it were a verified service account, this could go straight to the broadcast API instead — but my approach is to push to drafts first and review manually before publishing, which keeps things safe and under control.\n7. What the finished pipeline looks like # The flow now: write in OpenClaw → it auto-publishes to the blog → RSS updates automatically → the scheduled VPS script picks up the new post → images get routed through → it auto-pushes to the WeChat Official Account drafts. All of it unattended.\n8. Why this actually matters # This wasn\u0026rsquo;t really about writing a script. It\u0026rsquo;s that once publishing content stops eating up a meaningful chunk of your time, the barrier to publishing effectively disappears — which makes it that much easier to actually write down whatever thought crosses your mind, whenever it happens.\n","date":"2026-02-23","externalUrl":null,"permalink":"/posts/openclaw-blog-wechat-automation/","section":"Blog","summary":"From a rough idea in OpenClaw to a published blog post to a WeChat Official Account draft — with a VPS script standing in as the bridge and quietly fixing WeChat’s image hotlink problem along the way.","title":"OpenClaw × Blog × WeChat: Wiring Up a Full Automation Pipeline","type":"posts"},{"content":"","date":"2026-02-23","externalUrl":null,"permalink":"/tags/wechat/","section":"Tags","summary":"","title":"WeChat","type":"tags"},{"content":"Today I finished setting up blogwatcher inside OpenClaw, adding a few information sources per Claude\u0026rsquo;s suggestion (AI, startups, and technical-community directions).\nNot long after it was configured, it pushed through a piece of English content. I didn\u0026rsquo;t click through to the original link, but based on the structure and tone, it\u0026rsquo;s almost certainly from Hacker News or the YC (Y Combinator) hiring/startup feed — the whole piece was a textbook example of a startup pitch plus a technical-founder job listing, leaning hard on AI agents, product ownership, and PMF — pure YC-house-style language.\nWhat blogwatcher is actually doing here became clear pretty quickly: it\u0026rsquo;s not a plain RSS reader — it\u0026rsquo;s an AI-driven filter for information. Not just \u0026ldquo;grab articles,\u0026rdquo; but source → AI filtering → recommendation → reading → into my knowledge stream. What I\u0026rsquo;m really building is a system for automatically surfacing high-value information.\nWhat was that job listing, anyway? # The article was a YC startup hiring for a role with an interesting title: \u0026ldquo;Ex Technical Founder.\u0026rdquo; In plain terms: they want someone who\u0026rsquo;s started a company before, can write code, and can build a product with AI. Not an algorithms role, not a regular engineering role — a new kind of position entirely.\nWhat\u0026rsquo;s the pay like? # The listing offered £90K–£180K a year, plus 0.10%–0.40% equity, based in London. Put in context: £90K is already senior-engineer territory, £180K sits at the high end for AI engineers, and that equity range is close to what a founding engineer gets. In other words, this isn\u0026rsquo;t a regular job — it\u0026rsquo;s closer to a salaried co-founder position.\nWhat kind of person are they actually looking for? # The key point: this role isn\u0026rsquo;t about building AI models — it\u0026rsquo;s about using AI to build systems and products. The core skills: building AI agents, designing workflows, using RAG and context systems to give a model something like memory, wiring AI into business decisions, and shipping products and validating them with real data.\nThat\u0026rsquo;s no longer \u0026ldquo;write code → ship a feature.\u0026rdquo; It\u0026rsquo;s \u0026ldquo;find the problem → build the system → solve it with AI → ship and validate → iterate.\u0026rdquo; The role, at its core, is an AI product engineer — an AI builder.\nA trend that keeps getting more obvious # The old core of software engineering was writing code and shipping features. It\u0026rsquo;s turning into designing AI workflows, designing agents, building automated systems, and turning AI into leverage. In other words: knowing how to build systems with AI is, on its own, already becoming a real job category — and probably the one in highest demand going forward. There aren\u0026rsquo;t many people doing pure model research; what\u0026rsquo;s genuinely in high demand is people who can turn AI into a shipped product.\nClosing thought # Today\u0026rsquo;s small discovery confirmed something for me: the skill that actually matters in the AI era isn\u0026rsquo;t \u0026ldquo;knowing how to use AI\u0026rdquo; — it\u0026rsquo;s \u0026ldquo;being able to build systems with AI.\u0026rdquo; That\u0026rsquo;s already become a job category, and the demand for it is growing fast.\n","date":"2026-02-12","externalUrl":null,"permalink":"/posts/configuring-blogwatcher/","section":"Blog","summary":"Watching an AI-curated information feed surface a YC ’ex technical founder’ listing made one thing click: the real skill now is building systems with AI, not just using it.","title":"Configuring Blogwatcher, for a Wider View","type":"posts"},{"content":"Today I got Clawdbot directly editing and generating Obsidian notes.\nCombined with the \u0026ldquo;Obsidian to blog\u0026rdquo; automation I\u0026rsquo;d already built, this now gives me a genuinely full pipeline around Obsidian: Clawdbot handles the speech recognition and AI-assisted polishing up front, generates the Obsidian note, and that in turn automatically triggers publishing to both the blog and a short video.\nSince this whole setup runs on macOS, Clawdbot can also call on local system capabilities directly, so it now handles Apple Notes and Google Sheets automation as well.\nOh, and the email pipeline is connected too now — receiving, handling, and replying, all hands-free.\n","date":"2026-02-12","externalUrl":null,"permalink":"/posts/clawdbot-obsidian-apple-notes-integration/","section":"Blog","summary":"Voice in, an Obsidian note out, a blog post and short video out the other end automatically — plus Apple Notes, Google Sheets, and an email pipeline running hands-free.","title":"Connecting Clawdbot to Obsidian, Apple Notes, and Everything Else","type":"posts"},{"content":"","date":"2026-02-11","externalUrl":null,"permalink":"/tags/productivity/","section":"Tags","summary":"","title":"Productivity","type":"tags"},{"content":"Finally pulled the trigger today and signed up for Claude Pro, mainly to get access to Opus 4.6 — its complex-reasoning and code-review ability is genuinely stronger. My overall workflow has settled into something clearer now: split the work between Codex and Claude Code.\nCodex handles the high-volume, batch code generation and implementation — the \u0026ldquo;get the feature written fast\u0026rdquo; part. Claude Code handles code review, logic auditing, and structural cleanup — the \u0026ldquo;catch the problems, protect the quality\u0026rdquo; part. One leans toward production, the other toward review — together they\u0026rsquo;re basically a small AI development pipeline.\nOn top of that, Gemini plugs into OpenClaw mainly through its API, handling tool calls and filling in extra capability. Day to day, I also lean on NotebookLM for organizing material and distilling knowledge, and use Nanobanana for drawing and visualization. Altogether it\u0026rsquo;s become a coding-review-knowledge-visualization workflow spread across several models working together.\n","date":"2026-02-11","externalUrl":null,"permalink":"/posts/three-models-working-for-me/","section":"Blog","summary":"Codex writes the code fast, Claude Code audits it, Gemini plugs into OpenClaw as a tool, NotebookLM organizes notes, and Nanobanana handles visuals — a small AI production line.","title":"Three Models, All Working for Me Now","type":"posts"},{"content":"","date":"2026-02-09","externalUrl":null,"permalink":"/tags/hackintosh/","section":"Tags","summary":"","title":"Hackintosh","type":"tags"},{"content":"Couldn\u0026rsquo;t resist the urge this weekend to deploy ClawdBot on macOS, so I dug out an old hackintosh build I\u0026rsquo;d put together years ago (a Mac mini 2018 with an i7-8700B). The install itself went smoothly, but since this needed to run as a long-lived server, I figured it needed to auto-restart after a power loss and auto-login after boot — so I made two changes: set macOS as the priority boot target, and enabled auto-login for the user account.\nThe second change turned out to be fatal — it left macOS completely unable to reach the desktop.\nChasing the fix with Gemini # Following a plan Gemini worked out with me, I started troubleshooting. The first step went smoothly: booting into Windows, mounting the EFI partition with ProperTree, and editing config.plist to disable secure boot verification, per its guidance.\nAfter restarting, it got into the macOS boot loader — I thought that had fixed it, until the progress bar got halfway and the machine went black and restarted, over and over.\nThe real challenge: an infinite reboot loop. To track down the cause, I added -v to the boot arguments to see the verbose log. I figured, like in the old days, I\u0026rsquo;d just read the log and find the culprit — reality wasn\u0026rsquo;t that kind. The scrolling code did catch a kernel panic, but the machine stayed stuck in an endless restart regardless.\nAI-guided \u0026ldquo;blind\u0026rdquo; troubleshooting: Gemini vs. ChatGPT. Worth mentioning — Gemini and ChatGPT gave completely different approaches to fixing this. Given how much I\u0026rsquo;d come to trust Gemini\u0026rsquo;s reasoning lately, I went with its plan.\nGemini\u0026rsquo;s approach was \u0026ldquo;minimal boot\u0026rdquo; — disabling every non-essential driver under Windows (graphics, audio, custom USB, etc.), keeping only the bare minimum needed to boot. After a few rounds of this, the boot process did get further — the earlier kernel panic was gone — but it still hung right before the graphics interface loaded (a WindowServer crash).\nBy this point it was past 2 a.m.\nThe last resort: reinstall while keeping my data. With several rounds of fixes producing nothing real, Gemini\u0026rsquo;s final suggestion was a reinstall. As a hackintosh user, the word \u0026ldquo;reinstall\u0026rdquo; doesn\u0026rsquo;t exactly inspire confidence — beyond worrying whether the boot files would get overwritten, driver compatibility alone is enough of a headache. But by this point in the night, the only goal left was getting it working so I could go to sleep.\nThat\u0026rsquo;s where I hit a small snag: Gemini told me to boot into Recovery mode and reinstall from there, but because I\u0026rsquo;d disabled the network driver earlier as part of \u0026ldquo;minimal boot,\u0026rdquo; Recovery mode had no network to actually download the OS with. A neat little dead end: no network → can\u0026rsquo;t download the OS → can\u0026rsquo;t boot the OS → can\u0026rsquo;t fix the driver.\nStill had a plan, though — I switched back to Windows and turned the wired network driver (RealtekRTL8111) back on by itself. Back into Recovery, and this time the ethernet icon in the top corner finally lit up.\nWith the fans humming in the case (or possibly just tinnitus from how quiet 2 a.m. gets), I clicked \u0026ldquo;Reinstall macOS.\u0026rdquo; Thankfully, that operation only refreshes the core system files — it doesn\u0026rsquo;t touch your data.\nRight before I fully passed out, a line of gold text finally scrolled across the screen, and that familiar login screen was back.\nA thought on all this: are technical forums dying in the age of large models? # A quick reflection on the whole ordeal. With large models backing you up, the old role technical forums used to play feels like it\u0026rsquo;s basically disappearing. Back in the day, troubleshooting a hackintosh meant: hit an error → search forums for the keywords → dig through years-old posts looking for a similar setup → try someone else\u0026rsquo;s EFI → fail, repeat. You had to do a lot of mentally exhausting matching between someone else\u0026rsquo;s fix and your own situation.\nNow the pattern is: hit an error → snap a photo, hand it to the model → get the next step generated directly.\nThrough the whole process, I never really understood the reasoning behind every line of code — I was more of an executor. The model was the brain, and I was just its hands. As long as it was pointed in the right direction, solving the problem was just a matter of time. That\u0026rsquo;s efficient, but it also leaves this nagging feeling that we\u0026rsquo;re drifting further away from being a \u0026ldquo;hardcore tinkerer\u0026rdquo; in the old sense.\nPostscript # The macOS version of OpenClaw is finally up and running. I\u0026rsquo;m now running two Claw-based assistants: one doing real-time monitoring as a formal, production-style task — basically my ops person — and the other handling development and testing. Between the two, they\u0026rsquo;re now helping carry my own thinking and decisions.\n","date":"2026-02-09","externalUrl":null,"permalink":"/posts/hackintosh-autologin-breakage-ai-fix/","section":"Blog","summary":"One innocent auto-login setting later, my Mac mini hackintosh was stuck in a boot loop — a night of Gemini-guided EFI edits, a networkless recovery dead end, and a reinstall that saved my data.","title":"Weekend Chaos: Auto-Login Broke My Hackintosh, and AI Talked Me Back In","type":"posts"},{"content":"","date":"2026-02-08","externalUrl":null,"permalink":"/tags/career/","section":"Tags","summary":"","title":"Career","type":"tags"},{"content":"My notes on some unusually candid remarks from Jensen Huang at a 2026 AI summit.\n1. The old moat is gone: coding was just typing # Said three years ago, this would have sounded unhinged. Said in 2026, it feels like Jensen Huang just said the quiet part out loud: \u0026ldquo;Programming? That\u0026rsquo;s just typing. And typing isn\u0026rsquo;t worth much anymore.\u0026rdquo;\nFor thirty years we were told: learn Python, learn Java, and you\u0026rsquo;d hold the key to the future. Huang is telling us that era is over. As AI code generation has improved exponentially, syntax stopped being a real barrier.\nDoes that mean it\u0026rsquo;s over for programmers? No — it\u0026rsquo;s more like a release.\n2. The rise of the domain expert: not knowing how to code is actually an advantage now # Huang\u0026rsquo;s logic is blunt but genuinely hopeful: once the technical barrier drops to zero, the business barrier becomes effectively infinite.\nIf AI can write a perfect function in a second, what\u0026rsquo;s actually valuable? It\u0026rsquo;s the person who knows what function needs writing. The doctor who understands the underlying biology. The manager who understands the tangled logic of a supply chain. The marketer who genuinely understands people and markets.\nTomorrow\u0026rsquo;s standout individual contributor isn\u0026rsquo;t the person heads-down writing code — it\u0026rsquo;s the domain expert who understands the business, understands the customer, and knows how to direct AI to do the work. Huang put it directly: a fresh computer science graduate with great coding chops is worth less than a seasoned salesperson who understands what actually bothers their customers — because AI can write the code, but it can\u0026rsquo;t read a customer\u0026rsquo;s mind.\n3. A required course for founders: if you want to control innovation, go see a therapist # When asked how a company should manage AI-driven innovation that feels \u0026ldquo;out of control\u0026rdquo; internally, Huang\u0026rsquo;s answer was memorable: \u0026ldquo;If you want to control innovation, you should go see a therapist.\u0026rdquo;\nIn the AI era, the old habit of running everything through KPIs and ROI calculations is poison for innovation. Huang\u0026rsquo;s management philosophy is closer to \u0026ldquo;let a hundred flowers bloom.\u0026rdquo; Don\u0026rsquo;t ask on day one how much money an AI project will make. Allow some chaos first, allow trial and error, let a swarm of small AI-driven projects emerge inside the company. The leader\u0026rsquo;s job isn\u0026rsquo;t to control that — it\u0026rsquo;s to act like a gardener, pruning after things grow wild, and keeping the ones that actually turn into something real.\nTrying to plan an AI revolution with a spreadsheet is an epitaph for the old way of doing things.\n4. Resetting the business model: from \u0026ldquo;tools\u0026rdquo; to \u0026ldquo;digital labor\u0026rdquo; # This was the most disruptive business insight of the whole conversation.\nHuang pointed out that for decades, tech companies sold tools — screwdrivers, hammers, software. Now, we\u0026rsquo;re entering the era of the \u0026ldquo;AI factory.\u0026rdquo; Even hardware giants like Cisco and NVIDIA are finding their customers aren\u0026rsquo;t really buying \u0026ldquo;faster networking\u0026rdquo; or \u0026ldquo;more compute\u0026rdquo; anymore — they\u0026rsquo;re buying digital labor.\nA self-driving car isn\u0026rsquo;t a car — it\u0026rsquo;s a digital driver. A smart customer-service system isn\u0026rsquo;t software — it\u0026rsquo;s a digital support agent.\nOnce you realize you\u0026rsquo;re manufacturing labor rather than tools, your addressable market jumps from the roughly one-trillion-dollar IT industry to the roughly hundred-trillion-dollar global real economy. That\u0026rsquo;s part of why he\u0026rsquo;s willing to say things like: Disney would rather be Netflix, and Mercedes would rather be Tesla.\n5. Your question is worth more than the answer # On the subject of data sovereignty, Huang offered a genuinely philosophical warning: \u0026ldquo;My question is my most valuable IP. The answer is cheap.\u0026rdquo;\nIn the generative-AI era, getting an answer is easy. The hard part is asking the right question. How you prompt AI, and the reasoning you use to guide it, reflects your actual strategic thinking — and that\u0026rsquo;s the real secret. That\u0026rsquo;s why companies need their own \u0026ldquo;sovereign AI,\u0026rdquo; rather than routing every core conversation through a public cloud — because the way you ask questions is itself a competitive advantage that can\u0026rsquo;t be copied.\nClosing thought: an uncomfortable truth, and the only real way through it # As the evening wound down, the conversation ended on a lighter note, but Huang\u0026rsquo;s closing line is worth remembering. In an era where AI compute is scaling a million times faster than Moore\u0026rsquo;s Law, anxiety doesn\u0026rsquo;t help. He repeated his now-famous line, and in 2026 it lands with real weight: \u0026ldquo;I swear to God, go apply this technology. You\u0026rsquo;re not going to lose your job to AI. You\u0026rsquo;re going to lose it to someone who knows how to use AI.\u0026rdquo;\nIn this wild AI era, don\u0026rsquo;t be the \u0026ldquo;typist\u0026rdquo; left behind. Be the domain expert who knows how to direct AI. There\u0026rsquo;s no better time to start than right now.\n","date":"2026-02-08","externalUrl":null,"permalink":"/posts/programming-is-just-typing/","section":"Blog","summary":"My notes on Jensen Huang’s unusually candid remarks at a 2026 AI summit: syntax stopped being the moat, and domain expertise became the whole game.","title":"Programming Is Just Typing — Your Experience Is the Real Asset","type":"posts"},{"content":"Summary: now that AI isn\u0026rsquo;t just a chatbot but an agent that can operate a computer on your behalf, who\u0026rsquo;s actually winning? The latest benchmark data shows Anthropic\u0026rsquo;s Opus 4.6 redefining the ceiling for \u0026ldquo;agent\u0026rdquo; capability, while OpenAI and Google continue to hold their own ground in their respective strengths.\nA closely watched benchmark chart has been making the rounds recently, comparing Opus 4.6, Opus 4.5, Sonnet 4.5, Gemini 3 Pro, and GPT-5.2 side by side. If these represent the shape of the next flagship generation, the data points to one core fact: large models are making a genuine leap from being chatbots to being agents.\nHere\u0026rsquo;s a deeper look at the numbers.\n1. Opus 4.6: the undisputed ruler of the agentic era # The most striking part of the table is how Opus 4.6 performs on agentic tasks. If you need an AI that can browse the web, operate software, and work through complex workflows the way a person would, Opus 4.6 currently looks like the only real answer.\nAutonomous computer use: on the OSWorld benchmark, Opus 4.6 scored 72.7% — other top models don\u0026rsquo;t even have comparable numbers here, hinting at a real edge in understanding UI and executing OS-level instructions.\nSearch and novel problem-solving: on Agentic Search (84.0%) and novel problem-solving (ARC-AGI-2, 68.8%), Opus 4.6 clearly outpaces GPT-5.2 (77.9% and 54.2% respectively).\nOffice tasks: Opus 4.6 leads the field on Office Tasks with a score of 1606.\nTakeaway: Opus 4.6 is clearly built to get things done. Its strong generalization (shown by the ARC-AGI score) lets it handle complex logic and interfaces it\u0026rsquo;s never seen before.\n2. GPT-5.2: still the unshakeable top student # GPT-5.2 falls a bit short on autonomous task execution, but it still defends OpenAI\u0026rsquo;s honor on pure knowledge and academic reasoning.\nGraduate-level reasoning (GPQA Diamond): GPT-5.2 tops the field at 93.2%, with Opus 4.6 (91.3%) settling for third. For deep scientific questions, logical derivation, and hardcore knowledge work, GPT-5.2 is still the strongest brain around.\nCoding: on agentic coding (SWE-bench Verified), GPT-5.2 (80.0%) and the Opus line (80.8%) are neck and neck — the gap is nearly negligible.\nTakeaway: if your work is academic research, paper writing, or passing a tough qualifying exam, GPT-5.2 is still the first choice — like a professor who\u0026rsquo;s spent a lifetime in the library: deeply knowledgeable, if a bit less nimble at hands-on tasks than someone younger.\n3. Gemini 3 Pro: a moat in vision and multilingual work # Gemini 3 Pro didn\u0026rsquo;t fall behind in this comparison — it built a solid moat in its own areas of strength.\nVisual reasoning: on MMMU Pro, Gemini 3 Pro scored the highest at 81.0% without any tool assistance, suggesting genuinely strong native visual understanding — it can read complex charts and images without leaning on an external code interpreter.\nMultilingual ability: on multilingual Q\u0026amp;A, Gemini 3 Pro took the top spot at 91.8%, making it the best option for anyone handling global business, translation, or less common languages.\n4. An interesting wrinkle: a cost to the upgrade? # Looking closer at the numbers turns up something counterintuitive: Opus 4.6 doesn\u0026rsquo;t beat the older Opus 4.5 across the board. On scaled tool use and agentic coding specifically, the older Opus 4.5 edges out 4.6 by a small margin.\nWhat does that suggest? Model training may be running into a trade-off between specialization and generalization. In chasing extreme general reasoning ability (the big jump on ARC-AGI) and human-like computer operation, Opus 4.6 may have given up a little ground on some narrow, pure-code-generation paths — but that\u0026rsquo;s usually part of the road toward more general intelligence.\nWrap-up: how should you actually pick? # Based on this forward-looking benchmark, the guidance is fairly clear:\nPick Opus 4.6 if you need to build automated workflows, RPA, or want AI that can autonomously browse the web and pull together complex information — it\u0026rsquo;s the model that behaves most like a human employee.\nPick GPT-5.2 if you\u0026rsquo;re focused on research, deep logical reasoning, or need an extremely rigorous knowledge base — it\u0026rsquo;s the strongest academic tutor.\nPick Gemini 3 Pro if your work involves heavy image analysis, video understanding, or cross-language international business — it\u0026rsquo;s the strongest at perception.\nThe AI landscape is fragmenting — the era of one model to rule everything may be ending, and whether you\u0026rsquo;re a developer or a regular user, picking the right model for the job is becoming the new normal.\n","date":"2026-02-08","externalUrl":null,"permalink":"/posts/ai-benchmark-opus-gpt-gemini/","section":"Blog","summary":"A benchmark chart making the rounds shows large models shifting from chatbots to agents — with Opus 4.6 dominating agentic computer use, GPT-5.2 still king of pure reasoning, and Gemini 3 Pro owning vision and multilingual work.","title":"AI Benchmarks: Opus 4.6 vs. GPT-5.2 vs. Gemini 3 Pro, Head to Head","type":"posts"},{"content":"","date":"2026-02-08","externalUrl":null,"permalink":"/tags/llm-comparison/","section":"Tags","summary":"","title":"LLM Comparison","type":"tags"},{"content":"NVIDIA CEO Jensen Huang\u0026rsquo;s recent comment — \u0026ldquo;writing software code is dead\u0026rdquo; — sent a shockwave through the tech world. A lot of people found it alarming, but honestly, I agree with it. That\u0026rsquo;s not to say we can throw our computers away — it means the barrier to the actual act of \u0026ldquo;writing code\u0026rdquo; is dropping toward zero. The real competitive edge has shifted from \u0026ldquo;knowing the syntax\u0026rdquo; to \u0026ldquo;being able to turn domain expertise into a product.\u0026rdquo;\nWhoever can use AI well enough to quickly turn the specialized knowledge in their own head into a real product is the one who wins in this new era.\nI\u0026rsquo;ve recently been building exactly that way — single-handedly putting together a complete platform plus a companion app. Here\u0026rsquo;s what that process actually taught me, especially around which AI tools to use and how I\u0026rsquo;ve rethought what \u0026ldquo;domain expertise\u0026rdquo; even means now.\nBuilding it: Codex for the frame, Cursor for the fine work # On this project, I built both the platform side and the mobile app on my own — work that used to take a small dev team several weeks.\nMy main AI toolkit was Codex plus Cursor.\nCodex: the bold builder. Early on, Codex\u0026rsquo;s strong generation ability let me stand up the underlying code framework fast. Give it a clear instruction and it lays the foundation and puts up the walls almost instantly.\nCursor: the precise scalpel. As the codebase grew, though, relying purely on Codex\u0026rsquo;s \u0026ldquo;generate a big chunk\u0026rdquo; style started causing problems — once the project got more complex and I needed tighter control over changes, Codex would occasionally hallucinate or break existing logic. That\u0026rsquo;s where Cursor became indispensable — more like an experienced engineer: quickly locating the exact snippet that needs changing, proposing a fix rather than just overwriting things outright, and leaving me with final say before anything actually changes.\nThat combination — broad strokes for the big structure, precise edits for the fine detail — let one person handle genuinely complex business logic.\nThe result: a one-person army # Through this workflow, I built KanjiGo — the app and its backend system. Even built solo, the core functionality is genuinely solid, with a complete, fairly involved communication stack: SIP calling built into the app\u0026rsquo;s front end (the call interface shown above), smart routing on the backend that assigns calls to different agents, and a queuing strategy that handles priority when agents are busy.\nFrom the low-level protocol up through the front-end interaction, everything is fully functional — work that used to require a backend developer who understood VoIP protocols, a mobile developer, and a UI designer all working together.\nThe \u0026ldquo;rough\u0026rdquo; UI is actually the best proof of Huang\u0026rsquo;s point # At this point someone might reasonably say the app\u0026rsquo;s UI looks a bit rough. Fair — it\u0026rsquo;s not polished. But that\u0026rsquo;s exactly the part I want to highlight, and it\u0026rsquo;s the best evidence for Huang\u0026rsquo;s argument.\nWhy does the UI look rough? Because I lack domain expertise in design. AI is powerful enough to write me flawless calling logic, because I understand the technical logic and can direct it. But I don\u0026rsquo;t have much of an eye for design — I don\u0026rsquo;t know what \u0026ldquo;good design\u0026rdquo; actually looks like, so I can\u0026rsquo;t give the AI prompts with real aesthetic judgment behind them. No matter how capable the AI is, it can\u0026rsquo;t conjure taste it\u0026rsquo;s never been shown.\nWhat does that tell us? AI is an amplifier. If you understand the technical logic, AI lets you do the work of an entire dev team by yourself. If you understand visual design, AI lets you produce master-level UI fast. What I\u0026rsquo;m missing right now is just someone with real design experience handing me a proper design spec — with that document in hand, capturing real design expertise, I\u0026rsquo;m fully confident I could direct the AI to take this rough interface and turn it into a top-tier visual experience almost instantly.\nClosing thought # Software development isn\u0026rsquo;t dead — it\u0026rsquo;s just taking a different shape. Tomorrow\u0026rsquo;s developer doesn\u0026rsquo;t need to memorize every API by heart — they need to become a product manager with real depth in their field, and an architect who knows how to direct AI.\nAs long as you have a genuine, deep understanding of some domain — communications, healthcare, education, art, whatever it is — and know how to ask AI the right questions, you can build things that used to be unimaginable. A rough interface isn\u0026rsquo;t the real problem — that\u0026rsquo;s just technique. Complete functionality and coherent logic — that\u0026rsquo;s the substance.\nEmbracing AI isn\u0026rsquo;t just about writing code. It\u0026rsquo;s about unlocking the real value of what you already know.\n","date":"2026-02-08","externalUrl":null,"permalink":"/posts/solo-ai-built-platform-and-app/","section":"Blog","summary":"Codex framed the app, Cursor did the surgical edits, and a working SIP call-center app got built by one person — the rough UI is the actual lesson here.","title":"\"Code Is Dead\"? What Building a Platform and App Solo With AI Actually Taught Me","type":"posts"},{"content":"","date":"2026-02-08","externalUrl":null,"permalink":"/tags/coding/","section":"Tags","summary":"","title":"Coding","type":"tags"},{"content":"Today I gave my Obsidian vault a real cleanup — clearing out a batch of notes that started out red-hot and then just stopped cold. Looking back at these half-finished threads felt less like tidying and more like watching my own thinking evolve. The most obvious one was a series from April 2024 on large-model fine-tuning.\nBack then, I was genuinely eager to \u0026ldquo;train\u0026rdquo; a model with domain expertise.\n1. The 2024 conviction: building a \u0026ldquo;digital colleague\u0026rdquo; # In a note from April 23, 2024, I\u0026rsquo;d sketched out a pretty simple vision: I wanted the model to behave like a fresh graduate on their first day — strong general ability, but no industry context or business logic yet. Back then I was convinced fine-tuning was the only path to actually getting it up to speed — injecting domain knowledge and rules to turn a general model into an industry-specific one.\nChasing that goal, I set up a dedicated machine with an NVIDIA card on April 27th, and by May 8th, after working through environment setup, VRAM limits, and data cleaning, I finally had the model running locally. That feeling of \u0026ldquo;mastering the model\u0026rdquo; felt, at the time, like the finish line for the whole effort.\n2. The shift: fine-tuning stopped being the default path # Looking back now, though, the model ecosystem moved faster than anyone expected. Re-reading these notes today, I can see my core assumptions have shifted structurally. General-purpose models have gotten close to expert-level even in code, reasoning, and specific industry depth, so the number of situations that genuinely require fine-tuning has shrunk fast. And as RAG (retrieval-augmented generation), agents, and workflows have matured, it\u0026rsquo;s become clear that a lot of the time the model\u0026rsquo;s capability wasn\u0026rsquo;t the bottleneck — how we organized the data and the task was.\nFine-tuning went from being everyone\u0026rsquo;s default starting point to a heavier, more specialized tool you reach for only in particular situations.\n3. From \u0026ldquo;capability\u0026rdquo; to \u0026ldquo;value\u0026rdquo;: where the focus moved # I deleted these notes because they no longer reflect how I think now. Going through them made the shift over the past year really clear:\nThen (early 2024) Now Main focus The model itself (parameters, training, fine-tuning) Applying the model (agents, automation, systems) Perspective Experimental: how do I train the model well? Engineering: how do I get the model to keep producing value? Approach Compute + data + fine-tuning Prompting + workflow + RAG End goal Chasing model capability Chasing business results I used to care about operating on the model\u0026rsquo;s \u0026ldquo;brain.\u0026rdquo; Now I care more about giving it a good toolbox and a clear standard operating procedure to work from.\nClosing thought # Those deleted notes are really a marker of one stage of exploration ending. A technical path is never a straight line — it\u0026rsquo;s a constant process of revising your own earlier conclusions. In the AI era, some experiments leave behind code and models; for others, the process itself is the real output.\nRather than staying fixated on taming the perfect model, it\u0026rsquo;s more worthwhile to build the system that lets the model actually shine.\n","date":"2026-02-08","externalUrl":null,"permalink":"/posts/deleting-fine-tuning-notes/","section":"Blog","summary":"Clearing out a year-old Obsidian folder on model fine-tuning made the shift obvious: from chasing model capability to chasing business results through workflow and RAG.","title":"Deleting My Fine-Tuning Notes: From 'Taming a Model' to 'Steering a Process'","type":"posts"},{"content":"","date":"2026-02-08","externalUrl":null,"permalink":"/tags/llm/","section":"Tags","summary":"","title":"LLM","type":"tags"},{"content":"","date":"2026-02-05","externalUrl":null,"permalink":"/tags/networking/","section":"Tags","summary":"","title":"Networking","type":"tags"},{"content":"Date: February 5, 2026 · Environment: macOS Sequoia / Sonoma\nThe problem: couldn\u0026rsquo;t reach a NAS on the local network (192.168.5.154). Both ping and traceroute to that address kept failing with sendto: No route to host, even though it was on the same physical network.\nDigging in # Basic connectivity test. Running ping 192.168.5.154 returned No route to host — which meant the device wasn\u0026rsquo;t simply off; macOS genuinely didn\u0026rsquo;t know which network interface to send the packet out through.\nRoute tracing. traceroute -n 192.168.5.154 failed at the very first hop, confirming the traffic was being intercepted or dropped before it even left the local machine\u0026rsquo;s network stack.\nChecking the routing table (the key discovery). Running netstat -nr | grep 192.168.5 turned up something odd: that subnet\u0026rsquo;s route pointed to a virtual interface called feth3656, instead of the physical network card (en0) or an expected network tunnel.\nTracking that down: a feth (fake Ethernet) interface is typically created by the \u0026ldquo;transparent proxy\u0026rdquo; driver behind an ad-blocking app. Even after that app is turned off in its own UI, its system extension can stay resident at a lower level and keep hijacking traffic for that private subnet.\nFixing it # A quick manual fix. First, delete the hijacked route:\nsudo route delete -net 192.168.5.0/24 Then point that subnet back at the actual active network interface:\nsudo route add -net 192.168.5.0/24 -interface utunX (the interface name here needs to match your actual setup). This fixed things immediately, but the problem came back after a restart.\nActually removing the leftover driver. Go to System Settings → Network → VPN \u0026amp; Filters. The ad-blocking app showed as disabled, but its plugin entry was still registered — clicking the \u0026ldquo;–\u0026rdquo; button removed it from the system configuration entirely. After removing the leftover plugin, running sudo route -n flush cleared out all the dynamically generated bad routes.\nTuning the network service order. In network settings, I adjusted the service order so that commonly used virtual network interfaces rank above the physical adapter, to prevent the routing table from conflicting again in the future.\nRoot cause # An ad-blocking app I\u0026rsquo;d installed before hadn\u0026rsquo;t fully uninstalled — its leftover virtual network driver (the feth interface) had illegitimately claimed routing for the 192.168.5.0/24 subnet at the system level. Since the driver was in a half-broken state, every packet headed for that subnet ran into a dead end.\nFix: used netstat to pin down the exact rogue interface, removed the corresponding content-filter extension from System Settings, and after a restart the system went back to assigning correct routes automatically.\n","date":"2026-02-05","externalUrl":null,"permalink":"/posts/macos-lan-route-hijack-fix/","section":"Blog","summary":"A NAS on 192.168.5.x went unreachable with ‘No route to host’ — the culprit was a leftover feth interface from an ad-blocker’s content-filter extension that never fully uninstalled.","title":"Tech Log: Chasing Down a Hijacked LAN Route on macOS","type":"posts"},{"content":"Recording a course used to eat up several days of my time. This time I tried a completely different AI-assisted workflow, and the efficiency gain was a genuine step change.\nMy three-step AI workflow # Step 1: structure first — write the outline. Everything starts with real thinking. With Gemini\u0026rsquo;s help, I worked out the course\u0026rsquo;s core logic first. The AI acted like an experienced editor, helping me catch gaps and tighten up the reasoning. The lesson here: don\u0026rsquo;t let AI do the thinking for you — let it sharpen the thinking you\u0026rsquo;ve already done.\nStep 2: fill in the content — put together a full transcript. With the outline in hand, I turned my spoken notes and raw material into a full word-for-word transcript. This step is really the soul of the whole course — it\u0026rsquo;s what determines how substantial the final content actually is.\nStep 3: the magic moment — deep processing in NotebookLM. This is the key step. I fed both the finished outline and the full transcript into NotebookLM. Using its very strong ability to understand long text, it quickly pulled out the core points for a slide deck automatically, and it was smart about it too — pulling the right quotes and examples straight from my own material, so the generated deck matched my original thinking closely enough that it barely needed a second pass.\nThe result: recording got a lot faster # I already have a locally deployed voice-cloning setup, but it still gives itself away whenever Chinese and English get mixed together, so out of respect for the material, I recorded the course in my own voice. With a logically tight deck to guide me visually and a detailed transcript to lean on for the language, recording went smoothly — I barely stumbled at all.\nWhat used to take a week to put together now takes a day or two.\nClosing thought # AI isn\u0026rsquo;t going to replace the person making the content. Treat it as a collaborator rather than a one-click generator, and constrain it with a solid outline first — the amount of value it can unlock is genuinely huge.\n","date":"2026-02-04","externalUrl":null,"permalink":"/posts/ai-content-workflow-outline-to-slides/","section":"Blog","summary":"A three-step pipeline — outline with Gemini, flesh out a transcript, then let NotebookLM turn both into a presentation — that cut a week of course prep down to a day or two.","title":"My AI Content Workflow: From Logic Outline to a High-Quality Deck","type":"posts"},{"content":" The trigger: running Linux apps on a Windows VPS gets awkward # I needed a Linux environment, so my plan was to install WSL2 on my Hyonix VPS.\nThat hit a wall immediately: the install kept throwing errors, and it turned out my system version was 1809 — an early build of Windows Server 2019 that only supports WSL1 natively, not WSL2. Upgrading to a version of Windows Server that supports WSL2 wasn\u0026rsquo;t really an option either: after subtracting what the OS itself uses, my 25GB total disk space barely had anything left, nowhere near enough for a cross-version OS upgrade.\nAn unexpected break: doubling my space # While searching around for \u0026ldquo;Hyonix low disk space,\u0026rdquo; I stumbled on a comment from someone mentioning that filing a support ticket could get your disk quota doubled.\nFigured it was worth a shot — even though this box is an old one I bought back in 2022 — so I sent a support ticket in English. The response was genuinely impressive: support rep Nikkie replied fast, and despite this being a four-year-old machine, there was no pushback at all — they went ahead and bumped the disk quota straight from 25GB to 50GB at the underlying level.\nMaking Windows notice the new space # Once the hardware-side expansion was done, Windows didn\u0026rsquo;t pick it up automatically. Logging in over RDP and opening diskmgmt.msc showed the extra 25GB sitting there as unallocated space. Right-clicking the C: drive and running \u0026ldquo;Extend Volume\u0026rdquo; fixed it instantly — the available space on C: jumped from a cramped 6GB back up to over 30GB.\nWrap-up and what\u0026rsquo;s next # This experience left me with a lot more goodwill toward Hyonix as a provider — keeping ticket response times this good for an old, low-margin plan is worth sticking around and renewing for.\nWith double the space now, this box has a lot more room to grow. Once I get some time, I\u0026rsquo;m planning to properly upgrade the OS to Windows Server 2022 and turn it into my main utility machine.\n","date":"2026-02-04","externalUrl":null,"permalink":"/posts/hyonix-vps-expansion/","section":"Blog","summary":"A WSL2 dead end on an old Windows Server 1809 box turned into a pleasant surprise: one support ticket doubled the disk quota on a four-year-old VPS.","title":"Ops Diary: Expanding My Hyonix VPS","type":"posts"},{"content":"","date":"2026-02-04","externalUrl":null,"permalink":"/tags/windows/","section":"Tags","summary":"","title":"Windows","type":"tags"},{"content":"","date":"2026-01-30","externalUrl":null,"permalink":"/tags/agents/","section":"Tags","summary":"","title":"Agents","type":"tags"},{"content":"Over two days of heavy use, fixing issues as I went, I got a clear sense of just how big a change moltbot (formerly Clawdbot) actually represents.\nThis isn\u0026rsquo;t one feature getting polished — it\u0026rsquo;s a shift at the paradigm level.\n1. What AI is changing isn\u0026rsquo;t the tool — it\u0026rsquo;s the developer\u0026rsquo;s habits # In traditional software, we\u0026rsquo;ve long been used to one fixed path: deploy → configure → use → debug → iterate. Even under \u0026ldquo;low-code\u0026rdquo; or \u0026ldquo;automation\u0026rdquo; branding, that path hasn\u0026rsquo;t really changed — the steps are just wrapped a bit more friendlily.\nWith moltbot, that\u0026rsquo;s starting to shift. The AI isn\u0026rsquo;t just passively executing configuration anymore — it\u0026rsquo;s starting to understand intent. What I gave it wasn\u0026rsquo;t a step-by-step instruction, but a goal and an expected behavior: why didn\u0026rsquo;t the reminder fire on time? Did the task actually trigger? Is there a gap in the configuration? Can the system verify itself? What came after that, I didn\u0026rsquo;t have to trace line by line myself.\n2. From passively waiting to actively waking itself: the system starts correcting itself # In one verification pass, moltbot did several notable things on its own: checked why a task hadn\u0026rsquo;t fired, identified the cause as passive wake mode (next-heartbeat), switched the task over to active wake mode, filled in a scheduled task that had been missing, re-verified every key time point, and gave a clear conclusion — telling me I didn\u0026rsquo;t need to do anything further, just wait for the next automatic push.\nThat last part matters. It means this isn\u0026rsquo;t simple automation anymore — it\u0026rsquo;s a running system with the ability to self-check, self-repair, and self-confirm.\n3. The barrier to entry has genuinely dropped # A lot of people talk about \u0026ldquo;AI lowering the barrier,\u0026rdquo; but in practice that often just means the barrier shifted from \u0026ldquo;can write code\u0026rdquo; to \u0026ldquo;can click through configuration and understand the docs.\u0026rdquo;\nWith moltbot, for the first time, I clearly felt the barrier drop to something more like \u0026ldquo;can clearly state what you want.\u0026rdquo; You don\u0026rsquo;t need to worry about how a scheduled task is actually configured, the mechanics of wake mode, whether some step in the config got missed, or whether a new task node needs adding — all of that becomes the AI\u0026rsquo;s problem. As the user, the only thing left for you to do is judge whether the result matches what you expected.\n4. What this means for the software industry # From a software-engineering angle, this shift is genuinely disruptive — and a little unsettling. The management models we\u0026rsquo;ve talked about for years — Agile, Waterfall, DevOps, SRE — all share an underlying assumption: business people, requirements analysts, and developers are distinct roles.\nWith an AI-Agent-plus-Skills setup like moltbot, that boundary starts to blur: the system judges what\u0026rsquo;s not working correctly, adjusts how it runs on its own, verifies and reports on the outcome, and the human only steps in at the very end to confirm the result. A lot of management structures that used to rely on process, policy, and experience can, in principle, be rewritten.\n5. What moltbot actually is: a platformized AI agent # Structurally, moltbot has stopped being a \u0026ldquo;tool\u0026rdquo; in the usual sense — not a single-purpose piece of software, not a collection of scripts, not a traditional automation platform. It\u0026rsquo;s closer to a blend of PaaS and SaaS, built around an AI agent at its core, with Skills as the unit of capability, driven by intent rather than instructions. You\u0026rsquo;re not \u0026ldquo;using a feature\u0026rdquo; — you\u0026rsquo;re employing a digital worker that keeps operating on its own.\n6. A clear signal # After two days of heavy use, one thing feels very clear: once a system starts checking, on its own, whether the system itself is \u0026ldquo;working correctly,\u0026rdquo; the relationship between people and software has already changed. Moltbot isn\u0026rsquo;t making me configure systems more efficiently — it\u0026rsquo;s getting me to the point where I don\u0026rsquo;t need to care how the system is configured at all.\n","date":"2026-01-30","externalUrl":null,"permalink":"/posts/moltbot-two-days-deploy-configure-use/","section":"Blog","summary":"Watching an agent diagnose its own missed schedule, switch itself from passive to active wake mode, and confirm the fix — without me tracing a single step of it myself.","title":"Two Days Deep in Moltbot: AI Is Reshaping the Deploy-Configure-Use Path","type":"posts"},{"content":" Background # I\u0026rsquo;d been running Clawdbot against Google Gemini\u0026rsquo;s free tier for a while, but kept hitting Google\u0026rsquo;s rate limits or safety-review triggers, which meant the account kept getting temporarily frozen — not great for the stability of an automated pipeline. To get more consistent API responses and a higher quota, I decided to drop the free tier and register for one of Google\u0026rsquo;s paid plans instead.\nRegistering and paying # Since Google Cloud and its paid services aren\u0026rsquo;t available to mainland Chinese accounts, I registered a US Google account to make sure the service worked properly and stayed accessible. The payment came to $30, and on the bank\u0026rsquo;s side it showed as an \u0026ldquo;overseas online sale — authorized\u0026rdquo; (authorized/pending), meaning the funds were held but not yet finally settled.\nGetting flagged and suspended # Shortly after the payment went through (on January 29, 2026), the system flagged the account as \u0026ldquo;created by a bot or script\u0026rdquo; or \u0026ldquo;linked to multiple other accounts,\u0026rdquo; and suspended it immediately. My guess is that registering the account, attaching an overseas card, and making a fairly large payment all within a short window tripped some baseline fraud-detection logic in Google\u0026rsquo;s global security system.\nWhere things stand # I formally submitted a reinstatement appeal on January 29, asking Google to confirm the account was registered manually by a real person for legitimate development purposes, to restore normal access, and — if the account can\u0026rsquo;t be restored — to reverse the $30 pending charge. I\u0026rsquo;m currently waiting on a reply from Google\u0026rsquo;s review team, which typically takes 24–72 hours.\nIn the meantime, I\u0026rsquo;ve picked up a paid model plan through OpenRouter instead, starting with their simplest paid tier.\n","date":"2026-01-29","externalUrl":null,"permalink":"/posts/google-account-suspension-appeal/","section":"Blog","summary":"Registered a new Google account to pay for steadier API access after hitting rate limits on the free tier — and got flagged as bot-created within a day of paying.","title":"A Failed Attempt: Tracking a Google Account Suspension and Appeal","type":"posts"},{"content":"","date":"2026-01-29","externalUrl":null,"permalink":"/tags/google/","section":"Tags","summary":"","title":"Google","type":"tags"},{"content":" Background # The blog runs on a Tencent Cloud Lighthouse instance, using Typecho. The current state: both the plain HTTP and the HTTPS version of the site were reachable. That\u0026rsquo;s not great for security or SEO, so the goal was to force every HTTP request to redirect to HTTPS, with a single canonical domain.\nConfirming the environment # ps -ef | grep nginx confirmed the web server was Nginx, using Lighthouse\u0026rsquo;s bundled install at /usr/local/lighthouse/softwares/nginx/.\nFinding the HTTP (port 80) config # Searching with grep -R \u0026quot;listen 80\u0026quot; -n /usr/local/lighthouse/softwares/nginx/conf turned up the key file: the HTTP entry point lived in typecho.conf, set as the default_server, catching every HTTP request as a fallback. Looking at the existing config, it was still running PHP and rewrite rules under plain HTTP — which is exactly why the HTTP version of the site was still reachable at all.\nConfirming HTTPS was already fine (no changes needed there) # HTTPS for the domain was already configured separately, in mcetf.cn.conf and www.mcetf.cn.conf under the Nginx include directory. Both only listen on 443 SSL, and HTTPS itself was working correctly. The plan: leave the HTTPS config alone entirely, and only touch the HTTP redirect behavior.\nDesigning the redirect logic # The target behavior:\nhttp://mcetf.cn → 301 → https://mcetf.cn http://www.mcetf.cn → 301 → https://mcetf.cn https://www.mcetf.cn → 301 → https://mcetf.cn with https://mcetf.cn as the one canonical entry point Making the change # Back up the existing config first:\ncp /usr/local/lighthouse/softwares/nginx/conf/include/typecho.conf \\ /usr/local/lighthouse/softwares/nginx/conf/include/typecho.conf.bak.$(date +%F_%H%M%S) Then edit typecho.conf and replace it with:\nserver { listen 80; server_name www.mcetf.cn; return 301 https://mcetf.cn$request_uri; } server { listen 80 default_server; server_name mcetf.cn; return 301 https://mcetf.cn$request_uri; } The www host jumps straight to the bare domain in one hop (avoiding a double redirect), the default_server catches every other HTTP request as a fallback, and PHP/rewrite no longer runs under plain HTTP at all, which is safer.\nCheck and reload Nginx:\n/usr/local/lighthouse/softwares/nginx/sbin/nginx -t /usr/local/lighthouse/softwares/nginx/sbin/nginx -s reload Verifying it worked # curl -I http://mcetf.cn HTTP/1.1 301 Moved Permanently Location: https://mcetf.cn/ curl -I http://www.mcetf.cn HTTP/1.1 301 Moved Permanently Location: https://mcetf.cn/ curl -I https://www.mcetf.cn HTTP/1.1 301 Moved Permanently Location: https://mcetf.cn/ Goal achieved: HTTPS everywhere, with a single canonical domain.\nOne more thing on the Typecho side # To stop the admin panel or posts from generating http:// links on their own, it\u0026rsquo;s worth updating the site URL under Typecho\u0026rsquo;s Settings → General to https://mcetf.cn/.\nFinal state # Request Result http://mcetf.cn 301 → https://mcetf.cn http://www.mcetf.cn 301 → https://mcetf.cn https://www.mcetf.cn 301 → https://mcetf.cn https://mcetf.cn Loads normally ","date":"2026-01-28","externalUrl":null,"permalink":"/posts/forcing-https-typecho-tencent-lighthouse/","section":"Blog","summary":"The blog answered on both HTTP and HTTPS at once — tracking down the default_server catching every HTTP request and rewriting it into a clean 301 chain to a single canonical domain.","title":"Forcing HTTPS on My Typecho Blog on Tencent Cloud Lighthouse","type":"posts"},{"content":"","date":"2026-01-28","externalUrl":null,"permalink":"/tags/nginx/","section":"Tags","summary":"","title":"Nginx","type":"tags"},{"content":"","date":"2026-01-28","externalUrl":null,"permalink":"/tags/typecho/","section":"Tags","summary":"","title":"Typecho","type":"tags"},{"content":"Now that the platform\u0026rsquo;s up, there\u0026rsquo;s still more to dig into on the advanced side — but one thing has already struck me: on the capability side, something has genuinely changed. Configure how it runs, and without any financial-API setup at all, it will go through several rounds of trial and error on its own, find an API, generate the code to call it, pull financial data, and hand back an analysis.\nThis is my operational diary of installing Clawdbot — and I ran into far more than the \u0026ldquo;one command, fully unattended\u0026rdquo; experience the online tutorials promised; I hit plenty of error messages along the way.\nDate: January 28, 2026 · Environment: Linux VPS / Node.js v22 / Python (managed via uv) · Core model: Qwen 2026 (via Portal OAuth)\nStage 1: Core install and keeping the service alive # Goal: work around CLI compatibility issues on Linux, and get it to auto-start on boot with crash recovery.\nProblems hit: running clawdbot daemon install --system threw an unknown option error, and it couldn\u0026rsquo;t auto-register with systemd because of a root-permission issue. Installing the Python dependencies also hit an externally-managed-environment error.\nHow I fixed it: skipped the CLI entirely and wrote the systemd service file /etc/systemd/system/clawdbot.service by hand to manage the process directly. I also brought in Astral\u0026rsquo;s uv tool to handle system-level Python package installs instead of pip, which resolved the dependency conflicts.\nKey part of the systemd config:\n[Service] ExecStart=/usr/local/bin/clawdbot gateway Restart=always # auto-restart within 5 seconds of a crash User=root Stage 2: Wiring up the \u0026ldquo;brain\u0026rdquo; (Qwen OAuth) # Goal: get a stable connection to Qwen (Tongyi Qianwen) going, without the constant worry about authorization expiring.\nI ran clawdbot models auth login and logged in interactively by scanning a QR code, then set the default model with clawdbot models set-default qwen-portal/coder-model.\nOne thing worth knowing: the \u0026ldquo;expiring (6h)\u0026rdquo; shown in the console refers to the access token\u0026rsquo;s lifetime, not the connection itself. Clawdbot holds a refresh token in the background, and as long as the service is running, it silently renews the access token on its own — no manual intervention needed.\nStage 3: Adding capabilities — A-share market data and a code interpreter # Goal: give the bot the ability to look up Chinese A-share stock data, without access to the official plugin store (clawdhub) and with GitHub cloning blocked.\nObstacles: the web-search plugin failed to install, with doctor showing a long list of missing dependencies. GitHub had dropped password-based auth, which blocked cloning third-party skill repos. And by default, Qwen assumed it had no internet access and refused to look up stock prices at all.\nThe fix (the actual highlight here): I used a \u0026ldquo;swap in a tool\u0026rdquo; approach — leaning on Qwen Coder\u0026rsquo;s ability to write code as a substitute for a traditional plugin.\nInstall the libraries locally:\nuv pip install --system --break-system-packages akshare yfinance duckduckgo-search Then adjust the system prompt (essentially a bit of \u0026ldquo;reprogramming\u0026rdquo;) through clawdbot configure, forcing in an instruction along these lines: \u0026ldquo;You have Python access. When asked about stocks, don\u0026rsquo;t say you can\u0026rsquo;t get online — just run code that calls akshare to fetch the data directly.\u0026rdquo;\nResult: the bot no longer depends on a search plugin — it writes and runs its own Python script to pull the Shanghai Composite Index and individual stock quotes, with data that\u0026rsquo;s both more accurate and completely free.\nStage 4: Locking it down (firewall) # Goal: shrink the VPS\u0026rsquo;s attack surface as much as possible, to protect tokens and data.\nRisk analysis: if Clawdbot\u0026rsquo;s gateway port (18789) is left open to the public internet, it\u0026rsquo;s exposed to scanning or unauthorized connections, and SSH (port 22) is a standard target for brute-force attacks.\nHardening, following the principle of least privilege: allow SSH (ufw allow ssh, keeping the management channel open), and block the gateway port entirely with ufw delete allow 18789/tcp. The reasoning: the bot\u0026rsquo;s connection to Telegram is purely outbound, so there\u0026rsquo;s no need for that port to accept inbound connections at all. Result: nothing but SSH is reachable from outside the server.\nFinal firewall state:\nStatus: active 22/tcp ALLOW Anywhere (SSH only) 18789 DENY (localhost-only internal communication) A quick-reference ops kit # If the bot stops responding: systemctl restart clawdbot. To watch what it\u0026rsquo;s doing in real time: journalctl -u clawdbot -f. If it\u0026rsquo;s fully hung: pkill -9 -f clawdbot \u0026amp;\u0026amp; systemctl start clawdbot. To check firewall status: ufw status. And if the model connection genuinely drops: clawdbot models auth login.\nWrap-up # Today\u0026rsquo;s work turned the server from a bare, test-only setup into a highly available, well-secured, production-grade AI node with real financial-data analysis capability.\n","date":"2026-01-28","externalUrl":null,"permalink":"/posts/clawdbot-2026-vps-deployment/","section":"Blog","summary":"Working around a broken systemd installer, teaching a model to fetch stock data itself when the plugin store isn’t reachable, and locking the firewall down to SSH-only.","title":"Clawdbot 2026: VPS Deployment and Hardening Diary","type":"posts"},{"content":"","date":"2026-01-28","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":" 1. Dev environment: moving from Cursor to OpenAI Codex # I\u0026rsquo;ve stopped using Cursor and switched to OpenAI Codex, mainly for cost reasons — it piggybacks on my existing ChatGPT Plus subscription ($20/month), so there\u0026rsquo;s no separate Cursor subscription to pay for on top.\nLooking back on Cursor: the experience was genuinely excellent and it made me noticeably more productive — I recently used it to build an Android SIP communication app — and along the way I picked up a decent feel for using an AI coding assistant well, which should carry over to Codex.\nA few notes on getting Codex set up: you can use it through the web, directly in the ChatGPT Plus interface by selecting the Codex tool; through the CLI, installed with npm install -g @openai/codex for direct terminal interaction; or as an IDE plugin, installed and authorized inside VS Code. Usage is included in the ChatGPT Plus subscription and shares that plan\u0026rsquo;s usage limits (officially described as supporting multiple focused coding sessions per week).\n2. Reworking the network setup # Spent a full two days doing a complete overhaul of the existing network architecture. The key change: dropping the layered Caddy setup in favor of a simpler network path.\n3. Fixing Podsync\u0026rsquo;s YouTube rate-limiting # The original setup kept tripping YouTube\u0026rsquo;s anti-scraping/rate-limiting warnings. I worked out a fix that avoids the rate-limiting issue while still pulling audio/video without burning a lot of bandwidth on unnecessary cloud-storage syncing, and put together a small curl script to check ahead of time whether a given video is likely to trigger rate-limiting:\ncurl -sS -A \u0026#34;Mozilla/5.0\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ \u0026#34;https://www.youtube.com/youtubei/v1/player?key=xxxxxx\u0026#34; \\ --data \u0026#39;{\u0026#34;videoId\u0026#34;:\u0026#34;q1WpRru-OA8\u0026#34;,\u0026#34;context\u0026#34;:{\u0026#34;client\u0026#34;:{\u0026#34;clientName\u0026#34;:\u0026#34;WEB\u0026#34;,\u0026#34;clientVersion\u0026#34;:\u0026#34;2.20240101\u0026#34;}}}\u0026#39; \\ | jq -c \u0026#39;.playabilityStatus | {status: .status, reason: .reason, subreason: .messages, errorScreen: (.errorScreen.playerErrorMessageRenderer.subreason.runs[0].text // empty)}\u0026#39; 4. A NAS trick: benchmarking inside a disposable container # The pain point: NAS systems (Synology, for example) often don\u0026rsquo;t have a normal environment to run a network-speed benchmark in. The fix: run a one-off Docker container just to do the benchmark.\nThe idea is simple — host machine starts a container, the script runs inside it, then the container exits and gets torn down automatically. In practice:\nStart a temporary container:\ndocker run --rm -it debian:12 bash Inside the container, install what\u0026rsquo;s needed and run the benchmark:\napt update \u0026amp;\u0026amp; apt install -y wget curl procps \u0026amp;\u0026amp; wget -qO- bench.sh | bash Then just exit — the container cleans itself up automatically.\nOr, as a one-liner that does the whole thing at once:\ndocker run --rm -it debian:12 bash -c \u0026#34;apt update \u0026amp;\u0026amp; apt install -y wget curl \u0026amp;\u0026amp; wget -qO- bench.sh | bash\u0026#34; ","date":"2026-01-23","externalUrl":null,"permalink":"/posts/todays-ops-log-codex-network-podsync/","section":"Blog","summary":"Moving from Cursor to OpenAI Codex, two days spent simplifying the network layout, a fix for Podsync’s YouTube rate-limiting, and a disposable-container trick for benchmarking a NAS.","title":"Today's Ops Log: Switching AI Tools and Reworking the Network","type":"posts"},{"content":" Why I\u0026rsquo;m removing it # The experience after installing it wasn\u0026rsquo;t great — I still prefer the native macOS way of typing, where what I\u0026rsquo;m thinking, typing, and seeing on screen all stay in sync in one smooth motion. I\u0026rsquo;d also started using voice dictation as a complement, so I decided to remove Rime.\nOn macOS, uninstalling Squirrel (Rime) by just deleting the app usually doesn\u0026rsquo;t clean it out completely. To avoid leftover processes auto-launching or config files interfering with the system\u0026rsquo;s input behavior afterward, it\u0026rsquo;s worth following a proper full removal process.\nThe steps # 1. Remove Squirrel from the system\u0026rsquo;s input methods\nOpen System Settings → Keyboard → Input Sources, select Squirrel in the list, and click the \u0026ldquo;–\u0026rdquo; button to remove it. This stops the system from continuing to load the Squirrel input service in the background.\n2. Restart\nOnce it\u0026rsquo;s removed from Input Sources, restart the computer once to make sure any related processes fully quit.\n3. Delete the system-level input method app\nOpen Finder, press Command+Shift+G, and enter the path /Library/Input Methods. Find and delete Squirrel.app in that directory.\n4. Delete the user-level Rime config\nIn Finder, press Command+Shift+G again and enter ~/Library. Find and delete the Rime config directory there — it holds the input schemas, dictionaries, and user configuration, and it\u0026rsquo;s worth clearing out along with everything else during a full uninstall.\n5. Confirm after logging back in\nLog out and back in (or restart again), then confirm that Squirrel no longer appears in the system\u0026rsquo;s input source list, and that no related process is still running.\n","date":"2026-01-22","externalUrl":null,"permalink":"/posts/uninstalling-rime-macos/","section":"Blog","summary":"Going back to native macOS input plus voice dictation, and the full teardown needed since deleting the app alone leaves the input method partially resident.","title":"Fully Uninstalling Squirrel (Rime) on macOS","type":"posts"},{"content":"","date":"2026-01-21","externalUrl":null,"permalink":"/tags/caddy/","section":"Tags","summary":"","title":"Caddy","type":"tags"},{"content":"","date":"2026-01-21","externalUrl":null,"permalink":"/tags/freepbx/","section":"Tags","summary":"","title":"FreePBX","type":"tags"},{"content":"Environment: a VPS running Caddy as the web server, FreePBX (Asterisk/Apache), and a separate internal web service. Domain: xx.com (used here as a stand-in).\nThe goal # Run FreePBX and another internal web service on the same VPS, with Caddy in front as the reverse proxy, so that: Caddy listens on the standard 80/443 ports and handles HTTPS for everything; requests to a specific path get routed to the internal web service (port 8088), while everything else at the root path goes to FreePBX (port 8080); and FreePBX\u0026rsquo;s SIP traffic (port 5061) reuses the certificate Caddy already obtained for TLS.\nThe problems # Port conflict blocking certificate issuance. FreePBX occupies port 80 by default, which stopped Caddy from starting and from requesting a Let\u0026rsquo;s Encrypt certificate. Fix: move FreePBX\u0026rsquo;s Apache to port 8080, freeing 80 and 443 for Caddy.\nFreePBX\u0026rsquo;s stubborn redirect problem (the critical one). Even with the reverse proxy configured, visiting https://xx.com in the browser would have the address bar jump to http://xx.com:8080/admin/... — exposing the real backend port, and since it was plain HTTP, the browser flagged it as \u0026ldquo;not secure.\u0026rdquo;\nThe cause: FreePBX\u0026rsquo;s Apache has no idea it\u0026rsquo;s sitting behind a proxy. Whenever it needs to redirect (say, to the login page), it builds that redirect based on the port it thinks it\u0026rsquo;s listening on (8080), generating an HTTP 302 with a Location header that sends the browser straight back to port 8080.\nMy first attempt — adding X-Forwarded-Proto: https and X-Forwarded-Port: 443 headers — only fixed some of the internally-generated links; it couldn\u0026rsquo;t intercept the Location redirect Apache had already sent.\nThe actual fix # Editing the Caddyfile to apply two things at once: spoofing the incoming request (telling FreePBX \u0026ldquo;the user came in over HTTPS on port 443, please cooperate\u0026rdquo;), and — the key move — rewriting the response: whenever Caddy gets a \u0026ldquo;go to :8080\u0026rdquo; redirect back from FreePBX, it forcibly rewrites it to the correct HTTPS address before passing it on to the browser.\nThe Caddyfile config that finally worked:\nxx.com { # --- Traffic routing --- # 1. Route a specific path to the internal web service (8088) reverse_proxy /xx-path 127.0.0.1:8088 { header_up Host {host} header_up X-Real-IP {remote} header_up X-Forwarded-For {remote} # WebSocket support header_up Connection {http.request.header.Connection} header_up Upgrade {http.request.header.Upgrade} } # 2. Everything else routes to FreePBX (8080) reverse_proxy 127.0.0.1:8080 { header_up Host {host} header_up X-Real-IP {remote} header_up X-Forwarded-For {remote} # Tell the backend: this is an HTTPS request header_up X-Forwarded-Proto https header_up X-Forwarded-Port 443 # The key fix: intercept and rewrite the Location header on the way back # Replace http://...:8080 with https://... # This is what actually stops the browser jumping to port 8080 header_down Location http://xx.com:8080 https://xx.com } } Results # Visiting https://xx.com no longer jumps ports and stays on a clean HTTPS lock icon the whole way through. The path-based routing works correctly, with the internal service and FreePBX staying out of each other\u0026rsquo;s way. And since Caddy is now the only thing exposed, the firewall can fully block direct external access to ports 8080 and 8088, closing off that attack surface.\nOne thing to remember for later: SIP over TLS # FreePBX\u0026rsquo;s SIP call encryption (TLS on 5061) doesn\u0026rsquo;t route through Caddy, but it can reuse the same certificate — copy the certificate Caddy obtained (from /var/lib/caddy/...) into /etc/asterisk/keys/, grant the asterisk user access to it, then enable TLS on port 5061 in the FreePBX panel.\n","date":"2026-01-21","externalUrl":null,"permalink":"/posts/caddy-reverse-proxy-freepbx/","section":"Blog","summary":"FreePBX kept redirecting the browser back to its raw :8080 port even behind a reverse proxy — the fix was rewriting the Location header on the way out, not just spoofing headers going in.","title":"Making Caddy Reverse-Proxy FreePBX Alongside Another Internal Web Service","type":"posts"},{"content":"I run a Typecho blog on a Tencent Cloud Lighthouse instance, and it\u0026rsquo;s generally been stable day to day. But one problem kept coming back: the HTTPS certificate kept expiring. I was using Tencent Cloud\u0026rsquo;s free certificate, which is supposed to auto-renew, but in practice, every time it expired, I had to manually re-issue and re-bind it, or HTTPS would just break. Not only was that a hassle, it never really felt \u0026ldquo;automatic,\u0026rdquo; and I never quite figured out what was going wrong with the renewal mechanism behind it.\nWorse, there was never a clear warning ahead of time — usually I\u0026rsquo;d only find out once the browser started throwing certificate errors.\nACME\u0026rsquo;s smooth experience # While recently working on my FreePBX deployment, I ended up using ACME\u0026rsquo;s free certificate option (based on Let\u0026rsquo;s Encrypt) for real, and the whole experience can be summed up in one line: configure it once, and you basically never have to think about it again.\nSo I decided to switch my blog\u0026rsquo;s certificate over too, moving entirely off Tencent Cloud\u0026rsquo;s certificate and onto the ACME approach. Because of some network restrictions in my environment, the usual ACME install methods didn\u0026rsquo;t work directly — certain online install scripts or the default CA connection would fail — so in practice I had to use an alternative approach to request and renew the certificate.\n1. Installing via a Gitee mirror\nClone the repo from the Gitee mirror:\ngit clone https://gitee.com/neilpang/acme.sh.git Enter the directory and install:\ncd acme.sh ./acme.sh --install -m info@mcetf.cn Then reload the shell so the command takes effect:\nsource ~/.bashrc 2. Requesting and deploying the certificate\nOnce you see \u0026ldquo;Install success,\u0026rdquo; request the certificate right away:\nacme.sh --issue -d mcetf.cn -d www.mcetf.cn -w /usr/local/lighthouse/softwares/typecho Then install it into the Nginx config directory:\n# make sure the directory exists mkdir -p /usr/local/lighthouse/softwares/nginx/conf/ssl # install the certificate acme.sh --install-cert -d mcetf.cn \\ --key-file /usr/local/lighthouse/softwares/nginx/conf/ssl/mcetf.cn.key \\ --fullchain-file /usr/local/lighthouse/softwares/nginx/conf/ssl/mcetf.cn.cer \\ --reloadcmd \u0026#34;/usr/local/lighthouse/softwares/nginx/sbin/nginx -s reload\u0026#34; 3. Finding and updating the Nginx config\nThis is the last step, and the most important one — finding where Nginx points to the old certificate and updating it to the new path.\nFirst, find where the config lives:\nls /usr/local/lighthouse/softwares/nginx/conf/include/ Or search directly for whichever file references the SSL settings:\ngrep -r \u0026#34;ssl_certificate\u0026#34; /usr/local/lighthouse/softwares/nginx/conf/ Then edit the config file:\nvi /usr/local/lighthouse/softwares/nginx/conf/nginx.conf Find the ssl_certificate and ssl_certificate_key lines and point them at the paths just generated:\nssl_certificate /usr/local/lighthouse/softwares/nginx/conf/ssl/mcetf.cn.cer; ssl_certificate_key /usr/local/lighthouse/softwares/nginx/conf/ssl/mcetf.cn.key; Save and exit (Esc, then :wq), then reload Nginx:\n/usr/local/lighthouse/softwares/nginx/sbin/nginx -s reload ","date":"2026-01-21","externalUrl":null,"permalink":"/posts/switching-blog-to-acme-certificate/","section":"Blog","summary":"Tired of Tencent Cloud’s free certificate silently expiring and needing manual re-binding, I moved my Typecho blog over to acme.sh via a Gitee mirror instead.","title":"Switching My Blog to a Free ACME Certificate","type":"posts"},{"content":"","date":"2026-01-21","externalUrl":null,"permalink":"/tags/claude/","section":"Tags","summary":"","title":"Claude","type":"tags"},{"content":"Are you tired of dragging dozens of nodes around in Dify just to build an AI workflow that should be simple? Debugging in n8n until midnight, never quite sure which node is actually broken? Maintaining five or six flows in Coze, where every small change to the requirements makes you want to nuke the whole project and start over?\nI get it. I\u0026rsquo;ve been through all of it.\nBut the rules of the game have genuinely changed. Claude Skills makes every \u0026ldquo;draw a flowchart\u0026rdquo; AI workflow tool feel clunky and outdated. This isn\u0026rsquo;t hype — it\u0026rsquo;s a paradigm shift that\u0026rsquo;s already underway.\nThe core idea: you don\u0026rsquo;t need to draw flowcharts anymore # Starting today, you can walk away from dragging nodes, wiring up connections and debugging them, manually triggering execution, and playing \u0026ldquo;flow engineer\u0026rdquo; inside a low-code tool.\nMore strikingly: you just tell Claude, in plain language, what capability (Skill) you want, and Claude will draft a complete Skill document for you. The next time you make a related request, Claude automatically applies that Skill — no explicit invocation, no picking a flow, no clicking \u0026ldquo;run.\u0026rdquo;\nThat\u0026rsquo;s the leap Claude Skills brings.\nWhat is a Claude Skill, exactly? # By the official definition, a Skill is essentially a Markdown file that teaches Claude how to handle a specific category of task. When your request matches what a Skill is for, Claude decides on its own whether to use it — you don\u0026rsquo;t have to trigger or select it manually.\nHere\u0026rsquo;s a real example from Anthropic\u0026rsquo;s own Agent Skills repository (a \u0026ldquo;commit-helper\u0026rdquo; skill):\n--- name: commit-helper description: Generates clear commit messages from git diffs. Use when writing commit messages or reviewing staged changes. --- # Generate a commit message ## Instructions 1. Run `git diff --staged` to see the changes 2. Generate a message following the Conventional Commits format: - Summary under 50 characters - Describe the underlying logic change - Reference the related issue (if any) ## Best practices - Use the imperative present tense (\u0026#34;Add feature\u0026#34;, not \u0026#34;Added\u0026#34;) - Explain both what was done and why, not how This isn\u0026rsquo;t a demo — it\u0026rsquo;s a production-grade capability definition.\nComparing traditional workflows to Claude Skills # Traditional workflow (Dify, for example): to build a \u0026ldquo;analyze a document, then generate a report\u0026rdquo; flow, you typically need an input node, an LLM node (with a configured prompt), an API-call node, JSON parsing, a conditional branch, an aggregation step, and an output node — 10 to 15 nodes, an hour or two at minimum, not even counting debugging.\nThe Claude Skills approach: you just write (or have Claude write) a single Skill document. You can literally tell Claude: \u0026ldquo;Create a commit-message Skill for me that analyzes a git diff and writes a message following Conventional Commits.\u0026rdquo; Claude will draft the first version of the Skill document for you directly.\nClaude can even help you build the Skill itself # Through Anthropic\u0026rsquo;s official Skill Creator, you can install the official plugin, describe the capability you want directly to Claude, and have it auto-generate a draft Skill document. From there, you just save it to the right directory, tweak it a bit, and start using it.\nWhere Skills live (this matters) # There are three official scopes: project-level (.claude/skills/your-skill/SKILL.md), personal-level (~/.claude/skills/your-skill/SKILL.md), and enterprise-level (deployed centrally through managed configuration).\nThe real revolution: from \u0026ldquo;you execute\u0026rdquo; to \u0026ldquo;AI executes\u0026rdquo; # The core difference comes down to one line: with a workflow, you tell the AI how to do every step; with Skills, you tell the AI what the goal is.\nA workflow has you thinking about how nodes connect, how parameters get passed, and what runs first. Claude Skills has you thinking about what the actual intent of the task is, what success looks like, and what the key constraints are. That\u0026rsquo;s a jump from the execution layer to the intent layer.\nThe key design behind Skills: progressive disclosure # This is the smartest, and most underrated, part of how Claude Skills works.\nThe old problem: function-calling/workflow tools have a fatal flaw — every conversation has to load the full definitions of every available tool into context up front. Ten tools, each 500 lines long, and you\u0026rsquo;ve got a token bazooka.\nHow Claude Skills solves it: at startup, Claude only reads each Skill\u0026rsquo;s name and description. During matching, it intelligently judges relevance based on your request (not keyword matching). Only during loading does it pull in the full content of the Skill that actually matched.\nScenario Function calling Claude Skills 10 tools, only 1 used Loads all 10 full definitions Loads 10 summaries + 1 full Skill Token cost ~5,000 ~500 Savings — ~90% That\u0026rsquo;s what lets you maintain dozens of specialized Skills without them stepping on each other.\nBest practices for structuring a Skill # 1. YAML metadata (required): name, and description — the most important field, since it\u0026rsquo;s what decides whether the Skill triggers. Write the description to be specific and concrete, describe when to use it, and write it in the third person.\n2. Markdown body: instructions with clear steps, examples of input/output, and best practices covering principles and pitfalls to avoid.\n3. Advanced technique — splitting up a large Skill:\nskill-name/ ├── SKILL.md ├── DETAILED_GUIDE.md └── EXAMPLES.md Detailed content only loads when actually needed, which saves tokens and keeps things easier to maintain.\nWorkflow tools vs. Claude Skills # Dimension Workflow tools Claude Skills Building it Drag nodes Describe the need in plain language Learning curve High Very low Time to build Days Hours Maintenance cost High Low Auto-triggered No Yes Collaboration Look at a diagram Managed with Git Intelligence Fixed flow Claude decides autonomously A 3-day path from zero to production # Day 1: understand the fundamentals, build your first Skill. Read the official docs, have Claude generate a simple Skill for you, save it, and see the auto-trigger in action.\nDay 2: build something real, integrate MCP. Build a moderately complex Skill and learn to call external tools from it.\nDay 3: a project-level Skill. Build a full PR-review Skill, use progressive disclosure, and put it to work in a real project.\nClosing thoughts: a paradigm shift, not a tool upgrade # Dify, n8n, and Coze once lowered the barrier to building AI applications. But their real problem was getting you to think at the wrong level.\nClaude Skills pulls you back to the right one: from drawing flowcharts to stating intent, from manual triggers to automatic application, from being a low-code engineer to being a product and system designer.\nTools go out of date. Methodology doesn\u0026rsquo;t.\nSo close your flowchart editor, open Claude, describe in plain language the Skill you want, let it draft it, save it, and see what it feels like the moment it applies itself automatically.\n","date":"2026-01-21","externalUrl":null,"permalink":"/posts/claude-skills-vs-ai-workflows/","section":"Blog","summary":"Why describing what you want in plain language and letting Claude auto-apply a Skill beats another afternoon spent wiring up nodes in Dify, n8n, or Coze.","title":"From Dragging Nodes to Stating Intent: Claude Skills Is Ending the Old AI Workflow Era","type":"posts"},{"content":"With the basic deployment, security hardening, HTTPS, and backups covered in the first two posts, today\u0026rsquo;s step is where things get real: does this thing actually work for making calls?\nCreate a user (extension) → register a client → get an actual call working\nSounds simple, but on this particular combination of Debian 12, FreePBX 17, and OpenSSL 3, the TLS registration process had a few genuinely classic gotchas worth writing down in full.\n1. Creating extensions # Quick Create\nApplications → Extensions → Quick Create Extension Advanced extension settings — stay hands-off for now\nThe extension\u0026rsquo;s Advanced page has a long list of NAT, RTP, Contact, Force rport, and similar options. My rule for this stage was simple: don\u0026rsquo;t start tuning parameters right away — get the basic flow working first with the defaults.\nBulk import, for later\nBulk Handler exposes Extensions, Users, Groups, and UCP Templates. Using the export template that already had my first extension (200) in it, I had an AI model generate 19 more, imported them, and ended up with a small 20-user call platform. FreePBX is fully capable of managing users at that kind of scale.\n2. Testing from a phone: get UDP working first # On the phone, using Linphone with the server address over UDP: registration succeeded, and internal extensions could call each other.\nThat step matters more than it sounds — it confirms the extension configuration is correct, that the network/NAT/firewall path works, and that Asterisk\u0026rsquo;s core functionality is fine. Whatever goes wrong after this point, the basic environment is no longer the suspect.\n3. Fixing the TLS registration failure # Symptom: switching Linphone to use the domain name with TLS transport produced a \u0026ldquo;wrong password\u0026rdquo; error — a genuinely misleading message.\nFixing the certificate/PJSIP TLS binding. Two things to check:\nCertificate Manager — this must be set to the Let\u0026rsquo;s Encrypt certificate, never the default self-signed one. A self-signed certificate will fail 100% of the time on a phone client.\nSSL Method (critically important) — on Debian 12 with OpenSSL 3, the recommended order is Default (automatic negotiation) first, then tlsv1_2. Don\u0026rsquo;t use plain tlsv1 — it\u0026rsquo;s rejected at the system security-policy level.\nThe extension\u0026rsquo;s transport was locked to UDP. The root cause: the CSV/template extension (200) used UDP, and the new extension (201) inherited that template with its Transport fixed to 0.0.0.0-udp — so even with TLS enabled server-side, the extension itself simply refused to use it.\nThe fix (at the extension level):\nApplications → Extensions → edit the extension → Advanced Find Transport: if it\u0026rsquo;s set to 0.0.0.0-udp, change it to All - PJSIP Transports (or explicitly 0.0.0.0-tls). \u0026ldquo;All\u0026rdquo; is the one I\u0026rsquo;d recommend, since it stays compatible with UDP, TCP, and TLS at once.\nThen, if needed, apply the change and restart:\nSubmit Apply Config SSH into the server and run: fwconsole restart Wrap-up: the first successful test call # After waiting about 30 seconds for the service to restart, I opened Linphone, switched transport to TLS on port 5061, and logged in — while watching the log on the server at the same time:\ntail -f /var/log/asterisk/full TLS registration succeeded, and the first test call genuinely went through.\nBy the end of this post, FreePBX has gone from \u0026ldquo;the system runs,\u0026rdquo; to \u0026ldquo;users can be provisioned,\u0026rdquo; to \u0026ldquo;a real test call actually works.\u0026rdquo;\n","date":"2026-01-19","externalUrl":null,"permalink":"/posts/freepbx-diary-3-first-test-call/","section":"Blog","summary":"Creating extensions, getting UDP registration working first, then chasing down a misleading ‘wrong password’ error that was actually a TLS transport and certificate mismatch.","title":"FreePBX Deployment Diary #3: The First Successful Test Call","type":"posts"},{"content":"","date":"2026-01-19","externalUrl":null,"permalink":"/tags/telecom/","section":"Tags","summary":"","title":"Telecom","type":"tags"},{"content":"With the basic install and initial setup of FreePBX 17 out of the way, today was about filling in two things any production setup needs: HTTPS access for the web admin panel, and an automatic backup mechanism for the system and its configuration. Once these two are in place, this FreePBX instance is actually ready to run long-term.\n1. Setting up HTTPS # Port management\nRequesting a certificate needs port 80 open, so that has to be sorted first:\nSettings → System Admin → Port Management I moved the admin port to 8080 and left port 80 free for certificate issuance.\nA few things worth checking here: enable \u0026ldquo;Responsive Let\u0026rsquo;s Encrypt Rules\u0026rdquo;, which allows the ACME validation traffic through during certificate renewal; keep Intrusion Detection on; and keep Safe Mode on (a self-rescue mechanism for accidental misconfiguration). The overall approach is still the same as before: minimize exposure, and whitelist management IPs.\nRequesting and configuring the certificate\nI used a Let\u0026rsquo;s Encrypt certificate, which requires a domain that already resolves to the FreePBX server\u0026rsquo;s public IP, and port 80 reachable from the outside (for ACME validation).\nIn Certificate Management: add a new Let\u0026rsquo;s Encrypt certificate, confirm it shows as available once issued, and set it as the default certificate.\nHTTPS Setup (the key step)\nOn the HTTPS Setup page: pick the certificate just created under Certificate Manager, and click Install — the system confirms Apache has been configured and shows the certificate\u0026rsquo;s validity period.\nOn the same page, I left TLSv1.2 and TLSv1.3 enabled without trimming further, prioritizing compatibility, then saved and restarted Apache.\nAt that point the web service is ready for HTTPS — once applied and restarted, the admin panel is reachable over HTTPS.\n2. Setting up automatic backups # With HTTPS done, next came the backup configuration.\nBackup storage location\nAdmin → Backup \u0026amp; Restore → File Store For now I\u0026rsquo;m using local storage: path name \u0026ldquo;Local Backup\u0026rdquo;, actual path /var/spool/asterisk/backup. This can be extended to SFTP or object storage later if needed.\nCreating the backup job\nAdmin → Backup \u0026amp; Restore → Backup I added a new backup job (called \u0026ldquo;WeekBackup\u0026rdquo;) covering module configuration and the custom config directories (__ASTETCDIR__ and similar). For notifications, I set an email address to receive backup status updates, for both success and failure. For scheduling: enabled, running weekly, keeping the 3 most recent backups.\nVerifying it actually worked\nRather than just trusting that the web panel \u0026ldquo;looked fine,\u0026rdquo; I checked directly on the server:\nroot@freepbx:/var/spool/asterisk/backup/WeekBackup# That confirmed three things: the scheduled job actually ran, the backup content was genuinely written to disk, and the filenames include version and timestamp info, making it easy to roll back to a specific point later.\nWrap-up # The two things done today are both the kind of unglamorous configuration that quietly decides whether a system is actually trustworthy to run: HTTPS secures management access and future scalability, and automatic backups give me a way back from any misconfiguration or failed upgrade.\nAt this point, this FreePBX setup has a stable baseline environment, reasonable security boundaries, and the ability to recover.\n","date":"2026-01-18","externalUrl":null,"permalink":"/posts/freepbx-diary-2-https-backup/","section":"Blog","summary":"Getting Let’s Encrypt working on the admin panel and setting up a weekly automatic backup — the two unglamorous steps that make a FreePBX box actually production-ready.","title":"FreePBX Deployment Diary #2: HTTPS and Automatic Backups","type":"posts"},{"content":"A deep dive comparing Google\u0026rsquo;s TPU (Tensor Processing Unit) against NVIDIA\u0026rsquo;s GPU (Graphics Processing Unit).\nThe short version: the GPU is the all-around generalist, flexible with a mature ecosystem; the TPU is the specialist — brutally efficient at the specific large-scale matrix math that deep learning training relies on.\n1. Core positioning and architecture # This is the most fundamental difference between the two, and it decides what each one is actually good at.\nGPU TPU Original purpose Originally built for graphics rendering, later generalized into parallel computing (GPGPU) A custom ASIC purpose-built for machine learning, deep learning in particular Core architecture SIMD (single instruction, multiple data): thousands of smaller cores (like CUDA cores), good at handling many independent parallel tasks at once Systolic array: built around a large matrix-multiply unit (MXU); data flows through the chip and gets reused the way blood flows through a heart Flexibility Very high — beyond AI, also handles graphics rendering, physics simulation, scientific computing; supports multiple precisions (FP64, FP32, FP16, INT8) Lower — highly focused on matrix multiplication and convolution, usually optimized hard for low precision (like bfloat16) Memory access pattern Needs frequent reads from DRAM; bandwidth is high, but memory access is still a bottleneck Minimizes memory access — once data is loaded, it flows through the array stage by stage, maximizing reuse and cutting latency and power draw An analogy: a GPU is like a team of a few thousand ordinary mathematicians, each working a problem on their own sheet of paper and passing it to the next — great when everyone\u0026rsquo;s doing similar work in parallel. A TPU is like a precision assembly line: raw material (data) goes in, moves through a fixed sequence of processing steps without being shuffled back and forth, and comes out as a finished product. Extremely fast at one specific product (matrix operations), but hard to retool for anything non-standard.\n2. Performance and efficiency # Training speed: TPUs tend to beat GPUs of the same generation on large models (Transformers, BERT, ResNet) and very large batch sizes — a TPU Pod (cluster) scales to thousands of chips over a fast interconnect with excellent near-linear speedup. GPUs do better on small batches, non-standard architectures, or workloads with a lot of control flow.\nInference: TPUs offer very low latency and high throughput, which suits high-concurrency real-time services well. GPUs (especially inference cards like NVIDIA\u0026rsquo;s T4, A10, L4) are also very capable, and their generality often makes them easier to deploy flexibly.\nPerformance per watt: TPUs win outright. Since they cut out hardware logic for things like graphics rendering, TPUs deliver more FLOPS per unit of energy, which matters enormously at data-center scale.\n3. Ecosystem and development difficulty # This is currently the GPU\u0026rsquo;s biggest moat.\nGPU (NVIDIA CUDA): a near-monopoly ecosystem — virtually every deep learning framework (PyTorch, TensorFlow, MXNet, etc.) supports GPUs first and best. Community resources are extensive, and solutions to most problems are a search away. It also runs anywhere: your own machine, AWS, Azure, GCP, Alibaba Cloud, and more.\nTPU (Google Cloud): tightly bound to Google\u0026rsquo;s own ecosystem, mainly used through TensorFlow and JAX. PyTorch support exists via PyTorch/XLA, but compared to native CUDA support it has more rough edges and is harder to debug. It\u0026rsquo;s also cloud-exclusive — there\u0026rsquo;s no physical TPU card you can buy and plug into your own machine; it\u0026rsquo;s only available for rent on Google Cloud Platform (or through Colab\u0026rsquo;s free tier).\n4. Cost # GPUs are expensive (especially high-end cards like the H100/A100) and heavily exposed to supply and demand — prices spike hard when they\u0026rsquo;re in short supply. But for building compute locally, a GPU is the only option. TPUs from Google are usually competitively priced, and spot (preemptible) instances in particular can be very cheap — for long-running large-model training, TPUs often save a substantial amount on the cloud bill.\nScenario Recommended Why Getting started / learning / research GPU Plenty of tutorials, PyTorch code just runs, easy to debug, broadly compatible Small-scale experiments / personal projects GPU Flexible, no need to adapt code to the hardware Large-scale model training (LLMs) TPU (or a high-end GPU cluster) At massive data scale, TPU\u0026rsquo;s linear scaling and cost advantage stand out Using TensorFlow / JAX TPU Native support is excellent, squeezes out full hardware performance Using PyTorch and don\u0026rsquo;t want the hassle GPU CUDA\u0026rsquo;s ecosystem is the most mature, no XLA compiler compatibility issues to deal with Need on-prem deployment / edge computing GPU TPUs aren\u0026rsquo;t sold as physical hardware (aside from Edge TPU, which is its own separate thing) Companies currently using Google\u0026rsquo;s TPUs fall roughly into two groups: tech companies and AI labs that use TPUs directly for training and inference (the most common case), and hardware vendors that participate in actually manufacturing TPUs.\n5. Core users: the top tech giants and AI unicorns # These companies mainly rent TPU compute through Google Cloud to train their flagship models.\nApple — in a notable disclosure, Apple\u0026rsquo;s own technical papers state that Apple Intelligence\u0026rsquo;s foundation models were pretrained on Google\u0026rsquo;s TPUv4 and TPUv5 clusters, rather than relying solely on NVIDIA GPUs — one of the strongest endorsements yet of TPU performance and stability.\nAnthropic — as OpenAI\u0026rsquo;s strongest competitor (the company behind the Claude model family), Anthropic is a deep TPU user. Google has invested in Anthropic and signed a large-scale cloud computing agreement with it, and Anthropic uses substantial TPUv5e and TPUv5p capacity to train frontier models like Claude 3.\nMidjourney — the well-known AI image-generation tool\u0026rsquo;s training and inference compute is mainly powered by Google Cloud TPUs, which offer strong cost-efficiency for this kind of large-scale generative workload.\nHugging Face — as something like the \u0026ldquo;GitHub\u0026rdquo; of the AI world, Hugging Face partners with Google to make open-source models on its platform easier to run on TPUs, and uses TPUs for optimized training.\nCharacter.AI — a unicorn founded by former Google employees, relying heavily on Google Cloud TPUs to power high-concurrency, real-time conversations for millions of users.\nGoogle itself — the biggest user of all. Nearly every internal product depends on TPUs: Gemini and PaLM are trained entirely on TPUs; Search uses them to understand query semantics (BERT/RankBrain); Waymo uses them for autonomous-driving data processing and model training; Google Photos and Translate use them for image recognition and real-time translation.\n6. Enterprise and industry applications # These more traditional companies use TPU compute on Google Cloud to solve specific business problems.\nSalesforce uses TPUs to train its enterprise AI models (Einstein). LG AI Research, the AI research arm of Korea\u0026rsquo;s LG Group, uses TPUs to train its large multimodal model EXAONE. Ford partners with Google on autonomous driving and data analysis, using TPUs to accelerate simulation and computation. Kakao Brain, the AI division of Korean internet giant Kakao, uses TPUs to train Korean-language large models.\n7. The supply chain: who actually manufactures TPUs? # If \u0026ldquo;who makes TPUs\u0026rdquo; means the hardware supply chain behind the chip itself, the list looks like this: Google handles the core architecture design (the ASIC design itself). Broadcom is a key partner, helping with the physical chip design, IP licensing, and coordinating with the foundry (turning the design into something manufacturable). TSMC handles the actual wafer fabrication — TPUv4 used TSMC\u0026rsquo;s 7nm process, and the newer Trillium (TPUv6) and TPUv5p use more advanced nodes (5nm/4nm class). Some supply-chain reporting and rumors also point to Taiwanese ASIC design-service firms like Alchip and MediaTek potentially being involved in back-end design or peripheral chip work as TPU generations progress, alongside Broadcom.\n","date":"2026-01-18","externalUrl":null,"permalink":"/posts/tpu-vs-gpu-deep-dive/","section":"Blog","summary":"GPUs are the flexible all-rounder with a mature ecosystem; TPUs are the specialist that’s brutally efficient at large-scale matrix math — plus a rundown of who actually trains on TPUs.","title":"TPU vs. GPU: A Deep Dive, and Who's Actually Using TPUs","type":"posts"},{"content":"","date":"2026-01-18","externalUrl":null,"permalink":"/tags/tesla/","section":"Tags","summary":"","title":"Tesla","type":"tags"},{"content":"After getting the car back, I sent the dashcam footage to Tesla\u0026rsquo;s engineering team, and separately, out of curiosity, asked two AI models — Gemini Pro and ChatGPT 5.2 — to do their own engineering-style analysis of whether the emergency braking system had been slow to react.\nBoth models reached a similar conclusion. This was a classic \u0026ldquo;ghost probe\u0026rdquo; scenario — a vehicle suddenly appearing from a blind spot — where the other car became visible less than 5 meters away, leaving less than 0.5 seconds of physical reaction time. At roughly 35–39 km/h, the required stopping distance (around 11–16 meters) was far longer than the space actually available (under 10 meters). Neither model found clear evidence that the emergency braking system had been delayed — instead, both concluded the situation was close to a physical limit where a collision was nearly unavoidable no matter how the system responded.\nMy takeaway: autonomous driving isn\u0026rsquo;t a simple pass/fail system — it grows by continuously bumping up against the edges of what\u0026rsquo;s physically possible. Drive carefully, slow down when you can, and put safety first.\n","date":"2026-01-18","externalUrl":null,"permalink":"/posts/tesla-accident-ai-physics-analysis/","section":"Blog","summary":"Asking Gemini Pro and ChatGPT 5.2 to do an engineering-style read on whether emergency braking was slow to react — and what the physics of a ‘ghost probe’ collision actually allow.","title":"What Two AI Models Concluded From My Accident's Dashcam Footage","type":"posts"},{"content":"A couple of days ago, while helping the business side test an outbound-calling platform migration, I got my first hands-on exposure to FreePBX. Even though this is more of a data era than a voice one, as someone who works in telecom I found this kind of voice-trunking system genuinely interesting. Since it was the weekend, I figured I\u0026rsquo;d deploy a full instance myself and write down the process, as a reference for next time.\nThis post mainly covers installing and doing the basic setup of FreePBX 17 on Debian — it doesn\u0026rsquo;t get into actual call routing, trunks, or dialing policy yet.\n1. Picking the environment # Cloud host\nA lot of older tutorials recommend Vultr\u0026rsquo;s one-click FreePBX image, but that option has since been discontinued — now you install the OS yourself and deploy manually.\nVultr still offers a new-user trial credit (roughly $250), which is plenty for learning and testing. The credit is valid for 30 days, so there\u0026rsquo;s no need to be stingy with resources — just use it up.\nFor this deployment I went with:\nProvider: Vultr Instance: 2 vCPU / 4 GB RAM Disk: 50 GB (more than enough for learning and testing) Region: Osaka, Japan OS: Debian (FreePBX 17\u0026rsquo;s officially recommended choice) A note on the OS\nStarting with FreePBX 17, the project has made clear that it\u0026rsquo;s no longer based on CentOS — it now runs exclusively on Debian Linux, distributed as a Debian install script package. So when creating the cloud instance, just pick Debian directly — no need to agonize over which OS track to take.\n2. Basic system prep # After logging into the server, update the system first:\napt update \u0026amp;\u0026amp; apt upgrade -y If a PAM configuration prompt comes up (asking whether to overwrite /etc/pam.d/common-*), just choose Yes and go with the system defaults.\nThen install a few common tools:\napt install -y curl wget sudo gnupg2 3. Running the FreePBX 17 install script # Move into a temp directory and download the install script:\ncd /tmp wget https://github.com/FreePBX/sng_freepbx_debian_install/raw/master/sng_freepbx_debian_install.sh chmod +x sng_freepbx_debian_install.sh ./sng_freepbx_debian_install.sh A couple of things to note: the install takes a while (anywhere from ten-odd minutes to half an hour), and it doesn\u0026rsquo;t need any manual input along the way — just let it run.\nOnce it\u0026rsquo;s done, Asterisk and the FreePBX web service start automatically.\n4. Web interface setup # Visit the server\u0026rsquo;s IP address in a browser to reach the FreePBX setup screen.\nSystem activation\nThe first time in, you\u0026rsquo;ll see a \u0026ldquo;Welcome to your new FreePBX Server\u0026rdquo; prompt. Choose Activate.\nActivation requires registering a Portal Account — my first attempt failed, and it turned out I needed to fill in the optional fields too before it would go through.\n5. Firewall configuration (the important part) # FreePBX ships with its firewall and intrusion detection enabled by default, and if you\u0026rsquo;re not careful with the configuration, it\u0026rsquo;s very easy to lock yourself out.\nIf you get locked out\nRestart the server twice in a row within 5 minutes, and the system will automatically disable the firewall for about 5 minutes — use that window to fix your configuration.\nRecommended initial setup\nUnder Firewall → Networks, and under Interfaces, confirm the default zone is set to Internet (Default Firewall). Early on, I wouldn\u0026rsquo;t recommend getting too granular with the rules — just make sure you can reliably access and manage the box first.\n6. Updating the system and modules # Once the web setup is done, it\u0026rsquo;s worth updating everything right away.\nSystem update:\napt update \u0026amp;\u0026amp; apt upgrade FreePBX module update:\nfwconsole fwconsole ma upgradeall fwconsole restart After that, the system is in a relatively clean, usable baseline state.\nWrap-up # This round of deployment covered: picking a VPS and installing Debian, installing and deploying FreePBX 17 on top of it, and turning on the firewall for basic network protection.\nGoing to sleep now — next up, building on this: setting up HTTPS, and getting a client connected to actually place a call.\nThis post is a record of the install and initial setup; I\u0026rsquo;ll keep writing up the rest as the deployment progresses.\n","date":"2026-01-18","externalUrl":null,"permalink":"/posts/freepbx-diary-1-install-network/","section":"Blog","summary":"First contact with FreePBX 17 on Debian — a Vultr trial instance, the official one-shot install script, and the firewall gotcha that can lock you out of your own server.","title":"FreePBX Deployment Diary #1: Install and Network Setup","type":"posts"},{"content":" Background # Last Sunday afternoon, while I was driving normally, I collided with a vehicle that pulled out from under a bridge. My first instinct was to just let each side\u0026rsquo;s insurance handle it — mainly because I was worried the process would be slow and complicated. I uploaded photos from the scene through the Tesla app, and when the insurance company called back, they told me clearly: my side wasn\u0026rsquo;t at fault, and recommended letting the traffic police make the official liability determination.\nWhile I was talking with the insurance company, the other driver contacted the traffic police, who told them to come to the local traffic police station to handle it. Once there, the police pulled the surveillance footage from when the accident happened, and their final conclusion was: the other party was fully at fault, responsible for all repair costs.\nSince I rarely get into traffic accidents, I wasn\u0026rsquo;t familiar with how liability gets determined. Going back over the whole thing afterward, I found myself wondering: beyond learning the actual traffic-liability rules, are today\u0026rsquo;s large models already capable of making the right call in a situation like this?\nHow I tested it # I picked one photo from the accident scene and tested it against several models using the same identical prompt: \u0026ldquo;Based on Chinese road traffic regulations, and the information in this photo, determine liability for this accident.\u0026rdquo;\nHere\u0026rsquo;s how the different mainstream models did with this real-world scenario.\nChatGPT # Rating: ★\nFirst up was ChatGPT, the model I use the most day to day. There\u0026rsquo;s no longer a way to pick a specific model — it defaults to ChatGPT 5.2. I\u0026rsquo;ll cut to the conclusion: the result was genuinely disappointing.\nThe full response was long, so here\u0026rsquo;s just the core of its reasoning: after quoting Chinese traffic law at length, it judged from the photo that the white Tesla was driving against traffic, and on that basis concluded the Tesla bore 100% of the liability. That conclusion was the exact opposite of both the facts and the police\u0026rsquo;s actual determination.\nGemini # Rating: ★★★★★\nGoogle\u0026rsquo;s Gemini has been moving fast lately — from the TPU rollout to how mature its model applications have gotten, it\u0026rsquo;s started to feel like it\u0026rsquo;s clearly pulling ahead of OpenAI. Apple recently announcing it\u0026rsquo;s bringing in Gemini only reinforced that impression.\nIn this accident-liability test, Gemini genuinely impressed me — afterward I actually swapped the AI app on my phone\u0026rsquo;s home screen over to Gemini.\nSince I only gave it a static photo rather than video, Gemini took a more careful approach and laid out three possible scenarios. In my view, even the first scenario alone was enough to establish that my side wasn\u0026rsquo;t at fault. Working from that, Gemini\u0026rsquo;s liability judgment was basically correct, and its follow-up advice was reasonably sound and professional. (This test used Gemini Pro.)\nGrok # Rating: ★\nThis is a model app I\u0026rsquo;ve only recently started using — mainly because it\u0026rsquo;s \u0026ldquo;unrestricted\u0026rdquo; enough to handle almost any topic or borderline image.\nBut when it comes to a serious, rules-heavy question like this one, it turned out to be pretty unreliable. In this test, Grok read the scene as a same-direction sideswipe and decided the trailing Tesla bore primary responsibility. I honestly can\u0026rsquo;t figure out how it arrived at \u0026ldquo;same direction\u0026rdquo; from the photo.\nMicrosoft Copilot # Rating: ★\nI installed Copilot specifically to test this. I\u0026rsquo;d never used it before, mostly because of a lukewarm impression of it, and this test didn\u0026rsquo;t really change that.\nCopilot didn\u0026rsquo;t give a direct judgment at first — it just kept quoting traffic-law provisions. Only after I explicitly demanded \u0026ldquo;you must give a liability determination\u0026rdquo; did it actually answer the question.\nBut then the real problem showed up: it kept getting left and right mixed up — the Toyota was clearly on the Tesla\u0026rsquo;s left, but Copilot repeatedly described it as being on the right, which threw off its whole chain of reasoning. I had to keep cross-checking against the accident photo just to follow what it was saying.\nOverall: it wouldn\u0026rsquo;t commit to a conclusion, and when it finally did, the conclusion was clearly wrong.\nClaude # Rating: ★\nFor someone with no development background at all, Claude Code used to feel almost legendary. I once used it myself to whip up an H5 survey form for a business team in just a few minutes.\nBut for someone who already has some coding ability, that style of interaction doesn\u0026rsquo;t offer much control, so I eventually canceled my subscription and switched to Cursor.\nFor this test I could only use the free Sonnet 4.5. On the traffic-liability question, Claude\u0026rsquo;s performance fell well short of the impression Claude Code had left me with. It concluded that the Tesla was changing lanes or pulling over and failed to yield to a Toyota going straight — but looking at the actual scene, the two vehicles were nearly at a 90-degree angle to each other, which makes that \u0026ldquo;lane-change sideswipe\u0026rdquo; reasoning hard to follow.\nDomestic models # I won\u0026rsquo;t score the domestic models — you can judge the results yourselves.\nDoubao concluded the Tesla was changing lanes, and so held the Tesla primarily responsible.\nYuanbao got the right-of-way principle correct, but mixed up which vehicle was on which side.\nOPPO AI # What genuinely impressed me was the AI built into my OPPO phone. Its explanation of the right-of-way rules lined up closely with the traffic police\u0026rsquo;s actual determination — overall, its style was similar to Gemini\u0026rsquo;s: explain the rule clearly first, then give a conclusion, with clean logic and restrained language. That genuinely exceeded my expectations.\nFinal thoughts # I\u0026rsquo;m not an AI expert, and none of the above is meant as a rigorous benchmark — just one regular, heavily-AI-dependent user\u0026rsquo;s comparison in a real situation.\nGoing through the process also gave me a real appreciation for how digitized the traffic police system has become here: after the report was filed, officers could pull up the scene footage directly, complete a contactless review using a visualization system, and once liability was determined, the ruling synced straight to the official traffic-management app.\nAs long as there\u0026rsquo;s no dispute, the whole process is efficient, clear, and traceable.\nOne more thing that stuck with me: if what had pulled out from under that bridge hadn\u0026rsquo;t been a car, but an e-bike, or a pedestrian, the nature of the whole incident — and its consequences — would have been completely different. Even with liability clearly established, it could easily have turned into a personal-injury case, with the cost and stress multiplying many times over. This accident was a reminder, once again, that safe driving is what actually matters most.\n","date":"2026-01-15","externalUrl":null,"permalink":"/posts/ai-models-traffic-accident-liability/","section":"Blog","summary":"After my own Tesla was hit by a car pulling out from under a bridge, I ran the accident photo past ChatGPT, Gemini, Grok, Copilot, Claude, and a few domestic models to see which ones actually got liability right.","title":"Head-to-Head: How Mainstream AI Models Judge a Real Traffic Accident","type":"posts"},{"content":"Recently I ran an experiment that was interesting, and pretty practical: having different large models redesign one of my blog sites, using a method as close as possible to how I\u0026rsquo;d actually work day to day.\nThis wasn\u0026rsquo;t about benchmarking models or showing off prompt-engineering tricks — it came from a very plain question: a blog page that doesn\u0026rsquo;t look great, has mediocre UX, and isn\u0026rsquo;t great for SEO either — can AI help me improve the whole thing?\nWhat I actually cared about was this: among the models now being called \u0026ldquo;next-generation,\u0026rdquo; which one behaves more like a competent colleague when handed something real, ambiguous, and goal-oriented but without a prescribed solution — rather than just a tool.\n1. The testing approach: brief it like you\u0026rsquo;d brief a colleague # In real work, we rarely describe a design problem in precise technical language. More often we say things like: \u0026ldquo;this page feels a bit messy,\u0026rdquo; \u0026ldquo;the structure isn\u0026rsquo;t very clear,\u0026rdquo; \u0026ldquo;it doesn\u0026rsquo;t look very premium,\u0026rdquo; or \u0026ldquo;it\u0026rsquo;s not great for search engines.\u0026rdquo;\nSo for this test I deliberately avoided writing an elaborate prompt, and used only three very natural lines describing what I wanted, asking each model to redesign the page across three dimensions: user experience (UX), visuals, and SEO. No framework specified, no style specified, no task breakdown — just seeing whether the model could figure out on its own what to do, and in what order.\n2. The models compared # Three models went head to head: Gemini 3 (Google), Claude Opus 4.5 (Anthropic), and GPT-5.1 Codex (OpenAI). The test conditions were identical across all three: the same starting page, the same description, and no model-specific prompt tuning at all.\n3. Results: a clear split in design ability # The conclusion from the final results was pretty clear: if you\u0026rsquo;re judging overall design ability, Claude Opus 4.5 came out ahead.\n\u0026ldquo;Design\u0026rdquo; here isn\u0026rsquo;t just \u0026ldquo;does it look nice\u0026rdquo; — it\u0026rsquo;s a more complete idea covering things like whether the page structure makes sense, whether the information hierarchy is clear, whether the user\u0026rsquo;s path through the page flows well, whether it proactively considered technical SEO, and whether it filled in important design considerations I hadn\u0026rsquo;t explicitly asked for. Opus 4.5 was noticeably more complete on all of these fronts.\n4. The key difference: not generation ability, but planning depth # What struck me most from this comparison is that the real gap between these models isn\u0026rsquo;t about whether they can generate something anymore — it\u0026rsquo;s about whether they think it through before generating.\nGemini 3 leaned toward getting to a result quickly. Its output wasn\u0026rsquo;t wrong, exactly, but overall it felt more like it jumped straight to a solution, without much explicit reasoning about the overall structure first — the design decisions felt a bit scattered. It came across more as \u0026ldquo;figuring it out while building\u0026rdquo; than \u0026ldquo;designing, then building.\u0026rdquo;\nGPT-5.1 Codex was strong on engineering execution. Its performance was solid — a clear technical path, reliable implementation logic, and well suited to actually turning a plan into working code. But in this test it acted more like a highly capable front-end engineer than something driving the overall design direction.\nOpus 4.5 planned first, then built it out. The biggest difference with Opus 4.5 was that it laid out a complete design rationale up front, made the relationship between UX, content structure, and SEO explicit, and only then worked step by step toward the concrete implementation. It also proactively handled a number of things I hadn\u0026rsquo;t explicitly asked for, but that any real design work would need to account for. That\u0026rsquo;s what set the ceiling on the final result\u0026rsquo;s quality.\n5. What actually impressed me was the shift in how the work got done # Judging purely on output quality, this was already a successful test. But what really stood out to me was the change in the entire workflow: in under 20 minutes, working on the same site, I ended up with three complete, deployable design-and-optimization proposals — and the only thing left for me to do was pick the one I liked best.\nIn a traditional process, it\u0026rsquo;s hard to get a colleague to produce multiple complete proposals that cover design, structure, and SEO all at once, in that short a time, without a lot of back-and-forth.\n6. A practical takeaway from this comparison # This test left me more convinced of one thing: AI is shifting from being a \u0026ldquo;tool\u0026rdquo; to something closer to a collaborator with an actual role to play — but only if you hand it a role-level task rather than an instruction-level command.\nIn practice, I\u0026rsquo;ve now settled into a rough division of labor: use Opus for ideation, planning, and design direction; use Codex for engineering implementation and shipping code; use Gemini for quickly validating an idea. Rather than agonizing over which model is \u0026ldquo;best overall.\u0026rdquo;\n7. Closing thoughts # For this blog redesign, I\u0026rsquo;ve already gone ahead and shipped one of the three proposals. Not because it was perfect, but because it was extremely low-cost, highly efficient, clearly reasoned, and professional enough.\nIf AI used to be mostly about helping you get work done, it\u0026rsquo;s now starting to participate in the judgment calls and the design decisions themselves. That might be the part of this next generation of models that\u0026rsquo;s actually worth paying attention to.\n","date":"2026-01-07","externalUrl":null,"permalink":"/posts/ai-blog-redesign-model-comparison/","section":"Blog","summary":"Three models, one vague brief to redesign a blog’s UX, visuals, and SEO — and a clear split between models that plan first and models that just ship a result.","title":"When AI Starts Thinking Like a Designer: Comparing Gemini, Claude Opus, and GPT on a Real Redesign","type":"posts"},{"content":"Jensen Huang\u0026rsquo;s keynote at CES 2026 carried a lot of information, but what\u0026rsquo;s actually worth chewing over isn\u0026rsquo;t some flashy compute-performance slide — it\u0026rsquo;s that NVIDIA is systematically reshaping the economics of AI.\nIf you only take away \u0026ldquo;Blackwell is 10x stronger than Hopper, and Rubin is another 10x on top of that,\u0026rdquo; that\u0026rsquo;s just the surface. The real core is that AI is shifting from a compute race to a cost race.\n1. The shift from \u0026ldquo;performance\u0026rdquo; to \u0026ldquo;token cost\u0026rdquo; is a deliberate change in narrative # What Huang kept coming back to in this keynote wasn\u0026rsquo;t FLOPS — it was a metric that\u0026rsquo;s rarely been discussed publicly over the past year: the cost of generating a single token.\nUnder the Rubin architecture, token cost gets compressed to one-tenth of Blackwell\u0026rsquo;s.\nThat matters far more than \u0026ldquo;another generation of chips got stronger,\u0026rdquo; because better performance doesn\u0026rsquo;t equal commercial success, while lower cost equals expanded use cases. Only when token cost keeps falling can AI actually move from being a tool for a handful of big model companies to things like enterprise-wide deployment, AI agents running as a matter of course, long-context and real-time inference, and running multiple models in parallel or personalized to the user. In other words, this isn\u0026rsquo;t \u0026ldquo;stronger AI\u0026rdquo; — it\u0026rsquo;s cheaper, scalable AI.\n2. NVIDIA isn\u0026rsquo;t just \u0026ldquo;selling chips\u0026rdquo; anymore — it\u0026rsquo;s selling a complete system # Huang said it outright: NVIDIA is now building the entire system — AI is a full stack. That statement carries at least three layers of meaning.\nNVIDIA is compressing the middle of the supply chain. The old path used to be: GPU → OEM → cloud provider → developer/enterprise. It\u0026rsquo;s becoming: GPU + networking + storage + software + scheduling + reference architecture → customer directly. NVIDIA isn\u0026rsquo;t content staying in the middle of the chain anymore — it\u0026rsquo;s raising ecosystem lock-in and switching costs.\nNVIDIA\u0026rsquo;s real competitors have changed. It\u0026rsquo;s no longer just facing traditional chip rivals — it\u0026rsquo;s up against cloud providers\u0026rsquo; own in-house compute stacks, model companies\u0026rsquo; hardware-software co-design, and national-level, self-sufficient compute programs. As NVIDIA goes full-stack, the entire industry is forced to ask itself: should we reduce how systemically dependent we are on NVIDIA?\nFull-stack is a moat, but it\u0026rsquo;s also an amplifier of systemic risk. The upside is obvious — stronger ecosystem stickiness, deeper customer lock-in, bigger scale effects. But it also means heavier capital expenditure, stronger cyclical exposure, and a much bigger dependence on energy, policy, and the macro environment.\n3. The conditions for \u0026ldquo;10x times 10x\u0026rdquo; to hold up are actually quite demanding # For this logic to hold, several hidden assumptions all need to be true at once.\nAI demand has to keep expanding exponentially. Rubin\u0026rsquo;s whole premise rests on the assumption that AI training and inference demand will keep scaling up over the next 2–3 years. If enterprise ROI falls short of expectations, application-layer innovation slows down, or agents don\u0026rsquo;t take off the way people expect, then \u0026ldquo;10x more compute\u0026rdquo; could temporarily turn into structural oversupply.\nEnergy and data centers aren\u0026rsquo;t a side issue — they\u0026rsquo;re a core constraint. Huang repeatedly mentioning \u0026ldquo;energy efficiency\u0026rdquo; wasn\u0026rsquo;t just politeness. In the real world, the actual bottlenecks are power supply, data center approvals, energy prices, and policy constraints. If energy costs can\u0026rsquo;t fall in step, the whole token-cost argument weakens noticeably.\nSuccess itself is fueling a move away from NVIDIA. Once NVIDIA\u0026rsquo;s capabilities get strong enough, the industry\u0026rsquo;s natural response is for cloud providers to push harder on in-house development, for national systems to emphasize independence and self-sufficiency, and for model companies to optimize their own hardware-software co-design. That\u0026rsquo;s not a short-term negative — it\u0026rsquo;s a long-term, structural tug-of-war.\n4. Looking at the \u0026ldquo;material downgrade\u0026rdquo; rumors through this same lens # If you put CES\u0026rsquo;s messaging alongside the recent rumors about high-end PCB materials possibly getting \u0026ldquo;downgraded,\u0026rdquo; the two turn out to be logically consistent. If what NVIDIA keeps emphasizing is cost rather than the absolute limits of performance, then it\u0026rsquo;s not surprising if some high-end materials that overshoot on performance but underdeliver on value get systematically swapped out. That\u0026rsquo;s not necessarily bad news — it looks more like the natural result of system-level cost optimization.\n5. My take: this is a long-term logic, not a short-cycle safe bet # Putting it all together, my read is this: it\u0026rsquo;s a logically complete, clearly targeted, genuinely persuasive long-term industry narrative — one that\u0026rsquo;s trying to push AI from a capital-intensive race toward becoming universal infrastructure.\nBut it\u0026rsquo;s just as important to stay clear-eyed: this logic depends heavily on demand, energy, and policy all lining up together, and if any one key variable gets out of sync, the correction will be systemic too.\nNVIDIA is attempting something extremely difficult — and if it succeeds, something that will genuinely matter historically. This isn\u0026rsquo;t a short-term safe bet. It\u0026rsquo;s a long-term call that requires time, patience, and the ability to stomach volatility.\n","date":"2026-01-07","externalUrl":null,"permalink":"/posts/nvidia-ces-2026-token-economics/","section":"Blog","summary":"Jensen Huang’s keynote wasn’t really about FLOPS — it was about token cost, and NVIDIA quietly shifting from selling chips to selling the entire stack.","title":"Looking Again at NVIDIA's Real Logic After CES 2026","type":"posts"},{"content":"Today I tried out a new AI video workflow: first generate a set-environment image with an image-generation tool, then use Veo 3.1\u0026rsquo;s image-to-video bridge feature to generate a transition clip between two photos.\nHonestly, the final result still has room to improve on smoothness. The main reason is that Veo 3.1\u0026rsquo;s generation quota is pretty limited, so I couldn\u0026rsquo;t do repeated fine-tuning or re-rolls — I just had to accept a transition that came out a little rough around the edges.\nThat said, there\u0026rsquo;s no appreciation without comparison. Out of curiosity, I ran the exact same prompt through Jimeng (即梦), and the result nearly gave me a heart attack — the character\u0026rsquo;s head did a full 180-degree spin in the generated video. For a second it felt like something out of a horror movie; watching that late at night genuinely startled me.\nTakeaway: at this point, Veo 3.1 is clearly more reliable than Jimeng when it comes to understanding physics and human anatomy. It burns through GPU time and quota fast, but at least it doesn\u0026rsquo;t turn your film set into a horror movie.\n","date":"2025-12-18","externalUrl":null,"permalink":"/posts/veo-3-1-vs-jimeng-video-test/","section":"Blog","summary":"The same prompt, two video models — one produced a slightly rough transition clip, the other gave a character a 180-degree head spin.","title":"Veo 3.1 vs. Jimeng: Stress-Testing an Image-to-Video AI Workflow","type":"posts"},{"content":"I\u0026rsquo;d never been happy with how weak macOS\u0026rsquo;s native input method is at word prediction, and I wasn\u0026rsquo;t thrilled about putting up with the bloat and privacy trade-offs of some of the big commercial input methods either, so I kept looking for the \u0026ldquo;perfect\u0026rdquo; replacement.\nI recently finally got Squirrel (鼠须管) properly set up, paired with Rime-Ice (雾凇拼音) — a scheme with an excellent reputation in the community right now.\nAfter using it for a while, I have to say: this combination is genuinely great.\nWhy bother with Rime? # Rime (the Rime Input Method Engine) has always had legendary status in more technical circles. Its strengths are obvious:\nGenuine privacy — fully open source, fully local, never uploads user data anywhere. A very high ceiling — deeply customizable, you can shape it however you want. Very fast response — lightweight, with essentially no lag. But its drawbacks used to be just as off-putting: the barrier to entry was steep. Installing Rime used to feel like getting a computer with no OS on it — you had to write your own config files (YAML) and go find your own dictionary. For an average user, that\u0026rsquo;s a nightmare.\nThen I found Rime-Ice.\nWhat does Rime-Ice actually change? # If Squirrel/Rime is a powerful game engine, Rime-Ice is a beautifully optimized AAA game built on top of it.\nRime-Ice is an actively maintained open-source config scheme on GitHub. The author keeps the dictionary continuously updated, covering both everyday language and a large amount of current internet slang. Its real significance is that it turns Rime into a modern, out-of-the-box input method.\nWith this combination now set up, the improvements I\u0026rsquo;ve actually noticed are:\n1. A genuinely smooth typing feel. This is the most noticeable part. Squirrel is written in native code, and on macOS it\u0026rsquo;s extremely smooth — no stutter at all. That precise, responsive feel is something a lot of input methods built on Electron or other frameworks just can\u0026rsquo;t match.\n2. A local dictionary that actually keeps up. The biggest pain point with plain Rime used to be a dictionary that felt outdated. With Rime-Ice layered on, since it maintains a huge dictionary pulling from sources like Wikipedia and Moegirlpedia, the hit rate is impressively high whether I\u0026rsquo;m typing long sentences or current slang. Its whole-sentence smart correction is genuinely useful too — it can fix a mistyped pinyin sequence on the fly, so typing speed doesn\u0026rsquo;t suffer just because it\u0026rsquo;s a \u0026ldquo;local\u0026rdquo; input method.\n3. Rich features and good looks. Rime-Ice comes bundled with a lot of practical features, so I don\u0026rsquo;t have to dig through config files myself: selecting a single character out of a longer word with bracket keys, quick shortcuts for emoji and symbols, and automatic spacing between Chinese and English text — a small detail, but a genuine relief if that kind of thing bothers you.\nPaired with a skin either bundled with Squirrel or made by the community, the input interface now looks clean and modern — every bit as polished as any commercial input method.\n","date":"2025-12-18","externalUrl":null,"permalink":"/posts/squirrel-rime-ice-input-method/","section":"Blog","summary":"Native-smooth typing, a dictionary that actually keeps up with slang, and built-in emoji/spacing niceties — Rime finally out of the box.","title":"Setting Up Squirrel + Rime-Ice: My New macOS Input Method","type":"posts"},{"content":"","date":"2025-12-18","externalUrl":null,"permalink":"/tags/apple/","section":"Tags","summary":"","title":"Apple","type":"tags"},{"content":" Background # A lot of software is priced much better in the Indian App Store, so I looked into setting up an Indian Apple ID. What pushed me to actually do it was needing to make a cross-region payment through Google recently.\nMy first thought was simple enough: just register an Indian account directly, right? Turns out it wasn\u0026rsquo;t nearly that straightforward.\nRegistering an Indian Apple ID directly isn\u0026rsquo;t actually \u0026ldquo;direct\u0026rdquo; # Trying to register an Indian Apple ID outright turns out to demand a fairly specific network setup and device configuration. Whether I tried it in a browser or on my phone, the registration kept getting stuck.\n\u0026ldquo;We are unable to create your account at this time.\u0026rdquo;\nAfter a few attempts, it becomes clear the problem isn\u0026rsquo;t necessarily anything you\u0026rsquo;re doing wrong — the registration flow itself has fairly aggressive risk controls.\nA more reliable approach: start with a Chinese account first # I ended up switching to a more reliable method. Instead of going straight at the Indian region, I first registered a Chinese Apple ID with no proxy or VPN involved at all. That step went smoothly:\nNo special network setup needed Verification worked fine with a domestic phone number The registration flow basically never got blocked The whole process felt just like registering an Apple ID used to feel, years ago.\nAfter the domestic account is set up, switch regions # Once the Chinese account was registered, I turned the proxy back on and logged into that already-created Apple ID.\nBefore making the switch, there are a few things that have to be sorted out first, or the system just refuses the change outright:\nThe account can\u0026rsquo;t have any balance (including App Store balance) There can\u0026rsquo;t be any active subscriptions (Apple Music, iCloud, TV+, etc.) Family Sharing needs to be turned off first It\u0026rsquo;s best to log into the App Store on an iPhone or iPad beforehand, to avoid glitches on the web version The steps go roughly like this:\nOpen the App Store Tap the profile icon in the top corner to open the account page Go to Apple ID → Country or Region Tap Change Country or Region Select India from the list The system then asks for Indian billing information — I looked up an address in India on Google Maps and filled that in.\nAfter that, the Indian-region services become available. Worth noting: you can only join an existing Indian family group — you can\u0026rsquo;t make purchases directly yourself, since an Indian Apple ID has to be tied to a local bank card.\n","date":"2025-12-18","externalUrl":null,"permalink":"/posts/india-apple-id-the-roundabout-way/","section":"Blog","summary":"Registering an Indian Apple ID directly kept failing — the fix was registering a Chinese account with no proxy first, then switching region afterward.","title":"Registering an Indian Apple ID, the Roundabout Way","type":"posts"},{"content":"","date":"2025-12-18","externalUrl":null,"permalink":"/tags/tips/","section":"Tags","summary":"","title":"Tips","type":"tags"},{"content":"Mole is an open-source macOS command-line cleanup tool built for clearing out system junk, leftover app files, and various caches.\nIt doesn\u0026rsquo;t aim for \u0026ldquo;one-click clean everything automatically\u0026rdquo; — instead it\u0026rsquo;s built around being visible, controllable, and safe, so you always know exactly what\u0026rsquo;s on your system and what\u0026rsquo;s being deleted.\nProject page: github.com/tw93/Mole\nWhat can Mole do? # Mole keeps its feature set tightly focused, mainly around:\n1. Cleaning up leftover app files. Uninstalling an app on macOS often leaves behind caches, config files, and logs — things like Application Support, Caches, Preferences, and Logs. Mole helps you find these leftovers, so you don\u0026rsquo;t end up in the situation where you deleted the app but never got the space back.\n2. Cleaning system and developer caches. Mole supports clearing several common cache sources: system caches, Homebrew\u0026rsquo;s cache, Xcode derived data, and caches from other common dev tools. It\u0026rsquo;s particularly handy for developers, since it can free up a large amount of disk space quickly.\n3. Checking disk usage. Mole can help analyze what\u0026rsquo;s actually taking up space, so you find the directories and files that matter instead of cleaning blindly.\nInstalling Mole # Option 1: Homebrew (recommended)\nIf you already have Homebrew installed, it\u0026rsquo;s a single command:\nbrew install tw93/tap/mole Once installed, the mole command is available straight from the terminal.\nUsing Mole # 1. Check the help output\nmole -h This shows every feature and command option Mole supports.\n2. Scan for what can be cleaned\nmole scan This scans the system for common caches and leftover files and lists the results — it doesn\u0026rsquo;t delete anything on its own.\n3. Run the actual cleanup\nBased on the scan results, run the matching cleanup command. Mole typically confirms before it deletes anything, to avoid removing something important by accident.\nA few tips # Scan before you clean — don\u0026rsquo;t run cleanup commands blindly. Running it periodically is enough; there\u0026rsquo;s no need to run it constantly. And if you\u0026rsquo;re not sure what a file is for, check before deleting it.\nWrap-up # Mole is a lightweight, restrained, trustworthy macOS cleanup tool: it doesn\u0026rsquo;t run in the background, it doesn\u0026rsquo;t delete things carelessly, every action is transparent and under your control, and it\u0026rsquo;s fully open source. If you\u0026rsquo;re comfortable in the terminal and want a cleaner way to manage space on macOS, it\u0026rsquo;s well worth trying.\n","date":"2025-12-16","externalUrl":null,"permalink":"/posts/mole-macos-cleanup-tool/","section":"Blog","summary":"A restrained, transparent command-line cleaner for leftover app files and system/dev caches — scan first, then clean, nothing automatic or hidden.","title":"A Simple, Solid macOS Cleanup Tool: Mole","type":"posts"},{"content":"","date":"2025-08-08","externalUrl":null,"permalink":"/tags/chatgpt/","section":"Tags","summary":"","title":"ChatGPT","type":"tags"},{"content":"AI isn\u0026rsquo;t just a lab concept anymore — it\u0026rsquo;s genuinely part of daily life now. Today, right as I got my hands on GPT-5, I also started experimenting with using AI to help with some of my own regular investing research.\nWhat can it actually do? # ChatGPT\u0026rsquo;s range is broad — it helps out with both work and everyday life:\nTechnical assistant — helping me work through programming problems, network configuration, data analysis, even writing solid code. Language tutor — when I\u0026rsquo;m learning a foreign language, it breaks down grammar, explains vocabulary, corrects pronunciation, and generates practice exercises. Creative partner — whether it\u0026rsquo;s a blog post, a video script, or putting together a slide deck, it can quickly produce something well-structured and coherent. Research assistant — when I\u0026rsquo;m looking into ETFs or retirement planning, it helps me pull information, analyze data, and compare options. Everyday helper — travel planning, food suggestions, study plans, day-to-day questions — I turn to it for all of it. What stands out about it # If I had to sum up ChatGPT in a few words: it\u0026rsquo;s an omnivore for information — it can talk about almost anything, from quantum physics to latte art. It\u0026rsquo;s structure-obsessed, always able to break something complicated down into a clear framework. It\u0026rsquo;s detail-oriented — the more context I give it, the better it tailors what it gives back. And its tone is adjustable — it can be serious and professional, or relaxed and funny, depending on what I need.\nIn my day-to-day work and life, ChatGPT has become something I genuinely rely on. From handling fiddly technical details to turning scattered thoughts into a finished piece of writing, it consistently saves me real time and effort.\nWhere this could go # AI is really just getting started. I expect tools like ChatGPT to keep getting smarter and closer to how people actually think, taking on more creative work and eventually helping with problems we can\u0026rsquo;t even picture yet. For me, it\u0026rsquo;s stopped being just a piece of software — it\u0026rsquo;s more like a friend working alongside me in the digital world.\n","date":"2025-08-08","externalUrl":null,"permalink":"/posts/gpt-5-first-day-impressions/","section":"Blog","summary":"From debugging code to language practice to research, ChatGPT has quietly become a daily-use tool rather than a novelty.","title":"First Day With GPT-5, and How AI Has Changed My Daily Life","type":"posts"},{"content":"","date":"2025-07-09","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"I recently deployed Podsync on my NAS — a tool that turns YouTube channels or RSS feeds into podcast subscription URLs, so I can subscribe and listen offline through a podcast client. After setting it up, though, I noticed a problem: its web server was exposing the entire contents of its output directory.\nVisiting Podsync\u0026rsquo;s address in a browser showed something like:\nIndex of / - index.xml - feed.json - video001.mp4 That meant anyone who knew the URL could see — and even download — every file it had synced. For anything reachable on the open internet, that\u0026rsquo;s a real security problem.\nA simple, effective fix: add a default index.html # The fix turned out to be simple: drop an index.html file into Podsync\u0026rsquo;s output directory. The web server will load that file first, which replaces the default directory listing.\nHere\u0026rsquo;s an example file:\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Private Podsync Service\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to Podsync\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;This page is for a private subscription only — directory browsing is disabled.\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Just drop that HTML file into Podsync\u0026rsquo;s output directory (something like /volume1/docker/podsync/), refresh the browser, and instead of an \u0026ldquo;Index of /\u0026rdquo; listing, you\u0026rsquo;ll land on your own custom page.\nThe result # With that in place, anyone visiting the Podsync address now only sees the page I set up — no file listing at all. For anyone running Podsync on a home network or exposing it to the public internet, this is a genuinely useful bit of hardening.\n","date":"2025-07-09","externalUrl":null,"permalink":"/posts/podsync-directory-exposure-fix/","section":"Blog","summary":"A missing index.html meant Podsync’s web server was serving up a full directory listing of everything it had synced — here’s the one-file fix.","title":"Fixing an Exposed Directory Listing in My Podsync Docker Service","type":"posts"},{"content":"Getting a clear picture of how your assets are made up and distributed is the first step in managing your finances. This time I built a web app meant to give you a real-time, auto-refreshing asset dashboard — one that shows an overview at a glance while staying easy to maintain and update.\nThis page is the sixth project in my \u0026ldquo;100 practical websites and apps\u0026rdquo; series, built around one core goal: asset visualization, live exchange rates, and flexible editing.\nThe main features # 1. An asset overview across multiple currencies. The system supports entering assets in different currencies, automatically pulls the exchange rate in the background, and converts everything into both USD and RMB totals, so it\u0026rsquo;s easy to see the overall picture however you want to look at it.\n2. Charts showing asset breakdown. There are two ways to visualize the structure of your assets: by category (stocks, funds, cash, bonds, and so on) and by currency, which helps spot currency-exposure risk.\n3. Editable asset entries. Asset tracking can\u0026rsquo;t just be a static display — it needs to flex. This page lets you adjust existing amounts, add new asset categories, or remove categories you no longer hold. Clicking \u0026ldquo;Edit Assets\u0026rdquo; on the homepage takes you into an editing view where you can make changes in real time.\n4. Live exchange rates, cached for speed. For calculating and displaying totals, the app pulls in three key exchange rates: USD/RMB, EUR/USD, and AUD/RMB. To avoid hammering the API and slowing the page down, the rates are cached — staying fresh enough while keeping the page snappy.\n","date":"2025-07-09","externalUrl":null,"permalink":"/posts/realtime-asset-dashboard/","section":"Blog","summary":"A small web app for tracking multi-currency assets, with live exchange rates, category and currency breakdown charts, and inline editing.","title":"100 Projects Before I Retire, #6: A Real-Time Refreshing Asset Dashboard","type":"posts"},{"content":"","date":"2025-07-09","externalUrl":null,"permalink":"/tags/finance-tools/","section":"Tags","summary":"","title":"Finance Tools","type":"tags"},{"content":"","date":"2025-07-09","externalUrl":null,"permalink":"/tags/web-dev/","section":"Tags","summary":"","title":"Web Dev","type":"tags"},{"content":"When I went looking for a decent language-learning tool for my kids, most of what I found was either a bare video player or a bloated \u0026ldquo;learning app\u0026rdquo; packed with ads and too complicated for a young child to use. So I decided to build my own — a clean, focused native Android app that loads curated foreign-language nursery rhymes and fairy tales through a WebView, giving kids (and adults learning a language too) an immersive, controlled, ad-free environment to learn in.\nThe technical background # The app is built as a native Android app, with a stack that includes:\nKotlin — the modern, safe, concise language Android officially recommends. Android Jetpack components: androidx.appcompat.app.AppCompatActivity for compatibility with older system versions androidx.swiperefreshlayout.widget.SwipeRefreshLayout for pull-to-refresh on the embedded pages android.webkit.WebView as the embedded browser that displays the nursery-rhyme and fairy-tale content Why go native instead of cross-platform? # Cross-platform frameworks like Flutter and React Native have been getting a lot of attention lately, but I still went native, mainly because:\nBetter performance — the WebView starts up fast, and combined with native UI components, the interface responds quickly. Deeper system integration — down the line I can add background playback, speech recognition, keep-screen-on, eye-comfort mode, and more. More UI customization — I can heavily tailor content display, safety prompts, and parental controls. Content: WebView plus curated foreign-language resources # At its core, the app loads external resources through a WebView — things like kid-friendly foreign-language nursery-rhyme sites, multilingual versions of classic fairy tales, bilingual picture-book pages I\u0026rsquo;ve put together or collaborated on, and it can be extended to load local HTML pages or offline bundles.\nThis approach keeps the flexibility of web-based content management while avoiding the usual \u0026ldquo;nested webpage plus ads plus no feedback\u0026rdquo; experience. It makes it easy to pair English nursery rhymes with Chinese subtitles, Portuguese fairy tales with narrated audio, tap-a-word popups for translations and example sentences (doable later via a JSBridge), and offline caching so stories are available on the go.\nUpsides and challenges # Upsides: strong performance and smooth animation; compatible across Android versions; a manageable install size; a flexible, easily customizable UI; and it can plug straight into native features like WeChat sharing, parental controls, and speech recognition.\nChallenges: a WebView still doesn\u0026rsquo;t quite match native components, especially for gestures and load speed; the web content itself needs to be kept under control to prevent unwanted redirects or inappropriate content from slipping in; and the app is still fairly bare — no favorites, no queue management yet — things I\u0026rsquo;ll fill in as I keep using it.\n","date":"2025-06-17","externalUrl":null,"permalink":"/posts/android-language-learning-app/","section":"Blog","summary":"Frustrated by bloated, ad-filled kids’ language apps, I built a clean native Android app that loads curated foreign-language rhymes and stories through a WebView.","title":"100 Projects Before I Retire, #5: A Language-Learning App Built on Nursery Rhymes and Fairy Tales","type":"posts"},{"content":"","date":"2025-06-17","externalUrl":null,"permalink":"/tags/android/","section":"Tags","summary":"","title":"Android","type":"tags"},{"content":"","date":"2025-06-17","externalUrl":null,"permalink":"/tags/kotlin/","section":"Tags","summary":"","title":"Kotlin","type":"tags"},{"content":"As part of my \u0026ldquo;100 projects before I retire\u0026rdquo; plan, I recently finished project #4: setting up a network storage service. This time I picked up a very cheap server — modest specs, just a 1TB disk and 1GB of RAM — but its one advantage was plenty of storage space. So instead of hosting a website on it, I decided to turn it into an object storage service.\nWhy bother with this? # As my projects have piled up, so have the images, audio, and video files that go with them. Keeping all of that on a website server gets harder to manage over time, and access gets less reliable. Running a dedicated object storage service instead brings a few real benefits:\nCentralized resource management — images, audio, and video all live in one place, which makes them easier to reference and maintain. More reliable access — paired with a CDN, file loading speeds up considerably. External links and access control — you can generate temporary access links per file, which is both convenient and safer. Good compatibility — this system speaks the same API as Amazon S3, so migrating or upgrading later should be seamless. What I used # I went with an open-source project called MinIO — a very lightweight object storage system with a clean interface and practical features, and it doesn\u0026rsquo;t ask much of the server it runs on, which made it a good fit for this particular old, underpowered machine.\nPaired with a domain and encrypted access through Cloudflare, I ended up with an object storage platform that\u0026rsquo;s both secure and easy to use. Now, whether I\u0026rsquo;m uploading files, browsing what\u0026rsquo;s stored, or pulling images into a website, it\u0026rsquo;s all pretty painless.\nHow it\u0026rsquo;s working out # Several of my projects are now hooked up to this storage system: images in blog posts now load through the storage service and load faster, assets for video-narration projects are stored in one place instead of scattered around, and family videos and travel photos now have a private cloud backup space too.\n","date":"2025-06-16","externalUrl":null,"permalink":"/posts/minio-object-storage-project/","section":"Blog","summary":"Turning a cheap, low-spec server into a dedicated S3-compatible object store for images, audio, and video, instead of hosting another website on it.","title":"100 Projects Before I Retire, #4: A MinIO Object Storage Service","type":"posts"},{"content":"","date":"2025-06-16","externalUrl":null,"permalink":"/tags/minio/","section":"Tags","summary":"","title":"MinIO","type":"tags"},{"content":"I was recently trying out a system built by another team — you drag and drop a file to upload it, it runs OCR automatically, and then you can drag in a large-model node to intelligently organize the OCR results. That got me thinking: does a system like this — one that chains tools together to get a job done — actually count as an \u0026ldquo;agent\u0026rdquo;? What is an agent, and what\u0026rsquo;s really different between an agent and a workflow?\nWhat is a workflow? # Here\u0026rsquo;s an everyday example — an expense reimbursement process at a company:\nAn employee submits a receipt The system checks whether the receipt is compliant Above a certain amount, it needs manager approval Once approved, the payment goes out This is a standard, predefined process — every step follows a rule that was written in ahead of time (amounts over a certain threshold need manager approval, say). The process doesn\u0026rsquo;t change just because the receipt has \u0026ldquo;urgent\u0026rdquo; written on it.\nThat\u0026rsquo;s a textbook workflow system. Workflows are good at handling deterministic processes, like approval chains, data-sync pipelines, or order-processing pipelines.\nWhat is an agent? # Here\u0026rsquo;s another example. You ask a smart customer-service bot: \u0026ldquo;The blue wireless earbuds I bought last week are broken — how do I request a replacement?\u0026rdquo;\nFor the bot to answer, it needs to understand your question (a support issue? a replacement request?), look up the relevant policy, check your account details to see whether you qualify, and generate a reply tailored to your specific situation. Here the bot is doing a lot of understanding, reasoning, and content generation — the process isn\u0026rsquo;t hard-coded, and the answer can genuinely differ each time depending on context and judgment.\nThat\u0026rsquo;s the classic use case for an agent. Agents are good at handling tasks with uncertainty, like smart Q\u0026amp;A, organizing content, recommendations, or managing complex conversations.\nDoes chaining tools together count as an agent? # Back to the example at the start: drag-and-drop a file → run OCR → drag in a large-model node to organize the results. Does that count as an agent?\nBreaking it down: OCR is a fixed process — that\u0026rsquo;s a workflow node. The large-model step dynamically understands the text and generates new content based on your prompt and context — that\u0026rsquo;s agent behavior. So taken as a whole, this system is really a workflow-plus-agent hybrid: the process itself is a workflow, and the intelligence comes from the agent piece.\nA simple analogy: a workflow is like an assembly line — fixed steps, items processed station by station. An agent is like a clever worker at one of those stations, handling things flexibly based on the actual situation.\nA quick way to tell them apart # Three questions settle it:\nIs the process fixed? Yes → workflow. No, it needs to understand context → agent. Are the branches decided by if-else rules? Yes → workflow. No, by understanding/reasoning → agent. Is the output predictable? Yes → workflow. No, it could reasonably differ each time → agent. Scenario Workflow Agent Expense approval Yes No Smart customer-service Q\u0026amp;A No Yes Auto-forwarding email Yes No Smart email summarization No Yes Upload → OCR → save to database Yes No Upload → OCR → smart summary → send report Hybrid Hybrid Takeaway # Workflows are good at running deterministic, rule-based processes — like a robot doing physical labor. Agents are good at understanding, reasoning, and generating — like an assistant with an actual brain. Whether a system built by chaining tools together counts as an agent comes down to whether understanding and dynamic decision-making are actually involved.\n","date":"2025-06-13","externalUrl":null,"permalink":"/posts/agents-vs-workflows/","section":"Blog","summary":"A drag-and-drop OCR-plus-LLM tool got me thinking about where a workflow ends and an agent begins — with a simple three-question test.","title":"Agents vs. Workflows: What's the Actual Difference?","type":"posts"},{"content":"My parents had been using a router their ISP handed out for free, which — as it turns out — was really just an edge node for a PCDN storage scheme, and carriers are cracking down on that right now. Rather than risk causing them any trouble, I figured I\u0026rsquo;d just swap it out. I happened to have a spare Xiaomi router lying around, one I\u0026rsquo;d previously set up in AP (wireless access point) mode to extend Wi-Fi from the main router. It seemed like a natural fit to bring it to my parents\u0026rsquo; place and use it as the main router, plugged straight into the fiber modem. What I didn\u0026rsquo;t expect was forgetting to switch it back to router mode first, which cost me a bit of pointless troubleshooting — so I figured I\u0026rsquo;d write the process down for future reference, in case it helps anyone in the same situation.\nThe problem # The Xiaomi router was still configured in AP mode (bridge mode). After connecting it to the fiber modem at my parents\u0026rsquo; place, my phone connected to the Wi-Fi but got an IP in the 192.168.1.x range (assigned by the modem\u0026rsquo;s own default DHCP). I couldn\u0026rsquo;t reach 192.168.31.1, meaning the Xiaomi router hadn\u0026rsquo;t taken over DHCP and wasn\u0026rsquo;t in \u0026ldquo;router mode.\u0026rdquo; The router\u0026rsquo;s admin panel showed a bare-bones set of options — no dial-up or port-forwarding settings, none of the core router functionality. Figuring out what happened # The telltale signs of AP mode. The Xiaomi router had its own DHCP turned off and was only acting as a wireless extender — which is why my phone picked up a 192.168.1.x address from the modem instead. Why did it switch to AP mode in the first place? The first time a Xiaomi router connects to an upstream network, it checks whether a DHCP service already exists on it. If it detects one (say, the modem\u0026rsquo;s own router function hadn\u0026rsquo;t been disabled), it automatically suggests bridge mode — and once you confirm that, it sticks even after a restart. How to switch it back to router mode. Go into the Xiaomi Wi-Fi app or the admin web page and manually switch the \u0026ldquo;working mode\u0026rdquo; back to \u0026ldquo;router mode.\u0026rdquo; That re-enables DHCP, handing control of the home network back to the Xiaomi router. The steps # 1. Physical reset (recommended)\nIf you\u0026rsquo;re worried about leftover configuration, it\u0026rsquo;s simplest to just reset it: find the reset pinhole on the back of the router, hold it for 5–10 seconds while powered on, and release once the indicator light starts blinking rapidly. The router resets to factory defaults and reboots into the setup wizard.\n2. Setting it to \u0026ldquo;router mode\u0026rdquo;\nConnect a phone or computer to the router\u0026rsquo;s default Wi-Fi network (something like Xiaomi_xxx), then visit http://miwifi.com or http://192.168.31.1 in a browser and follow the setup wizard: choose router mode, choose how it connects to the internet (my parents\u0026rsquo; setup uses PPPoE dial-up through the fiber modem, so I entered the PPPoE username/password), set a new Wi-Fi name and password, and set an admin password. Once done, the router restarts and hands out 192.168.31.x addresses — router mode is properly active.\n3. Avoiding an accidental switch back to AP mode\nCheck whether the fiber modem\u0026rsquo;s own settings need adjusting (whether its routing function needs to be disabled, or set to bridge mode). If you\u0026rsquo;re not planning to touch the modem\u0026rsquo;s settings, make sure to actively choose \u0026ldquo;router mode\u0026rdquo; the first time you set up the Xiaomi router, so it doesn\u0026rsquo;t auto-suggest AP mode instead.\nThe result # The Xiaomi router now successfully manages the home network, DHCP works properly handing out 192.168.31.x addresses, the Wi-Fi signal is noticeably better than the old ISP-freebie router, and going forward it\u0026rsquo;ll be easy to set up port forwarding and other local-network features.\n","date":"2025-06-11","externalUrl":null,"permalink":"/posts/xiaomi-router-ap-to-router-mode/","section":"Blog","summary":"A router that stubbornly stayed in AP mode after being moved, and the DHCP debugging it took to switch it back.","title":"Swapping My Parents' ISP Router for a Spare Xiaomi Router (an AP-to-Router-Mode Story)","type":"posts"},{"content":"While cleaning my room recently, I stumbled across a GL.iNet GL-SFT1200 (Opal) router I\u0026rsquo;d shelved a while back — the web interface was painfully slow, and it got even worse after I flashed OpenWrt onto it. On a whim, I decided to try rescuing it through the official web-based upgrade path, and figured I\u0026rsquo;d write down the process and how it went, in case it\u0026rsquo;s useful to anyone with a similar device.\nWhy it got shelved # I originally bought the GL-SFT1200 for portable VPN use and setting up small temporary networks, but in practice:\nThe web interface was extremely slow, and switching between admin pages would nearly freeze it. Flashing OpenWrt 18.06 onto it made things worse — LuCI pages lagged badly. Anything with a slightly more complex plugin (SSR Plus+, Passwall) basically couldn\u0026rsquo;t run smoothly. Eventually I just shelved it in a drawer. Why give it another shot # GL.iNet\u0026rsquo;s official and community firmware support has gotten noticeably better since then — the newer OpenWrt 4.x official firmware is clearly more optimized. The web interface supports upgrading directly, with a low barrier to entry, so it seemed worth testing whether it would actually fix the performance. Why not take the chance to upgrade and see if it comes back to life? Getting ready to upgrade # 1. Picking the official web-upgrade path\nI went with GL.iNet\u0026rsquo;s official web-interface upgrade — a friendly process that doesn\u0026rsquo;t require messing with U-Boot mode, so it\u0026rsquo;s fine for everyday users:\nOpen the admin panel at http://192.168.8.1 → System → Firmware Upgrade. Upload the official .tar package directly through the manual-upload flow. 2. Things to watch out for during the upgrade\nAfter trying it out, I found:\nYou can\u0026rsquo;t jump straight to the newest version (4.3.25) — uploading the new firmware directly just fails. You need to upgrade to an intermediate version first (I used 4.3.19 as a stepping stone). Once that flashes successfully, run the web upgrade a second time to land on the latest 4.3.25. This \u0026ldquo;upgrade twice\u0026rdquo; approach worked around the firmware compatibility issue and avoided having to fuss with U-Boot flashing — the whole thing went very smoothly.\n3. Results after upgrading\nIt restarted automatically once the upgrade finished. The system reset to defaults, and the admin panel\u0026rsquo;s responsiveness noticeably improved. The GL.iNet UI and LuCI pages that used to lag are now snappy to switch between and load. Performance before and after # What Before (OpenWrt 18.06) After (4.3.25) Web interface load time 10+ seconds, sometimes freezing 1–2 seconds, responsive Plugin management Plugins often hung or failed Plugins load normally Basic Wi-Fi performance Unstable More stable, slightly better wall penetration System load Frequently maxed out Stays low The hardware is still limited compared to a proper software router (x86 or an ARM SBC), but as a portable backup router or a household IoT gateway, it\u0026rsquo;s now completely serviceable.\n","date":"2025-06-11","externalUrl":null,"permalink":"/posts/gl-sft1200-router-reflash/","section":"Blog","summary":"A painfully slow GL.iNet Opal, rescued from a drawer with a two-step official firmware upgrade instead of a U-Boot flash.","title":"Reviving a Shelved Router: Reflashing a GL-SFT1200","type":"posts"},{"content":"AI big-model era, a lot of companies are exploring LLM-plus-tools setups to make customer service more efficient. Today I\u0026rsquo;ll walk through a typical scenario — telecom customer service — and show how to quickly put together a multi-purpose smart customer-service agent using the Qwen-Agent framework. Honestly, a good chunk of this I was learning as I built it.\nThe scenario # Common things customers ask about:\nChecking their phone bill Figuring out why service was suspended Diagnosing network issues Checking what\u0026rsquo;s included in their plan Checking data usage Checking the status of a service request The question is: how do you support all these dynamic lookups elegantly?\nSystem design # The core idea is simple: the user asks a question → the agent uses the LLM to understand intent → it picks the right Tool (function) to call → the Tool runs and pulls real data → the model turns that into a natural-language reply.\nWhat\u0026rsquo;s a Tool, and why not just use a knowledge base? A lot of people assume everything should go into a knowledge base. In practice, dynamic real-time data is a better fit for a Tool, while static rules are a better fit for a knowledge base.\nWrap-up # With Qwen-Agent\u0026rsquo;s design, you get: each business function split out into its own independent Tool; the model deciding intelligently which Tool to call; static rules falling back to the knowledge base; no manually hard-coded if-else chains; and a system that can scale up to dozens or hundreds of query types without much extra effort.\nIn one line: the LLM is the brain, Tools provide the business capability, and the knowledge base fills in the explanations.\n","date":"2025-06-11","externalUrl":null,"permalink":"/posts/qwen-agent-smart-customer-service/","section":"Blog","summary":"A telecom customer-service example — using Tools for live data lookups and a knowledge base for static rules, with an LLM deciding which to call.","title":"Building a Smart Customer-Service Agent with Qwen-Agent","type":"posts"},{"content":" Background # I\u0026rsquo;ve always written my posts in Markdown inside Obsidian, paired with a script that automatically publishes the content to my Typecho blog — a seamless writing-to-publishing pipeline. The whole flow looks like:\nWrite a .md file in Obsidian A script converts the .md into HTML (or a content fragment) and publishes it to Typecho through an API Once published, the homepage automatically shows the post along with its cover image Everything worked fine, until I noticed that some posts on the homepage had stopped showing their cover images, and the layout was breaking in places.\nDigging in # After some investigation, I traced it to this: whenever a Markdown file in Obsidian had a video link embedded (a .mov file, for instance), the logic that auto-extracts a cover image would mistakenly treat the .mov as if it were an image. The homepage template would generate \u0026lt;img src=\u0026quot;xxx.mov\u0026quot; /\u0026gt;, which is invalid HTML, and the cover image would fail to load.\nRoot cause # The theme\u0026rsquo;s cover-image-extraction function just grabbed the src of the first \u0026lt;img\u0026gt; tag it found, without checking whether that source was actually an image format.\nThe fix # I rewrote the function to be more robust: it now walks through every \u0026lt;img\u0026gt; tag\u0026rsquo;s src, skips anything that\u0026rsquo;s a .mov, .mp4, or other non-image format, and only returns once it hits the first genuine image URL.\nResult # After the fix, the homepage went back to normal — it no longer mistakes a video link for an image, and correctly skips past any video links to find the actual next image.\n","date":"2025-06-10","externalUrl":null,"permalink":"/posts/obsidian-typecho-video-cover-bug/","section":"Blog","summary":"A cover-image extractor that grabbed the first tag without checking it was actually an image — and broke the homepage whenever a post embedded video.","title":"Debugging a Broken Homepage: When a .mov File Got Treated as a Cover Image","type":"posts"},{"content":"Tonight I tried out Google\u0026rsquo;s newly released video-generation AI. Just like I guessed a few days ago, it\u0026rsquo;s already been rolled down into the Pro plan.\nEvery AI-generated clip comes out at eight seconds long. I spent tonight really putting it through its paces.\nThe most obvious takeaway: eight seconds from the Pro tier is still too short. A lot of scenes don\u0026rsquo;t even have time to set up properly before the clip ends. That said, what actually makes it into those eight seconds is still fairly rich.\nBut there\u0026rsquo;s one clear problem: Chinese-language support isn\u0026rsquo;t great. Chinese keywords I typed in would either get read as English or just dropped entirely.\nIf I forced it to use Chinese text, I\u0026rsquo;d often end up with garbled characters. Sticking to English text worked fine.\nOverall, that part of the experience wasn\u0026rsquo;t great. Also, if you don\u0026rsquo;t specify what a character in the scene looks like, it defaults to generating what looks like a Western news reporter.\nHopefully future versions improve on this — longer clips, and better Chinese-language support.\n","date":"2025-06-09","externalUrl":null,"permalink":"/posts/testing-google-video-model/","section":"Blog","summary":"Eight-second clips, decent visual richness, and Chinese text prompts that either get dropped or come out garbled.","title":"A Night With Google's New Video-Generation Model","type":"posts"},{"content":"","date":"2025-06-09","externalUrl":null,"permalink":"/tags/cursor/","section":"Tags","summary":"","title":"Cursor","type":"tags"},{"content":"Good news for programmers: this AI editor is giving away three months of membership for free.\nThe AI coding tool that\u0026rsquo;s been blowing up lately is running a promotion — you can apply for three months of membership completely free. I already signed up, and today I want to talk about just how good this editor actually is.\nIn short, it\u0026rsquo;s a serious, fully-featured smart code editor. Think of it as your familiar coding tool, with all the same workflows and habits you already know, except with intelligence baked in deep enough to genuinely accelerate how fast you write code.\nI\u0026rsquo;ve already claimed my three free months, so I get unrestricted access to everything it offers, including several of the mainstream models.\nIf you want to feel that same rush of faster, smoother coding, go find the sign-up link and grab it early while it\u0026rsquo;s still free.\n","date":"2025-06-09","externalUrl":null,"permalink":"/posts/cursor-three-months-pro-free/","section":"Blog","summary":"A quick look at why the AI coding editor everyone’s been talking about is worth the free trial.","title":"Cursor Is Giving Away Three Months of Pro","type":"posts"},{"content":"Starting today, I\u0026rsquo;m a Google AI Pro subscriber.\nRight after Google\u0026rsquo;s 2025 conference wrapped up, it felt like they\u0026rsquo;d rebuilt basically every one of their own products around AI.\nWith Google stepping up like that, honestly, it threw me for a loop. I used to be a committed OpenAI user. Now that both sides are pushing this hard, with each new feature more capable than the last, I\u0026rsquo;m genuinely torn on which one to actually use.\nThe catch is that Google\u0026rsquo;s newest features right now need their top-tier subscription — even at half price during the current promotion, the monthly fee still stings a bit.\nFor now I\u0026rsquo;m starting with the Pro tier. Things move fast enough in this space that some of those flashier features will probably trickle down to Pro before too long anyway.\n","date":"2025-06-09","externalUrl":null,"permalink":"/posts/subscribing-google-ai-pro/","section":"Blog","summary":"Google I/O 2025 rebuilt every product around AI — enough to pull a committed OpenAI user over to a Pro subscription.","title":"Starting Today, I'm a Google AI Pro Subscriber","type":"posts"},{"content":"Comparing Gemini and OpenAI: Gemini will happily generate several thousand words of text without holding back, while OpenAI is noticeably stingier by comparison. Still, ChatGPT\u0026rsquo;s image generation is strong enough that I can\u0026rsquo;t give it up — hand it a blurry photo of a painting on a wall, and it can read what\u0026rsquo;s in it and recreate a new piece on the same theme.\nIt recreated the style of a Chinese woodblock print, along with what the original piece was trying to express.\nAnd it\u0026rsquo;s genuinely fun to play with — the same subject can just as easily be switched over to a 3D clay-style rendering.\n","date":"2025-06-09","externalUrl":null,"permalink":"/posts/openai-clay-style-3d-art/","section":"Blog","summary":"Comparing Gemini’s and OpenAI’s image generation on the same blurry wall painting, then pushing the same subject into a 3D clay-render style.","title":"Clay-Style 3D Art, Made with OpenAI","type":"posts"},{"content":"Perplexity has apparently joined the AI subsidy wars too — they\u0026rsquo;re giving away a full year of Perplexity Pro (normally $200/year) for free. You can redeem it by entering the discount code at checkout: PPLXLIUMBLHOTVAJ2QI\nOne subscription gets you access to pretty much all of the mainstream AI models currently available, which stacks up well against competing offers. As always with free-trial subscriptions, I\u0026rsquo;d recommend turning off auto-renewal right after you redeem it, just to be safe.\n","date":"2025-06-09","externalUrl":null,"permalink":"/posts/perplexity-pro-free-year-code/","section":"Blog","summary":"Perplexity has joined the AI subsidy wars — a full year of Pro for free with a discount code, plus the usual advice to turn off auto-renewal.","title":"Perplexity Pro, Free for a Year","type":"posts"},{"content":"Today I tried out an open-source voice cloning project just to see how good the results would be.\nDeploying this open-source project turned out to be pretty simple — it runs fine on a consumer-grade GPU. The voice cloning results were better than I expected going in.\nSaturday morning, I went ahead and wired this feature together with my document-editing pipeline, so that a document with text and images gets automatically turned into a video. That means whenever I publish a new blog post, a short video gets generated for it automatically at the same time.\nThere\u0026rsquo;s obviously still some rough edges to polish, but I\u0026rsquo;m treating that as part of the fun.\n","date":"2025-05-10","externalUrl":null,"permalink":"/posts/diy-voice-cloning-project/","section":"Blog","summary":"An open-source voice cloning project on a consumer GPU, then wired into my publishing pipeline so blog posts turn into short videos automatically.","title":"Bored, So I Coded Up My Own Voice Clone","type":"posts"},{"content":"","date":"2025-05-09","externalUrl":null,"permalink":"/tags/ollama/","section":"Tags","summary":"","title":"Ollama","type":"tags"},{"content":"I deployed a self-hosted immersive translation system built on the Qwen3 4B small model locally, and ended up with an AI translation experience I can use without any usage caps.\nWhy Qwen3 4B? # Compared to the larger model variants, Qwen3 4B strikes a decent balance between reasoning ability and resource use. It runs smoothly on an ordinary consumer GPU or even a reasonably powerful VPS, which makes it a good fit for anyone who wants to self-host their own private translation service.\nInstalling Ollama and Open WebUI # I won\u0026rsquo;t go through the install process in detail here — pull the qwen3:4b model, then grab an API key from inside Open WebUI.\nConfiguring Immersive Translate # Point Immersive Translate at a custom API endpoint.\nSpeeding up translation: adding the keyword nothink to the translation prompt meaningfully cuts down the model\u0026rsquo;s \u0026ldquo;thinking\u0026rdquo; time and speeds up responses — especially noticeable with immersive, sentence-by-sentence translation. The keyword itself doesn\u0026rsquo;t change the semantics of the translation, it just nudges the model toward translating directly rather than reasoning its way there.\n","date":"2025-05-09","externalUrl":null,"permalink":"/posts/qwen3-4b-unlimited-local-translation/","section":"Blog","summary":"Pairing a small local Qwen3 4B model with Immersive Translate for a private, uncapped translation setup — plus a prompt trick that speeds it up.","title":"Running Unlimited AI Translation Locally with Qwen3 4B","type":"posts"},{"content":"When traveling in Europe, ChatGPT has become my most reliable personal tour guide. Walk into a museum or a church, snap a photo and send it over, and it can walk me through the history and stories behind what I\u0026rsquo;m looking at. Of all the large models out there, ChatGPT might not be the strongest at every single thing, but taken as a whole, it\u0026rsquo;s hard to find anything that really replaces it.\nChatGPT has a knack for landing on the most fun ways to use its own features. Today\u0026rsquo;s \u0026ldquo;turn a photo into a cartoon\u0026rdquo; feature is a great example: hand it a photo, say something as simple as \u0026ldquo;turn this into cartoon style,\u0026rdquo; and you get back a genuinely impressive result — one you can keep revising from there.\nIdea 1: Turning a photo into a cartoon # This is a photo from my mom\u0026rsquo;s birthday a few days ago. I asked ChatGPT to turn it into a cartoon. What came back was a detailed, warmly colored cartoon image that still felt true to the original.\nIdea 2: Editing details within a generated image # In this generated image, the clothes the kid was wearing got misread as a Mao suit.\nI told ChatGPT the boy in the middle was actually wearing a suit, and it immediately regenerated a more accurate version — the clothing details came out looking natural.\nIdea 3: Adding elements into a photo # This is a photo of us on a beach in Tanzania. I asked ChatGPT to add a US warship in the distance.\nIt generated a scene with a warship visible far off on the horizon, and the result looked convincingly real.\nHere\u0026rsquo;s a leopard I managed to photograph on the African savanna — a rare shot. I asked ChatGPT to put the leopard face to face with a dinosaur.\nThe leopard\u0026rsquo;s pose ended up looking a little too relaxed for the situation, but ChatGPT did faithfully complete the \u0026ldquo;add a dinosaur\u0026rdquo; part of the request.\nHere\u0026rsquo;s my son getting a close look at a giraffe.\nThis time it generated a dinosaur hiding in the grass.\nIdea 4: Generating a new setting from a portrait # I also sent ChatGPT a photo of myself cycling around the neighborhood and asked it to restyle me in a military look. Not only did the outfit change, but the whole bearing of the person in the photo came out noticeably more rugged.\nOlder image-generation models felt more like VR — whatever they generated was its own separate world. ChatGPT-4o\u0026rsquo;s new image feature feels more like AR: it takes a real photo and drops you into a world you created yourself.\nIdea 5: Generating a photo of two people together # Give it individual portrait photos of two people, and with the right prompt it can generate an image of them together.\nRight now ChatGPT can\u0026rsquo;t produce a genuine joint photo — it can only convert the result into a cartoon style.\nA few more photos # That winter, building a snowman and having a snowball fight downstairs. That year, hiking together. This year\u0026rsquo;s family portrait.\n","date":"2025-03-26","externalUrl":null,"permalink":"/posts/chatgpt-4o-photo-editing-fun/","section":"Blog","summary":"Turning family photos into cartoons, adding a warship and a dinosaur to travel pictures, and restyling a portrait — a weekend with ChatGPT-4o’s new image tools.","title":"Playing Around With ChatGPT-4o's New Photo Editing","type":"posts"},{"content":"","date":"2024-11-24","externalUrl":null,"permalink":"/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare","type":"tags"},{"content":"I recently reworked how my blog stores images, moving away from a Google-Drive-based system I\u0026rsquo;d built myself and switching to Chevereto. Google Drive offered a generous 2TB of space, but in practice it came with enough friction that I started looking for something better.\nWhy I gave up on Google Drive # The obvious upside of using Google Drive as an image store is the sheer amount of space, but the downsides were just as real:\nConstant re-authorization. Because of Google\u0026rsquo;s API limits, I had to re-authorize every 2–3 days — genuinely annoying. Long-term auth is a hassle. Getting a long-lived authorization means going through Google\u0026rsquo;s review process, which is slow and complicated, and it took a lot of the shine off the whole setup. I originally picked Google Drive for the storage headroom, but over time these issues piled up into a real annoyance, and I started looking for something more flexible and under my own control.\nChevereto: a more self-sufficient setup # After some research I settled on Chevereto as the new image gallery tool. It\u0026rsquo;s open source, capable, and — most importantly — fully under my own control. To make it work, I paired it with:\nCloudflare Tunnel — using Cloudflare\u0026rsquo;s free tunnel service, I can reach my NAS securely without exposing my home network. NAS storage — my home NAS became the storage backend, giving me flexible, controllable capacity and better data security. Between the two, I ended up with an efficient, stable personal image host. Chevereto itself was easy to install and configure, and it comes with a solid feature set — multi-user management, batch uploads, automatic image optimization, and more.\nWhat the new setup gets me # Compared to the Google Drive approach, running Chevereto on my own NAS has some clear advantages:\nFull independence — no third-party service to depend on, and no API limits to work around. Safer and more controllable — Cloudflare Tunnel hides my real IP, and the data itself stays on storage I control rather than with an outside provider. Stable long-term — no more constant re-authorization; the whole system is just more reliable. ","date":"2024-11-24","externalUrl":null,"permalink":"/posts/migrating-image-host-to-chevereto/","section":"Blog","summary":"Swapping a 2TB Google Drive image host that needed re-authorizing every few days for a self-hosted Chevereto setup behind Cloudflare Tunnel.","title":"Migrating My Image Host From a Self-Built Google Drive Setup to Chevereto","type":"posts"},{"content":"","date":"2024-11-24","externalUrl":null,"permalink":"/tags/nas/","section":"Tags","summary":"","title":"NAS","type":"tags"},{"content":"Today I finally subscribed to GitHub Copilot. At $10/month, I think it\u0026rsquo;s worth it — Copilot\u0026rsquo;s code assistance is strong enough to meaningfully speed up how I write code. As a developer, I\u0026rsquo;m writing code fairly often, and having intelligent suggestions and autocomplete while I type is a real help.\nI\u0026rsquo;m hoping Copilot saves me enough time on the mechanical parts of coding that I can put more of my energy into the more creative, higher-level parts of development. Carving out time to code every day has become a habit of mine, and now that I have Copilot in the mix, I expect that to get even more productive.\n","date":"2024-11-15","externalUrl":null,"permalink":"/posts/github-copilot-subscription/","section":"Blog","summary":"Ten dollars a month for autocomplete that actually understands what I’m writing.","title":"Finally Subscribed to GitHub Copilot","type":"posts"},{"content":"This morning I noticed ChatGPT had quietly rolled out the new o1-preview and o1-mini models. I hadn\u0026rsquo;t expected much, but the update showed up on my account anyway. Usage is heavily capped right now — 50 uses a month for o1-preview and 30 for o1-mini — so I\u0026rsquo;m holding off and waiting for an important, sufficiently complex task before actually putting it through its paces. This version brings a noticeably big jump in reasoning ability.\n","date":"2024-09-14","externalUrl":null,"permalink":"/posts/chatgpt-o1-preview-o1-mini-first-look/","section":"Blog","summary":"A quiet rollout, a tight usage cap, and a version that’s clearly a big step up on reasoning.","title":"First Impressions of ChatGPT's o1-preview and o1-mini","type":"posts"},{"content":" The setup, before it broke # My blog originally ran on a server provided through a university program, with a fairly limited bandwidth quota. To keep that quota from getting eaten up, I moved all my images off to an external host rather than serving them from the main server.\nTo solve the \u0026ldquo;where do I host images cheaply\u0026rdquo; problem, I went with a slightly unusual setup: Cloudflare in front, and Telegram\u0026rsquo;s Telegraph service as the actual image store behind it. Using the open-source Telegraph-Image project on GitHub, I could upload images straight to Telegram\u0026rsquo;s Telegraph service without much extra cost. For safety, I even contributed a fix upstream — adding a restriction on which domains were allowed to upload, to stop the mechanism being abused by randoms.\nOn top of that, I used Obsidian to automate image uploads and fast blog posting. The whole pipeline made my workflow genuinely smooth — I was using it to manage and publish almost every day.\nThen, on a Friday night, it all stopped # Every attempt to upload an image started throwing errors. Fairly stressful timing — I went straight to the relevant discussion groups looking for answers, and found plenty of other people hitting the exact same problem.\nThe official word: it\u0026rsquo;s over # After some digging around in Telegram-related groups, I found the root cause: Telegram had officially discontinued Telegraph\u0026rsquo;s support for image storage. That meant the image-hosting method I\u0026rsquo;d been relying on was, from that point on, history.\nThe timing made me think of the recent arrest of Telegram founder Pavel Durov — I have no idea whether the two are actually connected, and there\u0026rsquo;s no official statement linking them, but the timing was unsettling regardless.\nRebuilt, and back in business # Spent most of Saturday fixing this. Fortunately I found a replacement fairly quickly, rebuilt a free image host, and got the auto-posting pipeline working again.\nIf you\u0026rsquo;re reading this post, it means everything\u0026rsquo;s running again — the new image host is up, and the blog\u0026rsquo;s full automated pipeline is back to normal. A small win, coming out of an otherwise annoying outage.\n","date":"2024-09-07","externalUrl":null,"permalink":"/posts/telegram-image-hosting-shutdown/","section":"Blog","summary":"Telegram quietly killed Telegraph’s image-hosting support, breaking the free image pipeline my blog depended on — here’s how I patched it back together in a day.","title":"My Free Image Host Died Overnight — and How I Rebuilt It","type":"posts"},{"content":"","date":"2024-09-07","externalUrl":null,"permalink":"/tags/telegram/","section":"Tags","summary":"","title":"Telegram","type":"tags"},{"content":"","date":"2024-08-25","externalUrl":null,"permalink":"/tags/fastapi/","section":"Tags","summary":"","title":"FastAPI","type":"tags"},{"content":" Background # Right now my NAS handles audio-to-text conversion, but without a GPU, anything more than a short clip takes hours to process. So I exposed the Whisper instance running on a desktop with a 4060 as an API service instead, and pointed the NAS at it.\nWrapping it # Wrote a small FastAPI app that wraps the Whisper model behind an API endpoint capable of accepting an externally-uploaded audio file — loading the medium model and handling transcription requests through a /transcribe/ POST endpoint.\nSince it\u0026rsquo;s only reachable from inside the home network, I skipped token authentication for now.\n","date":"2024-08-25","externalUrl":null,"permalink":"/posts/whisper-fastapi-remote-service/","section":"Blog","summary":"Offloading transcription from a GPU-less NAS to a desktop with an actual GPU, exposed as a simple internal API.","title":"Wrapping Whisper in a FastAPI Service for Remote Transcription","type":"posts"},{"content":" Overview # Spent the weekend deploying the open-source RAGFlow on my home machine. Picked it partly because it supports automatic OCR out of the box; I\u0026rsquo;m also planning to try Quivr (marketed as a \u0026ldquo;second brain\u0026rdquo;) and compare the two once it\u0026rsquo;s installed.\nWhat is RAG? # Concept: RAG (Retrieval-Augmented Generation) combines information retrieval with a generative model to improve performance on NLP tasks — question answering and content generation in particular.\nThe pieces:\nLLM — a deep neural network trained on large-scale data (GPT-4, for example), capable of generating natural language and understanding it broadly. Retrieval — pulling information or documents relevant to the input query from an external knowledge base. This closes the gap in the LLM\u0026rsquo;s own knowledge, especially for factual questions. Generation — once relevant information has been retrieved, the model generates an answer grounded in it, combining the LLM\u0026rsquo;s generative strength with facts it wouldn\u0026rsquo;t otherwise reliably know. Workflow: the user submits a query → the system retrieves the most relevant passages/documents from a pre-built knowledge base → the LLM generates an answer based on what was retrieved → the answer is returned to the user.\nHands-on testing # Tried three scenarios:\nQ\u0026amp;A over a procurement contract found online. The answer about the three-day payment window was correct. The first two clauses checked out too, but it couldn\u0026rsquo;t find anything about the finance department\u0026rsquo;s ¥5,000 pre-authorization — not sure if that\u0026rsquo;s a hallucination or just not in the source document. Fed it a scanned PDF book and asked questions about it. Since I hadn\u0026rsquo;t read the book myself, I genuinely can\u0026rsquo;t verify whether the answers were correct. Constraining it with careful prompting does seem to help avoid hallucination, though. Tried it against an 18-year-old technical standard document. All the answers checked out. There wasn\u0026rsquo;t a direct answer available in the source for one of my questions, but a capable LLM should, in principle, still be able to generate something reasonable. Install process # 1. Environment prep\nsysctl vm.max_map_count If that value is low, bump it up:\n# In this case, we set it to 262144: sudo sysctl -w vm.max_map_count=262144 Setting vm.max_map_count higher lets a process create more memory mappings — important for workloads that need a lot of small mappings. Alternatively, edit /etc/sysctl.conf and add:\nvm.max_map_count=262144 2. Docker install\ngit clone https://github.com/infiniflow/ragflow.git cd ragflow/docker chmod +x ./entrypoint.sh docker compose up -d If you have an NVIDIA card, use docker-compose-gpu.yml instead:\ndocker compose -f docker-compose-gpu.yml up -d If you do have an NVIDIA card but hit an error during this step, it usually means the NVIDIA Container Toolkit isn\u0026rsquo;t installed:\nsudo apt-get update sudo apt-get install -y nvidia-docker2 Then restart Docker:\nsudo systemctl restart docker Once GPU support is working, the speed difference over CPU-only is dramatic.\n3. Pointing RAGFlow at local Ollama models\nUsing a local model instead of a paid API brings the running cost down a lot.\nOpen the firewall port: sudo ufw allow 11434/tcp Confirm Ollama is running: visiting http://localhost:11434 should show \u0026ldquo;Ollama is running.\u0026rdquo; Check which models are installed: ollama list. I picked llama3.1 and gemma2 for RAGFlow. In the RAGFlow UI: click the avatar in the top-right → Model providers → select Ollama. Set the model type to \u0026ldquo;chat\u0026rdquo; and add llama3.1:latest and gemma2:latest, with the URL set to http://localhost:11434/. Then just enable those models under chat settings. Afterthoughts # Plan to gradually feed in my journal entries, blog posts, and other personal records, building it into a kind of digital record of my own history. The generation step is fairly resource-hungry, so RAG doesn\u0026rsquo;t seem well-suited to high-concurrency use cases — but for documents that change often, it\u0026rsquo;s still the more practical option compared to constantly re-indexing something heavier.\n","date":"2024-08-25","externalUrl":null,"permalink":"/posts/ragflow-personal-rag-knowledge-base/","section":"Blog","summary":"Deploying the open-source RAGFlow project, testing it against real documents, and wiring it up to local Ollama models to keep costs at zero.","title":"A Weekend with RAGFlow: Building My Own RAG-Based Knowledge Base","type":"posts"},{"content":"","date":"2024-08-25","externalUrl":null,"permalink":"/tags/rag/","section":"Tags","summary":"","title":"RAG","type":"tags"},{"content":" Background # Today\u0026rsquo;s project was simple: take the script I\u0026rsquo;d just written and use it to automatically transcribe audio files from Obsidian\u0026rsquo;s Whisper folder into Markdown text. The code itself was a light adaptation of the sample Python script from Whisper\u0026rsquo;s own docs — nothing complicated. Installing Whisper on the NAS, though, was a different story: mostly a string of errors caused by running out of space under root and /tmp.\nEnvironment install issues # 1. Create a virtual environment\ncd /volume1 python3 -m venv whisper_env source whisper_env/bin/activate 2. Point pip\u0026rsquo;s cache/temp dirs elsewhere\nmkdir -p /volume1/tmp/pip_cache XDG_CACHE_HOME=/volume1/tmp/pip_cache TMPDIR=/volume1/tmp pip3 install openai-whisper Installing this way finally succeeded.\nRuntime issue # When I first tried actually running it, I hit an out-of-space error again — this time because the program needs to download the Whisper model itself, and the NAS didn\u0026rsquo;t have room. Fixed it by symlinking Whisper\u0026rsquo;s cache directory over to a volume with more space:\n# Make sure the target directory exists mkdir -p /volume1/whisper_cache # Remove the old cache directory, if present rm -rf /root/.cache/whisper # Symlink /root/.cache/whisper to /volume1/whisper_cache ln -s /volume1/whisper_cache /root/.cache/whisper After that, it ran fine — automatically transcribing voice notes from Obsidian.\n","date":"2024-08-10","externalUrl":null,"permalink":"/posts/whisper-on-nas-storage-fixes/","section":"Blog","summary":"A simple script to auto-transcribe Obsidian voice notes, derailed twice by the NAS running out of space in root and /tmp.","title":"Deploying Whisper on My NAS: Two Storage Gotchas","type":"posts"},{"content":"","date":"2024-08-10","externalUrl":null,"permalink":"/tags/synology/","section":"Tags","summary":"","title":"Synology","type":"tags"},{"content":" The problem # Podsync is a Docker service I genuinely like — listening to it during the morning commute has become a habit. Except it suddenly stopped being able to fetch the latest content.\nDigging in # After restarting it and searching around on GitHub, it turns out this isn\u0026rsquo;t an isolated issue — it\u0026rsquo;s Google blocking access on their end.\nFor now # Disabled Podsync for the time being, while I look at alternatives. Keeping an eye on Podsync\u0026rsquo;s updates in the meantime, in case the team resolves it and service comes back.\n","date":"2024-08-10","externalUrl":null,"permalink":"/posts/podsync-google-blocking-issue/","section":"Blog","summary":"A morning-commute podcast habit broken by an upstream block, not anything on my end.","title":"Podsync Stopped Fetching New Episodes — Turns Out It Was Google","type":"posts"},{"content":"Installed the Smart Connections plugin in Obsidian today. It\u0026rsquo;s an AI-powered plugin for Obsidian that lets you chat with your notes through embedding-based AI, and automatically surfaces related notes based on the current one. It supports both local models and well over 100 different API-based models — Claude, Gemini, ChatGPT, Llama 3, and more.\nFirst impressions # I\u0026rsquo;m running llama-3.1-8b-instant, which handles normal conversation fine. What\u0026rsquo;s genuinely useful is that it can summarize content pulled from my own past posts.\nPointing it at a self-hosted server # Configuring it against OpenAI\u0026rsquo;s own API directly is straightforward, but pointing it at my self-hosted One API server kept throwing errors, and the plugin\u0026rsquo;s own error messages weren\u0026rsquo;t very informative. After digging in, it turns out it needs the standard three-part endpoint configuration — once set up that way, it works fine.\nWhat it\u0026rsquo;s for # Smart Connections is built for individuals who want AI to strengthen how they manage notes and surface connections in their own knowledge base. It supports local models as well as API models like Anthropic\u0026rsquo;s Claude, Google\u0026rsquo;s Gemini, and OpenAI\u0026rsquo;s GPT-4. Through its Smart Chat feature you can have a conversation with your notes, and save that conversation as a note or a canvas.\n","date":"2024-08-08","externalUrl":null,"permalink":"/posts/obsidian-smart-connections-plugin/","section":"Blog","summary":"Chatting with my own notes via a local model, plus a gotcha when pointing the plugin at a self-hosted One API server.","title":"Trying Out Smart Connections: an AI Plugin for Obsidian","type":"posts"},{"content":"I\u0026rsquo;m genuinely happy with the little Obsidian-based writing setup I\u0026rsquo;ve built. Obsidian\u0026rsquo;s frictionless writing experience means I can jot things down anywhere — at work, at home, on the move — and then, whenever I have time, tidy the notes up and publish them by category through a background file-sync process.\nMulti-platform sync # Blog folder — drop an article in here, and a Python script running on my home NAS automatically syncs it to my blog. DayOne folder — drop it here, and it publishes automatically into the DayOne app, for private storage. Media folder — drop it here, and the Markdown file gets converted into a video, ready to publish across various platforms. The one imperfection # The thing that kept bugging me: once a Markdown file synced over to Typecho, the Markdown-aware editor would fill up with a wall of raw HTML — making it basically impossible to go back and edit the post on the site itself.\nThe fix # Today, while setting up a visitor-tracking plugin on the site, I stumbled onto a setting that controls whether content submitted via the XML-RPC interface keeps its Markdown formatting instead of being converted to HTML. Flipping that setting solved the problem completely — the content is much tidier now.\nTakeaway # Whether it\u0026rsquo;s private notes or public posts, Obsidian and this sync setup let me manage and publish content far more efficiently. With the \u0026ldquo;can\u0026rsquo;t cleanly re-edit synced posts\u0026rdquo; problem solved, I can finally let that particular obsession go.\n","date":"2024-08-04","externalUrl":null,"permalink":"/posts/obsidian-typecho-markdown-sync-fix/","section":"Blog","summary":"My Obsidian-based writing setup was publishing beautifully, except synced posts landed in Typecho full of raw HTML clutter — a hidden XML-RPC setting fixed it.","title":"Keeping Markdown Formatting Intact When Obsidian Syncs to Typecho","type":"posts"},{"content":"Noticed by accident that any time my blog hit a code block, it rendered as an unfriendly, big blank white box.\nTried tracking it down with Chrome DevTools, tweaked the nginx config, tweaked the stylesheet — none of it fixed it.\nWent through troubleshooting steps suggested by ChatGPT\u0026hellip; and eventually landed on checking Typecho\u0026rsquo;s plugins.\nTurned out disabling the ColorHighlight syntax-highlighting plugin fixed the display immediately.\n","date":"2024-08-03","externalUrl":null,"permalink":"/posts/typecho-code-block-blank-fix/","section":"Blog","summary":"A CSS/plugin conflict was rendering code blocks as an ugly blank space — turned out to be a syntax-highlighting plugin fighting the theme.","title":"Fixing a Blank White Box Where Code Blocks Should Be in Typecho","type":"posts"},{"content":" Background # Testing whether locally-hosted, open-source models are good enough to take a customer-service conversation — after it\u0026rsquo;s been converted from speech to text — and produce a usable summary. All of this ran on consumer-grade GPU hardware, since that\u0026rsquo;s what\u0026rsquo;s actually practical for a setup like this.\nModels tested # Ten models pulled via Ollama:\nModel Size llama3:70b 39 GB llama3:latest 4.7 GB deepseek-v2:latest 8.9 GB llama3-groq-tool-use:latest 4.7 GB wangshenzhi/gemma2-9b-chinese-chat:latest 5.8 GB glm4:9b 5.5 GB gemma2:latest 5.4 GB gemma2:27b 15 GB qwen2:72b 41 GB qwen2:7b 4.4 GB Evaluation approach # Each model was scored across three dimensions: how accurately it captured the customer\u0026rsquo;s underlying intent, how well it documented the steps the agent actually took, and how precisely it identified the core issue. Scores were gathered from a mix of perspectives — customer-service staff, ML practitioners, and end users — and averaged into a final result.\n","date":"2024-08-03","externalUrl":null,"permalink":"/posts/comparing-llms-for-customer-service-summaries/","section":"Blog","summary":"Running ten locally-hosted models on consumer GPU hardware to see which ones best summarize customer-service calls after speech-to-text.","title":"Comparing Open-Source LLMs for Customer-Service Conversation Summaries","type":"posts"},{"content":" The problem # Running Ollama\u0026rsquo;s official install script failed with curl not found. Following Ubuntu\u0026rsquo;s suggestion, I installed curl via snap. After that, re-running the Ollama installer failed again — this time with a \u0026ldquo;directory does not exist\u0026rdquo; error, and the install wouldn\u0026rsquo;t complete.\nThe fix # Don\u0026rsquo;t install curl via snap. Once snap\u0026rsquo;s version of curl is on the PATH, reinstall curl properly instead:\nUpdate the package list: sudo apt update Install curl via apt: sudo apt install curl This is the right way to do it — apt is Ubuntu\u0026rsquo;s default package manager. Verify the install: curl --version This should print the installed curl version. To make sure the /usr/bin version of curl takes priority, edit ~/.bashrc and reload it:\nEdit ~/.bashrc: nano ~/.bashrc Add this PATH setting at the end of the file: export PATH=/usr/bin:/usr/local/cuda-12.4/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin Save and exit. Reload it: source ~/.bashrc That resolved the curl-related install failure — from there, the official Ollama installer ran through cleanly.\n","date":"2024-08-03","externalUrl":null,"permalink":"/posts/ollama-install-curl-fix/","section":"Blog","summary":"Installing curl via snap put it ahead of /usr/bin on PATH, which broke the Ollama installer in a confusing way.","title":"Fixing an Ollama Install Failure Caused by a Snap-Installed curl","type":"posts"},{"content":"","date":"2024-08-03","externalUrl":null,"permalink":"/tags/ubuntu/","section":"Tags","summary":"","title":"Ubuntu","type":"tags"},{"content":"","date":"2024-08-03","externalUrl":null,"permalink":"/tags/cuda/","section":"Tags","summary":"","title":"CUDA","type":"tags"},{"content":"","date":"2024-08-03","externalUrl":null,"permalink":"/tags/nvidia/","section":"Tags","summary":"","title":"NVIDIA","type":"tags"},{"content":"After a lot of tinkering on the previous install, the GPU driver eventually crashed — nvidia-smi started erroring out, and Ollama ended up running entirely on the CPU. Reinstalling was the only real option. This time, the machine is dedicated purely to running LLMs — no more installing random open-source software just to try it out, to avoid another crash.\nInstall process # 1. Driver installation # Check the GPU model and download the matching driver\nlspci | grep -i nvidia Find the right driver on NVIDIA\u0026rsquo;s site and download it locally.\nImportant: make sure GCC is upgraded to version 12 first.\nAdd the Ubuntu Toolchain PPA (if not already added): sudo add-apt-repository ppa:ubuntu-toolchain-r/test sudo apt update Install GCC-12: sudo apt install gcc-12 g++-12 Use update-alternatives to manage GCC versions: sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 120 --slave /usr/bin/g++ g++ /usr/bin/g++-12 Set GCC-12 as the default: sudo update-alternatives --config gcc Pick the matching number at the interactive prompt. Verify: gcc --version This should report GCC 12. Install the driver itself\nsudo ./NVIDIA-Linux-x86_64-550.107.02.run -no-x-check -no-nouveau-check -no-opengl-files The exact prompts vary by driver version, but the main ones: you can skip 32-bit compatibility libraries, and say yes to automatic configuration.\nOnce installed, nvidia-smi runs and opens normally.\n2. CUDA installation # CUDA has to be installed for an NVIDIA card to actually show its benefit for LLM workloads. Open:\nhttps://developer.nvidia.com/cuda-12-4-1-download-archive?target_os=Linux\u0026target_arch=x86_64\u0026Distribution=Ubuntu\u0026target_version=22.04\u0026target_type=deb_local\nPick the options matching your system, and it\u0026rsquo;ll show you the install commands:\nwget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600 wget https://developer.download.nvidia.com/compute/cuda/12.4.1/local_installers/cuda-repo-ubuntu2204-12-4-local_12.4.1-550.54.15-1_amd64.deb sudo dpkg -i cuda-repo-ubuntu2204-12-4-local_12.4.1-550.54.15-1_amd64.deb sudo cp /var/cuda-repo-ubuntu2204-12-4-local/cuda-*-keyring.gpg /usr/share/keyrings/ sudo apt-get update sudo apt-get -y install cuda-toolkit-12-4 Just run these in order.\n3. Update environment variables # export PATH=/usr/local/cuda-12.4/bin${PATH:+:${PATH}} export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} 4. Install cuDNN # To get the most out of the NVIDIA hardware, cuDNN is also worth installing — the process is basically the same as CUDA. Open:\nhttps://developer.nvidia.com/cudnn-downloads?target_os=Linux\u0026target_arch=x86_64\u0026Distribution=Ubuntu\u0026target_version=22.04\u0026target_type=deb_local\nPick your system config, and it\u0026rsquo;ll show the install steps:\nwget https://developer.download.nvidia.com/compute/cudnn/9.2.1/local_installers/cudnn-local-repo-ubuntu2204-9.2.1_1.0-1_amd64.deb sudo dpkg -i cudnn-local-repo-ubuntu2204-9.2.1_1.0-1_amd64.deb sudo cp /var/cudnn-local-repo-ubuntu2204-9.2.1/cudnn-*-keyring.gpg /usr/share/keyrings/ sudo apt-get update sudo apt-get -y install cudnn Once that\u0026rsquo;s done, you can run a cuDNN sample to verify everything works:\ncd /usr/src/cudnn_samples_v9/mnistCUDNN sudo make clean \u0026amp;\u0026amp; sudo make ./mnistCUDNN If it compiles and runs, you should see a success message.\n","date":"2024-08-03","externalUrl":null,"permalink":"/posts/ubuntu-nvidia-cuda-cudnn-setup/","section":"Blog","summary":"After one too many driver crashes from installing random open-source software, I wiped the machine and set it up as a single-purpose LLM box.","title":"Reinstalling Ubuntu for a Dedicated LLM Box: NVIDIA Driver, CUDA, and cuDNN","type":"posts"},{"content":" 1. What is a probability density function? # Imagine playing darts against a long straight line instead of a circular board.\nA probability density function (PDF) is basically a \u0026ldquo;map\u0026rdquo; of how likely your dart is to land at each point along that line. Why \u0026ldquo;density\u0026rdquo;? Because it tells you how concentrated the landings are near each point — the higher the density, the more likely a dart lands there. If you plot this \u0026ldquo;map,\u0026rdquo; you get a wavy curve: the higher the curve at a point, the more likely a dart lands near it. Two key properties: the total area under the curve always equals 1 (100% probability), and the curve never dips below zero (probability can\u0026rsquo;t be negative). Real-world uses: modelling the distribution of exam scores, estimating a product\u0026rsquo;s lifespan, analysing an athlete\u0026rsquo;s performance. An everyday analogy: picking out a specific height in a crowd — the PDF tells you which height range you\u0026rsquo;re most likely to find a match in. The key thing to remember: a PDF doesn\u0026rsquo;t hand you a probability directly — it describes the shape of the distribution. To get an actual probability, you need the area under the curve over some range.\n2. Mean squared error, weighted by a probability density # A quick recap: a data distribution D describes the overall pattern in your data, while a probability density function p gives the precise mathematical description of it.\nWhat is MSE? Mean Squared Error measures the gap between predicted and actual values.\nIn plain terms: back to darts — after each throw, measure the distance from the dart to the bullseye, then square that distance. MSE is just the average of all those squared distances.\nFormally, based on D and p:\nMSE = E[(Y - f(X))²] where E is the expected value (the mean), Y is the actual value, f(X) is your prediction, and the expectation is taken with respect to the distribution D / density p.\nIn plain terms again: say you\u0026rsquo;re predicting exam scores. Y is a student\u0026rsquo;s real score, f(X) is your prediction, (Y - f(X))² is the squared error, and we average that squared error using D and p.\nWhy square it? Squaring stops positive and negative errors from cancelling out, and it penalises large errors more heavily.\nA tiny worked example: predicting three students\u0026rsquo; scores — actual: 80, 85, 90; predicted: 82, 83, 91.\n(82-80)² + (83-85)² + (91-90)² = 4 + 4 + 1 = 9 MSE = 9 ÷ 3 = 3 Extending the example: bringing probability density into it # Now imagine a full class of 100 students instead of just three, and assume their scores follow something like a normal (bell-curve) distribution. The density function p(x) tells you how likely each score is to occur.\nInstead of just averaging every squared error equally, we weight each one by how likely that score was to occur in the first place:\nMSE = ∫ (y - f(x))² · p(x) dx The intuition: scores near the peak of the distribution (say, most students clustering around 70–80) carry more weight, because they\u0026rsquo;re common — errors there matter more to the overall picture. Scores out in the tails (very high or very low, and rare) contribute less, even if the error on any single one of them happens to be large.\nA simple way to think about it: every score has an \u0026ldquo;importance weight\u0026rdquo; attached, and common scores pull more weight in the final average than rare ones — similar to how you\u0026rsquo;d weigh a restaurant\u0026rsquo;s frequently-ordered dishes more heavily than a rarely-ordered special when judging its overall quality. In practice, you rarely know the exact density function up front, but you can estimate it from enough data — and doing so makes your error metric better reflect how the model will actually perform in the real world, rather than being skewed by rare edge cases.\n","date":"2024-07-31","externalUrl":null,"permalink":"/posts/ai-study-notes-pdf-and-mse/","section":"Blog","summary":"Working through two ML fundamentals in plain language: what a probability density function actually tells you, and how weighting by probability density changes mean squared error.","title":"AI Study Notes: Probability Density Functions and Weighted MSE","type":"posts"},{"content":"","date":"2024-07-31","externalUrl":null,"permalink":"/tags/machine-learning/","section":"Tags","summary":"","title":"Machine Learning","type":"tags"},{"content":"","date":"2024-07-31","externalUrl":null,"permalink":"/tags/study-notes/","section":"Tags","summary":"","title":"Study Notes","type":"tags"},{"content":" What is an agent? # In the context of large language models, an \u0026ldquo;agent\u0026rdquo; is an LLM-based system that can autonomously carry out tasks, make decisions, and interact with its environment. It combines the reasoning power of an LLM with the autonomy of a traditional software agent. A few defining traits:\nAutonomy — an agent can understand a task, plan, and act on it without constant human intervention. Goal-directed — it\u0026rsquo;s given a specific objective and works toward it. Environment interaction — it can perceive its surroundings and adjust its behaviour as conditions change. Reasoning and decision-making — it uses the underlying LLM\u0026rsquo;s capabilities to reason through non-trivial decisions. Tool use — many agents are built to call out to external tools and APIs: search engines, databases, or other software. Continuous learning — some more advanced agents can learn and improve from experience. Multimodality — some agents can process and generate more than just text, including images and audio. Common agent frameworks # A quick comparison of a few frameworks I looked at, roughly by strengths and how steep the learning curve is: LangChain, AutoGPT, Hugging Face Transformers, NVIDIA NeMo, and Microsoft\u0026rsquo;s DeepSpeed Chat — each has a different sweet spot depending on whether you\u0026rsquo;re optimising for flexibility, ease of setup, or production performance.\nA worked example: telecom customer service # As a concrete example, I sketched out a customer-service agent for a telecom scenario: it takes speech input, converts it to text, looks up account information (like balance) in a database, generates a response, and converts that back to speech. One deliberate design choice worth calling out: if the model\u0026rsquo;s answer doesn\u0026rsquo;t explicitly mention the account balance, the system appends it automatically — partly for compliance and consistency, and partly so the answer is useful even if the model\u0026rsquo;s own response wanders off-topic.\n","date":"2024-07-30","externalUrl":null,"permalink":"/posts/llm-agents-basics/","section":"Blog","summary":"What an ‘agent’ actually means in the context of large language models, and a worked example in a customer-service setting.","title":"LLM Agents 101","type":"posts"},{"content":" 1. Download the driver and disable the built-in one # Check the GPU model: lspci | grep -i nvidia Download the driver from NVIDIA\u0026rsquo;s official site. Remove Ubuntu\u0026rsquo;s bundled driver: sudo apt purge nvidia* Blacklist the nouveau driver by editing /etc/modprobe.d/blacklist.conf. Update the system and reboot. 2. Install the graphics driver # Stop the lightdm desktop service. Make sure GCC is up to date (12+ is required). Run the driver installer with the appropriate flags. Verify with nvidia-smi. 3. Install CUDA # Followed a CSDN write-up for the exact steps. Installed CUDA 12.4 via apt. Added the CUDA environment variables to .bashrc. Verified the install with the usual CUDA version commands. ","date":"2024-07-28","externalUrl":null,"permalink":"/posts/installing-nvidia-driver-ubuntu/","section":"Blog","summary":"Notes on getting an NVIDIA driver and CUDA 12.4 installed cleanly, including disabling the nouveau driver first.","title":"Installing an NVIDIA GPU Driver on Ubuntu 22.04","type":"posts"},{"content":"Command:\nollama run gemma2:27b Error:\nError: llama runner process has terminated: signal: aborted (core dumped) CUDA error: CUBLAS_STATUS_NOT_INITIALIZED current device: 0, in function cublas_handle at /go/src/github.com/ollama/ollama/llm/llama.cpp/ggml-cuda/common.cuh:826 cublasCreate_v2(\u0026amp;cublas_handles[device]) GGML_ASSERT: /go/src/github.com/ollama/ollama/llm/llama.cpp/ggml-cuda.cu:100: !\u0026#34;CUDA error\u0026#34; Fix:\ncurl -fsSL https://ollama.com/install.sh | sh Updating Ollama resolved it.\n","date":"2024-07-27","externalUrl":null,"permalink":"/posts/ollama-gemma2-cuda-error/","section":"Blog","summary":"A one-line fix for a CUBLAS_STATUS_NOT_INITIALIZED crash: just update Ollama.","title":"Fixing a CUDA Error When Running Gemma2 in Ollama","type":"posts"},{"content":"","date":"2024-05-10","externalUrl":null,"permalink":"/tags/fine-tuning/","section":"Tags","summary":"","title":"Fine-Tuning","type":"tags"},{"content":" Backstory # I hit a wall trying to set up local fine-tuning. I\u0026rsquo;d bought an AMD card for the price-to-performance ratio, and it\u0026rsquo;s fine for running models that are already trained — but trying to set up a local fine-tuning environment is where things fell apart:\nAMD\u0026rsquo;s software ecosystem lags well behind NVIDIA\u0026rsquo;s. CUDA is genuinely painless to get running, practically foolproof. ROCm, by contrast, threw error after error during install, and tracking down fixes was a slog — made worse by the fact I was also on a fresh Ubuntu 24.04 install, so there wasn\u0026rsquo;t much prior art to lean on. After finally fighting my way through the install, I discovered AMD\u0026rsquo;s own site doesn\u0026rsquo;t even list ROCm support for my consumer-grade card. So I looked at NVIDIA card prices again — and they\u0026rsquo;re well outside my budget. After all that, I decided to fine-tune using cloud compute instead. Of the options out there, I went with Google Colab — mainly because it\u0026rsquo;s free, which settled every other consideration.\nGiving up on local deployment turned out to be a relief: cloud-based fine-tuning meant I could still produce my own \u0026ldquo;customized\u0026rdquo; model, entirely for free — as long as your data doesn\u0026rsquo;t involve anything private or sensitive.\nDeployment steps # 1. Open the unsloth project\nhttps://github.com/unslothai/unsloth\nPick Llama 3 for training, and it walks you straight into running the process on Google Colab.\n2. Pick a GPU type\nThe free tier\u0026rsquo;s T4 GPU is enough — 15GB of VRAM.\n3. Follow unsloth\u0026rsquo;s steps one by one\nAt this point you need to swap in your own training set.\nThe training data needs to be in a specific question-answer format — you can use ChatGPT or a Python script to convert your own question bank or text into this shape. Once you\u0026rsquo;ve generated the JSON file, upload it to https://huggingface.co and swap that link into the Colab notebook.\nThen just continue running the rest of the notebook.\nTesting the result # After training, I asked it \u0026ldquo;who are you?\u0026rdquo; — testing with a new model each time — and it could already handle the question in more than three languages.\nYou can see the fine-tuned model already handles domain-specific questions competently.\nThat\u0026rsquo;s a genuinely solid answer — I\u0026rsquo;d bet this model could pass a professional certification exam at this point.\n","date":"2024-05-10","externalUrl":null,"permalink":"/posts/free-gpu-finetuning-colab/","section":"Blog","summary":"After AMD’s ROCm ecosystem let me down for local fine-tuning, free Colab GPUs turned out to be the pragmatic way to fine-tune Llama 3 for free.","title":"Fine-Tuning Your Own Model with Free GPU Compute","type":"posts"},{"content":"Set up a personal knowledge base today, using the open-source tool MaxKB. To keep it reachable anywhere while still being reasonably secure, I went with a self-hosted deployment on my home NAS rather than a cloud service.\nInstall steps # Open Docker on the Synology NAS and search for the MaxKB project. Download and configure it. Two things need changing: I set the port to 4444, and created a working directory mapped to /var/lib/postgresql/data. Once that\u0026rsquo;s done, it\u0026rsquo;s reachable at http://\u0026lt;ip\u0026gt;:4444 — default username admin, default password MaxKB@123... Change the password immediately after logging in. Create a knowledge base — I made two: one pointed at my blog\u0026rsquo;s URL, which pulls in and indexes the blog\u0026rsquo;s content automatically, and one built from local documents. Configure the application: pick the knowledge base from the previous step, then set the AI model. I used the OpenAI-compatible Workers AI endpoint from an earlier post, and since most of my documents are in Chinese, I picked Alibaba\u0026rsquo;s Qwen model specifically. Everything else I left at its default. And here\u0026rsquo;s how it looks in actual use: ","date":"2024-05-10","externalUrl":null,"permalink":"/posts/maxkb-personal-knowledge-base/","section":"Blog","summary":"Self-hosting the open-source MaxKB knowledge-base tool at home, and pointing it at a Qwen-backed model for Chinese-language documents.","title":"Building a Personal Knowledge Base with MaxKB on My NAS","type":"posts"},{"content":"Got Llama 3 running locally on my consumer-grade GPU and set up Open WebUI as the interface for it. The install was easy — following the docs, a single Docker command was all it took, no real technical hurdles.\nOn the backend, I hooked it up to One API as an aggregation layer, with Cloudflare and Groq configured as upstream providers. Combined with the locally-hosted Llama 3 8B and 70B models, that gives me one unified API surface spanning both local and remote inference.\n","date":"2024-05-10","externalUrl":null,"permalink":"/posts/local-llama3-open-webui/","section":"Blog","summary":"Pairing a consumer GPU running local Llama 3 with Open WebUI and One API to combine local and remote models behind one interface.","title":"Running Llama 3 Locally with Open WebUI","type":"posts"},{"content":"Finished wiring up Whisper and Obsidian today, so voice recordings dropped into a note now get automatically transcribed to text — fully hands-off.\nWhat this involved # Installing the Whisper plugin in Obsidian. Pointing it at the Cloudflare-backed, OpenAI-compatible API I set up in the previous post. ","date":"2024-05-01","externalUrl":null,"permalink":"/posts/whisper-obsidian-voice-notes/","section":"Blog","summary":"Wiring up Obsidian’s Whisper plugin to the free Cloudflare-backed OpenAI API from the previous post.","title":"Whisper + Obsidian: Fully Automatic Voice-to-Text Notes","type":"posts"},{"content":"Plenty of large language models are usable for free these days, but a reasonably stable API service still usually costs money. Cloudflare — famously generous when it comes to free tiers — offers a decent free allowance here too. Its \u0026ldquo;Beta\u0026rdquo; model tier is free, and remarkably that even includes a large model like Alibaba\u0026rsquo;s Qwen.\nSo here\u0026rsquo;s how to wrap Cloudflare\u0026rsquo;s AI service behind an OpenAI-compatible API, so a front-end can start using it without any code changes at all.\nWhy this approach? # As the field moves on, smaller and cheaper language models look increasingly competitive against OpenAI\u0026rsquo;s GPT-3.5/GPT-4 APIs. A lot of developers understandably don\u0026rsquo;t want to rewrite their entire codebase just to try a new model. Cloudflare Workers is also a great place to host both the AI service and the API layer, so I built an OpenAI-compatible API on top of it — letting developers swap in new LLMs without touching their existing code.\nCompatibility and implemented APIs # Implemented, or planned:\nCompletions Chat Completions Audio Transcription Embeddings Audio Translation Image Generation File handling, with Assistants support Storing assistants, threads, and messages in a D1 database Transcription runs on Whisper, language identification on Llama 2, and translation on m2m-100.\nDeployment steps # I deployed this from a MacBook; the steps are the same on Ubuntu. Note that this deployment flow needs a browser at one point, so SSH-ing into a remote box won\u0026rsquo;t work without some adjustment.\n1. Clone the repo\ngit clone https://github.com/chand1012/openai-cf-workers-ai cd openai-cf-workers-ai 2. Edit the config\nOpen wrangler.toml and set CLOUDFLARE_ACCOUNT_ID to your own Cloudflare account ID:\nCLOUDFLARE_ACCOUNT_ID = \u0026#34;your-account-id-here\u0026#34; # replace with your own You can find this ID on the Cloudflare dashboard.\n3. Install dependencies and deploy\nyarn — I didn\u0026rsquo;t have yarn installed, so this failed the first time. Install it with: brew install yarn If that complains about a missing Node.js:\nbrew install node Once installed, the earlier command runs fine.\nyarn init-prod — run this once. It\u0026rsquo;ll prompt for authorization in a browser window; just click Allow. If authorization fails, you\u0026rsquo;ll need to grant access in Cloudflare first: Open the Cloudflare dashboard. Go to the R2 Storage section (in the left-hand menu, or search for \u0026ldquo;R2\u0026rdquo;). Enable R2 if it isn\u0026rsquo;t already, following the prompts. Re-run yarn init-prod. Once authorized, run yarn deploy and wait for the app to finish deploying. 4. Set the access tokens\nOption 1: use wrangler to set ACCESS_TOKEN and CLOUDFLARE_API_TOKEN directly. You can create a new API token from the Cloudflare dashboard. Option 2 (recommended): open the Workers \u0026amp; Pages section in the Cloudflare dashboard, select the project you just created, go to Settings, and add ACCESS_TOKEN and CLOUDFLARE_API_TOKEN there. ACCESS_TOKEN is the credential your own clients will use to call this API; CLOUDFLARE_API_TOKEN is Cloudflare\u0026rsquo;s own Workers AI token — you can create one at https://dash.cloudflare.com/profile/api-tokens. On that page, create a new token and pick the \u0026ldquo;Workers AI\u0026rdquo; template.\nOnce created, the token is shown once — copy it down, paste it into the variable, and redeploy. Done.\nUsage # See the OpenAI API docs for the general shape of requests. A couple of examples:\ncurl https://openai-cf.yourusername.workers.dev/v1/chat/completions \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Authorization: Bearer \u0026lt;any string value you set\u0026gt;\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;@cf/meta/llama-2-7b-chat-int8\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;You are a helpful assistant.\u0026#34; }, { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Hello!\u0026#34; } ] }\u0026#39; curl https://openai-cf.\u0026lt;your-project-domain\u0026gt;.workers.dev/v1/chat/completions \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -H \u0026#34;Authorization: Bearer \u0026lt;your access token\u0026gt;\u0026#34; \\ -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;@cf/qwen/qwen1.5-7b-chat-awq\u0026#34;, \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;You are a helpful assistant.\u0026#34; }, { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Who are you?\u0026#34; } ] }\u0026#39; Caveats # Token usage isn\u0026rsquo;t tracked yet — it always reports zero. Stop sequences aren\u0026rsquo;t supported yet. ","date":"2024-05-01","externalUrl":null,"permalink":"/posts/free-qwen-openai-compatible-api/","section":"Blog","summary":"Wrapping Cloudflare’s free Workers AI models (including Qwen) behind an OpenAI-compatible endpoint, so existing front-ends don’t need to change a line of code.","title":"Building a Free, OpenAI-Compatible API on Top of Cloudflare Workers AI","type":"posts"},{"content":"While auto-syncing from Obsidian to Typecho in the background, I ran into a snag: XML-RPC doesn\u0026rsquo;t support uploading custom fields, so homepage thumbnail images weren\u0026rsquo;t showing up — which, with my particular brand of OCD, was hard to leave alone. I considered patching Typecho\u0026rsquo;s XML-RPC support properly, but in the end went with the lazier, simpler option: modify the theme directly so it automatically uses the first image in a post\u0026rsquo;s body as its homepage thumbnail.\nThe trade-off is that with more homepage images, page load got a bit slower. It\u0026rsquo;s a small site just for me, though, so I\u0026rsquo;m not chasing peak performance — as long as it looks right to me, that\u0026rsquo;s good enough.\nHere\u0026rsquo;s how I patched the Jasmine theme to show that thumbnail next to the post title on the homepage:\n1. Add a helper function to functions.php that grabs the first image out of the post content — this can go anywhere in the file:\n// Get the first image from the post content function getFirstImageFromContent($content) { $output = preg_match_all(\u0026#39;/\u0026lt;img.+src=\u0026#34;([^\u0026#34;]+)\u0026#34;/i\u0026#39;, $content, $matches); if ($output \u0026amp;\u0026amp; !empty($matches[1])) { return $matches[1][0]; } return false; } 2. In the theme\u0026rsquo;s component directory, open post-item-default.php and find:\n\u0026lt;?php if ($thumbnail = getThumbnail($this-\u0026gt;cid, \u0026#34;\u0026#34;)): ?\u0026gt; Replace it with:\n\u0026lt;?php if ($thumbnail = getFirstImageFromContent($this-\u0026gt;content, \u0026#34;\u0026#34;)): ?\u0026gt; Save and refresh.\n","date":"2024-04-28","externalUrl":null,"permalink":"/posts/typecho-jasmine-theme-thumbnails/","section":"Blog","summary":"Obsidian’s sync to Typecho doesn’t support custom fields over XML-RPC, so I patched the theme instead.","title":"Patching the Jasmine Theme to Show a Post's First Image as Its Thumbnail","type":"posts"},{"content":"Two small optimisations to the blog today: adding a caching plugin to speed up page loads, and wiring up visitor analytics.\nEnabling the TpCache plugin # Go to the TpCache GitHub repo. Download the plugin: https://github.com/phpgao/TpCache/archive/master.zip Drop it into the site\u0026rsquo;s /usr/plugins/ directory. Unzip it and rename the folder to TpCache. In the site admin panel, go to Plugins, find TpCache, and enable it. If Memcache/Redis PHP extensions aren\u0026rsquo;t installed, it\u0026rsquo;ll fall back to a MySQL-backed driver. Once it\u0026rsquo;s configured, force-refresh the homepage to see the effect. If you don\u0026rsquo;t want to bother setting up Redis, that\u0026rsquo;s as far as you need to go.\nAdding Google Analytics # Open Google Analytics and create a new account in settings. Once created, go into the tracking-code configuration and add it manually. Copy the tracking snippet, then find the header.php file in the current Typecho theme. Paste the snippet in anywhere sensible — you can verify it\u0026rsquo;s working from the Google Analytics dashboard. ","date":"2024-04-28","externalUrl":null,"permalink":"/posts/website-caching-and-analytics/","section":"Blog","summary":"Adding a caching plugin for faster page loads, plus wiring up Google Analytics.","title":"Two Quick Wins for This Blog: Caching and Visitor Analytics","type":"posts"},{"content":"","date":"2024-04-27","externalUrl":null,"permalink":"/tags/cloud/","section":"Tags","summary":"","title":"Cloud","type":"tags"},{"content":"This comparison table of large-model offerings turned out to be fairly comprehensive.\n","date":"2024-04-27","externalUrl":null,"permalink":"/posts/cloud-llm-landscape-2024/","section":"Blog","summary":"A quick note on a comparison table of large-model offerings across major cloud providers.","title":"International and Chinese Cloud Providers' LLM Landscape","type":"posts"},{"content":"Large language models have always felt a bit abstract to me, so to get a more concrete sense of what they can actually do, I recently deployed GPT-3.5 alongside Alibaba\u0026rsquo;s open-source Qwen-7B locally, and tried applying both to real scenarios from my day job.\nA few test scenarios # 1. Un-tuned open models sitting exams from different professional fields\nI picked a few exams from our internal training platform that don\u0026rsquo;t require any prep beforehand, and used a Chrome AI extension I built myself to have both GPT-3.5 and the open-source Qwen model take them — with no fine-tuning at all. A few things stood out from the results:\nThe more specialised the networking knowledge, the better the models scored. On national or industry-standard rules and procedures (procurement guidelines, for example), they also scored solidly above average. On exams about our own internal, highly customised products, the models fell well short — those questions are simply too specific. Takeaway: an off-the-shelf open model performs roughly like a competent new graduate (arguably a bit better). Some targeted \u0026ldquo;training\u0026rdquo; (i.e. fine-tuning) is enough to make it genuinely useful for certain roles.\n2. The \u0026ldquo;overconfident nonsense\u0026rdquo; problem\nCareful prompt engineering can mostly stop a model from improvising and keep it answering only from what it actually knows.\n3. How hard is it to stand up a local model?\nThe bar is low. Open models like Qwen-7B can be deployed with a single command. There\u0026rsquo;s no real technical difficulty in the deployment itself — what actually needs work is efficiency: GPU driver loading, system-level tuning, that kind of thing.\nSome thoughts # A rough three-layer way to think about LLM architecture:\nFoundation layer — both domestic and overseas open models are already good enough for direct use in a lot of professional contexts. What\u0026rsquo;s needed here isn\u0026rsquo;t teaching a model from scratch, it\u0026rsquo;s the equivalent of upskilling a new graduate. This layer should lean toward optimisation over construction: Efficiency: responding quickly to front-end requests and squeezing every bit of GPU capacity out of the hardware. Fine-tuning: loading tuning data from different formats into whichever base model sits underneath. Agent layer — the underlying model capability is open to everyone (a level playing field), and the front-end requirements are broadly the same across companies. The real differentiation in AI products comes from how well you build the \u0026ldquo;requirement ↔ model\u0026rdquo; middleman. Front-end UI — presenting the AI capability to the user. This is a comparatively conventional design problem. Where this could actually be used # 1. Browser-extension models rolled out department by department\nUsing a browser AI extension to vertically uplift existing systems without touching them:\nGuided walkthroughs at the point of service, with root-cause explanations when something errors out. Assisted troubleshooting for customer-service staff answering real customer questions. Surfacing the exact clause/source behind a procurement rule during the purchasing process. Giving analysts a \u0026ldquo;fine-tuned\u0026rdquo; explanation of what a given reporting metric actually means. 2. Building a shared knowledge base across different business lines\nUsing an LLM to build out a knowledge base sidesteps two common pain points.\n","date":"2024-04-23","externalUrl":null,"permalink":"/posts/local-llm-deployment-experience/","section":"Blog","summary":"Some hands-on experiments with un-tuned open models, and a few thoughts on where the real value in LLM products actually sits.","title":"Trying Out Local LLM Deployment: GPT-3.5 vs. Qwen-7B","type":"posts"},{"content":"","date":"2024-04-14","externalUrl":null,"permalink":"/tags/browser-extensions/","section":"Tags","summary":"","title":"Browser Extensions","type":"tags"},{"content":"Elmo is seriously good. This AI browser extension caught me off guard.\nWhether you\u0026rsquo;re on a Chinese site or an English one, it can summarize the page\u0026rsquo;s content almost instantly, which is a real efficiency boost — knowing the gist of an article up front helps you decide whether it\u0026rsquo;s worth reading in full.\nIt\u0026rsquo;s also surprisingly useful for language learning: when you\u0026rsquo;re just starting out, getting the gist first makes it much easier to get into the actual article afterwards.\n","date":"2024-04-14","externalUrl":null,"permalink":"/posts/elmo-ai-browser-extension/","section":"Blog","summary":"Elmo can summarize any page on the fly — and it turns out that’s genuinely useful for reading in a second language.","title":"This AI Browser Extension Blew Me Away","type":"posts"},{"content":"","date":"2024-04-14","externalUrl":null,"permalink":"/tags/immich/","section":"Tags","summary":"","title":"Immich","type":"tags"},{"content":"I came across an article comparing photo-management tools, and what caught my attention was how well Immich supported AI-powered features. Since it\u0026rsquo;s open-source and plugin-friendly, it\u0026rsquo;s relatively easy to pair with the AI capabilities available today. I finally got it deployed on my NAS over the weekend — but when I tried importing my back catalogue of photos from an external drive, every Chinese-language guide I found online was out of date and the import kept failing. I eventually found the answer in the official docs. By the time it started working, my NAS\u0026rsquo;s CPU was already pinned at 99% — it looked like the import wouldn\u0026rsquo;t finish until the following evening.\nHere\u0026rsquo;s the process for a bulk import using Immich\u0026rsquo;s CLI:\n1) SSH into the NAS and install the Immich CLI\nsudo npm install -g @immich/cli If that fails, it usually means the NAS\u0026rsquo;s bundled Node.js version is too old. You\u0026rsquo;ll need to install Node.js 20 from Synology\u0026rsquo;s Package Center — keep the existing 18.x install around too, or you\u0026rsquo;ll break Synology Drive, Synology Photos and similar apps that depend on it. Once Node is updated, re-running the install over SSH goes through cleanly.\n2) Grab an Immich API token\nOn the Immich web UI (http://\u0026lt;nas-ip\u0026gt;:2283), click your avatar in the top right → Account Settings, then New API Key. Copy the token that\u0026rsquo;s shown — it\u0026rsquo;s only displayed once.\n3) Start the bulk import\nLog in from the NAS\u0026rsquo;s SSH session:\nimmich login-key http://\u0026lt;nas-ip\u0026gt;:2283/api \u0026lt;the token from step 2\u0026gt; If everything checks out you\u0026rsquo;ll see Wrote auth info to /root/.config/immich/auth.yml, meaning you\u0026rsquo;re authenticated and the credentials file has been written.\nThen kick off the import itself:\nimmich upload --recursive /volume1/\u0026lt;your-photos-folder\u0026gt; The NAS\u0026rsquo;s CPU immediately jumps to 99% and photos start uploading in bulk:\nCrawling for assets... Checking files | ████████████████████████████████████████ | 100% | ETA: 0s | 237844/237844 assets Found 237819 new files and 25 duplicates Uploading assets | █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ | 1% | ETA: 07h30m | 7.9 GB/463.6 GB 237,844 assets and 463.6 GB in total — this is going to take a while.\n","date":"2024-04-14","externalUrl":null,"permalink":"/posts/immich-nas-batch-photo-import/","section":"Blog","summary":"Getting Immich running on a home NAS and bulk-uploading years of photos with the CLI.","title":"Self-Hosting Immich on My Synology NAS: Batch-Importing My Photo Library","type":"posts"},{"content":"Google announced today that Google One\u0026rsquo;s VPN feature will be shut down over the next few months. According to Google, it\u0026rsquo;s \u0026ldquo;sunsetting the VPN feature because we\u0026rsquo;ve found that people just weren\u0026rsquo;t using it much,\u0026rdquo; and retiring it will let the team \u0026ldquo;refocus\u0026rdquo; and \u0026ldquo;support more of the popular features people use Google One for.\u0026rdquo; The free Pixel VPN that ships with Pixel phones is unaffected — Google committed to five years of availability for it at launch.\nThe feature was originally marketed as adding \u0026ldquo;an extra layer of online protection\u0026rdquo; for Android phones and giving users \u0026ldquo;peace of mind about their data security.\u0026rdquo; Google had even published a whitepaper explaining how it worked, had the system independently audited by a third party, and open-sourced the client API.\n","date":"2024-04-12","externalUrl":null,"permalink":"/posts/google-one-vpn-shutdown/","section":"Blog","summary":"Google says usage was too low to justify keeping the feature around.","title":"Google One Is Shutting Down Its VPN Service","type":"posts"},{"content":"","date":"2024-04-12","externalUrl":null,"permalink":"/tags/news/","section":"Tags","summary":"","title":"News","type":"tags"},{"content":"I\u0026rsquo;m Chengyu, a final-year Computer Science student at the University of Sydney. I like building things end-to-end — self-hosted infrastructure on my home NAS, small AI-powered tools, the occasional Cloudflare Workers project — and writing down what I learn along the way. Most of the posts on this site started as personal projects: deploying open-source software, wiring AI models into real workflows, and occasionally breaking (and fixing) my own servers.\nOutside of that, I like hiking, travelling, gaming, and keeping up with new gadgets — and honestly, a good chunk of my \u0026ldquo;spare time\u0026rdquo; still goes into AI-assisted programming and side projects anyway.\n(Résumé, GitHub, LinkedIn, and contact links go here.)\n","externalUrl":null,"permalink":"/about/","section":"About","summary":"I’m Chengyu, a final-year Computer Science student at the University of Sydney. I like building things end-to-end — self-hosted infrastructure on my home NAS, small AI-powered tools, the occasional Cloudflare Workers project — and writing down what I learn along the way. Most of the posts on this site started as personal projects: deploying open-source software, wiring AI models into real workflows, and occasionally breaking (and fixing) my own servers.\n","title":"About","type":"about"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]